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, - For generated evidence, store report paths and conclusions, not raw secrets,
raw payloads, full account identifiers, invoice originals or confidential raw payloads, full account identifiers, invoice originals or confidential
business content. 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 kind="$3"
local code="${4:-0}" local code="${4:-0}"
local detail="${5:-}" 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 fs = require("node:fs");
const path = require("node:path"); 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({ fs.writeFileSync(path.join(reportDir, `${name}.step.json`), `${JSON.stringify({
name, name,
status, status,
kind, kind,
code: Number(code || 0), code: Number(code || 0),
detail: String(detail || "").slice(0, 600), 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(), checkedAt: new Date().toISOString(),
}, null, 2)}\n`, "utf8"); }, null, 2)}\n`, "utf8");
NODE NODE
@@ -201,9 +211,16 @@ function summarizePayload(json) {
if (typeof data === "object" && data) { if (typeof data === "object" && data) {
const summary = {}; const summary = {};
for (const [key, value] of Object.entries(data)) { for (const [key, value] of Object.entries(data)) {
if (/token|secret|password|phone|email|payload|attachment|comment|form/i.test(key)) continue; 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" || typeof value === "boolean") summary[key] = value; if (typeof value === "number" && /(count|total|size|days|pending|open|closed|passed|failed)$/i.test(key)) {
if (typeof value === "string" && value.length <= 60) summary[key] = value; 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; return summary;
} }
@@ -347,11 +364,19 @@ run_privacy_smoke() {
local matches local matches
matches="$(grep -RInE "$sensitive_pattern" "$REPORT_DIR" || true)" matches="$(grep -RInE "$sensitive_pattern" "$REPORT_DIR" || true)"
if [[ -n "$matches" ]]; then 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" log "[oaops1][FAIL] report-privacy-smoke matched sensitive pattern"
STATUS=1 STATUS=1
write_step_json "report-privacy-smoke" "FAIL" "required" 1 "sensitive pattern matched; see privacy-findings.txt"
else else
log "[oaops1][PASS] report-privacy-smoke" log "[oaops1][PASS] report-privacy-smoke"
write_step_json "report-privacy-smoke" "PASS" "required" 0 "ok"
fi 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) { function step(name) {
return readJson(path.join(reportDir, `${name}.step.json`)) || { return readJson(path.join(reportDir, `${name}.step.json`)) || {
name, name,
status: "MISSING", status: "MISSING",
kind: "unknown", kind: "unknown",
logPath: path.resolve(path.join(reportDir, `${name}.log`)), logRef: reportRef(path.join(reportDir, `${name}.log`)),
}; };
} }
function child(relativePath) { function child(relativePath) {
const file = path.join(reportDir, relativePath, "summary.json"); const file = path.join(reportDir, relativePath, "summary.json");
const json = readJson(file); const json = readJson(file);
const findingCount = Number(json?.findingCount ?? json?.findings?.length ?? json?.gates?.findings?.length ?? 0);
return { return {
status: json?.status || json?.conclusion || "MISSING", 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", "mobile-entry-matrix",
"wecom-rendering-matrix", "wecom-rendering-matrix",
"wecom-entry", "wecom-entry",
"report-privacy-smoke",
]; ];
const steps = Object.fromEntries(stepNames.map((name) => [name, step(name)])); const steps = Object.fromEntries(stepNames.map((name) => [name, step(name)]));
const readonlyProbe = readJson(path.join(reportDir, "authenticated-readonly-probe.json")); 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 = []; const findings = [];
for (const [name, item] of Object.entries(steps)) { 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") { if (readonlyProbe?.status && readonlyProbe.status !== "PASS") {
findings.push({ source: "authenticated-readonly-probe", status: readonlyProbe.status, severity: readonlyProbe.status === "FAIL" ? "finding" : "manual_or_config" }); 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"; let status = "PASS_OA_REAL_USE_DAILY_READY";
if (Number(statusInput || 0) !== 0 || findings.some((item) => item.severity === "blocker")) { if (Number(statusInput || 0) !== 0 || findings.some((item) => item.severity === "blocker")) {
@@ -440,7 +493,7 @@ const summary = {
baseURL, baseURL,
commit, commit,
checkedAt: new Date().toISOString(), checkedAt: new Date().toISOString(),
reportDir: path.resolve(reportDir), reportRef: reportRef(reportDir),
window: { window: {
durationDays: 7, durationDays: 7,
scope: "small-team-all-employees", scope: "small-team-all-employees",
@@ -479,9 +532,7 @@ const summary = {
actualPaymentChannel: "offline_bank_channel", actualPaymentChannel: "offline_bank_channel",
contractSensitiveFiles: "do_not_upload_confidential_body_sensitive_quote_cost_detail_to_public_oa", contractSensitiveFiles: "do_not_upload_confidential_body_sensitive_quote_cost_detail_to_public_oa",
}, },
childReports: { childReports,
liveReadiness: child("live-smoke-readiness"),
},
steps, steps,
readonlyProbe, readonlyProbe,
findingCount: findings.length, findingCount: findings.length,
@@ -498,7 +549,7 @@ const summary = {
}; };
fs.writeFileSync(path.join(reportDir, "summary.json"), `${JSON.stringify(summary, null, 2)}\n`, "utf8"); 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 NODE
} }
@@ -506,6 +557,12 @@ finish() {
write_manual_checklist write_manual_checklist
write_summary_json || STATUS=1 write_summary_json || STATUS=1
run_privacy_smoke 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" log "[oaops1] report=$REPORT_DIR"
exit "$STATUS" exit "$STATUS"
} }