agent-project-sdlc 0.1.20 → 0.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/assets/docs/README.md +5 -2
- package/assets/policies/allowed_paths.yaml +1 -0
- package/assets/policies/phase_contracts.yaml +3 -0
- package/assets/skills/pjsdlc_architect_design/SKILL.md +1 -0
- package/assets/skills/pjsdlc_dev_sprint/SKILL.md +9 -3
- package/assets/skills/pjsdlc_implementation_doc/SKILL.md +6 -3
- package/assets/skills/pjsdlc_reviewer/SKILL.md +2 -2
- package/assets/skills/pjsdlc_tester/SKILL.md +3 -2
- package/assets/templates/EVIDENCE_INDEX_TEMPLATE.md +17 -0
- package/assets/templates/EXPLORATION_APPENDIX_TEMPLATE.md +22 -0
- package/assets/templates/IMPLEMENTATION_DOC_TEMPLATE.md +40 -8
- package/assets/templates/PLAN_TEMPLATE.yaml +18 -1
- package/assets/templates/RUNBOOK_TEMPLATE.md +47 -0
- package/assets/tools/harness_utils.py +83 -0
- package/assets/tools/validate_harness.py +1 -0
- package/dist/lib/init.js +1 -0
- package/dist/lib/validators.js +355 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,6 +31,7 @@ npx sdlc-harness init --adopt
|
|
|
31
31
|
| Stage task control | `plan.yaml`, `make validate-plan`, `npx sdlc-harness validate-plan` | Keeps each stage's agent work in small `TASK-*` tasks with `phase` metadata and scoped paths/gates. |
|
|
32
32
|
| Natural-language control | `AGENTS.md` plus workflow skills | Lets users say things like "continue", "start development", "run tests" or "requirements changed"; agents map these to workflow actions. |
|
|
33
33
|
| Default parallel scheduling contract | `plan.yaml#parallel_execution` | Stage tasks default to a safe-parallelism check; suitable work uses Codex native subagents first, with user-orchestrated and worktree fallbacks. |
|
|
34
|
+
| Resume-first runtime handoff | `plan.yaml#resume_capsule`, `.docs/09_runbooks/**` | Keeps high-risk runtime/live/remote-operator tasks recoverable through a short resume card, runbook, evidence index and exploration appendix. |
|
|
34
35
|
| Workflow skills | `<harnessRoot>/skills/pjsdlc_*/SKILL.md` | Provides role prompts for product, architecture, development, implementation docs, review, testing, release and RFC recalibration. |
|
|
35
36
|
| Project-local skill overrides | `<harnessRoot>/pjsdlc_managed/override_skills/<skill_name>.md` + `npx sdlc-harness sync` | Appends project-specific role instructions to generated Skill output without editing managed Skill files. |
|
|
36
37
|
| Local policy overrides | `<harnessRoot>/pjsdlc_managed/policies/*.local.yaml` | Preserves project-specific policy additions separately from package defaults. |
|
|
@@ -86,9 +87,11 @@ Before development starts, `ARCHITECTING` can return to `REQUIREMENT_GATHERING`
|
|
|
86
87
|
|
|
87
88
|
`validate-design` treats semantic slicing as a hard gate. Generated `overview.md` files do not count as deliverables, development draft tasks in `plan.draft.yaml` must reference existing tech plan slices through `docs.tech_plan`, multiple development draft tasks need distinct primary tech plan slices, and explicit AI provider/copilot, external-system, or compliance/permission/audit themes require dedicated architecture slices. Draft tasks with runnable boundaries must also include `self_test_contract`, backed by a `Development Self-Test Contract` section in the tech plan; the contract must include `module_key_test_path` from local start or invocation to all self-test scenarios completion, covering every runnable entry promised by the current task/module and its internal key paths.
|
|
88
89
|
|
|
89
|
-
SPRINTING Definition of Done includes module-level runnable delivery boundaries. API, CLI, server route, service, agent, runtime, adapter, worker, provider, config-contract and fixture/live boundaries promised by a technical plan or task must be implemented or marked `BLOCKED` during development. Runtime/app/provider/live tasks must declare `evidence_level.required`, `target_runtime_environment` and `self_test_contract` in `plan.yaml`; every gate in `self_test_contract.required_gates` must also appear in task `required_gates`, and `self_test_contract.module_key_test_path` must describe the path from local start or invocation to all self-test scenarios completion, covering every runnable entry promised by the current task/module and its internal key paths. `deployed_runtime` cannot be closed by `unit`, `local_runtime`, `external_provider_live`, provider smoke, fake adapters or localhost smoke alone, and `business_handoff_ready` requires a Testing Handoff Contract. The current task implementation doc must include `Development Evidence` and a completed `Development Self-Test Report` with contract source, scenario results, executed gates, Module Key Test Path, actual evidence, missing/blockers and Testing Handoff Readiness; Module Key Test Path records actual entries, internal key paths, boundaries, checkpoints and observable completion evidence. Provider smoke, fixture smoke, fake adapters and one-shot smoke prove only local links; they do not by themselves prove application readiness. REVIEWING treats missing entry/exit, initialization, config contract, target runtime, evidence level or development evidence as blocking, and TESTING only exercises entrypoints that Review has confirmed as `PASS`; it must not add product runtime, bootstrap, provider adapter, deploy code or package runtime scripts.
|
|
90
|
+
SPRINTING Definition of Done includes module-level runnable delivery boundaries. API, CLI, server route, service, agent, runtime, adapter, worker, provider, config-contract and fixture/live boundaries promised by a technical plan or task must be implemented or marked `BLOCKED` during development. Runtime/app/provider/live tasks must declare `evidence_level.required`, `target_runtime_environment` and `self_test_contract` in `plan.yaml`; every gate in `self_test_contract.required_gates` must also appear in task `required_gates`, and `self_test_contract.module_key_test_path` must describe the path from local start or invocation to all self-test scenarios completion, covering every runnable entry promised by the current task/module and its internal key paths. `deployed_runtime` cannot be closed by `unit`, `local_runtime`, `external_provider_live`, provider smoke, fake adapters or localhost smoke alone, and `business_handoff_ready` requires a Testing Handoff Contract. The current task implementation doc must include `Development Evidence` and a completed `Development Self-Test Report` with `Report Status: PASS | BLOCKED | IN_PROGRESS | STALE`, contract source, scenario results, executed gates, Module Key Test Path, actual evidence, missing/blockers and Testing Handoff Readiness; only `Report Status: PASS` with every scenario `PASS` can close a development task. The report proves module entry, core path, exit and minimal evidence; it is not a debug log, operator log, runbook or exploration history. Fallback/diagnostic detail belongs in `.docs/09_runbooks/**` appendices or git history. Module Key Test Path records actual entries, internal key paths, boundaries, checkpoints and observable completion evidence. Provider smoke, fixture smoke, fake adapters and one-shot smoke prove only local links; they do not by themselves prove application readiness. REVIEWING treats missing entry/exit, initialization, config contract, target runtime, evidence level or development evidence as blocking, and TESTING only exercises entrypoints that Review has confirmed as `PASS`; it must not add product runtime, bootstrap, provider adapter, deploy code or package runtime scripts.
|
|
90
91
|
|
|
91
|
-
|
|
92
|
+
High-risk runtime/live/remote-operator tasks are resume-first. When the current SPRINTING task requires `external_provider_live`, `deployed_runtime` or `business_handoff_ready`, or its target runtime is `cloud_vm`, `managed_service`, `browser` or `worker`, `plan.yaml` must include top-level `resume_capsule` with the current state, canonical path, next step, blocker, last passed gate, do-not-retry list and recovery refs. Open task `working_notes` stays short, with a 5-8 item target and an 8 item validator limit. Long-term implementation facts stay in the implementation doc; operator paths, credential references and remote entrypoints live in `.docs/09_runbooks/**`; the implementation doc only keeps a short `Current Operator Path` with canonical operator path, runbook link, credential reference name, command/UI channel and do-not-retry summary. Evidence bodies live in an evidence index or external system; failed exploration stays in an exploration appendix. The Development Self-Test Report for these tasks must include a Gate Breakdown that separates local gate, cloud/service gate, executor/operator readiness and live smoke or handoff evidence.
|
|
93
|
+
|
|
94
|
+
`make validate-dev` and `npx sdlc-harness validate-dev` are in-development SPRINTING gates. They allow the current `current_task_id` open task to remain in `plan.yaml` while checking that it is a valid `phase: "SPRINTING"` task with `docs`, `allowed_paths`, `required_gates`, `acceptance_criteria`, `implementation_doc`, scoped dirty files, an empty `plan.draft.yaml` queue, runtime evidence task contract, `self_test_contract`, linked runnable-entry implementation docs, structured development evidence and a completed Development Self-Test Report. The report must include legal `Report Status` and Module Key Test Path so later agents can reuse the debug path from local entry to all self-test scenarios completion; that path is scoped to entries and internal key paths promised by the current task/module, not the whole system. `validate-dev` only passes completion-oriented dev evidence when `Report Status: PASS` and every scenario is `PASS`; `BLOCKED`, `IN_PROGRESS` and `STALE` reports may exist as recovery facts but cannot close the current development task. Page tasks need a dev server or page URL plus browser/Playwright/screenshot/equivalent interaction evidence; API/CLI/worker/service/agent/runtime tasks need a startup or invocation command, endpoint/health/status, and observable response/output/side effect. `validate-dev` checks content consistency and completeness between the report and current `self_test_contract`; it does not prove commands really executed in the current run. Agents must execute the current task `required_gates` before filling the report, and writing `PASS` without running those gates is an Agent execution violation. `make validate-current` and `/advance` are phase-exit gates; before moving to REVIEWING, the implementation commit and completion ledger must be done and no open task may remain.
|
|
92
95
|
|
|
93
96
|
`validate-test` keeps its command name as the TESTING phase gate. `.docs/07_test/TEST_STRATEGY.md` describes scope, environment, priority and execution strategy; `.docs/07_test/TEST_CASES.md` describes cases bound to real runnable entry/exit; `.docs/07_test/TEST_REPORT.md` only records executed TESTING evidence, test matrix, regression evidence, runnable entry/exit coverage, coverage gaps and final decision. `validate-test` only accepts `TEST_REPORT.md`; it no longer treats `TEST_PLAN.md` as a report fallback.
|
|
94
97
|
|
package/assets/docs/README.md
CHANGED
|
@@ -106,9 +106,11 @@ Agent 会读取 `<harnessRoot>/state/lifecycle.yaml` 和 `<harnessRoot>/state/pl
|
|
|
106
106
|
|
|
107
107
|
`validate-design` 会把架构阶段的语义切片作为硬 gate:`overview.md` 不计入 deliverables,`plan.draft.yaml` 中每个开发 draft task 必须通过 `docs.tech_plan` 指向存在的 tech plan slice;多个开发 draft task 默认需要不同 primary tech plan slice。PRD、tech plan 或 draft task 明确出现 AI provider / copilot、外部系统边界、合规 / 权限 / 审计等横切主题时,也需要对应的专门 architecture slice。可运行边界类 draft task 还必须带 `self_test_contract`,并在 tech plan 中有 `Development Self-Test Contract`;合同必须记录 `module_key_test_path`,说明从本地启动或调用入口开始,到完成全部自测 scenario 的模块关键测试路径,并覆盖本 task / 本模块承诺的所有可运行入口和内部关键路径。
|
|
108
108
|
|
|
109
|
-
SPRINTING 的 Definition of Done 包含模块级可运行交付边界:技术方案或 task 承诺的 API、CLI、server route、service、agent、runtime、adapter、worker、provider、配置契约和 fixture/live 边界必须在开发阶段实现或明确 `BLOCKED`。runtime/app/provider/live 类 task 必须在 `plan.yaml` 声明 `evidence_level.required`、`target_runtime_environment` 和 `self_test_contract`;`self_test_contract.required_gates` 必须同步出现在 task `required_gates`,`self_test_contract.module_key_test_path` 必须描述从本地启动或调用入口开始,到完成全部自测 scenario 的模块关键测试路径,并覆盖本 task / 本模块承诺的所有可运行入口和内部关键路径。`deployed_runtime` 不能用 `unit`、`local_runtime`、`external_provider_live`、provider smoke、fake adapter 或 localhost smoke 单独关闭,`business_handoff_ready` 还必须有 Testing Handoff Contract。当前 task 的 implementation doc 还必须写入 `Development Evidence` 和 `Development Self-Test Report`,其中自测报告记录 contract source、scenario results、executed gates、Module Key Test Path、actual evidence、missing/blockers 和 Testing Handoff Readiness
|
|
109
|
+
SPRINTING 的 Definition of Done 包含模块级可运行交付边界:技术方案或 task 承诺的 API、CLI、server route、service、agent、runtime、adapter、worker、provider、配置契约和 fixture/live 边界必须在开发阶段实现或明确 `BLOCKED`。runtime/app/provider/live 类 task 必须在 `plan.yaml` 声明 `evidence_level.required`、`target_runtime_environment` 和 `self_test_contract`;`self_test_contract.required_gates` 必须同步出现在 task `required_gates`,`self_test_contract.module_key_test_path` 必须描述从本地启动或调用入口开始,到完成全部自测 scenario 的模块关键测试路径,并覆盖本 task / 本模块承诺的所有可运行入口和内部关键路径。`deployed_runtime` 不能用 `unit`、`local_runtime`、`external_provider_live`、provider smoke、fake adapter 或 localhost smoke 单独关闭,`business_handoff_ready` 还必须有 Testing Handoff Contract。当前 task 的 implementation doc 还必须写入 `Development Evidence` 和 `Development Self-Test Report`,其中自测报告记录 `Report Status: PASS | BLOCKED | IN_PROGRESS | STALE`、contract source、scenario results、executed gates、Module Key Test Path、actual evidence、missing/blockers 和 Testing Handoff Readiness;只有 `Report Status: PASS` 且所有 scenario 为 `PASS` 才能关闭 development task。`Development Self-Test Report` 只证明模块入口、核心路径、出口和最小证据,不承担 debug log、operator log、runbook 或探索流水职责;fallback / diagnostic 最多一句总结,详细内容进入 `.docs/09_runbooks/**` appendix 或 git history。`Module Key Test Path` 必须记录实际入口、内部关键路径、关键边界、观察点和可观测完成证据。provider smoke、fixture smoke、fake adapter 或 one-shot smoke 只能证明局部链路,不能单独证明 application readiness。REVIEWING 会把缺少入口/出口、初始化、配置契约、目标运行环境、证据等级或开发自测证据作为阻断项;TESTING 只调用 Review 已确认 `PASS` 的既有入口做输入输出验证,不能新增 product runtime、bootstrap、provider adapter、deploy 或 package runtime script。
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
复杂 runtime/live/remote-operator 任务采用 resume-first 分层:当当前 SPRINTING task 要求 `external_provider_live`、`deployed_runtime`、`business_handoff_ready`,或目标环境是 `cloud_vm`、`managed_service`、`browser`、`worker` 时,`plan.yaml` 顶层必须维护 `resume_capsule`,只保留当前状态、canonical path、下一步、blocker、last passed gate、do-not-retry 和 recovery refs;open task 的 `working_notes` 只保留恢复短备注,目标 5-8 条且 validator 上限 8 条。长期实现事实写 implementation doc;操作路径、凭证引用、远端入口写 `.docs/09_runbooks/**` runbook;implementation doc 只放短的 `Current Operator Path`,记录 canonical operator path、runbook link、credential reference name、command/UI channel 和 do-not-retry summary;证据正文只进入 evidence index 或外部证据系统;失败探索隔离到 exploration appendix。高风险 task 的 `Development Self-Test Report` 还必须有 `Gate Breakdown`,把 local gate、cloud/service gate、executor/operator readiness 和 live smoke / handoff 分开记录,不能只用一个 `validate-dev PASS` 覆盖全部进度。
|
|
112
|
+
|
|
113
|
+
`make validate-dev` / `npx sdlc-harness validate-dev` 是 SPRINTING 开发中 gate:当前 `current_task_id` 指向的 open task 可以继续留在 `plan.yaml`,validator 会检查它是否是合法 `phase: "SPRINTING"` task、是否具备 `docs`、`allowed_paths`、`required_gates`、`acceptance_criteria`、`implementation_doc`,并校验 dirty files、`plan.draft.yaml`、runtime evidence task contract、`self_test_contract`、implementation doc、结构化 `Development Evidence` 和 `Development Self-Test Report`。自测报告必须记录合法 `Report Status` 和 `Module Key Test Path`,便于后续 Agent 复用从本地入口到全部自测用例完成的 debug 路径;该路径只要求覆盖本 task / 本模块承诺范围内的可运行入口和内部关键路径,不要求覆盖全系统所有模块。`validate-dev` 只接受 `Report Status: PASS` 且所有 scenario 为 `PASS` 的完成态;`BLOCKED`、`IN_PROGRESS`、`STALE` 可以记录恢复事实,但不能关闭当前 development task。页面类证据需要 dev server/page URL 与 browser check;API/CLI/worker/service/agent/runtime 类证据需要 startup/invocation command、endpoint/health/status 与 response/output/side effect。`validate-dev` 只校验自测报告内容与当前 `self_test_contract` 的一致性和完整性,不证明命令在本轮真实执行;Agent 必须先实际运行 current task `required_gates` 后再填写 `Development Self-Test Report`,未执行 required gates 却写 `PASS` 属于 Agent execution violation。`make validate-current` / `/advance` 是阶段出口 gate;进入 REVIEWING 前仍必须先完成 implementation commit 和 completion ledger,把 open task 从 `plan.yaml` 移除。
|
|
112
114
|
|
|
113
115
|
`validate-test` 仍然是 TESTING 阶段 gate 名称。`.docs/07_test/TEST_STRATEGY.md` 描述测试范围、环境、优先级和执行策略;`.docs/07_test/TEST_CASES.md` 描述绑定真实 runnable entry/exit 的测试用例;`.docs/07_test/TEST_REPORT.md` 只记录 TESTING 阶段实际执行后的 test matrix、regression evidence、runnable entry/exit coverage、coverage gaps 和 final decision。`validate-test` 只接受 `TEST_REPORT.md`,不会把 `TEST_PLAN.md` 当作 report fallback。
|
|
114
116
|
|
|
@@ -243,6 +245,7 @@ make docs-overview
|
|
|
243
245
|
| `.docs/06_review/` | Review 报告 |
|
|
244
246
|
| `.docs/07_test/` | 测试策略、测试用例、执行后测试报告、回归证据和覆盖缺口 |
|
|
245
247
|
| `.docs/08_release/` | 当前发布状态、smoke evidence、回滚方案和已知限制 |
|
|
248
|
+
| `.docs/09_runbooks/` | runtime/live/remote-operator 恢复路径、证据索引和探索附录 |
|
|
246
249
|
| `.docs/rfc/` | 需求变更和影响分析 |
|
|
247
250
|
|
|
248
251
|
`overview.md` 是生成物,用于浏览和阶段交接;Markdown slices 和 `.docs/INDEX.md` 才是事实源。
|
|
@@ -58,6 +58,7 @@ phases:
|
|
|
58
58
|
- "src/"
|
|
59
59
|
- "tests/"
|
|
60
60
|
- ".docs/04_implementation/"
|
|
61
|
+
- ".docs/09_runbooks/"
|
|
61
62
|
- "<harnessRoot>/state/plan.draft.yaml"
|
|
62
63
|
gates:
|
|
63
64
|
- "make validate-dev"
|
|
@@ -89,6 +90,7 @@ phases:
|
|
|
89
90
|
- ".docs/01_product/"
|
|
90
91
|
- ".docs/03_tech_plan/"
|
|
91
92
|
- ".docs/04_implementation/"
|
|
93
|
+
- ".docs/09_runbooks/"
|
|
92
94
|
- ".docs/06_review/"
|
|
93
95
|
outputs:
|
|
94
96
|
- "<harnessRoot>/state/plan.yaml"
|
|
@@ -138,6 +140,7 @@ phases:
|
|
|
138
140
|
outputs:
|
|
139
141
|
- ".docs/rfc/"
|
|
140
142
|
- ".docs/07_test/"
|
|
143
|
+
- ".docs/09_runbooks/"
|
|
141
144
|
- "<harnessRoot>/state/plan.yaml"
|
|
142
145
|
- ".docs/INDEX.md"
|
|
143
146
|
gates:
|
|
@@ -57,6 +57,7 @@ ADR 用来解决“后来的人只看到结果,看不到当年取舍”的问
|
|
|
57
57
|
- 如果实现计划改变了已有模块边界,应更新相关 architecture slice,而不是只在 task 描述里补一句。
|
|
58
58
|
- 只要技术方案或 draft task 出现 service、agent、runtime、worker、frontend app、provider/live integration 或外部可运行边界,task breakdown 必须包含最后一公里 runtime 初始化和 testing handoff 交付:目标运行环境、启动/部署或预览方式、health/readiness、smoke 输入输出、日志/错误证据、测试可调用入口和出口。
|
|
59
59
|
- 这类开发 draft task 必须写入 `evidence_level.required`、`target_runtime_environment` 和 `self_test_contract`。`evidence_level.required` 只能使用 `unit`、`local_runtime`、`external_provider_live`、`deployed_runtime`、`business_handoff_ready`;`target_runtime_environment.kind` 只能使用 `local`、`ci`、`staging`、`cloud_vm`、`managed_service`、`browser`、`worker`、`not_applicable`。`self_test_contract` 的 `source` 必须引用当前 tech plan slice,`required_gates` 必须同步到 task `required_gates`,`scenarios[]` 至少覆盖一个可运行入口和可观测出口,`module_key_test_path` 必须描述从本地启动或调用入口开始,到完成所有自测 scenario 的模块关键测试路径,并覆盖本 task / 本模块承诺的所有可运行入口和内部关键路径。
|
|
60
|
+
- 如果 draft task 属于 high-risk runtime/live/remote-operator 工作(`external_provider_live`、`deployed_runtime`、`business_handoff_ready`,或目标环境为 `cloud_vm`、`managed_service`、`browser`、`worker`),还必须预留恢复分层:`docs.runbook` 指向 `.docs/09_runbooks/**` 下的 runbook / evidence index / exploration appendix,`allowed_paths` 覆盖这些文件,acceptance criteria 要求 promote 后维护 `plan.yaml#resume_capsule`。runbook 写 canonical operator path,evidence index 只写证据指针,exploration appendix 隔离失败尝试;不要把这些内容塞进 implementation doc 主线。
|
|
60
61
|
- 如果用户明确要求把既有完整技术方案文件切成多个 `.docs/03_tech_plan/` slices,先确认 replacement slices 覆盖原文件中仍有效的接口契约、数据模型、模块方案、任务组和 gate;切片完成并更新 `plan.draft.yaml` 引用、`.docs/INDEX.md`、刷新 `overview.md` 后,删除被替代的完整 tech plan file,避免同一事实由完整文件和 slices 双重保留。
|
|
61
62
|
- 每次新增、拆分、合并或废弃 slice 后,都要更新 `.docs/INDEX.md`。
|
|
62
63
|
|
|
@@ -17,7 +17,11 @@ description: Use during SPRINTING to execute one task from plan.yaml, respecting
|
|
|
17
17
|
|
|
18
18
|
开发阶段的 Definition of Done 包含可运行的系统入口/出口。凡技术方案或 task 承诺 API、CLI、server route、service、agent、runtime、adapter、worker、provider、外部发送/写入执行器、配置契约或 live/fixture 双模式边界,当前实现必须提供对应入口、调用方式、初始化方式、输出/副作用边界和验证方式;如果真实入口/出口尚不可运行,不能把 task 当作完成,也不能把缺口留给 TESTING 补 runtime。runtime/app/provider/live 类 task 必须在 `plan.yaml` 声明 `evidence_level.required`、`target_runtime_environment` 和 `self_test_contract`,并按合同交付:`deployed_runtime` 不能用 `unit`、`local_runtime`、`external_provider_live`、provider smoke、fake adapter 或 localhost smoke 单独关闭;`business_handoff_ready` 必须提供 Testing Handoff Contract。Implementation doc 必须写明 `Runnable Entry/Exit`,并在 `Development Evidence` 中记录 `Evidence Level`、`Target Runtime Environment`、`Runnable Entry`、`Observable Exit`、`Client / Server Initialization`、`Config Contract`、`Testing Handoff Readiness`、`Known Missing Runtime Boundaries` 和 `Basic Self-test Evidence`;其中 `Basic Self-test Evidence` 应指向已执行的 `Development Self-Test Report`。确实不适用时也要显式写 `Not applicable` 和具体原因。provider smoke、fixture smoke、fake adapter 或 one-shot smoke 只能证明局部链路,不能单独证明 `Application readiness`。此时应保留或创建 `BLOCKED`/后续 dev task,或通过 RFC/ARCHITECTING 处理边界变更。
|
|
19
19
|
|
|
20
|
-
`self_test_contract` 是开发阶段自测合同,由 ARCHITECTING 或 RFC_RECALIBRATION 先定义,SPRINTING 负责执行并在 implementation doc 填写 `Development Self-Test Report`。开发者不得在开发结束后用现有实现反推自测合同;如果合同缺失、入口不匹配、required gates 未同步或场景无法执行,要先回到 ARCHITECTING/RFC 或把 task 保持为 `BLOCKED`。自测报告不是 TESTING
|
|
20
|
+
`self_test_contract` 是开发阶段自测合同,由 ARCHITECTING 或 RFC_RECALIBRATION 先定义,SPRINTING 负责执行并在 implementation doc 填写 `Development Self-Test Report`。开发者不得在开发结束后用现有实现反推自测合同;如果合同缺失、入口不匹配、required gates 未同步或场景无法执行,要先回到 ARCHITECTING/RFC 或把 task 保持为 `BLOCKED`。自测报告不是 TESTING 阶段产物,也不是 debug log、operator log、runbook 或历史流水;它只证明当前模块级可运行交付边界已经能被 Review/Testing 消费。报告必须写 `Report Status: PASS | BLOCKED | IN_PROGRESS | STALE`,只有 `PASS` 且所有 scenario 都是 `PASS` 才能关闭当前 development task;`BLOCKED`、`IN_PROGRESS`、`STALE` 可以作为恢复事实存在,但不能作为交接通过。报告还必须记录 `Module Key Test Path`:从本地启动或调用入口开始,执行并完成 `self_test_contract` 中全部自测用例的模块关键测试路径。该路径应覆盖本 task / 本模块承诺的所有可运行入口,以及自测用例实际经过的内部关键路径、关键边界、观察点和可观测完成证据,供后续 Agent 复用和 debug。
|
|
21
|
+
|
|
22
|
+
开发阶段交付包含两类产物:实现产物(代码、配置、脚本、测试等)和开发自测产物。`Development Self-Test Report` 是开发阶段产物,不是计划、模板或历史记录。若当前 task 或关联技术方案声明 `self_test_contract.status: "required"`,必须先逐条执行 `self_test_contract.scenarios[]` 和 `self_test_contract.required_gates`,再填写或更新 `Development Self-Test Report`。没有本轮执行过的 runnable entry、内部关键路径、observable exit / artifact / screenshot / response / log 等证据时,不得写 `PASS`,不得完成 task。fallback / diagnostic 在主报告最多一句总结,详细命令、截图过程、UI 操作细节和失败路径进入 `.docs/09_runbooks/**` exploration appendix 或 git history。
|
|
23
|
+
|
|
24
|
+
高风险 runtime/live/remote-operator task 必须维护恢复优先级。若 `evidence_level.required` 是 `external_provider_live`、`deployed_runtime`、`business_handoff_ready`,或 `target_runtime_environment.kind` 是 `cloud_vm`、`managed_service`、`browser`、`worker`,`plan.yaml` 顶层必须有 `resume_capsule`,并在路径选择结论变化时立即更新:`state`、`canonical_path`、`next_step`、`blocker`、`last_passed_gate`、`do_not_retry` 和 `recovery_refs`。`working_notes` 只保留短恢复备注,目标 5-8 条且不得超过 8 条;canonical operator path 写入 `.docs/09_runbooks/**` runbook,并在 implementation doc 写一个短的 `Current Operator Path` 链接 runbook、credential reference name、command/UI channel 和 do-not-retry summary。证据正文只在 evidence 文件或外部系统,失败探索写入 exploration appendix。不要把 A/B/C 路径探索流水混进 implementation doc 主线或 scenario evidence。
|
|
21
25
|
|
|
22
26
|
页面类任务在开发阶段必须启动 dev server 或等价预览入口,并用浏览器、Playwright、截图或等价方式验证页面可加载、主入口可访问、核心按钮/表单/跳转可用、没有明显报错或空白页。API/CLI/worker/RPA/service/agent/runtime 类任务必须记录实际启动或调用命令、endpoint、worker command、dry-run/live preflight、health/status 或 server action,以及可观察的 response、队列 item、审计日志、文件产物、发送结果、错误码或 PASS/BLOCKED 结果。
|
|
23
27
|
|
|
@@ -41,6 +45,7 @@ description: Use during SPRINTING to execute one task from plan.yaml, respecting
|
|
|
41
45
|
- 当前 task `allowed_paths` 范围内的测试改动
|
|
42
46
|
- `.docs/04_implementation/` 下相关模块、子系统或核心数据流的 implementation doc
|
|
43
47
|
- 当前 task `working_notes` 或 implementation doc `Verification` 中的 gate evidence
|
|
48
|
+
- high-risk runtime/live task 的 `plan.yaml#resume_capsule` 和 `.docs/09_runbooks/**` runbook / evidence index / exploration appendix
|
|
44
49
|
- implementation doc 中的 runnable entry/exit、observable exit、Development Self-Test Report、Module Key Test Path、配置契约和 fixture/live 边界事实
|
|
45
50
|
- 更新后的 `<harnessRoot>/state/plan.yaml`
|
|
46
51
|
- 如果本轮 promote draft,更新后的 `<harnessRoot>/state/plan.draft.yaml`
|
|
@@ -72,7 +77,7 @@ description: Use during SPRINTING to execute one task from plan.yaml, respecting
|
|
|
72
77
|
1. `current_task_id` 指向正在执行的 open task。
|
|
73
78
|
2. open task 直接声明 `phase: "SPRINTING"`、`docs`、`allowed_paths`、`required_gates`、`acceptance_criteria` 和 `implementation_doc`;runtime/app/provider/live 类 task 还必须声明 `evidence_level`、`target_runtime_environment` 和 `self_test_contract`。`self_test_contract.required_gates` 必须同步出现在 task `required_gates`,`self_test_contract.module_key_test_path` 必须描述从本地启动或调用入口开始,到完成所有自测 scenario 的模块关键测试路径,并覆盖本 task / 本模块承诺的所有可运行入口和内部关键路径。
|
|
74
79
|
3. 如果 open task 是由 `plan.draft.yaml.tasks[]` promote 而来,创建正式 `TASK-*` 和删除源 draft 必须发生在同一次状态更新中;正式 task 的恢复现场只保存在 `plan.yaml`。
|
|
75
|
-
4. 任务执行中只保留恢复所需的简短 `working_notes
|
|
80
|
+
4. 任务执行中只保留恢复所需的简短 `working_notes`,目标 5-8 条且不得超过 8 条;high-risk runtime/live task 用 `resume_capsule` 保存恢复卡片,并链接 runbook / evidence index / exploration appendix。
|
|
76
81
|
5. gate、implementation doc、`.docs/INDEX.md` 和 `overview.md` 完成后,在当前 task 仍位于 `plan.yaml` 时创建 task implementation commit。
|
|
77
82
|
6. implementation commit 完成后,再把该 task 从 `plan.yaml` 的 `tasks` 列表移除,并保留/递增 `next_task_sequence`。
|
|
78
83
|
7. 将移除当前 task 后的 `plan.yaml` 提交为 task completion ledger commit,并 `git push` 两个 commit 到当前 upstream branch。
|
|
@@ -108,7 +113,8 @@ done task 的执行流水不在当前 `plan.yaml` 长期保留,也不是默认
|
|
|
108
113
|
- [ ] 当前任务仍然是单一清晰的执行单元。
|
|
109
114
|
- [ ] 技术方案或 task 承诺的 API/CLI/adapter/worker/provider、配置契约、输出/副作用和 fixture/live 边界已可运行并写入 implementation doc,或已明确 `BLOCKED`/后续 dev task。
|
|
110
115
|
- [ ] implementation doc `Development Evidence` 已记录 `Evidence Level`、`Target Runtime Environment`、`Runnable Entry`、`Observable Exit`、`Client / Server Initialization`、`Config Contract`、`Testing Handoff Readiness`、`Known Missing Runtime Boundaries`、`Basic Self-test Evidence`,或写明带原因的 `Not applicable`。
|
|
111
|
-
- [ ] 如果当前 task 有 `self_test_contract.status: "required"
|
|
116
|
+
- [ ] 如果当前 task 有 `self_test_contract.status: "required"`,已逐条执行当前 `self_test_contract.scenarios[]` 和 `self_test_contract.required_gates`,并在 implementation doc `Development Self-Test Report` 写入 `Report Status`、本轮 contract source、scenario results、executed gates、runnable entry、内部关键路径、observable exit/evidence、Module Key Test Path、missing/blockers 和 Testing Handoff Readiness,且 `Report Status: PASS`、所有 scenario 都是 `PASS`。
|
|
117
|
+
- [ ] 如果当前 task 是 high-risk runtime/live/remote-operator 工作,`resume_capsule` 已更新为 5-8 条恢复事实,`recovery_refs` 链接 implementation doc 和 `.docs/09_runbooks/**` runbook/evidence,implementation doc 已填写短的 `Current Operator Path` 和分层 `Gate Breakdown`。
|
|
112
118
|
- [ ] 如果 task 要求 `business_handoff_ready`,implementation doc 已写入 Testing Handoff Contract,包含入口、配置、初始化/health、输入样例、预期出口、清理方式和证据等级。
|
|
113
119
|
- [ ] 如果当前 task 来自 `plan.draft.yaml.tasks[]`,源 draft 已在 promote 时从 draft 列表删除。
|
|
114
120
|
- [ ] implementation doc 已生成或更新,并反映相关模块的真实代码。
|
|
@@ -17,9 +17,11 @@ description: Use after development gates pass to update module-level implementat
|
|
|
17
17
|
|
|
18
18
|
文档应帮助后来者快速理解:某个模块或核心数据流的当前实现是什么、关键对象/函数职责是什么、行为如何从输入流到输出、测试覆盖了什么、还有什么未覆盖。task id 只作为 provenance,不作为默认切片粒度。
|
|
19
19
|
|
|
20
|
+
implementation doc 只写长期实现事实,不写完整操作日记。对于 high-risk runtime/live/remote-operator task,主线只保留当前 canonical path、当前实现边界、短的 `Current Operator Path` 和指向 `plan.yaml#resume_capsule`、`.docs/09_runbooks/**` runbook、evidence index、exploration appendix 的链接;失败路径和探索细节进入 exploration appendix,证据正文进入 evidence index 或外部系统。恢复入口必须比探索历史更显眼。
|
|
21
|
+
|
|
20
22
|
如果模块包含或承诺可运行系统边界,implementation doc 必须记录 runnable entry/exit:API/CLI/server route/service/agent/runtime/adapter/worker/provider 的调用方式、初始化方式、配置契约、输入来源、输出或副作用、fixture/live 模式边界,以及哪些真实外部执行器尚未实现。还必须在 `Development Evidence` 中记录开发阶段实际验证过的 `Evidence Level`、`Target Runtime Environment`、`Runnable Entry`、`Observable Exit`、`Client / Server Initialization`、`Config Contract`、`Testing Handoff Readiness`、`Known Missing Runtime Boundaries` 和 `Basic Self-test Evidence`;`Basic Self-test Evidence` 应指向已执行的 `Development Self-Test Report`。确实没有应用入口时,`Not applicable` 必须写清原因。不能把未来才会实现的入口写成当前事实,不能把 provider smoke、fixture smoke、fake adapter 或 one-shot smoke 单独写成 application readiness。如果 task 要求 `business_handoff_ready`,还必须写 Testing Handoff Contract,包含入口、配置、初始化/health、输入样例、预期出口、清理/reset/幂等说明和证据等级。
|
|
21
23
|
|
|
22
|
-
如果当前 task 有 `self_test_contract.status: "required"`,implementation doc 必须填写 `Development Self-Test Report`,把设计/RFC 阶段定义的自测合同执行完成:记录 contract source、每个 scenario
|
|
24
|
+
如果当前 task 有 `self_test_contract.status: "required"`,implementation doc 必须填写 `Development Self-Test Report`,把设计/RFC 阶段定义的自测合同执行完成:记录 `Report Status: PASS | BLOCKED | IN_PROGRESS | STALE`、contract source、每个 scenario 的结果、实际执行入口、实际出口、证据位置或命令输出、executed gates、Module Key Test Path、missing/blockers 和 Testing Handoff Readiness。`Development Self-Test Report` 不是 debug log、operator log、runbook 或历史流水;fallback / diagnostic 在主报告最多一句总结,详细命令、截图过程、UI 操作细节和失败路径进入 exploration appendix 或 git history。High-risk runtime/live task 还必须写 `Current Operator Path` 和 `Gate Breakdown`,把 canonical operator path、runbook link、credential reference name、command/UI channel、do-not-retry summary 以及 local gate、cloud/service gate、executor/operator readiness、live smoke / handoff 分层记录,不能只写一个大 `validate-dev PASS`。`Development Self-Test Report` 只能记录当前 task 本轮实际执行后的结果;不得用历史报告、模板字段、代码阅读或无关通用 gate 替代本轮 self-test scenario 执行。`Module Key Test Path` 必须说明从本地启动或调用入口开始,执行并完成 `self_test_contract` 中全部自测用例的模块关键测试路径。该路径应覆盖本 task / 本模块承诺的所有可运行入口,以及自测用例实际经过的内部关键路径、关键边界、观察点和可观测完成证据,供后续 Agent 复用和 debug。任何 scenario 非 `PASS`,或 `Report Status` 为 `BLOCKED`、`IN_PROGRESS`、`STALE` 时,不得把开发 task 写成完成。
|
|
23
25
|
|
|
24
26
|
## 输入
|
|
25
27
|
|
|
@@ -51,7 +53,7 @@ description: Use after development gates pass to update module-level implementat
|
|
|
51
53
|
3. 与技术方案的偏移必须明确记录,即便该偏移是合理的。
|
|
52
54
|
4. runnable entry/exit、配置契约和 fixture/live 边界必须记录当前事实;缺失项写入 `未覆盖(Not covered)` 或方案偏移。
|
|
53
55
|
5. `Development Evidence` 必须包含 task 合同要求的证据等级、目标运行环境、实际可调用入口、可观察出口、初始化方式、配置契约、测试交接状态、缺失 runtime 边界和开发自测证据;页面类任务记录 dev server/page URL 与 browser check,API/CLI/worker/RPA/service/agent/runtime 类任务记录 startup/invocation command、endpoint/health/status 与 response/output/side effect。
|
|
54
|
-
6. `Development Self-Test Report`
|
|
56
|
+
6. `Development Self-Test Report` 必须记录 `Report Status`、当前 task 本轮执行 `self_test_contract` 中全部 scenario 和 required gates 后的结果,并记录从本地启动或调用入口开始,到完成所有自测用例的 `Module Key Test Path`;路径必须覆盖本 task / 本模块承诺的所有可运行入口、内部关键路径、关键边界、观察点和完成证据,不能只补一句 smoke 结果,也不能复用历史 PASS、模板字段、代码阅读或无关通用 gate 作为本轮证据。
|
|
55
57
|
7. 测试覆盖必须列出具体测试,或明确记录覆盖缺口。
|
|
56
58
|
8. 文档粒度保持在模块、子系统或核心数据流级别;不要默认按 task 建文档,也不要写成跨全项目的巨型百科。
|
|
57
59
|
|
|
@@ -63,7 +65,8 @@ description: Use after development gates pass to update module-level implementat
|
|
|
63
65
|
- [ ] 核心数据流已说明。
|
|
64
66
|
- [ ] runnable entry/exit、配置契约和 fixture/live 边界已记录,或缺失项已明确标注。
|
|
65
67
|
- [ ] `Development Evidence` 已记录 `Evidence Level`、`Target Runtime Environment`、`Runnable Entry`、`Observable Exit`、`Client / Server Initialization`、`Config Contract`、`Testing Handoff Readiness`、`Known Missing Runtime Boundaries`、`Basic Self-test Evidence`,或带原因的 `Not applicable`。
|
|
66
|
-
- [ ] 如果当前 task 有 `self_test_contract.status: "required"`,`Development Self-Test Report` 已记录 contract source、scenario results、executed gates、Module Key Test Path、actual evidence、missing/blockers 和 Testing Handoff Readiness。
|
|
68
|
+
- [ ] 如果当前 task 有 `self_test_contract.status: "required"`,`Development Self-Test Report` 已记录 `Report Status`、contract source、scenario results、executed gates、Module Key Test Path、actual evidence、missing/blockers 和 Testing Handoff Readiness。
|
|
69
|
+
- [ ] 如果当前 task 是 high-risk runtime/live/remote-operator 工作,implementation doc 主线只保留实现事实、`Current Operator Path` 和恢复链接,`Gate Breakdown` 已分层记录,本轮失败探索已隔离到 exploration appendix。
|
|
67
70
|
- [ ] `business_handoff_ready` task 已记录 Testing Handoff Contract。
|
|
68
71
|
- [ ] 已判断 implementation doc 的语义切片边界。
|
|
69
72
|
- [ ] 方案偏移和测试覆盖已记录。
|
|
@@ -17,7 +17,7 @@ Review 时先建立证据链:PRD 说什么、技术方案承诺什么、implem
|
|
|
17
17
|
|
|
18
18
|
不要把个人偏好包装成 blocker。区分 blocking issue、follow-up improvement 和 open question。如果没有发现问题,要明确说明,同时列出剩余测试缺口或残余风险。
|
|
19
19
|
|
|
20
|
-
Review 必须把“当前模块没有可运行入口/出口”视为阻断项,而不是普通测试缺口。凡 PRD、技术方案或 implementation doc 承诺 API、CLI、server route、service、agent、runtime、adapter、worker、provider、外部发送/写入执行器、配置契约或 live/fixture 双模式边界,Review 都要读取技术方案的 `Development Deliverable Contract`、`Development Self-Test Contract` 或等价交付边界,并核对真实代码和实现文档是否提供可调用入口、初始化方式、输出/副作用边界和验证方式;如果 task 声明了 `evidence_level.required`、`target_runtime_environment` 或 `self_test_contract`,还必须核对实际证据等级、执行地点、目标运行环境、自测 scenario 结果、`module_key_test_path` 和 required gates 是否匹配。implementation doc 还必须包含结构化 `Development Evidence`,说明 `Evidence Level`、`Target Runtime Environment`、`Runnable Entry`、`Observable Exit`、`Client / Server Initialization`、`Config Contract`、`Testing Handoff Readiness`、`Known Missing Runtime Boundaries` 和 `Basic Self-test Evidence`,或带原因的 `Not applicable`;如果 task 有 `self_test_contract.status: "required"`,还必须包含已执行的 `Development Self-Test Report
|
|
20
|
+
Review 必须把“当前模块没有可运行入口/出口”视为阻断项,而不是普通测试缺口。凡 PRD、技术方案或 implementation doc 承诺 API、CLI、server route、service、agent、runtime、adapter、worker、provider、外部发送/写入执行器、配置契约或 live/fixture 双模式边界,Review 都要读取技术方案的 `Development Deliverable Contract`、`Development Self-Test Contract` 或等价交付边界,并核对真实代码和实现文档是否提供可调用入口、初始化方式、输出/副作用边界和验证方式;如果 task 声明了 `evidence_level.required`、`target_runtime_environment` 或 `self_test_contract`,还必须核对实际证据等级、执行地点、目标运行环境、自测 scenario 结果、`module_key_test_path` 和 required gates 是否匹配。high-risk runtime/live/remote-operator 工作要先看 `plan.yaml#resume_capsule` 和 `.docs/09_runbooks/**` runbook,确认 canonical path、do-not-retry、evidence index 和 exploration appendix 已把恢复主线与失败探索分开;Review 不应从失败探索中重新选择主路径。implementation doc 还必须包含结构化 `Development Evidence`,说明 `Evidence Level`、`Target Runtime Environment`、`Runnable Entry`、`Observable Exit`、`Client / Server Initialization`、`Config Contract`、`Testing Handoff Readiness`、`Known Missing Runtime Boundaries` 和 `Basic Self-test Evidence`,或带原因的 `Not applicable`;如果 task 有 `self_test_contract.status: "required"`,还必须包含已执行的 `Development Self-Test Report`,并记录 `Report Status`、从本地启动或调用入口开始到完成全部自测用例的 `Module Key Test Path`。该报告不是 debug log、operator log、runbook 或历史流水;看到这些混入 scenario evidence 时应判为 blocker。`Report Status` 必须是 `PASS` 且所有 scenario 都是 `PASS` 才能进入 TESTING。该路径应覆盖本 task / 本模块承诺的所有可运行入口、内部关键路径、关键边界、观察点和可观测完成证据;high-risk task 还必须包含短的 `Current Operator Path` 和分层 `Gate Breakdown`。如果 task 要求 `deployed_runtime` 或 `business_handoff_ready`,但证据只是在开发机 `localhost`、provider live smoke、fixture smoke、fake adapter 或文档描述,应判 `BLOCKED`。缺失时 gate decision 应为 `BLOCKED`,并要求回到 SPRINTING/RFC,而不是允许进入 TESTING 后补 runtime。Review 报告必须写出 `Runnable Entry`、`Observable Exit`、`Initialization`、`Config Contract`、`Testing Handoff Readiness` 的 `PASS`/`BLOCKED` checklist;任一 `BLOCKED` 不得进入 TESTING。Review 不创建 `.docs/07_test/**` 正式测试产物;如果发现现有测试事实源仍链接已被 RFC supersede 的旧路线证据,应将其列为进入 TESTING 前的 blocker,并要求 RFC 清理或更新索引。
|
|
21
21
|
|
|
22
22
|
Review 产出本身也是 workflow task。开始 review 前,先在 `<harnessRoot>/state/plan.yaml` 创建或选择一个足够小的 `TASK-*` open task,并设置 `phase: "REVIEWING"`;当前轮只产出一个 review batch、一个风险主题 slice 或一次 PR review 结论。不要在一个任务里覆盖多个互不相关的 review 主题。
|
|
23
23
|
|
|
@@ -81,7 +81,7 @@ Review 阶段受 `plan.yaml` 管控:
|
|
|
81
81
|
- [ ] 已评估架构和可维护性风险。
|
|
82
82
|
- [ ] 已评估 runnable entry/exit、配置契约和 fixture/live 边界是否足以进入 TESTING。
|
|
83
83
|
- [ ] 已评估 implementation doc 是否包含 Evidence Level、Target Runtime Environment、Runnable Entry、Observable Exit、Client / Server Initialization、Config Contract、Testing Handoff Readiness、Known Missing Runtime Boundaries 和 Basic Self-test Evidence。
|
|
84
|
-
- [ ] 已评估 `self_test_contract` 对应的 Development Self-Test Report
|
|
84
|
+
- [ ] 已评估 `self_test_contract` 对应的 Development Self-Test Report 是否 `Report Status: PASS`、执行全部 scenario 和 required gates,并记录可复用的 Module Key Test Path。
|
|
85
85
|
- [ ] 已核对证据等级和执行地点是否匹配 task / 技术方案承诺的目标运行环境。
|
|
86
86
|
- [ ] 已判断 review slice 的范围和风险主题边界。
|
|
87
87
|
- [ ] 已列出测试缺口。
|
|
@@ -17,7 +17,7 @@ description: Use during TESTING to produce a test matrix, run regression, and do
|
|
|
17
17
|
|
|
18
18
|
执行回归时,优先选择能证明阶段出口的 gate。测试无法运行、环境缺失或数据不可得时,不要宣布通过;如果已经进入 TESTING,应在 `TEST_REPORT.md` 中记录 `BLOCKED`、已完成检查和恢复条件。
|
|
19
19
|
|
|
20
|
-
TESTING 只能调用 SPRINTING/REVIEWING 已确认 `PASS` 的入口做输入/输出验证。可以补充测试、fixture、mock、assertion helper 和测试文档,但不能在 TESTING 中新增或长期维护 product runtime、server/API/CLI/adapter、direct poller、cloud bootstrap、systemd unit、真实 provider adapter、package runtime script 或部署脚本。如果发现真实入口/出口不存在、implementation doc 缺少 `Development Evidence` 或 `Development Self-Test Report
|
|
20
|
+
TESTING 只能调用 SPRINTING/REVIEWING 已确认 `PASS` 的入口做输入/输出验证。可以补充测试、fixture、mock、assertion helper 和测试文档,但不能在 TESTING 中新增或长期维护 product runtime、server/API/CLI/adapter、direct poller、cloud bootstrap、systemd unit、真实 provider adapter、package runtime script 或部署脚本。如果发现真实入口/出口不存在、implementation doc 缺少 `Development Evidence` 或 `Development Self-Test Report`、自测报告缺少 `Report Status: PASS`、缺少从本地启动或调用入口到完成全部自测用例的 `Module Key Test Path`、或该路径没有覆盖本 task / 本模块承诺的入口、内部关键路径、关键边界、观察点和完成证据,live 模式不可调用、配置契约缺失、Review readiness checklist 不是全 `PASS`,或 `Evidence Level` / `Target Runtime Environment` / `self_test_contract` 与 task 或技术方案承诺不一致,应记录 `BLOCKED`、生成 RFC 或后续 dev task 建议,并停止把测试阶段扩大成开发/集成搭建。`Development Self-Test Report` 不是 debug log、operator log、runbook 或探索流水;测试只消费其模块入口、核心路径、出口和最小证据。high-risk runtime/live/remote-operator 验证要先读 `plan.yaml#resume_capsule`,再读 `.docs/09_runbooks/**` runbook 和 evidence index,最后才读 exploration appendix;测试只沿 canonical path 验证,不重新尝试 `do_not_retry` 中的失败路径。开发尚未交付可测试 entry/exit、目标运行环境、Development Self-Test Report 或 Testing Handoff Contract 时,不要在 `.docs/07_test/**` 提前生成正式测试用例或正式报告;验收思路应留在 PRD acceptance criteria、tech plan verification strategy 或非 `.docs/07_test/**` 的草稿说明里。`TEST_REPORT.md` 不能在描述缺少 entry/exit、缺少 Development Evidence、缺少 Development Self-Test Report、证据等级不匹配或未交付应用入口时给出 `PASS`。
|
|
21
21
|
|
|
22
22
|
测试设计和回归证据产出本身也是 workflow task。开始测试前,先在 `<harnessRoot>/state/plan.yaml` 创建或选择一个足够小的 `TASK-*` open task,并设置 `phase: "TESTING"`;当前轮只产出一个测试策略 slice、测试用例 slice、回归批次、风险验证片区或一组 scoped test changes。`plan.yaml` 仍是唯一执行计划事实源,`.docs/07_test/**` 只记录当前方案的 test strategy、test cases、executed regression evidence、coverage gaps 和 final decision,不表达“下一步如何开发”,也不保留已被 RFC supersede 的旧测试结果。
|
|
23
23
|
|
|
@@ -87,7 +87,8 @@ TESTING 只能调用 SPRINTING/REVIEWING 已确认 `PASS` 的入口做输入/输
|
|
|
87
87
|
- [ ] Regression checklist 已完成。
|
|
88
88
|
- [ ] 测试只调用既有 runnable entry/exit;未在 TESTING 中新增 product runtime、bootstrap、provider adapter、deploy 或 package runtime script。
|
|
89
89
|
- [ ] 已核对 implementation doc 中的 Development Evidence、Evidence Level、Target Runtime Environment 和 Testing Handoff Contract,并只基于已交付入口设计测试。
|
|
90
|
-
- [ ] 已核对 Development Self-Test Report 中 scenario results、executed gates、Module Key Test Path 和 actual evidence。
|
|
90
|
+
- [ ] 已核对 Development Self-Test Report 中 Report Status、scenario results、executed gates、Module Key Test Path 和 actual evidence。
|
|
91
|
+
- [ ] high-risk runtime/live 验证已优先使用 `resume_capsule` 与 runbook/evidence index,未重复执行 exploration appendix 中的失败路径。
|
|
91
92
|
- [ ] 已判断 test report / test matrix 的语义切片边界。
|
|
92
93
|
- [ ] 未把测试计划、测试用例或待填内容写成 `TEST_REPORT.md`。
|
|
93
94
|
- [ ] 已确认 `.docs/07_test/**` 只包含当前方案仍有效的测试事实。
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# [Runtime / Live Smoke] Evidence Index
|
|
2
|
+
|
|
3
|
+
本文件只保存证据指针和缺口,不把证据正文塞回 implementation doc 主线。
|
|
4
|
+
|
|
5
|
+
| Scenario | Status | Evidence File / System | Gap / Next Action |
|
|
6
|
+
|---|---|---|---|
|
|
7
|
+
| | PASS / BLOCKED / GAP | | |
|
|
8
|
+
|
|
9
|
+
## Evidence Retention
|
|
10
|
+
|
|
11
|
+
- Temporary evidence:
|
|
12
|
+
- Stable artifact / CI / release record:
|
|
13
|
+
- Evidence that must not be copied into main docs:
|
|
14
|
+
|
|
15
|
+
## Missing Evidence
|
|
16
|
+
|
|
17
|
+
-
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# [Runtime / Operator Path] Exploration Appendix
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
记录失败尝试、诊断路径和不要重复踩坑的结论。本文件是记忆附录,不是恢复主线。
|
|
6
|
+
不要把本文件的长日志复制进 `Development Self-Test Report`;主报告最多保留一句 fallback / diagnostic 总结。
|
|
7
|
+
|
|
8
|
+
## Failed / Diagnostic Attempts
|
|
9
|
+
|
|
10
|
+
| Attempt | Result | Why It Failed Or Stayed Diagnostic | Do Not Retry Rule |
|
|
11
|
+
|---|---|---|---|
|
|
12
|
+
| | | | |
|
|
13
|
+
|
|
14
|
+
## Useful Observations
|
|
15
|
+
|
|
16
|
+
-
|
|
17
|
+
|
|
18
|
+
## Promoted Decisions
|
|
19
|
+
|
|
20
|
+
| Decision | Promoted To |
|
|
21
|
+
|---|---|
|
|
22
|
+
| | `plan.yaml#resume_capsule` / runbook / implementation doc |
|
|
@@ -51,10 +51,22 @@ Input
|
|
|
51
51
|
- Testing Handoff Readiness:
|
|
52
52
|
- Known Missing Runtime Boundaries:
|
|
53
53
|
- Basic Self-test Evidence: See `Development Self-Test Report`.
|
|
54
|
+
- Resume Capsule / Runbook:
|
|
54
55
|
- Not applicable:
|
|
55
56
|
|
|
56
|
-
## 7.
|
|
57
|
+
## 7. Current Operator Path(当前操作路径,仅 runtime/live/remote-operator 需要)
|
|
57
58
|
|
|
59
|
+
- Canonical path:
|
|
60
|
+
- Operator runbook: `.docs/09_runbooks/...`
|
|
61
|
+
- Credential reference: Keychain item name 或 secret reference name only;不要记录明文密钥。
|
|
62
|
+
- Command/UI channel:
|
|
63
|
+
- Do-not-retry summary: fallback / diagnostic 只写一句结论,详细内容进 exploration appendix 或 git history。
|
|
64
|
+
|
|
65
|
+
## 8. Development Self-Test Report(开发自测报告)
|
|
66
|
+
|
|
67
|
+
本节只证明模块入口、核心路径、出口和最小证据,不是 debug log、operator log、runbook 或探索流水。
|
|
68
|
+
|
|
69
|
+
- Report Status: PASS | BLOCKED | IN_PROGRESS | STALE
|
|
58
70
|
- Contract Source:
|
|
59
71
|
- Scenario Results:
|
|
60
72
|
- Executed Gates:
|
|
@@ -63,11 +75,31 @@ Input
|
|
|
63
75
|
- Missing / Blockers:
|
|
64
76
|
- Testing Handoff Readiness:
|
|
65
77
|
|
|
78
|
+
保留:
|
|
79
|
+
- Runnable Entry / Module Key Test Path / Observable Exit
|
|
80
|
+
- Scenario Results / Executed Gates / Actual Evidence
|
|
81
|
+
- Missing / Blockers / Testing Handoff Readiness
|
|
82
|
+
|
|
83
|
+
不保留:
|
|
84
|
+
- 每次工具探索的完整流水
|
|
85
|
+
- debug log、operator log、历史操作日记或 runbook 正文
|
|
86
|
+
- fallback / diagnostic 的长篇命令、截图过程或 UI 细节
|
|
87
|
+
- 与当前恢复路径无关的旧失败通道;只在 appendix 或 git history 保留
|
|
88
|
+
|
|
89
|
+
### Gate Breakdown(Gate 分层)
|
|
90
|
+
|
|
91
|
+
| Gate Layer | Status | Evidence | Gap / Next Action |
|
|
92
|
+
|---|---|---|---|
|
|
93
|
+
| Local gate | | | |
|
|
94
|
+
| Cloud/service gate | | | |
|
|
95
|
+
| Executor/operator readiness | | | |
|
|
96
|
+
| Live smoke / handoff | | | |
|
|
97
|
+
|
|
66
98
|
| Scenario ID | Result | Executed Entry | Actual Exit | Evidence |
|
|
67
99
|
|---|---|---|---|---|
|
|
68
|
-
|
|
|
100
|
+
| | | | | |
|
|
69
101
|
|
|
70
|
-
##
|
|
102
|
+
## 9. Testing Handoff Contract(测试交接合同)
|
|
71
103
|
|
|
72
104
|
- Entry:
|
|
73
105
|
- Config:
|
|
@@ -77,7 +109,7 @@ Input
|
|
|
77
109
|
- Cleanup / reset / idempotency:
|
|
78
110
|
- Evidence Level:
|
|
79
111
|
|
|
80
|
-
##
|
|
112
|
+
## 10. 关键实现逻辑
|
|
81
113
|
|
|
82
114
|
- 输入校验(Input validation):
|
|
83
115
|
- 核心分支(Core branches):
|
|
@@ -85,22 +117,22 @@ Input
|
|
|
85
117
|
- 边界兜底(Boundary fallback):
|
|
86
118
|
- 性能或并发注意事项(Performance or concurrency notes):
|
|
87
119
|
|
|
88
|
-
##
|
|
120
|
+
## 11. 与技术方案的偏移
|
|
89
121
|
|
|
90
122
|
-
|
|
91
123
|
|
|
92
|
-
##
|
|
124
|
+
## 12. 测试覆盖(Test Coverage)
|
|
93
125
|
|
|
94
126
|
| 测试(Test) | 覆盖范围(Coverage) | 结果(Result) |
|
|
95
127
|
|---|---|---|
|
|
96
128
|
| | | |
|
|
97
129
|
|
|
98
|
-
##
|
|
130
|
+
## 13. 变更记录(Change Log)
|
|
99
131
|
|
|
100
132
|
| 日期(Date) | Task ID | Commit | 摘要(Summary) |
|
|
101
133
|
|---|---|---|---|
|
|
102
134
|
| | | | |
|
|
103
135
|
|
|
104
|
-
##
|
|
136
|
+
## 14. 后续维护注意事项
|
|
105
137
|
|
|
106
138
|
-
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
current_task_id: "TASK-001"
|
|
2
2
|
next_task_sequence: 2
|
|
3
|
+
# Required while the current SPRINTING task is high-risk runtime/live work
|
|
4
|
+
# (`external_provider_live`, `deployed_runtime`, `business_handoff_ready`, or
|
|
5
|
+
# target runtime `cloud_vm` / `managed_service` / `browser` / `worker`). Keep this
|
|
6
|
+
# short: 5-8 recovery facts, not a full attempt log.
|
|
7
|
+
# resume_capsule:
|
|
8
|
+
# task_id: "TASK-001"
|
|
9
|
+
# state: "in_progress | blocked | ready_for_gate"
|
|
10
|
+
# canonical_path: "operator/runtime path to continue from"
|
|
11
|
+
# next_step: "one concrete next action"
|
|
12
|
+
# blocker: "current blocker, or none with context"
|
|
13
|
+
# last_passed_gate: "last concrete PASS gate or checkpoint"
|
|
14
|
+
# do_not_retry:
|
|
15
|
+
# - "known failed path or repeated trap to avoid"
|
|
16
|
+
# recovery_refs:
|
|
17
|
+
# - ".docs/04_implementation/example.md"
|
|
18
|
+
# - ".docs/09_runbooks/example_live_smoke_runbook.md"
|
|
3
19
|
# Optional top-level execution contract. Omit this block when the current task
|
|
4
20
|
# stays serial after the default parallel eligibility check. Use
|
|
5
21
|
# trigger: "workflow_default" when the workflow safely splits the task by
|
|
@@ -46,6 +62,7 @@ tasks:
|
|
|
46
62
|
architecture: []
|
|
47
63
|
tech_plan: []
|
|
48
64
|
rfc: []
|
|
65
|
+
runbook: []
|
|
49
66
|
allowed_paths:
|
|
50
67
|
- ".docs/00_raw/**"
|
|
51
68
|
- ".docs/01_product/**"
|
|
@@ -85,6 +102,6 @@ tasks:
|
|
|
85
102
|
evidence: "command/browser/API/log/screenshot/etc"
|
|
86
103
|
not_applicable_reason: ""
|
|
87
104
|
working_notes:
|
|
88
|
-
- "
|
|
105
|
+
- "执行现场备注只保留恢复所需的短备注;目标 5-8 条,validator 上限 8 条;路径选择结论提升到 resume_capsule。"
|
|
89
106
|
result_docs:
|
|
90
107
|
- ".docs/01_product/example.md"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# [Runtime / Operator Path] Runbook
|
|
2
|
+
|
|
3
|
+
本文件记录 operator/provisioning 恢复路径,不是 `Development Self-Test Report` 或 scenario evidence。
|
|
4
|
+
|
|
5
|
+
## 1. Recovery Summary
|
|
6
|
+
|
|
7
|
+
- Canonical path:
|
|
8
|
+
- Current state:
|
|
9
|
+
- Next command channel:
|
|
10
|
+
- Last known good checkpoint:
|
|
11
|
+
- Primary blocker:
|
|
12
|
+
|
|
13
|
+
## 2. Operator Path
|
|
14
|
+
|
|
15
|
+
```txt
|
|
16
|
+
canonical:
|
|
17
|
+
credentials: Keychain item name or secret reference only
|
|
18
|
+
remote host:
|
|
19
|
+
command channel:
|
|
20
|
+
UI channel:
|
|
21
|
+
do not prefer:
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 3. Preconditions
|
|
25
|
+
|
|
26
|
+
- Required access:
|
|
27
|
+
- Required local tools:
|
|
28
|
+
- Required remote services:
|
|
29
|
+
- Safety / cleanup notes:
|
|
30
|
+
|
|
31
|
+
## 4. Resume Steps
|
|
32
|
+
|
|
33
|
+
1.
|
|
34
|
+
2.
|
|
35
|
+
3.
|
|
36
|
+
|
|
37
|
+
## 5. Fallbacks And Diagnostics
|
|
38
|
+
|
|
39
|
+
- Preferred fallback:
|
|
40
|
+
- Diagnostic-only paths:
|
|
41
|
+
- Do not retry:
|
|
42
|
+
|
|
43
|
+
## 6. Linked Evidence
|
|
44
|
+
|
|
45
|
+
- Evidence index:
|
|
46
|
+
- Exploration appendix:
|
|
47
|
+
- Implementation doc:
|
|
@@ -375,6 +375,20 @@ CALLABLE_TASK_TERMS = [
|
|
|
375
375
|
"队列",
|
|
376
376
|
]
|
|
377
377
|
SELF_TEST_CONTRACT_STATUSES = {"required", "not_applicable"}
|
|
378
|
+
RESUME_CAPSULE_REQUIRED_EVIDENCE_LEVELS = {"external_provider_live", "deployed_runtime", "business_handoff_ready"}
|
|
379
|
+
RESUME_CAPSULE_REQUIRED_TARGET_KINDS = {"cloud_vm", "managed_service", "browser", "worker"}
|
|
380
|
+
RESUME_CAPSULE_FIELDS = [
|
|
381
|
+
"task_id",
|
|
382
|
+
"state",
|
|
383
|
+
"canonical_path",
|
|
384
|
+
"next_step",
|
|
385
|
+
"blocker",
|
|
386
|
+
"last_passed_gate",
|
|
387
|
+
"do_not_retry",
|
|
388
|
+
"recovery_refs",
|
|
389
|
+
]
|
|
390
|
+
RUNBOOK_DOC_PREFIX = ".docs/09_runbooks/"
|
|
391
|
+
MAX_WORKING_NOTES = 8
|
|
378
392
|
|
|
379
393
|
|
|
380
394
|
def as_string_list(value: Any) -> list[str]:
|
|
@@ -417,6 +431,16 @@ def needs_runnable_task_contract(task: dict[str, Any]) -> bool:
|
|
|
417
431
|
return contains_any(context, APPLICATION_READINESS_TASK_TERMS + PAGE_TASK_TERMS + CALLABLE_TASK_TERMS)
|
|
418
432
|
|
|
419
433
|
|
|
434
|
+
def requires_resume_capsule(task: dict[str, Any]) -> bool:
|
|
435
|
+
if task.get("phase") != "SPRINTING":
|
|
436
|
+
return False
|
|
437
|
+
evidence_level = task.get("evidence_level")
|
|
438
|
+
target_runtime = task.get("target_runtime_environment")
|
|
439
|
+
required = str(evidence_level.get("required") or "") if isinstance(evidence_level, dict) else ""
|
|
440
|
+
kind = str(target_runtime.get("kind") or "") if isinstance(target_runtime, dict) else ""
|
|
441
|
+
return required in RESUME_CAPSULE_REQUIRED_EVIDENCE_LEVELS or kind in RESUME_CAPSULE_REQUIRED_TARGET_KINDS
|
|
442
|
+
|
|
443
|
+
|
|
420
444
|
def self_test_contract_errors_for_task(task: dict[str, Any]) -> list[str]:
|
|
421
445
|
task_id = str(task.get("id") or "Task")
|
|
422
446
|
required_for_runnable = needs_runnable_task_contract(task)
|
|
@@ -658,6 +682,16 @@ def validate_task_shape(task: dict[str, Any], index: int) -> None:
|
|
|
658
682
|
require(isinstance(task["allowed_paths"], list) and task["allowed_paths"], f"{task['id']} must define allowed_paths")
|
|
659
683
|
require(isinstance(task["required_gates"], list) and task["required_gates"], f"{task['id']} must define required_gates")
|
|
660
684
|
require(isinstance(task["acceptance_criteria"], list) and task["acceptance_criteria"], f"{task['id']} must define acceptance_criteria")
|
|
685
|
+
if "working_notes" in task:
|
|
686
|
+
require(
|
|
687
|
+
isinstance(task["working_notes"], (list, str)),
|
|
688
|
+
f"{task['id']} working_notes must be a short string or list with at most {MAX_WORKING_NOTES} items",
|
|
689
|
+
)
|
|
690
|
+
note_count = len(task["working_notes"]) if isinstance(task["working_notes"], list) else (1 if str(task["working_notes"]).strip() else 0)
|
|
691
|
+
require(
|
|
692
|
+
note_count <= MAX_WORKING_NOTES,
|
|
693
|
+
f"{task['id']} working_notes must stay resume-first and contain at most {MAX_WORKING_NOTES} items; found {note_count}",
|
|
694
|
+
)
|
|
661
695
|
for error in self_test_contract_errors_for_task(task):
|
|
662
696
|
require(False, error)
|
|
663
697
|
for error in testing_boundary_errors_for_allowed_paths(task):
|
|
@@ -672,6 +706,54 @@ def task_sequence_number(task_id: str) -> int:
|
|
|
672
706
|
return int(match.group(1)) if match else 0
|
|
673
707
|
|
|
674
708
|
|
|
709
|
+
def validate_resume_capsule_contract(data: dict[str, Any]) -> None:
|
|
710
|
+
current_task_id = str(data.get("current_task_id") or "")
|
|
711
|
+
current_task = task_by_id(data, current_task_id) if current_task_id else None
|
|
712
|
+
if not current_task or current_task.get("status") not in OPEN_TASK_STATUSES or current_task.get("phase") != "SPRINTING":
|
|
713
|
+
require("resume_capsule" not in data, "plan.yaml resume_capsule must only be present for the current open SPRINTING task")
|
|
714
|
+
return
|
|
715
|
+
|
|
716
|
+
capsule = data.get("resume_capsule")
|
|
717
|
+
if not requires_resume_capsule(current_task):
|
|
718
|
+
if capsule is not None:
|
|
719
|
+
require(isinstance(capsule, dict), f"{current_task_id} resume_capsule must be a mapping when present")
|
|
720
|
+
return
|
|
721
|
+
|
|
722
|
+
require(isinstance(capsule, dict), f"{current_task_id} high-risk runtime task must define top-level resume_capsule")
|
|
723
|
+
for field in RESUME_CAPSULE_FIELDS:
|
|
724
|
+
require(field in capsule, f"{current_task_id} resume_capsule missing field: {field}")
|
|
725
|
+
|
|
726
|
+
require(str(capsule.get("task_id") or "").strip() == current_task_id, f"{current_task_id} resume_capsule.task_id must match current_task_id")
|
|
727
|
+
for field in ["state", "canonical_path", "next_step", "blocker", "last_passed_gate"]:
|
|
728
|
+
value = str(capsule.get(field) or "").strip()
|
|
729
|
+
require(value and not is_placeholder_evidence(value), f"{current_task_id} resume_capsule.{field} must contain concrete recovery information")
|
|
730
|
+
|
|
731
|
+
do_not_retry = as_string_list(capsule.get("do_not_retry"))
|
|
732
|
+
require(
|
|
733
|
+
do_not_retry and not any(is_placeholder_evidence(item) for item in do_not_retry),
|
|
734
|
+
f"{current_task_id} resume_capsule.do_not_retry must list concrete paths or attempts not to repeat",
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
refs = as_string_list(capsule.get("recovery_refs"))
|
|
738
|
+
require(refs, f"{current_task_id} resume_capsule.recovery_refs must link implementation doc and runbook/evidence documents")
|
|
739
|
+
implementation_doc = str(current_task.get("implementation_doc") or "").strip()
|
|
740
|
+
if implementation_doc:
|
|
741
|
+
require(
|
|
742
|
+
implementation_doc in refs,
|
|
743
|
+
f"{current_task_id} resume_capsule.recovery_refs must include current implementation_doc {implementation_doc}",
|
|
744
|
+
)
|
|
745
|
+
require(
|
|
746
|
+
any(ref.startswith(RUNBOOK_DOC_PREFIX) for ref in refs),
|
|
747
|
+
f"{current_task_id} resume_capsule.recovery_refs must include a runbook/evidence document under {RUNBOOK_DOC_PREFIX}",
|
|
748
|
+
)
|
|
749
|
+
for ref in refs:
|
|
750
|
+
require(
|
|
751
|
+
ref.startswith(".docs/04_implementation/") or ref.startswith(RUNBOOK_DOC_PREFIX),
|
|
752
|
+
f"{current_task_id} resume_capsule.recovery_refs may only point to implementation docs or runbook/evidence docs: {ref}",
|
|
753
|
+
)
|
|
754
|
+
require(repo_path(ref).exists(), f"{current_task_id} resume_capsule recovery_ref does not exist: {ref}")
|
|
755
|
+
|
|
756
|
+
|
|
675
757
|
def validate_plan_contract(data: dict[str, Any], allow_open: bool) -> None:
|
|
676
758
|
lifecycle = load_lifecycle()
|
|
677
759
|
current_phase = str(lifecycle.get("current_phase") or "")
|
|
@@ -695,6 +777,7 @@ def validate_plan_contract(data: dict[str, Any], allow_open: bool) -> None:
|
|
|
695
777
|
current_task_id = data.get("current_task_id") or ""
|
|
696
778
|
if current_task_id:
|
|
697
779
|
require(task_by_id(data, current_task_id), f"current_task_id does not match a task: {current_task_id}")
|
|
780
|
+
validate_resume_capsule_contract(data)
|
|
698
781
|
|
|
699
782
|
open_tasks = [task.get("id") for task in tasks if task.get("status") in OPEN_TASK_STATUSES]
|
|
700
783
|
if not allow_open:
|
package/dist/lib/init.js
CHANGED
package/dist/lib/validators.js
CHANGED
|
@@ -89,6 +89,7 @@ const TEST_REPORT_PLACEHOLDER_TERMS = ["pending", "tbd", "todo", "待填", "待
|
|
|
89
89
|
const TEST_FACT_SOURCE_PHASES = new Set(["TESTING", "RFC_RECALIBRATION"]);
|
|
90
90
|
const TEST_FACT_SOURCE_PATTERNS = [".docs/07_test/**", ".docs/07_test/"];
|
|
91
91
|
const TEST_FACT_SOURCE_REF = /\.docs\/07_test\/[^\s`,)]+/g;
|
|
92
|
+
const RUNBOOK_DOC_PREFIX = ".docs/09_runbooks/";
|
|
92
93
|
const RUNNABLE_ENTRY_EXIT_TERMS = [
|
|
93
94
|
"runnable entry/exit",
|
|
94
95
|
"entry/exit",
|
|
@@ -103,6 +104,8 @@ const DEVELOPMENT_SELF_TEST_CONTRACT_TERMS = ["development self-test contract",
|
|
|
103
104
|
const DEVELOPMENT_SELF_TEST_REPORT_TERMS = ["development self-test report", "开发自测报告"];
|
|
104
105
|
const DEVELOPMENT_SELF_TEST_IMPACT_TERMS = ["development self-test impact", "开发自测影响"];
|
|
105
106
|
const MODULE_KEY_TEST_PATH_TERMS = ["module key test path", "模块关键测试路径"];
|
|
107
|
+
const GATE_BREAKDOWN_TERMS = ["gate breakdown", "gate 分层", "gate breakdown(gate 分层)"];
|
|
108
|
+
const CURRENT_OPERATOR_PATH_TERMS = ["current operator path", "operator path", "当前操作路径", "当前 operator path"];
|
|
106
109
|
const TESTING_HANDOFF_TERMS = ["testing handoff contract", "测试交接合同"];
|
|
107
110
|
const EVIDENCE_PLACEHOLDER_TERMS = [
|
|
108
111
|
"pending",
|
|
@@ -113,6 +116,74 @@ const EVIDENCE_PLACEHOLDER_TERMS = [
|
|
|
113
116
|
"待补",
|
|
114
117
|
"待确认"
|
|
115
118
|
];
|
|
119
|
+
const SELF_TEST_REPORT_PLACEHOLDER_TERMS = [
|
|
120
|
+
"pass / blocked",
|
|
121
|
+
"pass or blocked",
|
|
122
|
+
"pass/block",
|
|
123
|
+
"pass/blocker",
|
|
124
|
+
"local start / invocation",
|
|
125
|
+
"all self-test scenarios",
|
|
126
|
+
"all task/module promised runnable entries",
|
|
127
|
+
"actual internal key paths",
|
|
128
|
+
"observable completion evidence"
|
|
129
|
+
];
|
|
130
|
+
const SELF_TEST_REPORT_STATUSES = new Set(["PASS", "BLOCKED", "IN_PROGRESS", "STALE"]);
|
|
131
|
+
const SELF_TEST_REPORT_DISALLOWED_SECTION_TERMS = [
|
|
132
|
+
"debug log",
|
|
133
|
+
"operator log",
|
|
134
|
+
"operation log",
|
|
135
|
+
"runbook",
|
|
136
|
+
"exploration",
|
|
137
|
+
"diagnostic attempts",
|
|
138
|
+
"fallback attempts",
|
|
139
|
+
"history log",
|
|
140
|
+
"remote operation log",
|
|
141
|
+
"调试日志",
|
|
142
|
+
"操作日志",
|
|
143
|
+
"远端操作日志",
|
|
144
|
+
"探索流水",
|
|
145
|
+
"失败探索",
|
|
146
|
+
"诊断尝试",
|
|
147
|
+
"历史流水"
|
|
148
|
+
];
|
|
149
|
+
const SELF_TEST_OBSERVABLE_EVIDENCE_TERMS = [
|
|
150
|
+
"pass output",
|
|
151
|
+
"response",
|
|
152
|
+
"output",
|
|
153
|
+
"side effect",
|
|
154
|
+
"log",
|
|
155
|
+
"artifact",
|
|
156
|
+
"health",
|
|
157
|
+
"status",
|
|
158
|
+
"audit",
|
|
159
|
+
"rendered",
|
|
160
|
+
"page state",
|
|
161
|
+
"screenshot",
|
|
162
|
+
"browser check",
|
|
163
|
+
"playwright",
|
|
164
|
+
"command output",
|
|
165
|
+
"queue",
|
|
166
|
+
"file"
|
|
167
|
+
];
|
|
168
|
+
const RESUME_CAPSULE_REQUIRED_EVIDENCE_LEVELS = new Set(["external_provider_live", "deployed_runtime", "business_handoff_ready"]);
|
|
169
|
+
const RESUME_CAPSULE_REQUIRED_TARGET_KINDS = new Set(["cloud_vm", "managed_service", "browser", "worker"]);
|
|
170
|
+
const RESUME_CAPSULE_FIELDS = [
|
|
171
|
+
"task_id",
|
|
172
|
+
"state",
|
|
173
|
+
"canonical_path",
|
|
174
|
+
"next_step",
|
|
175
|
+
"blocker",
|
|
176
|
+
"last_passed_gate",
|
|
177
|
+
"do_not_retry",
|
|
178
|
+
"recovery_refs"
|
|
179
|
+
];
|
|
180
|
+
const MAX_WORKING_NOTES = 8;
|
|
181
|
+
const GATE_BREAKDOWN_LAYER_GROUPS = [
|
|
182
|
+
["local gate", ["local", "unit", "lint", "test", "本地"]],
|
|
183
|
+
["cloud/service gate", ["cloud", "service", "runtime", "server", "managed_service", "cloud_vm", "服务", "云端"]],
|
|
184
|
+
["executor/operator readiness", ["executor", "operator", "worker", "browser", "provider", "adapter", "readiness", "执行器", "操控", "就绪"]],
|
|
185
|
+
["live smoke or handoff", ["live", "smoke", "handoff", "external_provider_live", "deployed_runtime", "business_handoff_ready", "冒烟", "交接"]]
|
|
186
|
+
];
|
|
116
187
|
const PAGE_TASK_TERMS = ["frontend", "front-end", "browser", "page", "页面", "前端", "按钮", "表单", "跳转"];
|
|
117
188
|
const PAGE_ENTRY_TERMS = ["http://", "https://", "localhost", "127.0.0.1", "page url", "页面 url", "dev server"];
|
|
118
189
|
const PAGE_BROWSER_CHECK_TERMS = ["browser check", "playwright", "screenshot", "click", "button", "form", "页面可加载", "浏览器验证"];
|
|
@@ -289,6 +360,7 @@ async function validateHarness(projectRoot) {
|
|
|
289
360
|
for (const required of [
|
|
290
361
|
"AGENTS.md",
|
|
291
362
|
".docs/INDEX.md",
|
|
363
|
+
".docs/09_runbooks",
|
|
292
364
|
harnessPath(root, "config.yaml"),
|
|
293
365
|
harnessPath(root, "state", "lifecycle.yaml"),
|
|
294
366
|
harnessPath(root, "state", "plan.yaml"),
|
|
@@ -756,6 +828,7 @@ async function validatePlanState(projectRoot, allowOpen) {
|
|
|
756
828
|
if (!Array.isArray(task.acceptance_criteria) || task.acceptance_criteria.length === 0) {
|
|
757
829
|
errors.push(`Open task ${task.id} must define acceptance_criteria`);
|
|
758
830
|
}
|
|
831
|
+
errors.push(...validateWorkingNotesLimit(task));
|
|
759
832
|
errors.push(...validateRuntimeEvidenceContract(task));
|
|
760
833
|
errors.push(...testingBoundaryErrorsForAllowedPaths(task));
|
|
761
834
|
}
|
|
@@ -774,8 +847,90 @@ async function validatePlanState(projectRoot, allowOpen) {
|
|
|
774
847
|
if (currentTaskId && !tasks.some((task) => isRecord(task) && task.id === currentTaskId)) {
|
|
775
848
|
errors.push(`current_task_id does not match a task: ${currentTaskId}`);
|
|
776
849
|
}
|
|
850
|
+
errors.push(...(await validateResumeCapsule(projectRoot, tasksData)));
|
|
777
851
|
return { taskCount: tasks.length, errors, plan: tasksData };
|
|
778
852
|
}
|
|
853
|
+
function validateWorkingNotesLimit(task) {
|
|
854
|
+
if (!("working_notes" in task))
|
|
855
|
+
return [];
|
|
856
|
+
const taskId = String(task.id ?? "Open task");
|
|
857
|
+
const notes = task.working_notes;
|
|
858
|
+
if (typeof notes !== "string" && !Array.isArray(notes)) {
|
|
859
|
+
return [`Open task ${taskId} working_notes must be a short string or list with at most ${MAX_WORKING_NOTES} items`];
|
|
860
|
+
}
|
|
861
|
+
const count = Array.isArray(notes) ? notes.length : notes.trim() ? 1 : 0;
|
|
862
|
+
if (count > MAX_WORKING_NOTES) {
|
|
863
|
+
return [`Open task ${taskId} working_notes must stay resume-first and contain at most ${MAX_WORKING_NOTES} items; found ${count}`];
|
|
864
|
+
}
|
|
865
|
+
return [];
|
|
866
|
+
}
|
|
867
|
+
async function validateResumeCapsule(projectRoot, plan) {
|
|
868
|
+
const errors = [];
|
|
869
|
+
const currentTask = currentOpenSprintTask(plan);
|
|
870
|
+
const capsule = plan.resume_capsule;
|
|
871
|
+
if (!currentTask) {
|
|
872
|
+
if (capsule !== undefined) {
|
|
873
|
+
errors.push("plan.yaml resume_capsule must only be present for the current open SPRINTING task");
|
|
874
|
+
}
|
|
875
|
+
return errors;
|
|
876
|
+
}
|
|
877
|
+
const taskId = String(currentTask.id ?? "current task");
|
|
878
|
+
const required = requiresResumeCapsule(currentTask);
|
|
879
|
+
if (!required && capsule === undefined)
|
|
880
|
+
return errors;
|
|
881
|
+
if (!isRecord(capsule)) {
|
|
882
|
+
errors.push(`${taskId} high-risk runtime task must define top-level resume_capsule`);
|
|
883
|
+
return errors;
|
|
884
|
+
}
|
|
885
|
+
for (const field of RESUME_CAPSULE_FIELDS) {
|
|
886
|
+
if (!(field in capsule)) {
|
|
887
|
+
errors.push(`${taskId} resume_capsule missing field: ${field}`);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
const capsuleTaskId = String(capsule.task_id ?? "").trim();
|
|
891
|
+
if (capsuleTaskId !== taskId) {
|
|
892
|
+
errors.push(`${taskId} resume_capsule.task_id must match current_task_id`);
|
|
893
|
+
}
|
|
894
|
+
for (const field of ["state", "canonical_path", "next_step", "blocker", "last_passed_gate"]) {
|
|
895
|
+
const value = String(capsule[field] ?? "").trim();
|
|
896
|
+
if (!value || isPlaceholderEvidence(value)) {
|
|
897
|
+
errors.push(`${taskId} resume_capsule.${field} must contain concrete recovery information`);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
const doNotRetry = asStringList(capsule.do_not_retry);
|
|
901
|
+
if (doNotRetry.length === 0 || doNotRetry.some((item) => isPlaceholderEvidence(item))) {
|
|
902
|
+
errors.push(`${taskId} resume_capsule.do_not_retry must list concrete paths or attempts not to repeat`);
|
|
903
|
+
}
|
|
904
|
+
const refs = asStringList(capsule.recovery_refs);
|
|
905
|
+
if (refs.length === 0) {
|
|
906
|
+
errors.push(`${taskId} resume_capsule.recovery_refs must link implementation doc and runbook/evidence documents`);
|
|
907
|
+
return errors;
|
|
908
|
+
}
|
|
909
|
+
const implementationDoc = String(currentTask.implementation_doc ?? "").trim();
|
|
910
|
+
if (implementationDoc && !refs.includes(implementationDoc)) {
|
|
911
|
+
errors.push(`${taskId} resume_capsule.recovery_refs must include current implementation_doc ${implementationDoc}`);
|
|
912
|
+
}
|
|
913
|
+
if (!refs.some((ref) => ref.startsWith(RUNBOOK_DOC_PREFIX))) {
|
|
914
|
+
errors.push(`${taskId} resume_capsule.recovery_refs must include a runbook/evidence document under ${RUNBOOK_DOC_PREFIX}`);
|
|
915
|
+
}
|
|
916
|
+
for (const ref of refs) {
|
|
917
|
+
if (!ref.startsWith(".docs/04_implementation/") && !ref.startsWith(RUNBOOK_DOC_PREFIX)) {
|
|
918
|
+
errors.push(`${taskId} resume_capsule.recovery_refs may only point to implementation docs or runbook/evidence docs: ${ref}`);
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
if (!(await pathExists(path.join(projectRoot, ref)))) {
|
|
922
|
+
errors.push(`${taskId} resume_capsule recovery_ref does not exist: ${ref}`);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
return errors;
|
|
926
|
+
}
|
|
927
|
+
function requiresResumeCapsule(task) {
|
|
928
|
+
if (String(task.phase ?? "") !== "SPRINTING")
|
|
929
|
+
return false;
|
|
930
|
+
const evidenceLevel = isRecord(task.evidence_level) ? String(task.evidence_level.required ?? "") : "";
|
|
931
|
+
const targetKind = isRecord(task.target_runtime_environment) ? String(task.target_runtime_environment.kind ?? "") : "";
|
|
932
|
+
return RESUME_CAPSULE_REQUIRED_EVIDENCE_LEVELS.has(evidenceLevel) || RESUME_CAPSULE_REQUIRED_TARGET_KINDS.has(targetKind);
|
|
933
|
+
}
|
|
779
934
|
function validateRuntimeEvidenceContract(task) {
|
|
780
935
|
const errors = [];
|
|
781
936
|
const taskId = String(task.id ?? "Task");
|
|
@@ -1297,6 +1452,14 @@ function validateDevelopmentSelfTestReport(fullText, developmentEvidenceSection,
|
|
|
1297
1452
|
if (!report) {
|
|
1298
1453
|
return [`${taskId} implementation_doc must include Development Self-Test Report for self_test_contract: ${implementationDoc}`];
|
|
1299
1454
|
}
|
|
1455
|
+
const reportStatus = normalizeSelfTestReportStatus(evidenceFieldValue(report, "Report Status"));
|
|
1456
|
+
if (!reportStatus) {
|
|
1457
|
+
errors.push(`${taskId} Development Self-Test Report must include Report Status: PASS | BLOCKED | IN_PROGRESS | STALE in ${implementationDoc}`);
|
|
1458
|
+
}
|
|
1459
|
+
else if (reportStatus !== "PASS") {
|
|
1460
|
+
errors.push(`${taskId} Development Self-Test Report Report Status is ${reportStatus}; validate-dev cannot handoff until the report status is PASS`);
|
|
1461
|
+
}
|
|
1462
|
+
errors.push(...validateSelfTestReportBoundary(report, taskId, implementationDoc));
|
|
1300
1463
|
const basicSelfTest = evidenceFieldValue(developmentEvidenceSection, "Basic Self-test Evidence") ?? "";
|
|
1301
1464
|
if (!containsAny(basicSelfTest, ["Development Self-Test Report", "开发自测报告", "self-test report"])) {
|
|
1302
1465
|
errors.push(`${taskId} Basic Self-test Evidence must reference the Development Self-Test Report in ${implementationDoc}`);
|
|
@@ -1318,11 +1481,26 @@ function validateDevelopmentSelfTestReport(fullText, developmentEvidenceSection,
|
|
|
1318
1481
|
}
|
|
1319
1482
|
}
|
|
1320
1483
|
const moduleKeyTestPath = evidenceFieldValue(report, "Module Key Test Path") ?? "";
|
|
1484
|
+
if (isPlaceholderSelfTestReportValue(moduleKeyTestPath) || isTemplateModuleKeyTestPath(moduleKeyTestPath)) {
|
|
1485
|
+
errors.push(`${taskId} Development Self-Test Report Module Key Test Path must replace template placeholders with actual executed path evidence in ${implementationDoc}`);
|
|
1486
|
+
}
|
|
1321
1487
|
const runnableEntry = String(contract.runnable_entry ?? "").trim();
|
|
1322
1488
|
if (runnableEntry && !moduleKeyTestPath.includes(runnableEntry)) {
|
|
1323
1489
|
errors.push(`${taskId} Development Self-Test Report Module Key Test Path must include runnable entry ${runnableEntry} in ${implementationDoc}`);
|
|
1324
1490
|
}
|
|
1325
1491
|
const scenarios = Array.isArray(contract.scenarios) ? contract.scenarios.filter(isRecord) : [];
|
|
1492
|
+
const exitEvidenceTerms = [
|
|
1493
|
+
String(contract.observable_exit ?? "").trim(),
|
|
1494
|
+
...scenarios.flatMap((scenario) => [
|
|
1495
|
+
String(scenario.expected_exit ?? "").trim(),
|
|
1496
|
+
String(scenario.evidence ?? "").trim()
|
|
1497
|
+
])
|
|
1498
|
+
].filter(Boolean);
|
|
1499
|
+
if (exitEvidenceTerms.length > 0
|
|
1500
|
+
&& !exitEvidenceTerms.some((term) => normalizedIncludes(moduleKeyTestPath, term))
|
|
1501
|
+
&& !containsAny(moduleKeyTestPath, SELF_TEST_OBSERVABLE_EVIDENCE_TERMS)) {
|
|
1502
|
+
errors.push(`${taskId} Development Self-Test Report Module Key Test Path must include observable exit or evidence from self_test_contract in ${implementationDoc}`);
|
|
1503
|
+
}
|
|
1326
1504
|
for (const scenario of scenarios) {
|
|
1327
1505
|
const scenarioId = String(scenario.id ?? "").trim();
|
|
1328
1506
|
if (!scenarioId)
|
|
@@ -1332,26 +1510,191 @@ function validateDevelopmentSelfTestReport(fullText, developmentEvidenceSection,
|
|
|
1332
1510
|
}
|
|
1333
1511
|
const status = scenarioStatus(report, scenarioId);
|
|
1334
1512
|
if (!status) {
|
|
1335
|
-
errors.push(`${taskId} Development Self-Test Report must record scenario ${scenarioId} as PASS or
|
|
1513
|
+
errors.push(`${taskId} Development Self-Test Report must record scenario ${scenarioId} as PASS, BLOCKED, IN_PROGRESS, or STALE in ${implementationDoc}`);
|
|
1336
1514
|
}
|
|
1337
|
-
else if (status === "
|
|
1338
|
-
errors.push(`${taskId} Development Self-Test Report scenario ${scenarioId}
|
|
1515
|
+
else if (status === "AMBIGUOUS") {
|
|
1516
|
+
errors.push(`${taskId} Development Self-Test Report scenario ${scenarioId} must choose exactly one status in ${implementationDoc}`);
|
|
1339
1517
|
}
|
|
1518
|
+
else if (status !== "PASS") {
|
|
1519
|
+
errors.push(`${taskId} Development Self-Test Report scenario ${scenarioId} is ${status}; validate-dev cannot handoff until every scenario is PASS`);
|
|
1520
|
+
}
|
|
1521
|
+
errors.push(...validateScenarioTableEvidence(report, scenarioId, taskId, implementationDoc));
|
|
1522
|
+
}
|
|
1523
|
+
const targetRuntime = isRecord(task.target_runtime_environment) ? task.target_runtime_environment : undefined;
|
|
1524
|
+
const reportContext = `${taskText(task)}\n${report}\n${Object.values(contract).map((value) => String(value ?? "")).join("\n")}`;
|
|
1525
|
+
if (String(targetRuntime?.kind ?? "") === "browser" || containsAny(reportContext, PAGE_TASK_TERMS)) {
|
|
1526
|
+
const loweredReport = report.toLowerCase();
|
|
1527
|
+
if (!containsAny(loweredReport, PAGE_ENTRY_TERMS)) {
|
|
1528
|
+
errors.push(`${taskId} page Development Self-Test Report must include a dev server or page URL in ${implementationDoc}`);
|
|
1529
|
+
}
|
|
1530
|
+
if (!containsAny(loweredReport, PAGE_BROWSER_CHECK_TERMS)) {
|
|
1531
|
+
errors.push(`${taskId} page Development Self-Test Report must include browser, Playwright, screenshot, or equivalent interaction evidence in ${implementationDoc}`);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
if (requiresResumeCapsule(task)) {
|
|
1535
|
+
errors.push(...validateCurrentOperatorPath(fullText, taskId, implementationDoc));
|
|
1536
|
+
errors.push(...validateGateBreakdown(fullText, taskId, implementationDoc));
|
|
1537
|
+
}
|
|
1538
|
+
return errors;
|
|
1539
|
+
}
|
|
1540
|
+
function normalizeSelfTestReportStatus(value) {
|
|
1541
|
+
if (!value)
|
|
1542
|
+
return undefined;
|
|
1543
|
+
const normalized = value.replace(/`/g, "").trim().toUpperCase().replace(/[\s-]+/g, "_");
|
|
1544
|
+
return SELF_TEST_REPORT_STATUSES.has(normalized) ? normalized : undefined;
|
|
1545
|
+
}
|
|
1546
|
+
function validateSelfTestReportBoundary(report, taskId, implementationDoc) {
|
|
1547
|
+
const errors = [];
|
|
1548
|
+
for (const line of report.split(/\r?\n/)) {
|
|
1549
|
+
const match = line.match(/^(#{1,6})\s+(.+)$/);
|
|
1550
|
+
if (!match)
|
|
1551
|
+
continue;
|
|
1552
|
+
const title = match[2].trim().toLowerCase();
|
|
1553
|
+
const blockedTerm = SELF_TEST_REPORT_DISALLOWED_SECTION_TERMS.find((term) => title.includes(term));
|
|
1554
|
+
if (blockedTerm) {
|
|
1555
|
+
errors.push(`${taskId} Development Self-Test Report must not include debug/operator/runbook/exploration log section "${match[2].trim()}" in ${implementationDoc}; link a runbook or exploration appendix instead`);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
return errors;
|
|
1559
|
+
}
|
|
1560
|
+
function validateCurrentOperatorPath(fullText, taskId, implementationDoc) {
|
|
1561
|
+
const section = markdownSection(fullText, CURRENT_OPERATOR_PATH_TERMS);
|
|
1562
|
+
if (!section) {
|
|
1563
|
+
return [`${taskId} high-risk runtime task must include a short Current Operator Path section in ${implementationDoc}`];
|
|
1564
|
+
}
|
|
1565
|
+
const errors = [];
|
|
1566
|
+
const requiredFields = [
|
|
1567
|
+
["canonical operator path", ["Canonical operator path", "Canonical path"]],
|
|
1568
|
+
["runbook link", ["Operator runbook", "Runbook"]],
|
|
1569
|
+
["credential reference name", ["Credential reference", "Credential reference name"]],
|
|
1570
|
+
["command/UI channel", ["Command/UI channel", "Command channel", "UI channel"]],
|
|
1571
|
+
["do-not-retry summary", ["Do-not-retry summary", "Do not retry summary"]]
|
|
1572
|
+
];
|
|
1573
|
+
for (const [label, fields] of requiredFields) {
|
|
1574
|
+
const value = fields.map((field) => evidenceFieldValue(section, field)).find((candidate) => candidate && candidate.trim());
|
|
1575
|
+
if (!value) {
|
|
1576
|
+
errors.push(`${taskId} Current Operator Path must record ${label} in ${implementationDoc}`);
|
|
1577
|
+
}
|
|
1578
|
+
else if (label !== "credential reference name" && isPlaceholderEvidence(value)) {
|
|
1579
|
+
errors.push(`${taskId} Current Operator Path ${label} must be concrete in ${implementationDoc}`);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
const runbookValue = evidenceFieldValue(section, "Operator runbook") ?? evidenceFieldValue(section, "Runbook") ?? section;
|
|
1583
|
+
if (!runbookValue.includes(RUNBOOK_DOC_PREFIX)) {
|
|
1584
|
+
errors.push(`${taskId} Current Operator Path must link a runbook/evidence document under ${RUNBOOK_DOC_PREFIX} in ${implementationDoc}`);
|
|
1585
|
+
}
|
|
1586
|
+
return errors;
|
|
1587
|
+
}
|
|
1588
|
+
function validateGateBreakdown(fullText, taskId, implementationDoc) {
|
|
1589
|
+
const section = markdownSection(fullText, GATE_BREAKDOWN_TERMS);
|
|
1590
|
+
if (!section) {
|
|
1591
|
+
return [`${taskId} high-risk runtime task Development Self-Test Report must include Gate Breakdown in ${implementationDoc}`];
|
|
1592
|
+
}
|
|
1593
|
+
const errors = [];
|
|
1594
|
+
const lowered = section.toLowerCase();
|
|
1595
|
+
for (const [label, terms] of GATE_BREAKDOWN_LAYER_GROUPS) {
|
|
1596
|
+
if (!containsAny(lowered, terms)) {
|
|
1597
|
+
errors.push(`${taskId} Gate Breakdown must include ${label} status/evidence in ${implementationDoc}`);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
const rows = markdownTableRows(section).filter((cells) => !cells.some((cell) => /gate layer|layer|层级/i.test(cell)));
|
|
1601
|
+
const concreteRows = rows.filter((cells) => cells.some((cell) => !isPlaceholderSelfTestReportValue(cell)));
|
|
1602
|
+
if (concreteRows.length < 2) {
|
|
1603
|
+
errors.push(`${taskId} Gate Breakdown must split evidence into multiple concrete gate layers in ${implementationDoc}`);
|
|
1604
|
+
}
|
|
1605
|
+
if (concreteRows.length <= 1 && lowered.includes("validate-dev")) {
|
|
1606
|
+
errors.push(`${taskId} Gate Breakdown cannot collapse high-risk runtime progress into only validate-dev in ${implementationDoc}`);
|
|
1340
1607
|
}
|
|
1341
1608
|
return errors;
|
|
1342
1609
|
}
|
|
1343
1610
|
function scenarioStatus(text, scenarioId) {
|
|
1344
1611
|
const escaped = scenarioId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1345
|
-
const
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
if (
|
|
1352
|
-
|
|
1612
|
+
const pattern = new RegExp("^.*" + escaped + ".*$", "gim");
|
|
1613
|
+
const seen = new Set();
|
|
1614
|
+
for (const match of text.matchAll(pattern)) {
|
|
1615
|
+
const status = selfTestLineStatus(match[0]);
|
|
1616
|
+
if (status === "AMBIGUOUS")
|
|
1617
|
+
return status;
|
|
1618
|
+
if (status)
|
|
1619
|
+
seen.add(status);
|
|
1620
|
+
}
|
|
1621
|
+
if (seen.size > 1)
|
|
1622
|
+
return "AMBIGUOUS";
|
|
1623
|
+
return [...seen][0];
|
|
1624
|
+
}
|
|
1625
|
+
function selfTestLineStatus(line) {
|
|
1626
|
+
const normalized = line.toUpperCase().replace(/\bIN[\s-]+PROGRESS\b/g, "IN_PROGRESS");
|
|
1627
|
+
const matches = [];
|
|
1628
|
+
if (hasStatusToken(normalized, "PASS"))
|
|
1629
|
+
matches.push("PASS");
|
|
1630
|
+
if (hasStatusToken(normalized, "BLOCKED"))
|
|
1631
|
+
matches.push("BLOCKED");
|
|
1632
|
+
if (hasStatusToken(normalized, "IN_PROGRESS"))
|
|
1633
|
+
matches.push("IN_PROGRESS");
|
|
1634
|
+
if (hasStatusToken(normalized, "STALE"))
|
|
1635
|
+
matches.push("STALE");
|
|
1636
|
+
if (matches.length > 1)
|
|
1637
|
+
return "AMBIGUOUS";
|
|
1638
|
+
return matches[0];
|
|
1639
|
+
}
|
|
1640
|
+
function hasStatusToken(line, status) {
|
|
1641
|
+
const escaped = status.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1642
|
+
return new RegExp(`(^|[^A-Z0-9_])${escaped}([^A-Z0-9_]|$)`).test(line);
|
|
1643
|
+
}
|
|
1644
|
+
function validateScenarioTableEvidence(report, scenarioId, taskId, implementationDoc) {
|
|
1645
|
+
const errors = [];
|
|
1646
|
+
const rows = markdownTableRows(report).filter((cells) => cells.some((cell) => normalizeCell(cell) === scenarioId));
|
|
1647
|
+
for (const cells of rows) {
|
|
1648
|
+
const [id, result, executedEntry, actualExit, evidence] = cells;
|
|
1649
|
+
if (!id || normalizeCell(id) !== scenarioId)
|
|
1650
|
+
continue;
|
|
1651
|
+
const requiredCells = [
|
|
1652
|
+
["Result", result],
|
|
1653
|
+
["Executed Entry", executedEntry],
|
|
1654
|
+
["Actual Exit", actualExit],
|
|
1655
|
+
["Evidence", evidence]
|
|
1656
|
+
];
|
|
1657
|
+
for (const [label, value] of requiredCells) {
|
|
1658
|
+
if (!value || isPlaceholderSelfTestReportValue(value)) {
|
|
1659
|
+
errors.push(`${taskId} Development Self-Test Report scenario ${scenarioId} table ${label} must contain concrete evidence in ${implementationDoc}`);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
const tableStatus = result ? scenarioStatus(`| ${cells.join(" | ")} |`, scenarioId) : undefined;
|
|
1663
|
+
if (tableStatus === "AMBIGUOUS") {
|
|
1664
|
+
errors.push(`${taskId} Development Self-Test Report scenario ${scenarioId} table Result must choose exactly one status in ${implementationDoc}`);
|
|
1665
|
+
}
|
|
1666
|
+
else if (tableStatus && tableStatus !== "PASS") {
|
|
1667
|
+
errors.push(`${taskId} Development Self-Test Report scenario ${scenarioId} table Result is ${tableStatus}; validate-dev cannot handoff until every scenario is PASS`);
|
|
1668
|
+
}
|
|
1353
1669
|
}
|
|
1354
|
-
return
|
|
1670
|
+
return errors;
|
|
1671
|
+
}
|
|
1672
|
+
function markdownTableRows(section) {
|
|
1673
|
+
return section
|
|
1674
|
+
.split(/\r?\n/)
|
|
1675
|
+
.map((line) => line.trim())
|
|
1676
|
+
.filter((line) => line.startsWith("|") && line.endsWith("|") && !/^\|\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|$/.test(line))
|
|
1677
|
+
.map((line) => line.slice(1, -1).split("|").map((cell) => cell.trim()));
|
|
1678
|
+
}
|
|
1679
|
+
function normalizeCell(value) {
|
|
1680
|
+
return value.replace(/`/g, "").trim();
|
|
1681
|
+
}
|
|
1682
|
+
function isTemplateModuleKeyTestPath(value) {
|
|
1683
|
+
const lowered = value.toLowerCase();
|
|
1684
|
+
return [
|
|
1685
|
+
"local start / invocation",
|
|
1686
|
+
"all self-test scenarios",
|
|
1687
|
+
"all task/module promised runnable entries",
|
|
1688
|
+
"actual internal key paths",
|
|
1689
|
+
"observable completion evidence"
|
|
1690
|
+
].some((term) => lowered.includes(term));
|
|
1691
|
+
}
|
|
1692
|
+
function isPlaceholderSelfTestReportValue(value) {
|
|
1693
|
+
const normalized = value.trim().toLowerCase();
|
|
1694
|
+
return isPlaceholderEvidence(value) || SELF_TEST_REPORT_PLACEHOLDER_TERMS.some((term) => normalized.includes(term));
|
|
1695
|
+
}
|
|
1696
|
+
function normalizedIncludes(text, needle) {
|
|
1697
|
+
return text.toLowerCase().includes(needle.toLowerCase());
|
|
1355
1698
|
}
|
|
1356
1699
|
function hasConcreteDevelopmentEvidenceFields(section) {
|
|
1357
1700
|
return ["Evidence Level", "Target Runtime Environment", "Runnable Entry", "Observable Exit", "Client / Server Initialization", "Config Contract"].some((field) => {
|