oaops1-patrol-feature-review 改进 OAOPS1 巡检报告传播与脱敏

这个提交包含在:
2026-05-26 14:03:51 +08:00
父节点 267240bd81
当前提交 d61b254064
共修改 15 个文件,包含 1448 行新增和 14 行删除
+87
查看文件
@@ -15,3 +15,90 @@ Start from `docs/文档索引.md` and `docs/文档事实源状态矩阵.md`.
- For generated evidence, store report paths and conclusions, not raw secrets,
raw payloads, full account identifiers, invoice originals or confidential
business content.
## Scenario: QA Readiness Report Summaries
### 1. Scope / Trigger
- Trigger: QA shell runners aggregate child reports for launch, patrol,
smoke or readiness gates.
- Applies to generated `summary.json`, step JSON and privacy findings under
`artifacts/qa-flow/` or Trellis task research directories.
### 2. Signatures
- Parent shell runner writes `summary.json`.
- Child runner summary input: `<child-report-dir>/summary.json`.
- Step summary input/output: `<step-name>.step.json`.
### 3. Contracts
- Use repo-relative `reportRef` / `logRef` when a path is inside the repo.
- Do not write absolute local workspace paths into `summary.json`.
- Parent summaries must include child report status and child finding counts.
- Parent `findingCount` must become non-zero when any child summary reports
`PASS_WITH_FINDINGS`, `SKIP`, `PARTIAL`, `PAUSED`, `MISSING`, blocker-like
status or child `findingCount > 0`.
- Machine-readable reports may include status, count, mode, conclusion,
boolean operational flags and relative report refs.
- Machine-readable reports must not include amounts, prices, costs, balances,
bank details, invoice values, full accounts, tokens, secrets, raw payloads,
raw form values or full local paths.
### 4. Validation & Error Matrix
- Child `PASS` + `findingCount=0` -> parent may stay pass.
- Child `PASS_WITH_FINDINGS` -> parent records a finding.
- Child `SKIP` / `PARTIAL` / `PAUSED` / `MISSING` -> parent records
`manual_or_config`.
- Child `FAIL` / `BLOCKED` / `ERROR` -> parent records a finding or blocker
according to whether the parent step is required.
- Privacy smoke match -> write redacted finding metadata and make final
summary reflect the failure.
### 5. Good/Base/Bad Cases
- Good: `summary.json` contains `reportRef`, `childReports.<name>.status`,
`childReports.<name>.findingCount` and no absolute local path.
- Base: Missing runtime credentials produce `PASS_WITH_FINDINGS` or
`manual_or_config`, not a clean pass.
- Bad: Parent command exits 0 for a child `PASS_WITH_FINDINGS` and the parent
summary still says clean pass.
### 6. Tests Required
- `bash -n <runner>.sh`
- Run the parent runner in a missing-credential scenario and assert:
- parent status is `PASS_WITH_FINDINGS` or blocked, not clean pass;
- child finding count is visible in parent summary;
- privacy step is present in parent summary;
- `summary.json` has no full workspace path or business amount fields.
- Run the matching launch/readiness aggregator when it consumes the report.
### 7. Wrong vs Correct
#### Wrong
```json
{
"status": "PASS_OA_REAL_USE_DAILY_READY",
"reportDir": "/home/user/project/artifacts/qa-flow/run",
"childReports": {}
}
```
#### Correct
```json
{
"status": "PASS_WITH_FINDINGS",
"reportRef": "artifacts/qa-flow/run",
"childReports": {
"oaOnlySmokeReadiness": {
"status": "PASS_WITH_FINDINGS",
"reportRef": "artifacts/qa-flow/run/oa-only-smoke-readiness/summary.json",
"findingCount": 7
}
}
}
```
@@ -0,0 +1,4 @@
{"file": ".trellis/spec/hyb-oa/project-governance.md", "reason": "Check review stayed within approved Trellis task scope."}
{"file": ".trellis/spec/hyb-oa/deployment-trialrun.md", "reason": "Check no public write/deployment/payment/database boundary was crossed."}
{"file": ".trellis/spec/hyb-oa/docs-and-evidence.md", "reason": "Check findings and report references avoid raw evidence leakage."}
{"file": ".trellis/spec/hyb-oa/security-privacy.md", "reason": "Check privacy findings for path, secret and sensitive business data exposure."}
@@ -0,0 +1,4 @@
{"file": ".trellis/spec/hyb-oa/project-governance.md", "reason": "Review task governance and approval boundaries."}
{"file": ".trellis/spec/hyb-oa/deployment-trialrun.md", "reason": "Public OA patrol, deployment and real-data operation boundaries."}
{"file": ".trellis/spec/hyb-oa/docs-and-evidence.md", "reason": "Evidence/report handling requirements."}
{"file": ".trellis/spec/hyb-oa/security-privacy.md", "reason": "Sensitive data and full local path exclusion rules for patrol reports."}
@@ -0,0 +1,37 @@
# OAOPS1 Patrol Review Execution
## What Was Reviewed
This task reviewed patrol/readiness behavior only. No production operation was executed.
Reviewed files:
- `scripts/qa/run-oa-real-use-daily.sh`
- `scripts/qa/run-oa-final-launch-readiness.sh`
- `scripts/qa/check-oa-only-smoke-readiness.mjs`
- `docs/管理员手册.md`
- `docs/文档事实源状态矩阵.md`
## Verification
Commands run:
- `bash -n scripts/qa/run-oa-real-use-daily.sh`
- `bash -n scripts/qa/run-oa-final-launch-readiness.sh`
- `python3 ./.trellis/scripts/task.py validate 05-26-oaops1-patrol-feature-review`
- `node scripts/qa/check-trellis-governance.mjs`
- `git diff --check`
- `OALAUNCH_REPORT_DIR=.trellis/tasks/05-26-oaops1-patrol-feature-review/research/oalaunch-after-patrol-fix bash scripts/qa/run-oa-final-launch-readiness.sh`
- `timeout 240s env OAOPS1_REPORT_DIR=.trellis/tasks/05-26-oaops1-patrol-feature-review/research/oaops1-after-patrol-fix-final OAOPS1_SKIP_ENV_FILE=1 OAOPS1_LOGIN_STEP_COOLDOWN_SECONDS=0 OAOPS1_E2E_COOLDOWN_SECONDS=0 bash scripts/qa/run-oa-real-use-daily.sh` with escalated read-only network/browser permissions.
Initial governance check failed only because this complex review task was missing `implement.md`. This file records that execution note.
## Result
The patrol feature was improved before starting the formal 7-day OAOPS1 window.
See `info.md` for findings, implementation details and validation evidence.
## Spec Update Decision
The implementation changed reusable QA report behavior. Updated `.trellis/spec/hyb-oa/docs-and-evidence.md` with the QA readiness report summary contract.
@@ -0,0 +1,155 @@
# OAOPS1 Patrol Review Findings
## Review Scope
Reviewed:
- `scripts/qa/run-oa-real-use-daily.sh`
- `scripts/qa/run-oa-final-launch-readiness.sh`
- `scripts/qa/check-oa-only-smoke-readiness.mjs`
- `docs/管理员手册.md`
- `docs/文档事实源状态矩阵.md`
Commands run:
- `bash -n scripts/qa/run-oa-real-use-daily.sh`
- `bash -n scripts/qa/run-oa-final-launch-readiness.sh`
No deployment, public OA writes, Enterprise WeChat writes, payment actions or database restore were performed.
## Findings
### High: Daily patrol can hide child `PASS_WITH_FINDINGS` results
`run-oa-real-use-daily.sh` wraps child checks with `run_finding_step`, which marks a step as `PASS` whenever the command exits 0. `check-oa-only-smoke-readiness.mjs` returns exit 0 when role credentials are missing but emits summary status `PASS_WITH_FINDINGS`. The parent summary only inspects parent step status plus the local authenticated probe, so child `PASS_WITH_FINDINGS` can be lost and the daily patrol can conclude `PASS_OA_REAL_USE_DAILY_READY`.
Why it matters:
- `docs/管理员手册.md` requires missing live smoke credentials to be recorded as finding or SKIP, not disguised as pass.
- OALAUNCH1 reads `latestOaops.findingCount`; if the daily report loses child findings, final readiness can understate risk.
Evidence:
- `scripts/qa/run-oa-real-use-daily.sh` lines 63-66 mark a child command as `PASS` only by exit code.
- `scripts/qa/check-oa-only-smoke-readiness.mjs` lines 253-259 emits `PASS_WITH_FINDINGS` when credentials are skipped.
- `scripts/qa/check-oa-only-smoke-readiness.mjs` line 288 exits non-zero only for blocking failures.
- `scripts/qa/run-oa-real-use-daily.sh` lines 412-426 build findings without reading the child summary statuses.
Recommended fix:
- Add a report-aware step wrapper for child reports. After each child command, read its `summary.json`; promote `PASS_WITH_FINDINGS`, `SKIP`, `PARTIAL`, `PAUSED` and child `findingCount > 0` into parent findings.
- Include `oa-only-smoke-readiness` in `childReports`, not just `live-readiness`.
### High: Privacy smoke failure can leave `summary.json` saying PASS
`run-oa-real-use-daily.sh` writes `summary.json` before running `report-privacy-smoke`. If privacy smoke fails, it sets `STATUS=1` and exits non-zero, but the already-written summary is not regenerated. The report can therefore contain a PASS conclusion while the script fails.
Why it matters:
- Day 7 review is driven by `summary.json`.
- A privacy violation must be visible in the machine-readable report, not only in process exit code or console output.
Evidence:
- `scripts/qa/run-oa-real-use-daily.sh` lines 505-510 write summary, then run privacy smoke, then exit.
- `scripts/qa/run-oa-final-launch-readiness.sh` lines 483-486 already has the better pattern: rewrite summary when privacy status changes.
Recommended fix:
- Mirror the OALAUNCH1 pattern in OAOPS1: run privacy smoke after summary, then rewrite summary if status changed.
- Add an explicit `report-privacy-smoke` step/finding to the daily summary.
### Medium: Privacy smoke prints raw matches to stdout
OAOPS1 privacy smoke prints the full grep matches when it detects sensitive patterns. If the match is a token, phone number, email or raw key line, the scanner itself leaks it into terminal/CI logs.
Why it matters:
- Privacy tooling should not echo the sensitive value it detected.
- This violates the current report discipline: status/count/path summaries only.
Evidence:
- `scripts/qa/run-oa-real-use-daily.sh` lines 347-351 capture and print raw grep matches.
Recommended fix:
- Store only redacted match metadata: file path, line number and rule name.
- Do not print full matched lines.
### Medium: Daily summary writes full local paths
OAOPS1 step JSON and summary use absolute local paths for `logPath` and `reportDir`. Current project privacy rules prohibit complete local paths in task/docs/reports.
Why it matters:
- The final launch script already uses repo-relative `reportRef` for this reason.
- These daily reports may be copied into Trellis task evidence or user-facing summaries.
Evidence:
- `scripts/qa/run-oa-real-use-daily.sh` line 35 writes absolute `logPath`.
- `scripts/qa/run-oa-real-use-daily.sh` line 380 writes absolute missing-step `logPath`.
- `scripts/qa/run-oa-real-use-daily.sh` line 443 writes absolute `reportDir`.
- `scripts/qa/run-oa-final-launch-readiness.sh` lines 160-167 use safer relative `reportRef`.
Recommended fix:
- Add a `reportRef(file)` helper to OAOPS1 and use repo-relative paths when possible.
- Replace `reportDir` with `reportRef` or keep `reportDirName` only.
### Medium: Read-only probe can record business totals, not only counts/status
The authenticated read-only probe records any short string field that is not excluded by a narrow key regex. In observed evidence, reimbursement aggregate amount appears in the patrol report. The report policy says generated evidence should keep status/count/report-path summaries, not business amounts.
Why it matters:
- Aggregate amounts can still be sensitive business information in a small team.
- OALAUNCH evidence should remain safe to copy into task notes.
Evidence:
- `scripts/qa/run-oa-real-use-daily.sh` lines 197-207 allow numeric/boolean values and short strings.
- Observed latest daily report includes a reimbursement `totalAmount` aggregate in the authenticated probe summary.
Recommended fix:
- Change `summarizePayload` to an allowlist: item counts, totals as record counts only, status/mode/conclusion and booleans that are operational states.
- Drop amount, money, balance, price, cost, bank and invoice-value-like fields.
## Overall Assessment
The patrol function is directionally useful and already covers the right operational domains: public readiness, live smoke, feature flags, OA-only roles, read-only business probes, mobile/WeCom entry and privacy checks.
Before starting the formal 7-day OAOPS1 window, it should be improved in a small technical task. The most important fix is report-status correctness: parent reports must faithfully include child findings and privacy failures.
## Suggested Improvement Order
1. Fix child summary propagation and parent `findingCount`.
2. Fix privacy-smoke summary rewrite and redacted match output.
3. Replace absolute paths with report refs.
4. Reduce authenticated probe payload summaries to operational counts/status only.
5. Re-run `bash -n`, default OAOPS1 daily, and default OALAUNCH1 readiness.
## Implementation Result
Implemented in `scripts/qa/run-oa-real-use-daily.sh`:
- Step JSON now writes repo-relative `logRef` instead of absolute `logPath`.
- Daily summary now writes `reportRef` instead of absolute `reportDir`.
- Parent daily summary reads child `summary.json` for `live-smoke-readiness` and `oa-only-smoke-readiness`.
- Child `PASS_WITH_FINDINGS`, `SKIP`, `MISSING`, `PAUSED`, `PARTIAL`, blocker statuses and child finding counts now contribute to parent findings.
- Privacy smoke now writes redacted `privacy-findings.txt` metadata instead of printing raw grep matches.
- Privacy smoke now records a `report-privacy-smoke` step and summary is rewritten after privacy checks.
- Authenticated probe payload summary now uses an allowlist and excludes amount, money, price, cost, balance, bank, receipt, account, invoice, tax, fee, paid and payment fields.
Final validation report:
- `.trellis/tasks/05-26-oaops1-patrol-feature-review/research/oaops1-after-patrol-fix-final/summary.json`
- Status: `PASS_WITH_FINDINGS`
- Parent finding count: 4
- `childReports.oaOnlySmokeReadiness.status`: `PASS_WITH_FINDINGS`
- `childReports.oaOnlySmokeReadiness.findingCount`: 7
- `report-privacy-smoke`: `PASS`
- No full local path matches in `summary.json`.
- No business amount key matches in `summary.json` or `authenticated-readonly-probe.json`.
@@ -0,0 +1,55 @@
# OAOPS1 patrol feature review
## Goal
审查当前 OAOPS1/OALAUNCH1 巡检功能是否足以支撑 7 天真实使用稳定化和最终启用 Go/No-Go,判断是否需要改进,并把结论、风险和建议记录到当前 Trellis task。
## Scope
- 只读审查巡检相关脚本、输出报告结构和事实源要求。
- 重点覆盖 `scripts/qa/run-oa-real-use-daily.sh` 和 `scripts/qa/run-oa-final-launch-readiness.sh`。
- 必要时参考管理员手册、文档索引、事实源状态矩阵和 Trellis spec。
- 用户确认“开始改进”后,允许在本 task 内对巡检报告逻辑做最小代码改进。
- 不部署、不跑公网写入、不写企业微信、不执行付款或数据库恢复。
## What I Already Know
- OALAUNCH1 当前结论为 `PAUSED_MANUAL_CONFIRMATION`,主要缺口在人工证据、7 天稳定化、权限矩阵、备份恢复演练和 Day 7 Go/No-Go。
- OAOPS1 每日巡检脚本默认只读,真实业务写入由人工完成。
- OALAUNCH1 最终启用脚本默认只聚合证据,不创建业务数据、不执行付款、不写企业微信、不做数据库恢复。
## Review Questions
- 当前巡检是否能稳定证明 OAOPS1 的真实使用状态?
- 当前巡检是否能让 Day 7 决策人快速知道阻断项、finding 和人工动作?
- 当前巡检是否有误报/漏报、敏感信息、可维护性或长期运行问题?
- 是否有必要在进入 7 天真实使用前先改进巡检脚本?
## Acceptance Criteria
- [ ] 列出明确 findings,按严重程度排序,并引用具体文件/行号。
- [ ] 说明哪些问题必须在 7 天真实使用前修,哪些可后置。
- [ ] 给出建议实施顺序和验证命令。
- [ ] 子报告 `PASS_WITH_FINDINGS` / `SKIP` / `PARTIAL` / `PAUSED` 能进入父巡检 summary。
- [ ] 隐私扫描失败能进入机器可读 summary,且不打印原始敏感命中行。
- [ ] 巡检 summary 使用相对 report ref,不写完整本地路径。
- [ ] 只读探针摘要不输出业务金额、成本、银行、票据等敏感业务值。
- [ ] 不泄露 Secret、完整账号、合同正文、银行资料、完整本地路径或原始 payload。
## Out Of Scope
- 不执行部署。
- 不写公网业务数据。
- 不执行付款。
- 不写企业微信。
- 不做生产数据库恢复或重置。
- 不直接启用 WREAL1 或深度镜像。
## Test Plan If Changes Are Approved Later
- `bash -n scripts/qa/run-oa-real-use-daily.sh`
- `bash -n scripts/qa/run-oa-final-launch-readiness.sh`
- `bash scripts/qa/run-oa-real-use-daily.sh`,默认只读,按凭据缺失可解释为 finding/SKIP。
- `bash scripts/qa/run-oa-final-launch-readiness.sh`,默认只读。
- `node scripts/qa/check-trellis-governance.mjs`
- `git diff --check`
@@ -0,0 +1,22 @@
# OALAUNCH1 最终启用人工门禁
## Day 7 Go/No-Go
- A:接受 `PASS_OA_LAUNCH_READY`,进入小团队正式启用。
- B:接受 `PASS_WITH_FINDINGS`,但必须指定 owner、处理日期和接受理由。
- C:暂停启用,先整改。
## 阻断处理
- A:当天新建修复合同并修复。
- B:临时人工绕行,记录风险、补录责任人和补录方式。
- C:暂停对应流程。
## 备份恢复演练
- A:接受恢复演练结果,进入 Go/No-Go。
- B:修复后重跑恢复演练。
- C:暂停正式启用。
## 必须补齐的业务证据
- 至少 1 条真实报销 PDF OCR -> 审批 -> 付款批次 -> 银行回单 -> 已付款 -> AP 导出。
- 至少 1 条采购/费用审批并关联项目或合同。
- 至少 1 条合同台账含归档位置和权限检查。
- 资产台账 ASSET1D 已完成后,补真实资产责任人或保管人抽查。
@@ -0,0 +1,400 @@
{
"issueId": "OALAUNCH1",
"mode": "oa-final-launch-readiness",
"status": "PAUSED_MANUAL_CONFIRMATION",
"conclusion": "PAUSED_MANUAL_CONFIRMATION",
"baseURL": "https://oa.hyinda.com",
"commit": "267240b",
"checkedAt": "2026-05-26T05:54:06.208Z",
"reportRef": ".trellis/tasks/05-26-oaops1-patrol-feature-review/research/oalaunch-final-after-patrol-fix",
"scope": {
"launchScope": "P0 + light P1",
"stabilizationDaysRequired": 7,
"smallTeamFormalUse": true,
"wecomHrAttendanceStatus": "frozen_at_wecom_owned_oa_archive_audit"
},
"evidence": {
"latestOaops": {
"name": "oa-real-use-daily-20260525-205848",
"reportRef": "artifacts/qa-flow/oa-real-use-daily-20260525-205848/summary.json",
"status": "PASS_OA_REAL_USE_DAILY_READY",
"findingCount": 0
},
"latestAssetTrial": {
"name": "asset1d-public-trial-20260523-155346",
"reportRef": "artifacts/qa-flow/asset1d-public-trial-20260523-155346/summary.json",
"status": "PASS_ASSET_UI_TRIAL_READY",
"findingCount": 0
},
"latestAssetImport": {
"name": "asset-redacted-index-import-live-20260523-162358",
"reportRef": "artifacts/qa-flow/asset-redacted-index-import-live-20260523-162358/summary.json",
"status": "PASS_EA_DOC_IMPORT_READY",
"findingCount": 0
},
"backupEvidence": null,
"permissionEvidence": null,
"businessEvidence": null,
"policyEvidence": {
"provided": true,
"exists": true,
"ref": "issues/2026-05-24_01-20-52-oa-policy-system-alignment-v1.csv"
},
"stabilization": {
"status": "MISSING_START_DATE",
"startDay": null,
"requiredDays": [],
"observedDays": [
"20260522",
"20260523",
"20260524",
"20260525"
],
"dayReports": {}
}
},
"issueStatus": {
"oaopsRequired": {
"OAOPS1-3": {
"title": "报销 PDF OCR 与付款闭环真实使用门禁",
"dev": "TODO",
"review": "TODO",
"regression": "TODO",
"done": false
},
"OAOPS1-4": {
"title": "采购费用与项目关联真实使用门禁",
"dev": "TODO",
"review": "TODO",
"regression": "TODO",
"done": false
},
"OAOPS1-5": {
"title": "合同台账与合同归档真实使用门禁",
"dev": "TODO",
"review": "TODO",
"regression": "TODO",
"done": false
},
"OAOPS1-6": {
"title": "每日问题登记与阻断即修机制",
"dev": "TODO",
"review": "TODO",
"regression": "TODO",
"done": false
}
},
"oalaunchRequired": [
"OALAUNCH1-0",
"OALAUNCH1-1",
"OALAUNCH1-2",
"OALAUNCH1-3",
"OALAUNCH1-4",
"OALAUNCH1-5",
"OALAUNCH1-6",
"OALAUNCH1-7",
"OALAUNCH1-8"
],
"oalaunchStatuses": {
"OALAUNCH1-0": {
"title": "建立最终启用 Go/No-Go 合同",
"dev": "DONE",
"review": "DONE",
"regression": "DONE",
"done": true
},
"OALAUNCH1-1": {
"title": "新增最终启用证据汇总脚本",
"dev": "DONE",
"review": "DONE",
"regression": "DONE",
"done": true
},
"OALAUNCH1-2": {
"title": "回写最终启用事实源和操作清单",
"dev": "DONE",
"review": "DONE",
"regression": "DONE",
"done": true
},
"OALAUNCH1-3": {
"title": "OAOPS1 真实业务闭环收口门禁",
"dev": "TODO",
"review": "TODO",
"regression": "TODO",
"done": false
},
"OALAUNCH1-4": {
"title": "7 天稳定化与每日巡检门禁",
"dev": "TODO",
"review": "TODO",
"regression": "TODO",
"done": false
},
"OALAUNCH1-5": {
"title": "权限矩阵最终验收",
"dev": "DONE",
"review": "DONE",
"regression": "DONE",
"done": true
},
"OALAUNCH1-6": {
"title": "备份恢复演练门禁",
"dev": "TODO",
"review": "TODO",
"regression": "TODO",
"done": false
},
"OALAUNCH1-7": {
"title": "数据治理培训与运营支持门禁",
"dev": "TODO",
"review": "TODO",
"regression": "TODO",
"done": false
},
"OALAUNCH1-8": {
"title": "OALAUNCH1 本地门禁与收口",
"dev": "DONE",
"review": "DONE",
"regression": "DONE",
"done": true
}
},
"opolicyRequired": [
"OPOLICY1-0",
"OPOLICY1-1",
"OPOLICY1-2",
"OPOLICY1-3",
"OPOLICY1-4",
"OPOLICY1-5",
"OPOLICY1-6",
"OPOLICY1-7"
],
"opolicyStatuses": {
"OPOLICY1-0": {
"title": "建立制度与 OA 启用门禁对齐合同",
"dev": "DONE",
"review": "DONE",
"regression": "DONE",
"done": true
},
"OPOLICY1-1": {
"title": "审视制度合理性和小团队裁剪口径",
"dev": "DONE",
"review": "DONE",
"regression": "DONE",
"done": true
},
"OPOLICY1-2": {
"title": "调整制度包为小团队 V1 试运行口径",
"dev": "DONE",
"review": "DONE",
"regression": "DONE",
"done": true
},
"OPOLICY1-3": {
"title": "回写 hyb-oa 事实源和员工/管理员口径",
"dev": "DONE",
"review": "DONE",
"regression": "DONE",
"done": true
},
"OPOLICY1-4": {
"title": "制度参数和人工问答门禁清单",
"dev": "DONE",
"review": "DONE",
"regression": "DONE",
"done": true
},
"OPOLICY1-5": {
"title": "评估 OA 必要小改范围",
"dev": "DONE",
"review": "DONE",
"regression": "DONE",
"done": true
},
"OPOLICY1-6": {
"title": "OALAUNCH1 数据治理证据接入",
"dev": "DONE",
"review": "DONE",
"regression": "DONE",
"done": true
},
"OPOLICY1-7": {
"title": "本地门禁与文档审计",
"dev": "DONE",
"review": "DONE",
"regression": "DONE",
"done": true
}
},
"assetDone": true
},
"gates": {
"openCount": 13,
"openGates": [
{
"gate": "OAOPS1-3",
"reason": "OAOPS1 真实业务闭环仍未完成",
"state": {
"title": "报销 PDF OCR 与付款闭环真实使用门禁",
"dev": "TODO",
"review": "TODO",
"regression": "TODO",
"done": false
}
},
{
"gate": "OAOPS1-4",
"reason": "OAOPS1 真实业务闭环仍未完成",
"state": {
"title": "采购费用与项目关联真实使用门禁",
"dev": "TODO",
"review": "TODO",
"regression": "TODO",
"done": false
}
},
{
"gate": "OAOPS1-5",
"reason": "OAOPS1 真实业务闭环仍未完成",
"state": {
"title": "合同台账与合同归档真实使用门禁",
"dev": "TODO",
"review": "TODO",
"regression": "TODO",
"done": false
}
},
{
"gate": "OAOPS1-6",
"reason": "OAOPS1 真实业务闭环仍未完成",
"state": {
"title": "每日问题登记与阻断即修机制",
"dev": "TODO",
"review": "TODO",
"regression": "TODO",
"done": false
}
},
{
"gate": "OALAUNCH1-3",
"reason": "OALAUNCH1 本轮合同/脚本/事实源/门禁尚未全部回填 DONE",
"state": {
"title": "OAOPS1 真实业务闭环收口门禁",
"dev": "TODO",
"review": "TODO",
"regression": "TODO",
"done": false
}
},
{
"gate": "OALAUNCH1-4",
"reason": "OALAUNCH1 本轮合同/脚本/事实源/门禁尚未全部回填 DONE",
"state": {
"title": "7 天稳定化与每日巡检门禁",
"dev": "TODO",
"review": "TODO",
"regression": "TODO",
"done": false
}
},
{
"gate": "OALAUNCH1-6",
"reason": "OALAUNCH1 本轮合同/脚本/事实源/门禁尚未全部回填 DONE",
"state": {
"title": "备份恢复演练门禁",
"dev": "TODO",
"review": "TODO",
"regression": "TODO",
"done": false
}
},
{
"gate": "OALAUNCH1-7",
"reason": "OALAUNCH1 本轮合同/脚本/事实源/门禁尚未全部回填 DONE",
"state": {
"title": "数据治理培训与运营支持门禁",
"dev": "TODO",
"review": "TODO",
"regression": "TODO",
"done": false
}
},
{
"gate": "SEVEN_DAY_STABILIZATION",
"reason": "缺 7 天稳定化起始日期;设置 OALAUNCH_STABILIZATION_START_DATE=YYYY-MM-DD 后按连续 7 天检查",
"stabilization": {
"status": "MISSING_START_DATE",
"startDay": null,
"requiredDays": [],
"observedDays": [
"20260522",
"20260523",
"20260524",
"20260525"
],
"dayReports": {}
}
},
{
"gate": "BACKUP_RESTORE_DRILL",
"reason": "缺备份恢复演练或影子库恢复证据;可用 OALAUNCH_BACKUP_EVIDENCE 指定脱敏证据路径"
},
{
"gate": "PERMISSION_MATRIX",
"reason": "缺最终权限矩阵负例/角色入口证据;可用 OALAUNCH_PERMISSION_EVIDENCE 指定脱敏证据路径"
},
{
"gate": "BUSINESS_ACCEPTANCE_EVIDENCE",
"reason": "缺真实业务闭环证据;可用 OALAUNCH_BUSINESS_EVIDENCE 指定脱敏证据路径"
},
{
"gate": "DAY7_GO_NO_GO",
"reason": "缺 Day 7 人工 Go/No-Go 接受结论;可用 OALAUNCH_MANUAL_ACCEPT=PASS_OA_LAUNCH_READY 或 PASS_WITH_FINDINGS 指定"
}
],
"findings": []
},
"manualGates": {
"day7GoNoGo": [
"A:接受 PASS_OA_LAUNCH_READY,进入小团队正式启用。",
"B:接受 PASS_WITH_FINDINGS,但指定 owner 和完成日期后启用。",
"C:暂停启用,先整改。"
],
"blockerHandling": [
"A:当天新建修复合同并修复。",
"B:临时人工绕行,记录风险。",
"C:暂停对应流程。"
],
"backupDrill": [
"A:接受恢复演练结果,进入 Go/No-Go。",
"B:修复后重跑恢复演练。",
"C:暂停正式启用。"
]
},
"boundaries": {
"defaultReadOnly": true,
"noBusinessWritesByThisScript": true,
"noWeComWrites": true,
"noPaymentExecution": true,
"noDbResetOrRestore": true,
"frozenItems": [
"image_ocr",
"complex_bpm",
"budget_control",
"complex_procurement",
"e_signature",
"full_e_archive",
"erp_write",
"bank_statement_import",
"ai_rule_writes",
"real_payment_provider",
"wecom_hr_deep_mirror",
"emergency_takeover",
"recovery_writeback",
"ruleops"
]
},
"privacy": "Report keeps paths/status/counts only; no passwords, tokens, Secrets, full accounts, contract bodies, bank statements, raw invoice images, or raw payloads."
}
@@ -0,0 +1,96 @@
{
"mode": "oaops1-authenticated-readonly-probe",
"status": "PASS_WITH_FINDINGS",
"baseUrl": "https://oa.hyinda.com",
"checkedAt": "2026-05-26T05:49:02.580Z",
"readOnly": true,
"duplicateRoleChecks": "employee/finance/payment/AP are covered by oa-only-smoke-readiness to avoid repeated login throttling",
"roles": {
"admin": {
"status": "SKIP",
"reason": "missing_admin_credentials",
"maskedIdentifier": null
}
},
"checks": [
{
"id": "portal",
"role": "admin",
"path": "/api/portal/home",
"domain": "portal",
"status": "SKIP",
"reason": "missing_admin_credentials"
},
{
"id": "workflow-tasks",
"role": "admin",
"path": "/api/workflow/tasks?page=1&pageSize=5",
"domain": "approval",
"status": "SKIP",
"reason": "missing_admin_credentials"
},
{
"id": "reimbursements",
"role": "admin",
"path": "/api/reimbursements?page=1&pageSize=5",
"domain": "reimbursement",
"status": "SKIP",
"reason": "missing_admin_credentials"
},
{
"id": "reimbursement-summary",
"role": "admin",
"path": "/api/reimbursements/stats/summary",
"domain": "reimbursement",
"status": "SKIP",
"reason": "missing_admin_credentials"
},
{
"id": "invoice-pool",
"role": "admin",
"path": "/api/invoice-pool?page=1&pageSize=5",
"domain": "invoice_ocr",
"status": "SKIP",
"reason": "missing_admin_credentials"
},
{
"id": "purchase-requests",
"role": "admin",
"path": "/api/purchase-requests?page=1&pageSize=5",
"domain": "procurement",
"status": "SKIP",
"reason": "missing_admin_credentials"
},
{
"id": "contracts-registry",
"role": "admin",
"path": "/api/contracts-registry",
"domain": "contracts",
"status": "SKIP",
"reason": "missing_admin_credentials"
},
{
"id": "projects",
"role": "admin",
"path": "/api/projects?page=1&pageSize=5",
"domain": "projects",
"status": "SKIP",
"reason": "missing_admin_credentials"
},
{
"id": "audit-logs",
"role": "admin",
"path": "/api/audit-logs?page=1&pageSize=5",
"domain": "audit",
"status": "SKIP",
"reason": "missing_admin_credentials"
}
],
"boundaries": {
"noBusinessWrites": true,
"noExportsByDefault": true,
"noFileUploads": true,
"noWeComWrites": true,
"noRealPaymentExecution": true
}
}
@@ -0,0 +1,11 @@
{
"issueId": "OAOPS1",
"baseURL": "https://oa.hyinda.com",
"commit": "267240b",
"checkedAt": "2026-05-26T05:49:02.138Z",
"status": "PASS",
"mode": "live-smoke-readiness",
"reportDir": ".trellis/tasks/05-26-oaops1-patrol-feature-review/research/oaops1-after-patrol-fix-final/live-smoke-readiness",
"groups": null,
"notes": "公网 /ready、public bundle、pending OCR、live smoke 和真实 OCR 可用性检查;缺少凭据或样票时按脚本规则记录 SKIP。"
}
@@ -0,0 +1,20 @@
# OAOPS1 每日人工收口清单
## 员工
- 真实报销:上传 PDF 发票,补充必要附件,提交 OA 审批。
- 采购/费用:在 OA 创建申请,关联项目;如涉及合同,关联或补录合同台账。
- 合同:只录入合同摘要、编号、负责人、项目、金额、线下归档位置;涉密合同正文、敏感报价和成本细节不上传公网 OA。
## 审批人
- 从 OA 待办进入详情,确认报销、采购、合同或项目上下文后再审批。
- 退回时写清需要补充的材料,避免线下口头说明替代系统记录。
## 出纳/财务
- 审批通过后创建付款批次,线下银行渠道付款。
- 上传银行回单附件后再在 OA 标记已付款。
- 每日抽查 AP 台账、报销导出、票据状态和付款批次状态。
## 管理员
- 查看本报告 summary.json 和人工问题登记。
- 阻断项当天建修复 Issue CSV 或暂停对应流程;非阻断体验问题进入反馈池。
- 确认 payment_batch_v1=force_on,erp_sync=force_off;图片 OCR、预算、ERP、银行流水导入、真实支付直连继续冻结。
@@ -0,0 +1,168 @@
{
"issueId": "OSMOKE1",
"mode": "oa-only-smoke-readiness",
"status": "PASS_WITH_FINDINGS",
"conclusion": "PASS_WITH_FINDINGS",
"baseUrl": "https://oa.hyinda.com",
"checkedAt": "2026-05-26T05:49:02.288Z",
"businessReadOnly": true,
"readOnlyNote": "performs login/readiness probes only; no business data writes and no WeCom writes",
"privacy": "role/masked identifier/status/counts only; no password/token/secret/full email/full phone",
"targets": [
{
"key": "admin",
"expectedRoles": [
"admin",
"employee"
],
"maskedIdentifier": null,
"login": {
"status": "SKIP",
"reason": "missing_credentials",
"code": null
},
"roleMatch": "SKIP",
"probes": []
},
{
"key": "employee",
"expectedRoles": [
"employee"
],
"maskedIdentifier": null,
"login": {
"status": "SKIP",
"reason": "missing_credentials",
"code": null
},
"roleMatch": "SKIP",
"probes": []
},
{
"key": "manager",
"expectedRoles": [
"manager",
"employee"
],
"maskedIdentifier": null,
"login": {
"status": "SKIP",
"reason": "missing_credentials",
"code": null
},
"roleMatch": "SKIP",
"probes": []
},
{
"key": "ceo",
"expectedRoles": [
"ceo",
"employee"
],
"maskedIdentifier": null,
"login": {
"status": "SKIP",
"reason": "missing_credentials",
"code": null
},
"roleMatch": "SKIP",
"probes": []
},
{
"key": "finance",
"expectedRoles": [
"finance",
"employee"
],
"maskedIdentifier": null,
"login": {
"status": "SKIP",
"reason": "missing_credentials",
"code": null
},
"roleMatch": "SKIP",
"probes": []
},
{
"key": "finance_mgr",
"expectedRoles": [
"finance",
"finance_mgr",
"employee"
],
"maskedIdentifier": null,
"login": {
"status": "SKIP",
"reason": "missing_credentials",
"code": null
},
"roleMatch": "SKIP",
"probes": []
},
{
"key": "hr",
"expectedRoles": [
"hr",
"employee"
],
"maskedIdentifier": null,
"login": {
"status": "SKIP",
"reason": "missing_credentials",
"code": null
},
"roleMatch": "SKIP",
"probes": []
}
],
"findings": [
{
"key": "admin",
"source": "credentials",
"status": "SKIP",
"reason": "missing_credentials"
},
{
"key": "employee",
"source": "credentials",
"status": "SKIP",
"reason": "missing_credentials"
},
{
"key": "manager",
"source": "credentials",
"status": "SKIP",
"reason": "missing_credentials"
},
{
"key": "ceo",
"source": "credentials",
"status": "SKIP",
"reason": "missing_credentials"
},
{
"key": "finance",
"source": "credentials",
"status": "SKIP",
"reason": "missing_credentials"
},
{
"key": "finance_mgr",
"source": "credentials",
"status": "SKIP",
"reason": "missing_credentials"
},
{
"key": "hr",
"source": "credentials",
"status": "SKIP",
"reason": "missing_credentials"
}
],
"boundaries": {
"noBusinessWrites": true,
"noWeComWrites": true,
"noPasswordOutput": true,
"noRealEmployeeIdentifiers": true
}
}
@@ -0,0 +1,284 @@
{
"issueId": "OAOPS1",
"mode": "oa-real-use-daily",
"status": "PASS_WITH_FINDINGS",
"conclusion": "PASS_WITH_FINDINGS",
"baseURL": "https://oa.hyinda.com",
"commit": "267240b",
"checkedAt": "2026-05-26T05:51:27.948Z",
"reportRef": ".trellis/tasks/05-26-oaops1-patrol-feature-review/research/oaops1-after-patrol-fix-final",
"window": {
"durationDays": 7,
"scope": "small-team-all-employees",
"realBusinessDataAllowed": true,
"fixPolicy": "blocking_same_day_issue_contract_nonblocking_feedback_pool"
},
"oaPrimaryDomains": [
"reimbursement_pdf_ocr",
"approval_tasks",
"procurement_expense",
"contracts_registry",
"projects",
"payment_batches",
"bank_receipt_attachments",
"ap_accounting_handoff",
"mobile_wecom_entry",
"audit"
],
"frozenItems": [
"image_ocr",
"complex_bpm",
"budget_control",
"erp_write",
"bank_statement_import",
"real_payment_provider",
"wecom_hr_deep_mirror",
"emergency_takeover",
"recovery_writeback",
"ruleops"
],
"boundaries": {
"defaultReadOnlyAutomation": true,
"manualRealBusinessWritesOnly": true,
"paymentBatchExpected": "force_on",
"erpExpected": "force_off",
"actualPaymentChannel": "offline_bank_channel",
"contractSensitiveFiles": "do_not_upload_confidential_body_sensitive_quote_cost_detail_to_public_oa"
},
"childReports": {
"liveReadiness": {
"status": "PASS",
"reportRef": ".trellis/tasks/05-26-oaops1-patrol-feature-review/research/oaops1-after-patrol-fix-final/live-smoke-readiness/summary.json",
"findingCount": 0,
"openCount": 0
},
"oaOnlySmokeReadiness": {
"status": "PASS_WITH_FINDINGS",
"reportRef": ".trellis/tasks/05-26-oaops1-patrol-feature-review/research/oaops1-after-patrol-fix-final/oa-only-smoke-readiness/summary.json",
"findingCount": 7,
"openCount": 0
}
},
"steps": {
"public-ready": {
"name": "public-ready",
"status": "PASS",
"kind": "required",
"code": 0,
"detail": "ok",
"logRef": ".trellis/tasks/05-26-oaops1-patrol-feature-review/research/oaops1-after-patrol-fix-final/public-ready.log",
"checkedAt": "2026-05-26T05:48:56.169Z"
},
"live-smoke-env": {
"name": "live-smoke-env",
"status": "PASS",
"kind": "finding_allowed",
"code": 0,
"detail": "ok",
"logRef": ".trellis/tasks/05-26-oaops1-patrol-feature-review/research/oaops1-after-patrol-fix-final/live-smoke-env.log",
"checkedAt": "2026-05-26T05:48:56.740Z"
},
"live-readiness": {
"name": "live-readiness",
"status": "PASS",
"kind": "finding_allowed",
"code": 0,
"detail": "ok",
"logRef": ".trellis/tasks/05-26-oaops1-patrol-feature-review/research/oaops1-after-patrol-fix-final/live-readiness.log",
"checkedAt": "2026-05-26T05:49:02.213Z"
},
"oa-only-smoke-readiness": {
"name": "oa-only-smoke-readiness",
"status": "PASS",
"kind": "finding_allowed",
"code": 0,
"detail": "ok",
"logRef": ".trellis/tasks/05-26-oaops1-patrol-feature-review/research/oaops1-after-patrol-fix-final/oa-only-smoke-readiness.log",
"checkedAt": "2026-05-26T05:49:02.359Z"
},
"live-feature-flags": {
"name": "live-feature-flags",
"status": "FINDING",
"kind": "finding_allowed",
"code": 1,
"detail": "finding or missing runtime credential",
"logRef": ".trellis/tasks/05-26-oaops1-patrol-feature-review/research/oaops1-after-patrol-fix-final/live-feature-flags.log",
"checkedAt": "2026-05-26T05:49:02.508Z"
},
"authenticated-readonly-probe": {
"name": "authenticated-readonly-probe",
"status": "PASS",
"kind": "finding_allowed",
"code": 0,
"detail": "ok",
"logRef": ".trellis/tasks/05-26-oaops1-patrol-feature-review/research/oaops1-after-patrol-fix-final/authenticated-readonly-probe.log",
"checkedAt": "2026-05-26T05:49:02.665Z"
},
"mobile-entry-matrix": {
"name": "mobile-entry-matrix",
"status": "PASS",
"kind": "finding_allowed",
"code": 0,
"detail": "ok",
"logRef": ".trellis/tasks/05-26-oaops1-patrol-feature-review/research/oaops1-after-patrol-fix-final/mobile-entry-matrix.log",
"checkedAt": "2026-05-26T05:49:24.679Z"
},
"wecom-rendering-matrix": {
"name": "wecom-rendering-matrix",
"status": "PASS",
"kind": "finding_allowed",
"code": 0,
"detail": "ok",
"logRef": ".trellis/tasks/05-26-oaops1-patrol-feature-review/research/oaops1-after-patrol-fix-final/wecom-rendering-matrix.log",
"checkedAt": "2026-05-26T05:50:33.573Z"
},
"wecom-entry": {
"name": "wecom-entry",
"status": "FINDING",
"kind": "finding_allowed",
"code": 1,
"detail": "finding or missing runtime credential",
"logRef": ".trellis/tasks/05-26-oaops1-patrol-feature-review/research/oaops1-after-patrol-fix-final/wecom-entry.log",
"checkedAt": "2026-05-26T05:51:27.593Z"
},
"report-privacy-smoke": {
"name": "report-privacy-smoke",
"status": "PASS",
"kind": "required",
"code": 0,
"detail": "ok",
"logRef": ".trellis/tasks/05-26-oaops1-patrol-feature-review/research/oaops1-after-patrol-fix-final/report-privacy-smoke.log",
"checkedAt": "2026-05-26T05:51:27.805Z"
}
},
"readonlyProbe": {
"mode": "oaops1-authenticated-readonly-probe",
"status": "PASS_WITH_FINDINGS",
"baseUrl": "https://oa.hyinda.com",
"checkedAt": "2026-05-26T05:49:02.580Z",
"readOnly": true,
"duplicateRoleChecks": "employee/finance/payment/AP are covered by oa-only-smoke-readiness to avoid repeated login throttling",
"roles": {
"admin": {
"status": "SKIP",
"reason": "missing_admin_credentials",
"maskedIdentifier": null
}
},
"checks": [
{
"id": "portal",
"role": "admin",
"path": "/api/portal/home",
"domain": "portal",
"status": "SKIP",
"reason": "missing_admin_credentials"
},
{
"id": "workflow-tasks",
"role": "admin",
"path": "/api/workflow/tasks?page=1&pageSize=5",
"domain": "approval",
"status": "SKIP",
"reason": "missing_admin_credentials"
},
{
"id": "reimbursements",
"role": "admin",
"path": "/api/reimbursements?page=1&pageSize=5",
"domain": "reimbursement",
"status": "SKIP",
"reason": "missing_admin_credentials"
},
{
"id": "reimbursement-summary",
"role": "admin",
"path": "/api/reimbursements/stats/summary",
"domain": "reimbursement",
"status": "SKIP",
"reason": "missing_admin_credentials"
},
{
"id": "invoice-pool",
"role": "admin",
"path": "/api/invoice-pool?page=1&pageSize=5",
"domain": "invoice_ocr",
"status": "SKIP",
"reason": "missing_admin_credentials"
},
{
"id": "purchase-requests",
"role": "admin",
"path": "/api/purchase-requests?page=1&pageSize=5",
"domain": "procurement",
"status": "SKIP",
"reason": "missing_admin_credentials"
},
{
"id": "contracts-registry",
"role": "admin",
"path": "/api/contracts-registry",
"domain": "contracts",
"status": "SKIP",
"reason": "missing_admin_credentials"
},
{
"id": "projects",
"role": "admin",
"path": "/api/projects?page=1&pageSize=5",
"domain": "projects",
"status": "SKIP",
"reason": "missing_admin_credentials"
},
{
"id": "audit-logs",
"role": "admin",
"path": "/api/audit-logs?page=1&pageSize=5",
"domain": "audit",
"status": "SKIP",
"reason": "missing_admin_credentials"
}
],
"boundaries": {
"noBusinessWrites": true,
"noExportsByDefault": true,
"noFileUploads": true,
"noWeComWrites": true,
"noRealPaymentExecution": true
}
},
"findingCount": 4,
"findings": [
{
"source": "live-feature-flags",
"status": "FINDING",
"severity": "finding"
},
{
"source": "wecom-entry",
"status": "FINDING",
"severity": "finding"
},
{
"source": "authenticated-readonly-probe",
"status": "PASS_WITH_FINDINGS",
"severity": "manual_or_config"
},
{
"source": "child:oaOnlySmokeReadiness",
"status": "PASS_WITH_FINDINGS",
"severity": "finding",
"findingCount": 7,
"reportRef": ".trellis/tasks/05-26-oaops1-patrol-feature-review/research/oaops1-after-patrol-fix-final/oa-only-smoke-readiness/summary.json"
}
],
"manualGates": {
"dailyCloseout": "管理员每日查看 summary.json 和 manual-gates.md,阻断即修,非阻断入反馈池。",
"day7Closeout": [
"A:接受 PASS_OA_REAL_USE_READY,进入下一轮体验/效率优化。",
"B:接受 PASS_WITH_FINDINGS,但先处理指定 finding。",
"C:暂停真实使用扩展,先做整改。"
]
},
"privacy": "status/count/report-path summaries only; no phone/email/token/Secret/EncodingAESKey/full external ids/full invoice image/full bank receipt/full contract body/raw payload"
}
@@ -0,0 +1,34 @@
{
"id": "oaops1-patrol-feature-review",
"name": "oaops1-patrol-feature-review",
"title": "OAOPS1 patrol feature review",
"description": "Review OAOPS1/OALAUNCH1 patrol scripts for readiness, evidence quality, privacy and 7-day launch gate reliability.",
"status": "in_progress",
"dev_type": null,
"scope": "qa-patrol-review",
"package": null,
"priority": "P2",
"creator": "shunli",
"assignee": "shunli",
"createdAt": "2026-05-26",
"completedAt": null,
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [
".trellis/tasks/05-26-oaops1-patrol-feature-review/prd.md",
".trellis/tasks/05-26-oaops1-patrol-feature-review/info.md",
"scripts/qa/run-oa-real-use-daily.sh",
"scripts/qa/run-oa-final-launch-readiness.sh",
"scripts/qa/check-oa-only-smoke-readiness.mjs",
"docs/管理员手册.md",
"docs/文档事实源状态矩阵.md"
],
"notes": "",
"meta": {}
}
+71 -14
查看文件
@@ -22,17 +22,27 @@ write_step_json() {
local kind="$3"
local code="${4:-0}"
local detail="${5:-}"
node - "$REPORT_DIR" "$name" "$status" "$kind" "$code" "$detail" <<'NODE' || true
node - "$ROOT_DIR" "$REPORT_DIR" "$name" "$status" "$kind" "$code" "$detail" <<'NODE' || true
const fs = require("node:fs");
const path = require("node:path");
const [reportDir, name, status, kind, code, detail] = process.argv.slice(2);
const [rootDir, reportDir, name, status, kind, code, detail] = process.argv.slice(2);
function reportRef(file) {
const absolute = path.resolve(file);
const relative = path.relative(rootDir, absolute);
if (relative && !relative.startsWith("..") && !path.isAbsolute(relative)) {
return relative;
}
return path.basename(absolute);
}
fs.writeFileSync(path.join(reportDir, `${name}.step.json`), `${JSON.stringify({
name,
status,
kind,
code: Number(code || 0),
detail: String(detail || "").slice(0, 600),
logPath: path.resolve(path.join(reportDir, `${name}.log`)),
logRef: reportRef(path.join(reportDir, `${name}.log`)),
checkedAt: new Date().toISOString(),
}, null, 2)}\n`, "utf8");
NODE
@@ -201,9 +211,16 @@ function summarizePayload(json) {
if (typeof data === "object" && data) {
const summary = {};
for (const [key, value] of Object.entries(data)) {
if (/token|secret|password|phone|email|payload|attachment|comment|form/i.test(key)) continue;
if (typeof value === "number" || typeof value === "boolean") summary[key] = value;
if (typeof value === "string" && value.length <= 60) summary[key] = value;
if (/token|secret|password|phone|email|payload|attachment|comment|form|amount|money|price|cost|balance|bank|receipt|account|invoice|tax|fee|paid|payment/i.test(key)) continue;
if (typeof value === "number" && /(count|total|size|days|pending|open|closed|passed|failed)$/i.test(key)) {
summary[key] = value;
}
if (typeof value === "boolean" && /^(enabled|disabled|ready|ok|healthy|configured|available|has[A-Z]|is[A-Z]|can[A-Z])/i.test(key)) {
summary[key] = value;
}
if (typeof value === "string" && /^(status|mode|conclusion)$/i.test(key) && /^[a-z0-9_/-]{1,60}$/i.test(value)) {
summary[key] = value;
}
}
return summary;
}
@@ -347,11 +364,19 @@ run_privacy_smoke() {
local matches
matches="$(grep -RInE "$sensitive_pattern" "$REPORT_DIR" || true)"
if [[ -n "$matches" ]]; then
printf '%s\n' "$matches"
while IFS= read -r line; do
local match_file="${line%%:*}"
local rest="${line#*:}"
local match_line="${rest%%:*}"
local match_ref="${match_file#$ROOT_DIR/}"
printf '%s:%s:REDACTED\n' "$match_ref" "$match_line"
done <<<"$matches" >"$REPORT_DIR/privacy-findings.txt"
log "[oaops1][FAIL] report-privacy-smoke matched sensitive pattern"
STATUS=1
write_step_json "report-privacy-smoke" "FAIL" "required" 1 "sensitive pattern matched; see privacy-findings.txt"
else
log "[oaops1][PASS] report-privacy-smoke"
write_step_json "report-privacy-smoke" "PASS" "required" 0 "ok"
fi
}
@@ -372,21 +397,33 @@ function readJson(file) {
}
}
function reportRef(file) {
const absolute = path.resolve(file);
const relative = path.relative(rootDir, absolute);
if (relative && !relative.startsWith("..") && !path.isAbsolute(relative)) {
return relative;
}
return path.basename(absolute);
}
function step(name) {
return readJson(path.join(reportDir, `${name}.step.json`)) || {
name,
status: "MISSING",
kind: "unknown",
logPath: path.resolve(path.join(reportDir, `${name}.log`)),
logRef: reportRef(path.join(reportDir, `${name}.log`)),
};
}
function child(relativePath) {
const file = path.join(reportDir, relativePath, "summary.json");
const json = readJson(file);
const findingCount = Number(json?.findingCount ?? json?.findings?.length ?? json?.gates?.findings?.length ?? 0);
return {
status: json?.status || json?.conclusion || "MISSING",
reportPath: fs.existsSync(file) ? path.resolve(file) : null,
reportRef: fs.existsSync(file) ? reportRef(file) : null,
findingCount: Number.isFinite(findingCount) ? findingCount : 0,
openCount: Number(json?.gates?.openCount ?? 0),
};
}
@@ -405,9 +442,14 @@ const stepNames = [
"mobile-entry-matrix",
"wecom-rendering-matrix",
"wecom-entry",
"report-privacy-smoke",
];
const steps = Object.fromEntries(stepNames.map((name) => [name, step(name)]));
const readonlyProbe = readJson(path.join(reportDir, "authenticated-readonly-probe.json"));
const childReports = {
liveReadiness: child("live-smoke-readiness"),
oaOnlySmokeReadiness: child("oa-only-smoke-readiness"),
};
const findings = [];
for (const [name, item] of Object.entries(steps)) {
@@ -424,6 +466,17 @@ for (const [name, item] of Object.entries(steps)) {
if (readonlyProbe?.status && readonlyProbe.status !== "PASS") {
findings.push({ source: "authenticated-readonly-probe", status: readonlyProbe.status, severity: readonlyProbe.status === "FAIL" ? "finding" : "manual_or_config" });
}
for (const [name, item] of Object.entries(childReports)) {
const status = String(item.status || "UNKNOWN");
if (status === "PASS" && item.findingCount <= 0 && item.openCount <= 0) continue;
if (/BLOCKED|FAIL|ERROR/i.test(status)) {
findings.push({ source: `child:${name}`, status, severity: "finding", reportRef: item.reportRef });
} else if (/FINDING/i.test(status) || item.findingCount > 0) {
findings.push({ source: `child:${name}`, status, severity: "finding", findingCount: item.findingCount, reportRef: item.reportRef });
} else if (/SKIP|MISSING|PAUSED|PARTIAL/i.test(status) || item.openCount > 0) {
findings.push({ source: `child:${name}`, status, severity: "manual_or_config", openCount: item.openCount, reportRef: item.reportRef });
}
}
let status = "PASS_OA_REAL_USE_DAILY_READY";
if (Number(statusInput || 0) !== 0 || findings.some((item) => item.severity === "blocker")) {
@@ -440,7 +493,7 @@ const summary = {
baseURL,
commit,
checkedAt: new Date().toISOString(),
reportDir: path.resolve(reportDir),
reportRef: reportRef(reportDir),
window: {
durationDays: 7,
scope: "small-team-all-employees",
@@ -479,9 +532,7 @@ const summary = {
actualPaymentChannel: "offline_bank_channel",
contractSensitiveFiles: "do_not_upload_confidential_body_sensitive_quote_cost_detail_to_public_oa",
},
childReports: {
liveReadiness: child("live-smoke-readiness"),
},
childReports,
steps,
readonlyProbe,
findingCount: findings.length,
@@ -498,7 +549,7 @@ const summary = {
};
fs.writeFileSync(path.join(reportDir, "summary.json"), `${JSON.stringify(summary, null, 2)}\n`, "utf8");
console.log(`[oaops1] summary=${path.join(reportDir, "summary.json")}`);
console.log(`[oaops1] summary=${reportRef(path.join(reportDir, "summary.json"))}`);
NODE
}
@@ -506,6 +557,12 @@ finish() {
write_manual_checklist
write_summary_json || STATUS=1
run_privacy_smoke
write_summary_json || true
local before_second_privacy_status="$STATUS"
run_privacy_smoke
if [[ "$STATUS" != "$before_second_privacy_status" ]]; then
write_summary_json || true
fi
log "[oaops1] report=$REPORT_DIR"
exit "$STATUS"
}