agent-project-sdlc 0.1.14 → 0.1.16
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 +6 -4
- package/assets/docs/README.md +6 -4
- package/assets/github/harness.yml +2 -0
- package/assets/policies/gates.yaml +1 -1
- package/assets/policies/phase_contracts.yaml +1 -1
- package/assets/skills/pjsdlc_dev_sprint/SKILL.md +6 -3
- package/assets/skills/pjsdlc_implementation_doc/SKILL.md +5 -3
- package/assets/skills/pjsdlc_reviewer/SKILL.md +3 -2
- package/assets/skills/pjsdlc_tester/SKILL.md +4 -3
- package/assets/templates/IMPLEMENTATION_DOC_TEMPLATE.md +12 -5
- package/assets/templates/REVIEW_TEMPLATE.md +1 -0
- package/assets/templates/TEST_CASES_TEMPLATE.md +1 -0
- package/assets/templates/TEST_REPORT_TEMPLATE.md +1 -0
- package/dist/commands/sync.js +3 -0
- package/dist/lib/init.js +9 -8
- package/dist/lib/managed-file.d.ts +2 -0
- package/dist/lib/managed-file.js +2 -0
- package/dist/lib/migrations.js +8 -12
- package/dist/lib/sync-engine.js +62 -6
- package/dist/lib/upgrade.js +3 -0
- package/dist/lib/user-owned-sections.d.ts +7 -0
- package/dist/lib/user-owned-sections.js +105 -0
- package/dist/lib/validators.js +139 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,8 +22,8 @@ npx sdlc-harness init --adopt
|
|
|
22
22
|
| Project initialization | `npx sdlc-harness init` | Creates `AGENTS.md`, `<harnessRoot>/state/**`, workflow skills, managed templates/policies, `.docs/**` and a Makefile include. |
|
|
23
23
|
| Existing project adoption | `npx sdlc-harness init --adopt` | Adds Harness non-destructively to an existing repository. |
|
|
24
24
|
| Configurable Harness root | `--harness-folder`, `package.json#sdlcHarness.harnessFolderName`, `sdlc-harness.config.json` | Supports Codex `.codex`, Claude `.claude`, Cursor `.cursor`, Cline `.cline`, Roo `.roo`, Gemini `.gemini` or a custom folder. |
|
|
25
|
-
| Managed file sync | `npx sdlc-harness sync` | Materializes package canonical assets
|
|
26
|
-
| Upgrade | `npx sdlc-harness upgrade` | Runs migrations and sync for already-adopted projects. |
|
|
25
|
+
| Managed file sync | `npx sdlc-harness sync` | Materializes package canonical assets and safely updates package-managed guidance sections inside user-owned Markdown files while preserving project state, docs and local overrides. |
|
|
26
|
+
| Upgrade | `npx sdlc-harness upgrade` | Runs migrations and sync for already-adopted projects, including legacy seed guidance migration. |
|
|
27
27
|
| Diagnostics | `npx sdlc-harness doctor` | Reports Harness root, package version, schema version and key managed paths. |
|
|
28
28
|
| Validators | `npx sdlc-harness validate-*`, `make validate-current`, `make validate-harness` | Checks product, design slicing, development, review, test, release, RFC, active plan shape, prompt language contract and generated overview freshness. |
|
|
29
29
|
| Lifecycle workflow | `<harnessRoot>/state/lifecycle.yaml`, `<harnessRoot>/state/plan.yaml`, `.docs/**` | Tracks REQUIREMENT_GATHERING, ARCHITECTING, SPRINTING, REVIEWING, TESTING, RELEASING and RFC_RECALIBRATION facts. |
|
|
@@ -84,9 +84,9 @@ Before development starts, `ARCHITECTING` can return to `REQUIREMENT_GATHERING`
|
|
|
84
84
|
|
|
85
85
|
`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.
|
|
86
86
|
|
|
87
|
-
SPRINTING Definition of Done includes runnable entry/exit boundaries. API, CLI, server route, adapter, worker, provider, config-contract and fixture/live boundaries promised by a technical plan or task must be implemented or marked `BLOCKED` during development. REVIEWING treats missing entry/exit as blocking, and TESTING only exercises existing entrypoints; it must not add product runtime, bootstrap, provider adapter, deploy code or package runtime scripts.
|
|
87
|
+
SPRINTING Definition of Done includes runnable entry/exit boundaries. API, CLI, server route, adapter, worker, provider, config-contract and fixture/live boundaries promised by a technical plan or task must be implemented or marked `BLOCKED` during development. The current task implementation doc must also include `Development Evidence` with `Runnable Entry`, `Observable Exit`, `Basic Self-test Evidence`, or a justified `Not applicable`. REVIEWING treats missing entry/exit or development evidence as blocking, and TESTING only exercises existing entrypoints; it must not add product runtime, bootstrap, provider adapter, deploy code or package runtime scripts.
|
|
88
88
|
|
|
89
|
-
`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
|
|
89
|
+
`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, linked runnable-entry implementation docs and structured development evidence. Page tasks need a dev server or page URL plus browser/Playwright/screenshot/equivalent interaction evidence; API/CLI/worker tasks need a command, endpoint or invocation plus observable response/output/side effect. `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.
|
|
90
90
|
|
|
91
91
|
`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.
|
|
92
92
|
|
|
@@ -98,6 +98,8 @@ Do not create formal `.docs/07_test/**` test cases or reports before development
|
|
|
98
98
|
|
|
99
99
|
`<harnessRoot>/state/memory.md` is only a short cross-stage reminder and navigation surface. It answers what an agent should remember next time and where to find the source. Memory may link to ADRs, PRDs, tech plans or implementation docs; full context, alternatives, tradeoffs and long-term consequences belong in `.docs/05_decisions/` ADRs or other formal `.docs/**` fact sources.
|
|
100
100
|
|
|
101
|
+
`sync` and `upgrade` also maintain fixed package-managed sections inside user-owned Markdown files: `## Harness Guidance` in `<harnessRoot>/state/memory.md` and `## Harness Maintenance Rules` in `.docs/INDEX.md`. User memory entries, artifact maps and links stay outside those sections and are preserved. Legacy untitled seed guidance is migrated into the titled section to avoid duplicates. `.github/workflows/harness.yml` is updated only when it has `pjsdlc:sdlc-harness:github-workflow:*` markers or exactly matches the old generated workflow; customized workflows without markers are skipped and reported as `customized`.
|
|
102
|
+
|
|
101
103
|
## Common Commands
|
|
102
104
|
|
|
103
105
|
```sh
|
package/assets/docs/README.md
CHANGED
|
@@ -57,8 +57,8 @@ npx sdlc-harness init --adopt
|
|
|
57
57
|
| 新项目初始化 | `npx sdlc-harness init` | 选择目标 Agent,生成 Harness 根目录、状态文件、workflow skills、模板、策略、`.docs/**` 和 Makefile include |
|
|
58
58
|
| 已有项目接入 | `npx sdlc-harness init --adopt` | 非破坏性接入已有仓库,不覆盖业务代码和已有项目事实源 |
|
|
59
59
|
| 可配置 Harness 根目录 | `--harness-folder`、`package.json#sdlcHarness.harnessFolderName`、`sdlc-harness.config.json` | 支持 `.codex`、`.claude`、`.cursor`、`.cline`、`.roo`、`.gemini` 或自定义目录 |
|
|
60
|
-
| 同步 managed workflow 文件 | `npx sdlc-harness sync` | 从包内 canonical assets 物化 `AGENTS.md` managed block、workflow skills、templates、policies、Makefile
|
|
61
|
-
| 升级已接入项目 | `npx sdlc-harness upgrade` | 执行迁移并自动 `sync`,保留 state、docs、业务代码和本地 override |
|
|
60
|
+
| 同步 managed workflow 文件 | `npx sdlc-harness sync` | 从包内 canonical assets 物化 `AGENTS.md` managed block、workflow skills、templates、policies、Makefile 片段、GitHub workflow,并安全更新 user-owned Markdown guidance sections |
|
|
61
|
+
| 升级已接入项目 | `npx sdlc-harness upgrade` | 执行迁移并自动 `sync`,保留 state、docs、业务代码和本地 override,同时迁移旧 seed guidance |
|
|
62
62
|
| 接入诊断 | `npx sdlc-harness doctor` | 检查 harness root、版本、schema、关键文件和 managed paths |
|
|
63
63
|
| 阶段 gate | `npx sdlc-harness validate-*`、`make validate-current`、`make validate-harness` | 校验需求、设计切片、开发、Review、测试、发布、RFC、Harness 骨架、提示词语言契约和 overview freshness |
|
|
64
64
|
| 生命周期工作流 | `lifecycle.yaml`、`plan.yaml`、`.docs/**` | 固定 REQUIREMENT_GATHERING、ARCHITECTING、SPRINTING、REVIEWING、TESTING、RELEASING、RFC_RECALIBRATION 等阶段事实链 |
|
|
@@ -105,9 +105,9 @@ Agent 会读取 `<harnessRoot>/state/lifecycle.yaml` 和 `<harnessRoot>/state/pl
|
|
|
105
105
|
|
|
106
106
|
`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。
|
|
107
107
|
|
|
108
|
-
SPRINTING 的 Definition of Done 包含可运行入口/出口:技术方案或 task 承诺的 API、CLI、server route、adapter、worker、provider、配置契约和 fixture/live 边界必须在开发阶段实现或明确 `BLOCKED`。REVIEWING
|
|
108
|
+
SPRINTING 的 Definition of Done 包含可运行入口/出口:技术方案或 task 承诺的 API、CLI、server route、adapter、worker、provider、配置契约和 fixture/live 边界必须在开发阶段实现或明确 `BLOCKED`。当前 task 的 implementation doc 还必须写入 `Development Evidence`,包含 `Runnable Entry`、`Observable Exit`、`Basic Self-test Evidence`,或带原因的 `Not applicable`。REVIEWING 会把缺少入口/出口或开发自测证据作为阻断项;TESTING 只调用既有入口做输入输出验证,不能新增 product runtime、bootstrap、provider adapter、deploy 或 package runtime script。
|
|
109
109
|
|
|
110
|
-
`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`
|
|
110
|
+
`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`、implementation doc 和结构化 `Development Evidence`。页面类证据需要 dev server/page URL 与 browser check;API/CLI/worker 类证据需要 command/endpoint/invocation 与 response/output/side effect。`make validate-current` / `/advance` 是阶段出口 gate;进入 REVIEWING 前仍必须先完成 implementation commit 和 completion ledger,把 open task 从 `plan.yaml` 移除。
|
|
111
111
|
|
|
112
112
|
`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。
|
|
113
113
|
|
|
@@ -119,6 +119,8 @@ SPRINTING 的 Definition of Done 包含可运行入口/出口:技术方案或
|
|
|
119
119
|
|
|
120
120
|
`<harnessRoot>/state/memory.md` 只做跨阶段快捷提示和导航,回答“下次进来要先记住什么、去哪里找”。memory 可以链接到 ADR、PRD、tech plan 或 implementation doc;完整背景、备选方案、取舍和长期后果放在 `.docs/05_decisions/` ADR 或其它正式 `.docs/**` 事实源里。
|
|
121
121
|
|
|
122
|
+
`sync` / `upgrade` 会维护用户耦合文件里的固定 package-managed sections:`<harnessRoot>/state/memory.md` 的 `## Harness Guidance` 和 `.docs/INDEX.md` 的 `## Harness Maintenance Rules`。用户自己的 memory 条目、文档产物地图和链接保留在这些标题区块之外;如果旧项目只有早期无标题 seed 文案,升级会把它迁移到固定标题区块,避免重复。`.github/workflows/harness.yml` 只在文件带 `pjsdlc:sdlc-harness:github-workflow:*` marker 或内容等于旧版 generated workflow 时自动更新;自定义且无 marker 的 workflow 会被跳过并报告 `customized`。
|
|
123
|
+
|
|
122
124
|
### Workflow skill 如何生效
|
|
123
125
|
|
|
124
126
|
`<harnessRoot>/skills/<name>/SKILL.md` 是 Harness 的 workflow skill 事实源,也是稳定的 hard file index。它有两种使用方式:
|
|
@@ -15,7 +15,9 @@ description: Use during SPRINTING to execute one task from plan.yaml, respecting
|
|
|
15
15
|
|
|
16
16
|
开始编码前,先确认当前 open task 是否完整,修改范围是否覆盖必要文件,验收标准是否能被测试或 gate 验证。如果发现任务边界、产品行为或技术方案不清晰,要停下来说明 blocker、给出可能解释和推荐下一步,而不是扩大范围继续写。
|
|
17
17
|
|
|
18
|
-
开发阶段的 Definition of Done 包含可运行的系统入口/出口。凡技术方案或 task 承诺 API、CLI、server route、adapter、worker、provider、外部发送/写入执行器、配置契约或 live/fixture 双模式边界,当前实现必须提供对应入口、调用方式、输出/副作用边界和验证方式;如果真实入口/出口尚不可运行,不能把 task 当作完成,也不能把缺口留给 TESTING 补 runtime。Implementation doc 必须写明 `Runnable Entry/Exit`;确实不适用时也要显式写 `Not applicable`
|
|
18
|
+
开发阶段的 Definition of Done 包含可运行的系统入口/出口。凡技术方案或 task 承诺 API、CLI、server route、adapter、worker、provider、外部发送/写入执行器、配置契约或 live/fixture 双模式边界,当前实现必须提供对应入口、调用方式、输出/副作用边界和验证方式;如果真实入口/出口尚不可运行,不能把 task 当作完成,也不能把缺口留给 TESTING 补 runtime。Implementation doc 必须写明 `Runnable Entry/Exit`,并在 `Development Evidence` 中记录 `Runnable Entry`、`Observable Exit`、`Basic Self-test Evidence`;确实不适用时也要显式写 `Not applicable` 和具体原因。此时应保留或创建 `BLOCKED`/后续 dev task,或通过 RFC/ARCHITECTING 处理边界变更。
|
|
19
|
+
|
|
20
|
+
页面类任务在开发阶段必须启动 dev server 或等价预览入口,并用浏览器、Playwright、截图或等价方式验证页面可加载、主入口可访问、核心按钮/表单/跳转可用、没有明显报错或空白页。API/CLI/worker/RPA 类任务必须记录实际调用命令、endpoint、worker command、dry-run/live preflight 或 server action,以及可观察的 response、队列 item、审计日志、文件产物、发送结果、错误码或 PASS/BLOCKED 结果。
|
|
19
21
|
|
|
20
22
|
`/dev` 和 `/devloop` 是开发阶段的两个入口。`/dev` 创建或选择下一个最小 `TASK-*` development task,设置 `phase: "SPRINTING"`,并只完成一个 task 闭环后停止。通用规则是从任何 draft queue promote 正式 `TASK-*` 时都必须同次消费源 draft;当前开发阶段的内置 draft queue 是 `plan.draft.yaml.tasks[]`,因此如果这个 task 来自 `plan.draft.yaml.tasks[]`,promote 时必须同次删除源 draft,避免已采用草案继续显示为 `pending`。`/devloop` 连续运行 `/dev`,直到 `plan.yaml.tasks[]` 和 `plan.draft.yaml.tasks[]` 都没有明确可创建/执行的任务,或遇到需求、架构、allowed_paths、gate、commit/push blocker。
|
|
21
23
|
|
|
@@ -37,7 +39,7 @@ description: Use during SPRINTING to execute one task from plan.yaml, respecting
|
|
|
37
39
|
- 当前 task `allowed_paths` 范围内的测试改动
|
|
38
40
|
- `.docs/04_implementation/` 下相关模块、子系统或核心数据流的 implementation doc
|
|
39
41
|
- 当前 task `working_notes` 或 implementation doc `Verification` 中的 gate evidence
|
|
40
|
-
- implementation doc 中的 runnable entry/exit、配置契约和 fixture/live 边界事实
|
|
42
|
+
- implementation doc 中的 runnable entry/exit、observable exit、basic self-test evidence、配置契约和 fixture/live 边界事实
|
|
41
43
|
- 更新后的 `<harnessRoot>/state/plan.yaml`
|
|
42
44
|
- 如果本轮 promote draft,更新后的 `<harnessRoot>/state/plan.draft.yaml`
|
|
43
45
|
- 更新后的 `.docs/INDEX.md`
|
|
@@ -85,7 +87,7 @@ done task 的执行流水不在当前 `plan.yaml` 长期保留,也不是默认
|
|
|
85
87
|
6. 如果 gate 因代码或测试逻辑失败,在任务范围内修复。
|
|
86
88
|
7. 如果 gate 因基础设施、凭证缺失、产品行为不清或高风险架构变化失败,进入 `BLOCKED`。
|
|
87
89
|
8. gate 通过后调用 `pjsdlc_implementation_doc`。
|
|
88
|
-
9. 只有 gate 通过、承诺的 runnable entry/exit 已实现或明确 `BLOCKED`,且 implementation doc 校验通过后,才能把任务标记为 `done`。
|
|
90
|
+
9. 只有 gate 通过、承诺的 runnable entry/exit 已实现或明确 `BLOCKED`,implementation doc 包含结构化 `Development Evidence`,且 implementation doc 校验通过后,才能把任务标记为 `done`。
|
|
89
91
|
10. 任务完成并写入或更新相关 implementation doc、刷新 `overview.md`、记录 gate 后,先创建 task implementation commit;此时不要移除该 task。
|
|
90
92
|
11. task implementation commit 必须发生在 task 移除前;后续默认不要读取其中的执行期字段,历史查询以模块级 implementation doc、RFC、PRD、tech plan 和代码为主。
|
|
91
93
|
12. implementation commit 完成后,从当前 `plan.yaml` 移除该 task,并创建 task completion ledger commit。
|
|
@@ -103,6 +105,7 @@ done task 的执行流水不在当前 `plan.yaml` 长期保留,也不是默认
|
|
|
103
105
|
- [ ] open task 在 `plan.yaml` 中包含完整执行合同。
|
|
104
106
|
- [ ] 当前任务仍然是单一清晰的执行单元。
|
|
105
107
|
- [ ] 技术方案或 task 承诺的 API/CLI/adapter/worker/provider、配置契约、输出/副作用和 fixture/live 边界已可运行并写入 implementation doc,或已明确 `BLOCKED`/后续 dev task。
|
|
108
|
+
- [ ] implementation doc `Development Evidence` 已记录 `Runnable Entry`、`Observable Exit`、`Basic Self-test Evidence`,或写明带原因的 `Not applicable`。
|
|
106
109
|
- [ ] 如果当前 task 来自 `plan.draft.yaml.tasks[]`,源 draft 已在 promote 时从 draft 列表删除。
|
|
107
110
|
- [ ] implementation doc 已生成或更新,并反映相关模块的真实代码。
|
|
108
111
|
- [ ] 如果启用了 `parallel_execution`,worker owned paths、forbidden paths、required gates 和主 Agent 集成结果已记录。
|
|
@@ -17,7 +17,7 @@ description: Use after development gates pass to update module-level implementat
|
|
|
17
17
|
|
|
18
18
|
文档应帮助后来者快速理解:某个模块或核心数据流的当前实现是什么、关键对象/函数职责是什么、行为如何从输入流到输出、测试覆盖了什么、还有什么未覆盖。task id 只作为 provenance,不作为默认切片粒度。
|
|
19
19
|
|
|
20
|
-
如果模块包含或承诺可运行系统边界,implementation doc 必须记录 runnable entry/exit:API/CLI/server route/adapter/worker/provider 的调用方式、配置契约、输入来源、输出或副作用、fixture/live
|
|
20
|
+
如果模块包含或承诺可运行系统边界,implementation doc 必须记录 runnable entry/exit:API/CLI/server route/adapter/worker/provider 的调用方式、配置契约、输入来源、输出或副作用、fixture/live 模式边界,以及哪些真实外部执行器尚未实现。还必须在 `Development Evidence` 中记录开发阶段实际验证过的 `Runnable Entry`、`Observable Exit` 和 `Basic Self-test Evidence`;确实没有应用入口时,`Not applicable` 必须写清原因。不能把未来才会实现的入口写成当前事实。
|
|
21
21
|
|
|
22
22
|
## 输入
|
|
23
23
|
|
|
@@ -48,8 +48,9 @@ description: Use after development gates pass to update module-level implementat
|
|
|
48
48
|
2. 每个被记录的文件都应说明它在该模块或数据流中的作用和关键函数/对象。
|
|
49
49
|
3. 与技术方案的偏移必须明确记录,即便该偏移是合理的。
|
|
50
50
|
4. runnable entry/exit、配置契约和 fixture/live 边界必须记录当前事实;缺失项写入 `未覆盖(Not covered)` 或方案偏移。
|
|
51
|
-
5.
|
|
52
|
-
6.
|
|
51
|
+
5. `Development Evidence` 必须包含实际可调用入口、可观察出口和开发自测证据;页面类任务记录 dev server/page URL 与 browser check,API/CLI/worker/RPA 类任务记录 invocation command/endpoint 与 response/output/side effect。
|
|
52
|
+
6. 测试覆盖必须列出具体测试,或明确记录覆盖缺口。
|
|
53
|
+
7. 文档粒度保持在模块、子系统或核心数据流级别;不要默认按 task 建文档,也不要写成跨全项目的巨型百科。
|
|
53
54
|
|
|
54
55
|
## 完成检查
|
|
55
56
|
|
|
@@ -58,6 +59,7 @@ description: Use after development gates pass to update module-level implementat
|
|
|
58
59
|
- [ ] 真实代码结构表已填写。
|
|
59
60
|
- [ ] 核心数据流已说明。
|
|
60
61
|
- [ ] runnable entry/exit、配置契约和 fixture/live 边界已记录,或缺失项已明确标注。
|
|
62
|
+
- [ ] `Development Evidence` 已记录 `Runnable Entry`、`Observable Exit`、`Basic Self-test Evidence`,或带原因的 `Not applicable`。
|
|
61
63
|
- [ ] 已判断 implementation doc 的语义切片边界。
|
|
62
64
|
- [ ] 方案偏移和测试覆盖已记录。
|
|
63
65
|
- [ ] `.docs/INDEX.md` 已链接 implementation doc。
|
|
@@ -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、adapter、worker、provider、外部发送/写入执行器、配置契约或 live/fixture 双模式边界,Review
|
|
20
|
+
Review 必须把“当前模块没有可运行入口/出口”视为阻断项,而不是普通测试缺口。凡 PRD、技术方案或 implementation doc 承诺 API、CLI、server route、adapter、worker、provider、外部发送/写入执行器、配置契约或 live/fixture 双模式边界,Review 都要核对真实代码和实现文档是否提供可调用入口、输出/副作用边界和验证方式;implementation doc 还必须包含结构化 `Development Evidence`,说明 `Runnable Entry`、`Observable Exit` 和 `Basic Self-test Evidence`,或带原因的 `Not applicable`。缺失时 gate decision 应为 `BLOCKED`,并要求回到 SPRINTING/RFC,而不是允许进入 TESTING 后补 runtime。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
|
|
|
@@ -66,7 +66,7 @@ Review 阶段受 `plan.yaml` 管控:
|
|
|
66
66
|
2. Findings 放在最前面,并按严重程度排序。
|
|
67
67
|
3. 每条 finding 尽量引用文件、需求、任务或文档路径。
|
|
68
68
|
4. 区分 blocking issues 和 follow-up improvements。
|
|
69
|
-
5. 缺少已承诺的 runnable entry/exit
|
|
69
|
+
5. 缺少已承诺的 runnable entry/exit、配置契约、fixture/live 边界或 `Development Evidence` 时,必须作为 P0/P1 blocking finding。
|
|
70
70
|
6. 如果未发现问题,明确说明,并列出剩余测试缺口或残余风险。
|
|
71
71
|
7. Review 阶段一次只执行一个 `TASK-*` task。
|
|
72
72
|
|
|
@@ -78,6 +78,7 @@ Review 阶段受 `plan.yaml` 管控:
|
|
|
78
78
|
- [ ] 已评估需求一致性。
|
|
79
79
|
- [ ] 已评估架构和可维护性风险。
|
|
80
80
|
- [ ] 已评估 runnable entry/exit、配置契约和 fixture/live 边界是否足以进入 TESTING。
|
|
81
|
+
- [ ] 已评估 implementation doc 是否包含 Runnable Entry、Observable Exit 和 Basic Self-test Evidence。
|
|
81
82
|
- [ ] 已判断 review slice 的范围和风险主题边界。
|
|
82
83
|
- [ ] 已列出测试缺口。
|
|
83
84
|
- [ ] 已运行 `make docs-overview` 刷新 `.docs/<stage>/overview.md`。
|
|
@@ -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 已经交付的入口做输入/输出验证。可以补充测试、fixture、mock、assertion helper 和测试文档,但不能在 TESTING 中新增或长期维护 product runtime、server/API/CLI/adapter、direct poller、cloud bootstrap、systemd unit、真实 provider adapter、package runtime script 或部署脚本。如果发现真实入口/出口不存在、live 模式不可调用、配置契约缺失或用户目标与已实现通道不一致,应记录 `BLOCKED`、生成 RFC 或后续 dev task 建议,并停止把测试阶段扩大成开发/集成搭建。开发尚未交付可测试 entry/exit 时,不要在 `.docs/07_test/**` 提前生成正式测试用例或正式报告;验收思路应留在 PRD acceptance criteria、tech plan verification strategy 或非 `.docs/07_test/**` 的草稿说明里。
|
|
20
|
+
TESTING 只能调用 SPRINTING 已经交付的入口做输入/输出验证。可以补充测试、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`、live 模式不可调用、配置契约缺失或用户目标与已实现通道不一致,应记录 `BLOCKED`、生成 RFC 或后续 dev task 建议,并停止把测试阶段扩大成开发/集成搭建。开发尚未交付可测试 entry/exit 时,不要在 `.docs/07_test/**` 提前生成正式测试用例或正式报告;验收思路应留在 PRD acceptance criteria、tech plan verification strategy 或非 `.docs/07_test/**` 的草稿说明里。
|
|
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
|
|
|
@@ -67,11 +67,11 @@ TESTING 只能调用 SPRINTING 已经交付的入口做输入/输出验证。可
|
|
|
67
67
|
|
|
68
68
|
## 规则
|
|
69
69
|
|
|
70
|
-
1. 测试用例必须追溯到 PRD acceptance criteria 或 Review findings,并绑定 SPRINTING/REVIEWING 已确认的 runnable entry/exit。
|
|
70
|
+
1. 测试用例必须追溯到 PRD acceptance criteria 或 Review findings,并绑定 SPRINTING/REVIEWING 已确认的 runnable entry/exit 和 Development Evidence。
|
|
71
71
|
2. 根据风险补充边界、负向、回归和集成测试。
|
|
72
72
|
3. 如果有意延后覆盖,必须记录风险和 follow-up。
|
|
73
73
|
4. 不得新增 product runtime、server/API/CLI/adapter、poller、cloud bootstrap、systemd unit、真实 provider adapter、package runtime script 或部署脚本;这些属于 SPRINTING/RFC。
|
|
74
|
-
5.
|
|
74
|
+
5. 测试发现入口/出口或 Development Evidence 缺失时,Final decision 必须为 `BLOCKED`,并指出回到 SPRINTING/RFC 的具体条件。
|
|
75
75
|
6. 新测试策略使用 `.docs/07_test/TEST_STRATEGY.md`,新测试用例使用 `.docs/07_test/TEST_CASES.md`,执行报告使用 `.docs/07_test/TEST_REPORT.md`;不要新建或继续依赖 `.docs/07_test/TEST_PLAN.md`。
|
|
76
76
|
7. `TEST_REPORT.md` 不得包含 `pending`、`TBD`、`待填`、`TODO` 或占位结论;未执行或不可执行时 Final decision 必须为 `BLOCKED` 并给出恢复条件。
|
|
77
77
|
8. RFC 改变技术路线、entry/exit 或验收边界后,必须确认 `.docs/07_test/**` 中旧路线测试证据已删除或不再从 `.docs/INDEX.md` 暴露。
|
|
@@ -86,6 +86,7 @@ TESTING 只能调用 SPRINTING 已经交付的入口做输入/输出验证。可
|
|
|
86
86
|
- [ ] 当前 task 已从 `plan.yaml` 移除,或因中断/blocker 保留为可恢复 open task。
|
|
87
87
|
- [ ] Regression checklist 已完成。
|
|
88
88
|
- [ ] 测试只调用既有 runnable entry/exit;未在 TESTING 中新增 product runtime、bootstrap、provider adapter、deploy 或 package runtime script。
|
|
89
|
+
- [ ] 已核对 implementation doc 中的 Development Evidence,并只基于已交付入口设计测试。
|
|
89
90
|
- [ ] 已判断 test report / test matrix 的语义切片边界。
|
|
90
91
|
- [ ] 未把测试计划、测试用例或待填内容写成 `TEST_REPORT.md`。
|
|
91
92
|
- [ ] 已确认 `.docs/07_test/**` 只包含当前方案仍有效的测试事实。
|
|
@@ -40,7 +40,14 @@ Input
|
|
|
40
40
|
- Fixture/live boundary:
|
|
41
41
|
- Missing runtime boundaries:
|
|
42
42
|
|
|
43
|
-
## 6.
|
|
43
|
+
## 6. Development Evidence(开发自测证据)
|
|
44
|
+
|
|
45
|
+
- Runnable Entry:
|
|
46
|
+
- Observable Exit:
|
|
47
|
+
- Basic Self-test Evidence:
|
|
48
|
+
- Not applicable:
|
|
49
|
+
|
|
50
|
+
## 7. 关键实现逻辑
|
|
44
51
|
|
|
45
52
|
- 输入校验(Input validation):
|
|
46
53
|
- 核心分支(Core branches):
|
|
@@ -48,22 +55,22 @@ Input
|
|
|
48
55
|
- 边界兜底(Boundary fallback):
|
|
49
56
|
- 性能或并发注意事项(Performance or concurrency notes):
|
|
50
57
|
|
|
51
|
-
##
|
|
58
|
+
## 8. 与技术方案的偏移
|
|
52
59
|
|
|
53
60
|
-
|
|
54
61
|
|
|
55
|
-
##
|
|
62
|
+
## 9. 测试覆盖(Test Coverage)
|
|
56
63
|
|
|
57
64
|
| 测试(Test) | 覆盖范围(Coverage) | 结果(Result) |
|
|
58
65
|
|---|---|---|
|
|
59
66
|
| | | |
|
|
60
67
|
|
|
61
|
-
##
|
|
68
|
+
## 10. 变更记录(Change Log)
|
|
62
69
|
|
|
63
70
|
| 日期(Date) | Task ID | Commit | 摘要(Summary) |
|
|
64
71
|
|---|---|---|---|
|
|
65
72
|
| | | | |
|
|
66
73
|
|
|
67
|
-
##
|
|
74
|
+
## 11. 后续维护注意事项
|
|
68
75
|
|
|
69
76
|
-
|
package/dist/commands/sync.js
CHANGED
|
@@ -2,6 +2,9 @@ import { runSync } from "../lib/sync-engine.js";
|
|
|
2
2
|
export async function sync() {
|
|
3
3
|
const report = await runSync(process.cwd());
|
|
4
4
|
console.log(`sync changed=${report.changed.length} skipped=${report.skipped.length} blocked=${report.blocked.length}`);
|
|
5
|
+
for (const skipped of report.skipped.filter((line) => line.includes("customized"))) {
|
|
6
|
+
console.log(`skipped: ${skipped}`);
|
|
7
|
+
}
|
|
5
8
|
for (const blocked of report.blocked) {
|
|
6
9
|
console.error(`blocked: ${blocked}`);
|
|
7
10
|
}
|
package/dist/lib/init.js
CHANGED
|
@@ -3,6 +3,7 @@ import { writeConfigIfMissing } from "./config.js";
|
|
|
3
3
|
import { harnessConfigPath, harnessPath, harnessRoot } from "./harness-root.js";
|
|
4
4
|
import { ensureDir, pathExists, writeTextIfChanged } from "./fs.js";
|
|
5
5
|
import { runSync } from "./sync-engine.js";
|
|
6
|
+
import { syncDocsIndexMaintenanceSection, syncMemoryGuidanceSection } from "./user-owned-sections.js";
|
|
6
7
|
const DOC_DIRS = [
|
|
7
8
|
".docs/00_raw",
|
|
8
9
|
".docs/01_product",
|
|
@@ -55,24 +56,24 @@ async function createProjectState(projectRoot, root, report) {
|
|
|
55
56
|
],
|
|
56
57
|
[harnessPath(root, "state", "plan.yaml"), `current_task_id: ""\nnext_task_sequence: 1\ntasks: []\n`],
|
|
57
58
|
[harnessPath(root, "state", "plan.draft.yaml"), `next_task_sequence: 1\ntasks: []\n`],
|
|
58
|
-
[
|
|
59
|
-
harnessPath(root, "state", "memory.md"),
|
|
60
|
-
"# Project Memory\n\n短期执行计划写入 plan.yaml;长期稳定知识只在这里记录简短摘要和链接。完整决策背景、备选方案、取舍和后果写入 `.docs/05_decisions/` ADR 或其它 `.docs/**` 正式事实源。\n"
|
|
61
|
-
]
|
|
62
59
|
];
|
|
63
60
|
for (const [relative, content] of files) {
|
|
64
61
|
if (await writeTextIfChanged(path.join(projectRoot, relative), content)) {
|
|
65
62
|
report.push(`created ${relative}`);
|
|
66
63
|
}
|
|
67
64
|
}
|
|
65
|
+
await syncMemoryGuidanceSection(projectRoot, root, {
|
|
66
|
+
changed: report,
|
|
67
|
+
skipped: []
|
|
68
|
+
});
|
|
68
69
|
}
|
|
69
70
|
async function createDocs(projectRoot, report) {
|
|
70
71
|
for (const dir of DOC_DIRS) {
|
|
71
72
|
await ensureDir(path.join(projectRoot, dir));
|
|
72
73
|
await writeTextIfChanged(path.join(projectRoot, dir, ".gitkeep"), "");
|
|
73
74
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
75
|
+
await syncDocsIndexMaintenanceSection(projectRoot, {
|
|
76
|
+
changed: report,
|
|
77
|
+
skipped: []
|
|
78
|
+
});
|
|
78
79
|
}
|
|
@@ -10,6 +10,8 @@ export declare const MAKEFILE_BLOCK_START = "# pjsdlc:sdlc-harness:make:begin";
|
|
|
10
10
|
export declare const MAKEFILE_BLOCK_END = "# pjsdlc:sdlc-harness:make:end";
|
|
11
11
|
export declare const LEGACY_MAKEFILE_BLOCK_START = "# sdlc-harness:make:begin";
|
|
12
12
|
export declare const LEGACY_MAKEFILE_BLOCK_END = "# sdlc-harness:make:end";
|
|
13
|
+
export declare const GITHUB_WORKFLOW_BLOCK_START = "# pjsdlc:sdlc-harness:github-workflow:begin";
|
|
14
|
+
export declare const GITHUB_WORKFLOW_BLOCK_END = "# pjsdlc:sdlc-harness:github-workflow:end";
|
|
13
15
|
export declare const MANAGED_METADATA_START = "<!-- pjsdlc:sdlc-harness-managed";
|
|
14
16
|
export declare const LEGACY_MANAGED_METADATA_START = "<!-- sdlc-harness-managed";
|
|
15
17
|
export declare const MANAGED_METADATA_END = "-->";
|
package/dist/lib/managed-file.js
CHANGED
|
@@ -6,6 +6,8 @@ export const MAKEFILE_BLOCK_START = "# pjsdlc:sdlc-harness:make:begin";
|
|
|
6
6
|
export const MAKEFILE_BLOCK_END = "# pjsdlc:sdlc-harness:make:end";
|
|
7
7
|
export const LEGACY_MAKEFILE_BLOCK_START = "# sdlc-harness:make:begin";
|
|
8
8
|
export const LEGACY_MAKEFILE_BLOCK_END = "# sdlc-harness:make:end";
|
|
9
|
+
export const GITHUB_WORKFLOW_BLOCK_START = "# pjsdlc:sdlc-harness:github-workflow:begin";
|
|
10
|
+
export const GITHUB_WORKFLOW_BLOCK_END = "# pjsdlc:sdlc-harness:github-workflow:end";
|
|
9
11
|
export const MANAGED_METADATA_START = "<!-- pjsdlc:sdlc-harness-managed";
|
|
10
12
|
export const LEGACY_MANAGED_METADATA_START = "<!-- sdlc-harness-managed";
|
|
11
13
|
export const MANAGED_METADATA_END = "-->";
|
package/dist/lib/migrations.js
CHANGED
|
@@ -3,6 +3,7 @@ import { readdir, rename, rm } from "node:fs/promises";
|
|
|
3
3
|
import { defaultConfig, readConfig } from "./config.js";
|
|
4
4
|
import { ensureDir, listFiles, pathExists, readText, writeTextIfChanged } from "./fs.js";
|
|
5
5
|
import { harnessConfigPath, harnessPath, harnessRoot } from "./harness-root.js";
|
|
6
|
+
import { syncDocsIndexMaintenanceSection, syncMemoryGuidanceSection } from "./user-owned-sections.js";
|
|
6
7
|
import { parseYaml, stringifyYaml } from "./yaml.js";
|
|
7
8
|
export const CURRENT_SCHEMA_VERSION = "1";
|
|
8
9
|
export const migrations = [];
|
|
@@ -27,7 +28,7 @@ export async function runMigrations(projectRoot) {
|
|
|
27
28
|
await migratePlan(projectRoot, root, report, "plan.draft.yaml", "tasks.draft.yaml", { activePlan: false });
|
|
28
29
|
await removeLegacyGateResults(projectRoot, root, report);
|
|
29
30
|
await removeLegacyCheckpoints(projectRoot, root, report);
|
|
30
|
-
await
|
|
31
|
+
await ensureUserOwnedGuidanceSections(projectRoot, root, report);
|
|
31
32
|
return report;
|
|
32
33
|
}
|
|
33
34
|
async function migrateConfig(projectRoot, root, report) {
|
|
@@ -161,6 +162,9 @@ function migrateManagedFiles(managedFiles, root) {
|
|
|
161
162
|
migrated.unshift(makefileEntry);
|
|
162
163
|
}
|
|
163
164
|
}
|
|
165
|
+
if (!seen.has(".github/workflows/harness.yml")) {
|
|
166
|
+
push({ path: ".github/workflows/harness.yml", strategy: "create-if-missing" });
|
|
167
|
+
}
|
|
164
168
|
return migrated;
|
|
165
169
|
}
|
|
166
170
|
async function migrateLifecycle(projectRoot, root, report) {
|
|
@@ -349,15 +353,7 @@ async function removeLegacyGateResults(projectRoot, root, report) {
|
|
|
349
353
|
await rm(gateResultsPath, { force: true });
|
|
350
354
|
report.changed.push(relativeGateResultsPath);
|
|
351
355
|
}
|
|
352
|
-
async function
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
if (await pathExists(memoryPath)) {
|
|
356
|
-
report.skipped.push(relativeMemoryPath);
|
|
357
|
-
return;
|
|
358
|
-
}
|
|
359
|
-
const content = "# Project Memory\n\n记录跨阶段长期有效知识的简短摘要和链接。完整决策背景、备选方案、取舍和后果写入 `.docs/05_decisions/` ADR 或其它 `.docs/**` 正式事实源。\n";
|
|
360
|
-
if (await writeTextIfChanged(memoryPath, content)) {
|
|
361
|
-
report.changed.push(relativeMemoryPath);
|
|
362
|
-
}
|
|
356
|
+
async function ensureUserOwnedGuidanceSections(projectRoot, root, report) {
|
|
357
|
+
await syncMemoryGuidanceSection(projectRoot, root, report);
|
|
358
|
+
await syncDocsIndexMaintenanceSection(projectRoot, report);
|
|
363
359
|
}
|
package/dist/lib/sync-engine.js
CHANGED
|
@@ -2,8 +2,9 @@ import path from "node:path";
|
|
|
2
2
|
import { readConfig } from "./config.js";
|
|
3
3
|
import { harnessRoot } from "./harness-root.js";
|
|
4
4
|
import { copyTree, listFiles, pathExists, readText, writeTextIfChanged } from "./fs.js";
|
|
5
|
-
import { AGENTS_BLOCK_MARKERS, MAKEFILE_BLOCK_END, MAKEFILE_BLOCK_MARKERS, MAKEFILE_BLOCK_START, MANAGED_BLOCK_END, MANAGED_BLOCK_START } from "./managed-file.js";
|
|
5
|
+
import { AGENTS_BLOCK_MARKERS, GITHUB_WORKFLOW_BLOCK_END, GITHUB_WORKFLOW_BLOCK_START, MAKEFILE_BLOCK_END, MAKEFILE_BLOCK_MARKERS, MAKEFILE_BLOCK_START, MANAGED_BLOCK_END, MANAGED_BLOCK_START } from "./managed-file.js";
|
|
6
6
|
import { packageAssetPath } from "./paths.js";
|
|
7
|
+
import { syncProjectGuidanceSections } from "./user-owned-sections.js";
|
|
7
8
|
import { parseYaml, stringifyYaml } from "./yaml.js";
|
|
8
9
|
export function emptySyncReport() {
|
|
9
10
|
return {
|
|
@@ -19,6 +20,7 @@ export async function runSync(projectRoot) {
|
|
|
19
20
|
for (const managedFile of config.managed_files) {
|
|
20
21
|
await syncManagedFile(projectRoot, root, managedFile, report);
|
|
21
22
|
}
|
|
23
|
+
await syncProjectGuidanceSections(projectRoot, root, report);
|
|
22
24
|
return report;
|
|
23
25
|
}
|
|
24
26
|
async function syncManagedFile(projectRoot, root, managedFile, report) {
|
|
@@ -48,11 +50,7 @@ async function syncManagedFile(projectRoot, root, managedFile, report) {
|
|
|
48
50
|
return;
|
|
49
51
|
}
|
|
50
52
|
if (managedFile.path === ".github/workflows/harness.yml") {
|
|
51
|
-
|
|
52
|
-
report.skipped.push(managedFile.path);
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
await syncFile(packageAssetPath("github", "harness.yml"), destination, report, "skip-if-missing");
|
|
53
|
+
await syncGithubWorkflow(packageAssetPath("github", "harness.yml"), destination, managedFile.path, report);
|
|
56
54
|
return;
|
|
57
55
|
}
|
|
58
56
|
report.skipped.push(managedFile.path);
|
|
@@ -368,3 +366,61 @@ async function syncFile(source, destination, report, missingMode) {
|
|
|
368
366
|
report.skipped.push(destination);
|
|
369
367
|
}
|
|
370
368
|
}
|
|
369
|
+
async function syncGithubWorkflow(source, destination, relativePath, report) {
|
|
370
|
+
if (!(await pathExists(source))) {
|
|
371
|
+
report.skipped.push(relativePath);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const sourceContent = await readText(source);
|
|
375
|
+
if (!(await pathExists(destination))) {
|
|
376
|
+
if (await writeTextIfChanged(destination, sourceContent)) {
|
|
377
|
+
report.changed.push(relativePath);
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
report.skipped.push(relativePath);
|
|
381
|
+
}
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const existing = await readText(destination);
|
|
385
|
+
const markerState = workflowMarkerState(existing);
|
|
386
|
+
if (markerState === "invalid") {
|
|
387
|
+
report.blocked.push(`${relativePath}: incomplete managed workflow markers`);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (markerState === "managed" || normalizeWorkflow(existing) === normalizeWorkflow(stripWorkflowMarkers(sourceContent))) {
|
|
391
|
+
if (await writeTextIfChanged(destination, sourceContent)) {
|
|
392
|
+
report.changed.push(relativePath);
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
report.skipped.push(relativePath);
|
|
396
|
+
}
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
report.skipped.push(`${relativePath}: customized`);
|
|
400
|
+
}
|
|
401
|
+
function workflowMarkerState(content) {
|
|
402
|
+
const startIndex = content.indexOf(GITHUB_WORKFLOW_BLOCK_START);
|
|
403
|
+
const endIndex = content.indexOf(GITHUB_WORKFLOW_BLOCK_END);
|
|
404
|
+
const hasStart = startIndex >= 0;
|
|
405
|
+
const hasEnd = endIndex >= 0;
|
|
406
|
+
if (!hasStart && !hasEnd) {
|
|
407
|
+
return "missing";
|
|
408
|
+
}
|
|
409
|
+
if (hasStart !== hasEnd || endIndex < startIndex) {
|
|
410
|
+
return "invalid";
|
|
411
|
+
}
|
|
412
|
+
if (content.indexOf(GITHUB_WORKFLOW_BLOCK_START, startIndex + GITHUB_WORKFLOW_BLOCK_START.length) >= 0 ||
|
|
413
|
+
content.indexOf(GITHUB_WORKFLOW_BLOCK_END, endIndex + GITHUB_WORKFLOW_BLOCK_END.length) >= 0) {
|
|
414
|
+
return "invalid";
|
|
415
|
+
}
|
|
416
|
+
return "managed";
|
|
417
|
+
}
|
|
418
|
+
function stripWorkflowMarkers(content) {
|
|
419
|
+
return content
|
|
420
|
+
.split(/\r?\n/)
|
|
421
|
+
.filter((line) => line.trim() !== GITHUB_WORKFLOW_BLOCK_START && line.trim() !== GITHUB_WORKFLOW_BLOCK_END)
|
|
422
|
+
.join("\n");
|
|
423
|
+
}
|
|
424
|
+
function normalizeWorkflow(content) {
|
|
425
|
+
return content.replace(/\r\n/g, "\n").trim();
|
|
426
|
+
}
|
package/dist/lib/upgrade.js
CHANGED
|
@@ -7,6 +7,9 @@ export async function runUpgrade(projectRoot) {
|
|
|
7
7
|
lines.push(`migrations changed=${migrationReport.changed.length} skipped=${migrationReport.skipped.length}`);
|
|
8
8
|
const syncReport = await runSync(projectRoot);
|
|
9
9
|
lines.push(`sync changed=${syncReport.changed.length} skipped=${syncReport.skipped.length} blocked=${syncReport.blocked.length}`);
|
|
10
|
+
for (const skipped of syncReport.skipped.filter((line) => line.includes("customized"))) {
|
|
11
|
+
lines.push(`sync skipped: ${skipped}`);
|
|
12
|
+
}
|
|
10
13
|
const doctor = await runDoctor(projectRoot);
|
|
11
14
|
lines.push(`doctor warnings=${doctor.warnings.length} errors=${doctor.errors.length}`);
|
|
12
15
|
if (syncReport.blocked.length > 0 || doctor.errors.length > 0) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface UserOwnedSectionReport {
|
|
2
|
+
changed: string[];
|
|
3
|
+
skipped: string[];
|
|
4
|
+
}
|
|
5
|
+
export declare function syncProjectGuidanceSections(projectRoot: string, root: string, report: UserOwnedSectionReport): Promise<void>;
|
|
6
|
+
export declare function syncMemoryGuidanceSection(projectRoot: string, root: string, report: UserOwnedSectionReport): Promise<void>;
|
|
7
|
+
export declare function syncDocsIndexMaintenanceSection(projectRoot: string, report: UserOwnedSectionReport): Promise<void>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { pathExists, readText, writeTextIfChanged } from "./fs.js";
|
|
3
|
+
import { harnessPath } from "./harness-root.js";
|
|
4
|
+
const MEMORY_GUIDANCE_HEADING = "## Harness Guidance";
|
|
5
|
+
const DOCS_INDEX_RULES_HEADING = "## Harness Maintenance Rules";
|
|
6
|
+
const LEGACY_MEMORY_PARAGRAPHS = [
|
|
7
|
+
"短期执行计划写入 plan.yaml;长期稳定知识只在这里记录简短摘要和链接。完整决策背景、备选方案、取舍和后果写入 `.docs/05_decisions/` ADR 或其它 `.docs/**` 正式事实源。",
|
|
8
|
+
"记录跨阶段长期有效知识的简短摘要和链接。完整决策背景、备选方案、取舍和后果写入 `.docs/05_decisions/` ADR 或其它 `.docs/**` 正式事实源。",
|
|
9
|
+
"内容保持简短,详细说明链接到 `.docs/` 下的对应文档。完整决策背景、备选方案、取舍和后果应写入 `.docs/05_decisions/` ADR 或其它正式 `.docs/**` 事实源。"
|
|
10
|
+
];
|
|
11
|
+
const LEGACY_INDEX_MAINTENANCE_SECTION = [
|
|
12
|
+
"## 维护规则",
|
|
13
|
+
"",
|
|
14
|
+
"- 每个新增产物都要从本索引链接。",
|
|
15
|
+
"- 仍属于产品、架构、实现、测试或 RFC 事实源的过时产物标记为 superseded;短期执行计划和历史发布流水以 git、tag、registry、CI 或外部 release 系统追溯。",
|
|
16
|
+
"- task/release 的历史动作记录以 git commit、tag 或外部 release 系统为准,不再维护 `<harnessRoot>/archive/` 常规归档。",
|
|
17
|
+
"- implementation docs 必须对齐真实代码,而不只是原始技术方案。"
|
|
18
|
+
].join("\n");
|
|
19
|
+
export async function syncProjectGuidanceSections(projectRoot, root, report) {
|
|
20
|
+
await syncMemoryGuidanceSection(projectRoot, root, report);
|
|
21
|
+
await syncDocsIndexMaintenanceSection(projectRoot, report);
|
|
22
|
+
}
|
|
23
|
+
export async function syncMemoryGuidanceSection(projectRoot, root, report) {
|
|
24
|
+
const relativePath = harnessPath(root, "state", "memory.md");
|
|
25
|
+
const targetPath = path.join(projectRoot, relativePath);
|
|
26
|
+
const existing = (await pathExists(targetPath)) ? await readText(targetPath) : "# Project Memory\n";
|
|
27
|
+
const next = mergeMarkdownSection(removeLegacyMemoryGuidance(existing), MEMORY_GUIDANCE_HEADING, renderMemoryGuidanceSection(root));
|
|
28
|
+
await writeSectionIfChanged(targetPath, relativePath, next, report);
|
|
29
|
+
}
|
|
30
|
+
export async function syncDocsIndexMaintenanceSection(projectRoot, report) {
|
|
31
|
+
const relativePath = ".docs/INDEX.md";
|
|
32
|
+
const targetPath = path.join(projectRoot, relativePath);
|
|
33
|
+
const existing = (await pathExists(targetPath))
|
|
34
|
+
? await readText(targetPath)
|
|
35
|
+
: "# Documentation Index\n\n本文件是 AI SDLC Harness 的文档路由表。\n";
|
|
36
|
+
const next = mergeMarkdownSection(removeLegacyIndexMaintenanceSection(existing), DOCS_INDEX_RULES_HEADING, renderDocsIndexMaintenanceSection());
|
|
37
|
+
await writeSectionIfChanged(targetPath, relativePath, next, report);
|
|
38
|
+
}
|
|
39
|
+
function renderMemoryGuidanceSection(root) {
|
|
40
|
+
const planPath = harnessPath(root, "state", "plan.yaml");
|
|
41
|
+
return [
|
|
42
|
+
MEMORY_GUIDANCE_HEADING,
|
|
43
|
+
"",
|
|
44
|
+
"- 内容保持简短,详细说明链接到 `.docs/` 下的对应文档。",
|
|
45
|
+
`- 短期执行计划写入 \`${planPath}\`;长期稳定知识只在这里记录简短摘要和链接。`,
|
|
46
|
+
"- 完整决策背景、备选方案、取舍和后果应写入 `.docs/05_decisions/` ADR 或其它正式 `.docs/**` 事实源。"
|
|
47
|
+
].join("\n");
|
|
48
|
+
}
|
|
49
|
+
function renderDocsIndexMaintenanceSection() {
|
|
50
|
+
return [
|
|
51
|
+
DOCS_INDEX_RULES_HEADING,
|
|
52
|
+
"",
|
|
53
|
+
"- `overview.md` 是 generated artifact,用于浏览和阶段交接;不要手写或局部编辑。",
|
|
54
|
+
"- Markdown slices 和 `.docs/INDEX.md` 是事实源。",
|
|
55
|
+
"- 任意 `.docs/<stage>/**/*.md` 新增、修改、拆分、合并或废弃后,运行 `make docs-overview`。",
|
|
56
|
+
"- 提交或阶段交付前,运行 `make validate-doc-overviews` 或 `make validate-harness` 确认 overview 未过期。",
|
|
57
|
+
"- 每个新增产物都要从本索引链接;implementation docs 必须对齐真实代码。"
|
|
58
|
+
].join("\n");
|
|
59
|
+
}
|
|
60
|
+
function removeLegacyMemoryGuidance(content) {
|
|
61
|
+
let next = content;
|
|
62
|
+
for (const paragraph of LEGACY_MEMORY_PARAGRAPHS) {
|
|
63
|
+
next = next.replace(paragraph, "");
|
|
64
|
+
}
|
|
65
|
+
return compactBlankLines(next);
|
|
66
|
+
}
|
|
67
|
+
function removeLegacyIndexMaintenanceSection(content) {
|
|
68
|
+
return compactBlankLines(content.replace(LEGACY_INDEX_MAINTENANCE_SECTION, ""));
|
|
69
|
+
}
|
|
70
|
+
function mergeMarkdownSection(existing, heading, section) {
|
|
71
|
+
const normalizedExisting = existing.replace(/\r\n/g, "\n").trimEnd();
|
|
72
|
+
const normalizedSection = section.trimEnd();
|
|
73
|
+
if (!normalizedExisting) {
|
|
74
|
+
return `${normalizedSection}\n`;
|
|
75
|
+
}
|
|
76
|
+
const lines = normalizedExisting.split("\n");
|
|
77
|
+
const startIndex = lines.findIndex((line) => line.trim() === heading);
|
|
78
|
+
if (startIndex < 0) {
|
|
79
|
+
return `${normalizedExisting}\n\n${normalizedSection}\n`;
|
|
80
|
+
}
|
|
81
|
+
const headingLevel = heading.match(/^#+/)?.[0].length ?? 2;
|
|
82
|
+
let endIndex = lines.length;
|
|
83
|
+
for (let index = startIndex + 1; index < lines.length; index += 1) {
|
|
84
|
+
const match = lines[index].match(/^(#{1,6})\s+/);
|
|
85
|
+
if (match && match[1].length <= headingLevel) {
|
|
86
|
+
endIndex = index;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const before = lines.slice(0, startIndex).join("\n").trimEnd();
|
|
91
|
+
const after = lines.slice(endIndex).join("\n").trimStart();
|
|
92
|
+
const parts = [before, normalizedSection, after].filter((part) => part.trim());
|
|
93
|
+
return `${parts.join("\n\n")}\n`;
|
|
94
|
+
}
|
|
95
|
+
async function writeSectionIfChanged(targetPath, relativePath, content, report) {
|
|
96
|
+
if (await writeTextIfChanged(targetPath, content)) {
|
|
97
|
+
report.changed.push(relativePath);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
report.skipped.push(relativePath);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function compactBlankLines(content) {
|
|
104
|
+
return content.replace(/\r\n/g, "\n").replace(/\n{3,}/g, "\n\n");
|
|
105
|
+
}
|
package/dist/lib/validators.js
CHANGED
|
@@ -83,6 +83,36 @@ const RUNNABLE_ENTRY_EXIT_TERMS = [
|
|
|
83
83
|
"入口/出口",
|
|
84
84
|
"not applicable"
|
|
85
85
|
];
|
|
86
|
+
const DEVELOPMENT_EVIDENCE_TERMS = ["development evidence", "开发自测证据"];
|
|
87
|
+
const EVIDENCE_PLACEHOLDER_TERMS = [
|
|
88
|
+
"pending",
|
|
89
|
+
"tbd",
|
|
90
|
+
"todo",
|
|
91
|
+
"placeholder",
|
|
92
|
+
"待填",
|
|
93
|
+
"待补",
|
|
94
|
+
"待确认"
|
|
95
|
+
];
|
|
96
|
+
const PAGE_TASK_TERMS = ["frontend", "front-end", "browser", "page", "页面", "前端", "按钮", "表单", "跳转"];
|
|
97
|
+
const PAGE_ENTRY_TERMS = ["http://", "https://", "localhost", "127.0.0.1", "page url", "页面 url", "dev server"];
|
|
98
|
+
const PAGE_BROWSER_CHECK_TERMS = ["browser check", "playwright", "screenshot", "click", "button", "form", "页面可加载", "浏览器"];
|
|
99
|
+
const CALLABLE_TASK_TERMS = [
|
|
100
|
+
"api",
|
|
101
|
+
"endpoint",
|
|
102
|
+
"cli",
|
|
103
|
+
"command",
|
|
104
|
+
"worker",
|
|
105
|
+
"route",
|
|
106
|
+
"server action",
|
|
107
|
+
"adapter",
|
|
108
|
+
"provider",
|
|
109
|
+
"rpa",
|
|
110
|
+
"bot",
|
|
111
|
+
"机器人",
|
|
112
|
+
"队列"
|
|
113
|
+
];
|
|
114
|
+
const CALLABLE_ENTRY_TERMS = ["command", "endpoint", "api", "cli", "worker", "route", "curl", "npm ", "npx ", "node ", "python", "make "];
|
|
115
|
+
const CALLABLE_RESULT_TERMS = ["pass", "response", "output", "result", "exit code", "queue", "log", "artifact", "created", "produced", "返回", "输出", "日志", "队列", "产物", "错误码"];
|
|
86
116
|
const validators = {
|
|
87
117
|
"validate-harness": validateHarness,
|
|
88
118
|
"validate-current": validateCurrent,
|
|
@@ -333,9 +363,10 @@ async function validateDevInternal(projectRoot, options) {
|
|
|
333
363
|
const pathErrors = options.phaseExit ? [] : await validateChangedPaths(projectRoot, plan.plan, true);
|
|
334
364
|
const draftErrors = await validateDevDraftConsumed(projectRoot, root);
|
|
335
365
|
const implementationDocErrors = await validateImplementationDocRunnableEntryExit(projectRoot);
|
|
366
|
+
const evidenceErrors = options.phaseExit ? [] : await validateCurrentTaskDevelopmentEvidence(projectRoot, plan.plan);
|
|
336
367
|
return {
|
|
337
368
|
info: [`validate-dev checked ${plan.taskCount} task(s)${options.phaseExit ? " for phase exit" : ""}`],
|
|
338
|
-
errors: [...phaseErrors, ...plan.errors, ...openTaskErrors, ...pathErrors, ...draftErrors, ...implementationDocErrors]
|
|
369
|
+
errors: [...phaseErrors, ...plan.errors, ...openTaskErrors, ...pathErrors, ...draftErrors, ...implementationDocErrors, ...evidenceErrors]
|
|
339
370
|
};
|
|
340
371
|
}
|
|
341
372
|
function validateDevOpenTaskState(plan) {
|
|
@@ -737,6 +768,113 @@ async function validateImplementationDocRunnableEntryExit(projectRoot) {
|
|
|
737
768
|
}
|
|
738
769
|
return errors;
|
|
739
770
|
}
|
|
771
|
+
async function validateCurrentTaskDevelopmentEvidence(projectRoot, plan) {
|
|
772
|
+
const currentTask = currentOpenSprintTask(plan);
|
|
773
|
+
if (!currentTask)
|
|
774
|
+
return [];
|
|
775
|
+
const taskId = String(currentTask.id ?? "");
|
|
776
|
+
const implementationDoc = String(currentTask.implementation_doc ?? "").trim();
|
|
777
|
+
if (!implementationDoc)
|
|
778
|
+
return [];
|
|
779
|
+
const docPath = path.join(projectRoot, implementationDoc);
|
|
780
|
+
if (!(await pathExists(docPath))) {
|
|
781
|
+
return [`${taskId} implementation_doc is missing: ${implementationDoc}`];
|
|
782
|
+
}
|
|
783
|
+
const text = await readText(docPath);
|
|
784
|
+
return validateDevelopmentEvidenceText(text, currentTask, implementationDoc);
|
|
785
|
+
}
|
|
786
|
+
function currentOpenSprintTask(plan) {
|
|
787
|
+
const currentTaskId = String(plan.current_task_id ?? "");
|
|
788
|
+
if (!currentTaskId)
|
|
789
|
+
return undefined;
|
|
790
|
+
const tasks = Array.isArray(plan.tasks) ? plan.tasks.filter(isRecord) : [];
|
|
791
|
+
return tasks.find((task) => String(task.id ?? "") === currentTaskId && OPEN_TASK_STATUSES.has(String(task.status)) && task.phase === "SPRINTING");
|
|
792
|
+
}
|
|
793
|
+
function validateDevelopmentEvidenceText(text, task, implementationDoc) {
|
|
794
|
+
const errors = [];
|
|
795
|
+
const taskId = String(task.id ?? "current task");
|
|
796
|
+
const section = markdownSection(text, DEVELOPMENT_EVIDENCE_TERMS);
|
|
797
|
+
if (!section) {
|
|
798
|
+
return [`${taskId} implementation_doc must include Development Evidence with Runnable Entry, Observable Exit, and Basic Self-test Evidence: ${implementationDoc}`];
|
|
799
|
+
}
|
|
800
|
+
if (hasJustifiedNotApplicableEvidence(section))
|
|
801
|
+
return [];
|
|
802
|
+
for (const field of ["Runnable Entry", "Observable Exit", "Basic Self-test Evidence"]) {
|
|
803
|
+
const value = evidenceFieldValue(section, field);
|
|
804
|
+
if (!value || isPlaceholderEvidence(value)) {
|
|
805
|
+
errors.push(`${taskId} Development Evidence ${field} must contain concrete, executed evidence in ${implementationDoc}`);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
const context = `${taskText(task)}\n${text}`.toLowerCase();
|
|
809
|
+
const loweredSection = section.toLowerCase();
|
|
810
|
+
if (containsAny(context, PAGE_TASK_TERMS)) {
|
|
811
|
+
if (!containsAny(loweredSection, PAGE_ENTRY_TERMS)) {
|
|
812
|
+
errors.push(`${taskId} page Development Evidence must include a dev server or page URL in ${implementationDoc}`);
|
|
813
|
+
}
|
|
814
|
+
if (!containsAny(loweredSection, PAGE_BROWSER_CHECK_TERMS)) {
|
|
815
|
+
errors.push(`${taskId} page Development Evidence must include a browser check, Playwright run, screenshot, or equivalent interaction evidence in ${implementationDoc}`);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
if (containsAny(context, CALLABLE_TASK_TERMS)) {
|
|
819
|
+
if (!containsAny(loweredSection, CALLABLE_ENTRY_TERMS)) {
|
|
820
|
+
errors.push(`${taskId} callable Development Evidence must include an API/CLI/worker command, endpoint, route, or invocation in ${implementationDoc}`);
|
|
821
|
+
}
|
|
822
|
+
if (!containsAny(loweredSection, CALLABLE_RESULT_TERMS)) {
|
|
823
|
+
errors.push(`${taskId} callable Development Evidence must include an observable response, output, side effect, log, artifact, or PASS/BLOCKED result in ${implementationDoc}`);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return errors;
|
|
827
|
+
}
|
|
828
|
+
function markdownSection(text, headerTerms) {
|
|
829
|
+
const lines = text.split(/\r?\n/);
|
|
830
|
+
let start = -1;
|
|
831
|
+
let level = 0;
|
|
832
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
833
|
+
const match = lines[index].match(/^(#{1,6})\s+(.+)$/);
|
|
834
|
+
if (!match)
|
|
835
|
+
continue;
|
|
836
|
+
const title = match[2].toLowerCase();
|
|
837
|
+
if (headerTerms.some((term) => title.includes(term.toLowerCase()))) {
|
|
838
|
+
start = index;
|
|
839
|
+
level = match[1].length;
|
|
840
|
+
break;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
if (start === -1)
|
|
844
|
+
return undefined;
|
|
845
|
+
let end = lines.length;
|
|
846
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
847
|
+
const match = lines[index].match(/^(#{1,6})\s+/);
|
|
848
|
+
if (match && match[1].length <= level) {
|
|
849
|
+
end = index;
|
|
850
|
+
break;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
return lines.slice(start, end).join("\n");
|
|
854
|
+
}
|
|
855
|
+
function hasJustifiedNotApplicableEvidence(section) {
|
|
856
|
+
for (const line of section.split(/\r?\n/)) {
|
|
857
|
+
const match = line.match(/^\s*[-*]\s*Not applicable\s*:[ \t]*(.+)$/i);
|
|
858
|
+
if (!match)
|
|
859
|
+
continue;
|
|
860
|
+
const value = match[1].trim();
|
|
861
|
+
if (value.length >= 24 && !isPlaceholderEvidence(value) && containsAny(value, ["because", "reason", "原因", "无应用入口", "no product runtime", "no runnable boundary"])) {
|
|
862
|
+
return true;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
return false;
|
|
866
|
+
}
|
|
867
|
+
function evidenceFieldValue(section, field) {
|
|
868
|
+
const escaped = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
869
|
+
const pattern = new RegExp(`^\\s*[-*]\\s*${escaped}\\s*:[ \\t]*(.+)$`, "im");
|
|
870
|
+
return section.match(pattern)?.[1]?.trim();
|
|
871
|
+
}
|
|
872
|
+
function isPlaceholderEvidence(value) {
|
|
873
|
+
const normalized = value.trim().toLowerCase();
|
|
874
|
+
if (!normalized || ["-", "n/a", "na", "none", "null", "不适用", "无"].includes(normalized))
|
|
875
|
+
return true;
|
|
876
|
+
return EVIDENCE_PLACEHOLDER_TERMS.some((term) => normalized === term || normalized.includes(term.toLowerCase()));
|
|
877
|
+
}
|
|
740
878
|
async function markdownFiles(root) {
|
|
741
879
|
const files = await listFiles(root);
|
|
742
880
|
return files.filter((file) => {
|