调研(08-11-internal-oa-usage-census-and-simplification): 固化采用度普查与减法决策
这个提交包含在:
+57
@@ -0,0 +1,57 @@
|
||||
# 生产使用度模块 allowlist
|
||||
|
||||
## 口径
|
||||
|
||||
- `主/辅表` 只用于模块级聚合,不返回原始行。`totalRows` 是所列主/辅表的记录合计,不代表独立业务单据数。
|
||||
- `activityAt` 优先使用 `updated_at`,不可变记录使用 `created_at`;`created30d/90d` 始终使用 `created_at`。
|
||||
- `actor` 只用于事务内分桶,输出只有 distinct count 和 `human/smoke/system/unknown` 计数。
|
||||
- `system` 表示由同步、投递、OCR、外部留存或 outbox 产生,不等于真人在 OA 中直接操作。
|
||||
- 审计 action 只做独立交叉验证,不能与表记录相加后解释为“操作次数”。
|
||||
|
||||
## 模块映射
|
||||
|
||||
| moduleKey | 产品含义 | 主/辅表与 actor | 审计 action 映射 | 导航/flag | 07-30 基线 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `portal_notifications` | 工作台通知 | `notifications(recipient_employee_id, system)` | 不以通知投递量证明真人使用 | `portal(main)` / `portal_home_v1` | 保留并简化 |
|
||||
| `workflow_approvals` | 待我处理、统一审批 | `workflow_instances(submitter_id)` | `APPROVAL_*`、`DELEGATION_*`、`WORKFLOW_*` | `workflowTasks(admin)`;流程定义另受 flag | 保留并优先验证 |
|
||||
| `reimbursements_invoices` | 报销、发票、OCR | `reimbursement_forms(owner_id)`、`invoice_pool(uploaded_by_employee_id)`、`ocr_results(system)` | `REIMBURSEMENT_*`、`INVOICE_*`、报销导出 | `reimbursements(main)`、`invoicePool(admin)` | 保留并优先验证 |
|
||||
| `payments_ap` | 付款、AP、银行回单 | `payment_batches(created_by)`、`payment_batch_receipt_attachments(uploaded_by)` | `PAYMENT_*`、`EXPORT_AP_OPERATIONS` | `paymentBatches/apOperations(admin)` / `payment_batch_v1` | 保留并优先验证 |
|
||||
| `procurement` | 采购 | `purchase_requests(applicant_id)` | `PURCHASE_*` | `purchaseRequests(main)` | 保留但按需使用 |
|
||||
| `contracts` | 合同台账 | `contract_registry(owner_employee_id)` | `CONTRACT_*`、`EXPORT_CONTRACTS_REGISTRY` | `contractsRegistry(main)` / `project_linking_v1` | 保留但简化 |
|
||||
| `projects` | 项目轻管理 | `projects(owner_employee_id)` | `PROJECT_*` | `projects(main)` / `project_domain_v1` | 保留但简化 |
|
||||
| `assets_inventory` | 资产与盘点 | `asset_cards(created_by)`、`inventory_checks(created_by)` | `ASSET_*`、`INVENTORY_*` | `assets(main)`、`inventoryChecks(admin)` | 资产保留;盘点默认隐藏 |
|
||||
| `leave_wecom_history` | OA 请假与企微审批留存 | `leave_requests(owner_id)`、`wecom_approval_archives(system)` | 留存主要由 workflow/企微表证明 | `leaveRequests(main)` | 企微主办、OA 后台留存 |
|
||||
| `attendance_presence` | 考勤修正、打卡留存、团队状态 | `attendance_corrections(owner_id)`、`wecom_checkin_records(system)`、`employee_presence(updated_by)` | `ATTENDANCE_*`、`PRESENCE_*` | `attendance(hidden)` / unified;`team(main)` / presence | 企微主办;团队状态默认隐藏冻结 |
|
||||
| `weekly_reports` | 周报 | `weekly_reports(employee_id)` | 无稳定专用 action | `weeklyReports(admin)` / `weekly_report_v1` | 默认隐藏、冻结开发 |
|
||||
| `todos` | 普通待办板 | `todo_items(created_by)` | 无稳定专用 action | `todos(hidden)` / `todo_board_v1` | 继续隐藏、退役前调研 |
|
||||
| `announcements` | 公告与制度签收 | `portal_announcements(created_by)`、签收记录以公告所属模块计数 | `PORTAL_ANNOUNCEMENT_*` | `announcements(admin)` / `announcements_v1` | 默认隐藏,必要时使用 |
|
||||
| `budgets` | 预算池与规则 | `budget_pools(created_by)` | `BUDGET_*` | `budgets(admin)` | 冻结开发 |
|
||||
| `travel` | 差旅申请、统付与对账 | `travel_requests(applicant_employee_id)`、`travel_orders(created_by)` | `TRAVEL_*` | `travelRequests(admin)` | 冻结开发 |
|
||||
| `people_lifecycle` | 人员生命周期、员工合同与培训 | `employee_lifecycle_events(created_by)`、`employee_contracts(created_by)`、`employee_trainings(created_by)` | `EMPLOYEE_*`、`SYSTEM_USER*`、`DEPARTMENT_*` | `people/organization(admin)` / lifecycle | 保留最小版 |
|
||||
| `payroll` | 薪资与工资条 | `payslips(created_by)` | salary/payslip 生成、更新、导出 actions | 无正式导航 | 默认隐藏、退役前调研 |
|
||||
| `performance` | 绩效 | `performance_reviews(reviewer_employee_id)` | `PERFORMANCE_*` | 无正式导航 | 默认隐藏、退役前调研 |
|
||||
| `system_configuration` | workspace、feature flags、cutover | `feature_flags(updated_by)`、`cutover_scopes(updated_by)` | `FEATURE_FLAG_*`、`WORKSPACE_*` | 管理入口;cutover flag | 基础配置保留;cutover 退役前调研 |
|
||||
| `integrations` | ERP/outbox 等集成运行 | `integration_outbox(system)` | `ERP_*` | 无日常入口 / `erp_sync` | ERP 已明确不接,退役前调研 |
|
||||
|
||||
## smoke 判据
|
||||
|
||||
事务内对 actor 的 `employee_no`、邮箱本地部分和名称复用现有 helper 语义:
|
||||
|
||||
- 邮箱本地部分以 `smoke-`、`smoke_`、`smoke.`、`oa-smoke`、`oa_smoke` 或 `oasmoke` 开头;最后一种是本次快照使用的保守扩展,只可能将极端命名的真人低估为 smoke,不会把 smoke 高估为真人。
|
||||
- 员工编号/标识以 `SMOKE`、`OSMOKE`、`WFULL`、`PRELAUNCH`、`LAUNCHAUTO`、`HRWDTEST` 开头;
|
||||
- 名称或标识含“测试”或“试运行”。
|
||||
|
||||
匹配结果只输出聚合计数,不输出命中的账号或个人字段。
|
||||
|
||||
## system action / table 口径
|
||||
|
||||
- 强制 `system` 表:`notifications`、`ocr_results`、`wecom_approval_archives`、`wecom_checkin_records`、`integration_outbox`。
|
||||
- 其余记录有 actor 时按 human/smoke 分类;actor 为空或已无法关联时为 unknown。
|
||||
- 审计 actor 无法关联员工时为 unknown;审计 action 未映射到上述模块时不进入模块 activity,但认证成功单独输出全局真人/smoke actor 聚合。
|
||||
|
||||
## 明确排除
|
||||
|
||||
- `auth_sessions`、password/email challenge、password hash、token version;
|
||||
- 附件正文、存储路径、合同/发票/银行/薪资金额和业务编号;
|
||||
- `audit_logs.extra`、IP、user agent、原始企业微信 payload;
|
||||
- migration、pgboss 内部表和动态 `information_schema` 扫描。
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
# 内部 OA 生产使用度结论与减法路线图
|
||||
|
||||
## 1. 执行边界
|
||||
|
||||
- 生产身份、运行 SHA、migration 和回滚窗口 PRE 均通过;运行候选为 `origin/main`,其后本地变更不含 `client/src`、`server/src` 或 Compose 产品差异。
|
||||
- 既有部署 wrapper 曾在 provenance 阶段因本地文档 HEAD 领先运行候选而 fail-closed;当时只完成 identity/13 项聚合体检,未进入 migration 或普查。随后用 task-scoped PRE 精确绑定已部署 `origin/main`,未放宽部署工具。
|
||||
- 唯一有效普查在一个 `READ ONLY` 事务内完成,20/20 模块 `queryStatus=PASS`,`effectiveQueryExecutions=1`、`zeroRetry=true`。
|
||||
- 第一次 `--execute` 在 stdin 入口条件处被拦截,`run()` 未调用、普查事务未开始;它记为 `preQueryInterceptions=1`,不计有效查询。
|
||||
- 本轮没有发布、登录、feature flag/导航修改、schema/data 写入或测试业务记录。
|
||||
- 完整脱敏计数见 `production-usage-snapshot.json`;本文只解释聚合,不包含个人、金额、正文或原始 payload。
|
||||
- 本次执行的邮箱 smoke 判据对无分隔符 `oasmoke…` 做了保守扩展;因此真人计数是下界。它不影响零记录模块,也不影响财务闭环已有真人证据的成立;未来复跑前可收紧为 helper 的精确分隔符规则。
|
||||
|
||||
## 2. 一句话结论
|
||||
|
||||
当前 OA **整体采用度极低,尚不能称为 5–10 人团队已经在稳定使用的办公系统**。问题不是继续补功能,而是管理层必须先决定:要求团队把明确流程真正迁入 OA,还是把 OA 收缩为低频台账和治理兜底;在该定位明确前停止产品功能开发。
|
||||
|
||||
| 核心指标 | 生产只读证据 |
|
||||
| --- | ---: |
|
||||
| 20 个模块聚合总行数 | 70(其中系统配置 26) |
|
||||
| 零行模块 | 13 / 20 |
|
||||
| 近 90 天真人归属/创建记录证据 | 13 |
|
||||
| 近 90 天认证成功审计中的真人 actor | 1 |
|
||||
| 近 30 天认证成功审计中的真人 actor | 1 |
|
||||
|
||||
这里的 `13` 是可归属真人的记录证据,不等于可精确还原的 13 次页面点击;认证审计只观察到 1 个真人 actor,也不排除代理提交或审计覆盖外的历史使用。两项口径都指向同一结论:采用度极低,而不是已形成全员稳定使用。
|
||||
|
||||
## 3. 关键观察
|
||||
|
||||
### 3.1 只有一条可证明的真人业务链
|
||||
|
||||
- `reimbursements_invoices`:13 条聚合记录中,近 30/90 天均有 5 条真人记录证据、2 个真人 actor;另有 4 条 smoke、4 条 system,说明不去污染会明显高估使用度。
|
||||
- `workflow_approvals`:2 条真人记录、1 个真人 actor,审计交叉验证 6 条真人 action。
|
||||
- `payments_ap`:3 条真人记录、1 个真人 actor,审计交叉验证 7 条真人 action。
|
||||
- 三者合计 18 条业务记录、10 条真人归属/创建记录证据,是当前唯一可证明的端到端财务链。它仍更接近少量流程验证或零星使用,不能据此声称团队已采用;三者也不应拆成三个并行产品路线。
|
||||
|
||||
### 3.2 自动化比真人活动更多
|
||||
|
||||
- 20 个模块近 30 天合计:真人记录证据 13、smoke 4、system 24、unknown 14。
|
||||
- 工作台/通知的 21 条记录没有真人交互证据;其中近 30 天的 18 条、近 90 天的 21 条均归为 system。企微请假留存的 3 条记录也全部是 system 证据。它们证明后台机制工作,不证明用户在 OA 页面主动使用;本次查询未统计通知 `read_at`,因此也不把它解释为“已证明没人看”。
|
||||
- 系统配置的 26 条记录中近 30 天只有 1 条真人、14 条 unknown;配置表初始化/历史更新时间不能解释成管理员日常使用。
|
||||
|
||||
### 3.3 主导航存在明确噪音
|
||||
|
||||
生产 workspace 将公告、普通待办、周报和项目都暴露在 `main`,对应 flag 为 on 或无总开关,但四者 90 天均为 0 记录、0 真人活动、0 审计活动。
|
||||
|
||||
其中公告、普通待办和周报还偏离当前代码默认层级(默认分别为 admin、hidden、admin)。这不是产品代码故障,而是生产 workspace 配置保留了历史扩张期入口。
|
||||
|
||||
### 3.4 零使用不等于同一种结论
|
||||
|
||||
- 合同、采购、资产虽均为 0,但有低频高失败成本、附件/审计和离职交接价值,应保留最小能力并冻结开发。
|
||||
- 周报、普通待办、公告和项目存在企微群、项目文档或直接沟通等便宜替代,当前主导航暴露没有生产证据支撑。
|
||||
- 薪资、绩效、预算、完整差旅和 ERP 集成同时满足“零数据/零真人活动 + 无日常入口或关闭 + 有可靠替代”,可进入退役前调研。
|
||||
|
||||
### 3.5 下一步是管理决策,不是工程猜测
|
||||
|
||||
- 近 30/90 天认证成功审计只观察到 1 个真人 actor、5/6 次真人登录;smoke 登录为 11 次。
|
||||
- 该数字不能单独证明其他员工从未使用(可能存在代理提交或审计覆盖差异),但足以否定“系统已经被全员稳定使用”的假设。
|
||||
- “功能不够好导致没人用”没有生产证据支持。请假、考勤本来由企微主办,其他零使用模块也可能只是业务未发生或流程仍在线下。
|
||||
- 管理层必须选择 OA 的实际定位:A 为要求团队把明确业务迁入 OA;B 为不强推日常使用、收缩成低频台账和治理兜底。该选择属于组织流程,不应伪装成代码需求。
|
||||
- 无论选择 A 或 B,先隐藏四个零使用主导航入口都成立;但隐藏动作必须另建 task 并单独批准,本任务只提供证据。
|
||||
|
||||
## 4. 四类模块矩阵
|
||||
|
||||
| 模块 | 生产证据(30/90 天) | 当前暴露 | 分类 | 理由与后续触发 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 报销/票据/OCR | 真人 5/5、2 actors;smoke 4、system 4 | main | **重点优化(财务闭环)** | 唯一有多人真人记录的主流程;先收集实际报销人/财务的摩擦点,再开小任务 |
|
||||
| 统一审批 | 真人 2/2、1 actor;审计真人 6/6 | admin | **重点优化(同一闭环)** | 与报销/付款合并看待,不单独扩建通用 BPM |
|
||||
| 付款/AP | 真人 3/3、1 actor;审计真人 7/7 | admin/on | **重点优化(同一闭环)** | maker-checker 与付款证据不可省;只优化真实闭环痛点 |
|
||||
| 工作台/通知 | system 18/21,真人 0 | main/on | **保持冻结** | 是真实流程的必要通知基础,但记录量不能证明页面价值;不扩成大门户 |
|
||||
| 采购 | 0/0 | main | **保持冻结** | 低频金额/附件/审批留痕有价值;建议收进二级入口,首个真实采购触发只读验收,不继续做复杂招采 |
|
||||
| 合同 | 0/0 | main/on | **保持冻结** | 低频高失败成本;建议收进二级入口,首条生产合同仍是 UI 验收触发条件,需业务确认是否应录未录 |
|
||||
| 资产/盘点 | 0/0 | main | **保持冻结** | 设备、电子资产和离职交接必要;建议收进二级入口,首批资产建档前不扩建盘点能力 |
|
||||
| 请假/企微历史 | system 2/3,真人 0 | main | **保持冻结** | 企微主办、OA 留存;不把系统同步当 OA 页面使用,不重造请假流程 |
|
||||
| 考勤/团队状态 | 0/0 | hidden/on | **保持冻结** | 企微主办且当前已隐藏;保留留存/兜底边界,不扩建 OA 原生打卡 |
|
||||
| 人员生命周期 | 真人 2/2、1 actor;审计真人 51/51 | admin/on | **保持冻结** | 账号、入离职、角色和主管是基础,但高审计量含近期账号治理,不代表 HR 套件高频使用 |
|
||||
| 系统配置 | 真人 1/1、unknown 14/25 | admin | **保持冻结** | 权限、workspace 和开关必要;保持后台化,工具债触达时再修 |
|
||||
| 公告 | 0/0 | main/on | **隐藏入口** | 小团队企微群可替代;需要制度签收时按事件启用,数据与路由先保留 |
|
||||
| 普通待办 | 0/0 | main/on | **隐藏入口** | 与审批待办、企微消息和项目事项重复;先恢复 hidden,不立即删表/路由 |
|
||||
| 周报 | 0/0 | main/on | **隐藏入口** | 无提交和阅读责任人证据;周会/项目纪要可替代,先恢复 admin/hidden |
|
||||
| 项目 | 0/0 | main/on | **隐藏入口** | 当前没有主档或关联数据;保留最小代码,真实项目关联需求出现时再开放 |
|
||||
| 预算 | 0/0 | admin | **候选退役** | 小团队可由报销/采购审批和表格控制;先确认无预算负责人及保留义务 |
|
||||
| 完整差旅 | 0/0 | admin | **候选退役** | 当前报销闭环可承接差旅费用;先确认无统付/卡交易真实流程 |
|
||||
| 薪资 | 0/0 | 无导航 | **候选退役** | 高敏低频,财务/代账与受控表格更合适;物理删除前确认无保留数据 |
|
||||
| 绩效 | 0/0 | 无导航 | **候选退役** | 5–10 人更适合直接反馈;无周期、无记录、无负责人证据 |
|
||||
| ERP/集成 | 主表 0;仅 1 条真人“跳过/关闭”类审计 | 无导航/off | **候选退役** | ERP 已明确不接,关闭审计不是业务使用;后续检查依赖/outbox/保留后分批移除 |
|
||||
|
||||
## 5. 后续决策与行动顺序
|
||||
|
||||
### 先做共同动作
|
||||
|
||||
1. **暂停产品功能开发**:在管理层明确选择 A 或 B 前,不以补功能、修视觉或完善测试矩阵解释采用度问题;真实线上缺陷和安全/恢复底线不在冻结范围内。
|
||||
2. **另批可逆导航减法**:建立独立 Trellis task,将公告、普通待办、周报、项目从主导航隐藏;合同、采购、资产保留最小能力但评估收进二级入口。保留路由、数据和恢复路径,生产 workspace/flag 写入另行批准。本任务不创建该 task。
|
||||
|
||||
### 管理层二选一
|
||||
|
||||
**A:要求团队真正使用 OA。** 选择一个明确且近期必然发生的流程作为团队规则;最小建议是下一轮报销统一从 OA 提交,不再用微信消息替代。目标不是先优化页面,而是让 5–10 人产生一次真实使用样本,再按实际阻塞点决定是否开一个小任务。若推动失败,先记录组织/流程原因,不默认归因于产品。
|
||||
|
||||
**B:不强推,OA 作为低频台账和治理兜底。** 大幅收缩日常入口与维护面;保留报销/付款、合同、采购、资产、认证、权限、审计、备份恢复和离职撤权等必要能力,冻结或分批审查预算、完整差旅、薪资、绩效、ERP/集成等超出实际需要的模块。
|
||||
|
||||
### 产品开发门禁
|
||||
|
||||
- A/B 未明确前,不启动新产品功能 task。
|
||||
- 选择 A 后,只有团队真实使用暴露出的最高优先级摩擦点可进入小型产品 task;不预先猜需求。
|
||||
- 选择 B 后,默认只做减法、必要维护和安全恢复,不再追求通用 OA 的功能完整度。
|
||||
- 低优先工具债只有再次触达对应工具时才处理,不挤占真实业务或减法预算。
|
||||
- 认证、审计、权限、付款 maker-checker、备份恢复和离职撤权继续作为安全底线,不用低使用频率否定。
|
||||
|
||||
### 明确不做
|
||||
|
||||
- 不新增 OA 模块、通用工作流/BPM、BI 看板、ERP、AI 助手或完整 HR/薪资/绩效套件。
|
||||
- 不为零使用模块继续补测试矩阵、视觉完善或“功能完整度”。
|
||||
- 不主动处理 SHA 比较器、repair helper 等低优先工具债;只有再次触达对应工具时才顺手纳入门禁。
|
||||
- 不因本次零数据物理删除表、路由或历史证据。
|
||||
|
||||
## 6. 后续独立 task 候选
|
||||
|
||||
| 优先级 | 候选 task | 触发/输入 | 验收 | 生产批准 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| P1 | 小团队主导航减法 | 用户接受本矩阵;另行批准建 task | 四个零使用入口不再占主导航;合同/采购/资产转二级入口的范围另审;直达/恢复能力保留 | 需要,workspace/flag 写入 |
|
||||
| P1 | OA 采用模式决策 | 管理层选择 A 或 B | 事实源明确强推使用或台账兜底;选择前产品开发保持冻结 | 无生产动作 |
|
||||
| P2 | 财务闭环单点优化 | 仅限选择 A 且团队真实使用产生明确摩擦点 | 只解决排名第一的问题;公网部署后才能通知测试 | 代码/部署时分别批准 |
|
||||
| P2 | 低频业务落地确认 | 负责人回答采购/合同/资产/差旅/预算事实 | 更新既有事实源或入口决策,不造生产数据 | 只读确认无需;配置变更需要 |
|
||||
| P3 | 退役前依赖与保留审查 | 再次触达预算/差旅/薪资/绩效/ERP 代码 | 数据导出、审计保留、依赖、回滚均明确 | 删除/迁移另批 |
|
||||
|
||||
## 7. 仍需业务负责人回答
|
||||
|
||||
- 过去 90 天是否签过合同、发生采购或新增/转移资产;若发生,为什么未进入 OA?
|
||||
- 实际报销人、审批人和财务是否都知道并愿意使用当前 OA 主流程?
|
||||
- 是否存在必须由 OA 承担的预算、差旅统付、薪资或绩效流程及明确负责人?
|
||||
|
||||
在 A/B 定位和这些业务事实得到回答前,相关模块保持冻结或候选状态,不做写入、不扩大范围。
|
||||
+677
@@ -0,0 +1,677 @@
|
||||
{
|
||||
"status": "PASS",
|
||||
"evidenceBoundary": "production_aggregate_read_only",
|
||||
"classificationBoundary": "human counts are a conservative lower bound because production v1 also treated an unseparated oasmoke email prefix as smoke-like",
|
||||
"preflight": {
|
||||
"exactIdentity": true,
|
||||
"runtimeBuildExact": true,
|
||||
"deployedSource": "origin/main",
|
||||
"productCodeDelta": 0,
|
||||
"migrationPending": 0,
|
||||
"rollbackWindow": "OPEN",
|
||||
"terminatedRows": 0
|
||||
},
|
||||
"execution": {
|
||||
"preflightToolInterceptions": 1,
|
||||
"preflightToolReason": "deploy wrapper compared current documentation HEAD instead of the deployed source",
|
||||
"preQueryInterceptions": 1,
|
||||
"preQueryReason": "stdin_entrypoint_not_invoked",
|
||||
"effectiveQueryExecutions": 1,
|
||||
"zeroRetry": true,
|
||||
"transactionReadOnly": true
|
||||
},
|
||||
"sourceBuildSha": "ec682565885a",
|
||||
"generatedAt": "2026-08-11T12:47:38.127Z",
|
||||
"authentication": {
|
||||
"humanActors30d": 1,
|
||||
"humanActors90d": 1,
|
||||
"humanEvents30d": 5,
|
||||
"humanEvents90d": 6,
|
||||
"smokeEvents30d": 11,
|
||||
"smokeEvents90d": 11,
|
||||
"unknownEvents30d": 0,
|
||||
"unknownEvents90d": 0
|
||||
},
|
||||
"modules": [
|
||||
{
|
||||
"moduleKey": "announcements",
|
||||
"totalRows": 0,
|
||||
"created30d": 0,
|
||||
"created90d": 0,
|
||||
"activity30d": 0,
|
||||
"activity90d": 0,
|
||||
"latestActivityAt": null,
|
||||
"humanActors30d": 0,
|
||||
"humanActors90d": 0,
|
||||
"humanEvents30d": 0,
|
||||
"humanEvents90d": 0,
|
||||
"smokeEvents30d": 0,
|
||||
"smokeEvents90d": 0,
|
||||
"systemEvents30d": 0,
|
||||
"systemEvents90d": 0,
|
||||
"unknownEvents30d": 0,
|
||||
"unknownEvents90d": 0,
|
||||
"auditEvents30d": 0,
|
||||
"auditEvents90d": 0,
|
||||
"auditHumanActors30d": 0,
|
||||
"auditHumanActors90d": 0,
|
||||
"auditHumanEvents30d": 0,
|
||||
"auditHumanEvents90d": 0,
|
||||
"auditSmokeEvents30d": 0,
|
||||
"auditSmokeEvents90d": 0,
|
||||
"auditUnknownEvents30d": 0,
|
||||
"auditUnknownEvents90d": 0,
|
||||
"navigationMode": "main",
|
||||
"effectiveFlagState": "on",
|
||||
"queryStatus": "PASS"
|
||||
},
|
||||
{
|
||||
"moduleKey": "assets_inventory",
|
||||
"totalRows": 0,
|
||||
"created30d": 0,
|
||||
"created90d": 0,
|
||||
"activity30d": 0,
|
||||
"activity90d": 0,
|
||||
"latestActivityAt": null,
|
||||
"humanActors30d": 0,
|
||||
"humanActors90d": 0,
|
||||
"humanEvents30d": 0,
|
||||
"humanEvents90d": 0,
|
||||
"smokeEvents30d": 0,
|
||||
"smokeEvents90d": 0,
|
||||
"systemEvents30d": 0,
|
||||
"systemEvents90d": 0,
|
||||
"unknownEvents30d": 0,
|
||||
"unknownEvents90d": 0,
|
||||
"auditEvents30d": 0,
|
||||
"auditEvents90d": 0,
|
||||
"auditHumanActors30d": 0,
|
||||
"auditHumanActors90d": 0,
|
||||
"auditHumanEvents30d": 0,
|
||||
"auditHumanEvents90d": 0,
|
||||
"auditSmokeEvents30d": 0,
|
||||
"auditSmokeEvents90d": 0,
|
||||
"auditUnknownEvents30d": 0,
|
||||
"auditUnknownEvents90d": 0,
|
||||
"navigationMode": "main",
|
||||
"effectiveFlagState": "not_applicable",
|
||||
"queryStatus": "PASS"
|
||||
},
|
||||
{
|
||||
"moduleKey": "attendance_presence",
|
||||
"totalRows": 0,
|
||||
"created30d": 0,
|
||||
"created90d": 0,
|
||||
"activity30d": 0,
|
||||
"activity90d": 0,
|
||||
"latestActivityAt": null,
|
||||
"humanActors30d": 0,
|
||||
"humanActors90d": 0,
|
||||
"humanEvents30d": 0,
|
||||
"humanEvents90d": 0,
|
||||
"smokeEvents30d": 0,
|
||||
"smokeEvents90d": 0,
|
||||
"systemEvents30d": 0,
|
||||
"systemEvents90d": 0,
|
||||
"unknownEvents30d": 0,
|
||||
"unknownEvents90d": 0,
|
||||
"auditEvents30d": 0,
|
||||
"auditEvents90d": 0,
|
||||
"auditHumanActors30d": 0,
|
||||
"auditHumanActors90d": 0,
|
||||
"auditHumanEvents30d": 0,
|
||||
"auditHumanEvents90d": 0,
|
||||
"auditSmokeEvents30d": 0,
|
||||
"auditSmokeEvents90d": 0,
|
||||
"auditUnknownEvents30d": 0,
|
||||
"auditUnknownEvents90d": 0,
|
||||
"navigationMode": "hidden",
|
||||
"effectiveFlagState": "on",
|
||||
"queryStatus": "PASS"
|
||||
},
|
||||
{
|
||||
"moduleKey": "budgets",
|
||||
"totalRows": 0,
|
||||
"created30d": 0,
|
||||
"created90d": 0,
|
||||
"activity30d": 0,
|
||||
"activity90d": 0,
|
||||
"latestActivityAt": null,
|
||||
"humanActors30d": 0,
|
||||
"humanActors90d": 0,
|
||||
"humanEvents30d": 0,
|
||||
"humanEvents90d": 0,
|
||||
"smokeEvents30d": 0,
|
||||
"smokeEvents90d": 0,
|
||||
"systemEvents30d": 0,
|
||||
"systemEvents90d": 0,
|
||||
"unknownEvents30d": 0,
|
||||
"unknownEvents90d": 0,
|
||||
"auditEvents30d": 0,
|
||||
"auditEvents90d": 0,
|
||||
"auditHumanActors30d": 0,
|
||||
"auditHumanActors90d": 0,
|
||||
"auditHumanEvents30d": 0,
|
||||
"auditHumanEvents90d": 0,
|
||||
"auditSmokeEvents30d": 0,
|
||||
"auditSmokeEvents90d": 0,
|
||||
"auditUnknownEvents30d": 0,
|
||||
"auditUnknownEvents90d": 0,
|
||||
"navigationMode": "admin",
|
||||
"effectiveFlagState": "not_applicable",
|
||||
"queryStatus": "PASS"
|
||||
},
|
||||
{
|
||||
"moduleKey": "contracts",
|
||||
"totalRows": 0,
|
||||
"created30d": 0,
|
||||
"created90d": 0,
|
||||
"activity30d": 0,
|
||||
"activity90d": 0,
|
||||
"latestActivityAt": null,
|
||||
"humanActors30d": 0,
|
||||
"humanActors90d": 0,
|
||||
"humanEvents30d": 0,
|
||||
"humanEvents90d": 0,
|
||||
"smokeEvents30d": 0,
|
||||
"smokeEvents90d": 0,
|
||||
"systemEvents30d": 0,
|
||||
"systemEvents90d": 0,
|
||||
"unknownEvents30d": 0,
|
||||
"unknownEvents90d": 0,
|
||||
"auditEvents30d": 0,
|
||||
"auditEvents90d": 0,
|
||||
"auditHumanActors30d": 0,
|
||||
"auditHumanActors90d": 0,
|
||||
"auditHumanEvents30d": 0,
|
||||
"auditHumanEvents90d": 0,
|
||||
"auditSmokeEvents30d": 0,
|
||||
"auditSmokeEvents90d": 0,
|
||||
"auditUnknownEvents30d": 0,
|
||||
"auditUnknownEvents90d": 0,
|
||||
"navigationMode": "main",
|
||||
"effectiveFlagState": "on",
|
||||
"queryStatus": "PASS"
|
||||
},
|
||||
{
|
||||
"moduleKey": "integrations",
|
||||
"totalRows": 0,
|
||||
"created30d": 0,
|
||||
"created90d": 0,
|
||||
"activity30d": 0,
|
||||
"activity90d": 0,
|
||||
"latestActivityAt": null,
|
||||
"humanActors30d": 0,
|
||||
"humanActors90d": 0,
|
||||
"humanEvents30d": 0,
|
||||
"humanEvents90d": 0,
|
||||
"smokeEvents30d": 0,
|
||||
"smokeEvents90d": 0,
|
||||
"systemEvents30d": 0,
|
||||
"systemEvents90d": 0,
|
||||
"unknownEvents30d": 0,
|
||||
"unknownEvents90d": 0,
|
||||
"auditEvents30d": 1,
|
||||
"auditEvents90d": 1,
|
||||
"auditHumanActors30d": 1,
|
||||
"auditHumanActors90d": 1,
|
||||
"auditHumanEvents30d": 1,
|
||||
"auditHumanEvents90d": 1,
|
||||
"auditSmokeEvents30d": 0,
|
||||
"auditSmokeEvents90d": 0,
|
||||
"auditUnknownEvents30d": 0,
|
||||
"auditUnknownEvents90d": 0,
|
||||
"navigationMode": "no_navigation",
|
||||
"effectiveFlagState": "off",
|
||||
"queryStatus": "PASS"
|
||||
},
|
||||
{
|
||||
"moduleKey": "leave_wecom_history",
|
||||
"totalRows": 3,
|
||||
"created30d": 2,
|
||||
"created90d": 3,
|
||||
"activity30d": 2,
|
||||
"activity90d": 3,
|
||||
"latestActivityAt": "2026-08-06T08:00:27.065Z",
|
||||
"humanActors30d": 0,
|
||||
"humanActors90d": 0,
|
||||
"humanEvents30d": 0,
|
||||
"humanEvents90d": 0,
|
||||
"smokeEvents30d": 0,
|
||||
"smokeEvents90d": 0,
|
||||
"systemEvents30d": 2,
|
||||
"systemEvents90d": 3,
|
||||
"unknownEvents30d": 0,
|
||||
"unknownEvents90d": 0,
|
||||
"auditEvents30d": 0,
|
||||
"auditEvents90d": 0,
|
||||
"auditHumanActors30d": 0,
|
||||
"auditHumanActors90d": 0,
|
||||
"auditHumanEvents30d": 0,
|
||||
"auditHumanEvents90d": 0,
|
||||
"auditSmokeEvents30d": 0,
|
||||
"auditSmokeEvents90d": 0,
|
||||
"auditUnknownEvents30d": 0,
|
||||
"auditUnknownEvents90d": 0,
|
||||
"navigationMode": "main",
|
||||
"effectiveFlagState": "not_applicable",
|
||||
"queryStatus": "PASS"
|
||||
},
|
||||
{
|
||||
"moduleKey": "payments_ap",
|
||||
"totalRows": 3,
|
||||
"created30d": 3,
|
||||
"created90d": 3,
|
||||
"activity30d": 3,
|
||||
"activity90d": 3,
|
||||
"latestActivityAt": "2026-08-04T06:28:26.141Z",
|
||||
"humanActors30d": 1,
|
||||
"humanActors90d": 1,
|
||||
"humanEvents30d": 3,
|
||||
"humanEvents90d": 3,
|
||||
"smokeEvents30d": 0,
|
||||
"smokeEvents90d": 0,
|
||||
"systemEvents30d": 0,
|
||||
"systemEvents90d": 0,
|
||||
"unknownEvents30d": 0,
|
||||
"unknownEvents90d": 0,
|
||||
"auditEvents30d": 7,
|
||||
"auditEvents90d": 7,
|
||||
"auditHumanActors30d": 3,
|
||||
"auditHumanActors90d": 3,
|
||||
"auditHumanEvents30d": 7,
|
||||
"auditHumanEvents90d": 7,
|
||||
"auditSmokeEvents30d": 0,
|
||||
"auditSmokeEvents90d": 0,
|
||||
"auditUnknownEvents30d": 0,
|
||||
"auditUnknownEvents90d": 0,
|
||||
"navigationMode": "admin",
|
||||
"effectiveFlagState": "on",
|
||||
"queryStatus": "PASS"
|
||||
},
|
||||
{
|
||||
"moduleKey": "payroll",
|
||||
"totalRows": 0,
|
||||
"created30d": 0,
|
||||
"created90d": 0,
|
||||
"activity30d": 0,
|
||||
"activity90d": 0,
|
||||
"latestActivityAt": null,
|
||||
"humanActors30d": 0,
|
||||
"humanActors90d": 0,
|
||||
"humanEvents30d": 0,
|
||||
"humanEvents90d": 0,
|
||||
"smokeEvents30d": 0,
|
||||
"smokeEvents90d": 0,
|
||||
"systemEvents30d": 0,
|
||||
"systemEvents90d": 0,
|
||||
"unknownEvents30d": 0,
|
||||
"unknownEvents90d": 0,
|
||||
"auditEvents30d": 0,
|
||||
"auditEvents90d": 0,
|
||||
"auditHumanActors30d": 0,
|
||||
"auditHumanActors90d": 0,
|
||||
"auditHumanEvents30d": 0,
|
||||
"auditHumanEvents90d": 0,
|
||||
"auditSmokeEvents30d": 0,
|
||||
"auditSmokeEvents90d": 0,
|
||||
"auditUnknownEvents30d": 0,
|
||||
"auditUnknownEvents90d": 0,
|
||||
"navigationMode": "no_navigation",
|
||||
"effectiveFlagState": "not_applicable",
|
||||
"queryStatus": "PASS"
|
||||
},
|
||||
{
|
||||
"moduleKey": "people_lifecycle",
|
||||
"totalRows": 2,
|
||||
"created30d": 2,
|
||||
"created90d": 2,
|
||||
"activity30d": 2,
|
||||
"activity90d": 2,
|
||||
"latestActivityAt": "2026-07-27T11:59:12.669Z",
|
||||
"humanActors30d": 1,
|
||||
"humanActors90d": 1,
|
||||
"humanEvents30d": 2,
|
||||
"humanEvents90d": 2,
|
||||
"smokeEvents30d": 0,
|
||||
"smokeEvents90d": 0,
|
||||
"systemEvents30d": 0,
|
||||
"systemEvents90d": 0,
|
||||
"unknownEvents30d": 0,
|
||||
"unknownEvents90d": 0,
|
||||
"auditEvents30d": 51,
|
||||
"auditEvents90d": 51,
|
||||
"auditHumanActors30d": 1,
|
||||
"auditHumanActors90d": 1,
|
||||
"auditHumanEvents30d": 51,
|
||||
"auditHumanEvents90d": 51,
|
||||
"auditSmokeEvents30d": 0,
|
||||
"auditSmokeEvents90d": 0,
|
||||
"auditUnknownEvents30d": 0,
|
||||
"auditUnknownEvents90d": 0,
|
||||
"navigationMode": "admin",
|
||||
"effectiveFlagState": "on",
|
||||
"queryStatus": "PASS"
|
||||
},
|
||||
{
|
||||
"moduleKey": "performance",
|
||||
"totalRows": 0,
|
||||
"created30d": 0,
|
||||
"created90d": 0,
|
||||
"activity30d": 0,
|
||||
"activity90d": 0,
|
||||
"latestActivityAt": null,
|
||||
"humanActors30d": 0,
|
||||
"humanActors90d": 0,
|
||||
"humanEvents30d": 0,
|
||||
"humanEvents90d": 0,
|
||||
"smokeEvents30d": 0,
|
||||
"smokeEvents90d": 0,
|
||||
"systemEvents30d": 0,
|
||||
"systemEvents90d": 0,
|
||||
"unknownEvents30d": 0,
|
||||
"unknownEvents90d": 0,
|
||||
"auditEvents30d": 0,
|
||||
"auditEvents90d": 0,
|
||||
"auditHumanActors30d": 0,
|
||||
"auditHumanActors90d": 0,
|
||||
"auditHumanEvents30d": 0,
|
||||
"auditHumanEvents90d": 0,
|
||||
"auditSmokeEvents30d": 0,
|
||||
"auditSmokeEvents90d": 0,
|
||||
"auditUnknownEvents30d": 0,
|
||||
"auditUnknownEvents90d": 0,
|
||||
"navigationMode": "no_navigation",
|
||||
"effectiveFlagState": "not_applicable",
|
||||
"queryStatus": "PASS"
|
||||
},
|
||||
{
|
||||
"moduleKey": "portal_notifications",
|
||||
"totalRows": 21,
|
||||
"created30d": 18,
|
||||
"created90d": 21,
|
||||
"activity30d": 18,
|
||||
"activity90d": 21,
|
||||
"latestActivityAt": "2026-08-04T06:28:26.161Z",
|
||||
"humanActors30d": 0,
|
||||
"humanActors90d": 0,
|
||||
"humanEvents30d": 0,
|
||||
"humanEvents90d": 0,
|
||||
"smokeEvents30d": 0,
|
||||
"smokeEvents90d": 0,
|
||||
"systemEvents30d": 18,
|
||||
"systemEvents90d": 21,
|
||||
"unknownEvents30d": 0,
|
||||
"unknownEvents90d": 0,
|
||||
"auditEvents30d": 0,
|
||||
"auditEvents90d": 0,
|
||||
"auditHumanActors30d": 0,
|
||||
"auditHumanActors90d": 0,
|
||||
"auditHumanEvents30d": 0,
|
||||
"auditHumanEvents90d": 0,
|
||||
"auditSmokeEvents30d": 0,
|
||||
"auditSmokeEvents90d": 0,
|
||||
"auditUnknownEvents30d": 0,
|
||||
"auditUnknownEvents90d": 0,
|
||||
"navigationMode": "main",
|
||||
"effectiveFlagState": "on",
|
||||
"queryStatus": "PASS"
|
||||
},
|
||||
{
|
||||
"moduleKey": "procurement",
|
||||
"totalRows": 0,
|
||||
"created30d": 0,
|
||||
"created90d": 0,
|
||||
"activity30d": 0,
|
||||
"activity90d": 0,
|
||||
"latestActivityAt": null,
|
||||
"humanActors30d": 0,
|
||||
"humanActors90d": 0,
|
||||
"humanEvents30d": 0,
|
||||
"humanEvents90d": 0,
|
||||
"smokeEvents30d": 0,
|
||||
"smokeEvents90d": 0,
|
||||
"systemEvents30d": 0,
|
||||
"systemEvents90d": 0,
|
||||
"unknownEvents30d": 0,
|
||||
"unknownEvents90d": 0,
|
||||
"auditEvents30d": 0,
|
||||
"auditEvents90d": 0,
|
||||
"auditHumanActors30d": 0,
|
||||
"auditHumanActors90d": 0,
|
||||
"auditHumanEvents30d": 0,
|
||||
"auditHumanEvents90d": 0,
|
||||
"auditSmokeEvents30d": 0,
|
||||
"auditSmokeEvents90d": 0,
|
||||
"auditUnknownEvents30d": 0,
|
||||
"auditUnknownEvents90d": 0,
|
||||
"navigationMode": "main",
|
||||
"effectiveFlagState": "not_applicable",
|
||||
"queryStatus": "PASS"
|
||||
},
|
||||
{
|
||||
"moduleKey": "projects",
|
||||
"totalRows": 0,
|
||||
"created30d": 0,
|
||||
"created90d": 0,
|
||||
"activity30d": 0,
|
||||
"activity90d": 0,
|
||||
"latestActivityAt": null,
|
||||
"humanActors30d": 0,
|
||||
"humanActors90d": 0,
|
||||
"humanEvents30d": 0,
|
||||
"humanEvents90d": 0,
|
||||
"smokeEvents30d": 0,
|
||||
"smokeEvents90d": 0,
|
||||
"systemEvents30d": 0,
|
||||
"systemEvents90d": 0,
|
||||
"unknownEvents30d": 0,
|
||||
"unknownEvents90d": 0,
|
||||
"auditEvents30d": 0,
|
||||
"auditEvents90d": 0,
|
||||
"auditHumanActors30d": 0,
|
||||
"auditHumanActors90d": 0,
|
||||
"auditHumanEvents30d": 0,
|
||||
"auditHumanEvents90d": 0,
|
||||
"auditSmokeEvents30d": 0,
|
||||
"auditSmokeEvents90d": 0,
|
||||
"auditUnknownEvents30d": 0,
|
||||
"auditUnknownEvents90d": 0,
|
||||
"navigationMode": "main",
|
||||
"effectiveFlagState": "on",
|
||||
"queryStatus": "PASS"
|
||||
},
|
||||
{
|
||||
"moduleKey": "reimbursements_invoices",
|
||||
"totalRows": 13,
|
||||
"created30d": 13,
|
||||
"created90d": 13,
|
||||
"activity30d": 13,
|
||||
"activity90d": 13,
|
||||
"latestActivityAt": "2026-08-05T03:07:41.581Z",
|
||||
"humanActors30d": 2,
|
||||
"humanActors90d": 2,
|
||||
"humanEvents30d": 5,
|
||||
"humanEvents90d": 5,
|
||||
"smokeEvents30d": 4,
|
||||
"smokeEvents90d": 4,
|
||||
"systemEvents30d": 4,
|
||||
"systemEvents90d": 4,
|
||||
"unknownEvents30d": 0,
|
||||
"unknownEvents90d": 0,
|
||||
"auditEvents30d": 107,
|
||||
"auditEvents90d": 107,
|
||||
"auditHumanActors30d": 4,
|
||||
"auditHumanActors90d": 4,
|
||||
"auditHumanEvents30d": 99,
|
||||
"auditHumanEvents90d": 99,
|
||||
"auditSmokeEvents30d": 8,
|
||||
"auditSmokeEvents90d": 8,
|
||||
"auditUnknownEvents30d": 0,
|
||||
"auditUnknownEvents90d": 0,
|
||||
"navigationMode": "main",
|
||||
"effectiveFlagState": "not_applicable",
|
||||
"queryStatus": "PASS"
|
||||
},
|
||||
{
|
||||
"moduleKey": "system_configuration",
|
||||
"totalRows": 26,
|
||||
"created30d": 0,
|
||||
"created90d": 26,
|
||||
"activity30d": 15,
|
||||
"activity90d": 26,
|
||||
"latestActivityAt": "2026-08-10T13:04:19.295Z",
|
||||
"humanActors30d": 1,
|
||||
"humanActors90d": 1,
|
||||
"humanEvents30d": 1,
|
||||
"humanEvents90d": 1,
|
||||
"smokeEvents30d": 0,
|
||||
"smokeEvents90d": 0,
|
||||
"systemEvents30d": 0,
|
||||
"systemEvents90d": 0,
|
||||
"unknownEvents30d": 14,
|
||||
"unknownEvents90d": 25,
|
||||
"auditEvents30d": 1,
|
||||
"auditEvents90d": 1,
|
||||
"auditHumanActors30d": 1,
|
||||
"auditHumanActors90d": 1,
|
||||
"auditHumanEvents30d": 1,
|
||||
"auditHumanEvents90d": 1,
|
||||
"auditSmokeEvents30d": 0,
|
||||
"auditSmokeEvents90d": 0,
|
||||
"auditUnknownEvents30d": 0,
|
||||
"auditUnknownEvents90d": 0,
|
||||
"navigationMode": "admin",
|
||||
"effectiveFlagState": "not_applicable",
|
||||
"queryStatus": "PASS"
|
||||
},
|
||||
{
|
||||
"moduleKey": "todos",
|
||||
"totalRows": 0,
|
||||
"created30d": 0,
|
||||
"created90d": 0,
|
||||
"activity30d": 0,
|
||||
"activity90d": 0,
|
||||
"latestActivityAt": null,
|
||||
"humanActors30d": 0,
|
||||
"humanActors90d": 0,
|
||||
"humanEvents30d": 0,
|
||||
"humanEvents90d": 0,
|
||||
"smokeEvents30d": 0,
|
||||
"smokeEvents90d": 0,
|
||||
"systemEvents30d": 0,
|
||||
"systemEvents90d": 0,
|
||||
"unknownEvents30d": 0,
|
||||
"unknownEvents90d": 0,
|
||||
"auditEvents30d": 0,
|
||||
"auditEvents90d": 0,
|
||||
"auditHumanActors30d": 0,
|
||||
"auditHumanActors90d": 0,
|
||||
"auditHumanEvents30d": 0,
|
||||
"auditHumanEvents90d": 0,
|
||||
"auditSmokeEvents30d": 0,
|
||||
"auditSmokeEvents90d": 0,
|
||||
"auditUnknownEvents30d": 0,
|
||||
"auditUnknownEvents90d": 0,
|
||||
"navigationMode": "main",
|
||||
"effectiveFlagState": "on",
|
||||
"queryStatus": "PASS"
|
||||
},
|
||||
{
|
||||
"moduleKey": "travel",
|
||||
"totalRows": 0,
|
||||
"created30d": 0,
|
||||
"created90d": 0,
|
||||
"activity30d": 0,
|
||||
"activity90d": 0,
|
||||
"latestActivityAt": null,
|
||||
"humanActors30d": 0,
|
||||
"humanActors90d": 0,
|
||||
"humanEvents30d": 0,
|
||||
"humanEvents90d": 0,
|
||||
"smokeEvents30d": 0,
|
||||
"smokeEvents90d": 0,
|
||||
"systemEvents30d": 0,
|
||||
"systemEvents90d": 0,
|
||||
"unknownEvents30d": 0,
|
||||
"unknownEvents90d": 0,
|
||||
"auditEvents30d": 0,
|
||||
"auditEvents90d": 0,
|
||||
"auditHumanActors30d": 0,
|
||||
"auditHumanActors90d": 0,
|
||||
"auditHumanEvents30d": 0,
|
||||
"auditHumanEvents90d": 0,
|
||||
"auditSmokeEvents30d": 0,
|
||||
"auditSmokeEvents90d": 0,
|
||||
"auditUnknownEvents30d": 0,
|
||||
"auditUnknownEvents90d": 0,
|
||||
"navigationMode": "admin",
|
||||
"effectiveFlagState": "not_applicable",
|
||||
"queryStatus": "PASS"
|
||||
},
|
||||
{
|
||||
"moduleKey": "weekly_reports",
|
||||
"totalRows": 0,
|
||||
"created30d": 0,
|
||||
"created90d": 0,
|
||||
"activity30d": 0,
|
||||
"activity90d": 0,
|
||||
"latestActivityAt": null,
|
||||
"humanActors30d": 0,
|
||||
"humanActors90d": 0,
|
||||
"humanEvents30d": 0,
|
||||
"humanEvents90d": 0,
|
||||
"smokeEvents30d": 0,
|
||||
"smokeEvents90d": 0,
|
||||
"systemEvents30d": 0,
|
||||
"systemEvents90d": 0,
|
||||
"unknownEvents30d": 0,
|
||||
"unknownEvents90d": 0,
|
||||
"auditEvents30d": 0,
|
||||
"auditEvents90d": 0,
|
||||
"auditHumanActors30d": 0,
|
||||
"auditHumanActors90d": 0,
|
||||
"auditHumanEvents30d": 0,
|
||||
"auditHumanEvents90d": 0,
|
||||
"auditSmokeEvents30d": 0,
|
||||
"auditSmokeEvents90d": 0,
|
||||
"auditUnknownEvents30d": 0,
|
||||
"auditUnknownEvents90d": 0,
|
||||
"navigationMode": "main",
|
||||
"effectiveFlagState": "on",
|
||||
"queryStatus": "PASS"
|
||||
},
|
||||
{
|
||||
"moduleKey": "workflow_approvals",
|
||||
"totalRows": 2,
|
||||
"created30d": 2,
|
||||
"created90d": 2,
|
||||
"activity30d": 2,
|
||||
"activity90d": 2,
|
||||
"latestActivityAt": "2026-08-01T13:47:24.242Z",
|
||||
"humanActors30d": 1,
|
||||
"humanActors90d": 1,
|
||||
"humanEvents30d": 2,
|
||||
"humanEvents90d": 2,
|
||||
"smokeEvents30d": 0,
|
||||
"smokeEvents90d": 0,
|
||||
"systemEvents30d": 0,
|
||||
"systemEvents90d": 0,
|
||||
"unknownEvents30d": 0,
|
||||
"unknownEvents90d": 0,
|
||||
"auditEvents30d": 6,
|
||||
"auditEvents90d": 6,
|
||||
"auditHumanActors30d": 1,
|
||||
"auditHumanActors90d": 1,
|
||||
"auditHumanEvents30d": 6,
|
||||
"auditHumanEvents90d": 6,
|
||||
"auditSmokeEvents30d": 0,
|
||||
"auditSmokeEvents90d": 0,
|
||||
"auditUnknownEvents30d": 0,
|
||||
"auditUnknownEvents90d": 0,
|
||||
"navigationMode": "admin",
|
||||
"effectiveFlagState": "not_applicable",
|
||||
"queryStatus": "PASS"
|
||||
}
|
||||
]
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
# 生产使用度普查只读 review
|
||||
|
||||
## 结论
|
||||
|
||||
`PASS_WITH_DECLARED_LIMITS`。生产查询、证据脱敏和四类裁决均在批准边界内;声明的两项测量限制不改变“财务闭环是唯一重点产品候选、四个主导航入口可隐藏、五类能力进入退役前调研”的结论。
|
||||
|
||||
## 1. 生产身份与执行次数
|
||||
|
||||
- identity bootstrap 先证明唯一运行环境和 origin/runtime agreement。
|
||||
- 既有 deploy wrapper 在 provenance 阶段因本地文档 HEAD 领先运行候选而 fail-closed;未进入 migration 或普查。
|
||||
- task-scoped PRE 改为精确比较运行容器 SHA 与 `origin/main`,并断言其后 `client/src`、`server/src`、Compose 产品差异为 0;没有放宽或修改正式部署工具。
|
||||
- migration pending=none,rollback window OPEN、terminatedRows=0。
|
||||
- 第一次 query runner 的 `node -` 入口没有调用 `run()`;本地复现证明 `require.main === module` 为 false,输出无 JSON、普查事务未开始。修正后用相同 stdin 入口在本地通过,再进行唯一有效生产执行。
|
||||
- 最终证据为 `preflightToolInterceptions=1`、`preQueryInterceptions=1`、`effectiveQueryExecutions=1`、`zeroRetry=true`。不存在未知状态下盲目重跑。
|
||||
|
||||
## 2. 只读与写入边界
|
||||
|
||||
- 查询器在第一批数据库语句中执行 `BEGIN TRANSACTION READ ONLY`、statement timeout、lock timeout,并断言 `transaction_read_only=on`。
|
||||
- module metrics、认证聚合、workspace setting 和 feature flag evaluator 共用同一 `pg` client/事务;adapter 只实现 `findUnique/findMany/findFirst` 三种 SELECT。
|
||||
- 静态合同拒绝 INSERT/UPDATE/DELETE/MERGE/DDL/CALL/COPY/GRANT/REVOKE 和 `FOR UPDATE`。
|
||||
- 生产未部署查询器;文件通过 stdin 进入现有 server 容器。没有登录、配置、schema、业务数据或审计写入。
|
||||
- 20/20 模块 `queryStatus=PASS`;record 分桶与 activity 总数、audit 分桶与 audit 总数逐模块/窗口自洽。
|
||||
|
||||
## 3. 隐私与归因
|
||||
|
||||
- 输出字段为固定 30 项模块聚合,另有安全认证聚合;不存在 ID、姓名、员工号、邮箱、手机号、金额、正文、附件、IP/user agent、audit extra 或 payload。
|
||||
- smoke、system 和 unknown 没有并入真人计数;生产结果清楚显示 system/unknown 活动多于真人活动,证明去污染改变了产品判断。
|
||||
- `humanEvents` 表示“由真人 actor 拥有/创建的记录证据”,不是精确点击或 mutation 次数;可变记录的系统更新可能刷新 `activityAt`。矩阵因此同时引用独立 audit 聚合,并使用“真人记录证据”措辞,没有把它写成操作次数。
|
||||
- 本次生产版本的邮箱 smoke 判据对无分隔符 `oasmoke…` 是 helper 的保守超集。它只可能降低真人计数,不会把 smoke 当真人;财务闭环仍有非 smoke 真人证据,零记录模块不受影响。该限制已写入 snapshot 和矩阵。
|
||||
|
||||
## 4. 分类抽查
|
||||
|
||||
- 真人样本:报销/票据 2 个真人 actor、5 条真人记录证据;审批和付款分别有真人记录与 audit 交叉验证,合并为一条财务闭环。
|
||||
- smoke 样本:报销/票据近 30 天 4 条 smoke 记录、8 条 smoke audit;未计入真人使用。
|
||||
- system 样本:工作台/通知近 30 天 18 条 system,真人 0;未因记录量高判为活跃页面。
|
||||
- unknown 样本:系统配置近 30 天 14 条 unknown;矩阵只将其视为配置历史,不推断管理员高频使用。
|
||||
- 零行边界:合同/采购/资产因低频高失败成本保持冻结;周报/普通待办/公告/项目因主导航暴露、零证据和便宜替代进入隐藏入口;没有“零行自动退役”。
|
||||
- flag 边界:辅助 invoice verification、流程定义和预算 precheck 没有冒充整个业务模块开关;有效主模块 exposure 已在本地解释层纠正,无需重查生产。
|
||||
|
||||
## 5. 路线图复核
|
||||
|
||||
- 重点产品候选只有一条:报销 → 审批 → 付款/AP。
|
||||
- 未来 30 天最多从下一笔真实业务的真人反馈中启动一个小任务;无反馈则不写产品代码。
|
||||
- 四入口导航减法是独立、可逆且需生产批准的后续 task;本任务没有提前执行。
|
||||
- 预算、差旅、薪资、绩效、ERP 仅为候选退役,仍需负责人、数据保留、依赖和回滚审查。
|
||||
- 低优先部署/repair 工具债不占用近期产品预算,触达时再处理。
|
||||
|
||||
## 6. Reviewer 判定
|
||||
|
||||
- AC1–AC7:证据满足。
|
||||
- AC8 的只读 review 部分:满足。
|
||||
- 稳定产品组合结论是否写入长期事实源:等待用户接受本矩阵;在此之前保持 task 证据,不抢先改 docs/spec。
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
|
||||
TASK_RESEARCH_DIR="$ROOT_DIR/.trellis/tasks/08-11-internal-oa-usage-census-and-simplification/research"
|
||||
CENSUS_SCRIPT="$TASK_RESEARCH_DIR/usage-census.cjs"
|
||||
IDENTITY_MODULE="$ROOT_DIR/deploy/trialrun/production-identity-bootstrap.mjs"
|
||||
IDENTITY_REMOTE="$ROOT_DIR/deploy/trialrun/production-identity-bootstrap-remote.sh"
|
||||
PREFLIGHT_SCRIPT="$ROOT_DIR/deploy/trialrun/production-preflight.mjs"
|
||||
CONTRACT_LIB="$ROOT_DIR/deploy/trialrun/deployment-contract-lib.sh"
|
||||
CONTRACT_UNIT="hyb-oa-automatic-backup-daily.service"
|
||||
CONFIRMATION="READ_ONLY_USAGE_CENSUS"
|
||||
|
||||
# shellcheck source=../../../../../deploy/trialrun/deployment-contract-lib.sh
|
||||
source "$CONTRACT_LIB"
|
||||
|
||||
fail() {
|
||||
printf '[usage-census-runner][FAILED] code=%s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
shell_quote() {
|
||||
printf "'%s'" "${1//\'/\'\\\'\'}"
|
||||
}
|
||||
|
||||
mode="${1:---preflight}"
|
||||
case "$mode" in
|
||||
--preflight)
|
||||
[[ "$#" -eq 1 ]] || fail "UNSUPPORTED_ARGUMENTS"
|
||||
;;
|
||||
--execute)
|
||||
[[ "$#" -eq 2 && "${2:-}" == "$CONFIRMATION" ]] \
|
||||
|| fail "EXPLICIT_CONFIRMATION_REQUIRED"
|
||||
;;
|
||||
*)
|
||||
fail "UNSUPPORTED_ARGUMENTS"
|
||||
;;
|
||||
esac
|
||||
|
||||
for file in "$CENSUS_SCRIPT" "$IDENTITY_MODULE" "$IDENTITY_REMOTE" "$PREFLIGHT_SCRIPT"; do
|
||||
[[ -f "$file" && ! -L "$file" ]] || fail "LOCAL_CONTRACT_MISSING"
|
||||
done
|
||||
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
# Reuse the reviewed history parsing and runbook-origin parser. Values remain
|
||||
# in process memory and are never printed.
|
||||
identity_fields="$({
|
||||
node --input-type=module - "$IDENTITY_MODULE" <<'NODE'
|
||||
import fs from "node:fs";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const modulePath = process.argv[2];
|
||||
const mod = await import(pathToFileURL(modulePath));
|
||||
const targetSource = execFileSync("git", ["show", "34181e3:deploy/trialrun/deploy.sh"], { encoding: "utf8" });
|
||||
const projectSource = execFileSync("git", ["show", "5e2d038938265c926e06d3cc6d0841f6dcc06148:deploy/trialrun/deploy.sh"], { encoding: "utf8" });
|
||||
const identity = mod.parseHistoricalDeploymentIdentity(targetSource, projectSource);
|
||||
const runbook = fs.readFileSync("docs/trialrun/OPS7运维交接Runbook.md", "utf8");
|
||||
const origin = mod.parseExpectedProductionOrigin(runbook);
|
||||
process.stdout.write(`${identity.deployServer}\t${identity.composeProjectName}\t${origin}`);
|
||||
NODE
|
||||
} 2>/dev/null)" || fail "LOCAL_IDENTITY_PARSE_FAILED"
|
||||
IFS=$'\t' read -r deploy_server expected_project expected_origin <<<"$identity_fields"
|
||||
deployment_contract_safe_ssh_target "$deploy_server" || fail "DEPLOY_SERVER_INVALID"
|
||||
deployment_contract_safe_project "$expected_project" || fail "COMPOSE_PROJECT_INVALID"
|
||||
[[ "$expected_origin" == https://* ]] || fail "PRODUCTION_ORIGIN_INVALID"
|
||||
|
||||
# Identity-only mode exits before database and migration reads. It also proves
|
||||
# the unique forced-command refresh identity and origin/runtime agreement.
|
||||
PRODUCTION_IDENTITY_ONLY=1 node "$IDENTITY_MODULE" \
|
||||
|| fail "PRODUCTION_IDENTITY_FAILED"
|
||||
|
||||
declare -A contract=()
|
||||
deployment_contract_load "$deploy_server" "$CONTRACT_UNIT" contract \
|
||||
|| fail "SYSTEMD_CONTRACT_UNAVAILABLE"
|
||||
[[ "${contract[COMPOSE_PROJECT_NAME]}" == "$expected_project" ]] \
|
||||
|| fail "COMPOSE_PROJECT_MISMATCH"
|
||||
|
||||
origin_hash="$(printf '%s' "$expected_origin" | sha256sum | awk '{print $1}')"
|
||||
database_output="$(
|
||||
ssh "$deploy_server" \
|
||||
"IDENTITY_BOOTSTRAP_MODE='database' REMOTE_DIR=$(shell_quote "${contract[REMOTE_DIR]}") REMOTE_ENV_FILE=$(shell_quote "${contract[REMOTE_ENV_FILE]}") COMPOSE_FILE=$(shell_quote "${contract[COMPOSE_FILE]}") COMPOSE_PROJECT_NAME=$(shell_quote "${contract[COMPOSE_PROJECT_NAME]}") EXPECTED_PRODUCTION_ORIGIN_SHA256='$origin_hash' bash -s" \
|
||||
< "$IDENTITY_REMOTE"
|
||||
)" || fail "DATABASE_IDENTITY_FAILED"
|
||||
database_hash="$(printf '%s' "$database_output" | sed -n 's/^\[production-identity-bootstrap\]\[DB_SHA256\] \([0-9a-f]\{64\}\)$/\1/p')"
|
||||
[[ "$database_hash" =~ ^[0-9a-f]{64}$ ]] || fail "DATABASE_IDENTITY_PAYLOAD_INVALID"
|
||||
|
||||
candidate_commit="$(git rev-parse origin/main)"
|
||||
[[ "$candidate_commit" =~ ^[0-9a-f]{40,64}$ ]] || fail "DEPLOYED_COMMIT_INVALID"
|
||||
[[ -z "$(git diff --name-only "$candidate_commit"..HEAD -- client/src server/src deploy/trialrun/docker-compose.yml)" ]] \
|
||||
|| fail "PRODUCT_DELTA_AFTER_DEPLOYED_COMMIT"
|
||||
|
||||
runtime_check_command="
|
||||
set -euo pipefail
|
||||
server_ids=\"\$(docker ps --filter label=com.docker.compose.project=$(shell_quote "${contract[COMPOSE_PROJECT_NAME]}") --filter label=com.docker.compose.service=server --format '{{.ID}}')\"
|
||||
[[ \"\$(printf '%s\\n' \"\$server_ids\" | sed '/^\$/d' | wc -l)\" -eq 1 ]]
|
||||
runtime_sha=\"\$(docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' \"\$server_ids\" | sed -n 's/^BUILD_SHA=//p' | tail -n 1)\"
|
||||
[[ \"\$runtime_sha\" == $(shell_quote "$candidate_commit") ]]
|
||||
printf '[usage-census-runner][PASS] runtime_build_exact\\n'
|
||||
"
|
||||
ssh "$deploy_server" "bash -c $(shell_quote "$runtime_check_command")" \
|
||||
|| fail "RUNTIME_BUILD_MISMATCH"
|
||||
|
||||
report_dir="$(mktemp -d)"
|
||||
trap 'rm -rf -- "$report_dir"' EXIT
|
||||
EXPECTED_PRODUCTION_ORIGIN="$expected_origin" \
|
||||
EXPECTED_PRODUCTION_DB_NAME_SHA256="$database_hash" \
|
||||
EXPECTED_PENDING_MIGRATIONS="" \
|
||||
EXPECTED_DEPLOY_SERVER="$deploy_server" \
|
||||
EXPECTED_REMOTE_DIR="${contract[REMOTE_DIR]}" \
|
||||
EXPECTED_COMPOSE_PROJECT_NAME="${contract[COMPOSE_PROJECT_NAME]}" \
|
||||
DEPLOY_SERVER="$deploy_server" \
|
||||
REMOTE_DIR="${contract[REMOTE_DIR]}" \
|
||||
REMOTE_ENV_FILE="${contract[REMOTE_ENV_FILE]}" \
|
||||
COMPOSE_FILE="${contract[COMPOSE_FILE]}" \
|
||||
COMPOSE_PROJECT_NAME="${contract[COMPOSE_PROJECT_NAME]}" \
|
||||
POSTGRES_DATA_DIR="${contract[POSTGRES_DATA_DIR]}" \
|
||||
STORAGE_HOST_DIR="${contract[STORAGE_HOST_DIR]}" \
|
||||
CANDIDATE_COMMIT="$candidate_commit" \
|
||||
PRODUCTION_PREFLIGHT_REPORT_DIR="$report_dir/preflight" \
|
||||
node "$PREFLIGHT_SCRIPT" \
|
||||
|| fail "PRODUCTION_PREFLIGHT_FAILED"
|
||||
|
||||
printf '[usage-census-runner][PASS] preflight deployedSource=origin/main productDelta=0\n'
|
||||
|
||||
if [[ "$mode" == "--preflight" ]]; then
|
||||
printf '[usage-census-runner][PASS] read_only_preflight_complete queryExecuted=0\n'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
remote_query_command="
|
||||
set -euo pipefail
|
||||
server_ids=\"\$(docker ps --filter label=com.docker.compose.project=$(shell_quote "${contract[COMPOSE_PROJECT_NAME]}") --filter label=com.docker.compose.service=server --format '{{.ID}}')\"
|
||||
[[ \"\$(printf '%s\\n' \"\$server_ids\" | sed '/^\$/d' | wc -l)\" -eq 1 ]]
|
||||
docker exec -i -w /app/server \"\$server_ids\" node -
|
||||
"
|
||||
query_output="$(ssh "$deploy_server" "bash -c $(shell_quote "$remote_query_command")" < "$CENSUS_SCRIPT")" \
|
||||
|| fail "QUERY_EXECUTION_FAILED"
|
||||
|
||||
validated_output="$(printf '%s' "$query_output" | node -e '
|
||||
const fs = require("node:fs");
|
||||
const expected = process.argv[1];
|
||||
const input = fs.readFileSync(0, "utf8");
|
||||
const value = JSON.parse(input);
|
||||
if (value.status !== "PASS" || value.transactionReadOnly !== true) process.exit(1);
|
||||
if (value.sourceBuildSha !== expected.slice(0, 12)) process.exit(1);
|
||||
if (!Array.isArray(value.modules) || value.modules.length !== 20) process.exit(1);
|
||||
if (value.modules.some((item) => item.queryStatus !== "PASS")) process.exit(1);
|
||||
process.stdout.write(JSON.stringify(value, null, 2));
|
||||
' "$candidate_commit")" || fail "QUERY_OUTPUT_INVALID"
|
||||
|
||||
printf '%s\n' "$validated_output"
|
||||
printf '[usage-census-runner][PASS] queryExecuted=1 zeroRetry=1\n' >&2
|
||||
+430
@@ -0,0 +1,430 @@
|
||||
"use strict";
|
||||
|
||||
const path = require("node:path");
|
||||
|
||||
const MODULE_KEYS = Object.freeze([
|
||||
"portal_notifications",
|
||||
"workflow_approvals",
|
||||
"reimbursements_invoices",
|
||||
"payments_ap",
|
||||
"procurement",
|
||||
"contracts",
|
||||
"projects",
|
||||
"assets_inventory",
|
||||
"leave_wecom_history",
|
||||
"attendance_presence",
|
||||
"weekly_reports",
|
||||
"todos",
|
||||
"announcements",
|
||||
"budgets",
|
||||
"travel",
|
||||
"people_lifecycle",
|
||||
"payroll",
|
||||
"performance",
|
||||
"system_configuration",
|
||||
"integrations",
|
||||
]);
|
||||
|
||||
const SAFE_RESULT_KEYS = Object.freeze([
|
||||
"moduleKey",
|
||||
"totalRows",
|
||||
"created30d",
|
||||
"created90d",
|
||||
"activity30d",
|
||||
"activity90d",
|
||||
"latestActivityAt",
|
||||
"humanActors30d",
|
||||
"humanActors90d",
|
||||
"humanEvents30d",
|
||||
"humanEvents90d",
|
||||
"smokeEvents30d",
|
||||
"smokeEvents90d",
|
||||
"systemEvents30d",
|
||||
"systemEvents90d",
|
||||
"unknownEvents30d",
|
||||
"unknownEvents90d",
|
||||
"auditEvents30d",
|
||||
"auditEvents90d",
|
||||
"auditHumanActors30d",
|
||||
"auditHumanActors90d",
|
||||
"auditHumanEvents30d",
|
||||
"auditHumanEvents90d",
|
||||
"auditSmokeEvents30d",
|
||||
"auditSmokeEvents90d",
|
||||
"auditUnknownEvents30d",
|
||||
"auditUnknownEvents90d",
|
||||
"navigationMode",
|
||||
"effectiveFlagState",
|
||||
"queryStatus",
|
||||
]);
|
||||
|
||||
const USAGE_SQL = String.raw`
|
||||
WITH module_keys(module_key) AS (
|
||||
VALUES
|
||||
('portal_notifications'), ('workflow_approvals'),
|
||||
('reimbursements_invoices'), ('payments_ap'), ('procurement'),
|
||||
('contracts'), ('projects'), ('assets_inventory'),
|
||||
('leave_wecom_history'), ('attendance_presence'), ('weekly_reports'),
|
||||
('todos'), ('announcements'), ('budgets'), ('travel'),
|
||||
('people_lifecycle'), ('payroll'), ('performance'),
|
||||
('system_configuration'), ('integrations')
|
||||
), record_events(module_key, record_id, created_at, activity_at, actor_id, forced_kind) AS (
|
||||
SELECT 'portal_notifications', id::text, created_at, created_at, recipient_employee_id, 'system' FROM notifications
|
||||
UNION ALL SELECT 'workflow_approvals', id::text, created_at, updated_at, submitter_id, NULL FROM workflow_instances
|
||||
UNION ALL SELECT 'reimbursements_invoices', id::text, created_at, updated_at, owner_id, NULL FROM reimbursement_forms
|
||||
UNION ALL SELECT 'reimbursements_invoices', id::text, created_at, updated_at, uploaded_by_employee_id, NULL FROM invoice_pool
|
||||
UNION ALL SELECT 'reimbursements_invoices', id::text, created_at, created_at, NULL::uuid, 'system' FROM ocr_results
|
||||
UNION ALL SELECT 'payments_ap', id::text, created_at, updated_at, created_by, NULL FROM payment_batches
|
||||
UNION ALL SELECT 'payments_ap', id::text, created_at, created_at, uploaded_by, NULL FROM payment_batch_receipt_attachments
|
||||
UNION ALL SELECT 'procurement', id::text, created_at, updated_at, applicant_id, NULL FROM purchase_requests
|
||||
UNION ALL SELECT 'contracts', id::text, created_at, updated_at, owner_employee_id, NULL FROM contract_registry
|
||||
UNION ALL SELECT 'projects', id::text, created_at, updated_at, owner_employee_id, NULL FROM projects
|
||||
UNION ALL SELECT 'assets_inventory', id::text, created_at, updated_at, created_by, NULL FROM asset_cards
|
||||
UNION ALL SELECT 'assets_inventory', id::text, created_at, updated_at, created_by, NULL FROM inventory_checks
|
||||
UNION ALL SELECT 'leave_wecom_history', id::text, created_at, updated_at, owner_id, NULL FROM leave_requests
|
||||
UNION ALL SELECT 'leave_wecom_history', id::text, created_at, updated_at, applicant_employee_id, 'system' FROM wecom_approval_archives
|
||||
UNION ALL SELECT 'attendance_presence', id::text, created_at, updated_at, owner_id, NULL FROM attendance_corrections
|
||||
UNION ALL SELECT 'attendance_presence', id::text, created_at, updated_at, employee_id, 'system' FROM wecom_checkin_records
|
||||
UNION ALL SELECT 'attendance_presence', id::text, created_at, updated_at, updated_by, NULL FROM employee_presence
|
||||
UNION ALL SELECT 'weekly_reports', id::text, created_at, updated_at, employee_id, NULL FROM weekly_reports
|
||||
UNION ALL SELECT 'todos', id::text, created_at, updated_at, created_by, NULL FROM todo_items
|
||||
UNION ALL SELECT 'announcements', id::text, created_at, updated_at, created_by, NULL FROM portal_announcements
|
||||
UNION ALL SELECT 'announcements', id::text, created_at, COALESCE(acknowledged_at, created_at), actor_id, NULL FROM portal_announcement_acknowledgements
|
||||
UNION ALL SELECT 'budgets', id::text, created_at, updated_at, created_by, NULL FROM budget_pools
|
||||
UNION ALL SELECT 'travel', id::text, created_at, updated_at, applicant_employee_id, NULL FROM travel_requests
|
||||
UNION ALL SELECT 'travel', id::text, created_at, updated_at, created_by, NULL FROM travel_orders
|
||||
UNION ALL SELECT 'people_lifecycle', id::text, created_at, created_at, created_by, NULL FROM employee_lifecycle_events
|
||||
UNION ALL SELECT 'people_lifecycle', id::text, created_at, updated_at, created_by, NULL FROM employee_contracts
|
||||
UNION ALL SELECT 'people_lifecycle', id::text, created_at, updated_at, created_by, NULL FROM employee_trainings
|
||||
UNION ALL SELECT 'payroll', id::text, created_at, updated_at, created_by, NULL FROM payslips
|
||||
UNION ALL SELECT 'performance', id::text, created_at, updated_at, reviewer_employee_id, NULL FROM performance_reviews
|
||||
UNION ALL SELECT 'system_configuration', id::text, created_at, updated_at, updated_by, NULL FROM feature_flags
|
||||
UNION ALL SELECT 'system_configuration', id::text, created_at, updated_at, updated_by, NULL FROM cutover_scopes
|
||||
UNION ALL SELECT 'integrations', id::text, created_at, updated_at, created_by, 'system' FROM integration_outbox
|
||||
), classified_records AS (
|
||||
SELECT
|
||||
r.*,
|
||||
CASE
|
||||
WHEN r.forced_kind = 'system' THEN 'system'
|
||||
WHEN r.actor_id IS NULL OR e.id IS NULL THEN 'unknown'
|
||||
WHEN lower(split_part(e.email, '@', 1)) ~ '^(smoke[-_.]|oa[-_.]?smoke[-_.]?)'
|
||||
OR e.employee_no ~* '^(SMOKE|OSMOKE|WFULL|PRELAUNCH|LAUNCHAUTO|HRWDTEST)'
|
||||
OR e.name ~ '(测试|试运行)'
|
||||
THEN 'smoke'
|
||||
ELSE 'human'
|
||||
END AS actor_kind
|
||||
FROM record_events r
|
||||
LEFT JOIN employees e ON e.id = r.actor_id
|
||||
), audit_mapped AS (
|
||||
SELECT
|
||||
CASE
|
||||
WHEN action ~ '^(APPROVAL_|DELEGATION_|WORKFLOW_)' THEN 'workflow_approvals'
|
||||
WHEN action ~ '^(REIMBURSEMENT_|INVOICE_)' OR action = 'EXPORT_REIMBURSEMENTS' THEN 'reimbursements_invoices'
|
||||
WHEN action ~ '^PAYMENT_' OR action = 'EXPORT_AP_OPERATIONS' THEN 'payments_ap'
|
||||
WHEN action ~ '^PURCHASE_' THEN 'procurement'
|
||||
WHEN action ~ '^CONTRACT_' OR action = 'EXPORT_CONTRACTS_REGISTRY' THEN 'contracts'
|
||||
WHEN action ~ '^PROJECT_' THEN 'projects'
|
||||
WHEN action ~ '^(ASSET_|INVENTORY_)' THEN 'assets_inventory'
|
||||
WHEN action ~ '^(ATTENDANCE_|PRESENCE_)' THEN 'attendance_presence'
|
||||
WHEN action ~ '^PORTAL_ANNOUNCEMENT_' THEN 'announcements'
|
||||
WHEN action ~ '^BUDGET_' THEN 'budgets'
|
||||
WHEN action ~ '^TRAVEL_' THEN 'travel'
|
||||
WHEN action ~ '^(EMPLOYEE_|SYSTEM_USER|DEPARTMENT_)' THEN 'people_lifecycle'
|
||||
WHEN action ~ '^(CREATE_SALARY_|UPDATE_SALARY_|UPSERT_EMPLOYEE_SALARY_|GENERATE_PAYSLIPS|ISSUE_PAYSLIPS|UPDATE_PAYSLIP|EXPORT_PAYSLIPS)' THEN 'payroll'
|
||||
WHEN action ~ '^PERFORMANCE_' THEN 'performance'
|
||||
WHEN action ~ '^(FEATURE_FLAG_|WORKSPACE_)' THEN 'system_configuration'
|
||||
WHEN action ~ '^ERP_' THEN 'integrations'
|
||||
ELSE NULL
|
||||
END AS module_key,
|
||||
a.actor_id,
|
||||
a.created_at,
|
||||
CASE
|
||||
WHEN e.id IS NULL THEN 'unknown'
|
||||
WHEN lower(split_part(e.email, '@', 1)) ~ '^(smoke[-_.]|oa[-_.]?smoke[-_.]?)'
|
||||
OR e.employee_no ~* '^(SMOKE|OSMOKE|WFULL|PRELAUNCH|LAUNCHAUTO|HRWDTEST)'
|
||||
OR e.name ~ '(测试|试运行)'
|
||||
THEN 'smoke'
|
||||
ELSE 'human'
|
||||
END AS actor_kind
|
||||
FROM audit_logs a
|
||||
LEFT JOIN employees e ON e.id = a.actor_id
|
||||
), record_metrics AS (
|
||||
SELECT
|
||||
module_key,
|
||||
count(*)::int AS total_rows,
|
||||
count(*) FILTER (WHERE created_at >= now() - interval '30 days')::int AS created_30d,
|
||||
count(*) FILTER (WHERE created_at >= now() - interval '90 days')::int AS created_90d,
|
||||
count(*) FILTER (WHERE activity_at >= now() - interval '30 days')::int AS activity_30d,
|
||||
count(*) FILTER (WHERE activity_at >= now() - interval '90 days')::int AS activity_90d,
|
||||
max(activity_at) AS latest_activity_at,
|
||||
count(DISTINCT actor_id) FILTER (WHERE actor_kind = 'human' AND activity_at >= now() - interval '30 days')::int AS human_actors_30d,
|
||||
count(DISTINCT actor_id) FILTER (WHERE actor_kind = 'human' AND activity_at >= now() - interval '90 days')::int AS human_actors_90d,
|
||||
count(*) FILTER (WHERE actor_kind = 'human' AND activity_at >= now() - interval '30 days')::int AS human_events_30d,
|
||||
count(*) FILTER (WHERE actor_kind = 'human' AND activity_at >= now() - interval '90 days')::int AS human_events_90d,
|
||||
count(*) FILTER (WHERE actor_kind = 'smoke' AND activity_at >= now() - interval '30 days')::int AS smoke_events_30d,
|
||||
count(*) FILTER (WHERE actor_kind = 'smoke' AND activity_at >= now() - interval '90 days')::int AS smoke_events_90d,
|
||||
count(*) FILTER (WHERE actor_kind = 'system' AND activity_at >= now() - interval '30 days')::int AS system_events_30d,
|
||||
count(*) FILTER (WHERE actor_kind = 'system' AND activity_at >= now() - interval '90 days')::int AS system_events_90d,
|
||||
count(*) FILTER (WHERE actor_kind = 'unknown' AND activity_at >= now() - interval '30 days')::int AS unknown_events_30d,
|
||||
count(*) FILTER (WHERE actor_kind = 'unknown' AND activity_at >= now() - interval '90 days')::int AS unknown_events_90d
|
||||
FROM classified_records
|
||||
GROUP BY module_key
|
||||
), audit_metrics AS (
|
||||
SELECT
|
||||
module_key,
|
||||
count(*) FILTER (WHERE created_at >= now() - interval '30 days')::int AS audit_events_30d,
|
||||
count(*) FILTER (WHERE created_at >= now() - interval '90 days')::int AS audit_events_90d,
|
||||
count(DISTINCT actor_id) FILTER (WHERE actor_kind = 'human' AND created_at >= now() - interval '30 days')::int AS audit_human_actors_30d,
|
||||
count(DISTINCT actor_id) FILTER (WHERE actor_kind = 'human' AND created_at >= now() - interval '90 days')::int AS audit_human_actors_90d,
|
||||
count(*) FILTER (WHERE actor_kind = 'human' AND created_at >= now() - interval '30 days')::int AS audit_human_events_30d,
|
||||
count(*) FILTER (WHERE actor_kind = 'human' AND created_at >= now() - interval '90 days')::int AS audit_human_events_90d,
|
||||
count(*) FILTER (WHERE actor_kind = 'smoke' AND created_at >= now() - interval '30 days')::int AS audit_smoke_events_30d,
|
||||
count(*) FILTER (WHERE actor_kind = 'smoke' AND created_at >= now() - interval '90 days')::int AS audit_smoke_events_90d,
|
||||
count(*) FILTER (WHERE actor_kind = 'unknown' AND created_at >= now() - interval '30 days')::int AS audit_unknown_events_30d,
|
||||
count(*) FILTER (WHERE actor_kind = 'unknown' AND created_at >= now() - interval '90 days')::int AS audit_unknown_events_90d
|
||||
FROM audit_mapped
|
||||
WHERE module_key IS NOT NULL
|
||||
GROUP BY module_key
|
||||
)
|
||||
SELECT
|
||||
k.module_key AS "moduleKey",
|
||||
COALESCE(r.total_rows, 0) AS "totalRows",
|
||||
COALESCE(r.created_30d, 0) AS "created30d",
|
||||
COALESCE(r.created_90d, 0) AS "created90d",
|
||||
COALESCE(r.activity_30d, 0) AS "activity30d",
|
||||
COALESCE(r.activity_90d, 0) AS "activity90d",
|
||||
r.latest_activity_at AS "latestActivityAt",
|
||||
COALESCE(r.human_actors_30d, 0) AS "humanActors30d",
|
||||
COALESCE(r.human_actors_90d, 0) AS "humanActors90d",
|
||||
COALESCE(r.human_events_30d, 0) AS "humanEvents30d",
|
||||
COALESCE(r.human_events_90d, 0) AS "humanEvents90d",
|
||||
COALESCE(r.smoke_events_30d, 0) AS "smokeEvents30d",
|
||||
COALESCE(r.smoke_events_90d, 0) AS "smokeEvents90d",
|
||||
COALESCE(r.system_events_30d, 0) AS "systemEvents30d",
|
||||
COALESCE(r.system_events_90d, 0) AS "systemEvents90d",
|
||||
COALESCE(r.unknown_events_30d, 0) AS "unknownEvents30d",
|
||||
COALESCE(r.unknown_events_90d, 0) AS "unknownEvents90d",
|
||||
COALESCE(a.audit_events_30d, 0) AS "auditEvents30d",
|
||||
COALESCE(a.audit_events_90d, 0) AS "auditEvents90d",
|
||||
COALESCE(a.audit_human_actors_30d, 0) AS "auditHumanActors30d",
|
||||
COALESCE(a.audit_human_actors_90d, 0) AS "auditHumanActors90d",
|
||||
COALESCE(a.audit_human_events_30d, 0) AS "auditHumanEvents30d",
|
||||
COALESCE(a.audit_human_events_90d, 0) AS "auditHumanEvents90d",
|
||||
COALESCE(a.audit_smoke_events_30d, 0) AS "auditSmokeEvents30d",
|
||||
COALESCE(a.audit_smoke_events_90d, 0) AS "auditSmokeEvents90d",
|
||||
COALESCE(a.audit_unknown_events_30d, 0) AS "auditUnknownEvents30d",
|
||||
COALESCE(a.audit_unknown_events_90d, 0) AS "auditUnknownEvents90d"
|
||||
FROM module_keys k
|
||||
LEFT JOIN record_metrics r ON r.module_key = k.module_key
|
||||
LEFT JOIN audit_metrics a ON a.module_key = k.module_key
|
||||
ORDER BY k.module_key
|
||||
`;
|
||||
|
||||
const AUTH_SQL = String.raw`
|
||||
WITH classified AS (
|
||||
SELECT
|
||||
a.actor_id,
|
||||
a.created_at,
|
||||
CASE
|
||||
WHEN e.id IS NULL THEN 'unknown'
|
||||
WHEN lower(split_part(e.email, '@', 1)) ~ '^(smoke[-_.]|oa[-_.]?smoke[-_.]?)'
|
||||
OR e.employee_no ~* '^(SMOKE|OSMOKE|WFULL|PRELAUNCH|LAUNCHAUTO|HRWDTEST)'
|
||||
OR e.name ~ '(测试|试运行)'
|
||||
THEN 'smoke'
|
||||
ELSE 'human'
|
||||
END AS actor_kind
|
||||
FROM audit_logs a
|
||||
LEFT JOIN employees e ON e.id = a.actor_id
|
||||
WHERE a.action IN ('AUTH_LOGIN_SUCCESS', 'AUTH_EMAIL_LOGIN_SUCCESS', 'AUTH_OIDC_LOGIN_SUCCESS')
|
||||
)
|
||||
SELECT
|
||||
count(DISTINCT actor_id) FILTER (WHERE actor_kind = 'human' AND created_at >= now() - interval '30 days')::int AS "humanActors30d",
|
||||
count(DISTINCT actor_id) FILTER (WHERE actor_kind = 'human' AND created_at >= now() - interval '90 days')::int AS "humanActors90d",
|
||||
count(*) FILTER (WHERE actor_kind = 'human' AND created_at >= now() - interval '30 days')::int AS "humanEvents30d",
|
||||
count(*) FILTER (WHERE actor_kind = 'human' AND created_at >= now() - interval '90 days')::int AS "humanEvents90d",
|
||||
count(*) FILTER (WHERE actor_kind = 'smoke' AND created_at >= now() - interval '30 days')::int AS "smokeEvents30d",
|
||||
count(*) FILTER (WHERE actor_kind = 'smoke' AND created_at >= now() - interval '90 days')::int AS "smokeEvents90d",
|
||||
count(*) FILTER (WHERE actor_kind = 'unknown' AND created_at >= now() - interval '30 days')::int AS "unknownEvents30d",
|
||||
count(*) FILTER (WHERE actor_kind = 'unknown' AND created_at >= now() - interval '90 days')::int AS "unknownEvents90d"
|
||||
FROM classified
|
||||
`;
|
||||
|
||||
function isSmokeLikeValue(value) {
|
||||
const text = String(value ?? "").trim();
|
||||
if (!text) return false;
|
||||
const lower = text.toLowerCase();
|
||||
const emailLocal = lower.includes("@") ? lower.split("@")[0] : lower;
|
||||
return (
|
||||
/^(smoke[-_.]|oa[-_.]?smoke[-_.]?)/u.test(emailLocal)
|
||||
|| /^(SMOKE|OSMOKE|WFULL|PRELAUNCH|LAUNCHAUTO|HRWDTEST)[A-Z0-9_-]*/iu.test(text)
|
||||
|| /测试|试运行/u.test(text)
|
||||
);
|
||||
}
|
||||
|
||||
function classifyFixtureActor(actor, forcedKind = null) {
|
||||
if (forcedKind === "system") return "system";
|
||||
if (!actor) return "unknown";
|
||||
if ([actor.employeeNo, actor.email, actor.name].some(isSmokeLikeValue)) return "smoke";
|
||||
return "human";
|
||||
}
|
||||
|
||||
function createReadOnlyAdapter(client) {
|
||||
return {
|
||||
featureFlag: {
|
||||
async findUnique({ where }) {
|
||||
const result = await client.query(
|
||||
"SELECT flag_key, override_mode, rollout_type, department_ids, note, version, created_at, created_by, updated_at, updated_by FROM feature_flags WHERE flag_key = $1",
|
||||
[where.flag_key],
|
||||
);
|
||||
return result.rows[0] ?? null;
|
||||
},
|
||||
async findMany({ where }) {
|
||||
const result = await client.query(
|
||||
"SELECT flag_key, override_mode, rollout_type, department_ids, note, version, created_at, created_by, updated_at, updated_by FROM feature_flags WHERE flag_key = ANY($1::text[]) ORDER BY flag_key ASC",
|
||||
[where.flag_key.in],
|
||||
);
|
||||
return result.rows;
|
||||
},
|
||||
},
|
||||
workspaceSetting: {
|
||||
async findFirst() {
|
||||
const result = await client.query(
|
||||
"SELECT profile, org_mode, primary_entry_mode, notification_strategy, device_priority, module_visibility_json, created_at, updated_at, updated_by FROM workspace_settings ORDER BY created_at ASC LIMIT 1",
|
||||
);
|
||||
return result.rows[0] ?? null;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function effectiveFlagState(item) {
|
||||
if (item.requiresDepartment && item.effectiveReason === "department_deferred") return "partial";
|
||||
return item.effectiveEnabled ? "on" : "off";
|
||||
}
|
||||
|
||||
function applyExposure(metrics, workspace, flags) {
|
||||
const flagState = new Map(flags.map((item) => [item.flagKey, effectiveFlagState(item)]));
|
||||
const moduleExposure = {
|
||||
portal_notifications: ["portal", "portal_home_v1"],
|
||||
workflow_approvals: ["workflowTasks", null],
|
||||
reimbursements_invoices: ["reimbursements", null],
|
||||
payments_ap: ["paymentBatches", "payment_batch_v1"],
|
||||
procurement: ["purchaseRequests", null],
|
||||
contracts: ["contractsRegistry", "project_linking_v1"],
|
||||
projects: ["projects", "project_domain_v1"],
|
||||
assets_inventory: ["assets", null],
|
||||
leave_wecom_history: ["leaveRequests", null],
|
||||
attendance_presence: ["attendance", "attendance_unified_v1"],
|
||||
weekly_reports: ["weeklyReports", "weekly_report_v1"],
|
||||
todos: ["todos", "todo_board_v1"],
|
||||
announcements: ["announcements", "announcements_v1"],
|
||||
budgets: ["budgets", null],
|
||||
travel: ["travelRequests", null],
|
||||
people_lifecycle: ["people", "employee_lifecycle_v1"],
|
||||
payroll: [null, null],
|
||||
performance: [null, null],
|
||||
system_configuration: ["featureFlags", null],
|
||||
integrations: [null, "erp_sync"],
|
||||
};
|
||||
|
||||
return metrics.map((metric) => {
|
||||
const [navigationKey, flagKey] = moduleExposure[metric.moduleKey] ?? [null, null];
|
||||
const navigationMode = navigationKey
|
||||
? workspace.moduleVisibility[navigationKey] ?? "unknown"
|
||||
: "no_navigation";
|
||||
const effectiveState = flagKey ? flagState.get(flagKey) ?? "unknown" : "not_applicable";
|
||||
return {
|
||||
...metric,
|
||||
latestActivityAt: metric.latestActivityAt?.toISOString?.() ?? metric.latestActivityAt ?? null,
|
||||
navigationMode,
|
||||
effectiveFlagState: effectiveState,
|
||||
queryStatus: "PASS",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function assertSafeResult(metrics) {
|
||||
if (!Array.isArray(metrics) || metrics.length !== MODULE_KEYS.length) {
|
||||
throw new Error("result_module_count_invalid");
|
||||
}
|
||||
const seen = new Set();
|
||||
for (const item of metrics) {
|
||||
if (!MODULE_KEYS.includes(item.moduleKey) || seen.has(item.moduleKey)) {
|
||||
throw new Error("result_module_key_invalid");
|
||||
}
|
||||
seen.add(item.moduleKey);
|
||||
const keys = Object.keys(item).sort();
|
||||
const expected = [...SAFE_RESULT_KEYS].sort();
|
||||
if (JSON.stringify(keys) !== JSON.stringify(expected)) {
|
||||
throw new Error("result_field_contract_invalid");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const appRoot = path.resolve(process.env.USAGE_CENSUS_APP_ROOT || process.cwd());
|
||||
const { Client } = require(path.join(appRoot, "node_modules/pg"));
|
||||
const { getConfig } = require(path.join(appRoot, "src/config.js"));
|
||||
const { listFeatureFlags } = require(path.join(appRoot, "src/services/featureFlags/index.js"));
|
||||
const { getWorkspaceSetting } = require(path.join(appRoot, "src/services/workspaceSettings.js"));
|
||||
const config = getConfig();
|
||||
const client = new Client({ connectionString: process.env.DATABASE_URL });
|
||||
let inTransaction = false;
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
await client.query("BEGIN TRANSACTION READ ONLY");
|
||||
inTransaction = true;
|
||||
await client.query("SET LOCAL statement_timeout = '30s'");
|
||||
await client.query("SET LOCAL lock_timeout = '2s'");
|
||||
const readOnly = await client.query("SELECT current_setting('transaction_read_only') AS value");
|
||||
if (readOnly.rows[0]?.value !== "on") throw new Error("transaction_not_read_only");
|
||||
|
||||
const adapter = createReadOnlyAdapter(client);
|
||||
const [metricResult, authResult, flags, workspace] = await Promise.all([
|
||||
client.query(USAGE_SQL),
|
||||
client.query(AUTH_SQL),
|
||||
listFeatureFlags(adapter, config),
|
||||
getWorkspaceSetting(adapter),
|
||||
]);
|
||||
const metrics = applyExposure(metricResult.rows, workspace, flags);
|
||||
assertSafeResult(metrics);
|
||||
await client.query("COMMIT");
|
||||
inTransaction = false;
|
||||
|
||||
process.stdout.write(`${JSON.stringify({
|
||||
status: "PASS",
|
||||
transactionReadOnly: true,
|
||||
sourceBuildSha: config.buildSha,
|
||||
generatedAt: new Date().toISOString(),
|
||||
authentication: authResult.rows[0],
|
||||
modules: metrics,
|
||||
}, null, 2)}\n`);
|
||||
} catch {
|
||||
if (inTransaction) {
|
||||
try { await client.query("ROLLBACK"); } catch { /* fail closed */ }
|
||||
}
|
||||
process.stderr.write("[usage-census][FAILED] code=QUERY_FAILED\n");
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
await client.end().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
AUTH_SQL,
|
||||
MODULE_KEYS,
|
||||
SAFE_RESULT_KEYS,
|
||||
USAGE_SQL,
|
||||
applyExposure,
|
||||
assertSafeResult,
|
||||
classifyFixtureActor,
|
||||
effectiveFlagState,
|
||||
isSmokeLikeValue,
|
||||
};
|
||||
|
||||
// `node -` names stdin as argv[1] and leaves require.main unset. Production
|
||||
// streams this task-scoped file through stdin so it never lands on the host.
|
||||
if (require.main === module || process.argv[1] === "-") {
|
||||
run();
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert/strict");
|
||||
const {
|
||||
AUTH_SQL,
|
||||
MODULE_KEYS,
|
||||
SAFE_RESULT_KEYS,
|
||||
USAGE_SQL,
|
||||
applyExposure,
|
||||
assertSafeResult,
|
||||
classifyFixtureActor,
|
||||
effectiveFlagState,
|
||||
isSmokeLikeValue,
|
||||
} = require("./usage-census.cjs");
|
||||
const fs = require("node:fs");
|
||||
|
||||
function assertSelectOnly(sql) {
|
||||
assert.doesNotMatch(sql, /\b(INSERT|UPDATE|DELETE|UPSERT|MERGE|ALTER|CREATE|DROP|TRUNCATE|CALL|COPY|GRANT|REVOKE)\b/iu);
|
||||
assert.doesNotMatch(sql, /\bFOR\s+UPDATE\b/iu);
|
||||
assert.match(sql, /^\s*WITH\b/iu);
|
||||
}
|
||||
|
||||
assertSelectOnly(USAGE_SQL);
|
||||
assertSelectOnly(AUTH_SQL);
|
||||
assert.equal(MODULE_KEYS.length, 20);
|
||||
assert.equal(new Set(MODULE_KEYS).size, MODULE_KEYS.length);
|
||||
assert.equal(new Set(SAFE_RESULT_KEYS).size, SAFE_RESULT_KEYS.length);
|
||||
assert.match(USAGE_SQL, /forced_kind = 'system'/u);
|
||||
assert.match(USAGE_SQL, /THEN 'smoke'/u);
|
||||
assert.match(USAGE_SQL, /THEN 'unknown'/u);
|
||||
assert.match(USAGE_SQL, /ELSE 'human'/u);
|
||||
assert.match(
|
||||
fs.readFileSync(require.resolve("./usage-census.cjs"), "utf8"),
|
||||
/require\.main === module \|\| process\.argv\[1\] === "-"/u,
|
||||
);
|
||||
|
||||
assert.equal(isSmokeLikeValue("[email protected]"), true);
|
||||
assert.equal(isSmokeLikeValue("[email protected]"), true);
|
||||
assert.equal(isSmokeLikeValue("SMOKE002"), true);
|
||||
assert.equal(isSmokeLikeValue("测试员工"), true);
|
||||
assert.equal(isSmokeLikeValue("[email protected]"), false);
|
||||
assert.equal(classifyFixtureActor(null), "unknown");
|
||||
assert.equal(classifyFixtureActor({ employeeNo: "E001", email: "[email protected]", name: "普通员工" }), "human");
|
||||
assert.equal(classifyFixtureActor({ employeeNo: "SMOKE002", email: "[email protected]", name: "普通员工" }), "smoke");
|
||||
assert.equal(classifyFixtureActor({ employeeNo: "E001", email: "[email protected]", name: "普通员工" }, "system"), "system");
|
||||
|
||||
assert.equal(effectiveFlagState({ requiresDepartment: true, effectiveReason: "department_deferred", effectiveEnabled: true }), "partial");
|
||||
assert.equal(effectiveFlagState({ requiresDepartment: false, effectiveReason: "flag_force_on_global", effectiveEnabled: true }), "on");
|
||||
assert.equal(effectiveFlagState({ requiresDepartment: false, effectiveReason: "flag_force_off", effectiveEnabled: false }), "off");
|
||||
|
||||
const zeroMetric = Object.fromEntries(SAFE_RESULT_KEYS
|
||||
.filter((key) => !["moduleKey", "latestActivityAt", "navigationMode", "effectiveFlagState", "queryStatus"].includes(key))
|
||||
.map((key) => [key, 0]));
|
||||
const rawMetrics = MODULE_KEYS.map((moduleKey) => ({
|
||||
moduleKey,
|
||||
...zeroMetric,
|
||||
latestActivityAt: null,
|
||||
auditEvents30d: 0,
|
||||
auditEvents90d: 0,
|
||||
auditHumanActors30d: 0,
|
||||
auditHumanActors90d: 0,
|
||||
auditHumanEvents30d: 0,
|
||||
auditHumanEvents90d: 0,
|
||||
auditSmokeEvents30d: 0,
|
||||
auditSmokeEvents90d: 0,
|
||||
auditUnknownEvents30d: 0,
|
||||
auditUnknownEvents90d: 0,
|
||||
}));
|
||||
const workspace = { moduleVisibility: {} };
|
||||
const exposed = applyExposure(rawMetrics, workspace, []);
|
||||
assert.equal(exposed.length, 20);
|
||||
assert.equal(exposed.find((item) => item.moduleKey === "payroll").navigationMode, "no_navigation");
|
||||
assert.equal(exposed.find((item) => item.moduleKey === "procurement").effectiveFlagState, "not_applicable");
|
||||
assert.equal(exposed.find((item) => item.moduleKey === "reimbursements_invoices").effectiveFlagState, "not_applicable");
|
||||
assert.equal(exposed.find((item) => item.moduleKey === "workflow_approvals").effectiveFlagState, "not_applicable");
|
||||
assert.equal(exposed.find((item) => item.moduleKey === "budgets").effectiveFlagState, "not_applicable");
|
||||
assertSafeResult(exposed);
|
||||
assert.throws(() => assertSafeResult(exposed.slice(1)), /result_module_count_invalid/u);
|
||||
assert.throws(
|
||||
() => assertSafeResult(exposed.map((item, index) => index === 0 ? { ...item, leakedField: "x" } : item)),
|
||||
/result_field_contract_invalid/u,
|
||||
);
|
||||
|
||||
process.stdout.write("usage-census contract: PASS\n");
|
||||
在新工单中引用
屏蔽一个用户