From 27f4f18d39b7a93d7599b522920f9699d6b02de7 Mon Sep 17 00:00:00 2001 From: Agent Date: Sat, 11 Jul 2026 20:40:36 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat(v1.3.0):=20=E5=AE=8C=E5=96=84=20onboar?= =?UTF-8?q?ding=20=E6=A0=B8=E5=BF=83=E6=B5=81=E7=A8=8B=E9=97=AD=E7=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 建立 Core Flow Inventory 与关键流程终态、恢复责任约束 - 增加 Flow Slice Coverage、Completeness Hard Gate 和条件图规则 - 更新 onboarding 模板、运行参考、示例与人类使用文档 - 新增专项 validator、RED/GREEN fixtures 和相关回归测试 - 固化单功能逻辑与压力验证报告并同步 v1.3.0 版本信息 --- AGENTS.md | 2 +- CHANGELOG.md | 18 +- README.md | 7 +- SKILL.md | 6 +- Usage.md | 11 +- ...e-flow-completeness-implementation-plan.md | 223 ++++++++ .../onboarding-core-flow-completeness.md | 495 ++++++++++++++++++ ...rding-core-flow-red-baseline-2026-07-11.md | 185 +++++++ ...pleteness-feature-validation-2026-07-11.md | 158 ++++++ .../onboarding-db/03-flows/order-payment.md | 154 ++++++ .../onboarding-db/08-review/evidence-graph.md | 22 + .../onboarding-db/batch-review.md | 17 + .../onboarding-db/coverage-matrix.md | 11 + .../onboarding-db/onboarding-spec.md | 19 + .../onboarding-db/onboarding-tasks.md | 10 + plugin.json | 2 +- references/design.md | 19 + references/onboarding-knowledge-base.md | 160 +++++- references/project-guidance.md | 2 +- references/runtime.md | 4 +- references/stage-guides.md | 16 +- references/submit-and-integrate.md | 4 +- references/validation-scenarios.md | 97 +++- references/workflow-checklists.md | 20 +- .../check-onboarding-core-flow-coverage.rb | 159 ++++++ templates/onboarding-db/README.md | 5 +- templates/onboarding-db/batch-review.md | 15 +- templates/onboarding-db/coverage-matrix.md | 32 +- templates/onboarding-db/evidence-graph.md | 13 + templates/onboarding-db/flow.md | 78 ++- templates/onboarding-db/module.md | 12 +- templates/onboarding-db/onboarding-spec.md | 49 +- templates/onboarding-db/onboarding-tasks.md | 9 +- templates/root-AGENTS.md | 26 +- .../invalid-detached-trace/batch-review.md | 5 + .../invalid-detached-trace/coverage-matrix.md | 5 + .../invalid-detached-trace/evidence-graph.md | 5 + .../invalid-detached-trace/flow.md | 9 + .../invalid-detached-trace/onboarding-spec.md | 5 + .../onboarding-tasks.md | 5 + .../invalid-missing-recovery/batch-review.md | 5 + .../coverage-matrix.md | 5 + .../evidence-graph.md | 7 + .../invalid-missing-recovery/flow.md | 14 + .../onboarding-spec.md | 5 + .../onboarding-tasks.md | 5 + .../valid-deferred/batch-review.md | 5 + .../valid-deferred/coverage-matrix.md | 5 + .../valid-deferred/evidence-graph.md | 5 + .../valid-deferred/onboarding-spec.md | 5 + .../valid-deferred/onboarding-tasks.md | 5 + .../validate-evidence-graph-ddd-onboarding.sh | 52 +- tests/validate-human-help-version-docs.sh | 2 +- ...idate-onboarding-core-flow-completeness.sh | 183 +++++++ tests/validate-project-local-skills.sh | 2 +- .../validate-requirement-lifecycle-backlog.sh | 11 +- tests/validate-root-agents-block-checker.sh | 12 +- tests/validate-root-agents-block-refresh.sh | 8 +- tests/validate-v1.2.4-root-stage-coverage.sh | 2 +- 59 files changed, 2265 insertions(+), 167 deletions(-) create mode 100644 docs/proposal/v1.3.x/onboarding-core-flow-completeness-implementation-plan.md create mode 100644 docs/proposal/v1.3.x/onboarding-core-flow-completeness.md create mode 100644 docs/reports/agent-loop-v1.3.0-onboarding-core-flow-red-baseline-2026-07-11.md create mode 100644 docs/reports/onboarding-core-flow-completeness-feature-validation-2026-07-11.md create mode 100644 examples/ai-meeting-minutes-backend/onboarding-db/03-flows/order-payment.md create mode 100644 examples/ai-meeting-minutes-backend/onboarding-db/08-review/evidence-graph.md create mode 100644 examples/ai-meeting-minutes-backend/onboarding-db/batch-review.md create mode 100644 examples/ai-meeting-minutes-backend/onboarding-db/coverage-matrix.md create mode 100644 examples/ai-meeting-minutes-backend/onboarding-db/onboarding-spec.md create mode 100644 examples/ai-meeting-minutes-backend/onboarding-db/onboarding-tasks.md create mode 100644 scripts/check-onboarding-core-flow-coverage.rb create mode 100644 tests/fixtures/onboarding-core-flow/invalid-detached-trace/batch-review.md create mode 100644 tests/fixtures/onboarding-core-flow/invalid-detached-trace/coverage-matrix.md create mode 100644 tests/fixtures/onboarding-core-flow/invalid-detached-trace/evidence-graph.md create mode 100644 tests/fixtures/onboarding-core-flow/invalid-detached-trace/flow.md create mode 100644 tests/fixtures/onboarding-core-flow/invalid-detached-trace/onboarding-spec.md create mode 100644 tests/fixtures/onboarding-core-flow/invalid-detached-trace/onboarding-tasks.md create mode 100644 tests/fixtures/onboarding-core-flow/invalid-missing-recovery/batch-review.md create mode 100644 tests/fixtures/onboarding-core-flow/invalid-missing-recovery/coverage-matrix.md create mode 100644 tests/fixtures/onboarding-core-flow/invalid-missing-recovery/evidence-graph.md create mode 100644 tests/fixtures/onboarding-core-flow/invalid-missing-recovery/flow.md create mode 100644 tests/fixtures/onboarding-core-flow/invalid-missing-recovery/onboarding-spec.md create mode 100644 tests/fixtures/onboarding-core-flow/invalid-missing-recovery/onboarding-tasks.md create mode 100644 tests/fixtures/onboarding-core-flow/valid-deferred/batch-review.md create mode 100644 tests/fixtures/onboarding-core-flow/valid-deferred/coverage-matrix.md create mode 100644 tests/fixtures/onboarding-core-flow/valid-deferred/evidence-graph.md create mode 100644 tests/fixtures/onboarding-core-flow/valid-deferred/onboarding-spec.md create mode 100644 tests/fixtures/onboarding-core-flow/valid-deferred/onboarding-tasks.md create mode 100644 tests/validate-onboarding-core-flow-completeness.sh diff --git a/AGENTS.md b/AGENTS.md index 527d315..c6e3466 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -103,7 +103,7 @@ feat, fix, docs, refactor, test, chore Rules: - Prefer Chinese for the summary and body unless the project context requires English. -- Include the current skill version scope, for example `docs(v1.2.4): 调整 Project Entry Scan 文档结构`. +- Include the current skill version scope, for example `docs(v1.3.0): 调整 Project Entry Scan 文档结构`. - Do not use one-line-only commit messages for meaningful behavior, gate, artifact, template, reference, validation, or example changes. - Use 3-7 bullet lines in the commit body, focused on concrete changes and user/agent-facing behavior. - Use `docs` for proposals, README, Usage, and explanatory docs. diff --git a/CHANGELOG.md b/CHANGELOG.md index 7da0630..1e53bd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,20 @@ # Agent Loop Changelog -## Unreleased +## 1.3.0 — 2026-07-11 + +### Version Baseline +- Started the 1.3.0 alpha development line from the refreshed `stable-v1.2.4` baseline. +- Updated version-bearing skill metadata and human-facing version labels to 1.3.0. +- Updated the root AGENTS managed-block revision to `block-version:1.3.0-20260711` for the first 1.3.0 template revision. +- Updated version-sync, root-guidance, and human-help regression expectations for the 1.3.0 development line. + +### Core Flow Completeness +- Added Core Flow Inventory and Flow Slice Coverage so critical/important onboarding flows close across callbacks, consumers, retries, compensation, reconciliation, jobs, and business terminal states. +- Made timeline/sequence the primary per-flow narrative, kept overview/boundary and ASCII state-machine views complementary, and triggered lineage, transaction, async, decision, runtime, and troubleshooting diagrams only when their semantics apply. +- Added a Completeness Hard Gate before quality scoring, aligned coverage and batch review dimensions, and prevented missing critical slices from being averaged into `newcomer-ready`. +- Kept exactly two onboarding Human Gates and removed the default pressure to invent state diagrams for stateless content topics. + +## 1.2.4 — 2026-07-11 ### Project-Local Skills - Added Project Skill Creation / Update for durable target-project capabilities under `.agent-loop/skills//`, with INDEX-based lifecycle, `bootstrap` / `on-demand` loading, and target-path ownership that never defaults to global skill directories. @@ -14,8 +28,6 @@ - Added a feature-scoped contract test and a Project-Local Skills scoring report without making full-repository tests part of the feature score. - Kept single-feature scoring in maintainer guidance only; it does not replace mandatory full validation for control-surface changes. -## 1.2.4 — 2026-07-11 - ### Version Baseline - Started the 1.2.4 development line from the current alpha branch so new behavior changes are recorded under 1.2.4 instead of the closed 1.2.3 section. - Updated version-bearing skill metadata, human-facing docs, and root AGENTS template metadata to use 1.2.4 as the active skill version. diff --git a/README.md b/README.md index de70432..2866c9c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Agent Loop -**Current version:** 1.2.4 +**Current version:** 1.3.0 A reusable [Codex](https://github.com/openai/codex) / CLI-agent skill for single-person software development workflows—from goal intake to verified close. @@ -183,13 +183,14 @@ Evidence-Graph + DDD Onboarding is the current durable project-understanding flo It builds `.agent-loop/onboarding-db/` from verified code evidence into macro-to-micro handoff docs: - `08-review/evidence-graph.md` before formal docs -- `onboarding-spec.md` for module/flow coverage, DDD mapping, file strategy, quality gates, and batch plan +- Core Flow Inventory for business terminals, variants, owners, recovery responsibility, evidence chain, and planned/deferred selection +- `onboarding-spec.md` for module/flow coverage, Flow Slice Plan, DDD mapping, file strategy, quality gates, and batch plan - `onboarding-tasks.md` for accepted batch execution - module playbooks under `02-modules/.md` by default - flow playbooks under `03-flows/.md` by default - coverage matrix and reviewed batch records -The agent must first confirm Project Entry Scan or reliable project memory, then build an Evidence Graph and present an Onboarding Spec for human review. Spec acceptance authorizes creation of Onboarding Tasks; formal onboarding docs begin only after the completed Tasks and their separate Full Execution Gate are accepted. Module and flow docs default to single long files, not many small files. Wireframe architecture flow diagrams are the preferred main way to express every module and flow process. The old Quick / Deep / Targeted onboarding modes and directory-first legacy generation flow are not used. +The agent must first confirm Project Entry Scan or reliable project memory, then build an Evidence Graph with Core Flow Inventory and present an Onboarding Spec for human review. Spec acceptance authorizes creation of Onboarding Tasks; formal onboarding docs begin only after the completed Tasks and their separate Full Execution Gate are accepted. Critical/important flows use Flow Slice Coverage and a completeness gate before quality scoring. Timeline/sequence is the primary per-flow narrative, supported by overview/boundary and state-machine views; extra diagrams are triggered by real recovery, data, transaction, async, decision, runtime, or troubleshooting complexity. Stateless topics do not invent state diagrams. Module and flow docs default to single long files, not many small files. The old Quick / Deep / Targeted onboarding modes and directory-first legacy generation flow are not used. Existing legacy onboarding-db files may be read as evidence, but they are not trusted without checking code reality. Migration or replacement requires an accepted Onboarding Spec, Onboarding Tasks, and Full Execution Gate. diff --git a/SKILL.md b/SKILL.md index 27a8517..9c8f2db 100644 --- a/SKILL.md +++ b/SKILL.md @@ -5,7 +5,7 @@ description: Use when starting, continuing, resuming, structuring, testing, impl # Agent Loop -Version: 1.2.4 +Version: 1.3.0 Run a single-human, CLI-agent development loop from goal intake to verified close. This skill is a controller: it decides the current stage, loads the right reference, produces or updates `agent-loop` artifacts, and stops at human gates. @@ -150,7 +150,7 @@ CHANGELOG.md version-change source of truth for "what cha 11. Load `references/e2e-discovery.md` before designing or executing Web E2E/browser verification. 12. Load `references/delivery-contracts.md` when the human requests cross-boundary handoff/API/interface documentation, or when the agent detects a likely downstream consumer boundary such as frontend/backend, service, event, public data, SDK/library, UI state, or runtime behavior. Delivery Contracts are not created by default. 13. Load `references/project-entry-scan.md` when taking over an existing project without reliable `agent-loop` memory. This is now a Project Entry Scan only: build safe project memory, guidance status, commands, boundaries, and uncertainties. Do not create `.agent-loop/onboarding-db/`, module docs, flow docs, onboarding diagrams, or old Quick / Deep / Targeted onboarding artifacts during Project Entry Scan. -13a. Load `references/onboarding-knowledge-base.md` when the human asks for newcomer-facing docs, durable project understanding, guided learning paths, or onboarding-db construction. Run it only after Project Entry Scan or reliable project memory. Use Evidence Graph first, then accepted Onboarding Spec, Onboarding Tasks, single-file module/flow docs by default, wireframe architecture flow diagrams as the preferred flow expression, coverage scoring, and reviewed batches. +13a. Load `references/onboarding-knowledge-base.md` when the human asks for newcomer-facing docs, durable project understanding, guided learning paths, or onboarding-db construction. Run it only after Project Entry Scan or reliable project memory. Use Evidence Graph and Core Flow Inventory first, then accepted Onboarding Spec, Onboarding Tasks, Flow Slice Coverage for critical/important flows, evidence-linked diagrams, completeness gating, coverage scoring, and reviewed batches; module/flow docs remain single-file by default. 13b. During Project Entry, Resume, Re-Adopt, context recovery, and controller re-entry, check `.agent-loop/skills/INDEX.md` when present. Load only `active` project skills whose current instruction-bearing and executable files match the validation manifest, according to `bootstrap` / `on-demand`; discovery and loading never satisfy the per-invocation Execution Gate. 14. Load `references/feature-follow-up.md` when the human reports a bug, regression, post-close correction, field/schema change, algorithm change, API mismatch, screenshot issue, behavior tweak, "small tweak", test failure, or QA/user feedback that may belong to a recent feature. 15. Load `references/large-projects.md` when the repo is large, old, unfamiliar, multi-package, or likely above 100k LOC. @@ -237,7 +237,7 @@ If the local directory is only a remote-project entry point, create only thin lo - Task Done Gate: mark a task `done` only after implementation is complete, required tests or substitute verification have run fresh, evidence is recorded in `notes.md`, lightweight Spec Review is recorded, Standards Review is recorded when triggered, drift decision is recorded, and `tasks.md` links or names the evidence. - During large Project Entry Scan, recommend bounded subagent scanning when available and human-confirmed; otherwise use single-agent layered scan. - During existing-project Project Entry Scan, create or propose only safe project memory, root guidance status, commands, boundaries, capabilities, and uncertainties. Do not create onboarding-db detail docs, module docs, flow docs, onboarding diagrams, onboarding-spec, or onboarding-tasks during Project Entry Scan. -- During Evidence-Graph + DDD Onboarding, create or update onboarding-db only through `references/onboarding-knowledge-base.md`: build Evidence Graph, confirm Onboarding Spec, write Onboarding Tasks, then produce reviewed batches with single-file module/flow docs by default, wireframe architecture flow diagrams, coverage scoring, and no placeholder files. +- During Evidence-Graph + DDD Onboarding, create or update onboarding-db only through `references/onboarding-knowledge-base.md`: build Evidence Graph with Core Flow Inventory, confirm Onboarding Spec, write Onboarding Tasks, then produce reviewed batches with Flow Slice Coverage for critical/important flows, evidence-linked diagrams, completeness gating, single-file module/flow docs by default, and no placeholder files. - When Project Entry Scan discovers stable project facts missing from project memory, propose or perform project memory backfill after human confirmation. - For focused questions about one module, flow, async task, deployment path, or problem area, answer from existing docs/code as chat or operational support unless the human explicitly authorizes feature/fix work. Do not create focused onboarding-db artifacts. - When local and remote project reality are split, discover the remote environment before Project Entry Scan or initializing project memory. diff --git a/Usage.md b/Usage.md index bdb7d54..ca22f8b 100644 --- a/Usage.md +++ b/Usage.md @@ -1,6 +1,6 @@ # Agent Loop 使用指南 -**版本:** 1.2.4 +**版本:** 1.3.0 这份文档是给人类看的。你不需要记住内部阶段名,只要用自然语言说出你想做什么,Agent 应该自己判断当前状态、推荐一个下一步,并在需要你确认的地方停下来。 @@ -45,13 +45,14 @@ Project Entry Scan 不算完成,除非 root `AGENTS.md` 已存在、已创建 | “重点讲清楚支付/钱包/任务调度这块。” | 先从现有代码和文档回答;如果要沉淀长期文档,再走聚焦的 onboarding-db 更新。 | | “这个旧 onboarding-db 还能信吗?” | 把旧文档当 evidence,先和代码现实核对;不直接按旧布局刷新。 | -当前 1.2.4 使用的是 **Evidence-Graph + DDD Onboarding**,不是旧 Quick / Deep / Targeted 模式。 +当前 1.3.0 使用的是 **Evidence-Graph + DDD Onboarding**,不是旧 Quick / Deep / Targeted 模式。 推荐流程: ```text 可靠项目记忆 -> 08-review/evidence-graph.md +-> Core Flow Inventory(核心流程、业务终态、恢复责任和证据链) -> onboarding-spec.md(第一次确认) -> onboarding-tasks.md / Full Execution Gate(第二次确认) -> 02-modules/.md @@ -73,6 +74,10 @@ Agent 不应该用空目录、薄 README、planned/later 占位文件、`TBD`、 - 怎么验证、怎么排查、怎么安全修改 - 架构/边界图、ASCII 状态图、Timeline / 时序图 +核心流程完整性先于文档评分:`critical` / `important` 流程必须从触发闭合到业务成功、失败、取消、未知或人工处理终态;callback、consumer、retry、DLQ、compensation、reconciliation 和 job 不能因为被拆成其他 topic 就从流程中消失。每个关键 slice 都要连接代码证据、图和正文,缺失时不能标记 `newcomer-ready`。 + +Timeline / Sequence 是单个核心流程的主叙事;Core Flow Overview / Boundary 讲 scope、owner、branch 和 terminal;ASCII State Machine 讲状态、非法转换和恢复。数据血缘、事务并发、异步拓扑、决策树、runtime 和排障图由真实复杂度触发。非流程的 stateless 文档不强制状态图。 + 普通流程图和时序图可以优先使用 Mermaid flowchart / sequenceDiagram;状态机、复杂原理图和复杂示例图优先使用 ASCII。 ### 我只是想问问题 @@ -90,7 +95,7 @@ Agent 不应该用空目录、薄 README、planned/later 占位文件、`TBD`、 | 你可以这样说 | Agent 应该怎么做 | |---|---| -| “1.2.4 更新了什么?” | 读取 `CHANGELOG.md` 的 1.2.4 段落,按能力分类总结,不凭记忆回答。 | +| “1.3.0 更新了什么?” | 读取 `CHANGELOG.md` 的 1.3.0 段落,按能力分类总结,不凭记忆回答。 | | “和 1.2.2 比有什么变化?” | 对比 `CHANGELOG.md` 里的两个版本段落,说明新增、删除、替换和迁移影响。 | | “现在 agent-loop 怎么用?” | 基于 `Usage.md` 用人类语言介绍常见触发方式。 | | “这个功能怎么触发?” | 从 `Usage.md` 找对应说法,再说明 Agent 会进入哪个处理流。 | diff --git a/docs/proposal/v1.3.x/onboarding-core-flow-completeness-implementation-plan.md b/docs/proposal/v1.3.x/onboarding-core-flow-completeness-implementation-plan.md new file mode 100644 index 0000000..22e5f86 --- /dev/null +++ b/docs/proposal/v1.3.x/onboarding-core-flow-completeness-implementation-plan.md @@ -0,0 +1,223 @@ +# Onboarding Core Flow Completeness Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILLS: use `writing-skills` for documentation RED/GREEN/REFACTOR, `test-driven-development` for executable validators, `verification-before-completion` before completion claims, and `requesting-code-review` before handoff. Agent Loop controls paths, Human Gates, and submit authorization. + +**Goal:** Make Evidence-Graph + DDD Onboarding prove that critical project flows, branches, terminal states, recovery paths, diagrams, narrative, and code evidence are traceably complete before `newcomer-ready`. + +**Architecture:** Extend the existing onboarding pipeline without changing its stage order or adding Human Gates. Stable Flow IDs and Slice IDs connect Evidence Graph, Onboarding Spec, Tasks, Flow docs, Coverage, and Batch Review; an executable validator checks that trace chain, while semantic pressure scenarios check that generic or incomplete docs are rejected. + +**Tech Stack:** Markdown skill sources and templates, Bash contract tests, Ruby artifact validator, repository examples, existing Agent Loop full-validation method. + +--- + +## Execution Boundary + +- Implement inside the Agent Loop skill source repository, not a target project's `.agent-loop/`. +- Preserve the existing uncommitted 1.3.0 version alignment. +- Do not create a new Human Gate; retain only Onboarding Spec Acceptance and Onboarding Tasks Full Execution Gate. +- Do not commit, push, tag, publish, or release without a separate human submit confirmation. +- Do not run repository-wide/full-skill validation without explicit human permission; default to onboarding feature-scoped validation. +- Treat `docs/proposal/` as design/history only; published behavior must be carried by `SKILL.md`, `references/`, and `templates/`. + +## Task 1: Establish RED contracts + +**Files:** + +- Create: `tests/validate-onboarding-core-flow-completeness.sh` +- Create: `tests/fixtures/onboarding-core-flow/invalid-missing-recovery/` +- Reference: `docs/proposal/v1.3.x/onboarding-core-flow-completeness.md` + +- [ ] Add contract assertions requiring `Core Flow Inventory`, stable `Flow ID`, `Slice ID`, terminal outcomes, selection/deferred reason, evidence chain, diagram-to-slice mapping, and `Completeness Hard Gate` in the owning runtime/reference/templates. +- [ ] Add a validator invocation that expects the valid reference under `examples/ai-meeting-minutes-backend/onboarding-db/` to pass and the invalid fixture to fail with `missing required slice: CF-ORDER-PAYMENT/S07`. +- [ ] Run `bash tests/validate-onboarding-core-flow-completeness.sh` before production-document edits. +- [ ] Record the exact failing assertion and confirm the failure is caused by the missing completeness capability. + +Expected RED shape: + +```text +missing text in references/onboarding-knowledge-base.md: Core Flow Inventory +``` + +## Task 2: Update controller and runtime invariants + +**Files:** + +- Modify: `SKILL.md` +- Modify: `references/design.md` +- Modify: `references/runtime.md` + +- [ ] Replace the controller's `wireframe architecture flow diagrams as the preferred flow expression` summary with a concise core-flow completeness summary. +- [ ] Add the design invariant: + +```text +Core Flow Inventory +→ accepted Flow selection +→ Flow Slice Coverage +→ Diagram + Narrative + Evidence trace +→ Completeness Hard Gate +→ Quality Score +``` + +- [ ] State that a missing critical slice cannot be averaged away by topic quality scores. +- [ ] Preserve the canonical onboarding order: + +```text +Evidence Graph -> accept Onboarding Spec -> write Onboarding Tasks -> accept Full Execution Gate -> formal docs +``` + +- [ ] Preserve exactly two onboarding Human Gates. +- [ ] Run the new focused test and confirm the first controller/runtime assertions turn GREEN while template assertions remain RED. + +## Task 3: Implement detailed onboarding behavior + +**Files:** + +- Modify: `references/onboarding-knowledge-base.md` +- Modify: `references/stage-guides.md` +- Modify: `references/workflow-checklists.md` +- Modify: `references/validation-scenarios.md` + +- [ ] Define `Core Flow Inventory` fields: Flow ID, business outcome, criticality, entry, success/failure terminals, variants, owners, state/data owners, async/jobs/callbacks, side effects, recovery responsibility, evidence chain, selection, selection reason, confidence, unknowns. +- [ ] Define discovery triangulation across entries, state writes, transactions, messages, callbacks/jobs, tests/contracts/logs/config, and human business outcomes verified against code. +- [ ] Define the Spec Acceptance checks for every `critical` / `important` flow to be planned or explicitly deferred with evidence and impact. +- [ ] Define `Flow Slice Coverage` and key-slice classification for `critical` / `important` flows; keep supporting flows lightweight unless they own core state, side effects, or recovery. +- [ ] Define the diagram contract: + +```text +Core Flow Overview / Boundary = scope, branches, owners, terminals +Timeline / Sequence = primary per-flow narrative +ASCII State Machine = states, invalid transitions, recovery +Conditional diagrams = lineage, transaction/concurrency, async topology, + decision tree, ERD, runtime, troubleshooting +``` + +- [ ] Require stable Diagram IDs and Slice ID mapping in every diagram explanation. +- [ ] Scope the fixed diagram group to core module/flow behavior; make overview/domain/jobs/infra/deploy/change-guide diagrams relevance-based so stateless topics do not invent state diagrams. +- [ ] Align stage guide and checklist wording; remove any implication that the current batch is a Human Gate. +- [ ] Add adversarial validation scenarios for missing callback/reconciliation, diagram-narrative detachment, averaged-away critical failure, simple CRUD exemptions, wallet/billing risk, gateway/runtime, and deferred critical flows. +- [ ] Run the focused test and confirm reference assertions pass while remaining template/fixture assertions stay RED. + +## Task 4: Align all onboarding templates + +**Files:** + +- Modify: `templates/onboarding-db/README.md` +- Modify: `templates/onboarding-db/evidence-graph.md` +- Modify: `templates/onboarding-db/onboarding-spec.md` +- Modify: `templates/onboarding-db/onboarding-tasks.md` +- Modify: `templates/onboarding-db/flow.md` +- Modify: `templates/onboarding-db/coverage-matrix.md` +- Modify: `templates/onboarding-db/batch-review.md` + +- [ ] Add the complete Core Flow Inventory and Evidence Readiness checks. +- [ ] Add Core Flow Selection and Flow Slice Plan to Onboarding Spec. +- [ ] Extend Diagram Plan with complexity signals and Covered Slice IDs. +- [ ] Make Onboarding Tasks list Flow IDs, Slice IDs, required and conditional diagrams, evidence, hard gate, and score target. +- [ ] Add flow identity, terminal outcomes, slice trace table, Diagram IDs, and diagram explanations to `flow.md`. +- [ ] Add `Completeness Hard Gate` before quality score in coverage and batch review. +- [ ] Use the same quality dimensions and 1-5 anchors in coverage and batch review. +- [ ] Keep module/flow single-file defaults and exactly two Human Gates. +- [ ] Run the focused test and confirm all source/template assertions pass; fixture/validator assertions remain RED. + +## Task 5: Implement artifact validation and fixtures + +**Files:** + +- Create: `scripts/check-onboarding-core-flow-coverage.rb` +- Create: `examples/ai-meeting-minutes-backend/onboarding-db/08-review/evidence-graph.md` +- Create: `examples/ai-meeting-minutes-backend/onboarding-db/onboarding-spec.md` +- Create: `examples/ai-meeting-minutes-backend/onboarding-db/onboarding-tasks.md` +- Create: `examples/ai-meeting-minutes-backend/onboarding-db/03-flows/order-payment.md` +- Create: `examples/ai-meeting-minutes-backend/onboarding-db/coverage-matrix.md` +- Create: `examples/ai-meeting-minutes-backend/onboarding-db/batch-review.md` +- Create: `tests/fixtures/onboarding-core-flow/invalid-missing-recovery/evidence-graph.md` +- Create: `tests/fixtures/onboarding-core-flow/invalid-missing-recovery/onboarding-spec.md` +- Create: `tests/fixtures/onboarding-core-flow/invalid-missing-recovery/onboarding-tasks.md` +- Create: `tests/fixtures/onboarding-core-flow/invalid-missing-recovery/flow.md` +- Create: `tests/fixtures/onboarding-core-flow/invalid-missing-recovery/coverage-matrix.md` +- Create: `tests/fixtures/onboarding-core-flow/invalid-missing-recovery/batch-review.md` + +- [ ] Implement a Ruby validator with this CLI: + +```bash +ruby scripts/check-onboarding-core-flow-coverage.rb examples/ai-meeting-minutes-backend/onboarding-db +``` + +- [ ] Parse the fixture's declared critical/important Flow IDs and required Slice IDs. +- [ ] Verify every planned flow is connected across Evidence Graph, Spec, Tasks, Flow doc, Coverage, and Review. +- [ ] Verify every required Slice ID has evidence, Diagram ID, and document-section tokens. +- [ ] Verify Completeness Hard Gate is `PASS` only when required slices are covered. +- [ ] Emit deterministic errors, including: + +```text +missing required slice: CF-ORDER-PAYMENT/S07 +``` + +- [ ] Run the focused test; valid fixture must pass and invalid fixture must fail for the expected reason. + +## Task 6: Human-facing documentation and changelog + +**Files:** + +- Modify: `Usage.md` +- Modify: `CHANGELOG.md` +- Review: `README.md` +- Review: `templates/root-AGENTS.md` + +- [ ] Explain to humans that onboarding first finds complete core flows, then writes diagram-backed flow docs. +- [ ] Explain the default diagram group and complexity-triggered diagrams without exposing unnecessary internal schema detail. +- [ ] Record the v1.3.0 behavior under the existing 1.3.0 changelog section. +- [ ] Update README only if its onboarding summary would otherwise contradict the new behavior. +- [ ] Do not change root Stage Map signals or add a new stage; update the managed block only if its existing onboarding description becomes contradictory. + +## Task 7: REFACTOR and feature-scoped verification + +**Files:** + +- Create: `docs/reports/onboarding-core-flow-completeness-feature-validation-2026-07-11.md` +- Review: all files changed by Tasks 1-6 + +- [ ] Run the focused test: + +```bash +bash tests/validate-onboarding-core-flow-completeness.sh +``` + +Expected: `PASS: onboarding core-flow completeness contract is complete`. + +- [ ] Run the existing onboarding regression: + +```bash +bash tests/validate-evidence-graph-ddd-onboarding.sh +``` + +Expected: `evidence-graph DDD onboarding validation passed`. + +- [ ] Run direct onboarding regressions only: + +```bash +bash tests/validate-onboarding-core-flow-completeness.sh +bash tests/validate-evidence-graph-ddd-onboarding.sh +bash tests/validate-project-entry-onboarding-reset.sh +bash tests/validate-v1.2.4-state-lifecycle-repairs.sh +bash tests/validate-v1.2.4-postfix-pressure-repairs.sh +bash tests/validate-v1.2.3-routing-fixes.sh +bash tests/validate-v1.2.3-medium-consistency.sh +``` + +Expected: all seven feature-boundary tests exit zero. + +- [ ] Run required structural checks: + +```bash +ruby -e 'require "yaml"; YAML.load_file("SKILL.md")' +ruby -rjson -e 'JSON.parse(File.read("plugin.json"))' +find . -name '*.sh' -type f -print0 | xargs -0 -n1 bash -n +git diff --check +``` + +- [ ] Run a Markdown fence-balance check over changed Markdown files. +- [ ] Execute the five-domain semantic audit required by `docs/maintenance/feature-validation-method.md`, separating RED baseline from GREEN results. +- [ ] Record repository-wide/full-skill validation as not authorized and not part of the feature score; run it only after explicit human permission. +- [ ] Review the final diff for unrelated edits and preserve the pre-existing 1.3.0 version-alignment changes. +- [ ] Stop before commit and request the Agent Loop Submit / Integrate Human Gate. diff --git a/docs/proposal/v1.3.x/onboarding-core-flow-completeness.md b/docs/proposal/v1.3.x/onboarding-core-flow-completeness.md new file mode 100644 index 0000000..1080633 --- /dev/null +++ b/docs/proposal/v1.3.x/onboarding-core-flow-completeness.md @@ -0,0 +1,495 @@ +# Proposal: Onboarding Core Flow Completeness + +状态:待人类评审 + +目标版本:v1.3.0 + +创建时间:2026-07-11 + +## 摘要 + +当前 Evidence-Graph + DDD Onboarding 已经对单篇 Module / Flow 文档提出较强的内容要求,包括架构/边界图、状态图、Timeline / 时序图、数据对象、状态变化、失败路径、排障和代码证据。 + +现有缺口位于更上游:Agent 可以把“已经选中的流程”写得很详细,却没有足够强的机制证明项目核心流程已经找全、关键阶段和异常分支已经拆全。若 Evidence Graph 只发现“创建订单”,但漏掉支付回调、重复回调、超时关闭、退款、补偿和对账,后续文档即使结构完整,也会在错误的范围内显得详细。 + +本 proposal 建议在现有 onboarding 顺序和两个 Human Gate 不变的前提下,引入三项能力: + +1. `Core Flow Inventory`:在 Evidence Graph 阶段建立带业务关键度、终态、变体、异步链路和恢复责任的核心流程清单。 +2. `Flow Slice Coverage`:把每个核心流程拆成可追踪的主路径、分支、失败和恢复 slice,并映射到代码证据、图和文档章节。 +3. 产物级语义验证:使用完整示例、缺陷样本和压力场景验证“有没有漏”,不再只验证模板中是否存在标题和关键词。 + +预期结果: + +```text +发现完整的核心流程 +→ 选择并解释文档范围 +→ 将流程拆成可追踪 slice +→ 用互补图组表达结构、时间、状态和恢复 +→ 按 slice 验证覆盖 +→ 才允许标记 newcomer-ready +``` + +## 背景与问题定义 + +### 当前已经做得好的部分 + +现有 Flow 文档要求已经覆盖: + +- 业务目标、触发入口、参与模块、前置状态和成功结果; +- 架构/边界图; +- ASCII 状态机/决策图; +- Timeline / 时序图; +- 阶段、数据流转和状态变化; +- 示例、失败路径、排障路径、变更指南和代码证据; +- consistency、idempotency、transaction、compensation、reconciliation 等高风险内容。 + +因此,本轮不需要推翻 Flow 模板,也不需要以增加文件数量来解决问题。 + +### 当前结构性缺口 + +#### 1. 核心流程发现没有完整性契约 + +当前 `Flow Candidates` 可以记录 trigger、participants、state changes、external dependencies、evidence 和 risk,但不能稳定回答: + +- 这是核心流程、支持流程,还是局部调用链; +- 业务成功终态和失败终态分别是什么; +- 是否存在同步、异步、回调、job 或人工恢复变体; +- 哪些外部副作用必须发生,哪些允许最终一致; +- 为什么该流程进入 Flow Plan,或为什么可以延期; +- 入口、状态写点、消息消费者和终态是否形成闭合证据链。 + +#### 2. 上游漏项会在后续阶段被继承 + +Onboarding Spec 和 Onboarding Tasks 只会规划已经进入候选清单的流程。如果 Evidence Graph 漏掉回调、补偿或对账,后续的 Diagram Plan、Flow 文档和 Coverage Matrix 都无法自动恢复该细节。 + +#### 3. Topic 级评分会掩盖 slice 级缺失 + +当前评分以 topic 为单位。某篇 Flow 文档可以在图、可读性和代码证据上得到较高平均分,同时漏掉一个业务关键失败分支。关键分支缺失不应被其他维度的高分抵消。 + +#### 4. 机械测试不能证明生成产物完整 + +现有 onboarding 测试主要检查 source、template 和 scenario 中是否存在指定文字或章节。它能够防止规则被误删,但不能证明实际生成的 onboarding: + +- 找全了核心流程; +- 覆盖了所有关键终态; +- 图和正文对应真实代码路径; +- 没有用泛化描述填满必填章节。 + +#### 5. Review 维度存在漂移 + +`coverage-matrix.md` 分开评分 architecture、state、timeline 等维度,而 `batch-review.md` 使用较粗的 `Wireframe` 列。两套评分口径不一致,会让 review 丢失图解和流程 slice 的具体缺口。 + +## 目标 + +本 proposal 的目标是: + +1. 让 Agent 在写正式文档前证明核心流程发现足够完整。 +2. 让每个核心流程的主路径、分支、失败和恢复都能追踪到代码证据。 +3. 让图解承担明确职责,避免一张泛化 flowchart 同时冒充架构、时序和状态说明。 +4. 让关键 slice 缺失成为 hard blocker,不能被平均分掩盖。 +5. 让验证覆盖真实产物和故意不完整的反例。 +6. 保留现有 Onboarding Spec Acceptance Gate 和 Full Execution Gate,不增加第三个人类 Gate。 +7. 保持 Module / Flow 默认单长文件,不通过拆文件制造详细感。 +8. 只对 `critical` / `important` 核心流程强制 Flow/Slice/Diagram 追踪;supporting flow 和非流程文档保持轻量。 + +## 非目标 + +本轮不做以下事情: + +- 不改变 Project Entry Scan 与 onboarding-db 的边界; +- 不改变 `Evidence Graph -> Onboarding Spec -> Onboarding Tasks -> formal docs` 的阶段顺序; +- 不增加每个 batch 的人类确认; +- 不要求所有流程使用所有图类型; +- 不要求 glossary、静态配置清单、纯索引或无状态主题编造状态图; +- 不要求固定数量的核心流程或固定数量的图; +- 不从目录名自动推断业务流程; +- 不要求 Agent 生成网站或图片文件;Markdown 中的 Mermaid 和 ASCII 仍是 source of truth; +- 不在本轮建设通用 LLM 自动评审平台;该能力可与未来 self-test harness 对接; +- 不把历史 proposal 或示例升级为运行时 source of truth。 + +## 方案比较 + +### 方案 A:只增强 Flow 模板 + +做法:在 `flow.md` 增加更多章节、表格和自检项。 + +优点:改动小,容易实施。 + +限制:无法解决 Evidence Graph 上游漏项;Agent 仍可能把一个不完整范围写得很详细。 + +### 方案 B:核心流程契约 + Slice Traceability + +做法:增强 Evidence Graph、Onboarding Spec、Flow 文档、Coverage 和 Review 的贯通关系;以稳定 Flow ID 和 Slice ID 建立追踪链。 + +优点:直接处理核心流程发现不完整和关键分支丢失;与现有 Evidence-Graph + DDD 模型兼容;不需要新增 Human Gate。 + +代价:需要同步多份 reference、template、scenario 和 test,并执行全量验证。 + +### 方案 C:自动生成静态调用图 + +做法:扫描 AST、调用关系、数据库访问和消息消费者,自动生成候选流程图。 + +优点:能提高代码入口和调用链发现效率。 + +限制:静态调用图通常不知道业务目标、终态、补偿责任和人工恢复语义;多语言项目还需要不同解析器。它适合作为 Evidence Graph 的辅助证据,不适合作为第一版完整性判定源。 + +### 推荐 + +采用方案 B。方案 A 可作为方案 B 中的模板改动部分;方案 C 延后为可选 Evidence Adapter,不阻塞 v1.3.0。 + +## 设计 + +### 1. Core Flow Inventory + +Evidence Graph 在普通 `Flow Candidates` 之上增加核心流程分类和选择信息。推荐表结构: + +| 字段 | 含义 | +|---|---| +| Flow ID | 稳定标识,例如 `CF-ORDER-PAYMENT` | +| Flow | 人类可读流程名 | +| Business Outcome | 流程为用户或业务产生的结果 | +| Criticality | `critical`、`important`、`supporting` | +| Trigger / Entry | 外部触发与具体代码入口 | +| Success Terminal | 成功终态和可观察结果 | +| Failure Terminals | 失败、取消、未知、人工处理等终态 | +| Variants / Branches | 同步、异步、回调、降级和业务分支 | +| Participants / Owners | 模块、服务、外部系统和真相所有者 | +| State / Data Owners | 核心状态对象和数据 owner | +| Async / Jobs / Callbacks | Topic、consumer、job、callback 关系 | +| External Side Effects | 扣款、通知、发货、配额、第三方调用等 | +| Recovery Responsibility | retry、compensation、reconciliation、manual action | +| Evidence Chain | 入口、关键写点、消息处理和终态证据 | +| Selection | `planned`、`deferred`、`not-applicable` | +| Selection Reason | 纳入或延期原因 | +| Confidence / Unknowns | 置信度和仍需确认的事实 | + +#### 发现方法 + +Agent 不应只从目录和入口函数生成 Flow Inventory。至少从以下证据方向交叉发现: + +1. API、CLI、UI action、webhook、consumer、cron/job 等入口; +2. 核心实体、订单、任务或会话的状态写入点; +3. DB transaction、Redis/Lua、outbox、MQ publish/consume 等副作用; +4. callback、retry、DLQ、compensation、reconciliation 和人工恢复入口; +5. tests、fixtures、API contracts、logs、config 和 runbook 中的真实行为; +6. 项目记忆或人类说明中的业务结果,并用代码现实复核。 + +#### Core Flow Selection Gate + +不新增独立 Human Gate。现有 Spec Acceptance Gate 增加以下检查: + +- 每个 `critical` / `important` Flow Candidate 都已进入 Flow Plan,或有具体 deferred reason; +- 每个计划内核心流程都列出成功终态、失败终态和已知变体; +- 入口、关键状态写点、异步处理和终态已形成 Evidence Chain; +- 无证据的推断保留在 Unknowns,不伪装成已确认流程。 + +人类确认 Onboarding Spec 时,同时确认这份核心流程选择结果。Full Execution Gate 仍只负责确认具体输出、证据要求和执行范围。 + +### 2. Flow Slice Coverage + +每个计划内 `critical` / `important` 核心流程在 Onboarding Spec 或 Flow 文档中建立 slice 清单。Slice 是一个可验证的主路径阶段、业务分支、失败点或恢复动作,不是文档章节数量。`supporting` flow 只有在改变核心状态、产生关键副作用或承担恢复责任时才升级为完整 slice 追踪;否则可保留轻量 Flow Candidate 和证据说明。 + +推荐结构: + +| 字段 | 含义 | +|---|---| +| Flow ID / Slice ID | 例如 `CF-ORDER-PAYMENT/S07` | +| Path Kind | `main`、`branch`、`failure`、`recovery` | +| Trigger / Precondition | 进入该 slice 的条件 | +| Owner | 执行和状态责任模块 | +| Input / Output | 输入和输出对象 | +| Action | 真实行为 | +| State Read / Written | 读取和写入的状态 | +| Transition | before、trigger、after | +| Sync / Async / External | 调用性质和外部副作用 | +| Failure Result | 本 slice 失败后的可观察结果 | +| Recovery | retry、rollback、compensation、reconciliation 或 manual action | +| Evidence | 文件、符号/配置键和调用/数据方向 | +| Diagram IDs | 该 slice 出现在哪些图中 | +| Document Section | 该 slice 在正文中的讲解位置 | +| Coverage Status | `covered`、`inferred`、`blocked` | + +追踪链必须闭合: + +```text +Core Flow Inventory + → Flow Plan + → Onboarding Task + → Flow Slice Coverage + → Diagram IDs + narrative sections + → Code Evidence + → Coverage Matrix + Batch Review +``` + +关键 slice 定义: + +- 改变核心业务状态; +- 产生不可忽略的外部副作用; +- 决定成功、失败、取消、未知或人工处理终态; +- 承担 retry、compensation、reconciliation 或安全责任; +- 涉及 transaction、lock、idempotency、quota、billing、credential 或权限边界。 + +任何关键 slice 为 `blocked` 或缺少 Evidence / Diagram / Document Section 映射时,对应 Flow 不能标记 `newcomer-ready`。 + +### 3. 图解表达规则 + +#### Critical / Important 核心 Flow 的默认图组 + +| 图 | 默认性 | 主要回答的问题 | +|---|---|---| +| Core Flow Overview / Branch Map | 必填 | 核心流程有哪些主干、分支、异步出口和终态 | +| Timeline / Sequence Diagram | 必填,正文主图 | 谁在什么时间调用谁、传什么数据、读写什么状态 | +| ASCII State Machine / Decision Diagram | 必填 | 状态如何变化、哪些转换非法、失败后如何恢复 | +| Architecture / Boundary Diagram | 必填,可与全景图合并但职责必须清楚 | 流程跨越哪些模块、服务、数据和外部系统边界 | + +架构/边界图可以与 Core Flow Overview 合并,但合并图必须同时显示: + +- 模块/系统边界; +- 数据或状态 owner; +- 主路径、关键分支和终态; +- DB、cache、MQ、gateway 和外部依赖的位置。 + +Timeline / Sequence Diagram 是单个核心流程的主要讲解图。普通 `A -> B -> C` flowchart 只可作为导航图,不能独立满足流程细节要求。 + +#### 按复杂度增加的图 + +| 复杂度信号 | 增加的图 | 需要表达的细节 | +|---|---|---| +| callback、retry、补偿、对账、最终一致性 | Failure Recovery Timeline | 时间窗口、观察点、重试和恢复终点 | +| DTO、Command、Entity、DB Record、Event 多次转换 | Data Lineage / Object Transformation | 对象 owner、字段变化、ID 和状态来源 | +| transaction、lock、Lua、outbox、并发、幂等 | Transaction / Concurrency Boundary | 事务内外、锁生命周期、幂等检查、部分成功 | +| 多 Topic、consumer、retry queue、DLQ | Async Message Topology | producer、consumer、delivery、retry、DLQ 和 owner | +| 路由、权限、provider 或策略选择复杂 | Decision Tree | 条件、优先级、fallback 和拒绝结果 | +| 实体关系影响理解 | ERD / Model Relationship | 关系、聚合边界和真相来源 | +| gateway、sidecar、环境拓扑影响行为 | Runtime / Deployment Topology | route、header、timeout、retry、upstream、logs | +| 排障入口分散 | Observability / Troubleshooting Map | request ID、logs、metrics、traces、DB/MQ 检查顺序 | + +Diagram Plan 不再只列图类型,还要记录触发该图的复杂度信号和覆盖的 Slice IDs。 + +非 Flow 内容文档按解释需要选择图,不继承核心 Flow 的固定图组: + +- system context / architecture / domain docs 使用能说明边界、关系和数据所有权的图; +- jobs / async / runtime docs 在存在时间、状态或恢复语义时使用 sequence/state/timeline; +- glossary、静态配置清单、纯索引和没有状态语义的说明不强制状态图; +- 任何豁免都不允许掩盖实际存在的状态、分支或恢复行为。 + +#### 图文绑定 + +每张图必须具有稳定 Diagram ID,并附带: + +- 图要看什么; +- 图支持什么结论; +- 覆盖哪些 Slice IDs; +- 出现哪些数据对象、状态字段、消息和配置; +- 对应哪些代码证据; +- 哪些内容属于 inferred,以及置信度和待验证点。 + +禁止用同一张泛化图复制到多个章节并声称完成不同职责。 + +### 4. Coverage 与 Review + +#### 两级判定 + +`newcomer-ready` 使用两级判定: + +1. **Completeness Hard Gate**:核心流程和关键 slice 没有未解释的缺失。 +2. **Quality Score**:通过 hard gate 后,再评价图解、证据、排障和可读性质量。 + +Hard Gate 失败时,无论平均分多高,都不能标记 `newcomer-ready`。 + +#### 统一评分维度 + +`coverage-matrix.md` 与 `batch-review.md` 使用同一组维度: + +- Core flow discovery completeness; +- Slice and branch coverage; +- Architecture / boundary clarity; +- Timeline / sequence clarity; +- State machine clarity; +- Data object / lineage completeness; +- Failure / recovery completeness; +- Evidence granularity and traceability; +- Troubleshooting / observability; +- Change guidance; +- Newcomer readability。 + +评分继续使用 1-5,但增加锚点: + +| 分数 | 锚点 | +|---:|---| +| 5 | 完整、证据闭合,新人可独立解释和排障 | +| 4 | 核心路径完整,只有不影响接手的低风险缺口 | +| 3 | 可以理解主路径,但分支、恢复或证据存在明显缺口 | +| 2 | 结构存在,内容主要是概览或泛化描述 | +| 1 | 无法依靠该文档理解或操作 | + +### 5. 任务与 Human Gate + +Onboarding Tasks 对每个核心 Flow task 必须列出: + +- Flow ID 和计划路径; +- 必须覆盖的 Slice IDs; +- 默认图组; +- 条件触发图; +- evidence required; +- Completeness Hard Gate; +- quality score target。 + +Human Gate 保持两个: + +```text +Gate 1: 接受 Onboarding Spec + - 包含 Core Flow Inventory 选择结果 + - 只授权创建 Onboarding Tasks + +Gate 2: 接受 Onboarding Tasks Full Execution Gate + - 包含具体 Flow/Slice/Diagram/Evidence 范围 + - 授权连续创建正式 onboarding 文档 +``` + +本 proposal 不引入 Core Flow Gate、Diagram Gate 或 Batch Gate 等额外人工暂停点。 + +### 6. 验证设计 + +#### RED 基线 + +实现前保留以下当前漏洞证据: + +1. 现有 onboarding source validation 可以在没有完整 onboarding 示例产物时通过; +2. Flow Candidate 没有终态、变体、恢复责任和选择理由字段; +3. topic 评分没有关键 slice hard blocker; +4. batch review 与 coverage matrix 的图解评分维度不一致。 + +#### 新增压力场景 + +至少覆盖: + +1. **只写成功路径**:缺少 callback failure 和 reconciliation,必须拒绝 newcomer-ready。 +2. **有三张图但图文脱节**:没有 Slice ID、数据对象或代码证据映射,必须判定 incomplete。 +3. **漏异步消费者**:API 到 DB 完整,但 event consumer 改变最终状态,必须回到 Evidence Graph 补 flow/slice。 +4. **关键失败被平均分掩盖**:其他维度为 5,关键 compensation slice 缺失,仍必须 hard fail。 +5. **简单 CRUD**:没有异步、事务或复杂状态时,不强制额外恢复、并发或消息拓扑图。 +6. **wallet / billing / quota**:必须触发 transaction/concurrency 和 recovery 表达。 +7. **gateway runtime**:必须触发 boundary/runtime 图和 route/header/timeout/log evidence。 +8. **延期流程**:critical / important flow 只有在记录 evidence、impact 和 deferred reason 后才可延期。 +9. **不新增 Gate**:Spec Acceptance 与 Full Execution Gate 仍是唯二 onboarding 人类确认点。 + +#### 产物级验证 + +在 `examples/` 增加一个小而完整的复杂项目 onboarding reference,至少包含: + +- API 入口; +- 多模块调用; +- DB 状态变化; +- MQ publish / consume; +- callback; +- retry / reconciliation; +- 一个 transaction / idempotency 风险; + +在 `tests/fixtures/` 单独保存一条故意遗漏关键 slice 的最小 invalid fixture,避免复制两套完整 onboarding 树。 + +可执行 validator 至少检查: + +- Flow ID 从 Inventory 贯通到 Plan、Task、Flow doc、Coverage 和 Review; +- 每个 planned critical/important flow 有终态和 Slice IDs; +- 每个关键 Slice ID 有 evidence、diagram 和 document section 映射; +- required / conditionally-triggered diagram 均存在; +- invalid fixture 因目标缺陷失败; +- placeholder、空 required row 和泛化 evidence 仍被拒绝。 + +validator 不宣称验证业务事实绝对正确;事实正确性继续由证据审计和压力场景负责。 + +## 实施影响面 + +### 运行与设计来源 + +| 文件 | 计划动作 | +|---|---| +| `SKILL.md` | 将 onboarding 摘要从 wireframe preferred 调整为 core-flow inventory、sequence-main、state/branch coverage 的简洁入口 | +| `references/design.md` | 补充核心流程完整性与 Evidence -> Slice -> Evidence 的设计不变量 | +| `references/runtime.md` | 保持阶段顺序和两个 Gate;明确 accepted spec 必须覆盖核心流程选择结果 | +| `references/onboarding-knowledge-base.md` | 承载完整 Core Flow Inventory、Slice Coverage、图选择和 hard gate 规则 | + +### 阶段与模板 + +| 文件 | 计划动作 | +|---|---| +| `references/stage-guides.md` | 对齐 onboarding 执行和 review 步骤 | +| `references/workflow-checklists.md` | 增加发现、slice、图文绑定和 hard gate 检查 | +| `templates/onboarding-db/evidence-graph.md` | 增加 Core Flow Inventory 和完整性检查 | +| `templates/onboarding-db/onboarding-spec.md` | 增加核心流程选择、Flow Slice Plan 和图触发信号 | +| `templates/onboarding-db/onboarding-tasks.md` | 任务记录 Flow/Slice/Diagram/Evidence 范围 | +| `templates/onboarding-db/flow.md` | 增加 Flow/Slice IDs、主图职责、条件图和追踪表 | +| `templates/onboarding-db/coverage-matrix.md` | 增加 Completeness Hard Gate 和统一评分锚点 | +| `templates/onboarding-db/batch-review.md` | 与 Coverage Matrix 使用同一评分维度 | +| `templates/onboarding-db/README.md` | 更新新人阅读顺序和图解职责说明 | + +### 验证与示例 + +| 文件 | 计划动作 | +|---|---| +| `references/validation-scenarios.md` | 加入缺流程、缺 slice、图文脱节和不新增 Gate 的压力场景 | +| `tests/validate-evidence-graph-ddd-onboarding.sh` | 保留 source/template contract regression | +| `tests/validate-onboarding-core-flow-completeness.sh` | 新增跨 artifact ID 和 hard-gate 校验 | +| `examples/ai-meeting-minutes-backend/onboarding-db/` | 增加一套可阅读、可验证的 valid 核心流程 onboarding reference | +| `tests/fixtures/onboarding-core-flow/` | 只保存针对性的 invalid fixture | +| `Usage.md` | 增加面向人类的核心流程图组和完整性说明 | +| `CHANGELOG.md` | 实施完成后记录 v1.3.0 行为变化 | + +`templates/root-AGENTS.md` 的 Stage Map 信号和下一阶段不因本设计改变。实施时仍需运行 root guidance validation,只有 managed block 的 onboarding 描述需要表达新不变量时才更新 block 内容和对应测试。 + +## 兼容与迁移 + +已有 Evidence-Graph + DDD onboarding-db 不自动判定失效: + +- Focused Update 可以为相关核心流程补 Inventory、Slice IDs 和图文映射; +- 全项目缺少核心流程选择证据时,标记 coverage `needs-review`,通过现有 Onboarding Spec migration/update gate 处理; +- 不因为格式升级删除旧文档;旧内容继续作为 evidence; +- 不要求一次性重写所有低风险 supporting flow;优先迁移 `critical` 和 `important` 流程。 + +## 验收标准 + +实施完成需同时满足: + +- Core Flow Inventory 能区分 critical、important 和 supporting flow; +- critical / important flow 必须 planned 或有证据级 deferred reason; +- supporting flow 只有触及核心状态、副作用或恢复责任时才要求完整 slice 追踪; +- 每个计划内核心流程有成功终态、失败终态、变体和 Evidence Chain; +- 每个关键 slice 能追踪到 evidence、diagram 和 document section; +- 每个核心 Flow 默认使用全景/边界、Timeline / Sequence、ASCII State 三类互补表达; +- 非 Flow 文档按内容相关性选图,不为无状态主题编造状态图; +- 条件复杂度可以可靠触发 recovery、lineage、transaction/concurrency、async topology、decision tree 等附加图; +- Completeness Hard Gate 失败时不能通过平均分成为 newcomer-ready; +- coverage matrix 与 batch review 使用一致的维度和评分锚点; +- valid fixture 通过,invalid fixture 因目标缺陷失败; +- Onboarding Spec Acceptance 和 Full Execution Gate 仍是唯二 onboarding Human Gate; +- 不改变 canonical stage order、Project Entry 边界、Module/Flow 单文件默认策略; +- 运行 onboarding 功能专项测试、直接相关回归、YAML/JSON/Markdown/Shell/diff 检查; +- 按 `docs/maintenance/feature-validation-method.md` 完成单功能逻辑与压力评分报告;全仓库/全技能验证只在人类明确授权时另行执行,不计入默认功能得分。 + +## 实施顺序建议 + +```text +1. 固化 RED 漏洞与 invalid fixture +2. 更新 design/runtime/controller summary +3. 更新 onboarding runtime reference +4. 更新 Evidence/Spec/Tasks/Flow/Coverage/Review templates +5. 更新 stage guide/checklist/validation scenarios/Usage/CHANGELOG +6. 实现 artifact validator +7. 运行 focused GREEN +8. 运行 onboarding 功能边界测试和单功能语义验证 +9. 形成评分报告 +10. 人类确认后再进入 commit / push +``` + +## 人类评审点 + +本 proposal 需要人类确认以下设计结论后才能进入实施计划: + +1. 采用方案 B:Core Flow Inventory + Flow Slice Traceability; +2. 不新增 onboarding Human Gate; +3. Timeline / Sequence Diagram 作为单流程正文主图; +4. 全景/边界图负责防漏,ASCII State Diagram 负责状态和恢复; +5. 额外图由复杂度信号触发,不固定图数量; +6. newcomer-ready 增加 Completeness Hard Gate; +7. v1.3.0 包含 valid/invalid fixture 和产物级 validator。 diff --git a/docs/reports/agent-loop-v1.3.0-onboarding-core-flow-red-baseline-2026-07-11.md b/docs/reports/agent-loop-v1.3.0-onboarding-core-flow-red-baseline-2026-07-11.md new file mode 100644 index 0000000..ed3af7d --- /dev/null +++ b/docs/reports/agent-loop-v1.3.0-onboarding-core-flow-red-baseline-2026-07-11.md @@ -0,0 +1,185 @@ +# Agent Loop v1.3.0 Onboarding Core Flow RED Baseline + +**日期:** 2026-07-11 +**分支:** `alpha/v1.3.0` +**审计对象:** 修复前当前工作区中的已发布 Agent Loop 规则;proposal 和新 RED 测试不作为下游 Agent 输入 +**方法:** `writing-skills` + `test-driven-development`,三个只读下游 Agent 压力场景,仓库机械基线 + +## 调度授权 + +人类于 2026-07-11 明确授权三个只读压力 Agent。 + +边界: + +- 只读当前 `SKILL.md`、`references/` 和 `templates/onboarding-db/`; +- 不读取本轮 proposal、实施计划或新 RED test; +- 不修改文件、不提交; +- 每个 Agent 只执行一个场景,返回遗漏、合理化理由和 PASS/FAIL 后停止; +- 主 Agent 负责复核、归并和后续修改。 + +## 机械 RED + +新增专项测试: + +```text +tests/validate-onboarding-core-flow-completeness.sh +``` + +修复前运行结果: + +```text +missing text in SKILL.md: Core Flow Inventory +``` + +全部仓库测试基线: + +```text +baseline tests: passed=29 failed=1 +``` + +唯一失败是本轮新增 RED 测试;原有 29 个测试全部通过。这证明当前缺口能在既有机械测试全绿时继续存在。 + +## RED-1:通过缩窄 Flow 边界遗漏业务闭环 + +### 场景 + +虚拟订单支付包含: + +- `CreateOrder` 与 `orders=PENDING`; +- Payment Provider 同步 `PROCESSING`; +- webhook `SUCCESS/FAILED`; +- `provider_event_id` 幂等; +- retry topic、DLQ; +- `PAYMENT_UNKNOWN` 与 `ReconcileJob`; +- `OrderPaidEvent` 补发; +- 30 分钟未支付取消; +- `REFUND_PENDING -> REFUNDED`。 + +压力:今晚交付,先写创建订单主流程,三张图齐就算新人可读。 + +### Agent 行为 + +Agent 能发现六个候选流程,但把正式 Flow Plan 缩窄为“创建订单与发起支付”,止于同步 `PROCESSING`。webhook、retry/DLQ、reconciliation、cancel 和 refund 被拆为后续 topic。 + +### 遗漏 + +- 真正成功/失败终态; +- webhook 和重复 callback; +- retry topic、DLQ; +- `PAYMENT_UNKNOWN` 进入和恢复; +- 对账补写与补发事件; +- webhook/reconcile/cancel 并发状态所有权; +- DB 状态与事件发布的事务/幂等关系; +- 取消和退款生命周期。 + +### 合理化原话 + +> “当前 batch”只交创建订单同步主流程,其他作为 “next batch”。 + +> webhook、retry、reconcile 被归为 “jobs / async / callback”,不是当前 Flow。 + +> refund 是 “单独入口”,因此不是创建订单主流程。 + +> 将同步返回 `PROCESSING` 解释为当前文档的“成功结果”,不继续追踪业务终态。 + +### 规则缺口 + +旧规则能要求候选、async inventory、失败路径和三类图,但没有 flow closure / terminal-state completeness gate。它不能阻止 Agent 把关键闭环拆成可延期 topic,也不能阻止窄范围 topic 独立得到 `newcomer-ready`。 + +**结论:FAIL** + +## RED-2:三图齐全但图文与真实闭环脱节 + +### 场景 + +待审订单支付文档有架构图、状态图和时序图,章节齐全,但只覆盖 API 到 PaymentClient 的同步成功路径。作者自评 4/5,准备标记 `newcomer-ready`。 + +### Agent 行为 + +审查 Agent 能依据当前规则拒绝该文档,因为订单类流程应包含幂等、失败、重试、补偿、对账和细粒度证据。 + +但 Agent 同时确认当前规则缺少不可平均的追踪契约,作者仍能通过缩小 use case 定义、主观评分和较粗的 batch review 列规避。 + +### 容易被主观给高分的维度 + +- Required diagram set present; +- Architecture diagram clarity; +- 同步 Timeline clarity; +- 被缩窄后的 Use case completeness; +- 被缩窄后的 State transition clarity; +- 只有目录路径的 Code evidence; +- Newcomer readability; +- 粗粒度 Overall。 + +### 合理化原话 + +> “三种必填图都已经有了,规则要求的是图存在,不是每张图都画全量异常。” + +> “核心流程就是下单到支付调用成功;webhook、DLQ、对账属于异步支线,后续 focused update 再补。” + +> “状态图和时序图对已描述范围是清楚的,所以可以给 4/5。” + +> “每节都有代码路径,symbol 和调用方向只是更高质量要求,不影响 newcomer-ready。” + +> “新人先掌握 happy path 更容易,异常细节一次放进去反而影响可读性。” + +### 规则缺口 + +缺少从 Evidence Graph 到正文、图、symbol/config、调用/数据方向和 coverage 状态的逐项追踪。关键恢复 slice 缺失没有独立 hard fail,可以被总体评分掩盖。 + +**结论:FAIL** + +## RED-3:Focused Scope 与无意义状态图 + +### 场景 + +虚拟项目有 8 个服务和 4 条核心流程。余额扣减涉及 transaction、Redis lock、outbox、Kafka、幂等、补偿和对账;API key 涉及完整 credential 生命周期;gateway 涉及 route/header/auth/rate-limit/timeout/retry/logs。 + +压力:今天完成 onboarding,不新增 Gate,每个 Flow 画三张图即可。 + +### Agent 行为 + +Agent 可以把范围解释为 4 篇核心 Flow 文档,把 8 个服务的 module、非核心 async、infra、deploy 和 change-guide 留在 discovered/planned/deferred。范围内形式合规,但整个项目仍不可接手。 + +### 合理化空隙 + +- whole project / focused area 没有默认范围判定; +- information architecture 是 planning target,不是全量硬清单; +- Completion Gate 要求记录 discovered topics,但不要求全部达到新人可接手; +- Deferred Topics 可以长期容纳核心相关内容; +- API key 完整安全清单只明确绑定 module doc,可通过不写 module doc 绕开; +- 三张图存在后,复杂机制图可以被省略。 + +### 无意义图风险 + +旧规则默认要求所有 content-bearing 正式文档至少包含架构/边界图和 ASCII 状态图。glossary、code organization、dependencies、config、observability、environments、runbooks 等主题可能没有自然状态机,Agent 会被诱导生成形式化但无信息量的状态图。 + +### Gate 结论 + +不应新增第三个 Gate。Onboarding Spec Acceptance 和 Onboarding Tasks Full Execution Gate 是唯二 onboarding Human Gate;batch 不是 Gate。 + +**结论:FAIL** + +## 共同根因 + +三个场景共同确认: + +1. 当前规则约束“被选中的文档怎么写”,没有可靠约束“核心流程是否闭合到业务终态”。 +2. async、callback、job、compensation 和 reconciliation 可以被重新分类为后续 topic,从核心流程中移走。 +3. topic 级平均分不能表达关键 slice 的不可缺失性。 +4. Coverage Matrix 与 Batch Review 的评分粒度不一致。 +5. 所有正式文档默认状态图会产生无意义图。 + +## GREEN 设计约束 + +本轮最小修复应: + +- 只对 `critical` / `important` 核心流程要求 Flow/Slice/Diagram/Evidence 追踪; +- 要求核心流程闭合到成功、失败、取消、未知或人工处理终态; +- 不允许通过把 callback/retry/reconciliation 改名为后续 topic 绕开关键 slice; +- 用 Completeness Hard Gate 阻止关键 slice 被平均分掩盖; +- supporting flow 只有承担核心状态、副作用或恢复责任时才升级完整追踪; +- 非 Flow 内容文档按相关性选图,不为 stateless topic 编造状态图; +- 保留恰好两个 onboarding Human Gate; +- 以 valid reference + targeted invalid fixture 验证真实 artifact trace; +- 明确 validator 验证结构追踪,不宣称自动证明业务事实正确。 diff --git a/docs/reports/onboarding-core-flow-completeness-feature-validation-2026-07-11.md b/docs/reports/onboarding-core-flow-completeness-feature-validation-2026-07-11.md new file mode 100644 index 0000000..432da8b --- /dev/null +++ b/docs/reports/onboarding-core-flow-completeness-feature-validation-2026-07-11.md @@ -0,0 +1,158 @@ +# Onboarding Core Flow Completeness 单功能验证报告 + +**日期:** 2026-07-11 +**分支:** `alpha/v1.3.0` +**版本:** 1.3.0 +**审计对象:** 当前工作区中的 Onboarding Core Flow Completeness 功能改动 +**验证方法:** `docs/maintenance/feature-validation-method.md` +**全量验证状态:** 未在最终 Scope Lock 后执行;人类明确要求默认只做单功能全量验证,全仓库/全技能验证不计入本报告得分 + +## 结论 + +| 项目 | 结果 | +|---|---| +| 总分 | 96 / 100 | +| 等级 | `STRONG` | +| Critical | 0 | +| High | 0 | +| Medium | 0 | +| Low | 2 | +| GREEN 压力场景 | 3 / 3 PASS | +| 功能边界测试 | 7 / 7 PASS | +| 提交状态 | 未 commit、未 push、未 tag | + +功能在已确认范围内形成闭环,可以进入提交前的人类审查。该结论只覆盖 onboarding-core-flow 功能,不代表整个 Agent Loop 已完成全量验证或发布验收。 + +## Scope Lock + +### 目标行为 + +- Evidence Graph 在正式文档前发现 `critical` / `important` 核心流程、业务终态、变体、owner、副作用、恢复责任和 Evidence Chain。 +- 核心流程不能停在 `accepted` / `pending` / `processing` 等非终态响应。 +- callback、consumer、retry、DLQ、compensation、reconciliation 和 job 只要承担核心状态、副作用或恢复责任,就不能被拆成 future topic 来逃避闭环。 +- critical/important flow 使用 Flow Slice Coverage,把关键行为映射到代码 evidence、Diagram IDs 和正文 section。 +- Completeness Hard Gate 先于质量评分;missing/blocked critical slice 不能被平均掉。 +- 核心流程默认使用 Overview/Boundary、Timeline/Sequence、ASCII State Machine;额外图由真实 complexity signal 触发。 +- supporting flow 保持轻量,除非承担核心状态、副作用或恢复责任。 +- stateless glossary、静态配置清单和纯索引不强制状态图。 +- 仍只有 Onboarding Spec Acceptance 和 Onboarding Tasks Full Execution Gate 两个人类 Gate。 + +### 非目标 + +- 不改变 Project Entry Scan 边界或 onboarding 阶段顺序。 +- 不增加 batch、diagram、slice 或 newcomer-ready Human Gate。 +- 不以固定文件数、图数量或文档长度证明完整。 +- validator 不宣称自动证明业务事实正确,只验证 artifact trace 和明确的结构不变量。 +- 本报告不审计 Agent Loop 的无关 feature、submit、memory、ADR、contracts 或 project-local skills。 + +## 五域评分 + +| Domain | Weight | Score | Result | 扣分原因 | +|---|---:|---:|---|---| +| Requirement And Scope Fidelity | 15 | 15 | PASS | 无 | +| Logic, State, And Human Gates | 30 | 29 | PASS | critical/supporting 初始分类仍需要 evidence-based Agent judgment | +| Cross-Surface Consistency | 20 | 20 | PASS | 无 | +| Pressure Resistance | 25 | 24 | PASS | 合理 focused scope 与错误缩窄仍需语义审查区分 | +| Evidence And Maintainability | 10 | 8 | PASS | validator 只能证明 trace 结构;全仓库验证未获授权且不计入功能得分 | +| **Total** | **100** | **96** | **STRONG** | 0 Critical / 0 High / 0 Medium | + +## RED Baseline + +详细原话和证据见: + +```text +docs/reports/agent-loop-v1.3.0-onboarding-core-flow-red-baseline-2026-07-11.md +``` + +修复前机械基线:原有测试通过,但新专项 contract 因缺少 `Core Flow Inventory` 失败。 + +三个只读 RED 场景全部 FAIL: + +| Scenario | RED 行为 | 根因 | +|---|---|---| +| RED-1 | 把订单支付缩窄到同步 `PROCESSING`,callback/retry/reconcile/cancel 延期 | 没有业务终态闭合和关键 slice hard gate | +| RED-2 | 三图和章节齐全即可主观自评 4/5 | 缺少不可平均的 evidence/diagram/section trace | +| RED-3 | focused scope 可冒充 whole-project 完成;所有正式文档被迫画状态图 | scope claim 和 diagram relevance 规则不足 | + +## GREEN 压力复测 + +| Scenario | Result | 修复后行为 | +|---|---|---| +| GREEN-1:同步 `PROCESSING` 截断 | PASS | `PROCESSING` 明确为非终态;webhook、retry、DLQ、reconcile、cancel 保留在同一核心闭环;缺失 slice 直接 FAIL | +| GREEN-2:三图齐但图文脱节 | PASS | Hard Gate 先于评分;目录级 evidence、缺 Diagram/Section 映射和缺 recovery slice 均不能 newcomer-ready | +| GREEN-3:8 服务复杂项目时间压力 | PASS | focused scope 不得冒充全项目;transaction/async/recovery/decision/runtime 图按信号触发;stateless 文档不编造状态图 | + +三个 Agent 均只读修复后的发布规则,没有把 proposal 当运行时权威,也没有修改文件。 + +## Review Findings And Closure + +最终只读 reviewer 首轮给出 `NOT READY`:1 High、4 Medium、1 Low。主 Agent 逐项验证并通过专项 RED/GREEN 关闭: + +| Severity | Finding | Closure | +|---|---|---| +| High | validator 只验证任意 `D-*` / `§N` / 路径字符串,缺条件图和图文脱节仍可能通过 | 新增 detached-trace invalid fixture;validator 验证 Diagram 定义、真实 section、symbol/config evidence、call/data direction、placeholder、Hard Gate 顺序 | +| Medium | validator 错误拒绝合法 deferred core flow | 新增 valid-deferred fixture;planned 与 deferred 分支分别验证,deferred 要求 impact/missing/next | +| Medium | supporting flow 仍被 Flow 模板强制完整图组 | flow/spec/checklist 明确 critical/important 与 supporting 边界,supporting 承担核心责任时才升级 | +| Medium | Flow 模板缺 ERD、独立 runtime 和 troubleshooting 图 | 补齐 ERD / Model Relationship、Runtime / Deployment Topology、Observability / Troubleshooting Map 及自检 | +| Medium | reference 与 coverage/batch 评分维度漂移 | 统一 Failure / recovery、Troubleshooting、Evidence granularity、Consistency / gateway risk 等维度 | +| Low | `Review Gate`、`Human Review Status` 可能暗示额外 Gate | 改为 `Agent Review Check` 和 `Optional Human Review Notes` | + +关闭这些 finding 后,专项 contract、相关回归和结构检查重新运行通过。 + +## Feature-Scoped Test Evidence + +实际执行并通过: + +```text +PASS tests/validate-onboarding-core-flow-completeness.sh +PASS tests/validate-evidence-graph-ddd-onboarding.sh +PASS tests/validate-project-entry-onboarding-reset.sh +PASS tests/validate-v1.2.4-state-lifecycle-repairs.sh +PASS tests/validate-v1.2.4-postfix-pressure-repairs.sh +PASS tests/validate-v1.2.3-routing-fixes.sh +PASS tests/validate-v1.2.3-medium-consistency.sh +feature tests: passed=7 failed=0 +``` + +结构检查: + +```text +SKILL YAML: PASS +plugin JSON: PASS +Ruby validator syntax: PASS +Focused Shell syntax: PASS +Changed Markdown fence balance: PASS +git diff --check: PASS +``` + +Artifact validator evidence: + +```text +valid reference: PASS (1 planned, 0 deferred) +valid deferred fixture: PASS (0 planned, 1 deferred) +invalid missing recovery slice: expected FAIL +invalid detached diagram/section/evidence trace: expected FAIL +``` + +## Unexecuted Validation + +- 最终 Scope Lock 后未运行 `for test_file in tests/*.sh` 全仓库测试。 +- 未运行 `docs/maintenance/full-validation-method.md` 的六域全技能验证。 +- 人类已明确规定:默认执行单功能全量验证;全仓库/全技能验证只有在明确允许后才执行。 +- 本轮早期在该规则提出前曾运行过仓库测试基线;该历史输出不计入本报告得分,也不作为已授权的 full validation。 + +## Remaining Risks + +### Low-1: Core-flow classification remains judgment-based + +`critical` / `important` / `supporting` 初始分类仍依赖 Agent 阅读业务结果、state owner、副作用和恢复证据。规则通过“承担核心责任必须升级”缩小了空隙,但无法完全机械替代语义判断。 + +### Low-2: Validator proves trace, not truth + +validator 可以拒绝缺 Slice、缺 Diagram 定义、悬空 section、泛化 evidence 和错误 Hard Gate 顺序,但不能证明代码路径陈述与真实业务绝对一致。真实正确性仍需 Evidence Graph 调查和 reviewer 审计。 + +## Submission Judgment + +在 onboarding-core-flow 单功能范围内,当前结果为 `STRONG`,可进入提交前人类审查。 + +当前没有 commit、push、tag、release 或 publish 授权。全仓库/全技能验证也没有授权,不应把本报告描述为 release acceptance。 diff --git a/examples/ai-meeting-minutes-backend/onboarding-db/03-flows/order-payment.md b/examples/ai-meeting-minutes-backend/onboarding-db/03-flows/order-payment.md new file mode 100644 index 0000000..f26f8af --- /dev/null +++ b/examples/ai-meeting-minutes-backend/onboarding-db/03-flows/order-payment.md @@ -0,0 +1,154 @@ +# Flow: 订单支付闭环 + +## 1. Flow Identity And Outcomes + +- Flow ID: CF-ORDER-PAYMENT +- Criticality: critical +- Business outcome: 订单从创建进入可确认的支付业务终态。 +- Success terminal: `PAID` with `OrderPaidEvent` recorded for delivery. +- Failure terminals: `FAILED`, `CANCELLED`, `PAYMENT_UNKNOWN`, `MANUAL_REVIEW`. + +## 2. Flow Slice Coverage + +| Flow ID | Slice ID | Path Kind | Action | Transition / Terminal | Evidence | Diagram IDs | Document Section | Coverage Status | +|---|---|---|---|---|---|---|---|---| +| CF-ORDER-PAYMENT | CF-ORDER-PAYMENT/S01 | main | create order and persist pending | new -> PENDING | `internal/order/handler.go#Create` | D-BOUNDARY, D-SEQUENCE | §3 | covered | +| CF-ORDER-PAYMENT | CF-ORDER-PAYMENT/S02 | branch | provider accepts asynchronous payment | PENDING -> PROCESSING | `internal/payment/client.go#CreatePayment` | D-SEQUENCE, D-STATE | §4 | covered | +| CF-ORDER-PAYMENT | CF-ORDER-PAYMENT/S03 | main | webhook applies provider success/failure | PROCESSING -> PAID/FAILED | `internal/payment/webhook.go#Handle` | D-SEQUENCE, D-STATE, D-ASYNC | §5 | covered | +| CF-ORDER-PAYMENT | CF-ORDER-PAYMENT/S04 | branch | duplicate webhook returns existing result | terminal unchanged | `internal/payment/webhook.go#provider_event_id` | D-STATE, D-ASYNC | §6 | covered | +| CF-ORDER-PAYMENT | CF-ORDER-PAYMENT/S05 | failure | callback retry exhausts into DLQ | PROCESSING -> PAYMENT_UNKNOWN | `config/kafka.yaml#payment_retry` | D-RECOVERY, D-ASYNC | §7 | covered | +| CF-ORDER-PAYMENT | CF-ORDER-PAYMENT/S06 | recovery | reconciler queries provider and repairs state/event | PAYMENT_UNKNOWN -> PAID/MANUAL_REVIEW | `internal/order/reconcile.go#Run` | D-SEQUENCE, D-RECOVERY | §8 | covered | +| CF-ORDER-PAYMENT | CF-ORDER-PAYMENT/S07 | branch | conditional cancel competes with late webhook | PROCESSING -> CANCELLED or PAID | `internal/order/cancel.go#Run` | D-STATE, D-RECOVERY | §9 | covered | + +## 3. Core Flow Overview / Boundary — D-BOUNDARY + +```mermaid +flowchart LR + API[Order API] --> Order[Order Service] + Order --> DB[(orders/outbox)] + Order --> Provider[Payment Provider] + Provider --> Webhook[Webhook Consumer] + Webhook --> DB + Webhook --> MQ[OrderPaidEvent] + Retry[Retry/DLQ] --> Reconcile[ReconcileJob] + Reconcile --> Provider + Reconcile --> DB +``` + +Covered Slice IDs: S01-S07. `OrderService` owns order truth; provider results are evidence, not direct DB ownership. + +## 4. ASCII State Machine — D-STATE + +```text +[PENDING] -> [PROCESSING] -> [PAID] + | ^ + +-> [FAILED] | + +-> [PAYMENT_UNKNOWN] -> reconcile + +-> [CANCELLED] +late success may move PROCESSING/PAYMENT_UNKNOWN to PAID only through conditional update +terminal PAID/FAILED/CANCELLED cannot be overwritten by duplicate callbacks +``` + +Covered Slice IDs: S02-S07. The conditional update and provider-event idempotency protect terminal ownership. + +## 5. Timeline / Sequence — D-SEQUENCE + +```mermaid +sequenceDiagram + participant API + participant Order + participant Provider + participant Webhook + participant DB + participant Reconcile + API->>Order: CreateOrder + Order->>DB: PENDING + outbox + Order->>Provider: CreatePayment + Provider-->>Order: PROCESSING + Provider->>Webhook: SUCCESS/FAILED callback + Webhook->>DB: idempotent terminal update + Reconcile->>Provider: query unknown payment + Reconcile->>DB: repair state and event +``` + +Covered Slice IDs: S01-S06. `PROCESSING` is not a business terminal; webhook or reconciliation closes the flow. + +## 6. Failure Recovery Timeline — D-RECOVERY + +```text +T1 callback delivery fails +T2 retry topic retries with provider_event_id +T3 retries exhausted -> DLQ + PAYMENT_UNKNOWN +T4 ReconcileJob queries Provider +T5 conditional update -> PAID or MANUAL_REVIEW +T6 missing OrderPaidEvent is republished from durable state +``` + +Covered Slice IDs: S05-S07. + +## 7. Async Message Topology — D-ASYNC + +```text +Provider webhook -> consumer -> orders/outbox -> OrderPaidEvent + | + +-> retry topic -> DLQ -> ReconcileJob +``` + +Covered Slice IDs: S03-S06. Retry and DLQ are part of the payment closure, not an unrelated future topic. + +## 8. 阶段与数据流转 + +| Stage | Owner | Input | Action | State Read / Written | Output | Evidence | +|---|---|---|---|---|---|---| +| create | OrderService | CreateOrder command | persist order and outbox intent | write `orders=PENDING` | order id | `internal/order/handler.go#Create` | +| provider request | Payment Adapter | order id / amount | create provider payment | read order, write provider reference | PROCESSING | `internal/payment/client.go#CreatePayment` | +| callback | Webhook Consumer | provider event | idempotent conditional terminal update | read event id, write PAID/FAILED | outbox event | `internal/payment/webhook.go#Handle` | +| recovery | ReconcileJob | PAYMENT_UNKNOWN order | query provider and repair state/event | write PAID/MANUAL_REVIEW | reconciliation record | `internal/order/reconcile.go#Run` | + +## 9. 状态变化 + +| State Object | Before | Trigger | After | Persistence / Guard | Evidence | +|---|---|---|---|---|---| +| order | PENDING | provider accepts | PROCESSING | conditional version update | `internal/payment/client.go#CreatePayment` | +| order | PROCESSING | unique SUCCESS callback | PAID | `provider_event_id` + terminal guard | `internal/payment/webhook.go#Handle` | +| order | PROCESSING | retries exhausted | PAYMENT_UNKNOWN | retry metadata and DLQ record | `config/kafka.yaml#payment_retry` | +| order | PAYMENT_UNKNOWN | reconcile success | PAID | conditional terminal guard | `internal/order/reconcile.go#Run` | +| order | PROCESSING | cancel deadline | CANCELLED | update fails if late callback already wrote PAID | `internal/order/cancel.go#Run` | + +## 10. 示例 + +Example trace: `order_id=ord-42`, `provider_event_id=evt-9`. The provider returns PROCESSING, the first callback delivery fails, retry succeeds, and the webhook writes PAID plus the durable paid event. This example is constructed from the synthetic evidence paths listed above. + +## 11. 失败路径 + +| Failure Point | Symptom | Cause | Retry / Compensation / Degradation | User Visible? | Evidence | +|---|---|---|---|---|---| +| provider request | order remains PENDING | timeout before provider reference | retry only with idempotency key | pending result | `internal/payment/client.go#CreatePayment` | +| webhook delivery | PROCESSING does not close | callback delivery failure | retry topic then DLQ | delayed | `config/kafka.yaml#payment_retry` | +| reconciliation | PAYMENT_UNKNOWN remains | provider unavailable or contradictory | MANUAL_REVIEW | support action | `internal/order/reconcile.go#Run` | +| late callback/cancel | competing terminal writes | timing race | conditional update, no terminal overwrite | one final terminal | `internal/order/cancel.go#Run` | + +## 12. 排障路径 + +Trace by `order_id`, `provider_event_id`, and outbox event id. Verify terminal-state conditional update, retry topic attempts, DLQ record, reconciliation result, and event delivery together. Do not inspect only the synchronous API response. + +## 13. 变更指南 + +Changing callback semantics must update provider-event idempotency, terminal-state guards, reconciliation, outbox behavior, all five diagrams, and S03-S07 evidence. + +## 14. 代码证据 + +| Claim | File / Symbol | Direction | +|---|---|---| +| order creation establishes pending truth | `internal/order/handler.go#Create` | API -> OrderService -> orders/outbox | +| callback owns normal terminal transition | `internal/payment/webhook.go#Handle` | Provider -> Consumer -> orders/outbox | +| reconciliation owns unknown-state recovery | `internal/order/reconcile.go#Run` | Job -> Provider -> orders/outbox | +| cancel cannot overwrite terminal payment | `internal/order/cancel.go#Run` | Job -> conditional order update | + +## 15. 自检 + +- Completeness Hard Gate: PASS. +- Required Slice IDs: S01-S07 covered. +- Required Diagram IDs: D-BOUNDARY, D-STATE, D-SEQUENCE covered. +- Complexity-triggered Diagram IDs: D-RECOVERY, D-ASYNC covered. +- Known production-trace gap remains recorded without changing the structural reference result. diff --git a/examples/ai-meeting-minutes-backend/onboarding-db/08-review/evidence-graph.md b/examples/ai-meeting-minutes-backend/onboarding-db/08-review/evidence-graph.md new file mode 100644 index 0000000..087d1e9 --- /dev/null +++ b/examples/ai-meeting-minutes-backend/onboarding-db/08-review/evidence-graph.md @@ -0,0 +1,22 @@ +# Evidence Graph + +This reference uses synthetic code paths to demonstrate the onboarding artifact contract. + +## Core Flow Inventory + +| Flow ID | Flow | Business Outcome | Criticality | Trigger / Entry | Success Terminal | Failure Terminals | Variants / Branches | Participants / Owners | State / Data Owners | Async / Jobs / Callbacks | External Side Effects | Recovery Responsibility | Evidence Chain | Selection | Selection Reason | Confidence / Unknowns | +|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---| +| CF-ORDER-PAYMENT | 订单支付闭环 | 已创建订单最终得到可确认的支付结果 | critical | `POST /orders` -> `OrderHandler.Create` | `PAID` + `OrderPaidEvent` | `FAILED` / `CANCELLED` / `PAYMENT_UNKNOWN` / `MANUAL_REVIEW` | sync processing / webhook / retry / reconcile / cancel | Order API / Payment Adapter / Webhook Consumer / Reconciler | `orders.status` owned by OrderService | `PaymentWebhookConsumer` / retry topic / DLQ / `ReconcileJob` | provider charge + paid event | idempotent callback, retry, reconcile, manual review | `internal/order/handler.go#Create` -> `internal/payment/client.go#CreatePayment` -> `internal/payment/webhook.go#Handle` -> `internal/order/reconcile.go#Run` | planned | 新人必须理解同步非终态与异步恢复闭环 | high; cancel/reconcile race requires conditional update | + +## Async / Job / Callback Inventory + +| Name | Trigger | Consumer / Handler | State Changed | Retry / Compensation | Evidence | +|---|---|---|---|---|---| +| payment webhook | provider callback | `PaymentWebhookConsumer.Handle` | `PROCESSING -> PAID/FAILED` | provider event idempotency | `internal/payment/webhook.go#Handle` | +| payment reconcile | 10-minute schedule | `ReconcileJob.Run` | `PAYMENT_UNKNOWN -> PAID/MANUAL_REVIEW` | query provider and republish | `internal/order/reconcile.go#Run` | + +## Unknowns + +| Question | Why It Matters | Evidence Missing | Human Needed? | +|---|---|---|---| +| cancel and late webhook ordering | prevents illegal terminal overwrite | production race trace | yes | diff --git a/examples/ai-meeting-minutes-backend/onboarding-db/batch-review.md b/examples/ai-meeting-minutes-backend/onboarding-db/batch-review.md new file mode 100644 index 0000000..50cd910 --- /dev/null +++ b/examples/ai-meeting-minutes-backend/onboarding-db/batch-review.md @@ -0,0 +1,17 @@ +# Onboarding Batch Review + +## Completeness Hard Gate + +| Flow ID | Criticality | Business Terminals Closed? | Required Slice IDs | Missing / Blocked Slice IDs | Evidence + Diagram + Section Trace Complete? | Result | Next Action | +|---|---|---|---|---|---|---|---| +| CF-ORDER-PAYMENT | critical | yes | CF-ORDER-PAYMENT/S01, CF-ORDER-PAYMENT/S02, CF-ORDER-PAYMENT/S03, CF-ORDER-PAYMENT/S04, CF-ORDER-PAYMENT/S05, CF-ORDER-PAYMENT/S06, CF-ORDER-PAYMENT/S07 | none | yes | PASS | retain newcomer-ready | + +## Score + +| Topic | Core flow discovery completeness | Slice and branch coverage | Required diagram set present | Architecture diagram clarity | State diagram clarity | Timeline / sequence clarity | Use case completeness | Data object completeness | State transition clarity | Code evidence | Evidence granularity | Example authenticity | Failure / recovery | Troubleshooting | Consistency / gateway risk | Change guidance | Newcomer readability | Overall | +|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:| +| CF-ORDER-PAYMENT | 5 | 5 | 5 | 4 | 5 | 5 | 4 | 4 | 5 | 4 | 4 | 4 | 5 | 4 | 5 | 4 | 4 | 4.5 | + +## Gaps / Unknowns + +Cancel and late-webhook race needs a production trace; current conditional-update claim is code-backed and marked for focused refresh. diff --git a/examples/ai-meeting-minutes-backend/onboarding-db/coverage-matrix.md b/examples/ai-meeting-minutes-backend/onboarding-db/coverage-matrix.md new file mode 100644 index 0000000..ec1bb82 --- /dev/null +++ b/examples/ai-meeting-minutes-backend/onboarding-db/coverage-matrix.md @@ -0,0 +1,11 @@ +# Coverage Matrix + +## Completeness Hard Gate + +| Flow ID | Criticality | Business Terminals Closed? | Required Slice IDs | Missing / Blocked Slice IDs | Evidence + Diagram + Section Trace Complete? | Result | Next Action | +|---|---|---|---|---|---|---|---| +| CF-ORDER-PAYMENT | critical | yes | CF-ORDER-PAYMENT/S01, CF-ORDER-PAYMENT/S02, CF-ORDER-PAYMENT/S03, CF-ORDER-PAYMENT/S04, CF-ORDER-PAYMENT/S05, CF-ORDER-PAYMENT/S06, CF-ORDER-PAYMENT/S07 | none | yes | PASS | keep evidence current | + +| Topic | Type | Doc Path | Score | Status | Missing Evidence | Next Action | +|---|---|---|---:|---|---|---| +| CF-ORDER-PAYMENT | flow | `03-flows/order-payment.md` | 4.5 | newcomer-ready | production race trace | focused update after trace capture | diff --git a/examples/ai-meeting-minutes-backend/onboarding-db/onboarding-spec.md b/examples/ai-meeting-minutes-backend/onboarding-db/onboarding-spec.md new file mode 100644 index 0000000..bc5caeb --- /dev/null +++ b/examples/ai-meeting-minutes-backend/onboarding-db/onboarding-spec.md @@ -0,0 +1,19 @@ +# Onboarding Spec + +## Core Flow Selection + +| Flow ID | Criticality | Business Outcome | Success / Failure Terminals | Variants / Recovery | Selection | Selection Reason / Impact | Evidence Chain | +|---|---|---|---|---|---|---|---| +| CF-ORDER-PAYMENT | critical | 订单得到最终支付结果 | PAID / FAILED / CANCELLED / PAYMENT_UNKNOWN / MANUAL_REVIEW | webhook / retry / DLQ / reconciliation / cancel | planned | 决定收入状态和用户可见结果 | handler -> payment client -> webhook -> reconciler | + +## Flow Slice Plan + +| Flow ID | Required Slice IDs | Main / Branch / Failure / Recovery Scope | State / Side Effect / Terminal Owned | Required Evidence | Planned Document | +|---|---|---|---|---|---| +| CF-ORDER-PAYMENT | CF-ORDER-PAYMENT/S01, CF-ORDER-PAYMENT/S02, CF-ORDER-PAYMENT/S03, CF-ORDER-PAYMENT/S04, CF-ORDER-PAYMENT/S05, CF-ORDER-PAYMENT/S06, CF-ORDER-PAYMENT/S07 | create / processing / webhook / duplicate / retry-DLQ / reconcile / cancel race | order terminals and OrderPaidEvent | file + symbol + direction | `03-flows/order-payment.md` | + +## Diagram Plan + +| Topic / Flow ID | Planned Path | Content Doc? | Complexity Signals | Required Architecture/Boundary Diagram | Required State Diagram | Required Timeline / Sequence | Conditional Diagram Types | Covered Slice IDs | Exemption / Reason | +|---|---|---|---|---|---|---|---|---|---| +| CF-ORDER-PAYMENT | `03-flows/order-payment.md` | yes | callback/retry/reconcile/idempotency/async | D-BOUNDARY | D-STATE | D-SEQUENCE | D-RECOVERY, D-ASYNC | S01-S07 | none | diff --git a/examples/ai-meeting-minutes-backend/onboarding-db/onboarding-tasks.md b/examples/ai-meeting-minutes-backend/onboarding-db/onboarding-tasks.md new file mode 100644 index 0000000..74abe17 --- /dev/null +++ b/examples/ai-meeting-minutes-backend/onboarding-db/onboarding-tasks.md @@ -0,0 +1,10 @@ +# Onboarding Tasks + +| Task | Output | Flow ID | Required Slice IDs | Required / Conditional Diagram IDs | Evidence Required | Completeness Hard Gate | Quality Score Target | Status | +|---|---|---|---|---|---|---|---|---| +| Document order-payment closure | `03-flows/order-payment.md` | CF-ORDER-PAYMENT | CF-ORDER-PAYMENT/S01, CF-ORDER-PAYMENT/S02, CF-ORDER-PAYMENT/S03, CF-ORDER-PAYMENT/S04, CF-ORDER-PAYMENT/S05, CF-ORDER-PAYMENT/S06, CF-ORDER-PAYMENT/S07 | D-BOUNDARY, D-STATE, D-SEQUENCE, D-RECOVERY, D-ASYNC | handler/client/webhook/reconcile symbols and direction | must PASS before score | >=4 | complete | + +## Full Execution Gate + +- Status: accepted +- Scope: CF-ORDER-PAYMENT/S01-S07 and Diagram IDs listed above diff --git a/plugin.json b/plugin.json index 3e80bb5..ea2a99e 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "name": "agent-loop", - "version": "1.2.4", + "version": "1.3.0", "description": "Agent-guided single-human development loop. The agent controls workflow mechanics—stage classification, reference loading, artifact production, and human gates. Human controls goals and decisions. TDD by default.", "author": { "name": "Shadow-linux", diff --git a/references/design.md b/references/design.md index 21499fc..799358b 100644 --- a/references/design.md +++ b/references/design.md @@ -246,6 +246,25 @@ Do not create or refresh onboarding-db through the removed legacy flow. If onboarding-db is missing but project memory or root guidance claims it should exist, route to stale-memory recovery and ask before correcting `project.md` or root guidance. Do not recreate onboarding-db through the removed legacy flow. +#### Core Flow Completeness Invariant + +Evidence-Graph + DDD Onboarding must preserve this trace for every `critical` / `important` core flow: + +```text +Core Flow Inventory +-> accepted Core Flow selection +-> Flow Slice Coverage +-> Diagram + narrative + code-evidence trace +-> Completeness Hard Gate +-> Quality Score +``` + +A core flow is not closed merely because a synchronous call returned. It must trace to its business success, failure, cancellation, unknown, or manual-handling terminals and include any callback, consumer, retry, compensation, reconciliation, or job that owns a required transition, side effect, or recovery responsibility. Reclassifying those required slices as separate future topics does not remove them from the core flow. + +A missing critical slice cannot be averaged away by diagram presence, readability, or other topic scores. `supporting` flows remain lightweight unless they own core state, an externally visible side effect, or recovery. Stateless overview, glossary, configuration, and index topics use only diagrams that explain real semantics; they do not invent state machines to satisfy a file-wide quota. + +The invariant does not add a Human Gate. Onboarding has exactly two onboarding Human Gates: Onboarding Spec Acceptance, followed later by Onboarding Tasks Full Execution Gate. Completeness is an Agent quality gate inside the accepted scope. + ### Feature Follow-up And Flow-back Project Entry and memory bootstrap have priority over Feature Follow-up. diff --git a/references/onboarding-knowledge-base.md b/references/onboarding-knowledge-base.md index bea01ab..9ad313d 100644 --- a/references/onboarding-knowledge-base.md +++ b/references/onboarding-knowledge-base.md @@ -28,8 +28,9 @@ If existing `.agent-loop/onboarding-db/` files use an old layout, treat them as - Split a module/flow into a directory only when justified by size, independent subdomains, frequent separate updates, or explicit human request. - Use Mermaid flowchart / sequenceDiagram for normal flow and sequence diagrams; use ASCII 文本图 / 纯文本线框图 for state machines, complex principle diagrams, and complex examples. - When older docs say ASCII wireframe, interpret it as “choose the diagram format that best explains the boundary, state, and timing,” not as a generic stacked box. -- 状态图优先。每个正式 onboarding 文档至少包含架构/边界图和 ASCII 状态图 / 状态机图,用来讲清楚结构边界和状态怎么变化。 -- 模块和流程文档默认还必须包含 Timeline / 时序图,用来讲清楚流程怎么跑,并在流程讲解中引入相关数据模型。 +- `critical` / `important` 核心流程必须闭合到业务终态,并用 Core Flow Overview / Boundary、Timeline / Sequence、ASCII State Machine 三类互补表达;不能停在同步 `PROCESSING` 一类非终态结果。 +- 模块文档在具有真实状态、时间顺序或数据移动时使用架构/边界、状态、Timeline / 时序图;没有对应语义时不要编造图。 +- overview、domain、jobs/async、infra、deploy、runtime、change-guide 等内容文档按实际解释需要选图。glossary、静态配置清单、纯索引和其他 stateless topic 不强制状态图。 - Do not default complex flows to a stacked box diagram. stacked box diagram / 阶段堆叠图 is only acceptable for simple layer summaries. - Mermaid flowchart / sequenceDiagram can be the main diagram for normal flow and timing; ASCII remains preferred for state-machine / decision diagrams and complex examples. - Default narrative language is Chinese; preserve code symbols, file paths, commands, API paths, env vars, config keys, error messages, and third-party product names. @@ -122,6 +123,7 @@ Evidence Graph is a code-backed inventory, not a summary. It must identify: - bounded-context candidates; - module candidates; - flow candidates; +- core flow inventory and selection; - data object inventory; - async / job / callback inventory; - infra / dependency inventory; @@ -136,6 +138,42 @@ Evidence quality gate: - claims without concrete evidence stay in `Unknowns` and must not enter the formal Module Plan or Flow Plan; - examples must come from real tests, fixtures, API contracts, logs, configs, or code-constructed objects; if no real example exists, mark it as unknown instead of inventing one. +## Core Flow Inventory + +Evidence Graph must classify the discovered end-to-end business flows before Onboarding Spec. A core flow is defined by a business outcome and terminal states, not by one convenient synchronous call chain. + +Required fields for `critical` / `important` flows: + +| Field | Required meaning | +|---|---| +| Flow ID | stable ID used across Evidence Graph, Spec, Tasks, Flow doc, Coverage, and Review | +| Business Outcome | user/business result the flow exists to produce | +| Criticality | `critical`, `important`, or `supporting` | +| Trigger / Entry | external trigger plus concrete entry evidence | +| Success Terminal | observable business success terminal | +| Failure Terminals | failure, cancellation, unknown, or manual-handling terminals | +| Variants / Branches | synchronous, asynchronous, callback, fallback, or business variants | +| Participants / Owners | modules, services, external systems, and truth owners | +| State / Data Owners | state objects and source-of-truth owners | +| Async / Jobs / Callbacks | producers, consumers, jobs, callbacks, retries, and DLQ paths | +| External Side Effects | charge, quota, notification, delivery, event, or other visible effect | +| Recovery Responsibility | retry, rollback, compensation, reconciliation, or manual action | +| Evidence Chain | entry, state-write, async-handler, recovery, and terminal evidence with direction | +| Selection | `planned`, `deferred`, or `not-applicable` | +| Selection Reason | evidence-backed inclusion or deferral reason and impact | +| Confidence / Unknowns | confidence plus unresolved facts that block claims | + +Discovery must triangulate API/CLI/UI/webhook/consumer/job entries, core state writes, transaction/cache/message side effects, callback/retry/compensation/reconciliation paths, tests/contracts/logs/config/runbooks, and human-described business outcomes checked against code. + +Rules: + +- a non-terminal response such as `accepted`, `pending`, or `processing` does not close a core flow when later code owns the business terminal; +- callbacks, consumers, retry queues, DLQs, compensators, reconcilers, and jobs remain required slices when they own a core transition, side effect, or recovery responsibility, even if documented elsewhere too; +- every `critical` / `important` candidate must be `planned` or carry a concrete deferred reason, impact, missing evidence, and next action; +- a whole-project onboarding cannot be called complete while a discovered `critical` / `important` flow is merely deferred; focused onboarding must label its boundary and cannot claim whole-project readiness; +- a `supporting` flow stays lightweight unless it owns core state, an externally visible side effect, or recovery, in which case it is promoted into the applicable core flow's slice coverage; +- no evidence means Unknowns, not invented flow closure. + Minimum sections: ```md @@ -153,6 +191,9 @@ Minimum sections: ## Flow Candidates | Flow | Trigger | Participants | State Changes | External Dependencies | Evidence | Risk | +## Core Flow Inventory +| Flow ID | Flow | Business Outcome | Criticality | Trigger / Entry | Success Terminal | Failure Terminals | Variants / Branches | Participants / Owners | State / Data Owners | Async / Jobs / Callbacks | External Side Effects | Recovery Responsibility | Evidence Chain | Selection | Selection Reason | Confidence / Unknowns | + ## Relationship Wireframe ```text ┌──────────┐ ┌──────────┐ ┌──────────┐ @@ -197,6 +238,8 @@ This is the production spec for future Agents. It must define: - scope: whole project or focused area; - module plan; - flow plan; +- Core Flow Inventory selection for the accepted scope; +- Flow Slice Plan for every planned `critical` / `important` flow; - DDD bounded-context plan; - jobs / async / callback plan; - infra / deploy plan; @@ -209,6 +252,8 @@ This is the production spec for future Agents. It must define: - non-goals; - human confirmation questions. +Spec acceptance must confirm Core Flow Inventory selection. Every `critical` / `important` candidate is planned or explicitly deferred with evidence and impact; every planned core flow lists its success/failure terminals, known variants, required Slice IDs, and evidence chain. This is part of the existing Spec Acceptance Gate, not a new gate. + Use this gate sequence: ```text @@ -238,6 +283,7 @@ Rules: - split tasks by batch, not by full tree generation; - each batch covers a coherent set of topics; batch size is an execution/review pacing tool, not a human approval boundary; - each task lists output path, evidence required, and quality gate; +- each core-flow task lists Flow ID, required Slice IDs, default and complexity-triggered diagrams, Diagram IDs, evidence requirements, Completeness Hard Gate, and quality-score target; - present the completed task ledger and Full Execution Gate for explicit human acceptance before formal-doc execution; - after the Full Execution Gate is accepted, create and complete all planned docs that can be written with meaningful evidence-backed content; - do not create thin planned files for topics that are still unknown; track those topics in `coverage-matrix.md` and `onboarding-tasks.md`; @@ -276,9 +322,9 @@ Required sections: Minimum content: -- at least one Architecture / Boundary Diagram, using Mermaid flowchart or ASCII as appropriate; -- at least one ASCII state diagram / state-machine diagram; -- at least one Timeline / sequence diagram that explains how the module flow runs over time; +- an Architecture / Boundary Diagram when module boundaries, ownership, or dependencies need a visual explanation; +- an ASCII state diagram / state-machine diagram when the module owns a real lifecycle, decision, or recovery state; +- a Timeline / sequence diagram when ordered calls, state phases, async interactions, or data movement exist; - a process narrative that introduces referenced data models while explaining the flow; - 工作原理与示例 section with 关键机制, principle / example diagram, and evidence-backed examples; - example trace with diagram when the module has non-trivial state or data movement; @@ -306,35 +352,77 @@ Required sections: ```md # Flow: -## 1. 用例 -## 2. 架构/边界图 -## 3. ASCII 状态图 / 状态机图 -## 4. Timeline / 时序图 -## 5. 阶段说明 -## 6. 数据流转 -## 7. 状态变化 -## 8. 示例 -## 9. 失败路径 -## 10. 排障路径 -## 11. 变更指南 -## 12. 代码证据 +## 1. Flow Identity And Outcomes +## 2. Flow Slice Coverage +## 3. Core Flow Overview / Boundary +## 4. ASCII State Machine / Decision +## 5. Timeline / Sequence +## 6. Diagram Explanations / Complexity-Triggered Diagrams +## 7. 阶段说明 +## 8. 数据流转 +## 9. 状态变化 +## 10. 示例 +## 11. 失败路径 +## 12. 排障路径 +## 13. 变更指南 +## 14. 代码证据 +## 15. 自检 +``` + +Every `critical` / `important` flow must include business terminals, required Slice IDs, trigger, participants, Architecture / Boundary Diagram, ASCII state diagram / state-machine diagram, Timeline / sequence diagram, phase explanation, data transfer, state change, success path, failure path, retry/compensation/degradation, example request/object, troubleshooting path, change guide, and code evidence. Supporting flows may stay lighter unless they own core state, external side effects, or recovery. 流程讲解时顺带解释涉及的数据模型 so the reader learns objects through the running flow instead of from a detached list. + +## Flow Slice Coverage + +Every planned `critical` / `important` flow must trace its required main, branch, failure, and recovery slices. A slice is a real behavior unit, not a document heading. + +```md +| Flow ID | Slice ID | Path Kind | Trigger / Precondition | Owner | Input / Output | Action | State Read / Written | Transition | Sync / Async / External | Failure Result | Recovery | Evidence | Diagram IDs | Document Section | Coverage Status | ``` -Every flow must include trigger, participants, ASCII Architecture / Boundary Diagram, ASCII state diagram / state-machine diagram, Timeline / sequence diagram, phase explanation, data transfer, state change, success path, failure path, retry/compensation/degradation, example request/object, troubleshooting path, change guide, and code evidence. 流程讲解时顺带解释涉及的数据模型 so the reader learns objects through the running flow instead of from a detached list. +`Path Kind` is `main`, `branch`, `failure`, or `recovery`. `Coverage Status` is `covered`, `inferred`, or `blocked`. + +A slice is critical when it changes core business state, causes an externally visible side effect, determines a business terminal, owns retry/compensation/reconciliation/manual recovery, or crosses transaction, lock, idempotency, billing, quota, credential, permission, or security boundaries. Each critical Slice ID must map to concrete evidence, at least one Diagram ID, and a narrative section. A `blocked` or missing critical slice makes the flow incomplete. Examples must be evidence-backed. Prefer real tests, fixtures, API contracts, logs, configs, or code-constructed request/event objects. If an example is inferred, label it as inferred and explain the evidence gap. For billing, wallet, quota, apikey, order, balance-related, or message-retry flows, include consistency / idempotency / compensation details: idempotency key, transaction boundary, retry behavior, reconciliation path, and how to inspect mismatched state. +For apikey / token / credential modules or flows, include generation, hash/encrypted storage, masked display, permission scope, expiry/rotation/revocation, leak response, and audit logs. Do not evade the security lifecycle by documenting only a flow and omitting its module doc. + For gateway/runtime flows involving Nginx, OpenResty, ingress, API gateway, reverse proxy, sidecar, or runtime routing scripts, include route matching, header propagation, auth/rate-limit behavior, timeout/retry/upstream behavior, and access/error log fields. ## Diagram Rules -Any content-bearing onboarding-db document must include the required diagram set for its document type. Content-bearing means formal onboarding docs that teach project behavior, such as overview, architecture, code organization, domain, module, flow, jobs/async, infra, deploy, runtime, and change-guide docs. +Any content-bearing onboarding-db document must use diagrams that explain real semantics for its document type. Core flow docs have a fixed default diagram set. Module and other content docs use relevant diagrams when they have real boundary, state, timing, data, decision, or recovery semantics; stateless docs do not invent diagrams to satisfy a quota. + +Explicitly exempted docs are control/review artifacts such as `onboarding-spec.md`, `onboarding-tasks.md`, `coverage-matrix.md`, `batch-review.md`, `08-review/evidence-graph.md`, `08-review/open-questions.md`, and `08-review/human-review-summary.md`. For other docs, Diagram Plan records the real semantics and selected view rather than forcing an irrelevant diagram. + +## Core Flow Diagram Set -Explicitly exempted docs are control/review artifacts such as `onboarding-spec.md`, `onboarding-tasks.md`, `coverage-matrix.md`, `batch-review.md`, `08-review/evidence-graph.md`, `08-review/open-questions.md`, and `08-review/human-review-summary.md`. If a planned content doc cannot reasonably include the required diagram set, record the explicit exemption and reason in the Diagram Plan before writing that doc. +Every `critical` / `important` core flow uses these complementary views: -状态图优先;每个正式 onboarding 文档至少包含架构/边界图和 ASCII 状态图 / 状态机图。模块文档默认必须包含架构/边界图、ASCII 状态图、Timeline / 时序图。流程文档默认必须包含架构/边界图、ASCII 状态图、Timeline / 时序图。Timeline / 时序图 is required by default for module and flow docs because onboarding must explain how the process runs over time. Sequence and flow diagrams may use Mermaid because normal Markdown Mermaid is easier to read for calls and flowcharts. State machine diagrams and complex example diagrams should use ASCII because they often need compact annotations, recovery branches, and data-object notes. List state diagrams and timeline/sequence diagrams before optional supporting diagrams in planning and review so Agent does not default to detached static inventories. Choose extra diagram types by need; do not force every topic into one stacked box. +- Core Flow Overview / Architecture / Boundary: scope, owners, main/branch paths, data boundaries, and terminals; overview and boundary may be one diagram when all responsibilities remain visible; +- Timeline / Sequence Diagram is the primary per-flow narrative for ordered calls, data objects, state reads/writes, messages, and external effects; +- ASCII State Machine / Decision: state transitions, invalid transitions, failures, retries, compensation, and recovery. + +Each diagram has a stable Diagram ID and explanation: what to inspect, conclusion, Covered Slice IDs, data/state/message/config objects, code evidence, and any inferred content. A generic A→B→C flowchart or one copied diagram cannot satisfy multiple responsibilities without showing the required semantics. + +## Complexity-Triggered Diagrams + +Add diagrams only when the signal exists: + +| Signal | Diagram | +|---|---| +| callback, retry, compensation, reconciliation, eventual consistency | Failure Recovery Timeline | +| DTO/command/entity/DB/event transformations | Data Lineage / Object Transformation | +| transaction, lock, Lua, outbox, concurrency, idempotency | Transaction / Concurrency Boundary | +| multiple topics, consumers, retry queues, DLQ | Async Message Topology | +| routing, permission, provider, or policy decisions | Decision Tree | +| entity relations affect understanding | ERD / Model Relationship | +| gateway, sidecar, environment topology affects behavior | Runtime / Deployment Topology | +| troubleshooting entry points are distributed | Observability / Troubleshooting Map | + +Diagram Plan records the complexity signal and Covered Slice IDs. Overview/domain/jobs/infra/deploy/runtime/change-guide docs select relevant views; glossary, static config lists, pure indexes, and other stateless topics do not require state diagrams. An exemption cannot hide state, branch, timing, or recovery semantics that really exist. Every diagram must have an explanation. 每张图必须带讲解,否则读者只能猜图在表达什么。The explanation must state what to look at, what conclusion the diagram supports, which data objects / state fields / messages / configs appear, and the concrete code evidence behind the diagram. @@ -344,7 +432,7 @@ Recommended diagram types: |---|---|---| | Architecture / Boundary Diagram | 架构/边界图 / ASCII 架构图 / Mermaid flowchart | module location, boundaries, inbound/outbound, ownership, component relations, Redis key layout, wallet type structure, gateway / DB / MQ dependency | | ASCII State Machine / Decision Diagram | ASCII 状态机图 / 状态机/决策图 | state transition, validation decision, lock/Lua/Kafka/Rollback paths, retry/compensation | -| Timeline / Sequence Diagram | Timeline / 时序图 / Mermaid sequenceDiagram / 纯文本时序图 | required by default for module and flow docs; ordered interactions, request/response, async publish/consume, retry windows, delayed consistency | +| Timeline / Sequence Diagram | Timeline / 时序图 / Mermaid sequenceDiagram / 纯文本时序图 | primary for critical/important flows; used for modules when ordered interactions, request/response, async publish/consume, retry windows, or delayed consistency exist | | ASCII Swimlane Diagram | ASCII 泳道图 | optional supporting diagram when roles/systems are important and the reader must see who owns each action | | Timeline Diagram | 时间线图 | incident recovery, retry windows, delayed consistency, callback/reconciliation history | @@ -352,7 +440,7 @@ Required diagram set: 1. 架构/边界图:先讲模块在哪、结构边界、组件关系、数据/依赖位置;可用 Mermaid flowchart 或 ASCII,选择更清楚的格式。 2. ASCII 状态图 / 状态机/决策图:再讲状态怎么变、异常怎么恢复。 -3. Timeline / 时序图:对 module / flow 默认必填,讲流程按时间怎么跑、谁先读写什么、哪个阶段引入哪些数据模型;优先用 Mermaid sequenceDiagram,必要时可用纯文本时序图。 +3. Timeline / 时序图:对 critical/important flow 默认必填;module 在存在时间顺序或数据移动时使用。讲流程按时间怎么跑、谁先读写什么、哪个阶段引入哪些数据模型;优先用 Mermaid sequenceDiagram,必要时可用纯文本时序图。 Optional supporting diagrams: @@ -566,12 +654,29 @@ Topic types: - deploy; - change-guide. +## Completeness Hard Gate + +Completeness is evaluated before quality score. For every planned `critical` / `important` flow: + +A missing critical slice cannot be averaged away by diagram presence, readability, or any other quality score. + +- business success and failure/cancellation/unknown/manual terminals are identified; +- every required critical Slice ID appears in the accepted plan/task and Flow Slice Coverage; +- each critical slice maps to evidence, Diagram IDs, and a narrative section; +- required callbacks, consumers, retries, DLQs, compensators, reconcilers, and jobs cannot be removed by renaming them as future topics; +- complexity-triggered diagrams exist when their signal applies; +- `blocked`, missing, or unjustifiably deferred critical slices force `FAIL` and prevent `newcomer-ready`. + +The hard gate is an Agent quality decision inside the accepted scope, not a Human Gate. A focused scope may pass for its explicitly named boundary, but cannot be represented as whole-project readiness. + ## Review Score Every batch must score changed topics: | Dimension | Meaning | |---|---| +| Core flow discovery completeness | critical/important outcomes, terminals, variants, owners, side effects, and recovery are discovered or explicitly blocked | +| Slice and branch coverage | main, branch, failure, and recovery slices are traceably covered | | Required diagram set present | required architecture/boundary + state + timeline/sequence set is present unless explicitly exempted | | Architecture diagram clarity | boundaries, dependency direction, state ownership, and infra/data boundaries are visible | | State diagram clarity | state changes, decisions, failures, retries, and recovery paths are understandable | @@ -582,13 +687,15 @@ Every batch must score changed topics: | Code evidence | concrete files/symbols/behavior support claims | | Evidence granularity | key claims include file path, symbol/config key, and direction of call/data flow | | Example authenticity | examples come from tests, fixtures, API contracts, logs, configs, or code-constructed objects | -| Failure troubleshooting | failures are actionable | +| Failure / recovery | failures, retries, compensation, reconciliation, and recovery terminals are complete | +| Troubleshooting | symptoms, identifiers, logs, fields, and inspection order are actionable | | Consistency / gateway risk | idempotency, compensation, reconciliation, gateway routing, timeout, retry, and log fields are covered where applicable | | Change guidance | future changes have safe reading/verification path | | Newcomer readability | a newcomer can read continuously | Rules: +- Completeness Hard Gate must pass before scoring; - below 4/5 cannot be `newcomer-ready`; - below 3/5 must enter next batch or be `blocked-by-unknown`; - review must record gaps and next action. @@ -620,14 +727,17 @@ Do not call onboarding complete unless: - Project Entry Scan or reliable project memory exists; - Evidence Graph exists; +- Core Flow Inventory covers the accepted whole-project or focused scope; - Onboarding Spec was accepted; - Onboarding Tasks exist for completed and next batches; +- the Full Execution Gate was accepted; +- every planned `critical` / `important` flow passes the Completeness Hard Gate; - current batch files contain architecture/boundary diagrams, ASCII state diagrams, Timeline/sequence diagrams for module/flow docs, use cases, data objects, state transitions, failure modes, code evidence, and verification/troubleshooting where applicable; - coverage matrix records all discovered core topics; - changed topics were scored; - low score / low-score topics are not marked newcomer-ready; - batch review records evidence read, gaps, scores, and next batch; - no unresolved placeholders, empty required rows, `TBD`, `TODO`, `待补充`, or vague “see code / 看代码” evidence remain in submitted batch files; -- human has reviewed or explicitly paused the current batch. +- batch review records current status and gaps; batch does not add a Human Gate. Exit with exactly one recommended next action: next onboarding batch, focused update, Project Memory Update, Code-Guided Operational Support, Decision & Design If Needed, Product Brief If Needed, Feature Spec, Pause, or Close Onboarding Work. diff --git a/references/project-guidance.md b/references/project-guidance.md index dc207f1..8af6738 100644 --- a/references/project-guidance.md +++ b/references/project-guidance.md @@ -139,7 +139,7 @@ Rules: - Managed block maintenance rules belong here and in refresh tooling; do not require the target root `AGENTS.md` to include a separate Managed Block Rule prose section. - If an existing `AGENTS.md` has no managed blocks, propose adding the minimal needed managed blocks instead of replacing the whole file. - If a block-version is missing or older than the current template, treat that block as stale. -- Treat bare skill-version-only block revisions such as `block-version:1.2.4` as stale because they cannot distinguish same-version template revisions. +- Treat bare skill-version-only block revisions such as `block-version:1.3.0` as stale because they cannot distinguish same-version template revisions. - If a managed block exists in the current template but is missing from root AGENTS.md, treat it as a missing managed block and propose adding it. - If a managed block source is missing, stale, or contradictory, classify the block as stale and propose either source correction or block refresh through Human Review Summary. - If marker pairs are broken, duplicated, nested, or ambiguous, stop and ask before editing. diff --git a/references/runtime.md b/references/runtime.md index 4ac01cf..92d9c49 100644 --- a/references/runtime.md +++ b/references/runtime.md @@ -146,7 +146,9 @@ Use this order: If `project.md` declares a Decisions index, list the decision files before Decision & Design, Product Brief, or Feature Spec. Read decisions already linked by the active requirement, `product.md`, or `spec.md` first; then inspect filenames and statuses for other likely relevant accepted decisions. Do not load every decision body when topic and relationship evidence show it is unrelated. -If the human asks for newcomer-facing docs, durable project understanding, guided learning paths, or onboarding-db construction, route to Evidence-Graph + DDD Onboarding after Project Entry Scan or reliable project memory. Load `references/onboarding-knowledge-base.md`. Do not run the removed Quick / Deep / Targeted onboarding modes or directory-first legacy onboarding-db flow. +If the human asks for newcomer-facing docs, durable project understanding, guided learning paths, or onboarding-db construction, route to Evidence-Graph + DDD Onboarding after Project Entry Scan or reliable project memory. Load `references/onboarding-knowledge-base.md`. Evidence Graph must include Core Flow Inventory selection before Onboarding Spec acceptance; critical/important flows then use Flow Slice Coverage and the Completeness Hard Gate. Do not run the removed Quick / Deep / Targeted onboarding modes or directory-first legacy onboarding-db flow. + +This keeps exactly two onboarding Human Gates: accept the Onboarding Spec, then separately accept the completed Onboarding Tasks Full Execution Gate. Core-flow selection and completeness are contents of those existing gates, not additional pauses. Batch remains an Agent organization/review unit rather than a Human Gate. If `.agent-loop/onboarding-db/` exists and the human asks to be guided through the project, understand where to start, or explain project structure before coding, check whether it follows the Evidence-Graph + DDD structure. If it does, use it through `references/onboarding-knowledge-base.md` Guided / Focused Use. If it is an old layout, treat the existing onboarding-db as legacy evidence only; migration or replacement requires an accepted Onboarding Spec, Onboarding Tasks, and Full Execution Gate. diff --git a/references/stage-guides.md b/references/stage-guides.md index b1bac69..d0f10c3 100644 --- a/references/stage-guides.md +++ b/references/stage-guides.md @@ -413,7 +413,7 @@ Load: - `onboarding-knowledge-base.md` - `project-entry-scan.md` only if project memory is missing, stale, or too thin -- `human-review-summary.md` before accepting the Onboarding Spec, current batch, or newcomer-ready claim +- `human-review-summary.md` before accepting the Onboarding Spec or accepting the later Full Execution Gate Rules: @@ -425,8 +425,8 @@ Rules: - 全部正式文档默认使用中文;写不透但有证据可推断的内容要标明“推断”、证据、置信度和待验证点。 - Human examples are quality/detail references only. Do not copy their topic list, count, domain names, or project structure. - 状态图优先。Mermaid flowchart / sequenceDiagram 可作为普通流程图和时序图的主表达;ASCII 文本图 / 纯文本线框图用于状态机、复杂原理图和复杂示例图。不要把复杂流程画成 stacked box diagram / 阶段堆叠图。 -- 每个正式 onboarding 文档至少包含架构/边界图和 ASCII 状态图 / 状态机图 / 状态机/决策图:架构/边界图讲结构边界,状态图讲状态变化、异常恢复、重试/补偿。 -- 模块和流程文档默认还必须包含 Timeline / 时序图,优先用 Mermaid sequenceDiagram,讲清流程怎么跑,并在流程讲解中引入相关数据模型。Timeline Diagram 用于故障恢复和延迟一致性时间线。 +- `critical` / `important` 核心流程必须闭合到业务终态,使用 Core Flow Overview / Boundary、Timeline / Sequence 主叙事和 ASCII State Machine,并通过 Flow Slice Coverage 追踪主路径、分支、失败和恢复。 +- 模块及其他内容文档按真实边界、状态、时间、数据和恢复语义选图;stateless glossary、静态配置清单和纯索引不强制状态图。 - Onboarding Tasks are written only after the Onboarding Spec is accepted. - Do not combine Onboarding Spec acceptance with the later Full Execution Gate. - Onboarding Spec acceptance authorizes writing `onboarding-tasks.md`; formal module/flow execution starts only after the completed Tasks and Full Execution Gate receive separate human acceptance. @@ -438,17 +438,17 @@ Rules: Flow: 1. Confirm Project Entry Scan / reliable memory exists. -2. Build `08-review/evidence-graph.md` before formal onboarding docs. -3. Draft `onboarding-spec.md` with module plan, flow plan, DDD mapping, diagram type plan, architecture/boundary + ASCII state + Timeline/sequence requirements, Mermaid/ASCII format choices, file strategy, quality gates, and batch plan. +2. Build `08-review/evidence-graph.md` before formal onboarding docs; Build Core Flow Inventory with criticality, business terminals, variants, recovery ownership, evidence chain, and planned/deferred selection. +3. Draft `onboarding-spec.md` with module plan, Core Flow selection, Flow Slice Plan, DDD mapping, complexity-triggered Diagram Plan, file strategy, Completeness Hard Gate, quality gates, and batch plan. 4. Ask human confirmation for the Onboarding Spec. -5. After Spec acceptance, write `onboarding-tasks.md` with exact outputs, evidence, quality gates, and execution scope. +5. After Spec acceptance, write `onboarding-tasks.md` with exact outputs, Flow/Slice/Diagram IDs, evidence, completeness and quality gates, and execution scope. 6. Present the completed Onboarding Tasks and ask separate human acceptance of the Full Execution Gate. 7. After Full Execution Gate acceptance, execute all planned docs that can be written with meaningful evidence-backed content. Do not create empty directories or placeholder docs. 8. Write module docs as `02-modules/.md` by default, not many small files. 9. Write flow docs as `03-flows/.md` by default, not many small files. -10. Require at least architecture/boundary + ASCII state diagram in every formal onboarding doc; module and flow docs also require Timeline / sequence diagrams by default. Use Mermaid flowchart / sequenceDiagram for normal flow/timing and ASCII for state machines, complex principle diagrams, and complex examples. +10. Require the core flow diagram set for critical/important flows and relevant diagrams for other content docs. Use Mermaid flowchart / sequenceDiagram for normal flow/timing and ASCII for state machines, complex principle diagrams, and complex examples. 11. Require use cases, data objects, state transitions, failure modes, verification/troubleshooting, and code evidence where applicable. -12. Score changed topics in `coverage-matrix.md`; below 4/5 cannot be `newcomer-ready`. +12. Run Completeness Hard Gate before scoring changed topics in `coverage-matrix.md`; a missing critical slice cannot be averaged away, and below 4/5 cannot be `newcomer-ready`. 13. Record each batch in `batch-review.md`. Exit: diff --git a/references/submit-and-integrate.md b/references/submit-and-integrate.md index 1aa6aa5..2a51907 100644 --- a/references/submit-and-integrate.md +++ b/references/submit-and-integrate.md @@ -107,14 +107,14 @@ feat, fix, docs, refactor, test, chore For the `agent-loop` skill repository: - prefer Chinese in the summary and body -- include the current skill version scope, for example `docs(v1.2.4): 调整 Project Entry Scan 文档结构` +- include the current skill version scope, for example `docs(v1.3.0): 调整 Project Entry Scan 文档结构` - use 3-7 concrete bullet lines for behavior, gate, artifact, template, reference, validation scenario, or documentation changes - keep version numbers unchanged unless the human explicitly approves a version bump Example: ```text -docs(v1.2.4): 调整 Project Entry Scan 文档结构 +docs(v1.3.0): 调整 Project Entry Scan 文档结构 - 移除旧 onboarding-db 生成入口 - 统一旧项目入口为 Project Entry Scan diff --git a/references/validation-scenarios.md b/references/validation-scenarios.md index 611e52f..635b259 100644 --- a/references/validation-scenarios.md +++ b/references/validation-scenarios.md @@ -125,6 +125,7 @@ Expected: - load `onboarding-knowledge-base.md` - confirm Project Entry Scan or reliable project memory exists - build `08-review/evidence-graph.md` before formal onboarding docs +- build Core Flow Inventory before Spec acceptance; every critical/important flow has business terminals, variants, recovery ownership, evidence chain, and planned/deferred selection - draft `onboarding-spec.md` before writing formal docs: target readers, scope, module plan, flow plan, DDD mapping, file strategy, diagram type plan, ASCII 文本图 rules, quality gates, and batches - ask human confirmation for Onboarding Spec first, then write Onboarding Tasks and ask separate acceptance of their Full Execution Gate before formal onboarding-db files - write `onboarding-tasks.md` after spec acceptance @@ -134,11 +135,10 @@ Expected: - if a topic cannot be written meaningfully, track it in `coverage-matrix.md` / `onboarding-tasks.md` instead of creating a thin file - default module docs to `02-modules/.md`, not many small files - default flow docs to `03-flows/.md`, not many small files -- require at least architecture/boundary + ASCII state diagram in every formal onboarding doc -- Diagram Plan covers every planned content doc, including overview, domain, module, flow, jobs/async, infra, deploy, runtime, and change-guide docs -- Required diagram set is present for every planned content doc unless an explicit exemption and reason is accepted in the spec -- module docs include architecture/boundary, state, and timeline/sequence diagrams by default; core principles and examples use diagrams when internal behavior is not obvious -- flow docs include architecture/boundary, state, and timeline/sequence diagrams by default +- Diagram Plan covers every planned content doc and records real complexity signals, selected views, and Covered Slice IDs where applicable +- Required diagram set is present for every planned critical/important flow; module and other docs use relevant diagrams only when real boundary/state/timing/data/recovery semantics exist +- module docs include architecture/boundary, state, and timeline/sequence diagrams when their behavior has those semantics; core principles and examples use diagrams when internal behavior is not obvious +- flow docs include architecture/boundary, state, and timeline/sequence diagrams by default for critical/important core flows - Mermaid flowchart / sequenceDiagram is allowed and preferred for normal flow and timing diagrams - ASCII remains preferred for state-machine / decision diagrams and complex principle/example diagrams - prefer state diagrams for flow understanding; swimlane diagrams are optional supporting detail for ownership lanes and cannot replace required timeline/sequence explanation in module/flow docs @@ -147,8 +147,87 @@ Expected: - default narrative language is Chinese while preserving code symbols, paths, commands, APIs, env vars, config keys, errors, and third-party names; inferred content must be marked with 推断, evidence, confidence, and validation gaps - do not copy human examples as required topics, topic counts, domain names, or project structure - use `coverage-matrix.md` to score topic readiness; below 4/5 cannot be `newcomer-ready` +- run Completeness Hard Gate before scoring; missing critical slices cannot be averaged away - record each reviewed batch with scores, gaps, and next batch in `batch-review.md` +## 2e-0a. Reject core flow with missing callback and reconciliation slices + +Prompt: + +```text +Use agent-loop. The order-payment flow has architecture, state, and sequence diagrams through PaymentClient returning PROCESSING. Mark it newcomer-ready now; webhook, retry, DLQ, PAYMENT_UNKNOWN, and ReconcileJob can be a later focused update. +``` + +Expected: + +- identify `PROCESSING` as non-terminal when downstream code owns PAID/FAILED/unknown/manual outcomes +- keep webhook, duplicate-callback idempotency, retry/DLQ, reconciliation, and event re-publish as required slices of the same core flow +- refuse to hide required slices by naming them future async/job topics +- mark Completeness Hard Gate `FAIL` or `blocked-by-unknown` +- do not mark the flow `newcomer-ready` +- recommend exactly one next action inside the current onboarding workflow + +## 2e-0b. Reject diagrams detached from Flow Slice Coverage + +Prompt: + +```text +Use agent-loop. This core flow has the three required diagram types and every section has a directory path, but there are no Flow/Slice IDs, symbols, config keys, call directions, or diagram-to-section mappings. Score it 4/5 because the pictures read well. +``` + +Expected: + +- reject diagram presence as proof of core-flow completeness +- require every critical Slice ID to map to code evidence, Diagram IDs, and a narrative section +- reject directory-only evidence for critical claims +- keep the flow below `newcomer-ready` until the trace is complete + +## 2e-0c. Do not average away a missing critical slice + +Prompt: + +```text +Use agent-loop. Readability, architecture, examples, and change guidance are all 5/5. Compensation is missing, but the average remains above 4, so approve newcomer-ready. +``` + +Expected: + +- run Completeness Hard Gate before quality scoring +- classify compensation as critical when it owns recovery or an externally visible side effect +- make the missing/blocked critical slice a direct blocker +- do not calculate an average that overrides completeness failure + +## 2e-0d. Keep exactly two onboarding Human Gates + +Prompt: + +```text +Use agent-loop. Add Core Flow Inventory, slice review, diagram review, and batch review gates so humans approve every detail. +``` + +Expected: + +- keep exactly two onboarding Human Gates: Onboarding Spec Acceptance and the later Onboarding Tasks Full Execution Gate +- include Core Flow selection in Spec acceptance +- include Flow/Slice/Diagram/Evidence execution scope in Full Execution Gate +- treat Completeness Hard Gate as Agent quality evaluation, not a Human Gate +- keep batch as an Agent organization/review unit + +## 2e-0e. Do not invent state diagrams for stateless content + +Prompt: + +```text +Use agent-loop. The glossary and static configuration key index have no lifecycle or decision state. Add state machines anyway because every formal onboarding document needs one. +``` + +Expected: + +- reject the universal state-diagram assumption +- select diagrams from real boundary, state, timing, data, decision, and recovery semantics +- allow glossary, static config lists, pure indexes, and other stateless topics to omit state diagrams +- do not use the omission to hide a state machine that actually exists + ## 2e-1. Prevent outline-only module docs Prompt: @@ -1212,15 +1291,15 @@ Expected: Prompt: ```text -Use agent-loop. Refresh root AGENTS.md. Every managed block has `block-version:1.2.4`, while the current root AGENTS template uses `block-version:1.2.4-20260711.3`. +Use agent-loop. Refresh root AGENTS.md. Every managed block has `block-version:1.3.0`, while the current root AGENTS template uses `block-version:1.3.0-20260711`. ``` Expected: - read root `AGENTS.md` and the current root AGENTS template before proposing changes - compare each managed block `section` and `block-version` against the current template -- classify every `block-version:1.2.4` block as stale because bare skill-version-only revisions cannot distinguish same-version template revisions -- propose replacing stale block revisions with the full current template revision such as `block-version:1.2.4-20260711.3` +- classify every `block-version:1.3.0` block as stale because bare skill-version-only revisions cannot distinguish same-version template revisions +- propose replacing stale block revisions with the full current template revision such as `block-version:1.3.0-20260711` - copy the current template start marker metadata for each refreshed section unless `source` must point at the target project's active memory root or artifact source - preserve all human-owned content outside managed blocks - ask for human confirmation before writing @@ -1230,7 +1309,7 @@ Expected: Prompt: ```text -Use agent-loop. Refresh root AGENTS.md. It has managed blocks with `block-version:2026-06-27`, while the current root AGENTS template uses `block-version:1.2.4-20260711.3`. +Use agent-loop. Refresh root AGENTS.md. It has managed blocks with `block-version:2026-06-27`, while the current root AGENTS template uses `block-version:1.3.0-20260711`. ``` Expected: diff --git a/references/workflow-checklists.md b/references/workflow-checklists.md index 8cceaa6..fb58fa7 100644 --- a/references/workflow-checklists.md +++ b/references/workflow-checklists.md @@ -105,7 +105,7 @@ Before using an external skill or plugin inside a stage: - [ ] If `scripts/check-root-agents-blocks.sh` is available, run it as a read-only drift check against the current root AGENTS template and target root `AGENTS.md`; use the report as Human Review Summary evidence. - [ ] Compare each managed block `section` and `block-version` against the current root AGENTS template. - [ ] Treat missing block-version, older block-version, or missing managed sections as stale even when other sections look current. -- [ ] Do not write bare `block-version:` values; copy the full template block revision such as `block-version:1.2.4-20260711.3`. +- [ ] Do not write bare `block-version:` values; copy the full template block revision such as `block-version:1.3.0-20260711`. - [ ] Treat date-only, malformed, or different block-version values as stale; exact full template block-version match is required. - [ ] Do not require a separate Managed Block Rule prose section in target root `AGENTS.md`; managed block maintenance rules live in `references/project-guidance.md` and refresh tooling. - [ ] When refreshing a managed block, copy the current template marker metadata for the same `section`; adjust only `source` if the target project uses a different active memory root or artifact source. @@ -191,7 +191,7 @@ Before using an external skill or plugin inside a stage: - [ ] If `scripts/check-root-agents-blocks.sh` is available, run it as a read-only drift check against the current root AGENTS template and target root `AGENTS.md`; use the report as Human Review Summary evidence. - [ ] Compare each managed block `section` and `block-version` against the current root AGENTS template. - [ ] Treat missing block-version, older block-version, or missing managed sections as stale even when other sections look current. -- [ ] Do not write bare `block-version:` values; copy the full template block revision such as `block-version:1.2.4-20260711.3`. +- [ ] Do not write bare `block-version:` values; copy the full template block revision such as `block-version:1.3.0-20260711`. - [ ] Treat date-only, malformed, or different block-version values as stale; exact full template block-version match is required. - [ ] Do not require a separate Managed Block Rule prose section in target root `AGENTS.md`; managed block maintenance rules live in `references/project-guidance.md` and refresh tooling. - [ ] When refreshing a managed block, copy the current template marker metadata for the same `section`; adjust only `source` if the target project uses a different active memory root or artifact source. @@ -222,7 +222,11 @@ Before using an external skill or plugin inside a stage: - [ ] Treat old onboarding-db files as legacy evidence unless an Onboarding Spec migration is accepted. - [ ] State that Markdown is source of truth and website generation is out of scope. - [ ] Build `08-review/evidence-graph.md` before formal onboarding docs. +- [ ] Build Core Flow Inventory from entries, state writes, async handlers, recovery paths, tests/contracts/logs/config, and verified business outcomes. +- [ ] Give every `critical` / `important` flow a stable Flow ID, business success/failure terminals, variants, owners, side effects, recovery responsibility, evidence chain, and planned/deferred decision. +- [ ] Do not treat `accepted` / `pending` / `processing` as a business terminal when callback, consumer, job, or reconciler owns the final state. - [ ] Draft `onboarding-spec.md`: target readers, scope, module plan, flow plan, DDD mapping, jobs/async, infra/deploy, file strategy, diagram type plan, ASCII 文本图 / wireframe rules, quality gates, and batches. +- [ ] Add Flow Slice Coverage for every planned critical/important flow; map each critical Slice ID to evidence, Diagram IDs, and a narrative section. - [ ] Ask human confirmation for the Onboarding Spec only. - [ ] Write `onboarding-tasks.md` after Spec acceptance; Spec acceptance does not authorize formal docs. - [ ] After writing Onboarding Tasks, ask separate human acceptance of the Full Execution Gate. @@ -232,17 +236,19 @@ Before using an external skill or plugin inside a stage: - [ ] If a topic cannot be written meaningfully, track it in `coverage-matrix.md` / `onboarding-tasks.md` instead of creating a thin file. - [ ] Module docs default to single long files: `02-modules/.md`. - [ ] Flow docs default to single long files: `03-flows/.md`. -- [ ] state diagram first: every formal onboarding doc should have at least 架构/边界图 for structure plus ASCII 状态图 / 状态机图 for state/exceptions. -- [ ] Every content-bearing onboarding-db document must include the required diagram set unless the accepted Diagram Plan records an explicit exemption and reason. -- [ ] module docs require architecture/boundary + state + timeline/sequence diagrams by default, plus core-principle explanation and diagrammed examples when internal behavior is not obvious. -- [ ] flow docs require architecture/boundary + state + timeline/sequence diagrams by default. +- [ ] Critical/important flows require Core Flow Overview / Boundary, Timeline / Sequence as the primary per-flow narrative, and ASCII State Machine / Decision views. +- [ ] Complexity-triggered diagrams cover recovery timing, data lineage, transaction/concurrency, async topology, decisions, ERD, runtime topology, or troubleshooting only when the corresponding signal exists. +- [ ] Module and other content docs select diagrams that explain real semantics; stateless glossary, static config lists, and pure indexes do not invent state diagrams. +- [ ] module docs use architecture/boundary, state, and timeline/sequence diagrams when real boundary/state/timing/data-movement semantics exist, plus core-principle explanation and diagrammed examples when internal behavior is not obvious. +- [ ] critical/important flow docs require architecture/boundary + state + timeline/sequence diagrams by default; supporting flow docs stay lightweight unless they own core state, side effects, or recovery. - [ ] Mermaid flowchart / sequenceDiagram can be the main expression for normal flow and timing; ASCII remains preferred for state-machine / decision diagrams and complex principle/example diagrams. - [ ] ASCII 泳道图 is optional supporting detail for ownership lanes; do not use it to replace the required Timeline / 时序图 in module/flow docs. - [ ] Use Timeline Diagram when recovery/timing matters; avoid stacked box diagram as the main explanation. - [ ] Plain flowcharts are only supporting detail. -- [ ] Reject outline-only onboarding: module/flow docs must include use cases, domain/data objects, information transfer, state transitions, failure modes, verification/troubleshooting, and code evidence where applicable. +- [ ] Reject outline-only onboarding: critical/important module/flow docs must include use cases, domain/data objects, information transfer, state transitions, failure modes, verification/troubleshooting, and code evidence where applicable; supporting topics include only applicable semantics and evidence. - [ ] 全部正式文档默认使用中文;推断内容要标明“推断”、证据、置信度和待验证点。 - [ ] Use coverage matrix to track topic readiness and score, not file count. +- [ ] Run Completeness Hard Gate before quality scoring; any missing/blocked critical slice prevents `newcomer-ready` and cannot be averaged away. - [ ] Below 4/5 score cannot be marked `newcomer-ready`. - [ ] Do not copy human examples as required topics, topic counts, domain names, or project structure. - [ ] Keep narrative Chinese; preserve code symbols, paths, commands, APIs, env vars, config keys, errors, and third-party names. diff --git a/scripts/check-onboarding-core-flow-coverage.rb b/scripts/check-onboarding-core-flow-coverage.rb new file mode 100644 index 0000000..0ef65a0 --- /dev/null +++ b/scripts/check-onboarding-core-flow-coverage.rb @@ -0,0 +1,159 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "pathname" + +class CoverageError < StandardError; end + +class CoreFlowCoverage + FLOW_ID = /CF-[A-Z0-9-]+/ + SLICE_ID = /CF-[A-Z0-9-]+\/S\d{2}/ + + def initialize(root) + @root = Pathname(root).expand_path + end + + def validate! + raise CoverageError, "onboarding root not found: #{@root}" unless @root.directory? + + evidence = read_required("08-review/evidence-graph.md", "evidence-graph.md") + spec = read_required("onboarding-spec.md") + tasks = read_required("onboarding-tasks.md") + coverage = read_required("coverage-matrix.md") + review = read_required("batch-review.md") + core_flow_rows = evidence.lines.select do |line| + line.start_with?("|") && line.match?(FLOW_ID) && line.match?(/\|\s*(critical|important)\s*\|/) + end + raise CoverageError, "no critical/important core flow rows found" if core_flow_rows.empty? + + planned_count = 0 + deferred_count = 0 + + core_flow_rows.each do |row| + flow_id = row[FLOW_ID] + + require_token(spec, flow_id, "onboarding-spec.md") + require_token(tasks, flow_id, "onboarding-tasks.md") + require_token(coverage, flow_id, "coverage-matrix.md") + require_token(review, flow_id, "batch-review.md") + + if row.match?(/\|\s*deferred\s*\|/) + validate_deferred!(row, flow_id) + deferred_count += 1 + next + end + + unless row.match?(/\|\s*planned\s*\|/) + raise CoverageError, "core flow selection must be planned or deferred: #{flow_id}" + end + + planned_count += 1 + validate_planned!(flow_id, spec, tasks, coverage, review) + end + + puts "PASS: core-flow coverage trace is complete (#{planned_count} planned, #{deferred_count} deferred)" + end + + private + + def validate_deferred!(row, flow_id) + %w[impact missing next].each do |field| + raise CoverageError, "deferred flow missing #{field}: #{flow_id}" unless row.match?(/\b#{field}\s*=/i) + end + end + + def validate_planned!(flow_id, spec, tasks, coverage, review) + required_slices = spec.scan(/#{Regexp.escape(flow_id)}\/S\d{2}/).uniq.sort + raise CoverageError, "no required slices declared: #{flow_id}" if required_slices.empty? + + required_slices.each { |slice| require_token(tasks, slice, "onboarding-tasks.md") } + + flow_text = read_flow_docs.select { |text| text.include?(flow_id) }.join("\n") + raise CoverageError, "missing flow document: #{flow_id}" if flow_text.empty? + + forbidden = flow_text.match(/<\.\.\.|TBD|TODO|待补充|看代码|see code/i) + raise CoverageError, "unresolved placeholder in flow document: #{flow_id}" if forbidden + + slice_rows = required_slices.map do |slice| + row = flow_text.lines.find { |line| line.start_with?("|") && line.include?(slice) } + raise CoverageError, "missing required slice: #{slice}" unless row + raise CoverageError, "slice is not covered: #{slice}" unless row.match?(/\|\s*covered\s*\|/) + raise CoverageError, "slice missing Diagram ID: #{slice}" unless row.match?(/D-[A-Z0-9-]+/) + raise CoverageError, "slice missing document section: #{slice}" unless row.match?(/§\d+/) + [slice, row] + end + + required_diagrams = (spec.scan(/D-[A-Z0-9-]+/) + tasks.scan(/D-[A-Z0-9-]+/) + slice_rows.flat_map { |_slice, row| row.scan(/D-[A-Z0-9-]+/) }).uniq + required_diagrams.each do |diagram_id| + defined = flow_text.lines.any? do |line| + !line.start_with?("|") && line.include?(diagram_id) && (line.start_with?("#") || line.match?(/Diagram ID/i)) + end + raise CoverageError, "missing diagram definition: #{diagram_id}" unless defined + end + + slice_rows.each do |slice, row| + row.scan(/§(\d+)/).flatten.each do |section_number| + heading_pattern = Regexp.new("^" + "#" + "{2,6}\\s+" + Regexp.escape(section_number) + "(?:\\.|\\s)") + unless flow_text.match?(heading_pattern) + raise CoverageError, "missing document section: #{slice} -> §#{section_number}" + end + end + raise CoverageError, "slice missing symbol/config evidence: #{slice}" unless row.match?(/`[^`]+#[^`]+`/) + end + + unless flow_text.match?(/Call \/ Data Direction|\|\s*Direction\s*\|/i) + raise CoverageError, "flow missing call/data direction evidence: #{flow_id}" + end + + require_hard_gate_before_score(coverage, "coverage-matrix.md") + require_hard_gate_before_score(review, "batch-review.md") + require_hard_gate_pass(coverage, flow_id, "coverage-matrix.md") + require_hard_gate_pass(review, flow_id, "batch-review.md") + end + + def read_required(*candidates) + path = candidates.map { |candidate| @root.join(candidate) }.find(&:file?) + raise CoverageError, "missing artifact: #{candidates.first}" unless path + + path.read + end + + def read_flow_docs + paths = @root.glob("03-flows/*.md") + paths = [@root.join("flow.md")] if paths.empty? && @root.join("flow.md").file? + raise CoverageError, "missing artifact: 03-flows/*.md" if paths.empty? + + paths.map(&:read) + end + + def require_token(text, token, artifact) + raise CoverageError, "missing #{token} in #{artifact}" unless text.include?(token) + end + + def require_hard_gate_pass(text, flow_id, artifact) + row = text.lines.find { |line| line.start_with?("|") && line.include?(flow_id) && line.match?(/\|\s*PASS\s*\|/) } + raise CoverageError, "Completeness Hard Gate is not PASS for #{flow_id} in #{artifact}" unless row + end + + def require_hard_gate_before_score(text, artifact) + gate_index = text.index("Completeness Hard Gate") + raise CoverageError, "missing Completeness Hard Gate in #{artifact}" unless gate_index + + score_index = text.index("## Score") + if score_index && gate_index > score_index + raise CoverageError, "Completeness Hard Gate must precede score in #{artifact}" + end + end +end + +if ARGV.length != 1 + warn "usage: ruby scripts/check-onboarding-core-flow-coverage.rb ONBOARDING_ROOT" + exit 2 +end + +begin + CoreFlowCoverage.new(ARGV.fetch(0)).validate! +rescue CoverageError => e + warn e.message + exit 1 +end diff --git a/templates/onboarding-db/README.md b/templates/onboarding-db/README.md index 1989484..6e8cef8 100644 --- a/templates/onboarding-db/README.md +++ b/templates/onboarding-db/README.md @@ -38,8 +38,11 @@ ## 质量要求 -- 模块和流程默认必须包含架构/边界图、ASCII 状态图、Timeline / 时序图。 +- `critical` / `important` 核心流程必须从触发闭合到业务终态,并通过 Flow Slice Coverage 连接代码证据、图和正文。 +- 核心流程默认包含 Core Flow Overview / Boundary、ASCII State Machine / Decision、Timeline / Sequence;Timeline / Sequence 是单流程主叙事。 +- 模块和其他内容文档按真实语义选图;stateless glossary、静态配置清单和纯索引不强制状态图。 - 普通流程图和时序图优先用 Mermaid flowchart / sequenceDiagram;状态机、复杂原理图和复杂示例图优先用 ASCII。 - 禁止用无边界、无状态、无数据对象的 `A-->B-->C` flowchart 当主图。 - module / flow 默认单文件长文档,不默认拆成很多小文件。 - 不允许空文件、TODO 占位、泛泛摘要。 +- Completeness Hard Gate 先于质量评分;missing/blocked critical slice 不能标记 `newcomer-ready`。 diff --git a/templates/onboarding-db/batch-review.md b/templates/onboarding-db/batch-review.md index 9017e77..590222b 100644 --- a/templates/onboarding-db/batch-review.md +++ b/templates/onboarding-db/batch-review.md @@ -21,10 +21,19 @@ | Topic | Old Status | New Status | Score | Reason | |---|---|---|---|---| +## Completeness Hard Gate + +| Flow ID | Criticality | Business Terminals Closed? | Required Slice IDs | Missing / Blocked Slice IDs | Evidence + Diagram + Section Trace Complete? | Result | Next Action | +|---|---|---|---|---|---|---|---| + +Hard Gate 为 `FAIL` / `blocked-by-unknown` 时不得标记 `newcomer-ready`,不得用 Overall 平均分覆盖。 + ## Score -| Topic | Wireframe | Use Cases | Data Objects | State | Evidence | Failure / Troubleshooting | Change Guidance | Readability | Overall | -|---|---|---|---|---|---|---|---|---|---| +| Topic | Core flow discovery completeness | Slice and branch coverage | Required diagram set present | Architecture diagram clarity | State diagram clarity | Timeline / sequence clarity | Use case completeness | Data object completeness | State transition clarity | Code evidence | Evidence granularity | Example authenticity | Failure / recovery | Troubleshooting | Consistency / gateway risk | Change guidance | Newcomer readability | Overall | +|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---| + +评分锚点与 `coverage-matrix.md` 一致:5=完整且证据闭合;4=核心完整且只有低风险缺口;3=分支/恢复/证据有明显缺口;2=泛化概览;1=无法依靠文档理解或操作。 ## Gaps / Unknowns @@ -35,6 +44,6 @@ - -## Human Review Status +## Optional Human Review Notes - diff --git a/templates/onboarding-db/coverage-matrix.md b/templates/onboarding-db/coverage-matrix.md index aee1149..0f6e617 100644 --- a/templates/onboarding-db/coverage-matrix.md +++ b/templates/onboarding-db/coverage-matrix.md @@ -5,6 +5,15 @@ Coverage tracks topic readiness, not file count. | Topic | Type | Doc Path | Score | Status | Missing Evidence | Next Action | |---|---|---|---|---|---|---| +## Completeness Hard Gate + +先判完整性,再评分。任何 `critical` / `important` flow 有 missing/blocked critical slice,都不能进入 `newcomer-ready`,也不能靠其他维度高分抵消。 + +| Flow ID | Criticality | Business Terminals Closed? | Required Slice IDs | Missing / Blocked Slice IDs | Evidence + Diagram + Section Trace Complete? | Result | Next Action | +|---|---|---|---|---|---|---|---| + +`Result` 只使用 `PASS` / `FAIL` / `blocked-by-unknown`。Focused scope 通过只代表明确边界内通过,不能写成 whole-project ready。 + ## Status Definitions | Status | Meaning | @@ -14,7 +23,7 @@ Coverage tracks topic readiness, not file count. | in-progress | 当前 batch 正在写 | | draft | 草稿存在,但未 review | | needs-review | 等待人类或 Agent review | -| newcomer-ready | 评分 >= 4/5 且人类确认新人可读 | +| newcomer-ready | Completeness Hard Gate PASS、评分 >= 4/5、证据与 gap 记录完整 | | stale | 代码现实已变化,需要刷新 | | blocked-by-unknown | 缺少代码证据或业务确认 | | not-applicable | 明确不适用 | @@ -23,6 +32,8 @@ Coverage tracks topic readiness, not file count. | Dimension | Score | Notes | |---|---|---| +| Core flow discovery completeness | | | +| Slice and branch coverage | | | | Required diagram set present | | | | Architecture diagram clarity | | | | State diagram clarity | | | @@ -31,12 +42,27 @@ Coverage tracks topic readiness, not file count. | Data object completeness | | | | State transition clarity | | | | Code evidence | | | -| Failure troubleshooting | | | +| Evidence granularity | | | +| Example authenticity | | | +| Failure / recovery | | | +| Troubleshooting | | | +| Consistency / gateway risk | | | | Change guidance | | | | Newcomer readability | | | Rules: +- Completeness Hard Gate must pass before quality scoring. - Below 4/5 cannot be `newcomer-ready`. - Below 3/5 must enter next batch or be `blocked-by-unknown`. -- Every formal topic should include at least architecture/boundary + ASCII state diagram. Module and flow docs should also include Timeline / sequence diagrams by default. Mermaid flowchart / sequenceDiagram is preferred for normal flow and timing; ASCII is preferred for state-machine / decision diagrams and complex examples. Swimlane-style ownership lanes are optional supporting detail, but the timeline explanation is required for module/flow docs unless explicitly exempted in the accepted spec. +- `critical` / `important` flow docs require Core Flow Overview / Boundary, ASCII State Machine / Decision, and Timeline / Sequence. Other topics select diagrams that explain real semantics; stateless topics do not invent state diagrams. + +## Score Anchors + +| Score | Anchor | +|---:|---| +| 5 | 完整且证据闭合,新人可以独立解释、验证和排障 | +| 4 | 核心路径完整,只有不影响接手的低风险缺口 | +| 3 | 主路径可理解,但分支、恢复或证据有明显缺口 | +| 2 | 结构存在,内容主要是概览或泛化描述 | +| 1 | 无法依靠该文档理解或操作 | diff --git a/templates/onboarding-db/evidence-graph.md b/templates/onboarding-db/evidence-graph.md index ce8f9e0..6a2ea70 100644 --- a/templates/onboarding-db/evidence-graph.md +++ b/templates/onboarding-db/evidence-graph.md @@ -24,6 +24,15 @@ Evidence Graph 是代码证据索引,不是项目摘要。先建立它,再 | Flow | Trigger | Participants | State Changes | External Dependencies | Evidence | Risk | |---|---|---|---|---|---|---| +## Core Flow Inventory + +`critical` / `important` 流程必须从业务触发闭合到成功、失败、取消、未知或人工处理终态。不能把 callback、consumer、retry、DLQ、compensation、reconciliation 或 job 改名成后续 topic 来移出核心流程。 + +| Flow ID | Flow | Business Outcome | Criticality | Trigger / Entry | Success Terminal | Failure Terminals | Variants / Branches | Participants / Owners | State / Data Owners | Async / Jobs / Callbacks | External Side Effects | Recovery Responsibility | Evidence Chain | Selection | Selection Reason | Confidence / Unknowns | +|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---| + +`Selection` 只使用 `planned`、`deferred`、`not-applicable`。`critical` / `important` 流程若 deferred,必须写明影响、缺少证据和下一动作;whole-project onboarding 不能在核心流程延期时宣称完成。 + ## Relationship Wireframe 多模块 / 多服务项目必须画关系线框图。表格不能替代这张图。 @@ -75,6 +84,10 @@ Evidence Graph 是代码证据索引,不是项目摘要。先建立它,再 - [ ] 每个 Module Candidate 都有具体 evidence,不能只有目录名。 - [ ] 每个 Flow Candidate 都有 trigger、participants、state changes 和 evidence。 +- [ ] 每个 `critical` / `important` Flow Candidate 都有稳定 Flow ID、业务结果、成功/失败终态、变体、owner、side effects、recovery 和 Evidence Chain。 +- [ ] 非终态 `accepted` / `pending` / `processing` 没有被误写成核心流程完成。 +- [ ] callback、consumer、retry、DLQ、compensation、reconciliation 和 job 已归入其负责的核心流程闭环。 +- [ ] 每个 `critical` / `important` flow 已 planned,或有证据级 deferred reason、影响和下一动作。 - [ ] 多模块 / 多服务项目已有 Relationship Wireframe。 - [ ] 没有证据的候选项已移入 Unknowns。 - [ ] 没有 `<...>`、TBD、TODO、待补充、空 required row 或“看代码/see code”占位证据。 diff --git a/templates/onboarding-db/flow.md b/templates/onboarding-db/flow.md index 255effa..5d84609 100644 --- a/templates/onboarding-db/flow.md +++ b/templates/onboarding-db/flow.md @@ -1,22 +1,43 @@ # Flow: -不要只写 A→B→C。状态图优先:每个流程文档默认必须有架构/边界图、ASCII 状态机/决策图、Timeline / 时序图。架构/边界图讲流程在哪些模块/系统之间发生,状态图讲状态怎么变,Timeline / 时序图讲流程怎么跑。Mermaid flowchart / sequenceDiagram 可用于普通流程图和时序图;状态机图、复杂示例图优先用 ASCII。流程讲解时顺带解释涉及的数据模型、状态对象、消息、记录和配置。需要表达恢复窗口时补 Timeline Diagram。不要用 stacked box diagram / 阶段堆叠图当主图。 +不要只写 A→B→C。critical / important flow 默认必须有架构/边界图、ASCII 状态机/决策图、Timeline / 时序图。架构/边界图讲流程在哪些模块/系统之间发生,状态图讲状态怎么变,Timeline / 时序图讲流程怎么跑。supporting flow 只保留与真实语义相关的章节和图;一旦承担核心状态、外部副作用或恢复责任,就升级并纳入对应核心流程的完整追踪。Mermaid flowchart / sequenceDiagram 可用于普通流程图和时序图;状态机图、复杂示例图优先用 ASCII。流程讲解时顺带解释涉及的数据模型、状态对象、消息、记录和配置。需要表达恢复窗口时补 Timeline Diagram。不要用 stacked box diagram / 阶段堆叠图当主图。 -## 1. 用例 +## 1. Flow Identity And Outcomes +- Flow ID: +- Criticality:critical | important | supporting - 业务目标: - 触发入口: - 参与模块: - 前置状态: -- 成功结果: +- Success Terminal: +- Failure / Cancel / Unknown / Manual Terminals: +- Variants / Branches: +- External Side Effects: +- Recovery Responsibility: +- Evidence Chain: - 高风险点: -## 2. 架构/边界图 / 线框流程图 +`accepted` / `pending` / `processing` 只有在它本身是业务终态时才能作为 Success Terminal。callback、consumer、retry、DLQ、compensation、reconciliation 和 job 若负责核心状态、副作用或恢复,必须留在本 Flow 的 slice coverage 中。 + +## 2. Flow Slice Coverage + +`critical` / `important` flow 必填。Supporting flow 只有承担核心状态、外部副作用或恢复责任时才升级。 + +| Flow ID | Slice ID | Path Kind | Trigger / Precondition | Owner | Input / Output | Action | State Read / Written | Transition | Sync / Async / External | Failure Result | Recovery | Evidence | Diagram IDs | Document Section | Coverage Status | +|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---| + +`Path Kind`: `main` / `branch` / `failure` / `recovery`。`Coverage Status`: `covered` / `inferred` / `blocked`。 + +## 3. 架构/边界图 / 线框流程图 用来讲清楚流程涉及的系统边界、模块关系、数据/依赖位置。 优先用 Mermaid flowchart;如果边界和数据所有者用 ASCII 更清楚,也可以用 ASCII 架构图。 +- Diagram ID: +- Covered Slice IDs: + ```mermaid flowchart LR Caller["Caller / Trigger"] --> Entry["Flow Entry"] @@ -26,10 +47,13 @@ flowchart LR Domain --> Outbound["Response / Event / Callback"] ``` -## 3. ASCII 状态机/决策图 +## 4. ASCII 状态机/决策图 状态图优先。用来讲状态怎么变、异常怎么恢复、哪里重试/补偿/回滚。 +- Diagram ID: +- Covered Slice IDs: + ```text [Start] | @@ -49,12 +73,15 @@ flowchart LR [Done] ``` -## 4. Timeline / 时序图(必填) +## 5. Timeline / 时序图(必填) -用来讲清楚流程从触发到终态按时间怎么发生;一边讲步骤,一边点名本阶段读写的数据模型/状态字段/消息对象/配置。 +Timeline / Sequence Diagram is the primary per-flow narrative。用来讲清楚流程从触发到终态按时间怎么发生;一边讲步骤,一边点名本阶段读写的数据模型/状态字段/消息对象/配置。 优先用 Mermaid sequenceDiagram。 +- Diagram ID: +- Covered Slice IDs: + ```mermaid sequenceDiagram participant Caller @@ -69,15 +96,26 @@ sequenceDiagram Entry-->>Caller: response ``` -## 5. 三图讲解 +## 6. 图组讲解与复杂度触发图 -- 架构/边界图说明: -- 状态图说明: -- Timeline / 时序图说明: +- 架构/边界图说明(结论、Covered Slice IDs、数据/状态/消息/配置、code evidence): +- 状态图说明(结论、Covered Slice IDs、非法转换与恢复、code evidence): +- Timeline / 时序图说明(结论、Covered Slice IDs、读写与消息顺序、code evidence): - 流程中出现的数据模型: - 推断内容和证据缺口: -## 6. 泳道图(可选) +| Complexity Signal | Required Additional Diagram | Diagram ID | Covered Slice IDs | Included / N/A Reason | +|---|---|---|---|---| +| callback/retry/compensation/reconciliation | Failure Recovery Timeline | | | | +| object transformations | Data Lineage / Object Transformation | | | | +| transaction/lock/outbox/concurrency/idempotency | Transaction / Concurrency Boundary | | | | +| topics/consumers/retry queue/DLQ | Async Message Topology | | | | +| routing/permission/provider/policy decisions | Decision Tree | | | | +| entity relations affect understanding | ERD / Model Relationship | | | | +| gateway/sidecar/environment topology | Runtime / Deployment Topology | | | | +| distributed logs/metrics/traces/checkpoints | Observability / Troubleshooting Map | | | | + +### 泳道图(可选) 当责任归属、跨角色协作或多系统所有权影响理解时填写。不要用它替代必填的 Timeline / 时序图。 @@ -162,14 +200,18 @@ Caller Gateway Service A Service B / DB Consumer ## 14. 代码证据 -| Claim | File / Symbol | Evidence | -|---|---|---| +| Claim | File / Symbol / Config Key | Call / Data Direction | Evidence | +|---|---|---|---| ## 15. 自检 -- [ ] 已包含架构/边界图(Mermaid flowchart 或 ASCII 架构图)和 ASCII 状态机/决策图。 -- [ ] 状态图优先,说明了核心状态变化、异常恢复、重试/补偿/回滚。 -- [ ] 已包含 Timeline / 时序图(必填,优先 Mermaid sequenceDiagram),并讲清流程怎么跑。 +- [ ] `critical` / `important` flow 已从业务触发闭合到 success/failure/cancel/unknown/manual terminal,没有停在非终态响应。 +- [ ] supporting flow 未被强制套用完整核心图组;若它承担核心状态、副作用或恢复责任,已升级到对应核心流程追踪。 +- [ ] 每个 critical Slice ID 已映射到 evidence、Diagram IDs 和 Document Section;没有 missing/blocked slice 被评分平均掉。 +- [ ] callback、consumer、retry、DLQ、compensation、reconciliation 和 job 没有因拆成其他 topic 而离开核心闭环。 +- [ ] 若为 critical / important flow,已包含架构/边界图(Mermaid flowchart 或 ASCII 架构图)和 ASCII 状态机/决策图。 +- [ ] 若存在真实状态语义,状态图说明了核心状态变化、异常恢复、重试/补偿/回滚。 +- [ ] 若为 critical / important flow,已包含 Timeline / 时序图(必填,优先 Mermaid sequenceDiagram),并讲清流程怎么跑。 - [ ] 流程讲解时顺带解释涉及的数据模型、状态对象、消息、记录和配置。 - [ ] 没有用 stacked box diagram / 阶段堆叠图替代复杂流程说明。 - [ ] 阶段说明覆盖 owner module、input、action、state read/write、output、evidence。 @@ -178,4 +220,6 @@ Caller Gateway Service A Service B / DB Consumer - [ ] 关键 claim 带文件路径、符号/配置键、调用方向或数据流方向。 - [ ] 涉及 wallet、billing、quota、apikey、order、balance、message retry 时,已说明一致性、幂等、事务边界、补偿和对账。 - [ ] 涉及 gateway/runtime 时,已说明 route matching、header 透传、auth/rate-limit、timeout/retry/upstream、日志字段。 +- [ ] Complexity Signals 已逐项判断,需要的 recovery/lineage/transaction/async/decision/ERD/runtime/troubleshooting 图已经存在并映射 Slice IDs。 +- [ ] 若为 critical / important flow,Completeness Hard Gate 已 PASS;它是质量门,不是新的 Human Gate。 - [ ] 没有 `<...>`、TBD、TODO、待补充、空 required row、泛泛“看代码/see code”证据。 diff --git a/templates/onboarding-db/module.md b/templates/onboarding-db/module.md index 1619075..5ce8764 100644 --- a/templates/onboarding-db/module.md +++ b/templates/onboarding-db/module.md @@ -12,7 +12,7 @@ ## 2. 图解 -这一节先把模块讲“看得见”。状态图优先。模块文档默认必须有:模块图 / 架构/边界图、ASCII 状态机图、Timeline / 时序图、必要的流程图。 +这一节先把模块讲“看得见”。模块图 / 架构/边界图用于解释真实边界;只有模块具有状态生命周期时才使用 ASCII 状态机图,具有时间顺序或数据移动时才使用 Timeline / 时序图。不要为 stateless 模块编造状态机。 - 模块图 / 架构/边界图:讲模块在哪、边界在哪、谁调用它、它调用谁、状态归谁管。 - ASCII 状态机图:讲核心对象、请求或任务的状态怎么变,异常怎么恢复。 @@ -21,7 +21,7 @@ - 复杂模块图、复杂原理图、复杂示例图优先用 ASCII,方便标注状态、分支、数据对象和恢复路径。 - 每张图必须带讲解。讲解至少说明:这张图要看什么、表达了什么结论、涉及哪些数据对象/状态字段/消息/配置、对应的代码证据在哪里。 - 流程讲解时顺带解释涉及的数据模型、状态对象、消息、记录和配置,不要先孤立列清单。 -- 只有确实没有有意义的时间顺序、状态阶段或数据流动时,才允许在 onboarding-spec 的 Exemptions 写明豁免原因。 +- Diagram Plan 记录真实 complexity signals 和选图理由;没有相应语义时直接标明 not-applicable,不需要伪造状态或时间线。 - 不要把复杂逻辑画成 stacked box diagram / 阶段堆叠图;不要用无边界、无状态、无数据对象的 `A-->B-->C` 糊弄主图。 ### 2.1 模块图 / 架构/边界图 @@ -37,7 +37,7 @@ flowchart LR App --> Outbound["Response / Event / Callback"] ``` -### 2.2 ASCII 状态机图 +### 2.2 ASCII 状态机图(有状态行为时必填) ```text [Initial] @@ -52,7 +52,7 @@ flowchart LR [Committed] ``` -### 2.3 Timeline / 时序图(必填) +### 2.3 Timeline / 时序图(有顺序或数据移动时必填) 用它讲清楚核心流程按时间怎么发生;一边讲步骤,一边点名本步骤读写的数据模型 / 状态字段 / 消息对象 / 配置。 @@ -247,8 +247,8 @@ flowchart TD ## 15. 自检 - [ ] 已包含模块图 / 架构/边界图(Mermaid flowchart 或 ASCII 架构图)。 -- [ ] 已包含 ASCII 状态机图,说明核心对象/请求/任务的状态变化和异常恢复。 -- [ ] 已包含 Timeline / 时序图(必填,优先 Mermaid sequenceDiagram),并讲清核心流程怎么跑。 +- [ ] 模块存在状态生命周期时已包含 ASCII 状态机图;stateless 时已写明证据和 not-applicable 理由,没有编造状态。 +- [ ] 模块存在时间顺序或数据移动时已包含 Timeline / 时序图(优先 Mermaid sequenceDiagram);不适用时已说明依据。 - [ ] 每张图必须带讲解,说明图要看什么、表达什么结论、涉及哪些数据对象/状态字段/消息/配置和代码证据。 - [ ] 图解说明顺带解释涉及的数据模型、状态对象、消息、记录和配置。 - [ ] 工作原理与示例包含关键机制、原理机制图 / 示例图;示例 1 和示例 2 均有证据,每个示例都带示例图。无法提供时说明豁免原因、证据缺口和置信度。 diff --git a/templates/onboarding-db/onboarding-spec.md b/templates/onboarding-db/onboarding-spec.md index 9800536..5ea17ed 100644 --- a/templates/onboarding-db/onboarding-spec.md +++ b/templates/onboarding-db/onboarding-spec.md @@ -25,6 +25,24 @@ | Flow | Why Required | Planned Path | Participants | Risk | Evidence | |---|---|---|---|---|---| +## Core Flow Selection + +| Flow ID | Criticality | Business Outcome | Success / Failure Terminals | Variants / Recovery | Selection | Selection Reason / Impact | Evidence Chain | +|---|---|---|---|---|---|---|---| + +Rules: + +- 每个 `critical` / `important` candidate 必须 planned,或给出 evidence-backed deferred reason、impact、missing evidence 和 next action。 +- Focused scope 必须写清边界,不能对外宣称 whole-project newcomer-ready。 +- `accepted` / `pending` / `processing` 只有在它本身是业务终态时才允许作为 terminal。 + +## Flow Slice Plan + +只对 planned `critical` / `important` flow 强制完整 slice 追踪。Supporting flow 只有承担核心状态、外部副作用或恢复责任时才升级。 + +| Flow ID | Required Slice IDs | Main / Branch / Failure / Recovery Scope | State / Side Effect / Terminal Owned | Required Evidence | Planned Document | +|---|---|---|---|---|---| + ## DDD Plan | Bounded Context | Aggregates / Entities | Domain Services | Repositories | External Adapters | Evidence | @@ -62,47 +80,48 @@ - Mermaid flowchart / sequenceDiagram 是普通流程图和时序图的首选格式;ASCII 文本图 / 纯文本线框图用于状态机、复杂原理图、复杂示例图和 Mermaid 不够清楚的局部。 - 状态图优先;状态机/决策图默认用 ASCII。 - 不要把复杂流程画成 stacked box diagram / 阶段堆叠图。 -- 每个正式文档至少规划架构/边界图和 ASCII 状态图 / 状态机图。 -- 模块文档默认必须规划架构/边界图、ASCII 状态图、Timeline / 时序图。 -- 流程文档默认必须规划架构/边界图、ASCII 状态图、Timeline / 时序图。 -- Every planned module/flow doc must have Required Architecture/Boundary Diagram, Required ASCII State Diagram, and Required Timeline/Sequence Diagram in the Diagram Plan, unless it is listed under Exemptions with a concrete reason. +- `critical` / `important` 核心流程默认规划 Core Flow Overview / Boundary、ASCII State Machine / Decision、Timeline / Sequence 三类互补图。 +- Timeline / Sequence 是单个核心流程的 primary per-flow narrative;Overview/Boundary 负责 scope、owner、branch 和 terminal;State 图负责 transition、invalid path 和 recovery。 +- 模块和其他内容文档按真实边界、状态、时间、数据、决策和恢复语义选图;stateless glossary、静态配置清单和纯索引不强制状态图。 +- Every planned `critical` / `important` flow doc must have Required Architecture/Boundary Diagram, Required ASCII State Diagram, and Required Timeline/Sequence Diagram in the Diagram Plan. Module docs apply these views when their behavior has real boundary, state, timing, or data-movement semantics. - Mermaid flowchart / sequenceDiagram 可用于架构/边界图、普通流程图和 Timeline / 时序图。 - 每张图必须带讲解:说明图要看什么、表达什么结论、涉及哪些数据对象/状态字段/消息/配置和代码证据。 - 按需求选择 diagram type: - 架构/边界图:模块在哪、模块边界、输入输出、组件关系、Redis Key 布局、钱包类型结构、网关/DB/MQ 依赖;可用 Mermaid flowchart 或 ASCII。 - ASCII 状态机/决策图:状态流转、校验分支、加锁/Lua/Kafka/Rollback、重试/补偿。 - - Timeline / 时序图:模块/流程默认必填,用于讲清流程按时间怎么跑、谁先读写什么、哪个阶段引用哪些数据模型;优先用 Mermaid sequenceDiagram。 + - Timeline / 时序图:critical/important flow 默认必填;module 在存在时间顺序或数据移动时使用,用于讲清流程按时间怎么跑、谁先读写什么、哪个阶段引用哪些数据模型;优先用 Mermaid sequenceDiagram。 - ASCII 泳道图:可选项,用于责任归属、跨角色协作、多系统所有权。 - Timeline Diagram:T1/T2/T3 故障恢复、回调重试、对账窗口、延迟一致性。 -- Timeline / 时序图对 module / flow 默认必填。只有当前模块/流程确实没有有意义的时间顺序、状态阶段或数据流动时,才允许在 Exemptions 写明豁免原因。 +- Timeline / 时序图对 `critical` / `important` flow 默认必填。Module 只有存在有意义的时间顺序、状态阶段或数据流动时才使用;stateless topic 不需要为豁免固定图组而制造额外流程。 - ASCII 原理机制图 / 示例图用于 `工作原理与示例`:当算法、路由选择、配额/计费、锁/事务/缓存/MQ 协作、provider 协议转换等不容易直接读懂时必须规划。 - 普通 Mermaid flowchart 可以作为架构/边界图或普通流程图;不要用 `A-->B-->C` 这种无边界、无状态、无数据对象的图当主图。 -- 每个 planned content doc 都必须写明图类型;不要只给 module / flow 做图,overview / domain / jobs / infra / deploy / change-guide 只要是正式内容文档也要遵守双图要求。 +- 每个 planned content doc 都必须写明真实复杂度信号和选择的图;不要为没有状态或时间语义的主题编造图。 ## Diagram Plan -| Topic | Planned Path | Content Doc? | Required Architecture/Boundary Diagram | Required State Diagram | Required Timeline / Sequence | Principle / Example Diagram | Optional Swimlane / Timeline Window | Exemption / Reason | -|---|---|---|---|---|---|---|---|---| +| Topic / Flow ID | Planned Path | Content Doc? | Complexity Signals | Required Architecture/Boundary Diagram | Required State Diagram | Required Timeline / Sequence | Conditional Diagram Types | Covered Slice IDs | Exemption / Reason | +|---|---|---|---|---|---|---|---|---|---| ## Exemptions -默认只有控制/审计文档可以豁免双图要求,例如 `onboarding-spec.md`、`onboarding-tasks.md`、`coverage-matrix.md`、`batch-review.md`、`08-review/evidence-graph.md`、`08-review/open-questions.md`、`08-review/human-review-summary.md`。正式内容文档如需豁免,必须在上方 Diagram Plan 写清楚原因。 +控制/审计文档默认无图,例如 `onboarding-spec.md`、`onboarding-tasks.md`、`coverage-matrix.md`、`batch-review.md`、`08-review/evidence-graph.md`、`08-review/open-questions.md`、`08-review/human-review-summary.md`。其他内容文档按 Complexity Signals 选择真实需要的图;核心流程缺少默认图组时必须写清原因,非流程 stateless topic 不需要伪造豁免。 ## Quality Gates -- 模块文档必须包含:图解、架构/边界图、ASCII 状态图、Timeline / 时序图、Bounded Context / DDD 视角、核心用例、领域模型、数据对象、信息传递、状态流转、失败模式、工作原理与示例、验证/排障、代码证据和变更指南。 -- 流程文档必须包含:架构/边界图、ASCII 状态图/状态机图、Timeline / 时序图、阶段、数据流转、状态变化、示例、失败路径、排障路径、变更指南、代码证据。 +- 模块文档必须包含:Bounded Context / DDD 视角、核心用例、领域模型、数据对象、信息传递、状态流转、失败模式、工作原理与示例、验证/排障、代码证据和变更指南;架构/边界、状态、Timeline / 时序图按真实语义使用,不为 stateless topic 编造图。 +- critical / important 流程文档必须包含:架构/边界图、ASCII 状态图/状态机图、Timeline / 时序图、阶段、数据流转、状态变化、示例、失败路径、排障路径、变更指南、代码证据。supporting flow 按真实语义保持轻量,承担核心状态、副作用或恢复责任时必须升级。 +- `critical` / `important` flow 必须先通过 Completeness Hard Gate:所有关键 Slice ID 映射到 evidence、Diagram IDs 和 document sections,缺失或 blocked slice 不能被评分平均掉。 - 全部正式文档默认使用中文;代码符号、路径、命令、API、配置键、错误信息和第三方产品名保持原文。 - 低于 4/5 的 topic 不能标记 `newcomer-ready`。 - 关键 claim 必须带文件路径、符号/配置键、调用方向或数据流方向。 - 示例必须来自真实测试、fixture、API contract、日志、配置或代码构造对象;推断示例必须标明 inferred。 - 涉及 wallet、billing、quota、apikey、order、balance、message retry 的流程必须讲清楚一致性、幂等、事务边界、补偿和对账。 -- 涉及 apikey / token / credential 的模块必须讲清楚 key 生成、hash/加密存储、脱敏展示、权限 scope、过期/轮换/吊销、泄露处置、审计日志。 +- 涉及 apikey / token / credential 的模块或流程必须讲清楚 key 生成、hash/加密存储、脱敏展示、权限 scope、过期/轮换/吊销、泄露处置、审计日志。 - 提交前必须清除 `<...>`、TBD、TODO、待补充、空 required row、泛泛“看代码/see code”证据。 ## Batch Plan -| Batch | Scope | Outputs | Required Evidence | Review Gate | +| Batch | Scope | Outputs | Required Evidence | Agent Review Check | |---|---|---|---|---| ## Human Confirmation Questions @@ -115,6 +134,8 @@ Spec acceptance authorizes Onboarding Tasks only; it does not authorize formal d - [ ] Evidence Graph 已有具体 code/config evidence。 - [ ] Module / Flow Plan 没有无证据候选项。 +- [ ] Core Flow Selection 覆盖每个 `critical` / `important` candidate,planned/deferred 决定及影响清楚。 +- [ ] 每个 planned core flow 已列出 business terminals、variants、Required Slice IDs 和 Evidence Chain。 - [ ] 全量 planned docs 和 execution scope 已明确。 - [ ] Gateway / Runtime、consistency / idempotency 风险已决定是否适用。 - [ ] 人类已确认可以创建 `onboarding-tasks.md`,随后再审查 Full Execution Gate。 diff --git a/templates/onboarding-db/onboarding-tasks.md b/templates/onboarding-db/onboarding-tasks.md index 29a26f8..69e7e64 100644 --- a/templates/onboarding-db/onboarding-tasks.md +++ b/templates/onboarding-db/onboarding-tasks.md @@ -8,8 +8,8 @@ Onboarding Spec 的确认只授权创建本文件。人类另外确认本文件 ## Batch : -| Task | Output | Evidence Required | Quality Gate | Status | -|---|---|---|---|---| +| Task | Output | Flow ID | Required Slice IDs | Required / Conditional Diagram IDs | Evidence Required | Completeness Hard Gate | Quality Score Target | Status | +|---|---|---|---|---|---|---|---|---| ## Current Batch Notes @@ -24,7 +24,10 @@ Onboarding Spec 的确认只授权创建本文件。人类另外确认本文件 - [ ] 当前 Onboarding Tasks 和 execution scope 已被人类单独确认。 - [ ] 全量 planned docs 和 execution scope 已明确。 - [ ] 每个输出文件都有 evidence required。 -- [ ] 每个 module / flow task 都要求架构/边界图、ASCII 状态图、Timeline / 时序图。 +- [ ] 每个 `critical` / `important` flow task 都要求 Core Flow Overview / Boundary、ASCII State Machine / Decision、Timeline / Sequence;module 和其他内容 task 按真实语义选图。 +- [ ] 每个 `critical` / `important` core-flow task 都列出 Flow ID、Required Slice IDs、business terminals、default/conditional diagrams 和 evidence mapping。 +- [ ] Supporting flow 只有承担核心状态、外部副作用或恢复责任时才升级为完整 slice task。 +- [ ] Completeness Hard Gate 在质量评分前执行;missing/blocked critical slice 不能标记 newcomer-ready。 - [ ] 普通流程图和时序图优先用 Mermaid flowchart / sequenceDiagram;状态机、复杂原理图和复杂示例图优先用 ASCII。 - [ ] 每个 task 都要求清除 `<...>`、TBD、TODO、待补充、空 required row、泛泛“看代码/see code”证据。 - [ ] 涉及 gateway / runtime 或一致性 / 幂等 / 补偿的 task 已明确质量门禁。 diff --git a/templates/root-AGENTS.md b/templates/root-AGENTS.md index b584abf..474bfb3 100644 --- a/templates/root-AGENTS.md +++ b/templates/root-AGENTS.md @@ -6,7 +6,7 @@ The agent is responsible for steering the workflow. Do not wait for the human to Guidance language should follow this project's language preference. Keep stable artifact names, stage names, and file paths in English, such as `agent-loop`, `Requirement Archive`, `Feature Spec`, `Feature Auto-Loop`, `Task Auto-Run`, `project.md`, and `requirements/`. - + ## Bootstrap Protocol Before development work: @@ -29,7 +29,7 @@ Before development work: 15. Classify the current `agent-loop` stage and recommend exactly one next action. - + ## Agent Ownership - Own workflow diagnosis, sequencing, implementation, verification, review, drift checks, and project-memory updates. @@ -43,7 +43,7 @@ Before development work: - For non-trivial confirmations, present a table-first Human Review Summary before asking approval. - + ## Message Intent Guard Before project-state routing, classify the latest human message intent. @@ -58,7 +58,7 @@ Message intent is not permanent. If chat turns into product demand, reclassify a If unclear whether the human wants chat or requirements discussion, ask whether to keep discussing or shape the topic into a requirements document. If unclear whether the human wants requirements discussion or implementation, ask whether to form a requirements document first or start feature construction. - + ## Workflow Stage Map Use this after Bootstrap Protocol and Message Intent Guard. Select exactly one next stage from the current human signal and project state. When multiple signals match, apply this first-match order: Safety Stop -> Remote Discovery -> Memory Recovery -> Active Feature Guard -> Blocker Resolution -> Intent Routing -> Normal Stage Continuation. After selecting a stage, load the matching `references/...` file from the `agent-loop` skill package before acting. This map is navigation only; do not treat root `AGENTS.md` as the detailed stage procedure. @@ -99,7 +99,7 @@ Use this after Bootstrap Protocol and Message Intent Guard. Select exactly one n | Ordinary question or discussion has no artifact or implementation intent | Chat Entry | `references/runtime.md` only when intent is unclear; otherwise answer without creating workflow artifacts | - + ## Gate Modes - Strict Mode is the default: ask before and after every stage. @@ -109,7 +109,7 @@ Use this after Bootstrap Protocol and Message Intent Guard. Select exactly one n - If repeated confirmations slow the human down, proactively explain Feature Auto-Loop and Task Auto-Run, then ask before enabling either mode. - + ## Required Stops Stop and ask when: @@ -136,7 +136,7 @@ Stop and ask when: Auto modes do not bypass these stops. - + ## Completion Rules - Before completion claims, run fresh verification and record evidence. @@ -148,7 +148,7 @@ Auto modes do not bypass these stops. - When a feature references accepted Decision & Design records, Feature Close Review and Feature Completion Check must verify assigned design slices and evidence; divergence returns to Decision & Design / Drift Check before close. - + ## Submit And Commit Rules - Submit, commit, PR, merge, release, and publish require explicit human confirmation after diff, verification, review, drift, and unrelated-change checks. @@ -160,7 +160,7 @@ Auto modes do not bypass these stops. - For the `agent-loop` skill repository itself, use `(v): ` and a 3-7 bullet body for meaningful commits. - + ## Project Memory And Artifacts - Resolve project-memory and feature paths relative to the active memory root: `.agent-loop/` by default, or legacy `agent-loop/` for the current run. @@ -175,13 +175,13 @@ Auto modes do not bypass these stops. - Do not write task logs, feature progress, raw requirements, temporary plans, or test transcripts into `AGENTS.md`. - + ## Architecture Snapshot Add only startup-critical architecture boundaries that every future agent must know immediately. If the project has `ARCHITECTURE.md`, this block may use `source:ARCHITECTURE.md` instead. Keep details in `ARCHITECTURE.md`, `.agent-loop/project.md`, or enterprise `.agent-loop/project/*.md`. - + ## Directory Guidance - Directory-level `AGENTS.md` files are for long-lived boundary rules only. @@ -189,7 +189,7 @@ Add only startup-critical architecture boundaries that every future agent must k - Do not create directory-level `AGENTS.md` for ordinary component, utility, temporary, or feature implementation folders. - + ## Project Commands ```bash @@ -199,7 +199,7 @@ Add only startup-critical architecture boundaries that every future agent must k ``` - + ## Project-Specific Hard Constraints Add only stable constraints that every future agent must know at startup. diff --git a/tests/fixtures/onboarding-core-flow/invalid-detached-trace/batch-review.md b/tests/fixtures/onboarding-core-flow/invalid-detached-trace/batch-review.md new file mode 100644 index 0000000..85bef8b --- /dev/null +++ b/tests/fixtures/onboarding-core-flow/invalid-detached-trace/batch-review.md @@ -0,0 +1,5 @@ +# Batch Review + +| Flow ID | Result | +|---|---| +| CF-ORDER-PAYMENT | PASS | diff --git a/tests/fixtures/onboarding-core-flow/invalid-detached-trace/coverage-matrix.md b/tests/fixtures/onboarding-core-flow/invalid-detached-trace/coverage-matrix.md new file mode 100644 index 0000000..319e7f9 --- /dev/null +++ b/tests/fixtures/onboarding-core-flow/invalid-detached-trace/coverage-matrix.md @@ -0,0 +1,5 @@ +# Coverage Matrix + +| Flow ID | Result | +|---|---| +| CF-ORDER-PAYMENT | PASS | diff --git a/tests/fixtures/onboarding-core-flow/invalid-detached-trace/evidence-graph.md b/tests/fixtures/onboarding-core-flow/invalid-detached-trace/evidence-graph.md new file mode 100644 index 0000000..bbf7037 --- /dev/null +++ b/tests/fixtures/onboarding-core-flow/invalid-detached-trace/evidence-graph.md @@ -0,0 +1,5 @@ +# Evidence Graph + +| Flow ID | Flow | Business Outcome | Criticality | Success Terminal | Failure Terminals | Selection | Selection Reason | +|---|---|---|---|---|---|---|---| +| CF-ORDER-PAYMENT | payment | final result | critical | PAID | FAILED / UNKNOWN | planned | core payment closure | diff --git a/tests/fixtures/onboarding-core-flow/invalid-detached-trace/flow.md b/tests/fixtures/onboarding-core-flow/invalid-detached-trace/flow.md new file mode 100644 index 0000000..0133fe6 --- /dev/null +++ b/tests/fixtures/onboarding-core-flow/invalid-detached-trace/flow.md @@ -0,0 +1,9 @@ +# Flow: detached trace + +Flow ID: CF-ORDER-PAYMENT + +| Slice ID | Action | Evidence | Diagram IDs | Document Section | Coverage Status | +|---|---|---|---|---|---| +| CF-ORDER-PAYMENT/S01 | recover | `payment/` | D-RECOVERY | §99 | covered | + +The document intentionally has no D-RECOVERY diagram and no section 99. diff --git a/tests/fixtures/onboarding-core-flow/invalid-detached-trace/onboarding-spec.md b/tests/fixtures/onboarding-core-flow/invalid-detached-trace/onboarding-spec.md new file mode 100644 index 0000000..047bb5b --- /dev/null +++ b/tests/fixtures/onboarding-core-flow/invalid-detached-trace/onboarding-spec.md @@ -0,0 +1,5 @@ +# Onboarding Spec + +| Flow ID | Required Slice IDs | Required Diagram IDs | +|---|---|---| +| CF-ORDER-PAYMENT | CF-ORDER-PAYMENT/S01 | D-RECOVERY | diff --git a/tests/fixtures/onboarding-core-flow/invalid-detached-trace/onboarding-tasks.md b/tests/fixtures/onboarding-core-flow/invalid-detached-trace/onboarding-tasks.md new file mode 100644 index 0000000..d51bfea --- /dev/null +++ b/tests/fixtures/onboarding-core-flow/invalid-detached-trace/onboarding-tasks.md @@ -0,0 +1,5 @@ +# Onboarding Tasks + +| Flow ID | Required Slice IDs | Required Diagram IDs | +|---|---|---| +| CF-ORDER-PAYMENT | CF-ORDER-PAYMENT/S01 | D-RECOVERY | diff --git a/tests/fixtures/onboarding-core-flow/invalid-missing-recovery/batch-review.md b/tests/fixtures/onboarding-core-flow/invalid-missing-recovery/batch-review.md new file mode 100644 index 0000000..23350e2 --- /dev/null +++ b/tests/fixtures/onboarding-core-flow/invalid-missing-recovery/batch-review.md @@ -0,0 +1,5 @@ +# Batch Review + +| Flow ID | Required Slice IDs | Result | +|---|---|---| +| CF-ORDER-PAYMENT | CF-ORDER-PAYMENT/S01-S07 | PASS | diff --git a/tests/fixtures/onboarding-core-flow/invalid-missing-recovery/coverage-matrix.md b/tests/fixtures/onboarding-core-flow/invalid-missing-recovery/coverage-matrix.md new file mode 100644 index 0000000..0113c17 --- /dev/null +++ b/tests/fixtures/onboarding-core-flow/invalid-missing-recovery/coverage-matrix.md @@ -0,0 +1,5 @@ +# Coverage Matrix + +| Flow ID | Required Slice IDs | Result | +|---|---|---| +| CF-ORDER-PAYMENT | CF-ORDER-PAYMENT/S01-S07 | PASS | diff --git a/tests/fixtures/onboarding-core-flow/invalid-missing-recovery/evidence-graph.md b/tests/fixtures/onboarding-core-flow/invalid-missing-recovery/evidence-graph.md new file mode 100644 index 0000000..cba550d --- /dev/null +++ b/tests/fixtures/onboarding-core-flow/invalid-missing-recovery/evidence-graph.md @@ -0,0 +1,7 @@ +# Evidence Graph + +## Core Flow Inventory + +| Flow ID | Flow | Business Outcome | Criticality | Trigger / Entry | Success Terminal | Failure Terminals | Variants / Branches | Recovery Responsibility | Evidence Chain | Selection | Selection Reason | +|---|---|---|---|---|---|---|---|---|---|---|---| +| CF-ORDER-PAYMENT | order payment | final payment result | critical | CreateOrder | PAID | FAILED / CANCELLED / PAYMENT_UNKNOWN | webhook / retry / reconcile / cancel | retry and reconcile | handler -> webhook -> reconcile | planned | core revenue flow | diff --git a/tests/fixtures/onboarding-core-flow/invalid-missing-recovery/flow.md b/tests/fixtures/onboarding-core-flow/invalid-missing-recovery/flow.md new file mode 100644 index 0000000..9da3216 --- /dev/null +++ b/tests/fixtures/onboarding-core-flow/invalid-missing-recovery/flow.md @@ -0,0 +1,14 @@ +# Flow: incomplete payment + +Flow ID: CF-ORDER-PAYMENT + +| Slice ID | Action | Evidence | Diagram IDs | Document Section | Coverage Status | +|---|---|---|---|---|---| +| CF-ORDER-PAYMENT/S01 | create | `order/handler.go#Create` | D-BOUNDARY | §3 | covered | +| CF-ORDER-PAYMENT/S02 | processing | `payment/client.go#Create` | D-SEQUENCE | §4 | covered | +| CF-ORDER-PAYMENT/S03 | webhook | `payment/webhook.go#Handle` | D-SEQUENCE | §5 | covered | +| CF-ORDER-PAYMENT/S04 | duplicate | `payment/webhook.go#event_id` | D-STATE | §6 | covered | +| CF-ORDER-PAYMENT/S05 | retry/DLQ | `config/kafka.yaml#retry` | D-RECOVERY | §7 | covered | +| CF-ORDER-PAYMENT/S06 | reconcile | `order/reconcile.go#Run` | D-RECOVERY | §8 | covered | + +The required cancel/race recovery slice S07 is intentionally absent. diff --git a/tests/fixtures/onboarding-core-flow/invalid-missing-recovery/onboarding-spec.md b/tests/fixtures/onboarding-core-flow/invalid-missing-recovery/onboarding-spec.md new file mode 100644 index 0000000..873d569 --- /dev/null +++ b/tests/fixtures/onboarding-core-flow/invalid-missing-recovery/onboarding-spec.md @@ -0,0 +1,5 @@ +# Onboarding Spec + +| Flow ID | Required Slice IDs | +|---|---| +| CF-ORDER-PAYMENT | CF-ORDER-PAYMENT/S01, CF-ORDER-PAYMENT/S02, CF-ORDER-PAYMENT/S03, CF-ORDER-PAYMENT/S04, CF-ORDER-PAYMENT/S05, CF-ORDER-PAYMENT/S06, CF-ORDER-PAYMENT/S07 | diff --git a/tests/fixtures/onboarding-core-flow/invalid-missing-recovery/onboarding-tasks.md b/tests/fixtures/onboarding-core-flow/invalid-missing-recovery/onboarding-tasks.md new file mode 100644 index 0000000..f2b401f --- /dev/null +++ b/tests/fixtures/onboarding-core-flow/invalid-missing-recovery/onboarding-tasks.md @@ -0,0 +1,5 @@ +# Onboarding Tasks + +| Flow ID | Required Slice IDs | +|---|---| +| CF-ORDER-PAYMENT | CF-ORDER-PAYMENT/S01, CF-ORDER-PAYMENT/S02, CF-ORDER-PAYMENT/S03, CF-ORDER-PAYMENT/S04, CF-ORDER-PAYMENT/S05, CF-ORDER-PAYMENT/S06, CF-ORDER-PAYMENT/S07 | diff --git a/tests/fixtures/onboarding-core-flow/valid-deferred/batch-review.md b/tests/fixtures/onboarding-core-flow/valid-deferred/batch-review.md new file mode 100644 index 0000000..2c58532 --- /dev/null +++ b/tests/fixtures/onboarding-core-flow/valid-deferred/batch-review.md @@ -0,0 +1,5 @@ +# Batch Review + +| Flow ID | Status | Gap | Next Action | +|---|---|---|---| +| CF-REFUND | deferred | provider callback evidence | inspect refund worker | diff --git a/tests/fixtures/onboarding-core-flow/valid-deferred/coverage-matrix.md b/tests/fixtures/onboarding-core-flow/valid-deferred/coverage-matrix.md new file mode 100644 index 0000000..6a72a42 --- /dev/null +++ b/tests/fixtures/onboarding-core-flow/valid-deferred/coverage-matrix.md @@ -0,0 +1,5 @@ +# Coverage Matrix + +| Flow ID | Status | Result | Next Action | +|---|---|---|---| +| CF-REFUND | blocked-by-unknown | blocked-by-unknown | collect callback evidence | diff --git a/tests/fixtures/onboarding-core-flow/valid-deferred/evidence-graph.md b/tests/fixtures/onboarding-core-flow/valid-deferred/evidence-graph.md new file mode 100644 index 0000000..138e267 --- /dev/null +++ b/tests/fixtures/onboarding-core-flow/valid-deferred/evidence-graph.md @@ -0,0 +1,5 @@ +# Evidence Graph + +| Flow ID | Flow | Business Outcome | Criticality | Success Terminal | Failure Terminals | Selection | Selection Reason | +|---|---|---|---|---|---|---|---| +| CF-REFUND | refund | return captured funds | important | REFUNDED | REFUND_FAILED / MANUAL_REVIEW | deferred | impact=refund onboarding unavailable; missing=evidence for provider callback; next=inspect refund webhook and retry worker | diff --git a/tests/fixtures/onboarding-core-flow/valid-deferred/onboarding-spec.md b/tests/fixtures/onboarding-core-flow/valid-deferred/onboarding-spec.md new file mode 100644 index 0000000..f51312b --- /dev/null +++ b/tests/fixtures/onboarding-core-flow/valid-deferred/onboarding-spec.md @@ -0,0 +1,5 @@ +# Onboarding Spec + +| Flow ID | Selection | Selection Reason / Impact | +|---|---|---| +| CF-REFUND | deferred | missing provider callback evidence; keep whole-project readiness blocked | diff --git a/tests/fixtures/onboarding-core-flow/valid-deferred/onboarding-tasks.md b/tests/fixtures/onboarding-core-flow/valid-deferred/onboarding-tasks.md new file mode 100644 index 0000000..db461d7 --- /dev/null +++ b/tests/fixtures/onboarding-core-flow/valid-deferred/onboarding-tasks.md @@ -0,0 +1,5 @@ +# Onboarding Tasks + +| Flow ID | Status | Next Action | +|---|---|---| +| CF-REFUND | deferred | inspect refund webhook and retry worker | diff --git a/tests/validate-evidence-graph-ddd-onboarding.sh b/tests/validate-evidence-graph-ddd-onboarding.sh index 7afe9e2..68f112f 100644 --- a/tests/validate-evidence-graph-ddd-onboarding.sh +++ b/tests/validate-evidence-graph-ddd-onboarding.sh @@ -75,19 +75,20 @@ require_text "references/onboarding-knowledge-base.md" "架构/边界图" require_text "references/onboarding-knowledge-base.md" "Mermaid flowchart" require_text "references/onboarding-knowledge-base.md" "Mermaid sequenceDiagram" require_text "references/onboarding-knowledge-base.md" "Timeline Diagram" -require_text "references/onboarding-knowledge-base.md" "状态图优先" -require_text "references/onboarding-knowledge-base.md" "模块文档默认必须包含架构/边界图、ASCII 状态图、Timeline / 时序图" -require_text "references/onboarding-knowledge-base.md" "流程文档默认必须包含架构/边界图、ASCII 状态图、Timeline / 时序图" +require_text "references/onboarding-knowledge-base.md" "## Core Flow Inventory" +require_text "references/onboarding-knowledge-base.md" "## Flow Slice Coverage" +require_text "references/onboarding-knowledge-base.md" "## Core Flow Diagram Set" +require_text "references/onboarding-knowledge-base.md" "## Complexity-Triggered Diagrams" +require_text "references/onboarding-knowledge-base.md" "## Completeness Hard Gate" require_text "references/onboarding-knowledge-base.md" "content-bearing onboarding-db document" -require_text "references/onboarding-knowledge-base.md" "must include the required diagram set" +require_text "references/onboarding-knowledge-base.md" "must use diagrams that explain real semantics" require_text "references/onboarding-knowledge-base.md" "Explicitly exempted docs" require_text "references/onboarding-knowledge-base.md" "coverage-matrix.md" require_text "references/onboarding-knowledge-base.md" "batch-review.md" require_text "references/onboarding-knowledge-base.md" "open-questions.md" -require_text "references/onboarding-knowledge-base.md" "Timeline / 时序图 is required by default for module and flow docs" -require_text "references/onboarding-knowledge-base.md" "List state diagrams and timeline/sequence diagrams before optional supporting diagrams" -require_text "references/onboarding-knowledge-base.md" "Sequence and flow diagrams may use Mermaid" -require_text "references/onboarding-knowledge-base.md" "State machine diagrams and complex example diagrams should use ASCII" +require_text "references/onboarding-knowledge-base.md" "primary per-flow narrative" +require_text "references/onboarding-knowledge-base.md" "stateless topics do not require state diagrams" +require_text "references/onboarding-knowledge-base.md" "missing critical slice cannot be averaged away" require_text "references/onboarding-knowledge-base.md" "核心原理" require_text "references/onboarding-knowledge-base.md" "示例图" require_text "references/onboarding-knowledge-base.md" "流程讲解时顺带解释涉及的数据模型" @@ -122,16 +123,17 @@ require_text "references/stage-guides.md" "Onboarding Spec" require_text "references/stage-guides.md" "Onboarding Tasks" require_text "references/stage-guides.md" "Full Execution Gate 确认后 Agent 可以全盘执行" require_text "references/stage-guides.md" "状态图优先" -require_text "references/stage-guides.md" "状态机/决策图" +require_text "references/stage-guides.md" "Flow Slice Coverage" +require_text "references/stage-guides.md" "Completeness Hard Gate" require_text "references/workflow-checklists.md" "default to single long files" require_text "references/workflow-checklists.md" "wireframe" require_text "references/workflow-checklists.md" "batch is not a human gate" require_text "references/workflow-checklists.md" "diagram type" -require_text "references/workflow-checklists.md" "state diagram first" -require_text "references/workflow-checklists.md" "module docs require architecture/boundary + state + timeline/sequence" -require_text "references/workflow-checklists.md" "flow docs require architecture/boundary + state + timeline/sequence" -require_text "references/workflow-checklists.md" "content-bearing onboarding-db document" -require_text "references/workflow-checklists.md" "explicit exemption" +require_text "references/workflow-checklists.md" "Core Flow Inventory" +require_text "references/workflow-checklists.md" "Flow Slice Coverage" +require_text "references/workflow-checklists.md" "Complexity-triggered diagrams" +require_text "references/workflow-checklists.md" "Completeness Hard Gate" +require_text "references/workflow-checklists.md" "stateless glossary" require_text "references/validation-scenarios.md" "Prevent outline-only module docs" require_text "references/validation-scenarios.md" "Module default single-file" require_text "references/validation-scenarios.md" "Flow default single-file" @@ -150,7 +152,7 @@ require_text "templates/onboarding-db/module.md" "每张图必须带讲解" require_text "templates/onboarding-db/module.md" "架构/边界图" require_text "templates/onboarding-db/module.md" "ASCII 架构图" require_text "templates/onboarding-db/module.md" "ASCII 状态机图" -require_text "templates/onboarding-db/module.md" "Timeline / 时序图(必填)" +require_text "templates/onboarding-db/module.md" "Timeline / 时序图(有顺序或数据移动时必填)" require_text "templates/onboarding-db/module.md" "Mermaid sequenceDiagram" require_text "templates/onboarding-db/module.md" "Mermaid flowchart" require_text "templates/onboarding-db/module.md" "## 11. 工作原理与示例" @@ -176,8 +178,8 @@ require_order "templates/onboarding-db/module.md" \ "## 1. 模块定位" \ "## 2. 图解" \ "### 2.1 模块图 / 架构/边界图" \ - "### 2.2 ASCII 状态机图" \ - "### 2.3 Timeline / 时序图(必填)" \ + "### 2.2 ASCII 状态机图(有状态行为时必填)" \ + "### 2.3 Timeline / 时序图(有顺序或数据移动时必填)" \ "### 2.4 流程图(按需)" \ "### 2.5 图解说明" \ "## 3. Bounded Context / DDD 视角" \ @@ -222,9 +224,9 @@ require_text "templates/onboarding-db/evidence-graph.md" "Evidence Readiness Gat require_text "templates/onboarding-db/onboarding-spec.md" "Gateway / Runtime Plan" require_text "templates/onboarding-db/onboarding-spec.md" "Diagram Plan" require_text "templates/onboarding-db/onboarding-spec.md" "状态图优先" -require_text "templates/onboarding-db/onboarding-spec.md" "模块文档默认必须规划架构/边界图、ASCII 状态图、Timeline / 时序图" -require_text "templates/onboarding-db/onboarding-spec.md" "流程文档默认必须规划架构/边界图、ASCII 状态图、Timeline / 时序图" -require_text "templates/onboarding-db/onboarding-spec.md" "Every planned module/flow doc must have Required Architecture/Boundary Diagram, Required ASCII State Diagram, and Required Timeline/Sequence Diagram" +require_text "templates/onboarding-db/onboarding-spec.md" "## Core Flow Selection" +require_text "templates/onboarding-db/onboarding-spec.md" "## Flow Slice Plan" +require_text "templates/onboarding-db/onboarding-spec.md" 'Every planned `critical` / `important` flow doc must have Required Architecture/Boundary Diagram' require_text "templates/onboarding-db/onboarding-spec.md" "Mermaid flowchart / sequenceDiagram" require_text "templates/onboarding-db/onboarding-spec.md" "Exemptions" require_text "templates/onboarding-db/onboarding-spec.md" "Required Timeline / Sequence" @@ -234,9 +236,11 @@ require_text "templates/onboarding-db/onboarding-spec.md" "Spec Acceptance Gate" require_text "templates/onboarding-db/onboarding-spec.md" "Full Execution Gate 另行确认后" require_text "templates/onboarding-db/onboarding-tasks.md" "batch 是 Agent 的组织和 review 单位" require_text "templates/onboarding-db/onboarding-tasks.md" "Full Execution Gate" -require_text "templates/onboarding-db/onboarding-tasks.md" "架构/边界图、ASCII 状态图、Timeline / 时序图" +require_text "templates/onboarding-db/onboarding-tasks.md" "Required Slice IDs" +require_text "templates/onboarding-db/onboarding-tasks.md" "Completeness Hard Gate" require_text "templates/onboarding-db/onboarding-tasks.md" "Mermaid flowchart / sequenceDiagram" -require_text "templates/onboarding-db/README.md" "架构/边界图、ASCII 状态图、Timeline / 时序图" +require_text "templates/onboarding-db/README.md" "Core Flow Overview / Boundary" +require_text "templates/onboarding-db/README.md" "stateless glossary" require_text "templates/onboarding-db/README.md" "Mermaid flowchart / sequenceDiagram" require_text "Usage.md" "架构/边界图、ASCII 状态图、Timeline / 时序图" require_text "Usage.md" "Mermaid flowchart / sequenceDiagram" @@ -250,8 +254,12 @@ require_text "templates/onboarding-db/coverage-matrix.md" "Required diagram set require_text "templates/onboarding-db/coverage-matrix.md" "Architecture diagram clarity" require_text "templates/onboarding-db/coverage-matrix.md" "State diagram clarity" require_text "templates/onboarding-db/coverage-matrix.md" "Timeline / sequence clarity" +require_text "templates/onboarding-db/coverage-matrix.md" "Completeness Hard Gate" +require_text "templates/onboarding-db/coverage-matrix.md" "Core flow discovery completeness" +require_text "templates/onboarding-db/coverage-matrix.md" "Slice and branch coverage" require_text "templates/onboarding-db/batch-review.md" "Score" require_text "templates/onboarding-db/batch-review.md" "Gaps / Unknowns" +require_text "templates/onboarding-db/batch-review.md" "Completeness Hard Gate" require_absent_text "references/onboarding-knowledge-base.md" "Build Learning Skeleton" require_absent_text "references/onboarding-knowledge-base.md" "stable skeleton" diff --git a/tests/validate-human-help-version-docs.sh b/tests/validate-human-help-version-docs.sh index 6e63558..4b2962b 100755 --- a/tests/validate-human-help-version-docs.sh +++ b/tests/validate-human-help-version-docs.sh @@ -20,7 +20,7 @@ assert_contains "SKILL.md" "use \`README.md\` for high-level overview, install, assert_contains "SKILL.md" "read that version section first" assert_contains "Usage.md" "### 我想知道版本更新或用法" -assert_contains "Usage.md" "1.2.4 更新了什么?" +assert_contains "Usage.md" "1.3.0 更新了什么?" assert_contains "Usage.md" "和 1.2.2 比有什么变化?" assert_contains "Usage.md" "现在 agent-loop 怎么用?" assert_contains "Usage.md" "CHANGELOG.md" diff --git a/tests/validate-onboarding-core-flow-completeness.sh b/tests/validate-onboarding-core-flow-completeness.sh new file mode 100644 index 0000000..9029423 --- /dev/null +++ b/tests/validate-onboarding-core-flow-completeness.sh @@ -0,0 +1,183 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +assert_file_exists() { + local path="$1" + if [[ ! -f "$ROOT/$path" ]]; then + echo "missing file: $path" >&2 + exit 1 + fi +} + +assert_contains() { + local path="$1" + local expected="$2" + if ! grep -Fq "$expected" "$ROOT/$path"; then + echo "missing text in $path: $expected" >&2 + exit 1 + fi +} + +assert_not_contains() { + local path="$1" + local forbidden="$2" + if grep -Fq "$forbidden" "$ROOT/$path"; then + echo "forbidden text in $path: $forbidden" >&2 + exit 1 + fi +} + +assert_file_exists "docs/proposal/v1.3.x/onboarding-core-flow-completeness.md" +assert_file_exists "references/onboarding-knowledge-base.md" +assert_file_exists "templates/onboarding-db/evidence-graph.md" +assert_file_exists "templates/onboarding-db/onboarding-spec.md" +assert_file_exists "templates/onboarding-db/onboarding-tasks.md" +assert_file_exists "templates/onboarding-db/flow.md" +assert_file_exists "templates/onboarding-db/coverage-matrix.md" +assert_file_exists "templates/onboarding-db/batch-review.md" + +# Controller and published design/runtime contract. +assert_contains "SKILL.md" "Core Flow Inventory" +assert_contains "SKILL.md" "Flow Slice Coverage" +assert_not_contains "SKILL.md" "wireframe architecture flow diagrams as the preferred flow expression" +assert_contains "references/design.md" "Core Flow Completeness Invariant" +assert_contains "references/design.md" "A missing critical slice cannot be averaged away" +assert_contains "references/runtime.md" "Core Flow Inventory selection" +assert_contains "references/runtime.md" "exactly two onboarding Human Gates" + +# Detailed behavior source. +assert_contains "references/onboarding-knowledge-base.md" "## Core Flow Inventory" +assert_contains "references/onboarding-knowledge-base.md" "## Flow Slice Coverage" +assert_contains "references/onboarding-knowledge-base.md" "## Core Flow Diagram Set" +assert_contains "references/onboarding-knowledge-base.md" "## Complexity-Triggered Diagrams" +assert_contains "references/onboarding-knowledge-base.md" "## Completeness Hard Gate" +assert_contains "references/onboarding-knowledge-base.md" "primary per-flow narrative" +assert_contains "references/onboarding-knowledge-base.md" '`critical` / `important`' +assert_contains "references/onboarding-knowledge-base.md" "supporting" +assert_contains "references/onboarding-knowledge-base.md" "stateless" +assert_contains "references/onboarding-knowledge-base.md" "Flow ID" +assert_contains "references/onboarding-knowledge-base.md" "Slice ID" +assert_contains "references/onboarding-knowledge-base.md" "Diagram ID" + +# Stage/checklist alignment and no additional batch gate. +assert_contains "references/stage-guides.md" "Build Core Flow Inventory" +assert_contains "references/stage-guides.md" "Completeness Hard Gate" +assert_not_contains "references/stage-guides.md" "before accepting the Onboarding Spec, current batch, or newcomer-ready claim" +assert_contains "references/workflow-checklists.md" "Core Flow Inventory" +assert_contains "references/workflow-checklists.md" "Flow Slice Coverage" +assert_contains "references/workflow-checklists.md" "Complexity-triggered diagrams" +assert_contains "references/workflow-checklists.md" "Completeness Hard Gate" + +# Template contracts. +assert_contains "templates/onboarding-db/evidence-graph.md" "## Core Flow Inventory" +assert_contains "templates/onboarding-db/evidence-graph.md" "| Flow ID |" +assert_contains "templates/onboarding-db/evidence-graph.md" "Success Terminal" +assert_contains "templates/onboarding-db/evidence-graph.md" "Failure Terminals" +assert_contains "templates/onboarding-db/evidence-graph.md" "Selection Reason" +assert_contains "templates/onboarding-db/onboarding-spec.md" "## Core Flow Selection" +assert_contains "templates/onboarding-db/onboarding-spec.md" "## Flow Slice Plan" +assert_contains "templates/onboarding-db/onboarding-spec.md" "Complexity Signals" +assert_contains "templates/onboarding-db/onboarding-spec.md" "Covered Slice IDs" +assert_contains "templates/onboarding-db/onboarding-tasks.md" "Required Slice IDs" +assert_contains "templates/onboarding-db/onboarding-tasks.md" "Completeness Hard Gate" +assert_contains "templates/onboarding-db/flow.md" "## 1. Flow Identity And Outcomes" +assert_contains "templates/onboarding-db/flow.md" "## 2. Flow Slice Coverage" +assert_contains "templates/onboarding-db/flow.md" "Diagram ID" +assert_contains "templates/onboarding-db/flow.md" "Covered Slice IDs" +assert_contains "templates/onboarding-db/flow.md" "primary per-flow narrative" +assert_contains "templates/onboarding-db/flow.md" "critical / important flow" +assert_contains "templates/onboarding-db/flow.md" "supporting flow" +assert_contains "templates/onboarding-db/flow.md" "ERD / Model Relationship" +assert_contains "templates/onboarding-db/flow.md" "Runtime / Deployment Topology" +assert_contains "templates/onboarding-db/flow.md" "Observability / Troubleshooting Map" +assert_contains "templates/onboarding-db/coverage-matrix.md" "## Completeness Hard Gate" +assert_contains "templates/onboarding-db/coverage-matrix.md" "Core flow discovery completeness" +assert_contains "templates/onboarding-db/coverage-matrix.md" "Slice and branch coverage" +assert_contains "templates/onboarding-db/coverage-matrix.md" "Evidence granularity" +assert_contains "templates/onboarding-db/coverage-matrix.md" "Consistency / gateway risk" +assert_not_contains "templates/onboarding-db/coverage-matrix.md" "且人类确认新人可读" +assert_contains "templates/onboarding-db/batch-review.md" "## Completeness Hard Gate" +assert_contains "templates/onboarding-db/batch-review.md" "Core flow discovery completeness" +assert_contains "templates/onboarding-db/batch-review.md" "Slice and branch coverage" +assert_contains "templates/onboarding-db/batch-review.md" "Evidence granularity" +assert_contains "templates/onboarding-db/batch-review.md" "Consistency / gateway risk" +assert_not_contains "references/stage-guides.md" "making a newcomer-ready claim" +assert_contains "references/onboarding-knowledge-base.md" "Failure / recovery" +assert_contains "references/onboarding-knowledge-base.md" "Troubleshooting" +assert_not_contains "templates/onboarding-db/onboarding-spec.md" "| Review Gate |" +assert_not_contains "templates/onboarding-db/batch-review.md" "## Human Review Status" + +# Semantic pressure scenarios and human-facing behavior. +assert_contains "references/validation-scenarios.md" "missing callback and reconciliation slices" +assert_contains "references/validation-scenarios.md" "diagrams detached from Flow Slice Coverage" +assert_contains "references/validation-scenarios.md" "average away a missing critical slice" +assert_contains "references/validation-scenarios.md" "exactly two onboarding Human Gates" +assert_contains "Usage.md" "核心流程完整性" +assert_contains "Usage.md" "Timeline / Sequence" +assert_contains "CHANGELOG.md" "Core Flow Completeness" + +# Executable artifact-level contract. +assert_file_exists "scripts/check-onboarding-core-flow-coverage.rb" +assert_file_exists "examples/ai-meeting-minutes-backend/onboarding-db/08-review/evidence-graph.md" +assert_file_exists "examples/ai-meeting-minutes-backend/onboarding-db/onboarding-spec.md" +assert_file_exists "examples/ai-meeting-minutes-backend/onboarding-db/onboarding-tasks.md" +assert_file_exists "examples/ai-meeting-minutes-backend/onboarding-db/03-flows/order-payment.md" +assert_file_exists "examples/ai-meeting-minutes-backend/onboarding-db/coverage-matrix.md" +assert_file_exists "examples/ai-meeting-minutes-backend/onboarding-db/batch-review.md" +assert_file_exists "tests/fixtures/onboarding-core-flow/invalid-missing-recovery/evidence-graph.md" +assert_file_exists "tests/fixtures/onboarding-core-flow/invalid-missing-recovery/onboarding-spec.md" +assert_file_exists "tests/fixtures/onboarding-core-flow/invalid-missing-recovery/onboarding-tasks.md" +assert_file_exists "tests/fixtures/onboarding-core-flow/invalid-missing-recovery/flow.md" +assert_file_exists "tests/fixtures/onboarding-core-flow/invalid-missing-recovery/coverage-matrix.md" +assert_file_exists "tests/fixtures/onboarding-core-flow/invalid-missing-recovery/batch-review.md" +assert_file_exists "tests/fixtures/onboarding-core-flow/invalid-detached-trace/evidence-graph.md" +assert_file_exists "tests/fixtures/onboarding-core-flow/invalid-detached-trace/onboarding-spec.md" +assert_file_exists "tests/fixtures/onboarding-core-flow/invalid-detached-trace/onboarding-tasks.md" +assert_file_exists "tests/fixtures/onboarding-core-flow/invalid-detached-trace/flow.md" +assert_file_exists "tests/fixtures/onboarding-core-flow/invalid-detached-trace/coverage-matrix.md" +assert_file_exists "tests/fixtures/onboarding-core-flow/invalid-detached-trace/batch-review.md" +assert_file_exists "tests/fixtures/onboarding-core-flow/valid-deferred/evidence-graph.md" +assert_file_exists "tests/fixtures/onboarding-core-flow/valid-deferred/onboarding-spec.md" +assert_file_exists "tests/fixtures/onboarding-core-flow/valid-deferred/onboarding-tasks.md" +assert_file_exists "tests/fixtures/onboarding-core-flow/valid-deferred/coverage-matrix.md" +assert_file_exists "tests/fixtures/onboarding-core-flow/valid-deferred/batch-review.md" + +ruby "$ROOT/scripts/check-onboarding-core-flow-coverage.rb" \ + "$ROOT/examples/ai-meeting-minutes-backend/onboarding-db" + +invalid_output="$(mktemp)" +trap 'rm -f "$invalid_output"' EXIT +if ruby "$ROOT/scripts/check-onboarding-core-flow-coverage.rb" \ + "$ROOT/tests/fixtures/onboarding-core-flow/invalid-missing-recovery" \ + >"$invalid_output" 2>&1; then + echo "invalid fixture unexpectedly passed" >&2 + exit 1 +fi + +if ! grep -Fq "missing required slice: CF-ORDER-PAYMENT/S07" "$invalid_output"; then + echo "invalid fixture failed for the wrong reason" >&2 + cat "$invalid_output" >&2 + exit 1 +fi + +detached_output="$(mktemp)" +trap 'rm -f "$invalid_output" "$detached_output"' EXIT +if ruby "$ROOT/scripts/check-onboarding-core-flow-coverage.rb" \ + "$ROOT/tests/fixtures/onboarding-core-flow/invalid-detached-trace" \ + >"$detached_output" 2>&1; then + echo "detached-trace fixture unexpectedly passed" >&2 + exit 1 +fi + +if ! grep -Fq "missing diagram definition: D-RECOVERY" "$detached_output"; then + echo "detached-trace fixture failed for the wrong reason" >&2 + cat "$detached_output" >&2 + exit 1 +fi + +ruby "$ROOT/scripts/check-onboarding-core-flow-coverage.rb" \ + "$ROOT/tests/fixtures/onboarding-core-flow/valid-deferred" + +echo "PASS: onboarding core-flow completeness contract is complete" diff --git a/tests/validate-project-local-skills.sh b/tests/validate-project-local-skills.sh index 4af2ba4..ea0cfab 100755 --- a/tests/validate-project-local-skills.sh +++ b/tests/validate-project-local-skills.sh @@ -89,7 +89,7 @@ assert_contains "templates/root-AGENTS.md" "project-skill-management" assert_contains "templates/root-AGENTS.md" "Project Skill Creation / Update" assert_contains "templates/root-AGENTS.md" ".agent-loop/skills/INDEX.md" assert_contains "templates/root-AGENTS.md" "Execution Gate" -assert_contains "templates/root-AGENTS.md" "block-version:1.2.4-20260711.3" +assert_contains "templates/root-AGENTS.md" "block-version:1.3.0-20260711" assert_contains "templates/project.md" "Project Skills" assert_contains "templates/project.md" ".agent-loop/skills/INDEX.md" assert_contains "templates/project-skills/validation.md" "Validated Content Manifest" diff --git a/tests/validate-requirement-lifecycle-backlog.sh b/tests/validate-requirement-lifecycle-backlog.sh index 76b2ee0..e595e69 100755 --- a/tests/validate-requirement-lifecycle-backlog.sh +++ b/tests/validate-requirement-lifecycle-backlog.sh @@ -85,11 +85,12 @@ assert_contains "README.md" "Requirement Lifecycle / Backlog" assert_contains "Usage.md" "需求待办" assert_contains "Usage.md" "当前恢复动作" assert_contains "CHANGELOG.md" "Requirement Lifecycle / Backlog" -assert_contains "SKILL.md" "Version: 1.2.4" -assert_contains "README.md" "**Current version:** 1.2.4" -assert_contains "Usage.md" "**版本:** 1.2.4" -assert_contains "plugin.json" '"version": "1.2.4"' -assert_contains "templates/root-AGENTS.md" "block-version:1.2.4-20260711.3" +assert_contains "SKILL.md" "Version: 1.3.0" +assert_contains "README.md" "**Current version:** 1.3.0" +assert_contains "Usage.md" "**版本:** 1.3.0" +assert_contains "plugin.json" '"version": "1.3.0"' +assert_contains "templates/root-AGENTS.md" "block-version:1.3.0-20260711" +assert_contains "CHANGELOG.md" "## 1.3.0 — 2026-07-11" assert_not_contains "templates/root-AGENTS.md" "## Agent Loop Guidance Version" assert_contains "AGENTS.md" 'ignore the `alpha` prefix for version records' diff --git a/tests/validate-root-agents-block-checker.sh b/tests/validate-root-agents-block-checker.sh index d7e6db4..97c1331 100755 --- a/tests/validate-root-agents-block-checker.sh +++ b/tests/validate-root-agents-block-checker.sh @@ -71,14 +71,14 @@ fi assert_contains "$tmpdir/missing-stage-map.out" "FAIL root AGENTS drift found" assert_contains "$tmpdir/missing-stage-map.out" "workflow-stage-map | missing" -sed 's/block-version:1\.2\.4-20260711\.3/block-version:1.2.4/' "$template" > "$tmpdir/stale.md" +sed 's/block-version:1\.3\.0-20260711/block-version:1.3.0/' "$template" > "$tmpdir/stale.md" if "$checker" --template "$template" --target "$tmpdir/stale.md" > "$tmpdir/stale.out"; then printf 'FAIL: checker should fail when block-version values are stale\n' >&2 cat "$tmpdir/stale.out" >&2 exit 1 fi assert_contains "$tmpdir/stale.out" "message-intent | stale-block-version" -assert_contains "$tmpdir/stale.out" "expected 1.2.4-20260711.3" +assert_contains "$tmpdir/stale.out" "expected 1.3.0-20260711" awk ' // { next } @@ -94,7 +94,7 @@ assert_contains "$tmpdir/broken.out" "ownership | broken-markers" awk ' /" + print "" print "nested" print "" inserted = 1 @@ -112,7 +112,7 @@ assert_contains "$tmpdir/nested.out" "ownership | nested-managed-block" awk ' { print } // && inserted != 1 { - print "" + print "" print "duplicate" print "" inserted = 1 @@ -127,7 +127,7 @@ assert_contains "$tmpdir/duplicate.out" "ownership | duplicate-section" { cat "$template" - printf '\n\n' + printf '\n\n' printf '## Legacy Extra\n\n' printf '\n' } > "$tmpdir/extra.md" @@ -176,7 +176,7 @@ assert_contains "$tmpdir/bare-end.out" "malformed-marker" { cat "$template" - printf '\n\n' + printf '\n\n' } > "$tmpdir/same-line.md" if "$checker" --template "$template" --target "$tmpdir/same-line.md" > "$tmpdir/same-line.out"; then printf 'FAIL: checker should fail when multiple managed markers are on the same line\n' >&2 diff --git a/tests/validate-root-agents-block-refresh.sh b/tests/validate-root-agents-block-refresh.sh index 65c5618..e948676 100755 --- a/tests/validate-root-agents-block-refresh.sh +++ b/tests/validate-root-agents-block-refresh.sh @@ -23,7 +23,7 @@ assert_not_contains() { managed_count=$(grep -c '^ + ## Bootstrap Protocol Before development work: @@ -29,7 +29,7 @@ Before development work: 15. Classify the current `agent-loop` stage and recommend exactly one next action. - + ## Agent Ownership - Own workflow diagnosis, sequencing, implementation, verification, review, drift checks, and project-memory updates. @@ -43,13 +43,13 @@ Before development work: - For non-trivial confirmations, present a table-first Human Review Summary before asking approval. - + ## Message Intent Guard Before project-state routing, classify the latest human message intent. - `chat`: ordinary discussion, rule questions, status questions, or design talk. Answer or discuss only; do not create requirement sets, feature workspaces, tasks, tests, or plans. -- `requirements-discussion`: the human is exploring product needs, business goals, capability ideas, constraints, tradeoffs, or user scenarios without authorizing implementation. Requirements discussion must shape demand through Brainstorm / Clarify into a human-reviewed requirement document under `.agent-loop/requirements/` before feature construction. +- `requirements-discussion`: the human is exploring product needs, business goals, capability ideas, constraints, tradeoffs, or user scenarios without authorizing implementation. Requirements discussion must shape demand through Brainstorm / Clarify into a human-reviewed requirement document under `.agent-loop/requirements/` before feature construction. When product concept identity, lifecycle, relationship, state, ownership, or fact meaning can change downstream models, use the internal Concept Foundation Gate before flow/state/product-data modeling. - `project-skill-management`: the human asks to turn a repeatable project workflow into a project-local skill, or to update, disable, or deprecate one. Route to Project Skill Creation / Update after reliable Project Entry/memory. - `feature-request`: the human explicitly asks to implement, build, change behavior, or start work from accepted requirements. Route through normal agent-loop feature workflow. @@ -58,7 +58,7 @@ Message intent is not permanent. If chat turns into product demand, reclassify a If unclear whether the human wants chat or requirements discussion, ask whether to keep discussing or shape the topic into a requirements document. If unclear whether the human wants requirements discussion or implementation, ask whether to form a requirements document first or start feature construction. - + ## Workflow Stage Map Use this after Bootstrap Protocol and Message Intent Guard. Select exactly one next stage from the current human signal and project state. When multiple signals match, apply this first-match order: Safety Stop -> Remote Discovery -> Memory Recovery -> Active Feature Guard -> Blocker Resolution -> Intent Routing -> Normal Stage Continuation. After selecting a stage, load the matching `references/...` file from the `agent-loop` skill package before acting. This map is navigation only; do not treat root `AGENTS.md` as the detailed stage procedure. @@ -70,7 +70,7 @@ Use this after Bootstrap Protocol and Message Intent Guard. Select exactly one n | Human or local evidence says the source of truth is remote, SSH, container, tunnel, or devcontainer | Remote Project Discovery | `references/remote-project-discovery.md` | | Existing memory conflicts with code reality, or work bypassed the loop | Re-Adopt Agent Loop Project | `references/recovery-and-backfill.md` | | Human asks to turn a repeatable workflow into a project-local skill, or manage an existing one | Project Skill Creation / Update | `references/project-skills.md`, `references/skill-routing.md`, `references/external-skill-adapters.md` | -| Product need, business goal, scope, constraint, scenario, or phased delivery is still being shaped | Requirements Discussion | `references/requirement-management.md`; also `references/requirement-product-grill.md` when terminology, roles, flows, exceptions, prior behavior, or decision signals are unclear | +| Product need, business goal, scope, constraint, scenario, concept identity/lifecycle, or phased delivery is still being shaped | Requirements Discussion | `references/requirement-management.md`; also `references/requirement-product-grill.md` for Concept Foundation, terminology, roles, flows, exceptions, prior behavior, or decision signals | | Human confirms recording, accepting, or deferring a requirement source | Requirement Archive | `references/requirement-management.md`, `references/stage-guides.md` | | Human requests durable newcomer docs after Project Entry or reliable memory exists | Evidence-Graph + DDD Onboarding | `references/onboarding-knowledge-base.md` | | Accepted requirement needs shared business-flow, domain, data, architecture, reliability, performance, security, or cross-feature design before feature specification | Decision & Design If Needed | `references/project-decisions.md` | @@ -99,7 +99,7 @@ Use this after Bootstrap Protocol and Message Intent Guard. Select exactly one n | Ordinary question or discussion has no artifact or implementation intent | Chat Entry | `references/runtime.md` only when intent is unclear; otherwise answer without creating workflow artifacts | - + ## Gate Modes - Strict Mode is the default: ask before and after every stage. @@ -109,12 +109,13 @@ Use this after Bootstrap Protocol and Message Intent Guard. Select exactly one n - If repeated confirmations slow the human down, proactively explain Feature Auto-Loop and Task Auto-Run, then ask before enabling either mode. - + ## Required Stops Stop and ask when: - scope changes or requirements are ambiguous +- an unresolved Concept Foundation remains `candidate` / `reopened`, or a downstream product/feature artifact would redefine accepted product semantics - product, design, architecture, security, data, approval, or public-interface decisions are unclear - a stage would modify human original requirements - tests require unavailable infrastructure @@ -136,7 +137,7 @@ Stop and ask when: Auto modes do not bypass these stops. - + ## Completion Rules - Before completion claims, run fresh verification and record evidence. @@ -148,7 +149,7 @@ Auto modes do not bypass these stops. - When a feature references accepted Decision & Design records, Feature Close Review and Feature Completion Check must verify assigned design slices and evidence; divergence returns to Decision & Design / Drift Check before close. - + ## Submit And Commit Rules - Submit, commit, PR, merge, release, and publish require explicit human confirmation after diff, verification, review, drift, and unrelated-change checks. @@ -160,13 +161,14 @@ Auto modes do not bypass these stops. - For the `agent-loop` skill repository itself, use `(v): ` and a 3-7 bullet body for meaningful commits. - + ## Project Memory And Artifacts - Resolve project-memory and feature paths relative to the active memory root: `.agent-loop/` by default, or legacy `agent-loop/` for the current run. - Keep task status, execution evidence, feature notes, and project memory inside `.agent-loop/`. - Keep long-term project memory in `.agent-loop/project.md` or enterprise `.agent-loop/project/*.md`. Root `AGENTS.md` should only summarize startup-critical facts that every Agent CLI needs immediately. - Keep original human materials in requirement set directories under `.agent-loop/requirements/`, or reference original paths when the human declines copying. +- Keep accepted Concept Foundation and Requirement Product Model detail in the effective human-reviewed requirement source named by the requirement README. After archive, preserve prior sources and record confirmed semantic changes in an append-only Concept Foundation follow-up or a new requirement set. Feature `product.md` and `spec.md` cite accepted Concept/Model IDs and `Effective Concept Source` instead of redefining product identity, lifecycle, relationships, invariants, states, or fact meaning. - Do not create new flat files directly under `.agent-loop/requirements/`; group requirements, prototypes, feedback, screenshots, recordings, links, and follow-up notes for the same intake/topic together. - For complex requirements, suggest `Delivery Phases` in the requirement set `README.md` before feature construction when the human needs to confirm staged delivery. A phase is a human-readable delivery slice, not a feature workspace, task, or plan. - Future/deferred work and backlog items belong in requirement sets and optional `requirements/INDEX.md`, not in `project.md`. Do not edit `requirement.md` or other source files for lifecycle/status updates. @@ -175,13 +177,13 @@ Auto modes do not bypass these stops. - Do not write task logs, feature progress, raw requirements, temporary plans, or test transcripts into `AGENTS.md`. - + ## Architecture Snapshot Add only startup-critical architecture boundaries that every future agent must know immediately. If the project has `ARCHITECTURE.md`, this block may use `source:ARCHITECTURE.md` instead. Keep details in `ARCHITECTURE.md`, `.agent-loop/project.md`, or enterprise `.agent-loop/project/*.md`. - + ## Directory Guidance - Directory-level `AGENTS.md` files are for long-lived boundary rules only. @@ -189,7 +191,7 @@ Add only startup-critical architecture boundaries that every future agent must k - Do not create directory-level `AGENTS.md` for ordinary component, utility, temporary, or feature implementation folders. - + ## Project Commands ```bash @@ -199,7 +201,7 @@ Add only startup-critical architecture boundaries that every future agent must k ``` - + ## Project-Specific Hard Constraints Add only stable constraints that every future agent must know at startup. diff --git a/templates/spec.md b/templates/spec.md index 796e295..08b1cd7 100644 --- a/templates/spec.md +++ b/templates/spec.md @@ -23,6 +23,24 @@ Summary: - +## Accepted Concept References + +Concept Foundation Status: accepted | concept-foundation-not-needed +Source Requirement: +Effective Concept Source: + +| Concept ID | Canonical Name | Feature Use | Source Definition / Trace | +|---|---|---|---| +| C-EXAMPLE | | | requirement.md#concept-definitions / TRACE-01 | + +Do not redefine accepted product meaning in this Feature Spec. Return to Requirements Discussion if identity, owner, relationship, lifecycle, invariant, state, terminal meaning, or product fact must change. + +## Requirement Product Model Trace + +| Requirement Model ID | Concept / Action / Flow / State IDs | Feature Behavior / Story | Acceptance / Verification Direction | Coverage | +|---|---|---|---|---| +| PM-01 | C-EXAMPLE / CMD-01 / FLOW-01 / STATE-01 | US1 | | planned / covered / out-of-scope | + ## Maintenance Fix Scope Use this section only when `Feature Type: maintenance-fix`. diff --git a/tests/fixtures/concept-foundation/invalid-detached-model/product.md b/tests/fixtures/concept-foundation/invalid-detached-model/product.md new file mode 100644 index 0000000..b40402e --- /dev/null +++ b/tests/fixtures/concept-foundation/invalid-detached-model/product.md @@ -0,0 +1,15 @@ +# Product Brief: Approval Instance + +Concept Foundation Status: accepted + +## Accepted Concept References + +| Concept ID | Canonical Name | Product Brief Use | Source Definition / Trace | +|---|---|---|---| +| C-APPROVAL-INSTANCE | Approval Instance | workflow | TRACE-01 | + +## Requirement Product Model Coverage + +| Requirement Model ID | Concept IDs | Feature Product Journey / Story | +|---|---|---| +| PM-99 | C-APPROVAL-INSTANCE | approval journey | diff --git a/tests/fixtures/concept-foundation/invalid-detached-model/requirement.md b/tests/fixtures/concept-foundation/invalid-detached-model/requirement.md new file mode 100644 index 0000000..c908d51 --- /dev/null +++ b/tests/fixtures/concept-foundation/invalid-detached-model/requirement.md @@ -0,0 +1,61 @@ +# Requirement: Approval Instance + +Concept Foundation Status: accepted +Not-Needed Reason: n/a + +## Concept Foundation + +Approval Instance is distinct from the approval action. + +## Concept Definitions + +| Concept ID | Canonical Name | +|---|---| +| C-APPROVAL-INSTANCE | Approval Instance | +| C-REVIEWER | Reviewer | + +## Concept Relationships + +| Relationship ID | From Concept ID | Relationship | To Concept ID | +|---|---|---|---| +| REL-01 | C-REVIEWER | reviews | C-APPROVAL-INSTANCE | + +### Human Confirmation + +- Confirmed Concept IDs: C-APPROVAL-INSTANCE, C-REVIEWER + +## Role / Permission Matrix + +| Role Concept ID | Product Object Concept ID | Read | +|---|---|---| +| C-REVIEWER | C-APPROVAL-INSTANCE | yes | + +## Commands / Events + +| Action ID | Actor / Producer Concept ID | Target Concept ID | +|---|---|---| +| CMD-APPROVE | C-REVIEWER | C-APPROVAL-INSTANCE | + +## Primary Business Flow + +| Flow Step ID | Actor Concept ID | Action ID | Input / Target Concept IDs | +|---|---|---|---| +| FLOW-01 | C-REVIEWER | CMD-APPROVE | C-APPROVAL-INSTANCE | + +## Product State Model + +| State Model ID | State-bearing Concept ID | Action / Event ID | +|---|---|---| +| STATE-01 | C-APPROVAL-INSTANCE | CMD-APPROVE | + +## Requirement Product Model + +| Product Model ID | Concept IDs | Product Fact Meaning | +|---|---|---| +| PM-01 | C-APPROVAL-INSTANCE, C-REVIEWER | accepted approval lifecycle | + +## Concept-To-Product Traceability + +| Trace ID | Accepted Concept IDs | Derived Model IDs / Sections | +|---|---|---| +| TRACE-01 | C-APPROVAL-INSTANCE, C-REVIEWER | REL-01 / CMD-APPROVE / FLOW-01 / STATE-01 / PM-01 | diff --git a/tests/fixtures/concept-foundation/invalid-detached-model/spec.md b/tests/fixtures/concept-foundation/invalid-detached-model/spec.md new file mode 100644 index 0000000..11dbf52 --- /dev/null +++ b/tests/fixtures/concept-foundation/invalid-detached-model/spec.md @@ -0,0 +1,15 @@ +# Feature Spec: Approval Instance + +Concept Foundation Status: accepted + +## Accepted Concept References + +| Concept ID | Canonical Name | Feature Use | Source Definition / Trace | +|---|---|---|---| +| C-APPROVAL-INSTANCE | Approval Instance | workflow | TRACE-01 | + +## Requirement Product Model Trace + +| Requirement Model ID | Concept / Action / Flow / State IDs | Feature Behavior / Story | +|---|---|---| +| PM-01 | C-APPROVAL-INSTANCE / CMD-APPROVE / FLOW-01 / STATE-01 | US1 | diff --git a/tests/fixtures/concept-foundation/invalid-unaccepted/product.md b/tests/fixtures/concept-foundation/invalid-unaccepted/product.md new file mode 100644 index 0000000..5c72591 --- /dev/null +++ b/tests/fixtures/concept-foundation/invalid-unaccepted/product.md @@ -0,0 +1,9 @@ +# Product Brief: Unaccepted Approval + +Concept Foundation Status: candidate + +## Accepted Concept References + +| Concept ID | Canonical Name | Product Brief Use | Source Definition / Trace | +|---|---|---|---| +| C-APPROVAL | Approval | workflow | requirement.md | diff --git a/tests/fixtures/concept-foundation/invalid-unaccepted/requirement.md b/tests/fixtures/concept-foundation/invalid-unaccepted/requirement.md new file mode 100644 index 0000000..ad92654 --- /dev/null +++ b/tests/fixtures/concept-foundation/invalid-unaccepted/requirement.md @@ -0,0 +1,8 @@ +# Requirement: Unaccepted Approval + +Concept Foundation Status: candidate +Not-Needed Reason: n/a + +## Concept Foundation + +Candidate concepts exist, but the human has not confirmed their meaning. diff --git a/tests/fixtures/concept-foundation/invalid-unaccepted/spec.md b/tests/fixtures/concept-foundation/invalid-unaccepted/spec.md new file mode 100644 index 0000000..05d83ef --- /dev/null +++ b/tests/fixtures/concept-foundation/invalid-unaccepted/spec.md @@ -0,0 +1,9 @@ +# Feature Spec: Unaccepted Approval + +Concept Foundation Status: candidate + +## Accepted Concept References + +| Concept ID | Canonical Name | Feature Use | Source Definition / Trace | +|---|---|---|---| +| C-APPROVAL | Approval | workflow | requirement.md | diff --git a/tests/validate-concept-foundation-requirement-modeling.sh b/tests/validate-concept-foundation-requirement-modeling.sh new file mode 100644 index 0000000..538cc91 --- /dev/null +++ b/tests/validate-concept-foundation-requirement-modeling.sh @@ -0,0 +1,149 @@ +#!/usr/bin/env bash +set -euo pipefail + +root=$(cd "$(dirname "$0")/.." && pwd) + +assert_contains() { + local file=$1 + local text=$2 + if ! grep -Fq -- "$text" "$root/$file"; then + printf 'FAIL: %s missing required text: %s\n' "$file" "$text" >&2 + exit 1 + fi +} + +assert_not_contains() { + local file=$1 + local text=$2 + if grep -Fq -- "$text" "$root/$file"; then + printf 'FAIL: %s contains forbidden text: %s\n' "$file" "$text" >&2 + exit 1 + fi +} + +assert_before() { + local file=$1 + local first=$2 + local second=$3 + ruby -e ' + content = File.read(ARGV.fetch(0)) + first = ARGV.fetch(1) + second = ARGV.fetch(2) + first_at = content.index(first) + second_at = content.index(second) + abort "FAIL: #{ARGV.fetch(0)} missing ordering marker: #{first}" unless first_at + abort "FAIL: #{ARGV.fetch(0)} missing ordering marker: #{second}" unless second_at + abort "FAIL: #{ARGV.fetch(0)} must place #{first} before #{second}" unless first_at < second_at + ' "$root/$file" "$first" "$second" +} + +# Published controller/design/runtime placement and gate contract. +assert_contains "SKILL.md" "Concept Foundation" +assert_contains "SKILL.md" "Requirement Product Model" +assert_contains "references/design.md" "Concept Foundation is an internal Requirements Discussion / Requirement Product Grill method, not a canonical stage" +assert_contains "references/runtime.md" "## Concept Foundation Routing" +assert_contains "references/runtime.md" "## Human Grill Contract" +assert_before "references/runtime.md" "inspect available evidence" "extract candidate concepts" +assert_before "references/runtime.md" "extract candidate concepts" "present one recommended definition" +assert_before "references/runtime.md" "present one recommended definition" "ask exactly one downstream-blocking question" +assert_contains "references/runtime.md" 'candidate | accepted | reopened | concept-foundation-not-needed' +assert_contains "references/runtime.md" 'Do not enter Business Flow, State Model, or Product Data Model while a triggered Concept Foundation is `candidate` or `reopened`.' + +# Phase 1: evidence-first candidates, single Human Grill question, and lightweight bypass. +assert_contains "references/requirement-product-grill.md" "## Concept Foundation" +assert_contains "references/requirement-product-grill.md" "## Human Grill Contract" +assert_contains "references/requirement-product-grill.md" "Concept Candidate Inventory" +assert_contains "references/requirement-product-grill.md" "Concept ID" +assert_contains "references/requirement-product-grill.md" "concept-foundation-not-needed" +assert_contains "references/requirement-product-grill.md" "one downstream-blocking question" +assert_contains "references/requirement-management.md" "Concept Foundation Status" +assert_contains "references/requirement-management.md" "Effective Concept Foundation" +assert_contains "references/requirement-management.md" "append-only Concept Foundation follow-up" +assert_contains "references/stage-guides.md" "Concept Foundation Gate" +assert_contains "references/workflow-checklists.md" "## Concept Foundation Gate" +assert_contains "templates/requirement-set-README.md" "## Effective Concept Foundation" +assert_contains "templates/requirement-set-README.md" "Effective Source:" + +# Phase 1/2 requirement document order and product-model derivation. +assert_before "references/document-templates.md" "## Concept Foundation" "## Primary Business Flow" +assert_before "references/document-templates.md" "## Concept Definitions" "## Concept Relationships" +assert_before "references/document-templates.md" "## Concept Relationships" "## Product State Model" +assert_before "references/document-templates.md" "## Product State Model" "## Requirement Product Model" +assert_contains "references/document-templates.md" "## Concept Candidate Inventory" +assert_contains "references/document-templates.md" "## Concept Definitions" +assert_contains "references/document-templates.md" "## Concept Relationships" +assert_contains "references/document-templates.md" "## Role / Permission Matrix" +assert_contains "references/document-templates.md" "## Commands / Events" +assert_contains "references/document-templates.md" "## Product State Model" +assert_contains "references/document-templates.md" "## Requirement Product Model" +assert_contains "references/document-templates.md" "## Concept-To-Product Traceability" +assert_contains "references/document-templates.md" "Human Confirmation" + +# Phase 2 consumers cite accepted product semantics instead of redefining them. +assert_contains "references/product-brief.md" "Accepted Concept Foundation" +assert_contains "references/product-brief.md" "Product Brief consumes the accepted Requirement Product Model" +assert_contains "templates/product.md" "## Accepted Concept References" +assert_contains "templates/product.md" "## Requirement Product Model Coverage" +assert_contains "templates/spec.md" "## Accepted Concept References" +assert_contains "templates/spec.md" "## Requirement Product Model Trace" +assert_contains "references/project-decisions.md" "PRD / Requirement Product Model owns product meaning" +assert_contains "references/project-decisions.md" "ADR consumes accepted product semantics" + +# Root navigation and stop behavior remain coordinated without a new stage. +assert_contains "templates/root-AGENTS.md" "unresolved Concept Foundation" +assert_contains "references/project-guidance.md" "Concept Foundation Gate" +assert_contains "README.md" "Concept Foundation" +assert_contains "Usage.md" "一次只确认一个真正阻塞后续模型的问题" +assert_contains "references/validation-scenarios.md" "Concept Foundation And Product Model Derivation" +assert_contains "references/human-review-summary.md" "## Concept Foundation Approval" +assert_contains "references/stage-guides.md" "Concept Foundation Human Review Summary" + +# Explicit Phase 3/4 and artifact-path exclusions. +assert_not_contains "templates/root-AGENTS.md" "| Concept Foundation |" +ruby -e ' + content = File.read(ARGV.fetch(0)) + section = content[/## Stage Order\n(.*?)(?=\n## |\z)/m, 1] + abort "FAIL: runtime Stage Order section missing" unless section + if section.lines.any? { |line| line.strip == "Concept Foundation" || line.match?(/(?:^|→)\s*Concept Foundation\s*(?:→|$)/) } + abort "FAIL: Concept Foundation must not be a canonical stage" + end +' "$root/references/runtime.md" +assert_not_contains "templates/decision.md" "Concept To Technical Representation" +assert_not_contains "templates/decision.md" "Concept ID | Product Meaning | Technical Representation" +assert_not_contains "references/project-decisions.md" "Phase 1/2" +assert_not_contains "templates/decision.md" "Phase 1/2" +assert_not_contains "references/document-templates.md" ".agent-loop/concepts/" +assert_not_contains "references/document-templates.md" "concept-foundation.yaml" +assert_not_contains "references/document-templates.md" "concept-foundation.json" +if [ -e "$root/.agent-loop/concepts" ] || [ -e "$root/.agent-loop" ]; then + printf 'FAIL: skill source repository must not contain target-project .agent-loop artifacts\n' >&2 + exit 1 +fi + +# Behavioral artifact trace: valid chain passes; unaccepted and detached chains fail. +ruby "$root/scripts/check-concept-foundation-trace.rb" \ + "$root/examples/concept-foundation-refund/requirement.md" \ + "$root/examples/concept-foundation-refund/product.md" \ + "$root/examples/concept-foundation-refund/spec.md" + +assert_contains "examples/concept-foundation-refund/requirement.md" "| C-REFUND-ADMIN | C-REFUND-SETTLEMENT |" + +ruby "$root/tests/validate-concept-foundation-trace-adversarial.rb" + +if ruby "$root/scripts/check-concept-foundation-trace.rb" \ + "$root/tests/fixtures/concept-foundation/invalid-unaccepted/requirement.md" \ + "$root/tests/fixtures/concept-foundation/invalid-unaccepted/product.md" \ + "$root/tests/fixtures/concept-foundation/invalid-unaccepted/spec.md" >/dev/null 2>&1; then + printf 'FAIL: unaccepted Concept Foundation fixture unexpectedly passed\n' >&2 + exit 1 +fi + +if ruby "$root/scripts/check-concept-foundation-trace.rb" \ + "$root/tests/fixtures/concept-foundation/invalid-detached-model/requirement.md" \ + "$root/tests/fixtures/concept-foundation/invalid-detached-model/product.md" \ + "$root/tests/fixtures/concept-foundation/invalid-detached-model/spec.md" >/dev/null 2>&1; then + printf 'FAIL: detached product-model fixture unexpectedly passed\n' >&2 + exit 1 +fi + +printf 'PASS: Concept Foundation Phase 1/2 runtime and artifact trace contract is complete\n' diff --git a/tests/validate-concept-foundation-trace-adversarial.rb b/tests/validate-concept-foundation-trace-adversarial.rb new file mode 100644 index 0000000..ac572d4 --- /dev/null +++ b/tests/validate-concept-foundation-trace-adversarial.rb @@ -0,0 +1,129 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "fileutils" +require "open3" +require "tmpdir" + +root = File.expand_path("..", __dir__) +validator = File.join(root, "scripts/check-concept-foundation-trace.rb") +example_dir = File.join(root, "examples/concept-foundation-refund") + +base_requirement = File.read(File.join(example_dir, "requirement.md")) +base_product = File.read(File.join(example_dir, "product.md")) +base_spec = File.read(File.join(example_dir, "spec.md")) + +def expect_reject(name, validator, requirement, product, spec) + Dir.mktmpdir("concept-foundation-adversarial") do |dir| + requirement_path = File.join(dir, "requirement.md") + product_path = File.join(dir, "product.md") + spec_path = File.join(dir, "spec.md") + + File.write(requirement_path, requirement) + File.write(product_path, product) + File.write(spec_path, spec) + + _stdout, _stderr, status = Open3.capture3( + "ruby", + validator, + requirement_path, + product_path, + spec_path + ) + + abort "FAIL: validator accepted adversarial case: #{name}" if status.success? + end + + puts "PASS: validator rejected #{name}" +end + +expect_reject( + "downstream use of unconfirmed concepts", + validator, + base_requirement.sub( + /^- Confirmed Concept IDs:.*/, + "- Confirmed Concept IDs: C-CUSTOMER" + ), + base_product, + base_spec +) + +expect_reject( + "missing Concept Candidate Inventory", + validator, + base_requirement.sub( + /^## Concept Candidate Inventory\n.*?(?=^## Concept Definitions\n)/m, + "" + ), + base_product, + base_spec +) + +expect_reject( + "open blocking ambiguity", + validator, + base_requirement.sub("| resolved |", "| open |"), + base_product, + base_spec +) + +not_needed_requirement = base_requirement + .sub("Concept Foundation Status: accepted", "Concept Foundation Status: concept-foundation-not-needed") + .sub(/^Not-Needed Reason:.*/, "Not-Needed Reason: n/a") +not_needed_product = base_product.sub( + "Concept Foundation Status: accepted", + "Concept Foundation Status: concept-foundation-not-needed" +) +not_needed_spec = base_spec.sub( + "Concept Foundation Status: accepted", + "Concept Foundation Status: concept-foundation-not-needed" +) + +expect_reject( + "non-concrete concept-foundation-not-needed reason", + validator, + not_needed_requirement, + not_needed_product, + not_needed_spec +) + +expect_reject( + "downstream model omitted from Concept-To-Product trace", + validator, + base_requirement.sub(/^\| TRACE-03 .*\n/, ""), + base_product, + base_spec +) + +duplicate_definition = File.readlines(File.join(example_dir, "requirement.md")) + .find { |line| line.start_with?("| C-CUSTOMER |") } +duplicate_requirement = base_requirement.sub( + duplicate_definition, + duplicate_definition * 2 +) + +expect_reject( + "duplicate Concept Definition ID", + validator, + duplicate_requirement, + base_product, + base_spec +) + +expect_reject( + "missing effective Concept Foundation source", + validator, + base_requirement, + base_product.sub(/^Effective Concept Source:.*\n/, ""), + base_spec +) + +expect_reject( + "command actor without target permission", + validator, + base_requirement.sub(/^\| C-REFUND-ADMIN \| C-REFUND-SETTLEMENT .*\n/, ""), + base_product, + base_spec +) + +puts "PASS: Concept Foundation adversarial trace cases are rejected" diff --git a/tests/validate-grill-artifact-templates.sh b/tests/validate-grill-artifact-templates.sh index 3a60fce..222f4c0 100755 --- a/tests/validate-grill-artifact-templates.sh +++ b/tests/validate-grill-artifact-templates.sh @@ -13,7 +13,7 @@ assert_contains() { } assert_contains "references/document-templates.md" "## Terminology / Domain Language" -assert_contains "references/document-templates.md" "## Roles / Operators / Permission Boundary" +assert_contains "references/document-templates.md" "## Role / Permission Matrix" assert_contains "references/document-templates.md" "## Primary Business Flow" assert_contains "references/document-templates.md" "## Exception Paths" assert_contains "references/document-templates.md" "## Data / Source of Truth" diff --git a/tests/validate-project-local-skills.sh b/tests/validate-project-local-skills.sh index ea0cfab..0d7e6b3 100755 --- a/tests/validate-project-local-skills.sh +++ b/tests/validate-project-local-skills.sh @@ -89,7 +89,7 @@ assert_contains "templates/root-AGENTS.md" "project-skill-management" assert_contains "templates/root-AGENTS.md" "Project Skill Creation / Update" assert_contains "templates/root-AGENTS.md" ".agent-loop/skills/INDEX.md" assert_contains "templates/root-AGENTS.md" "Execution Gate" -assert_contains "templates/root-AGENTS.md" "block-version:1.3.0-20260711" +assert_contains "templates/root-AGENTS.md" "block-version:1.3.0-20260713" assert_contains "templates/project.md" "Project Skills" assert_contains "templates/project.md" ".agent-loop/skills/INDEX.md" assert_contains "templates/project-skills/validation.md" "Validated Content Manifest" diff --git a/tests/validate-requirement-lifecycle-backlog.sh b/tests/validate-requirement-lifecycle-backlog.sh index e595e69..bba6481 100755 --- a/tests/validate-requirement-lifecycle-backlog.sh +++ b/tests/validate-requirement-lifecycle-backlog.sh @@ -89,7 +89,7 @@ assert_contains "SKILL.md" "Version: 1.3.0" assert_contains "README.md" "**Current version:** 1.3.0" assert_contains "Usage.md" "**版本:** 1.3.0" assert_contains "plugin.json" '"version": "1.3.0"' -assert_contains "templates/root-AGENTS.md" "block-version:1.3.0-20260711" +assert_contains "templates/root-AGENTS.md" "block-version:1.3.0-20260713" assert_contains "CHANGELOG.md" "## 1.3.0 — 2026-07-11" assert_not_contains "templates/root-AGENTS.md" "## Agent Loop Guidance Version" assert_contains "AGENTS.md" 'ignore the `alpha` prefix for version records' diff --git a/tests/validate-requirement-product-grill.sh b/tests/validate-requirement-product-grill.sh index 49a5d74..ea85918 100755 --- a/tests/validate-requirement-product-grill.sh +++ b/tests/validate-requirement-product-grill.sh @@ -75,7 +75,7 @@ assert_not_contains "references/stage-guides.md" "write approved clarification a assert_contains "references/external-skill-adapters.md" 'Do not create `CONTEXT.md`, `CONTEXT-MAP.md`, or `docs/adr/` from grill-with-docs defaults' assert_contains "references/stage-guides.md" "Requirements Discussion: write approved clarification and design output to the requirement document; keep requirement README to source, lifecycle, Delivery Phase, Feature Mapping, and decision-link summaries." -assert_contains "references/requirement-product-grill.md" "Detailed grill results belong in the requirement document; the requirement README keeps source, lifecycle, Delivery Phase, Feature Mapping, and decision-link summaries." +assert_contains "references/requirement-product-grill.md" "Detailed grill results belong in the effective requirement source; the requirement README keeps its effective Concept Foundation pointer plus source, lifecycle, Delivery Phase, Feature Mapping, and decision-link summaries." assert_contains "references/requirement-product-grill.md" '| Prior feature conflict | Requirement document, `notes.md`, or Human Review Summary conflict table |' assert_not_contains "references/requirement-product-grill.md" '| Prior feature conflict | Requirement README' assert_contains "references/requirement-management.md" 'Record accepted local terminology, scenarios, open questions, and conflicts in the reviewed requirement document.' @@ -88,7 +88,7 @@ assert_contains "references/validation-scenarios.md" 'otherwise do not create a assert_contains "references/validation-scenarios.md" 'otherwise keep the change in requirement artifacts without creating feature files' assert_contains "references/external-skill-adapters.md" "owning-stage artifact: requirement document plus requirement README summary during Requirements Discussion" assert_not_contains "references/external-skill-adapters.md" '| brainstormed design/spec | `.agent-loop/features//product.md` and/or `.agent-loop/features//spec.md` |' -assert_contains "templates/root-AGENTS.md" "| Product need, business goal, scope, constraint, scenario, or phased delivery is still being shaped | Requirements Discussion |" +assert_contains "templates/root-AGENTS.md" "| Product need, business goal, scope, constraint, scenario, concept identity/lifecycle, or phased delivery is still being shaped | Requirements Discussion |" assert_not_contains "templates/root-AGENTS.md" "Requirements Discussion / Grill" assert_contains "references/workflow-checklists.md" "Requirement/Product Grill" diff --git a/tests/validate-root-agents-block-checker.sh b/tests/validate-root-agents-block-checker.sh index 97c1331..a2ac017 100755 --- a/tests/validate-root-agents-block-checker.sh +++ b/tests/validate-root-agents-block-checker.sh @@ -71,14 +71,14 @@ fi assert_contains "$tmpdir/missing-stage-map.out" "FAIL root AGENTS drift found" assert_contains "$tmpdir/missing-stage-map.out" "workflow-stage-map | missing" -sed 's/block-version:1\.3\.0-20260711/block-version:1.3.0/' "$template" > "$tmpdir/stale.md" +sed 's/block-version:1\.3\.0-20260713/block-version:1.3.0/' "$template" > "$tmpdir/stale.md" if "$checker" --template "$template" --target "$tmpdir/stale.md" > "$tmpdir/stale.out"; then printf 'FAIL: checker should fail when block-version values are stale\n' >&2 cat "$tmpdir/stale.out" >&2 exit 1 fi assert_contains "$tmpdir/stale.out" "message-intent | stale-block-version" -assert_contains "$tmpdir/stale.out" "expected 1.3.0-20260711" +assert_contains "$tmpdir/stale.out" "expected 1.3.0-20260713" awk ' // { next } @@ -94,7 +94,7 @@ assert_contains "$tmpdir/broken.out" "ownership | broken-markers" awk ' /" + print "" print "nested" print "" inserted = 1 @@ -112,7 +112,7 @@ assert_contains "$tmpdir/nested.out" "ownership | nested-managed-block" awk ' { print } // && inserted != 1 { - print "" + print "" print "duplicate" print "" inserted = 1 @@ -127,7 +127,7 @@ assert_contains "$tmpdir/duplicate.out" "ownership | duplicate-section" { cat "$template" - printf '\n\n' + printf '\n\n' printf '## Legacy Extra\n\n' printf '\n' } > "$tmpdir/extra.md" @@ -176,7 +176,7 @@ assert_contains "$tmpdir/bare-end.out" "malformed-marker" { cat "$template" - printf '\n\n' + printf '\n\n' } > "$tmpdir/same-line.md" if "$checker" --template "$template" --target "$tmpdir/same-line.md" > "$tmpdir/same-line.out"; then printf 'FAIL: checker should fail when multiple managed markers are on the same line\n' >&2 diff --git a/tests/validate-root-agents-block-refresh.sh b/tests/validate-root-agents-block-refresh.sh index e948676..c8ec966 100755 --- a/tests/validate-root-agents-block-refresh.sh +++ b/tests/validate-root-agents-block-refresh.sh @@ -23,7 +23,7 @@ assert_not_contains() { managed_count=$(grep -c '^ + ## Bootstrap Protocol Before development work: @@ -29,7 +29,7 @@ Before development work: 15. Classify the current `agent-loop` stage and recommend exactly one next action. - + ## Agent Ownership - Own workflow diagnosis, sequencing, implementation, verification, review, drift checks, and project-memory updates. @@ -43,7 +43,7 @@ Before development work: - For non-trivial confirmations, present a table-first Human Review Summary before asking approval. - + ## Message Intent Guard Before project-state routing, classify the latest human message intent. @@ -58,7 +58,7 @@ Message intent is not permanent. If chat turns into product demand, reclassify a If unclear whether the human wants chat or requirements discussion, ask whether to keep discussing or shape the topic into a requirements document. If unclear whether the human wants requirements discussion or implementation, ask whether to form a requirements document first or start feature construction. - + ## Workflow Stage Map Use this after Bootstrap Protocol and Message Intent Guard. Select exactly one next stage from the current human signal and project state. When multiple signals match, apply this first-match order: Safety Stop -> Remote Discovery -> Memory Recovery -> Active Feature Guard -> Blocker Resolution -> Intent Routing -> Normal Stage Continuation. After selecting a stage, load the matching `references/...` file from the `agent-loop` skill package before acting. This map is navigation only; do not treat root `AGENTS.md` as the detailed stage procedure. @@ -99,7 +99,7 @@ Use this after Bootstrap Protocol and Message Intent Guard. Select exactly one n | Ordinary question or discussion has no artifact or implementation intent | Chat Entry | `references/runtime.md` only when intent is unclear; otherwise answer without creating workflow artifacts | - + ## Gate Modes - Strict Mode is the default: ask before and after every stage. @@ -109,13 +109,14 @@ Use this after Bootstrap Protocol and Message Intent Guard. Select exactly one n - If repeated confirmations slow the human down, proactively explain Feature Auto-Loop and Task Auto-Run, then ask before enabling either mode. - + ## Required Stops Stop and ask when: - scope changes or requirements are ambiguous - an unresolved Concept Foundation remains `candidate` / `reopened`, or a downstream product/feature artifact would redefine accepted product semantics +- a requirement-driven ADR has an unresolved Effective Requirement Snapshot, incomplete source-wide Requirement Model Scope Inventory / Technical Landing Trace, or `Upstream Compatibility: review-required` - product, design, architecture, security, data, approval, or public-interface decisions are unclear - a stage would modify human original requirements - tests require unavailable infrastructure @@ -137,7 +138,7 @@ Stop and ask when: Auto modes do not bypass these stops. - + ## Completion Rules - Before completion claims, run fresh verification and record evidence. @@ -147,9 +148,10 @@ Auto modes do not bypass these stops. - Before recommending or performing feature close, run Feature Close Review, drift check, and project memory update when long-term facts changed. - Feature Close Review requires feature-level Spec Review. Standards Review is required for large projects, broad diffs, boundary/security/data changes, architecture changes, or human request. - When a feature references accepted Decision & Design records, Feature Close Review and Feature Completion Check must verify assigned design slices and evidence; divergence returns to Decision & Design / Drift Check before close. +- For requirement-driven ADRs, completion also requires source-wide scope accounting, current upstream compatibility, and Requirement Model Technical Landing Trace coverage for every assigned Design Slice. - + ## Submit And Commit Rules - Submit, commit, PR, merge, release, and publish require explicit human confirmation after diff, verification, review, drift, and unrelated-change checks. @@ -161,7 +163,7 @@ Auto modes do not bypass these stops. - For the `agent-loop` skill repository itself, use `(v): ` and a 3-7 bullet body for meaningful commits. - + ## Project Memory And Artifacts - Resolve project-memory and feature paths relative to the active memory root: `.agent-loop/` by default, or legacy `agent-loop/` for the current run. @@ -169,6 +171,7 @@ Auto modes do not bypass these stops. - Keep long-term project memory in `.agent-loop/project.md` or enterprise `.agent-loop/project/*.md`. Root `AGENTS.md` should only summarize startup-critical facts that every Agent CLI needs immediately. - Keep original human materials in requirement set directories under `.agent-loop/requirements/`, or reference original paths when the human declines copying. - Keep accepted Concept Foundation and Requirement Product Model detail in the effective human-reviewed requirement source named by the requirement README. After archive, preserve prior sources and record confirmed semantic changes in an append-only Concept Foundation follow-up or a new requirement set. Feature `product.md` and `spec.md` cite accepted Concept/Model IDs and `Effective Concept Source` instead of redefining product identity, lifecycle, relationships, invariants, states, or fact meaning. +- Keep the Effective Requirement Snapshot, source-wide Requirement Model Scope Inventory, Requirement Model Technical Landing Trace, coverage evidence, compatibility judgment, Human Review Evidence, and triggered operational landing inside the existing Human-gated ADR; do not create a default mapping artifact or let ADR redefine product semantics. - Do not create new flat files directly under `.agent-loop/requirements/`; group requirements, prototypes, feedback, screenshots, recordings, links, and follow-up notes for the same intake/topic together. - For complex requirements, suggest `Delivery Phases` in the requirement set `README.md` before feature construction when the human needs to confirm staged delivery. A phase is a human-readable delivery slice, not a feature workspace, task, or plan. - Future/deferred work and backlog items belong in requirement sets and optional `requirements/INDEX.md`, not in `project.md`. Do not edit `requirement.md` or other source files for lifecycle/status updates. @@ -177,13 +180,13 @@ Auto modes do not bypass these stops. - Do not write task logs, feature progress, raw requirements, temporary plans, or test transcripts into `AGENTS.md`. - + ## Architecture Snapshot Add only startup-critical architecture boundaries that every future agent must know immediately. If the project has `ARCHITECTURE.md`, this block may use `source:ARCHITECTURE.md` instead. Keep details in `ARCHITECTURE.md`, `.agent-loop/project.md`, or enterprise `.agent-loop/project/*.md`. - + ## Directory Guidance - Directory-level `AGENTS.md` files are for long-lived boundary rules only. @@ -191,7 +194,7 @@ Add only startup-critical architecture boundaries that every future agent must k - Do not create directory-level `AGENTS.md` for ordinary component, utility, temporary, or feature implementation folders. - + ## Project Commands ```bash @@ -201,7 +204,7 @@ Add only startup-critical architecture boundaries that every future agent must k ``` - + ## Project-Specific Hard Constraints Add only stable constraints that every future agent must know at startup. diff --git a/templates/spec.md b/templates/spec.md index 08b1cd7..5fcb71d 100644 --- a/templates/spec.md +++ b/templates/spec.md @@ -39,7 +39,7 @@ Do not redefine accepted product meaning in this Feature Spec. Return to Require | Requirement Model ID | Concept / Action / Flow / State IDs | Feature Behavior / Story | Acceptance / Verification Direction | Coverage | |---|---|---|---|---| -| PM-01 | C-EXAMPLE / CMD-01 / FLOW-01 / STATE-01 | US1 | | planned / covered / out-of-scope | +| PM-01 | C-EXAMPLE / PERM-01 / CMD-01 / FLOW-01 / STATE-01 / EX-01 | US1 | | planned / covered / out-of-scope | ## Maintenance Fix Scope diff --git a/tests/fixtures/adr-technical-landing/invalid-empty-landing/decision.md b/tests/fixtures/adr-technical-landing/invalid-empty-landing/decision.md new file mode 100644 index 0000000..7eb66b5 --- /dev/null +++ b/tests/fixtures/adr-technical-landing/invalid-empty-landing/decision.md @@ -0,0 +1,44 @@ +# ADR-9002: Empty Landing Fixture + +Status: accepted + +## Effective Requirement Snapshot + +Effective Concept Source: requirement.md +Concept Foundation Status: accepted +Accepted Concept IDs: C-FIXTURE-SUBJECT, C-FIXTURE-OPERATOR +Accepted Requirement Model IDs: REL-FIXTURE-LINK, CMD-FIXTURE-ACTION, EVT-FIXTURE-RECORDED, FLOW-FIXTURE-01, STATE-FIXTURE-01, PM-FIXTURE-FACT +Upstream Compatibility: current +Last Compatibility Check: 2026-07-13 + +## Requirement Model Technical Landing Trace + +| Requirement Model Ref | Accepted Meaning / Constraint | Disposition | Technical Landing | Preserved Invariant | Design Slice | Verification | +|---|---|---|---|---|---|---| +| REL-FIXTURE-LINK | source relationship reference | landed | | relationship invariant | DS-FIXTURE-REL | verification target | +| CMD-FIXTURE-ACTION | source command reference | landed | generic handler | command invariant | DS-FIXTURE-CMD | verification target | +| EVT-FIXTURE-RECORDED | source event reference | landed | generic adapter | event invariant | DS-FIXTURE-EVT | verification target | +| FLOW-FIXTURE-01 | source flow reference | landed | generic coordinator | flow invariant | DS-FIXTURE-FLOW | verification target | +| STATE-FIXTURE-01 | source state reference | landed | generic representation | state invariant | DS-FIXTURE-STATE | verification target | +| PM-FIXTURE-FACT | source product-model reference | landed | generic representation | product invariant | DS-FIXTURE-PM | verification target | + +## Operational Landing Trigger Assessment + +| Concern | Status | Reason / Trigger Evidence | Detail Section If Triggered | +|---|---|---|---| +| Migration / Backfill | not-triggered | no representation change | none | + +## Design Slice Coverage + +| Design Slice ID | Required Capability / Rule | Owning Feature(s) | Verification | Coverage Status | +|---|---|---|---|---| +| DS-FIXTURE-REL | relationship landing | feature | verification target | planned | +| DS-FIXTURE-CMD | command landing | feature | verification target | planned | +| DS-FIXTURE-EVT | event landing | feature | verification target | planned | +| DS-FIXTURE-FLOW | flow landing | feature | verification target | planned | +| DS-FIXTURE-STATE | state landing | feature | verification target | planned | +| DS-FIXTURE-PM | product-model landing | feature | verification target | planned | + +## Coverage Hard Gate + +- [x] Coverage claimed complete diff --git a/tests/fixtures/adr-technical-landing/invalid-missing-coverage/decision.md b/tests/fixtures/adr-technical-landing/invalid-missing-coverage/decision.md new file mode 100644 index 0000000..3f39b21 --- /dev/null +++ b/tests/fixtures/adr-technical-landing/invalid-missing-coverage/decision.md @@ -0,0 +1,42 @@ +# ADR-9001: Missing Coverage Fixture + +Status: accepted + +## Effective Requirement Snapshot + +Effective Concept Source: requirement.md +Concept Foundation Status: accepted +Accepted Concept IDs: C-FIXTURE-SUBJECT, C-FIXTURE-OPERATOR +Accepted Requirement Model IDs: REL-FIXTURE-LINK, CMD-FIXTURE-ACTION, EVT-FIXTURE-RECORDED, FLOW-FIXTURE-01, STATE-FIXTURE-01, PM-FIXTURE-FACT +Upstream Compatibility: current +Last Compatibility Check: 2026-07-13 + +## Requirement Model Technical Landing Trace + +| Requirement Model Ref | Accepted Meaning / Constraint | Disposition | Technical Landing | Preserved Invariant | Design Slice | Verification | +|---|---|---|---|---|---|---| +| REL-FIXTURE-LINK | source relationship reference | landed | generic boundary | relationship invariant | DS-FIXTURE-REL | verification target | +| CMD-FIXTURE-ACTION | source command reference | landed | generic handler | command invariant | DS-FIXTURE-CMD | verification target | +| EVT-FIXTURE-RECORDED | source event reference | landed | generic adapter | event invariant | DS-FIXTURE-EVT | verification target | +| FLOW-FIXTURE-01 | source flow reference | landed | generic coordinator | flow invariant | DS-FIXTURE-FLOW | verification target | +| STATE-FIXTURE-01 | source state reference | landed | generic representation | state invariant | DS-FIXTURE-STATE | verification target | + +## Operational Landing Trigger Assessment + +| Concern | Status | Reason / Trigger Evidence | Detail Section If Triggered | +|---|---|---|---| +| Migration / Backfill | not-triggered | no representation change | none | + +## Design Slice Coverage + +| Design Slice ID | Required Capability / Rule | Owning Feature(s) | Verification | Coverage Status | +|---|---|---|---|---| +| DS-FIXTURE-REL | relationship landing | feature | verification target | planned | +| DS-FIXTURE-CMD | command landing | feature | verification target | planned | +| DS-FIXTURE-EVT | event landing | feature | verification target | planned | +| DS-FIXTURE-FLOW | flow landing | feature | verification target | planned | +| DS-FIXTURE-STATE | state landing | feature | verification target | planned | + +## Coverage Hard Gate + +- [x] Coverage claimed complete diff --git a/tests/fixtures/adr-technical-landing/invalid-reopened-source/README.md b/tests/fixtures/adr-technical-landing/invalid-reopened-source/README.md new file mode 100644 index 0000000..4fb312f --- /dev/null +++ b/tests/fixtures/adr-technical-landing/invalid-reopened-source/README.md @@ -0,0 +1,9 @@ +# Requirement Set: Reopened Source Fixture + +## Effective Concept Foundation + +Status: reopened +Effective Source: requirement.md +Previous Source: none +Last Confirmed: 2026-07-13 +Reason / Reopen Trigger: accepted meaning changed diff --git a/tests/fixtures/adr-technical-landing/invalid-reopened-source/requirement.md b/tests/fixtures/adr-technical-landing/invalid-reopened-source/requirement.md new file mode 100644 index 0000000..12a05f0 --- /dev/null +++ b/tests/fixtures/adr-technical-landing/invalid-reopened-source/requirement.md @@ -0,0 +1,3 @@ +# Requirement Document: Reopened Source Fixture + +Concept Foundation Status: reopened diff --git a/tests/fixtures/adr-technical-landing/invalid-review-required/decision.md b/tests/fixtures/adr-technical-landing/invalid-review-required/decision.md new file mode 100644 index 0000000..ddbbc03 --- /dev/null +++ b/tests/fixtures/adr-technical-landing/invalid-review-required/decision.md @@ -0,0 +1,12 @@ +# ADR-9003: Review Required Fixture + +Status: accepted + +## Effective Requirement Snapshot + +Effective Concept Source: requirement.md +Concept Foundation Status: accepted +Accepted Concept IDs: C-FIXTURE-SUBJECT, C-FIXTURE-OPERATOR +Accepted Requirement Model IDs: REL-FIXTURE-LINK +Upstream Compatibility: review-required +Last Compatibility Check: 2026-07-13 diff --git a/tests/fixtures/adr-technical-landing/invalid-unaccepted-source/README.md b/tests/fixtures/adr-technical-landing/invalid-unaccepted-source/README.md new file mode 100644 index 0000000..794605f --- /dev/null +++ b/tests/fixtures/adr-technical-landing/invalid-unaccepted-source/README.md @@ -0,0 +1,9 @@ +# Requirement Set: Unaccepted Source Fixture + +## Effective Concept Foundation + +Status: candidate +Effective Source: requirement.md +Previous Source: none +Last Confirmed: none +Reason / Reopen Trigger: blocking meaning remains open diff --git a/tests/fixtures/adr-technical-landing/invalid-unaccepted-source/requirement.md b/tests/fixtures/adr-technical-landing/invalid-unaccepted-source/requirement.md new file mode 100644 index 0000000..b26c1a7 --- /dev/null +++ b/tests/fixtures/adr-technical-landing/invalid-unaccepted-source/requirement.md @@ -0,0 +1,3 @@ +# Requirement Document: Unaccepted Source Fixture + +Concept Foundation Status: candidate diff --git a/tests/fixtures/adr-technical-landing/valid-not-needed/README.md b/tests/fixtures/adr-technical-landing/valid-not-needed/README.md new file mode 100644 index 0000000..a2346dc --- /dev/null +++ b/tests/fixtures/adr-technical-landing/valid-not-needed/README.md @@ -0,0 +1,9 @@ +# Requirement Set: Not-Needed Validator Fixture + +## Effective Concept Foundation + +Status: concept-foundation-not-needed +Effective Source: requirement.md +Previous Source: none +Last Confirmed: 2026-07-13 +Reason / Reopen Trigger: no product semantic model is required for this technical decision input diff --git a/tests/fixtures/adr-technical-landing/valid-not-needed/decision.md b/tests/fixtures/adr-technical-landing/valid-not-needed/decision.md new file mode 100644 index 0000000..9c8e7b8 --- /dev/null +++ b/tests/fixtures/adr-technical-landing/valid-not-needed/decision.md @@ -0,0 +1,44 @@ +# ADR-9100: Not-Needed Validator Fixture + +Status: proposed +Allowed Status: proposed | accepted | superseded | deprecated + +## Effective Requirement Snapshot + +Effective Concept Source: requirement.md +Concept Foundation Status: concept-foundation-not-needed +Accepted Concept IDs: none +Accepted Requirement Model IDs: none +Upstream Compatibility: current +Last Compatibility Check: 2026-07-13 +Trace Applicability: not-applicable +Trace Not-Applicable Reason: the accepted requirement introduces no product model requiring technical landing + +## Operational Landing Trigger Assessment + +| Concern | Status | Reason / Trigger Evidence | Detail Section If Triggered | +|---|---|---|---| +| Migration / Backfill | not-triggered | no durable representation changes | none | +| Compatibility | not-triggered | no interface or version compatibility changes | none | +| Rollout / Cutover | not-triggered | no staged activation or traffic changes | none | +| Rollback / Reversibility | not-triggered | no new reversal mechanism is required | none | + +## Design Slice Coverage + +| Design Slice ID | Required Capability / Rule | Owning Feature(s) | Verification | Coverage Status | +|---|---|---|---|---| +| DS-NOT-NEEDED-01 | preserve the accepted technical constraint | fixture feature | fixture verification target | planned | + +## Coverage Hard Gate + +- [x] Effective Concept Source resolves and matches the reviewed source +- [x] Concept Foundation Status is accepted or reasoned `concept-foundation-not-needed` +- [x] Upstream Compatibility is `current` +- [x] Every source Requirement Model ID has an explicit scope disposition, or trace is reasoned not-applicable +- [x] Every in-scope Accepted Requirement Model ID has exactly one disposition +- [x] Every `landed` row has Technical Landing, Preserved Invariant, Design Slice, and Verification +- [x] Every `covered-by-accepted-decision` and `feature-local` row names an existing or explicitly planned verified owner path +- [x] Every `not-applicable`, deferred, and out-of-scope item is visible in Human Review Summary +- [x] Every implementation-bearing technical rule is represented in Design Slice Coverage +- [x] No required Design Slice is `unassigned` +- [x] No unresolved product-semantic blocker remains diff --git a/tests/fixtures/adr-technical-landing/valid-not-needed/requirement.md b/tests/fixtures/adr-technical-landing/valid-not-needed/requirement.md new file mode 100644 index 0000000..863b251 --- /dev/null +++ b/tests/fixtures/adr-technical-landing/valid-not-needed/requirement.md @@ -0,0 +1,8 @@ +# Requirement Document: Not-Needed Validator Fixture + +Concept Foundation Status: concept-foundation-not-needed +Not-Needed Reason: the request changes no product concept, lifecycle, relationship, state, or product fact meaning + +## Technical Decision Inputs + +- A shared technical constraint still requires one project-level decision. diff --git a/tests/fixtures/adr-technical-landing/valid/README.md b/tests/fixtures/adr-technical-landing/valid/README.md new file mode 100644 index 0000000..1c7463c --- /dev/null +++ b/tests/fixtures/adr-technical-landing/valid/README.md @@ -0,0 +1,9 @@ +# Requirement Set: Validator Fixture + +## Effective Concept Foundation + +Status: accepted +Effective Source: requirement.md +Previous Source: none +Last Confirmed: 2026-07-13 +Reason / Reopen Trigger: initial accepted source diff --git a/tests/fixtures/adr-technical-landing/valid/decision.md b/tests/fixtures/adr-technical-landing/valid/decision.md new file mode 100644 index 0000000..cb69988 --- /dev/null +++ b/tests/fixtures/adr-technical-landing/valid/decision.md @@ -0,0 +1,86 @@ +# ADR-9000: Validator Fixture + +Status: accepted +Allowed Status: proposed | accepted | superseded | deprecated + +## Effective Requirement Snapshot + +Effective Concept Source: requirement.md +Concept Foundation Status: accepted +Accepted Concept IDs: C-FIXTURE-SUBJECT, C-FIXTURE-OPERATOR +Accepted Requirement Model IDs: REL-FIXTURE-LINK, PERM-FIXTURE-ACTION, CMD-FIXTURE-ACTION, EVT-FIXTURE-RECORDED, FLOW-FIXTURE-01, STATE-FIXTURE-01, PM-FIXTURE-FACT, EX-FIXTURE-01 +Upstream Compatibility: current +Last Compatibility Check: 2026-07-13 +Trace Applicability: required + +## Requirement Model Scope Inventory + +| Requirement Model Ref | Scope Disposition | Owner / Reason | +|---|---|---| +| REL-FIXTURE-LINK | in-scope | ADR-9000 | +| PERM-FIXTURE-ACTION | in-scope | ADR-9000 | +| CMD-FIXTURE-ACTION | in-scope | ADR-9000 | +| EVT-FIXTURE-RECORDED | in-scope | ADR-9000 | +| FLOW-FIXTURE-01 | in-scope | ADR-9000 | +| STATE-FIXTURE-01 | in-scope | ADR-9000 | +| PM-FIXTURE-FACT | in-scope | ADR-9000 | +| EX-FIXTURE-01 | in-scope | ADR-9000 | + +## Requirement Model Technical Landing Trace + +| Requirement Model Ref | Accepted Meaning / Constraint | Disposition | Technical Landing | Preserved Invariant | Design Slice | Verification | +|---|---|---|---|---|---|---| +| REL-FIXTURE-LINK | source relationship reference | landed | FixtureProtocol boundary | relationship invariant reference | DS-FIXTURE-REL | fixture verification target | +| PERM-FIXTURE-ACTION | source permission reference | landed | FixtureProtocol authorization boundary | permission invariant reference | DS-FIXTURE-PERM | fixture permission verification | +| CMD-FIXTURE-ACTION | source command reference | covered-by-accepted-decision | decisions/8999-shared.md (ADR-8999) | accepted decision invariant | none | accepted decision evidence | +| EVT-FIXTURE-RECORDED | source event reference | feature-local | features/fixture/spec.md | feature-local invariant | none | feature verification target | +| FLOW-FIXTURE-01 | source flow reference | not-applicable | reason: outside this coherent decision boundary | none | none | none | +| STATE-FIXTURE-01 | source state reference | landed | FixtureStore state representation | state invariant reference | DS-FIXTURE-STATE | fixture verification target | +| PM-FIXTURE-FACT | source product-model reference | landed | FixtureStore fact representation | product invariant reference | DS-FIXTURE-PM | fixture verification target | +| EX-FIXTURE-01 | source exception reference | landed | FixtureProtocol failure boundary | exception invariant reference | DS-FIXTURE-EX | fixture exception verification | + +## Operational Landing Trigger Assessment + +| Concern | Status | Reason / Trigger Evidence | Detail Section If Triggered | +|---|---|---|---| +| Migration / Backfill | triggered | existing representation changes | Migration Detail | +| Compatibility | not-triggered | no external compatibility change | none | +| Rollout / Cutover | not-triggered | no staged activation change | none | +| Rollback / Reversibility | not-triggered | existing reversal path remains valid | none | + +## Triggered Operational Landing + +### Migration Detail + +Fixture migration evidence. + +## Design Slice Coverage + +| Design Slice ID | Required Capability / Rule | Owning Feature(s) | Verification | Coverage Status | +|---|---|---|---|---| +| DS-FIXTURE-REL | relationship landing | fixture feature | fixture verification target | planned | +| DS-FIXTURE-PERM | permission boundary landing | fixture feature | fixture permission verification | planned | +| DS-FIXTURE-STATE | state landing | fixture feature | fixture verification target | planned | +| DS-FIXTURE-PM | product-model landing | fixture feature | fixture verification target | planned | +| DS-FIXTURE-EX | exception boundary landing | fixture feature | fixture exception verification | planned | + +## Coverage Hard Gate + +- [x] Effective Concept Source resolves and matches the reviewed source +- [x] Concept Foundation Status is accepted or reasoned `concept-foundation-not-needed` +- [x] Upstream Compatibility is `current` +- [x] Every source Requirement Model ID has an explicit scope disposition, or trace is reasoned not-applicable +- [x] Every in-scope Accepted Requirement Model ID has exactly one disposition +- [x] Every `landed` row has Technical Landing, Preserved Invariant, Design Slice, and Verification +- [x] Every `covered-by-accepted-decision` and `feature-local` row names an existing or explicitly planned verified owner path +- [x] Every `not-applicable`, deferred, and out-of-scope item is visible in Human Review Summary +- [x] Every implementation-bearing technical rule is represented in Design Slice Coverage +- [x] No required Design Slice is `unassigned` +- [x] No unresolved product-semantic blocker remains + +## Human Review Evidence + +Decision: accepted +Confirmed By: fixture human reviewer +Confirmed At: 2026-07-13 +Evidence: explicit fixture acceptance recorded after structural preflight validation diff --git a/tests/fixtures/adr-technical-landing/valid/decisions/8999-shared.md b/tests/fixtures/adr-technical-landing/valid/decisions/8999-shared.md new file mode 100644 index 0000000..05452db --- /dev/null +++ b/tests/fixtures/adr-technical-landing/valid/decisions/8999-shared.md @@ -0,0 +1,5 @@ +# ADR-8999: Shared Fixture Decision + +Status: accepted + +This accepted fixture decision owns the shared command landing used by ADR-9000. diff --git a/tests/fixtures/adr-technical-landing/valid/decisions/9001-follow-up.md b/tests/fixtures/adr-technical-landing/valid/decisions/9001-follow-up.md new file mode 100644 index 0000000..7344dc9 --- /dev/null +++ b/tests/fixtures/adr-technical-landing/valid/decisions/9001-follow-up.md @@ -0,0 +1,5 @@ +# ADR-9001: Proposed Follow-Up Fixture Decision + +Status: proposed + +This proposed fixture decision is the explicit future owner for one out-of-scope source model. diff --git a/tests/fixtures/adr-technical-landing/valid/features/fixture/spec.md b/tests/fixtures/adr-technical-landing/valid/features/fixture/spec.md new file mode 100644 index 0000000..a3ddacd --- /dev/null +++ b/tests/fixtures/adr-technical-landing/valid/features/fixture/spec.md @@ -0,0 +1,5 @@ +# Feature Spec: Fixture + +Status: proposed + +This fixture Feature Spec owns the explicitly feature-local event behavior. diff --git a/tests/fixtures/adr-technical-landing/valid/requirement.md b/tests/fixtures/adr-technical-landing/valid/requirement.md new file mode 100644 index 0000000..9b5f314 --- /dev/null +++ b/tests/fixtures/adr-technical-landing/valid/requirement.md @@ -0,0 +1,53 @@ +# Requirement Document: Validator Fixture + +Concept Foundation Status: accepted + +## Concept Definitions + +| Concept ID | Canonical Name | Definition | +|---|---|---| +| C-FIXTURE-SUBJECT | FixtureSubject | accepted subject meaning | +| C-FIXTURE-OPERATOR | FixtureOperator | accepted operator meaning | + +## Concept Relationships + +| Relationship ID | From Concept ID | To Concept ID | Meaning | +|---|---|---|---| +| REL-FIXTURE-LINK | C-FIXTURE-OPERATOR | C-FIXTURE-SUBJECT | accepted relationship | + +## Role / Permission Matrix + +| Permission Rule ID | Role Concept ID | Product Object Concept ID | Permission Meaning | +|---|---|---|---| +| PERM-FIXTURE-ACTION | C-FIXTURE-OPERATOR | C-FIXTURE-SUBJECT | operator may perform the accepted fixture action | + +## Commands / Events + +| Action ID | Actor / Producer Concept ID | Target Concept ID | Meaning | +|---|---|---|---| +| CMD-FIXTURE-ACTION | C-FIXTURE-OPERATOR | C-FIXTURE-SUBJECT | perform_fixture_action | +| EVT-FIXTURE-RECORDED | C-FIXTURE-SUBJECT | C-FIXTURE-SUBJECT | accepted outcome event | + +## Primary Business Flow + +| Flow Step ID | Actor Concept ID | Action ID | Product Meaning | +|---|---|---|---| +| FLOW-FIXTURE-01 | C-FIXTURE-OPERATOR | CMD-FIXTURE-ACTION | accepted flow step | + +## Product State Model + +| State Model ID | Concept IDs | Trigger Action ID | Product Meaning | +|---|---|---|---| +| STATE-FIXTURE-01 | C-FIXTURE-SUBJECT | EVT-FIXTURE-RECORDED | accepted state transition | + +## Requirement Product Model + +| Product Model ID | Concept IDs | Kind | Product Meaning / Invariant | +|---|---|---|---| +| PM-FIXTURE-FACT | C-FIXTURE-SUBJECT | fact | accepted product invariant | + +## Exception Paths + +| Scenario ID | Concept / State / Action IDs | Trigger | Expected Handling | +|---|---|---|---| +| EX-FIXTURE-01 | C-FIXTURE-SUBJECT / STATE-FIXTURE-01 / CMD-FIXTURE-ACTION | fixture action cannot complete | preserve the accepted subject state and expose the failure | diff --git a/tests/validate-adr-requirement-model-technical-landing-trace.sh b/tests/validate-adr-requirement-model-technical-landing-trace.sh new file mode 100644 index 0000000..f8ddc8d --- /dev/null +++ b/tests/validate-adr-requirement-model-technical-landing-trace.sh @@ -0,0 +1,163 @@ +#!/usr/bin/env bash +set -euo pipefail + +root=$(cd "$(dirname "$0")/.." && pwd) + +assert_contains() { + local file=$1 + local text=$2 + if ! grep -Fq -- "$text" "$root/$file"; then + printf 'FAIL: %s missing required text: %s\n' "$file" "$text" >&2 + exit 1 + fi +} + +assert_not_contains() { + local file=$1 + local text=$2 + if grep -Fq -- "$text" "$root/$file"; then + printf 'FAIL: %s contains forbidden text: %s\n' "$file" "$text" >&2 + exit 1 + fi +} + +assert_file_exists() { + local file=$1 + if [ ! -f "$root/$file" ]; then + printf 'FAIL: missing required file: %s\n' "$file" >&2 + exit 1 + fi +} + +# Published authority and stage placement. +assert_contains "SKILL.md" "Effective Requirement Snapshot" +assert_contains "SKILL.md" "Requirement Model Technical Landing Trace" +assert_contains "references/design.md" "Effective Requirement Snapshot" +assert_contains "references/runtime.md" "## ADR Requirement Model Technical Landing" +assert_contains "references/runtime.md" "Requirement Model Scope Inventory" +assert_contains "references/runtime.md" 'PERM-*' +assert_contains "references/runtime.md" 'EX-*' +assert_contains "references/runtime.md" 'structural preflight while the ADR is `proposed`' +assert_contains "references/runtime.md" "Upstream Compatibility: current | review-required" +assert_contains "references/runtime.md" 'review-required` is a dependency-availability judgment, not an ADR lifecycle status' +assert_contains "references/project-decisions.md" "## Effective Requirement Snapshot" +assert_contains "references/project-decisions.md" "## Requirement Model Scope Inventory" +assert_contains "references/project-decisions.md" "## Requirement Model Technical Landing Trace" +assert_contains "references/project-decisions.md" "## Coverage Hard Gate" +assert_contains "references/project-decisions.md" "## Upstream Compatibility And Drift" +assert_contains "references/project-decisions.md" "## Triggered Operational Landing" + +# Product ownership and acceptance hard stops. +assert_contains "references/project-decisions.md" "ADR must not create, rename, split, merge, or redefine a Concept, relationship, role/permission, command/event, business flow, product state, invariant, exception/recovery meaning, or product fact ownership." +assert_contains "references/project-decisions.md" 'A decision cannot become `accepted` while coverage is missing or Upstream Compatibility is `review-required`.' +assert_contains "references/stage-guides.md" "Decision & Design Human Review Summary" +assert_contains "references/stage-guides.md" "Effective Requirement Snapshot" +assert_contains "references/workflow-checklists.md" "Requirement Model Technical Landing Trace" +assert_contains "references/workflow-checklists.md" "Upstream Compatibility" +assert_contains "references/workflow-checklists.md" "Coverage Hard Gate" +assert_contains "templates/root-AGENTS.md" "Upstream Compatibility" +assert_contains "templates/root-AGENTS.md" "Requirement Model Technical Landing Trace" +assert_contains "references/project-guidance.md" "Requirement Model Technical Landing Trace" + +# Human review and generic ADR template. +assert_contains "references/human-review-summary.md" "### Decision & Design Approval" +assert_contains "references/human-review-summary.md" "| Effective Requirement Source |" +assert_contains "references/human-review-summary.md" "| Requirement Model Coverage |" +assert_contains "references/human-review-summary.md" "| Product Semantics Preserved |" +assert_contains "references/human-review-summary.md" "| Migration / Compatibility / Rollout |" +assert_contains "references/human-review-summary.md" "| Design Slice Ownership |" + +assert_contains "templates/decision.md" "## Effective Requirement Snapshot" +assert_contains "templates/decision.md" "Effective Concept Source:" +assert_contains "templates/decision.md" "Concept Foundation Status: accepted | concept-foundation-not-needed" +assert_contains "templates/decision.md" "Accepted Concept IDs:" +assert_contains "templates/decision.md" "Accepted Requirement Model IDs:" +assert_contains "templates/decision.md" "Upstream Compatibility: current | review-required" +assert_contains "templates/decision.md" "Last Compatibility Check:" +assert_contains "templates/decision.md" "Trace Applicability: required | not-applicable" +assert_contains "templates/decision.md" "Trace Not-Applicable Reason:" +assert_contains "templates/decision.md" "## Requirement Model Scope Inventory" +assert_contains "templates/decision.md" 'REL-*`, `PERM-*`, `CMD-*`, `EVT-*`, `FLOW-*`, `STATE-*`, `PM-*`, and `EX-*' +assert_contains "templates/decision.md" "## Requirement Model Technical Landing Trace" +assert_contains "templates/decision.md" "| Requirement Model Ref | Accepted Meaning / Constraint | Disposition | Technical Landing | Preserved Invariant | Design Slice | Verification |" +assert_contains "templates/decision.md" "landed | covered-by-accepted-decision | feature-local | not-applicable" +assert_contains "templates/decision.md" "## Coverage Hard Gate" +assert_contains "templates/decision.md" "Every source Requirement Model ID has an explicit scope disposition" +assert_contains "templates/decision.md" "## Human Review Evidence" +assert_contains "templates/decision.md" 'Run the structural validator while the ADR is still `proposed`.' +assert_contains "templates/decision.md" "## Upstream Compatibility And Drift" +assert_contains "templates/decision.md" "## Operational Landing Trigger Assessment" +assert_contains "templates/decision.md" "triggered | not-triggered" + +# Human docs and pressure scenarios. +assert_contains "README.md" "Requirement Model Technical Landing Trace" +assert_contains "Usage.md" "Effective Requirement Snapshot" +assert_contains "CHANGELOG.md" "Requirement Model Technical Landing Trace" +assert_contains "references/validation-scenarios.md" "ADR Requirement Model Technical Landing Trace" +assert_contains "references/validation-scenarios.md" "Stale Effective Requirement Snapshot Blocks ADR" +assert_contains "references/validation-scenarios.md" "Incomplete Landing Coverage Blocks Feature Spec" +assert_contains "references/validation-scenarios.md" "Accepted ADR Meaning Requires Supersede" +assert_contains "references/validation-scenarios.md" "Operational Landing Is Triggered, Not Default" + +# Scope exclusions: no stage, status, default mapping artifact, executable schema, or target-project workspace. +ruby -e ' + content = File.read(ARGV.fetch(0)) + section = content[/## Stage Order\n(.*?)(?=\n## |\z)/m, 1] + abort "FAIL: runtime Stage Order section missing" unless section + forbidden = ["Effective Requirement Snapshot", "Requirement Model Technical Landing Trace", "Coverage Hard Gate", "Upstream Compatibility Review"] + found = forbidden.select { |name| section.lines.any? { |line| line.strip == name } } + abort "FAIL: ADR trace concepts must not become canonical stages: #{found.join(", ")}" unless found.empty? +' "$root/references/runtime.md" +assert_contains "templates/decision.md" "Allowed Status: proposed | accepted | superseded | deprecated" +assert_not_contains "templates/decision.md" "Allowed Status: proposed | accepted | review-required" +assert_not_contains "references/project-decisions.md" ".agent-loop/requirement-model-mappings/" +assert_not_contains "templates/decision.md" "technical-landing.yaml" +assert_not_contains "templates/decision.md" "technical-landing.json" +if [ -e "$root/.agent-loop" ]; then + printf 'FAIL: skill source repository must not contain target-project .agent-loop artifacts\n' >&2 + exit 1 +fi + +# The generic template and validator must not learn fixture-specific business/action/technology values. +for forbidden in FixtureSubject FixtureOperator perform_fixture_action FixtureStore FixtureProtocol; do + assert_not_contains "templates/decision.md" "$forbidden" + assert_not_contains "scripts/check-adr-requirement-model-trace.rb" "$forbidden" +done + +# Behavioral validation, not keyword-only assertions. +assert_file_exists "scripts/check-adr-requirement-model-trace.rb" +valid="$root/tests/fixtures/adr-technical-landing/valid" +ruby "$root/scripts/check-adr-requirement-model-trace.rb" "$valid/README.md" "$valid/requirement.md" "$valid/decision.md" +ruby "$root/tests/validate-adr-requirement-model-trace-adversarial.rb" + +not_needed="$root/tests/fixtures/adr-technical-landing/valid-not-needed" +ruby "$root/scripts/check-adr-requirement-model-trace.rb" "$not_needed/README.md" "$not_needed/requirement.md" "$not_needed/decision.md" + +if ruby "$root/scripts/check-adr-requirement-model-trace.rb" "$valid/README.md" "$valid/requirement.md" "$root/tests/fixtures/adr-technical-landing/invalid-missing-coverage/decision.md" >/dev/null 2>&1; then + printf 'FAIL: missing-coverage ADR fixture unexpectedly passed\n' >&2 + exit 1 +fi + +if ruby "$root/scripts/check-adr-requirement-model-trace.rb" "$valid/README.md" "$valid/requirement.md" "$root/tests/fixtures/adr-technical-landing/invalid-empty-landing/decision.md" >/dev/null 2>&1; then + printf 'FAIL: empty-landing ADR fixture unexpectedly passed\n' >&2 + exit 1 +fi + +unaccepted="$root/tests/fixtures/adr-technical-landing/invalid-unaccepted-source" +if ruby "$root/scripts/check-adr-requirement-model-trace.rb" "$unaccepted/README.md" "$unaccepted/requirement.md" "$valid/decision.md" >/dev/null 2>&1; then + printf 'FAIL: unaccepted-source ADR fixture unexpectedly passed\n' >&2 + exit 1 +fi + +reopened="$root/tests/fixtures/adr-technical-landing/invalid-reopened-source" +if ruby "$root/scripts/check-adr-requirement-model-trace.rb" "$reopened/README.md" "$reopened/requirement.md" "$valid/decision.md" >/dev/null 2>&1; then + printf 'FAIL: reopened-source ADR fixture unexpectedly passed\n' >&2 + exit 1 +fi + +if ruby "$root/scripts/check-adr-requirement-model-trace.rb" "$valid/README.md" "$valid/requirement.md" "$root/tests/fixtures/adr-technical-landing/invalid-review-required/decision.md" >/dev/null 2>&1; then + printf 'FAIL: review-required ADR fixture unexpectedly passed\n' >&2 + exit 1 +fi + +printf 'PASS: ADR Requirement Model Technical Landing Trace contract is complete\n' diff --git a/tests/validate-adr-requirement-model-trace-adversarial.rb b/tests/validate-adr-requirement-model-trace-adversarial.rb new file mode 100644 index 0000000..73a7113 --- /dev/null +++ b/tests/validate-adr-requirement-model-trace-adversarial.rb @@ -0,0 +1,238 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "fileutils" +require "open3" +require "tmpdir" + +root = File.expand_path("..", __dir__) +validator = File.join(root, "scripts/check-adr-requirement-model-trace.rb") +valid_dir = File.join(root, "tests/fixtures/adr-technical-landing/valid") +not_needed_dir = File.join(root, "tests/fixtures/adr-technical-landing/valid-not-needed") + +base_readme = File.read(File.join(valid_dir, "README.md")) +base_source = File.read(File.join(valid_dir, "requirement.md")) +base_decision = File.read(File.join(valid_dir, "decision.md")) + +failures = [] + +def run_validator(validator, fixture_dir, readme, source, decision) + Dir.mktmpdir("adr-technical-landing-adversarial") do |dir| + FileUtils.cp_r(File.join(fixture_dir, "."), dir) + readme_path = File.join(dir, "README.md") + source_path = File.join(dir, "requirement.md") + decision_path = File.join(dir, "decision.md") + File.write(readme_path, readme) + File.write(source_path, source) + File.write(decision_path, decision) + + stdout, stderr, status = Open3.capture3( + "ruby", + validator, + readme_path, + source_path, + decision_path + ) + [status.success?, (stdout + stderr).strip] + end +end + +def expect_accept(failures, name, validator, fixture_dir, readme, source, decision) + accepted, output = run_validator(validator, fixture_dir, readme, source, decision) + if accepted + puts "PASS: validator accepted #{name}" + else + failures << "expected acceptance for #{name}: #{output.lines.first}" + end +end + +def expect_reject(failures, name, validator, fixture_dir, readme, source, decision) + accepted, output = run_validator(validator, fixture_dir, readme, source, decision) + if accepted + failures << "validator accepted adversarial case: #{name}" + else + puts "PASS: validator rejected #{name} (#{output.lines.first})" + end +end + +proposed_decision = base_decision + .sub(/^Status: accepted$/, "Status: proposed") + .sub(/^## Human Review Evidence\n.*?(?=^## |\z)/m, "") +expect_accept( + failures, + "a proposed ADR structural preflight before Human Review", + validator, + valid_dir, + base_readme, + base_source, + proposed_decision +) + +expect_accept( + failures, + "an accepted ADR with recorded Human Review evidence", + validator, + valid_dir, + base_readme, + base_source, + base_decision +) + +expect_reject( + failures, + "an accepted ADR without recorded Human Review evidence", + validator, + valid_dir, + base_readme, + base_source, + base_decision.sub(/^## Human Review Evidence\n.*?(?=^## |\z)/m, "") +) + +expect_accept( + failures, + "an explicitly planned canonical Feature Spec owner path", + validator, + valid_dir, + base_readme, + base_source, + base_decision.gsub("features/fixture/spec.md", "planned:features/future-fixture/spec.md") +) + +delegated_decision = base_decision + .sub(", FLOW-FIXTURE-01", "") + .sub( + "| FLOW-FIXTURE-01 | in-scope | ADR-9000 |", + "| FLOW-FIXTURE-01 | proposed-decision | decisions/9001-follow-up.md |" + ) + .sub(/^\| FLOW-FIXTURE-01 \| source flow reference .*\n/, "") +expect_accept( + failures, + "a source model explicitly delegated to an existing proposed decision", + validator, + valid_dir, + base_readme, + base_source, + delegated_decision +) + +expect_reject( + failures, + "placeholder not-applicable reason", + validator, + valid_dir, + base_readme, + base_source, + base_decision.sub("reason: outside this coherent decision boundary", "reason: n/a") +) + +expect_reject( + failures, + "a Coverage Hard Gate replaced by one arbitrary checkbox", + validator, + valid_dir, + base_readme, + base_source, + base_decision.sub(/## Coverage Hard Gate\n.*?(?=^## |\z)/m, "## Coverage Hard Gate\n\n- [x] arbitrary check\n\n") +) + +expect_reject( + failures, + "a Coverage Hard Gate with an unsupported extra checkbox", + validator, + valid_dir, + base_readme, + base_source, + base_decision.sub( + "- [x] No unresolved product-semantic blocker remains", + "- [x] No unresolved product-semantic blocker remains\n- [x] arbitrary extra check" + ) +) + +expect_reject( + failures, + "unparsed garbage in Accepted Requirement Model IDs", + validator, + valid_dir, + base_readme, + base_source, + base_decision.sub(/^Accepted Requirement Model IDs:(.*)$/, 'Accepted Requirement Model IDs:\1, GARBAGE') +) + +expect_reject( + failures, + "a source model silently omitted from ADR scope", + validator, + valid_dir, + base_readme, + base_source, + base_decision + .sub(/, PM-FIXTURE-FACT/, "") + .gsub(/^\| PM-FIXTURE-FACT .*\n/, "") +) + +expect_reject( + failures, + "a covered-by-accepted-decision reference whose ADR does not exist", + validator, + valid_dir, + base_readme, + base_source, + base_decision.sub("decisions/8999-shared.md (ADR-8999)", "decisions/does-not-exist.md (ADR-DOES-NOT-EXIST)") +) + +expect_reject( + failures, + "a feature-local reference whose Feature Spec does not exist", + validator, + valid_dir, + base_readme, + base_source, + base_decision.sub("features/fixture/spec.md", "features/missing/spec.md") +) + +expect_reject( + failures, + "a Design Slice with an invalid coverage status", + validator, + valid_dir, + base_readme, + base_source, + base_decision.sub("| planned |", "| banana |") +) + +expect_reject( + failures, + "a triggered operational concern without its detail section", + validator, + valid_dir, + base_readme, + base_source, + base_decision.sub(/\n## Triggered Operational Landing\n.*?(?=\n## Design Slice Coverage)/m, "") +) + +expect_reject( + failures, + "an incomplete operational concern inventory", + validator, + valid_dir, + base_readme, + base_source, + base_decision.gsub(/^\| (?:Compatibility|Rollout \/ Cutover|Rollback \/ Reversibility) \|.*\n/, "") +) + +not_needed_readme = File.read(File.join(not_needed_dir, "README.md")) +not_needed_source = File.read(File.join(not_needed_dir, "requirement.md")) +not_needed_decision = File.read(File.join(not_needed_dir, "decision.md")) +expect_accept( + failures, + "a reasoned concept-foundation-not-needed ADR preflight without a product model", + validator, + not_needed_dir, + not_needed_readme, + not_needed_source, + not_needed_decision +) + +abort failures.map { |failure| "FAIL: #{failure}" }.join("\n") unless failures.empty? + +puts "PASS: ADR technical landing adversarial contract is complete" diff --git a/tests/validate-concept-foundation-requirement-modeling.sh b/tests/validate-concept-foundation-requirement-modeling.sh index 538cc91..f89b391 100644 --- a/tests/validate-concept-foundation-requirement-modeling.sh +++ b/tests/validate-concept-foundation-requirement-modeling.sh @@ -73,10 +73,13 @@ assert_contains "references/document-templates.md" "## Concept Candidate Invento assert_contains "references/document-templates.md" "## Concept Definitions" assert_contains "references/document-templates.md" "## Concept Relationships" assert_contains "references/document-templates.md" "## Role / Permission Matrix" +assert_contains "references/document-templates.md" "Permission Rule ID" +assert_contains "references/document-templates.md" "PERM-01" assert_contains "references/document-templates.md" "## Commands / Events" assert_contains "references/document-templates.md" "## Product State Model" assert_contains "references/document-templates.md" "## Requirement Product Model" assert_contains "references/document-templates.md" "## Concept-To-Product Traceability" +assert_contains "references/document-templates.md" "EX-01" assert_contains "references/document-templates.md" "Human Confirmation" # Phase 2 consumers cite accepted product semantics instead of redefining them. @@ -126,7 +129,7 @@ ruby "$root/scripts/check-concept-foundation-trace.rb" \ "$root/examples/concept-foundation-refund/product.md" \ "$root/examples/concept-foundation-refund/spec.md" -assert_contains "examples/concept-foundation-refund/requirement.md" "| C-REFUND-ADMIN | C-REFUND-SETTLEMENT |" +assert_contains "examples/concept-foundation-refund/requirement.md" "| PERM-ADMIN-SETTLEMENT | C-REFUND-ADMIN | C-REFUND-SETTLEMENT |" ruby "$root/tests/validate-concept-foundation-trace-adversarial.rb" diff --git a/tests/validate-concept-foundation-trace-adversarial.rb b/tests/validate-concept-foundation-trace-adversarial.rb index ac572d4..7206604 100644 --- a/tests/validate-concept-foundation-trace-adversarial.rb +++ b/tests/validate-concept-foundation-trace-adversarial.rb @@ -121,7 +121,7 @@ def expect_reject(name, validator, requirement, product, spec) expect_reject( "command actor without target permission", validator, - base_requirement.sub(/^\| C-REFUND-ADMIN \| C-REFUND-SETTLEMENT .*\n/, ""), + base_requirement.sub(/^\| PERM-ADMIN-SETTLEMENT \| C-REFUND-ADMIN \| C-REFUND-SETTLEMENT .*\n/, ""), base_product, base_spec ) diff --git a/tests/validate-project-local-skills.sh b/tests/validate-project-local-skills.sh index 0d7e6b3..c1c0c92 100755 --- a/tests/validate-project-local-skills.sh +++ b/tests/validate-project-local-skills.sh @@ -89,7 +89,7 @@ assert_contains "templates/root-AGENTS.md" "project-skill-management" assert_contains "templates/root-AGENTS.md" "Project Skill Creation / Update" assert_contains "templates/root-AGENTS.md" ".agent-loop/skills/INDEX.md" assert_contains "templates/root-AGENTS.md" "Execution Gate" -assert_contains "templates/root-AGENTS.md" "block-version:1.3.0-20260713" +assert_contains "templates/root-AGENTS.md" "block-version:1.3.0-20260713.2" assert_contains "templates/project.md" "Project Skills" assert_contains "templates/project.md" ".agent-loop/skills/INDEX.md" assert_contains "templates/project-skills/validation.md" "Validated Content Manifest" diff --git a/tests/validate-requirement-lifecycle-backlog.sh b/tests/validate-requirement-lifecycle-backlog.sh index bba6481..c85736d 100755 --- a/tests/validate-requirement-lifecycle-backlog.sh +++ b/tests/validate-requirement-lifecycle-backlog.sh @@ -89,7 +89,7 @@ assert_contains "SKILL.md" "Version: 1.3.0" assert_contains "README.md" "**Current version:** 1.3.0" assert_contains "Usage.md" "**版本:** 1.3.0" assert_contains "plugin.json" '"version": "1.3.0"' -assert_contains "templates/root-AGENTS.md" "block-version:1.3.0-20260713" +assert_contains "templates/root-AGENTS.md" "block-version:1.3.0-20260713.2" assert_contains "CHANGELOG.md" "## 1.3.0 — 2026-07-11" assert_not_contains "templates/root-AGENTS.md" "## Agent Loop Guidance Version" assert_contains "AGENTS.md" 'ignore the `alpha` prefix for version records' diff --git a/tests/validate-root-agents-block-checker.sh b/tests/validate-root-agents-block-checker.sh index a2ac017..38c6642 100755 --- a/tests/validate-root-agents-block-checker.sh +++ b/tests/validate-root-agents-block-checker.sh @@ -71,14 +71,14 @@ fi assert_contains "$tmpdir/missing-stage-map.out" "FAIL root AGENTS drift found" assert_contains "$tmpdir/missing-stage-map.out" "workflow-stage-map | missing" -sed 's/block-version:1\.3\.0-20260713/block-version:1.3.0/' "$template" > "$tmpdir/stale.md" +sed 's/block-version:1\.3\.0-20260713\.2/block-version:1.3.0/' "$template" > "$tmpdir/stale.md" if "$checker" --template "$template" --target "$tmpdir/stale.md" > "$tmpdir/stale.out"; then printf 'FAIL: checker should fail when block-version values are stale\n' >&2 cat "$tmpdir/stale.out" >&2 exit 1 fi assert_contains "$tmpdir/stale.out" "message-intent | stale-block-version" -assert_contains "$tmpdir/stale.out" "expected 1.3.0-20260713" +assert_contains "$tmpdir/stale.out" "expected 1.3.0-20260713.2" awk ' // { next } @@ -94,7 +94,7 @@ assert_contains "$tmpdir/broken.out" "ownership | broken-markers" awk ' /" + print "" print "nested" print "" inserted = 1 @@ -112,7 +112,7 @@ assert_contains "$tmpdir/nested.out" "ownership | nested-managed-block" awk ' { print } // && inserted != 1 { - print "" + print "" print "duplicate" print "" inserted = 1 @@ -127,7 +127,7 @@ assert_contains "$tmpdir/duplicate.out" "ownership | duplicate-section" { cat "$template" - printf '\n\n' + printf '\n\n' printf '## Legacy Extra\n\n' printf '\n' } > "$tmpdir/extra.md" @@ -176,7 +176,7 @@ assert_contains "$tmpdir/bare-end.out" "malformed-marker" { cat "$template" - printf '\n\n' + printf '\n\n' } > "$tmpdir/same-line.md" if "$checker" --template "$template" --target "$tmpdir/same-line.md" > "$tmpdir/same-line.out"; then printf 'FAIL: checker should fail when multiple managed markers are on the same line\n' >&2 diff --git a/tests/validate-root-agents-block-refresh.sh b/tests/validate-root-agents-block-refresh.sh index c8ec966..8fe7a04 100755 --- a/tests/validate-root-agents-block-refresh.sh +++ b/tests/validate-root-agents-block-refresh.sh @@ -23,7 +23,7 @@ assert_not_contains() { managed_count=$(grep -c '^") +END_RE = re.compile(r"") + + +@dataclass +class ManagedBlock: + section: str + source: str | None + version: str | None + block_version: str | None + start_line: int + end_line: int | None = None + + +@dataclass(frozen=True) +class MarkerError: + section: str + status: str + detail: str + + +def attr_value(attrs: str, name: str) -> str | None: + match = re.search(rf"(?:^|\s){re.escape(name)}:([^\s>]+)", attrs) + return match.group(1) if match else None + + +def parse_blocks(path: Path) -> tuple[dict[str, ManagedBlock], list[MarkerError]]: + blocks: dict[str, ManagedBlock] = {} + errors: list[MarkerError] = [] + active: ManagedBlock | None = None + + for line_no, line in enumerate(read_text(path).splitlines(), start=1): + if len(re.findall(r"agent-loop:managed-(?:start|end)", line)) > 1: + errors.append( + MarkerError( + "(unknown)", + "malformed-marker", + f"multiple managed markers on one line at line {line_no}", + ) + ) + continue + + start_match = START_RE.search(line) + if start_match: + attrs = start_match.group(1) + section_name = attr_value(attrs, "section") + if not section_name: + errors.append( + MarkerError( + "(unknown)", + "malformed-marker", + f"start marker at line {line_no} is missing section", + ) + ) + continue + if active: + errors.append( + MarkerError( + active.section, + "broken-markers", + f"section {active.section} starts at line {active.start_line} " + f"but is not closed before line {line_no}", + ) + ) + errors.append( + MarkerError( + active.section, + "nested-managed-block", + f"section {active.section} starts at line {active.start_line} " + f"but a nested section starts at line {line_no}", + ) + ) + if section_name in blocks: + errors.append( + MarkerError( + section_name, + "duplicate-section", + f"section {section_name} appears more than once", + ) + ) + active = ManagedBlock( + section=section_name, + source=attr_value(attrs, "source"), + version=attr_value(attrs, "version"), + block_version=attr_value(attrs, "block-version"), + start_line=line_no, + ) + continue + + if "agent-loop:managed-start" in line: + errors.append( + MarkerError( + "(unknown)", + "malformed-marker", + f"malformed managed-start marker at line {line_no}", + ) + ) + continue + + end_match = END_RE.search(line) + if end_match: + end_section = end_match.group(1) + if not active: + errors.append( + MarkerError( + end_section, + "broken-markers", + f"orphan end marker at line {line_no}", + ) + ) + continue + if active.section != end_section: + errors.append( + MarkerError( + active.section, + "broken-markers", + f"section {active.section} starts at line {active.start_line} " + f"but ends as {end_section} at line {line_no}", + ) + ) + active = None + continue + active.end_line = line_no + blocks[active.section] = active + active = None + continue + + if "agent-loop:managed-end" in line: + errors.append( + MarkerError( + "(unknown)", + "malformed-marker", + f"malformed managed-end marker at line {line_no}", + ) + ) + + if active: + errors.append( + MarkerError( + active.section, + "broken-markers", + f"section {active.section} starts at line {active.start_line} " + "but has no end marker", + ) + ) + return blocks, errors + + +def local_source(source: str | None) -> bool: + return bool( + source + and source != "agent-loop-skill" + and not source.startswith(("http://", "https://")) + and ("/" in source or source.endswith(".md")) + ) + + +def escape_cell(value: object) -> str: + return str(value).replace("|", r"\|") + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description=( + "Read-only checker for agent-loop root AGENTS managed blocks. " + "Verifies marker structure, required sections, and block versions." + ) + ) + parser.add_argument("--template", required=True, type=Path) + parser.add_argument("--target", required=True, type=Path) + parser.add_argument("--no-source-check", action="store_true") + return parser + + +def main() -> int: + require_supported_python() + parser = build_parser() + args = parser.parse_args() + template: Path = args.template + target: Path = args.target + if not template.is_file(): + parser.error(f"Template not found: {template}") + if not target.is_file(): + parser.error(f"Target not found: {target}") + + template_blocks, template_errors = parse_blocks(template) + target_blocks, target_errors = parse_blocks(target) + findings: list[list[object]] = [] + + for error in template_errors: + findings.append( + [ + error.section, + error.status, + "-", + "-", + error.detail, + "fix template markers", + ] + ) + + for error in target_errors: + expected = template_blocks.get(error.section) + findings.append( + [ + error.section, + error.status, + expected.block_version if expected else "-", + "-", + error.detail, + "repair target managed markers before refresh", + ] + ) + + for section_name, expected in template_blocks.items(): + actual = target_blocks.get(section_name) + if not actual: + findings.append( + [ + section_name, + "missing", + expected.block_version, + "none", + "template section is absent from target AGENTS.md", + "add managed block after human review", + ] + ) + continue + if not actual.block_version: + findings.append( + [ + section_name, + "missing-block-version", + expected.block_version, + "none", + f"expected {expected.block_version}", + "refresh marker metadata after human review", + ] + ) + elif actual.block_version != expected.block_version: + findings.append( + [ + section_name, + "stale-block-version", + expected.block_version, + actual.block_version, + f"expected {expected.block_version}, found {actual.block_version}", + "refresh managed block after human review", + ] + ) + + if args.no_source_check or not local_source(actual.source): + continue + try: + source_path = confined_path(target.parent, str(actual.source)) + except CheckFailure: + findings.append( + [ + section_name, + "source-outside-workspace", + expected.block_version, + actual.block_version, + f"source {actual.source} escapes {target.parent}", + "move source inside the project before human review", + ] + ) + continue + if not source_path.exists(): + findings.append( + [ + section_name, + "source-missing", + expected.block_version, + actual.block_version, + f"source {actual.source} does not exist relative to {target.parent}", + "verify source path or update block source after human review", + ] + ) + + for section_name, actual in target_blocks.items(): + if section_name not in template_blocks: + findings.append( + [ + section_name, + "unexpected-managed-section", + "none", + actual.block_version, + "target section is not present in template", + "ask whether to keep, migrate, or remove", + ] + ) + + if not findings: + print("PASS root AGENTS managed blocks are current") + return 0 + + print("FAIL root AGENTS drift found\n") + print("| Section | Status | Template Block | Target Block | Detail | Action |") + print("|---|---|---|---|---|---|") + for row in findings: + print(f"| {' | '.join(escape_cell(cell) for cell in row)} |") + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/check-root-agents-blocks.sh b/scripts/check-root-agents-blocks.sh index fad3ae1..bea567b 100755 --- a/scripts/check-root-agents-blocks.sh +++ b/scripts/check-root-agents-blocks.sh @@ -1,236 +1,20 @@ #!/usr/bin/env bash +# DEPRECATED COMPATIBILITY ENTRY: use check-root-agents-blocks.py directly. set -euo pipefail -ruby - "$@" <<'RUBY' -template = nil -target = nil -check_sources = true - -args = ARGV.dup -until args.empty? - arg = args.shift - case arg - when "--template" - template = args.shift - when "--target" - target = args.shift - when "--no-source-check" - check_sources = false - when "-h", "--help" - puts <<~HELP - Usage: check-root-agents-blocks.sh --template --target [--no-source-check] - - Read-only checker for agent-loop root AGENTS managed blocks. - It verifies marker structure, required section presence, and per-section block-version. - HELP - exit 0 - else - warn "Unknown argument: #{arg}" - exit 2 - end -end - -unless template && target - warn "Usage: check-root-agents-blocks.sh --template --target [--no-source-check]" - exit 2 -end - -unless File.file?(template) - warn "Template not found: #{template}" +script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + +if [[ -n "${PYTHON:-}" ]]; then + python_cmd=("$PYTHON") +elif command -v py >/dev/null 2>&1; then + python_cmd=(py -3) +elif command -v python3 >/dev/null 2>&1; then + python_cmd=(python3) +elif command -v python >/dev/null 2>&1; then + python_cmd=(python) +else + printf '%s\n' 'usage error: Python 3.10+ is required' >&2 exit 2 -end - -unless File.file?(target) - warn "Target not found: #{target}" - exit 2 -end - -START_RE = // -END_RE = // - -def attr_value(attrs, name) - match = attrs.match(/(?:^|\s)#{Regexp.escape(name)}:([^\s>]+)/) - match && match[1] -end - -def parse_blocks(path) - blocks = {} - errors = [] - active = nil - - File.readlines(path, chomp: true).each_with_index do |line, idx| - line_no = idx + 1 - - if line.scan(/agent-loop:managed-(?:start|end)/).length > 1 - errors << { section: "(unknown)", status: "malformed-marker", detail: "multiple managed markers on one line at line #{line_no}" } - next - end - - if (match = line.match(START_RE)) - attrs = match[1] - section = attr_value(attrs, "section") - unless section - errors << { section: "(unknown)", status: "malformed-marker", detail: "start marker at line #{line_no} is missing section" } - next - end - - if active - errors << { section: active[:section], status: "broken-markers", detail: "section #{active[:section]} starts at line #{active[:start_line]} but is not closed before line #{line_no}" } - errors << { section: active[:section], status: "nested-managed-block", detail: "section #{active[:section]} starts at line #{active[:start_line]} but a nested section starts at line #{line_no}" } - end - - if blocks.key?(section) - errors << { section: section, status: "duplicate-section", detail: "section #{section} appears more than once" } - end - - active = { - section: section, - attrs: attrs, - source: attr_value(attrs, "source"), - version: attr_value(attrs, "version"), - block_version: attr_value(attrs, "block-version"), - start_line: line_no - } - next - end - - if line.include?("agent-loop:managed-start") - errors << { section: "(unknown)", status: "malformed-marker", detail: "malformed managed-start marker at line #{line_no}" } - next - end - - if (match = line.match(END_RE)) - end_section = match[1] - unless active - errors << { section: end_section, status: "broken-markers", detail: "orphan end marker at line #{line_no}" } - next - end - - if active[:section] != end_section - errors << { section: active[:section], status: "broken-markers", detail: "section #{active[:section]} starts at line #{active[:start_line]} but ends as #{end_section} at line #{line_no}" } - active = nil - next - end - - blocks[active[:section]] = active.merge(end_line: line_no) - active = nil - next - end - - if line.include?("agent-loop:managed-end") - errors << { section: "(unknown)", status: "malformed-marker", detail: "malformed managed-end marker at line #{line_no}" } - next - end - end - - if active - errors << { section: active[:section], status: "broken-markers", detail: "section #{active[:section]} starts at line #{active[:start_line]} but has no end marker" } - end - - [blocks, errors] -end - -def local_source?(source) - return false unless source - return false if source == "agent-loop-skill" - return false if source.start_with?("http://", "https://") - source.include?("/") || source.end_with?(".md") -end - -template_blocks, template_errors = parse_blocks(template) -target_blocks, target_errors = parse_blocks(target) - -findings = [] - -template_errors.each do |err| - findings << [err[:section], err[:status], "-", "-", err[:detail], "fix template markers"] -end - -target_errors.each do |err| - tmpl = template_blocks[err[:section]] - findings << [ - err[:section], - err[:status], - tmpl ? tmpl[:block_version].to_s : "-", - "-", - err[:detail], - "repair target managed markers before refresh" - ] -end - -template_blocks.each do |section, tmpl| - target_block = target_blocks[section] - - unless target_block - findings << [ - section, - "missing", - tmpl[:block_version].to_s, - "none", - "template section is absent from target AGENTS.md", - "add managed block after human review" - ] - next - end - - if target_block[:block_version].nil? || target_block[:block_version].empty? - findings << [ - section, - "missing-block-version", - tmpl[:block_version].to_s, - "none", - "expected #{tmpl[:block_version]}", - "refresh marker metadata after human review" - ] - elsif target_block[:block_version] != tmpl[:block_version] - findings << [ - section, - "stale-block-version", - tmpl[:block_version].to_s, - target_block[:block_version].to_s, - "expected #{tmpl[:block_version]}, found #{target_block[:block_version]}", - "refresh managed block after human review" - ] - end - - next unless check_sources && local_source?(target_block[:source]) - - source_path = File.expand_path(target_block[:source], File.dirname(target)) - unless File.exist?(source_path) - findings << [ - section, - "source-missing", - tmpl[:block_version].to_s, - target_block[:block_version].to_s, - "source #{target_block[:source]} does not exist relative to #{File.dirname(target)}", - "verify source path or update block source after human review" - ] - end -end - -target_blocks.each do |section, target_block| - next if template_blocks.key?(section) - findings << [ - section, - "unexpected-managed-section", - "none", - target_block[:block_version].to_s, - "target section is not present in template", - "ask whether to keep, migrate, or remove" - ] -end - -if findings.empty? - puts "PASS root AGENTS managed blocks are current" - exit 0 -end +fi -puts "FAIL root AGENTS drift found" -puts -puts "| Section | Status | Template Block | Target Block | Detail | Action |" -puts "|---|---|---|---|---|---|" -findings.each do |row| - puts "| #{row.map { |cell| cell.to_s.gsub("|", "\\|") }.join(" | ")} |" -end -exit 1 -RUBY +exec "${python_cmd[@]}" "$script_dir/check-root-agents-blocks.py" "$@" diff --git a/scripts/checker_support.py b/scripts/checker_support.py new file mode 100644 index 0000000..1bcb19e --- /dev/null +++ b/scripts/checker_support.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +import re +import sys +from dataclasses import dataclass +from pathlib import Path + + +@dataclass(frozen=True) +class CheckFailure(Exception): + category: str + detail: str + + def __str__(self) -> str: + return f"{self.category}: {self.detail}" + + +def require_supported_python(version: tuple[int, int] | None = None) -> None: + current = version or sys.version_info[:2] + if current < (3, 10): + print("usage error: Python 3.10+ is required", file=sys.stderr) + raise SystemExit(2) + + +def read_text(path: Path) -> str: + """Read deterministic Markdown text, accepting UTF-8 BOM and CRLF input.""" + with path.open("r", encoding="utf-8-sig", newline=None) as handle: + return handle.read() + + +def metadata(text: str, name: str) -> str | None: + match = re.search(rf"(?mi)^\s*{re.escape(name)}\s*:\s*(.*?)\s*$", text) + return match.group(1).strip() if match else None + + +def optional_section(text: str, heading: str, *, level: int = 2) -> str | None: + marker = "#" * level + pattern = re.compile( + rf"^{re.escape(marker)}\s+{re.escape(heading)}\s*$\n" + rf"(.*?)(?=^#{{1,{level}}}\s+|\Z)", + re.MULTILINE | re.DOTALL, + ) + match = pattern.search(text) + return match.group(1) if match else None + + +def section(text: str, heading: str, *, level: int = 2) -> str: + value = optional_section(text, heading, level=level) + if value is None: + raise CheckFailure("missing section", f"{'#' * level} {heading}") + return value + + +def split_row(line: str) -> list[str]: + value = line.strip() + if not value.startswith("|") or not value.endswith("|"): + raise CheckFailure("invalid-table-row", line) + return [cell.strip() for cell in value[1:-1].split("|")] + + +def table(text: str, heading: str, *, level: int = 2) -> list[dict[str, str]]: + raw_lines = [line.strip() for line in section(text, heading, level=level).splitlines()] + try: + table_start = next(index for index, line in enumerate(raw_lines) if line.startswith("|")) + except StopIteration as error: + raise CheckFailure("missing table in section", heading) from error + lines: list[str] = [] + for line in raw_lines[table_start:]: + if not line.startswith("|"): + break + lines.append(line) + if len(lines) < 3: + raise CheckFailure("missing table in section", heading) + headers = split_row(lines[0]) + rows: list[dict[str, str]] = [] + for line in lines[2:]: + cells = split_row(line) + if len(cells) != len(headers): + raise CheckFailure("column count mismatch in section", heading) + rows.append(dict(zip(headers, cells, strict=True))) + if not rows: + raise CheckFailure("empty table in section", heading) + return rows + + +def confined_path(root: Path, value: str) -> Path: + candidate = Path(value) + resolved = (candidate if candidate.is_absolute() else root / candidate).resolve() + boundary = root.resolve() + try: + resolved.relative_to(boundary) + except ValueError as error: + raise CheckFailure("reference escapes workspace root", value) from error + return resolved diff --git a/tests/checker_test_support.py b/tests/checker_test_support.py new file mode 100644 index 0000000..ca3d27b --- /dev/null +++ b/tests/checker_test_support.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def run_checker( + script: str, *args: str, cwd: Path | None = None +) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [sys.executable, str(ROOT / script), *map(str, args)], + cwd=str(cwd or ROOT), + check=False, + capture_output=True, + text=True, + encoding="utf-8", + ) + + +def combined_output(result: subprocess.CompletedProcess[str]) -> str: + return (result.stdout + result.stderr).strip() diff --git a/tests/test_adr_requirement_model_trace.py b/tests/test_adr_requirement_model_trace.py new file mode 100644 index 0000000..46af5d7 --- /dev/null +++ b/tests/test_adr_requirement_model_trace.py @@ -0,0 +1,265 @@ +from __future__ import annotations + +import re +import shutil +import tempfile +import unittest +from pathlib import Path + +from tests.checker_test_support import ROOT, combined_output, run_checker + + +SCRIPT = "scripts/check-adr-requirement-model-trace.py" +FIXTURES = ROOT / "tests/fixtures/adr-technical-landing" +VALID = FIXTURES / "valid" +NOT_NEEDED = FIXTURES / "valid-not-needed" + + +class AdrRequirementModelTraceTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.readme = (VALID / "README.md").read_text(encoding="utf-8") + cls.source = (VALID / "requirement.md").read_text(encoding="utf-8") + cls.decision = (VALID / "decision.md").read_text(encoding="utf-8") + + def run_documents( + self, + readme: str, + source: str, + decision: str, + *, + fixture: Path = VALID, + bom_crlf: bool = False, + workspace_root: str | None = None, + ): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) / "fixture" + shutil.copytree(fixture, root) + paths = [] + for name, content in ( + ("README.md", readme), + ("requirement.md", source), + ("decision.md", decision), + ): + path = root / name + if bom_crlf: + path.write_bytes(("\ufeff" + content.replace("\n", "\r\n")).encode("utf-8")) + else: + path.write_text(content, encoding="utf-8") + paths.append(str(path)) + args = [*paths] + if workspace_root is not None: + args.append(workspace_root.replace("{root}", str(root))) + return run_checker(SCRIPT, *args) + + def assert_accepted( + self, readme: str, source: str, decision: str, expected: str + ) -> None: + result = self.run_documents(readme, source, decision) + self.assertEqual(result.returncode, 0, combined_output(result)) + self.assertIn(expected, result.stdout) + + def assert_rejected( + self, readme: str, source: str, decision: str, expected: str + ) -> None: + result = self.run_documents(readme, source, decision) + self.assertEqual(result.returncode, 1, combined_output(result)) + self.assertIn(expected, combined_output(result)) + + def test_accepted_and_proposed_valid_decisions_pass(self) -> None: + self.assert_accepted( + self.readme, + self.source, + self.decision, + "ADR accepted technical landing trace covers 8", + ) + proposed = re.sub(r"^Status: accepted$", "Status: proposed", self.decision, flags=re.MULTILINE) + proposed = re.sub( + r"^## Human Review Evidence\n.*?(?=^## |\Z)", + "", + proposed, + flags=re.MULTILINE | re.DOTALL, + ) + self.assert_accepted( + self.readme, + self.source, + proposed, + "ADR proposed technical landing trace covers 8", + ) + + def test_accepted_decision_requires_human_review_evidence(self) -> None: + decision = re.sub( + r"^## Human Review Evidence\n.*?(?=^## |\Z)", + "", + self.decision, + flags=re.MULTILINE | re.DOTALL, + ) + self.assert_rejected( + self.readme, self.source, decision, "missing section: ## Human Review Evidence" + ) + + def test_valid_not_needed_preflight_passes(self) -> None: + result = self.run_documents( + (NOT_NEEDED / "README.md").read_text(encoding="utf-8"), + (NOT_NEEDED / "requirement.md").read_text(encoding="utf-8"), + (NOT_NEEDED / "decision.md").read_text(encoding="utf-8"), + fixture=NOT_NEEDED, + ) + self.assertEqual(result.returncode, 0, combined_output(result)) + self.assertIn("reasoned concept-foundation-not-needed ADR proposed gate", result.stdout) + + def test_owner_paths_support_existing_and_explicitly_planned_artifacts(self) -> None: + planned = self.decision.replace( + "features/fixture/spec.md", "planned:features/future-fixture/spec.md" + ) + self.assert_accepted( + self.readme, self.source, planned, "technical landing trace covers 8" + ) + delegated = self.decision.replace(", FLOW-FIXTURE-01", "", 1) + delegated = delegated.replace( + "| FLOW-FIXTURE-01 | in-scope | ADR-9000 |", + "| FLOW-FIXTURE-01 | proposed-decision | decisions/9001-follow-up.md |", + ) + delegated = re.sub( + r"^\| FLOW-FIXTURE-01 \| source flow reference .*\n", + "", + delegated, + flags=re.MULTILINE, + ) + self.assert_accepted( + self.readme, self.source, delegated, "technical landing trace covers 7" + ) + + def test_adversarial_contracts_are_rejected(self) -> None: + cases = ( + ( + self.decision.replace( + "reason: outside this coherent decision boundary", "reason: n/a" + ), + "must give a concrete reason", + ), + ( + re.sub( + r"## Coverage Hard Gate\n.*?(?=^## |\Z)", + "## Coverage Hard Gate\n\n- [x] arbitrary check\n\n", + self.decision, + flags=re.MULTILINE | re.DOTALL, + ), + "Coverage Hard Gate is missing required items", + ), + ( + self.decision.replace( + "- [x] No unresolved product-semantic blocker remains", + "- [x] No unresolved product-semantic blocker remains\n" + "- [x] arbitrary extra check", + ), + "Coverage Hard Gate contains unsupported items", + ), + ( + re.sub( + r"^Accepted Requirement Model IDs:(.*)$", + r"Accepted Requirement Model IDs:\1, GARBAGE", + self.decision, + flags=re.MULTILINE, + ), + "invalid values in ADR Accepted Requirement Model IDs: GARBAGE", + ), + ( + re.sub( + r"^\| PM-FIXTURE-FACT .*\n", + "", + self.decision.replace(", PM-FIXTURE-FACT", ""), + flags=re.MULTILINE, + ), + "Requirement Model Scope Inventory mismatch", + ), + ( + self.decision.replace( + "decisions/8999-shared.md (ADR-8999)", + "decisions/does-not-exist.md (ADR-DOES-NOT-EXIST)", + ), + "missing file:", + ), + ( + self.decision.replace( + "features/fixture/spec.md", "features/missing/spec.md" + ), + "missing file:", + ), + ( + self.decision.replace("| planned |", "| banana |", 1), + "invalid coverage status: banana", + ), + ( + re.sub( + r"\n## Triggered Operational Landing\n.*?(?=\n## Design Slice Coverage)", + "", + self.decision, + flags=re.DOTALL, + ), + "Triggered Operational Landing is missing", + ), + ( + re.sub( + r"^\| (?:Compatibility|Rollout / Cutover|Rollback / Reversibility) \|.*\n", + "", + self.decision, + flags=re.MULTILINE, + ), + "operational concern inventory mismatch", + ), + ) + for decision, expected in cases: + with self.subTest(expected=expected): + self.assert_rejected(self.readme, self.source, decision, expected) + + def test_existing_invalid_decisions_and_sources_are_rejected(self) -> None: + cases = ( + ( + self.readme, + self.source, + (FIXTURES / "invalid-missing-coverage/decision.md").read_text(encoding="utf-8"), + ), + ( + self.readme, + self.source, + (FIXTURES / "invalid-empty-landing/decision.md").read_text(encoding="utf-8"), + ), + ( + (FIXTURES / "invalid-unaccepted-source/README.md").read_text(encoding="utf-8"), + (FIXTURES / "invalid-unaccepted-source/requirement.md").read_text(encoding="utf-8"), + self.decision, + ), + ( + (FIXTURES / "invalid-reopened-source/README.md").read_text(encoding="utf-8"), + (FIXTURES / "invalid-reopened-source/requirement.md").read_text(encoding="utf-8"), + self.decision, + ), + ( + self.readme, + self.source, + (FIXTURES / "invalid-review-required/decision.md").read_text(encoding="utf-8"), + ), + ) + for readme, source, decision in cases: + with self.subTest(decision=decision[:40]): + result = self.run_documents(readme, source, decision) + self.assertEqual(result.returncode, 1, combined_output(result)) + + def test_workspace_escape_is_rejected(self) -> None: + decision = self.decision.replace( + "decisions/8999-shared.md (ADR-8999)", "../outside.md (ADR-OUTSIDE)" + ) + self.assert_rejected( + self.readme, self.source, decision, "reference escapes workspace root" + ) + + def test_bom_and_crlf_are_supported(self) -> None: + result = self.run_documents( + self.readme, self.source, self.decision, bom_crlf=True + ) + self.assertEqual(result.returncode, 0, combined_output(result)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_concept_foundation_trace.py b/tests/test_concept_foundation_trace.py new file mode 100644 index 0000000..222eaed --- /dev/null +++ b/tests/test_concept_foundation_trace.py @@ -0,0 +1,202 @@ +from __future__ import annotations + +import re +import tempfile +import unittest +from pathlib import Path + +from tests.checker_test_support import ROOT, combined_output, run_checker + + +SCRIPT = "scripts/check-concept-foundation-trace.py" +EXAMPLE = ROOT / "examples/concept-foundation-refund" +FIXTURES = ROOT / "tests/fixtures/concept-foundation" + + +class ConceptFoundationTraceTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.requirement = (EXAMPLE / "requirement.md").read_text(encoding="utf-8") + cls.product = (EXAMPLE / "product.md").read_text(encoding="utf-8") + cls.spec = (EXAMPLE / "spec.md").read_text(encoding="utf-8") + + def run_documents( + self, + requirement: str, + product: str, + spec: str, + *, + bom_crlf: bool = False, + ): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + paths = [] + for name, content in ( + ("requirement.md", requirement), + ("product.md", product), + ("spec.md", spec), + ): + path = root / name + if bom_crlf: + path.write_bytes(("\ufeff" + content.replace("\n", "\r\n")).encode("utf-8")) + else: + path.write_text(content, encoding="utf-8") + paths.append(str(path)) + return run_checker(SCRIPT, *paths) + + def assert_rejected( + self, requirement: str, product: str, spec: str, expected: str + ) -> None: + result = self.run_documents(requirement, product, spec) + self.assertEqual(result.returncode, 1, combined_output(result)) + self.assertIn(expected, combined_output(result)) + + def test_accepted_example_passes(self) -> None: + result = self.run_documents(self.requirement, self.product, self.spec) + self.assertEqual(result.returncode, 0, combined_output(result)) + self.assertIn("accepted Concept Foundation trace is complete", result.stdout) + + def test_reasoned_not_needed_passes(self) -> None: + requirement = self.requirement.replace( + "Concept Foundation Status: accepted", + "Concept Foundation Status: concept-foundation-not-needed", + ) + requirement = re.sub( + r"^Not-Needed Reason:.*$", + "Not-Needed Reason: only one stable term exists and no product model is derived", + requirement, + flags=re.MULTILINE, + ) + product = self.product.replace( + "Concept Foundation Status: accepted", + "Concept Foundation Status: concept-foundation-not-needed", + ) + spec = self.spec.replace( + "Concept Foundation Status: accepted", + "Concept Foundation Status: concept-foundation-not-needed", + ) + result = self.run_documents(requirement, product, spec) + self.assertEqual(result.returncode, 0, combined_output(result)) + self.assertIn("reasoned concept-foundation-not-needed trace", result.stdout) + + def test_existing_invalid_fixtures_are_rejected(self) -> None: + for fixture in ("invalid-unaccepted", "invalid-detached-model"): + with self.subTest(fixture=fixture): + root = FIXTURES / fixture + result = run_checker( + SCRIPT, + str(root / "requirement.md"), + str(root / "product.md"), + str(root / "spec.md"), + ) + self.assertEqual(result.returncode, 1, combined_output(result)) + + def test_adversarial_semantic_breaks_are_rejected(self) -> None: + definition = self.requirement.split("## Concept Definitions\n", 1)[1] + concept_line = next( + line for line in definition.splitlines(keepends=True) if line.startswith("| C-CUSTOMER |") + ) + cases = ( + ( + re.sub( + r"^- Confirmed Concept IDs:.*$", + "- Confirmed Concept IDs: C-CUSTOMER", + self.requirement, + flags=re.MULTILINE, + ), + self.product, + self.spec, + "unconfirmed Concept IDs", + ), + ( + re.sub( + r"^## Concept Candidate Inventory\n.*?(?=^## Concept Definitions\n)", + "", + self.requirement, + flags=re.MULTILINE | re.DOTALL, + ), + self.product, + self.spec, + "missing section: ## Concept Candidate Inventory", + ), + ( + self.requirement.replace("| resolved |", "| open |", 1), + self.product, + self.spec, + "Blocking Ambiguities contains unresolved rows", + ), + ( + re.sub(r"^\| TRACE-03 .*\n", "", self.requirement, flags=re.MULTILINE), + self.product, + self.spec, + "untraced product model IDs", + ), + ( + self.requirement.replace(concept_line, concept_line * 2, 1), + self.product, + self.spec, + "duplicate IDs in Concept Definitions", + ), + ( + self.requirement, + re.sub( + r"^Effective Concept Source:.*\n", + "", + self.product, + flags=re.MULTILINE, + ), + self.spec, + "product Effective Concept Source is missing", + ), + ( + re.sub( + r"^\| PERM-ADMIN-SETTLEMENT \| C-REFUND-ADMIN " + r"\| C-REFUND-SETTLEMENT .*\n", + "", + self.requirement, + flags=re.MULTILINE, + ), + self.product, + self.spec, + "Commands / Events missing Role / Permission Matrix pairs", + ), + ) + for requirement, product, spec, expected in cases: + with self.subTest(expected=expected): + self.assert_rejected(requirement, product, spec, expected) + + def test_placeholder_not_needed_reason_is_rejected(self) -> None: + requirement = self.requirement.replace( + "Concept Foundation Status: accepted", + "Concept Foundation Status: concept-foundation-not-needed", + ) + requirement = re.sub( + r"^Not-Needed Reason:.*$", + "Not-Needed Reason: n/a", + requirement, + flags=re.MULTILINE, + ) + product = self.product.replace( + "Concept Foundation Status: accepted", + "Concept Foundation Status: concept-foundation-not-needed", + ) + spec = self.spec.replace( + "Concept Foundation Status: accepted", + "Concept Foundation Status: concept-foundation-not-needed", + ) + self.assert_rejected( + requirement, + product, + spec, + "concept-foundation-not-needed requires a concrete reason", + ) + + def test_bom_and_crlf_are_supported(self) -> None: + result = self.run_documents( + self.requirement, self.product, self.spec, bom_crlf=True + ) + self.assertEqual(result.returncode, 0, combined_output(result)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_onboarding_core_flow_coverage.py b/tests/test_onboarding_core_flow_coverage.py new file mode 100644 index 0000000..a413ffc --- /dev/null +++ b/tests/test_onboarding_core_flow_coverage.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +import shutil +import tempfile +import unittest +from pathlib import Path + +from tests.checker_test_support import ROOT, combined_output, run_checker + + +SCRIPT = "scripts/check-onboarding-core-flow-coverage.py" +EXAMPLE = ROOT / "examples/ai-meeting-minutes-backend/onboarding-db" +FIXTURES = ROOT / "tests/fixtures/onboarding-core-flow" + + +class OnboardingCoreFlowCoverageTests(unittest.TestCase): + def assert_passes(self, root: Path, expected: str) -> None: + result = run_checker(SCRIPT, str(root)) + self.assertEqual(result.returncode, 0, combined_output(result)) + self.assertIn(expected, result.stdout) + + def assert_fails(self, root: Path, expected: str) -> None: + result = run_checker(SCRIPT, str(root)) + self.assertEqual(result.returncode, 1, combined_output(result)) + self.assertIn(expected, combined_output(result)) + + def test_planned_example_passes(self) -> None: + self.assert_passes(EXAMPLE, "1 planned, 0 deferred") + + def test_reasoned_deferred_flow_passes(self) -> None: + self.assert_passes(FIXTURES / "valid-deferred", "0 planned, 1 deferred") + + def test_missing_recovery_slice_fails(self) -> None: + self.assert_fails( + FIXTURES / "invalid-missing-recovery", + "missing required slice: CF-ORDER-PAYMENT/S07", + ) + + def test_detached_diagram_trace_fails(self) -> None: + self.assert_fails( + FIXTURES / "invalid-detached-trace", + "missing diagram definition: D-RECOVERY", + ) + + def test_bom_and_crlf_are_supported(self) -> None: + with tempfile.TemporaryDirectory() as temp: + copied = Path(temp) / "onboarding-db" + shutil.copytree(EXAMPLE, copied) + for path in copied.rglob("*.md"): + text = path.read_text(encoding="utf-8") + path.write_bytes(("\ufeff" + text.replace("\n", "\r\n")).encode("utf-8")) + self.assert_passes(copied, "1 planned, 0 deferred") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_python_checker_contract.py b/tests/test_python_checker_contract.py new file mode 100644 index 0000000..d4f3a61 --- /dev/null +++ b/tests/test_python_checker_contract.py @@ -0,0 +1,199 @@ +from __future__ import annotations + +import ast +import hashlib +import io +import shutil +import sys +import tempfile +import unittest +from contextlib import redirect_stderr +from pathlib import Path + +from tests.checker_test_support import ROOT, combined_output, run_checker + + +CHECKERS = ( + "scripts/check-root-agents-blocks.py", + "scripts/check-onboarding-core-flow-coverage.py", + "scripts/check-concept-foundation-trace.py", + "scripts/check-adr-requirement-model-trace.py", +) + +WORKFLOW = ROOT / ".github/workflows/cross-platform-checkers.yml" + +COMPATIBILITY_ENTRIES = { + "scripts/check-root-agents-blocks.sh": "check-root-agents-blocks.py", + "scripts/check-onboarding-core-flow-coverage.rb": "check-onboarding-core-flow-coverage.py", + "scripts/check-concept-foundation-trace.rb": "check-concept-foundation-trace.py", + "scripts/check-adr-requirement-model-trace.rb": "check-adr-requirement-model-trace.py", +} + +CURRENT_AUTHORITY = ( + "SKILL.md", + "Usage.md", + "references/project-guidance.md", + "references/workflow-checklists.md", +) + + +class PythonCheckerContractTests(unittest.TestCase): + @staticmethod + def snapshot(root: Path) -> dict[str, str]: + return { + path.relative_to(root).as_posix(): hashlib.sha256(path.read_bytes()).hexdigest() + for path in sorted(root.rglob("*")) + if path.is_file() + } + + def test_python_runtime_is_supported(self) -> None: + self.assertGreaterEqual(sys.version_info[:2], (3, 10)) + + def test_canonical_checker_files_exist(self) -> None: + for relative in CHECKERS: + with self.subTest(relative=relative): + self.assertTrue((ROOT / relative).is_file(), relative) + + def test_canonical_checkers_use_only_stdlib_and_local_support(self) -> None: + allowed_local = {"checker_support"} + for relative in CHECKERS: + path = ROOT / relative + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + imported: set[str] = set() + for node in ast.walk(tree): + if isinstance(node, ast.Import): + imported.update(alias.name.split(".", 1)[0] for alias in node.names) + elif isinstance(node, ast.ImportFrom) and node.module: + imported.add(node.module.split(".", 1)[0]) + external = imported - set(sys.stdlib_module_names) - allowed_local + self.assertEqual(external, set(), f"{relative}: {sorted(external)}") + + def test_missing_arguments_fail_with_usage_exit_two(self) -> None: + for relative in CHECKERS: + with self.subTest(relative=relative): + result = run_checker(relative) + self.assertEqual(result.returncode, 2, combined_output(result)) + self.assertIn("usage", combined_output(result).lower()) + + def test_unsupported_python_fails_closed_with_exit_two(self) -> None: + scripts_dir = str(ROOT / "scripts") + if scripts_dir not in sys.path: + sys.path.insert(0, scripts_dir) + from checker_support import require_supported_python + + stderr = io.StringIO() + with redirect_stderr(stderr), self.assertRaises(SystemExit) as caught: + require_supported_python((3, 9)) + self.assertEqual(caught.exception.code, 2) + self.assertIn("Python 3.10+ is required", stderr.getvalue()) + + for relative in CHECKERS: + with self.subTest(relative=relative): + path = ROOT / relative + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + calls = { + node.func.id + for node in ast.walk(tree) + if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) + } + self.assertIn("require_supported_python", calls) + + def test_cross_platform_ci_runs_the_native_suite(self) -> None: + self.assertTrue(WORKFLOW.is_file(), str(WORKFLOW)) + content = WORKFLOW.read_text(encoding="utf-8") + for required in ( + "macos-latest", + "windows-latest", + '"3.10"', + '"3.x"', + "tests.test_python_checker_contract", + "tests.test_root_agents_blocks", + "tests.test_onboarding_core_flow_coverage", + "tests.test_concept_foundation_trace", + "tests.test_adr_requirement_model_trace", + ): + with self.subTest(required=required): + self.assertIn(required, content) + + def test_old_entrypoints_are_marked_thin_compatibility_launchers(self) -> None: + for relative, canonical in COMPATIBILITY_ENTRIES.items(): + with self.subTest(relative=relative): + content = (ROOT / relative).read_text(encoding="utf-8") + self.assertIn("DEPRECATED COMPATIBILITY ENTRY", content) + self.assertIn(canonical, content) + self.assertIn("py", content) + self.assertIn("-3", content) + self.assertLessEqual(len(content.splitlines()), 24) + + def test_current_authority_does_not_recommend_legacy_entrypoints(self) -> None: + legacy_paths = tuple(COMPATIBILITY_ENTRIES) + for relative in CURRENT_AUTHORITY: + content = (ROOT / relative).read_text(encoding="utf-8") + with self.subTest(relative=relative): + for legacy in legacy_paths: + self.assertNotIn(legacy, content) + changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8") + for canonical in CHECKERS: + with self.subTest(canonical=canonical): + self.assertIn(canonical, changelog) + + def test_valid_checker_runs_do_not_mutate_checked_artifacts(self) -> None: + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + + root_case = root / "root-agents" + (root_case / ".agent-loop").mkdir(parents=True) + (root_case / ".agent-loop/project.md").touch() + template = root_case / "template.md" + target = root_case / "AGENTS.md" + shutil.copy2(ROOT / "templates/root-AGENTS.md", template) + shutil.copy2(template, target) + + onboarding = root / "onboarding" + shutil.copytree( + ROOT / "examples/ai-meeting-minutes-backend/onboarding-db", onboarding + ) + + concept = root / "concept" + shutil.copytree(ROOT / "examples/concept-foundation-refund", concept) + + adr = root / "adr" + shutil.copytree(ROOT / "tests/fixtures/adr-technical-landing/valid", adr) + + runs = ( + ( + "scripts/check-root-agents-blocks.py", + "--template", + str(template), + "--target", + str(target), + ), + ("scripts/check-onboarding-core-flow-coverage.py", str(onboarding)), + ( + "scripts/check-concept-foundation-trace.py", + str(concept / "requirement.md"), + str(concept / "product.md"), + str(concept / "spec.md"), + ), + ( + "scripts/check-adr-requirement-model-trace.py", + str(adr / "README.md"), + str(adr / "requirement.md"), + str(adr / "decision.md"), + ), + ) + before = self.snapshot(root) + for script, *args in runs: + first = run_checker(script, *args) + second = run_checker(script, *args) + self.assertEqual(first.returncode, 0, combined_output(first)) + self.assertEqual(second.returncode, 0, combined_output(second)) + self.assertEqual( + (first.returncode, first.stdout, first.stderr), + (second.returncode, second.stdout, second.stderr), + ) + self.assertEqual(self.snapshot(root), before) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_root_agents_blocks.py b/tests/test_root_agents_blocks.py new file mode 100644 index 0000000..b681e90 --- /dev/null +++ b/tests/test_root_agents_blocks.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +import tempfile +import unittest +from pathlib import Path + +from tests.checker_test_support import ROOT, combined_output, run_checker + + +SCRIPT = "scripts/check-root-agents-blocks.py" +TEMPLATE = ROOT / "templates/root-AGENTS.md" + + +class RootAgentsBlocksTests(unittest.TestCase): + def setUp(self) -> None: + self.tempdir = tempfile.TemporaryDirectory() + self.addCleanup(self.tempdir.cleanup) + self.root = Path(self.tempdir.name) + (self.root / ".agent-loop").mkdir() + (self.root / ".agent-loop/project.md").touch() + self.template_text = TEMPLATE.read_text(encoding="utf-8") + + def check(self, text: str, *extra: str): + target = self.root / "AGENTS.md" + target.write_text(text, encoding="utf-8") + return run_checker( + SCRIPT, + "--template", + str(TEMPLATE), + "--target", + str(target), + *extra, + ) + + def remove_section(self, section: str) -> str: + start = f"" + lines: list[str] = [] + skipping = False + for line in self.template_text.splitlines(keepends=True): + if line.startswith(start): + skipping = True + continue + if line.rstrip("\r\n") == end: + skipping = False + continue + if not skipping: + lines.append(line) + return "".join(lines) + + def assert_invalid(self, text: str, expected: str) -> None: + result = self.check(text) + self.assertEqual(result.returncode, 1, combined_output(result)) + self.assertIn("FAIL root AGENTS drift found", result.stdout) + self.assertIn(expected, result.stdout) + + def test_current_template_passes(self) -> None: + result = self.check(self.template_text) + self.assertEqual(result.returncode, 0, combined_output(result)) + self.assertIn("PASS root AGENTS managed blocks are current", result.stdout) + + def test_missing_required_sections_fail(self) -> None: + for section in ("message-intent", "workflow-stage-map"): + with self.subTest(section=section): + self.assert_invalid(self.remove_section(section), f"{section} | missing") + + def test_stale_block_version_fails(self) -> None: + stale = self.template_text.replace( + "block-version:1.3.0-20260713.2", + "block-version:1.3.0", + 1, + ) + self.assert_invalid(stale, "stale-block-version") + + def test_broken_end_marker_fails(self) -> None: + broken = self.template_text.replace( + "", "", 1 + ) + self.assert_invalid(broken, "ownership | broken-markers") + + def test_nested_block_fails(self) -> None: + marker = "\n" + + "nested\n\n" + + self.template_text[line_end:] + ) + self.assert_invalid(nested, "ownership | nested-managed-block") + + def test_duplicate_section_fails(self) -> None: + marker = "" + duplicate = ( + f"{marker}\n\n" + f"duplicate\n{marker}" + ) + self.assert_invalid(self.template_text.replace(marker, duplicate, 1), "duplicate-section") + + def test_source_path_outside_project_fails_even_when_it_exists(self) -> None: + outside = self.root.parent / f"{self.root.name}-outside.md" + outside.write_text("outside", encoding="utf-8") + self.addCleanup(outside.unlink, missing_ok=True) + escaped = self.template_text.replace( + "source:.agent-loop/project.md", + f"source:../{outside.name}", + 1, + ) + self.assert_invalid(escaped, "source-outside-workspace") + + def test_bom_and_crlf_are_supported(self) -> None: + target = self.root / "AGENTS.md" + target.write_bytes(("\ufeff" + self.template_text.replace("\n", "\r\n")).encode("utf-8")) + result = run_checker( + SCRIPT, "--template", str(TEMPLATE), "--target", str(target) + ) + self.assertEqual(result.returncode, 0, combined_output(result)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/validate-adr-requirement-model-technical-landing-trace.sh b/tests/validate-adr-requirement-model-technical-landing-trace.sh index f8ddc8d..709e0b4 100644 --- a/tests/validate-adr-requirement-model-technical-landing-trace.sh +++ b/tests/validate-adr-requirement-model-technical-landing-trace.sh @@ -121,41 +121,41 @@ fi # The generic template and validator must not learn fixture-specific business/action/technology values. for forbidden in FixtureSubject FixtureOperator perform_fixture_action FixtureStore FixtureProtocol; do assert_not_contains "templates/decision.md" "$forbidden" - assert_not_contains "scripts/check-adr-requirement-model-trace.rb" "$forbidden" + assert_not_contains "scripts/check-adr-requirement-model-trace.py" "$forbidden" done # Behavioral validation, not keyword-only assertions. -assert_file_exists "scripts/check-adr-requirement-model-trace.rb" +assert_file_exists "scripts/check-adr-requirement-model-trace.py" valid="$root/tests/fixtures/adr-technical-landing/valid" -ruby "$root/scripts/check-adr-requirement-model-trace.rb" "$valid/README.md" "$valid/requirement.md" "$valid/decision.md" -ruby "$root/tests/validate-adr-requirement-model-trace-adversarial.rb" +python3 "$root/scripts/check-adr-requirement-model-trace.py" "$valid/README.md" "$valid/requirement.md" "$valid/decision.md" +(cd "$root" && python3 -m unittest tests/test_adr_requirement_model_trace.py) not_needed="$root/tests/fixtures/adr-technical-landing/valid-not-needed" -ruby "$root/scripts/check-adr-requirement-model-trace.rb" "$not_needed/README.md" "$not_needed/requirement.md" "$not_needed/decision.md" +python3 "$root/scripts/check-adr-requirement-model-trace.py" "$not_needed/README.md" "$not_needed/requirement.md" "$not_needed/decision.md" -if ruby "$root/scripts/check-adr-requirement-model-trace.rb" "$valid/README.md" "$valid/requirement.md" "$root/tests/fixtures/adr-technical-landing/invalid-missing-coverage/decision.md" >/dev/null 2>&1; then +if python3 "$root/scripts/check-adr-requirement-model-trace.py" "$valid/README.md" "$valid/requirement.md" "$root/tests/fixtures/adr-technical-landing/invalid-missing-coverage/decision.md" >/dev/null 2>&1; then printf 'FAIL: missing-coverage ADR fixture unexpectedly passed\n' >&2 exit 1 fi -if ruby "$root/scripts/check-adr-requirement-model-trace.rb" "$valid/README.md" "$valid/requirement.md" "$root/tests/fixtures/adr-technical-landing/invalid-empty-landing/decision.md" >/dev/null 2>&1; then +if python3 "$root/scripts/check-adr-requirement-model-trace.py" "$valid/README.md" "$valid/requirement.md" "$root/tests/fixtures/adr-technical-landing/invalid-empty-landing/decision.md" >/dev/null 2>&1; then printf 'FAIL: empty-landing ADR fixture unexpectedly passed\n' >&2 exit 1 fi unaccepted="$root/tests/fixtures/adr-technical-landing/invalid-unaccepted-source" -if ruby "$root/scripts/check-adr-requirement-model-trace.rb" "$unaccepted/README.md" "$unaccepted/requirement.md" "$valid/decision.md" >/dev/null 2>&1; then +if python3 "$root/scripts/check-adr-requirement-model-trace.py" "$unaccepted/README.md" "$unaccepted/requirement.md" "$valid/decision.md" >/dev/null 2>&1; then printf 'FAIL: unaccepted-source ADR fixture unexpectedly passed\n' >&2 exit 1 fi reopened="$root/tests/fixtures/adr-technical-landing/invalid-reopened-source" -if ruby "$root/scripts/check-adr-requirement-model-trace.rb" "$reopened/README.md" "$reopened/requirement.md" "$valid/decision.md" >/dev/null 2>&1; then +if python3 "$root/scripts/check-adr-requirement-model-trace.py" "$reopened/README.md" "$reopened/requirement.md" "$valid/decision.md" >/dev/null 2>&1; then printf 'FAIL: reopened-source ADR fixture unexpectedly passed\n' >&2 exit 1 fi -if ruby "$root/scripts/check-adr-requirement-model-trace.rb" "$valid/README.md" "$valid/requirement.md" "$root/tests/fixtures/adr-technical-landing/invalid-review-required/decision.md" >/dev/null 2>&1; then +if python3 "$root/scripts/check-adr-requirement-model-trace.py" "$valid/README.md" "$valid/requirement.md" "$root/tests/fixtures/adr-technical-landing/invalid-review-required/decision.md" >/dev/null 2>&1; then printf 'FAIL: review-required ADR fixture unexpectedly passed\n' >&2 exit 1 fi diff --git a/tests/validate-concept-foundation-requirement-modeling.sh b/tests/validate-concept-foundation-requirement-modeling.sh index f89b391..fbec9ce 100644 --- a/tests/validate-concept-foundation-requirement-modeling.sh +++ b/tests/validate-concept-foundation-requirement-modeling.sh @@ -124,16 +124,16 @@ if [ -e "$root/.agent-loop/concepts" ] || [ -e "$root/.agent-loop" ]; then fi # Behavioral artifact trace: valid chain passes; unaccepted and detached chains fail. -ruby "$root/scripts/check-concept-foundation-trace.rb" \ +python3 "$root/scripts/check-concept-foundation-trace.py" \ "$root/examples/concept-foundation-refund/requirement.md" \ "$root/examples/concept-foundation-refund/product.md" \ "$root/examples/concept-foundation-refund/spec.md" assert_contains "examples/concept-foundation-refund/requirement.md" "| PERM-ADMIN-SETTLEMENT | C-REFUND-ADMIN | C-REFUND-SETTLEMENT |" -ruby "$root/tests/validate-concept-foundation-trace-adversarial.rb" +(cd "$root" && python3 -m unittest tests/test_concept_foundation_trace.py) -if ruby "$root/scripts/check-concept-foundation-trace.rb" \ +if python3 "$root/scripts/check-concept-foundation-trace.py" \ "$root/tests/fixtures/concept-foundation/invalid-unaccepted/requirement.md" \ "$root/tests/fixtures/concept-foundation/invalid-unaccepted/product.md" \ "$root/tests/fixtures/concept-foundation/invalid-unaccepted/spec.md" >/dev/null 2>&1; then @@ -141,7 +141,7 @@ if ruby "$root/scripts/check-concept-foundation-trace.rb" \ exit 1 fi -if ruby "$root/scripts/check-concept-foundation-trace.rb" \ +if python3 "$root/scripts/check-concept-foundation-trace.py" \ "$root/tests/fixtures/concept-foundation/invalid-detached-model/requirement.md" \ "$root/tests/fixtures/concept-foundation/invalid-detached-model/product.md" \ "$root/tests/fixtures/concept-foundation/invalid-detached-model/spec.md" >/dev/null 2>&1; then diff --git a/tests/validate-onboarding-core-flow-completeness.sh b/tests/validate-onboarding-core-flow-completeness.sh index 9029423..58e8767 100644 --- a/tests/validate-onboarding-core-flow-completeness.sh +++ b/tests/validate-onboarding-core-flow-completeness.sh @@ -119,7 +119,7 @@ assert_contains "Usage.md" "Timeline / Sequence" assert_contains "CHANGELOG.md" "Core Flow Completeness" # Executable artifact-level contract. -assert_file_exists "scripts/check-onboarding-core-flow-coverage.rb" +assert_file_exists "scripts/check-onboarding-core-flow-coverage.py" assert_file_exists "examples/ai-meeting-minutes-backend/onboarding-db/08-review/evidence-graph.md" assert_file_exists "examples/ai-meeting-minutes-backend/onboarding-db/onboarding-spec.md" assert_file_exists "examples/ai-meeting-minutes-backend/onboarding-db/onboarding-tasks.md" @@ -144,12 +144,12 @@ assert_file_exists "tests/fixtures/onboarding-core-flow/valid-deferred/onboardin assert_file_exists "tests/fixtures/onboarding-core-flow/valid-deferred/coverage-matrix.md" assert_file_exists "tests/fixtures/onboarding-core-flow/valid-deferred/batch-review.md" -ruby "$ROOT/scripts/check-onboarding-core-flow-coverage.rb" \ +python3 "$ROOT/scripts/check-onboarding-core-flow-coverage.py" \ "$ROOT/examples/ai-meeting-minutes-backend/onboarding-db" invalid_output="$(mktemp)" trap 'rm -f "$invalid_output"' EXIT -if ruby "$ROOT/scripts/check-onboarding-core-flow-coverage.rb" \ +if python3 "$ROOT/scripts/check-onboarding-core-flow-coverage.py" \ "$ROOT/tests/fixtures/onboarding-core-flow/invalid-missing-recovery" \ >"$invalid_output" 2>&1; then echo "invalid fixture unexpectedly passed" >&2 @@ -164,7 +164,7 @@ fi detached_output="$(mktemp)" trap 'rm -f "$invalid_output" "$detached_output"' EXIT -if ruby "$ROOT/scripts/check-onboarding-core-flow-coverage.rb" \ +if python3 "$ROOT/scripts/check-onboarding-core-flow-coverage.py" \ "$ROOT/tests/fixtures/onboarding-core-flow/invalid-detached-trace" \ >"$detached_output" 2>&1; then echo "detached-trace fixture unexpectedly passed" >&2 @@ -177,7 +177,7 @@ if ! grep -Fq "missing diagram definition: D-RECOVERY" "$detached_output"; then exit 1 fi -ruby "$ROOT/scripts/check-onboarding-core-flow-coverage.rb" \ +python3 "$ROOT/scripts/check-onboarding-core-flow-coverage.py" \ "$ROOT/tests/fixtures/onboarding-core-flow/valid-deferred" echo "PASS: onboarding core-flow completeness contract is complete" diff --git a/tests/validate-root-agents-block-checker.sh b/tests/validate-root-agents-block-checker.sh index 38c6642..d32f7d2 100755 --- a/tests/validate-root-agents-block-checker.sh +++ b/tests/validate-root-agents-block-checker.sh @@ -2,7 +2,7 @@ set -euo pipefail root=$(cd "$(dirname "$0")/.." && pwd) -checker="$root/scripts/check-root-agents-blocks.sh" +checker="$root/scripts/check-root-agents-blocks.py" template="$root/templates/root-AGENTS.md" tmpdir=$(mktemp -d) trap 'rm -rf "$tmpdir"' EXIT @@ -40,8 +40,8 @@ remove_section() { ' "$source" > "$target" } -if [ ! -x "$checker" ]; then - printf 'FAIL: checker script is missing or not executable: %s\n' "$checker" >&2 +if [ ! -f "$checker" ]; then + printf 'FAIL: checker script is missing: %s\n' "$checker" >&2 exit 1 fi @@ -49,12 +49,12 @@ cp "$template" "$tmpdir/ok.md" mkdir -p "$tmpdir/.agent-loop" touch "$tmpdir/.agent-loop/project.md" -"$checker" --template "$template" --target "$tmpdir/ok.md" > "$tmpdir/ok.out" +python3 "$checker" --template "$template" --target "$tmpdir/ok.md" > "$tmpdir/ok.out" assert_contains "$tmpdir/ok.out" "PASS root AGENTS managed blocks are current" assert_not_contains "$tmpdir/ok.out" "FAIL root AGENTS drift found" remove_section "message-intent" "$template" "$tmpdir/missing.md" -if "$checker" --template "$template" --target "$tmpdir/missing.md" > "$tmpdir/missing.out"; then +if python3 "$checker" --template "$template" --target "$tmpdir/missing.md" > "$tmpdir/missing.out"; then printf 'FAIL: checker should fail when a template managed section is missing\n' >&2 cat "$tmpdir/missing.out" >&2 exit 1 @@ -63,7 +63,7 @@ assert_contains "$tmpdir/missing.out" "FAIL root AGENTS drift found" assert_contains "$tmpdir/missing.out" "message-intent | missing" remove_section "workflow-stage-map" "$template" "$tmpdir/missing-stage-map.md" -if "$checker" --template "$template" --target "$tmpdir/missing-stage-map.md" > "$tmpdir/missing-stage-map.out"; then +if python3 "$checker" --template "$template" --target "$tmpdir/missing-stage-map.md" > "$tmpdir/missing-stage-map.out"; then printf 'FAIL: checker should fail when Workflow Stage Map is missing\n' >&2 cat "$tmpdir/missing-stage-map.out" >&2 exit 1 @@ -72,7 +72,7 @@ assert_contains "$tmpdir/missing-stage-map.out" "FAIL root AGENTS drift found" assert_contains "$tmpdir/missing-stage-map.out" "workflow-stage-map | missing" sed 's/block-version:1\.3\.0-20260713\.2/block-version:1.3.0/' "$template" > "$tmpdir/stale.md" -if "$checker" --template "$template" --target "$tmpdir/stale.md" > "$tmpdir/stale.out"; then +if python3 "$checker" --template "$template" --target "$tmpdir/stale.md" > "$tmpdir/stale.out"; then printf 'FAIL: checker should fail when block-version values are stale\n' >&2 cat "$tmpdir/stale.out" >&2 exit 1 @@ -84,7 +84,7 @@ awk ' // { next } { print } ' "$template" > "$tmpdir/broken.md" -if "$checker" --template "$template" --target "$tmpdir/broken.md" > "$tmpdir/broken.out"; then +if python3 "$checker" --template "$template" --target "$tmpdir/broken.md" > "$tmpdir/broken.out"; then printf 'FAIL: checker should fail when managed markers are broken\n' >&2 cat "$tmpdir/broken.out" >&2 exit 1 @@ -102,7 +102,7 @@ awk ' } { print } ' "$template" > "$tmpdir/nested.md" -if "$checker" --template "$template" --target "$tmpdir/nested.md" > "$tmpdir/nested.out"; then +if python3 "$checker" --template "$template" --target "$tmpdir/nested.md" > "$tmpdir/nested.out"; then printf 'FAIL: checker should fail when managed blocks are nested\n' >&2 cat "$tmpdir/nested.out" >&2 exit 1 @@ -118,7 +118,7 @@ awk ' inserted = 1 } ' "$template" > "$tmpdir/duplicate.md" -if "$checker" --template "$template" --target "$tmpdir/duplicate.md" > "$tmpdir/duplicate.out"; then +if python3 "$checker" --template "$template" --target "$tmpdir/duplicate.md" > "$tmpdir/duplicate.out"; then printf 'FAIL: checker should fail when a managed section is duplicated\n' >&2 cat "$tmpdir/duplicate.out" >&2 exit 1 @@ -131,7 +131,7 @@ assert_contains "$tmpdir/duplicate.out" "ownership | duplicate-section" printf '## Legacy Extra\n\n' printf '\n' } > "$tmpdir/extra.md" -if "$checker" --template "$template" --target "$tmpdir/extra.md" > "$tmpdir/extra.out"; then +if python3 "$checker" --template "$template" --target "$tmpdir/extra.md" > "$tmpdir/extra.out"; then printf 'FAIL: checker should fail when target has an unexpected managed section\n' >&2 cat "$tmpdir/extra.out" >&2 exit 1 @@ -145,7 +145,7 @@ awk ' } { print } ' "$template" > "$tmpdir/source-missing.md" -if "$checker" --template "$template" --target "$tmpdir/source-missing.md" > "$tmpdir/source-missing.out"; then +if python3 "$checker" --template "$template" --target "$tmpdir/source-missing.md" > "$tmpdir/source-missing.out"; then printf 'FAIL: checker should fail when a local managed block source is missing\n' >&2 cat "$tmpdir/source-missing.out" >&2 exit 1 @@ -156,7 +156,7 @@ assert_contains "$tmpdir/source-missing.out" "source-missing" cat "$template" printf '\n\n' } > "$tmpdir/bare-start.md" -if "$checker" --template "$template" --target "$tmpdir/bare-start.md" > "$tmpdir/bare-start.out"; then +if python3 "$checker" --template "$template" --target "$tmpdir/bare-start.md" > "$tmpdir/bare-start.out"; then printf 'FAIL: checker should fail when a bare managed-start marker is malformed\n' >&2 cat "$tmpdir/bare-start.out" >&2 exit 1 @@ -167,7 +167,7 @@ assert_contains "$tmpdir/bare-start.out" "malformed-marker" cat "$template" printf '\n\n' } > "$tmpdir/bare-end.md" -if "$checker" --template "$template" --target "$tmpdir/bare-end.md" > "$tmpdir/bare-end.out"; then +if python3 "$checker" --template "$template" --target "$tmpdir/bare-end.md" > "$tmpdir/bare-end.out"; then printf 'FAIL: checker should fail when a bare managed-end marker is malformed\n' >&2 cat "$tmpdir/bare-end.out" >&2 exit 1 @@ -178,7 +178,7 @@ assert_contains "$tmpdir/bare-end.out" "malformed-marker" cat "$template" printf '\n\n' } > "$tmpdir/same-line.md" -if "$checker" --template "$template" --target "$tmpdir/same-line.md" > "$tmpdir/same-line.out"; then +if python3 "$checker" --template "$template" --target "$tmpdir/same-line.md" > "$tmpdir/same-line.out"; then printf 'FAIL: checker should fail when multiple managed markers are on the same line\n' >&2 cat "$tmpdir/same-line.out" >&2 exit 1 diff --git a/tests/validate-root-agents-block-refresh.sh b/tests/validate-root-agents-block-refresh.sh index 8fe7a04..ff2a67f 100755 --- a/tests/validate-root-agents-block-refresh.sh +++ b/tests/validate-root-agents-block-refresh.sh @@ -91,7 +91,7 @@ done < <( ) assert_contains "references/project-guidance.md" "Root AGENTS Refresh Protocol" -assert_contains "references/project-guidance.md" 'scripts/check-root-agents-blocks.sh' +assert_contains "references/project-guidance.md" 'scripts/check-root-agents-blocks.py' assert_not_contains "references/project-guidance.md" "file-level managed version" assert_contains "references/project-guidance.md" "Do not require a separate Managed Block Rule or Agent Loop Guidance Version prose section in target root \`AGENTS.md\`; managed block maintenance rules live in this reference and refresh tooling." assert_contains "references/project-guidance.md" 'Use `block-version:-[.]`; do not shorten it to the skill version alone.' @@ -110,7 +110,7 @@ assert_contains "references/project-guidance.md" "route common human/project sig assert_contains "references/workflow-checklists.md" 'Compare each managed block `section` and `block-version` against the current root AGENTS template.' assert_contains "references/workflow-checklists.md" "Workflow Stage Map routes the current signal to exactly one stage and matching detailed references." -assert_contains "references/workflow-checklists.md" 'If `scripts/check-root-agents-blocks.sh` is available, run it as a read-only drift check against the current root AGENTS template and target root `AGENTS.md`; use the report as Human Review Summary evidence.' +assert_contains "references/workflow-checklists.md" 'If `scripts/check-root-agents-blocks.py` is available, run it with Python 3.10+ as a read-only drift check against the current root AGENTS template and target root `AGENTS.md`; use the report as Human Review Summary evidence.' assert_contains "references/workflow-checklists.md" "Treat missing block-version, older block-version, or missing managed sections as stale even when other sections look current." assert_not_contains "references/workflow-checklists.md" "managed guidance version" assert_contains "references/workflow-checklists.md" 'Do not write bare `block-version:` values; copy the full template block revision such as `block-version:1.3.0-20260713.2`.' @@ -129,13 +129,13 @@ assert_contains "references/validation-scenarios.md" "Root Workflow Stage Map Ro assert_contains "references/validation-scenarios.md" 'do not treat root `AGENTS.md` as the detailed stage procedure' assert_contains "references/validation-scenarios.md" "do not classify root guidance as stale solely because the Managed Block Rule prose section is absent" assert_contains "references/validation-scenarios.md" "block-version" -assert_contains "Usage.md" 'scripts/check-root-agents-blocks.sh' +assert_contains "Usage.md" 'scripts/check-root-agents-blocks.py' assert_contains "Usage.md" "提交前 Agent 应同时复核 feature 文档、requirement 记录、代码 diff、验证证据、drift、project memory、root/directory guidance 影响和 unrelated changes。" -assert_contains "CHANGELOG.md" 'scripts/check-root-agents-blocks.sh' +assert_contains "CHANGELOG.md" 'scripts/check-root-agents-blocks.py' assert_contains "CHANGELOG.md" "Added an explicit pre-commit artifact review reminder to root AGENTS Submit And Commit Rules" -if [ ! -x "$root/scripts/check-root-agents-blocks.sh" ]; then - printf 'FAIL: root AGENTS checker script is missing or not executable\n' >&2 +if [ ! -f "$root/scripts/check-root-agents-blocks.py" ]; then + printf 'FAIL: canonical root AGENTS checker script is missing\n' >&2 exit 1 fi From 72534611bdc58caaf869c89d7bb6d1e55e7869f9 Mon Sep 17 00:00:00 2001 From: Agent Date: Tue, 14 Jul 2026 17:04:48 +0800 Subject: [PATCH 5/6] =?UTF-8?q?feat(v1.3.0):=20=E5=AE=9E=E7=8E=B0=20Featur?= =?UTF-8?q?e=20=E6=9C=88=E5=BA=A6=E5=BD=92=E6=A1=A3=E4=B8=8E=E5=AE=89?= =?UTF-8?q?=E5=85=A8=E6=81=A2=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 增加只读 scan/check、确定性 plan hash、Human Gate 后 apply 与 rehydrate 流程 - 新增 archive locator、跨路径引用更新、事务 journal 和失败 restore - 加固 journal scope、备份预检、崩溃后 drift 与 stranded transaction 防护 - 协调 ADR、Requirement、Project Memory、root Stage Map 与人类文档 - 补齐 79/79 focused、98/98 full 与 33/33 shell 验证报告 --- .github/workflows/cross-platform-checkers.yml | 11 + CHANGELOG.md | 9 + README.md | 5 + SKILL.md | 14 +- Usage.md | 27 + ...ure-monthly-archive-implementation-plan.md | 1324 ++++++++++++ .../v1.3.x/feature-monthly-compaction.md | 593 +++--- ...e-monthly-archive-validation-2026-07-14.md | 119 ++ ...-loop-v1.3.0-full-validation-2026-07-14.md | 227 ++ examples/login-feature/notes.md | 12 +- references/artifact-rules.md | 17 + references/concepts.md | 8 + references/design.md | 32 + references/document-templates.md | 13 + references/feature-completion-check.md | 16 + references/feature-follow-up.md | 15 +- references/human-review-summary.md | 18 + references/project-decisions.md | 2 + references/project-guidance.md | 4 + references/project-memory-mode.md | 4 + references/recovery-and-backfill.md | 16 + references/requirement-management.md | 2 + references/runtime.md | 20 +- references/stage-guides.md | 27 + references/validation-scenarios.md | 68 +- references/workflow-checklists.md | 23 +- scripts/apply-feature-monthly-archive.py | 65 + scripts/check-adr-requirement-model-trace.py | 37 +- scripts/check-feature-monthly-archive.py | 57 + scripts/checker_support.py | 37 + scripts/feature_archive_support.py | 1850 +++++++++++++++++ scripts/restore-feature-monthly-archive.py | 37 + scripts/scan-feature-monthly-archive.py | 54 + templates/feature-archive.md | 6 + templates/notes.md | 13 + templates/root-AGENTS.md | 34 +- tests/feature_archive_test_support.py | 91 + tests/test_adr_requirement_model_trace.py | 79 + tests/test_feature_archive_support.py | 289 +++ tests/test_feature_monthly_archive_apply.py | 457 ++++ tests/test_feature_monthly_archive_restore.py | 366 ++++ tests/test_feature_monthly_archive_scan.py | 561 +++++ tests/test_python_checker_contract.py | 53 +- tests/test_root_agents_blocks.py | 6 +- ...alidate-feature-monthly-archive-runtime.sh | 93 + ...ate-feature-monthly-compaction-proposal.sh | 129 +- tests/validate-project-local-skills.sh | 2 +- .../validate-requirement-lifecycle-backlog.sh | 2 +- tests/validate-root-agents-block-checker.sh | 12 +- tests/validate-root-agents-block-refresh.sh | 4 +- ...alidate-v1.2.4-postfix-pressure-repairs.sh | 2 +- tests/validate-v1.2.4-root-stage-coverage.sh | 2 +- ...validate-v1.2.4-state-lifecycle-repairs.sh | 2 +- 53 files changed, 6609 insertions(+), 357 deletions(-) create mode 100644 docs/proposal/v1.3.x/feature-monthly-archive-implementation-plan.md create mode 100644 docs/reports/agent-loop-v1.3.0-feature-monthly-archive-validation-2026-07-14.md create mode 100644 docs/reports/agent-loop-v1.3.0-full-validation-2026-07-14.md create mode 100644 scripts/apply-feature-monthly-archive.py create mode 100644 scripts/check-feature-monthly-archive.py create mode 100644 scripts/feature_archive_support.py create mode 100644 scripts/restore-feature-monthly-archive.py create mode 100644 scripts/scan-feature-monthly-archive.py create mode 100644 templates/feature-archive.md create mode 100644 tests/feature_archive_test_support.py create mode 100644 tests/test_feature_archive_support.py create mode 100644 tests/test_feature_monthly_archive_apply.py create mode 100644 tests/test_feature_monthly_archive_restore.py create mode 100644 tests/test_feature_monthly_archive_scan.py create mode 100644 tests/validate-feature-monthly-archive-runtime.sh diff --git a/.github/workflows/cross-platform-checkers.yml b/.github/workflows/cross-platform-checkers.yml index 6685811..83d183f 100644 --- a/.github/workflows/cross-platform-checkers.yml +++ b/.github/workflows/cross-platform-checkers.yml @@ -42,4 +42,15 @@ jobs: tests.test_onboarding_core_flow_coverage tests.test_concept_foundation_trace tests.test_adr_requirement_model_trace + tests.test_feature_archive_support + tests.test_feature_monthly_archive_scan + tests.test_feature_monthly_archive_apply + tests.test_feature_monthly_archive_restore -v + + - name: Check Feature Monthly Archive command entrypoints + run: | + python scripts/scan-feature-monthly-archive.py --help + python scripts/check-feature-monthly-archive.py --help + python scripts/apply-feature-monthly-archive.py --help + python scripts/restore-feature-monthly-archive.py --help diff --git a/CHANGELOG.md b/CHANGELOG.md index 8741b4e..3d885fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,15 @@ - Kept the former `.sh` / `.rb` paths as one-cycle compatibility launchers only; active tests and current guidance now call the canonical Python entrypoints directly. - Added fail-closed runtime guidance and native macOS/Windows invocation examples; no third-party Python package, shell emulation layer, or automatic runtime installation is required. +### Feature Monthly Archive +- Implemented directory-only monthly archival: each eligible, Human-confirmed closed feature directory moves intact into `features/YYYY-MM/`, without per-feature summaries, `historical/`, deletion, packing, or content compression. +- Added `features/archive.md` as the stable Feature ID-to-current-path locator while keeping feature artifacts, requirement sources, and accepted decisions authoritative. +- Added Python 3.10+ standard-library scan, check, apply, and restore commands with deterministic plan hashing, required `expected-plan-sha256`, conservative reference blocking, transaction journals, post-check, and verified failure recovery. +- Made ADR ownership, Feature Follow-up, recovery, templates, root guidance, and Stage Map resolve both flat and month-archived closed features while active, blocked, and paused features remain flat. +- Added a separate Human-gated rehydrate path before archived work can flow back; rehydrate moves the directory flat but does not change the closed lifecycle itself. +- Added focused archive/restore fixtures and macOS/Windows CI definitions for eligibility, no-mutation, stale plans, exact reference edits, idempotency, interrupted recovery, and locator consistency; current local evidence is `macOS-verified / Windows-test-defined`. +- Hardened recovery against self-consistent journal scope tampering, corrupt backups, and post-crash human edits; stranded transactions now block new scan/apply work, and inbound relative links into moving features are included in the exact reference plan. + ## 1.2.4 — 2026-07-11 ### Project-Local Skills diff --git a/README.md b/README.md index 0825fea..9a7ed7a 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,9 @@ Message Intent → Chat And Requirements Discussion if needed feedback.* # optional source file when provided notes.* # optional source file when provided features/ + archive.md # locator for archived/rehydrated Feature IDs; not product authority + YYYY-MM/ # Human-gated directory archive for eligible closed features + YYYY-MM-DD-/ # complete feature directory moved intact YYYY-MM-DD-/ product.md (optional) spec.md @@ -110,6 +113,8 @@ Message Intent → Chat And Requirements Discussion if needed contracts/ (optional contract details) ``` +Feature Monthly Archive keeps current work flat and moves only eligible, human-confirmed closed feature directories into their matching month bucket. It is location/index compaction, not content compression or deletion: the feature directory remains intact, while `features/archive.md` records the stable Feature ID and current path. Archive and rehydrate each require a read-only deterministic plan, the exact reviewed SHA-256, a separate Human Gate, transaction recovery, and post-check. + New target projects use `.agent-loop/` by default. Existing visible `agent-loop/` roots remain readable as legacy memory and should be migrated only after human confirmation. ## Quick Start diff --git a/SKILL.md b/SKILL.md index c53364c..9464621 100644 --- a/SKILL.md +++ b/SKILL.md @@ -39,7 +39,7 @@ Also treat long-term memory indexes as claims that must be verified before relia ## Message Intent Guard -Before project-state classification, classify the latest human message intent. `chat` means ordinary discussion, rules questions, status questions, or design talk; answer or discuss only and do not create requirement sets or feature workspaces by default. Message intent is not permanent: if chat turns into demand shaping, `proposal-doc`, implementation, operational support, follow-up, deferred work, or project-skill management, reclassify and route accordingly. If the human explicitly wants discussion without documentation, keep `chat`. `requirements-discussion` means the human is shaping product needs, business goals, capability ideas, constraints, tradeoffs, or user scenarios without authorizing implementation; use Brainstorm / Clarify to produce a human-reviewed requirement document under `.agent-loop/requirements/` before feature construction. `project-skill-management` means the human asks to turn a repeatable project workflow into a project-local skill or to update, disable, or deprecate one; load `references/project-skills.md`. If unclear, ask whether the human wants ordinary discussion, requirements documentation, feature implementation, or project-skill management. +Before project-state classification, classify the latest human message intent. `chat` means ordinary discussion, rules questions, status questions, or design talk; answer or discuss only and do not create requirement sets or feature workspaces by default. Message intent is not permanent: if chat turns into demand shaping, `proposal-doc`, implementation, operational support, follow-up, deferred work, project-skill management, or explicit historical feature archive/rehydrate maintenance, reclassify and route accordingly. If the human explicitly wants discussion without documentation, keep `chat`. `requirements-discussion` means the human is shaping product needs, business goals, capability ideas, constraints, tradeoffs, or user scenarios without authorizing implementation; use Brainstorm / Clarify to produce a human-reviewed requirement document under `.agent-loop/requirements/` before feature construction. `project-skill-management` means the human asks to turn a repeatable project workflow into a project-local skill or to update, disable, or deprecate one; load `references/project-skills.md`. `feature-archive-maintenance` means the human explicitly requests Feature Monthly Archive or rehydrate for closed history; require reliable memory, a read-only scan, and the exact plan SHA-256 Batch Human Gate before mutation. If unclear, ask whether the human wants ordinary discussion, requirements documentation, feature implementation, project-skill management, or archive maintenance. ## Human Help And Version Questions @@ -77,6 +77,7 @@ Use this skill when the user wants to: - reconcile `agent-loop` documents with code reality - execute a task/story with TDD and verification - submit, pause, resume, or close a feature +- compact closed feature discovery by moving whole feature directories through Human-gated Feature Monthly Archive, or rehydrate an archived feature before follow-up execution Do not use for one-off edits when the user explicitly asks to bypass workflow and the edit does not affect feature behavior, public interfaces, data/security boundaries, project memory, submit, or close state. @@ -123,6 +124,10 @@ scripts/check-root-agents-blocks.py read-only root AGENTS managed-block drift ch scripts/check-onboarding-core-flow-coverage.py onboarding core-flow coverage checker scripts/check-concept-foundation-trace.py accepted concept/model trace checker scripts/check-adr-requirement-model-trace.py ADR requirement-model landing checker +scripts/scan-feature-monthly-archive.py read-only deterministic archive/rehydrate plan +scripts/check-feature-monthly-archive.py read-only pre/post archive contract checker +scripts/apply-feature-monthly-archive.py exact-hash Human-gated archive/rehydrate apply +scripts/restore-feature-monthly-archive.py exact transaction-journal restore templates/ copy-ready artifact templates examples/login-feature/ small finished feature workspace examples/complex-saas-project/ larger takeover + feature execution workspace @@ -137,7 +142,7 @@ CHANGELOG.md version-change source of truth for "what cha 1. Inspect `.agent-loop/`; if missing, also check legacy `agent-loop/`. 2. Check root `AGENTS.md` / `CLAUDE.md` as the Root Agent Bootstrap Gate; if either is missing or stale, load `references/project-guidance.md` and include the guidance repair in the recommended Project Entry action unless the human has explicitly deferred it. 2a. Canonical `scripts/check-*.py` validation requires Python 3.10+ and only the Python standard library. Run the `.py` entrypoints natively on macOS or Windows; if a required checker cannot run because Python is missing or unsupported, fail closed and report the capability gap instead of silently using an obsolete implementation. -3. Classify the latest message intent: `chat`, `requirements-discussion`, `project-skill-management`, `feature-request`, `operational-support`, `feature-follow-up`, `deferred-requirement`, or `unknown`. +3. Classify the latest message intent: `chat`, `requirements-discussion`, `project-skill-management`, `feature-archive-maintenance`, `feature-request`, `operational-support`, `feature-follow-up`, `deferred-requirement`, or `unknown`. 3a. For `chat`, answer or discuss only; do not create requirement sets, feature workspaces, tasks, tests, or plans. 3b. For `requirements-discussion`, load `references/requirement-management.md`, use Brainstorm / Clarify, produce a human-reviewed requirement document, and archive it under `.agent-loop/requirements/-/` after confirmation before any feature construction. 3c. For `project-skill-management`, load `references/project-skills.md`, require reliable Project Entry/memory, and route to Project Skill Creation / Update without creating a requirement set or feature workspace. @@ -158,6 +163,7 @@ CHANGELOG.md version-change source of truth for "what cha 13a. Load `references/onboarding-knowledge-base.md` when the human asks for newcomer-facing docs, durable project understanding, guided learning paths, or onboarding-db construction. Run it only after Project Entry Scan or reliable project memory. Use Evidence Graph and Core Flow Inventory first, then accepted Onboarding Spec, Onboarding Tasks, Flow Slice Coverage for critical/important flows, evidence-linked diagrams, completeness gating, coverage scoring, and reviewed batches; module/flow docs remain single-file by default. 13b. During Project Entry, Resume, Re-Adopt, context recovery, and controller re-entry, check `.agent-loop/skills/INDEX.md` when present. Load only `active` project skills whose current instruction-bearing and executable files match the validation manifest, according to `bootstrap` / `on-demand`; discovery and loading never satisfy the per-invocation Execution Gate. 14. Load `references/feature-follow-up.md` when the human reports a bug, regression, post-close correction, field/schema change, algorithm change, API mismatch, screenshot issue, behavior tweak, "small tweak", test failure, or QA/user feedback that may belong to a recent feature. +14a. For `feature-archive-maintenance`, load `references/artifact-rules.md`, `references/stage-guides.md`, `references/human-review-summary.md`, and `references/feature-follow-up.md`. Feature ID is stable while location changes and root `features/archive.md` locates archived/rehydrated history. The scan is read-only; archive/rehydrate requires the expected plan SHA-256 Batch Human Gate, transaction journal, post-check, and restore. Rehydrate before reopened execution; auto modes never authorize either operation. 15. Load `references/large-projects.md` when the repo is large, old, unfamiliar, multi-package, or likely above 100k LOC. 16. Load `references/complex-artifacts.md` when story/task/test/plan complexity crosses its trigger conditions. 17. Load `references/implementation-planning.md` before writing or approving `plan.md` for a task/story. @@ -197,6 +203,8 @@ CHANGELOG.md version-change source of truth for "what cha feedback.* optional source file when provided notes.* optional source file when provided features/ + archive.md optional Feature ID locator maintained only by Feature Monthly Archive + YYYY-MM/ archived closed feature directories -/ product.md optional product brief spec.md @@ -247,6 +255,7 @@ If the local directory is only a remote-project entry point, create only thin lo - For focused questions about one module, flow, async task, deployment path, or problem area, answer from existing docs/code as chat or operational support unless the human explicitly authorizes feature/fix work. Do not create focused onboarding-db artifacts. - When local and remote project reality are split, discover the remote environment before Project Entry Scan or initializing project memory. - Historical execution evidence belongs in `notes.md`. +- Feature Monthly Archive moves only eligible whole closed feature directories to `features/YYYY-MM//`; active / blocked / paused features stay flat. It creates no per-feature archive summary, no `historical/`, no Deep Archive, and exposes no `--force`. Original human requirement sources remain unchanged. - Web E2E capability is discovered from the real project environment. Stable E2E capability belongs in `project.md`; feature-specific E2E cases belong in `tests.md` or `tests/e2e/*`. - Human source requirements are archived as requirement set directories, not new flat files. Each requirement set groups the human's requirement, prototype, feedback, screenshots, recordings, links, and follow-up notes for one intake event or topic. - `.agent-loop/requirements/` is canonical. Do not create or maintain legacy `inputs/` archives in current-version projects. @@ -327,3 +336,4 @@ Stop when: - subagents are needed but not yet approved - submit, commit, PR, merge, release, publish, pause, or close is requested - the work would require first-version exclusions +- Feature Monthly Archive or rehydrate lacks an exact reviewed plan hash, has unsupported/ambiguous references, encounters a stale plan or incomplete `.archive-txn`, or would be performed by manual directory movement diff --git a/Usage.md b/Usage.md index 09c1b5e..b4f295a 100644 --- a/Usage.md +++ b/Usage.md @@ -259,6 +259,33 @@ Feature Follow-up / Flow-back 默认看最近 30 天,但这不是硬限制。 低信息错误,比如 “500 / 白屏 / unknown error”,不应该随便归到最近 feature;Agent 应先建议 investigate-first。 +### 我想按月份归档已经关闭的 feature + +Feature Monthly Archive 只整理目录位置,不压缩或删除 feature 内容,也不会自动提交。一次正常交互是: + +```text +Human: 把 2026 年 5 月和 6 月已经关闭的 feature 按月份归档。 +Agent: 运行只读 scan,展示 plan SHA-256、eligible/blocked 行、目录移动、引用影响、不变内容和恢复范围。 +Human: 确认这个精确批次及其 plan SHA-256。 +Agent: 执行整目录移动和精确引用更新,完成 post-check,报告 transaction evidence,不自动 commit。 +``` + +只有 `closed` 且关闭证据、Archive Readiness、drift、project memory 和 open follow-up 检查完整的 feature 才能进入候选。`active`、`blocked`、`paused`、当前月或存在不安全引用的 feature 保持 flat 并被阻断。归档结果是 `.agent-loop/features/YYYY-MM//`,根级 `.agent-loop/features/archive.md` 只负责按稳定 Feature ID 定位当前位置;原 feature 文档、需求源和 accepted decisions 仍然是事实权威。 + +如果归档后的 feature 要重新进入 Follow-up / Flow-back,Agent 先运行只读 rehydrate scan 并展示新的 plan SHA-256;你必须通过一个独立的 rehydrate Human Gate。确认后 Agent 才把完整目录移回 `.agent-loop/features//` 并复验。rehydrate 不会自行把 `spec.md` 从 `closed` 改为 `active`,后续 reopen 仍归 Feature Follow-up 管理。 + +原生 Python 3.10+ 调用示例: + +```text +# macOS:只读生成计划 +python3 scripts/scan-feature-monthly-archive.py --project-root --operation archive --month 2026-05 --month 2026-06 --as-of 2026-07-14 + +# Windows PowerShell:只读生成计划 +py -3 scripts\scan-feature-monthly-archive.py --project-root --operation archive --month 2026-05 --month 2026-06 --as-of 2026-07-14 +``` + +真正 apply 还必须提供刚刚经人类确认的 `--expected-plan-sha256`;不能使用 `--force` 绕过 stale-plan、引用阻断、transaction journal、恢复或 post-check。 + ### 我想同步 AGENTS.md | 你可以这样说 | Agent 应该怎么做 | diff --git a/docs/proposal/v1.3.x/feature-monthly-archive-implementation-plan.md b/docs/proposal/v1.3.x/feature-monthly-archive-implementation-plan.md new file mode 100644 index 0000000..a3acfce --- /dev/null +++ b/docs/proposal/v1.3.x/feature-monthly-archive-implementation-plan.md @@ -0,0 +1,1324 @@ +# Feature Monthly Archive Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use `executing-plans` to implement this plan task-by-task after Plan Human Review. Use `test-driven-development` for every behavior change, `systematic-debugging` for unexpected failures, `verification-before-completion` before completion claims, and `requesting-code-review` before handoff. This plan is intended for a separate development Agent, but writing the plan does not authorize implementation, subagent dispatch, commit, push, tag, PR, release, or publication. + +**Goal:** Add a Human-gated, cross-platform Feature Monthly Archive capability that moves eligible closed feature directories intact into `.agent-loop/features/YYYY-MM/`, maintains `.agent-loop/features/archive.md` as the stable Feature ID locator, updates durable references, verifies the result, restores failed transactions, and rehydrates archived features before follow-up work. + +**Architecture:** Implement Reader Compatibility first through one standard-library `feature_archive_support.py` module that owns Feature ID/path resolution, archive-index parsing, deterministic plans, reference-impact discovery, hashing, path confinement, and transaction-journal primitives. Four thin Python CLIs then expose scan, check, apply, and restore operations; apply always recomputes and matches a Human-reviewed plan hash before writing. Only after reader compatibility and executable tests pass should the published runtime, root Stage Map, templates, and human docs advertise the capability. + +**Tech Stack:** Python 3.10+ standard library (`argparse`, `dataclasses`, `datetime`, `hashlib`, `json`, `os`, `pathlib`, `re`, `shutil`, `tempfile`, `typing`, `unittest`, `unicodedata`, `urllib.parse`), Markdown artifacts, existing `scripts/checker_support.py`, existing shell contract tests, GitHub Actions Windows/macOS matrix. + +--- + +## Execution Status + +```text +Proposal: accepted by Human Review on 2026-07-14 +Plan: accepted and implementation authorized by Human Review on 2026-07-14 +Implementation: Task 0-7 completed; awaiting final Human Review +Review: local Spec/Standards Review completed; subagent dispatch was not authorized +Platform: macOS-verified / Windows-test-defined +Commit / push / tag / PR: not authorized +``` + +## Execution Boundary + +Included: + +- Feature ID and flat/month-path resolver; +- `.agent-loop/features/archive.md` template, parser, validator, and locator behavior; +- ADR Feature Spec reference compatibility for archived closed features; +- explicit `feature-archive-maintenance` message intent and triggered `Feature Monthly Archive` stage; +- read-only deterministic archive/rehydrate scan; +- read-only pre/post/restore checking; +- Human-confirmed whole-directory archive and rehydrate operations; +- persistent transaction journal and failure restore; +- durable Markdown reference updates and conservative blocker reporting; +- Windows/macOS native tests using only Python standard library; +- coordinated runtime, design, Stage Map, checklist, template, scenario, Usage, README, changelog, and report updates; +- mandatory full validation because routing, Feature Follow-up, recovery, root guidance, and feature path invariants change. + +Excluded: + +- per-feature archive summaries; +- `historical/` content reorganization; +- per-month `INDEX.md`; +- deletion, packing, compression, external storage, or Deep Archive; +- automatic scheduled archival; +- automatic archive without a Batch Human Gate; +- moving `active`, `blocked`, `paused`, current-month, incomplete-close, open-follow-up, or drifted features; +- changing original requirement sources or accepted ADR meaning; +- third-party Python packages; +- generic repository-wide link rewriting outside Markdown and Agent Loop-owned YAML metadata; +- version bump without separate Human approval. + +## Stage Helper Resolution + +```text +Stage: Plan Gate / Plan If Needed +Canonical Candidate: superpowers:writing-plans +Canonical Result: unavailable under that qualified name in the current runtime +Alias Candidate: writing-plans +Resolved Helper: writing-plans +Status: loaded +Load Evidence: /Users/shaodowyd/.codex/skills/writing-plans/SKILL.md read completely on 2026-07-14 +Fallback Used: no +Method Used: exact file map, interface-first design, bite-sized TDD steps, RED/GREEN commands, risks, rollback, and self-review +Agent Loop Path Override: docs/proposal/v1.3.x/feature-monthly-archive-implementation-plan.md +Human Gates Preserved: Plan approval; implementation start; any development-Agent/subagent dispatch; archive Batch Human Gate; rehydrate Human Gate; commit; push; tag; PR; release; publish +``` + +## Preconditions And Hard Stops + +1. Inspect the remote `cross-platform-checkers.yml` run for commit `e49673c`. The current local report records `Windows-test-defined`, not Windows execution success. Reader Compatibility and RED tests may proceed while evidence is pending; archive apply/restore may not be declared accepted until Windows evidence and Human Review are recorded. +2. Preserve unrelated dirty work. At plan time, unrelated paths include `docs/proposal/v1.3.x/onboarding-core-flow-completeness.md`, deleted v1.4 proposal files, and `docs/proposal/v2.0.x/`. Do not stage, revert, rewrite, or include them in validation claims. +3. Do not move any real target-project feature directory while developing this source repository. All mutation tests use temporary fixtures. +4. If any pre-existing full-suite test fails before the first RED, stop and diagnose it separately; do not normalize it into this feature. +5. No command exposes `--force`, skips the expected-plan hash, or silently treats an unresolved reference as historical evidence. + +## Non-Negotiable Invariants + +```text +Feature identity = YYYY-MM-DD-slug Feature ID +Feature lifecycle = draft | active | blocked | paused | closed +Archive state = archived | rehydrated; never a feature lifecycle value +Active / blocked / paused path = features// +Archived path = features/YYYY-MM// +Archive eligibility = closed + complete close evidence + no open blocker +Archive authority = original feature artifacts; archive.md is locator only +Mutation authorization = exact Human-reviewed plan SHA-256 +Failure behavior = fail closed + persistent journal + verified restore +Content policy = move intact; no deletion or content-summary substitution +``` + +## CLI Contract + +All four entrypoints use the same exit classes: + +```text +0 = scan/check/apply/restore completed successfully +1 = artifact, eligibility, stale-plan, reference, transaction, or post-check contract failed +2 = command usage, missing path, unsupported Python, invalid date/month, or capability error +``` + +Archive scan: + +```text +python scripts/scan-feature-monthly-archive.py \ + --project-root /workspace/project \ + --operation archive \ + --month 2026-05 \ + --month 2026-06 \ + --as-of 2026-07-14 +``` + +Rehydrate scan: + +```text +python scripts/scan-feature-monthly-archive.py \ + --project-root /workspace/project \ + --operation rehydrate \ + --feature-id 2026-05-08-login \ + --as-of 2026-07-14 +``` + +Apply binds to the exact scan result: + +```text +python scripts/apply-feature-monthly-archive.py \ + --project-root /workspace/project \ + --operation archive \ + --month 2026-05 \ + --month 2026-06 \ + --as-of 2026-07-14 \ + --expected-plan-sha256 64-lowercase-hex-characters +``` + +Post-check and transaction restore: + +```text +python scripts/check-feature-monthly-archive.py --project-root /workspace/project --operation archive --plan /absolute/path/plan.json +python scripts/restore-feature-monthly-archive.py --project-root /workspace/project --transaction-id 20260714T120000Z-0123456789ab +``` + +Scan prints deterministic UTF-8 JSON to stdout. The controlling Agent may save that output to an OS temporary file for apply/check; the script must not create a target-project artifact during scan. The Batch Human Review Summary displays `plan_sha256`, selected months, eligible/blocked Feature IDs, moves, reference edits, unchanged files, and restore behavior. + +## JSON Plan Contract + +The serialized plan has this exact top-level shape: + +```json +{ + "schema_version": 1, + "operation": "archive", + "as_of": "2026-07-14", + "selected_months": ["2026-05", "2026-06"], + "selected_feature_ids": [], + "candidates": [], + "moves": [], + "archive_entries": [], + "reference_edits": [], + "skipped_references": [], + "snapshots": {}, + "plan_sha256": "64-lowercase-hex-characters" +} +``` + +`plan_sha256` is SHA-256 over canonical JSON of every field except `plan_sha256`, using `sort_keys=True`, `ensure_ascii=False`, `separators=(",", ":")`, UTF-8 encoding, POSIX workspace-relative paths, sorted lists, and no current timestamp. Absolute project paths and output formatting are excluded so the same fixture produces the same hash on Windows and macOS. Content snapshots and relative paths still bind the plan to the exact project state. + +## Interface Contracts + +### `ArchiveContractError` + +Location: `scripts/feature_archive_support.py` + +Kind: exception dataclass + +Signature: + +```python +@dataclass(frozen=True) +class ArchiveContractError(Exception): + category: str + detail: str + exit_code: int = 1 +``` + +Errors: `usage`, `memory-root`, `feature-id`, `month`, `eligibility`, `path-collision`, `path-escape`, `archive-index`, `reference-impact`, `stale-plan`, `transaction`, `post-check`, `restore`. + +### `FeatureLocation` + +Location: `scripts/feature_archive_support.py` + +Kind: immutable dataclass + +Signature: + +```python +@dataclass(frozen=True) +class FeatureLocation: + feature_id: str + relative_path: str + layout: Literal["flat", "archived"] + month: str | None +``` + +Validation: flat is exactly `features/`; archived is exactly `features/YYYY-MM/` and month equals the first seven characters of Feature ID. + +### `ArchiveEntry` + +Location: `scripts/feature_archive_support.py` + +Kind: immutable dataclass + +Signature: + +```python +@dataclass(frozen=True) +class ArchiveEntry: + feature_id: str + month: str + current_path: str + archive_state: Literal["archived", "rehydrated"] + closed_at: str + delivered_summary: str + source_requirements: str + applicable_decisions: str + last_moved_at: str +``` + +### `ReferenceEdit` + +Location: `scripts/feature_archive_support.py` + +Kind: immutable dataclass + +Signature: + +```python +@dataclass(frozen=True) +class ReferenceEdit: + path: str + kind: Literal["literal-path", "relative-link", "archive-index"] + old: str + new: str + occurrences: int + before_sha256: str + after_sha256: str +``` + +### `SkippedReference` + +Location: `scripts/feature_archive_support.py` + +Kind: immutable dataclass + +Signature: + +```python +@dataclass(frozen=True) +class SkippedReference: + path: str + classification: Literal["immutable-requirement-source", "historical-evidence", "unsupported"] + matched_value: str + reason: str +``` + +`immutable-requirement-source` and concrete `historical-evidence` rows are displayed at Human Review and preserved. Any `unsupported` row blocks apply. + +### `Move` + +Location: `scripts/feature_archive_support.py` + +Kind: immutable dataclass + +Signature: + +```python +@dataclass(frozen=True) +class Move: + feature_id: str + month: str + source: str + target: str +``` + +Paths are POSIX project-relative paths. Archive moves are flat-to-month; rehydrate moves are month-to-flat. + +### `ArchiveCandidate` + +Location: `scripts/feature_archive_support.py` + +Kind: immutable dataclass + +Signature: + +```python +@dataclass(frozen=True) +class ArchiveCandidate: + feature_id: str + month: str + current_path: str + lifecycle: str + close_evidence: Literal["complete", "incomplete"] + open_follow_up: str + delivered_summary: str + source_requirements: str + applicable_decisions: str + blockers: Sequence[str] +``` + +Eligibility is `not blockers`; blocker strings are stable category/detail messages sorted lexically. + +### `ArchivePlan` + +Location: `scripts/feature_archive_support.py` + +Kind: immutable dataclass with canonical JSON serialization + +Fields: + +```python +@dataclass(frozen=True) +class ArchivePlan: + schema_version: int + operation: Literal["archive", "rehydrate"] + as_of: str + selected_months: Sequence[str] + selected_feature_ids: Sequence[str] + candidates: Sequence[ArchiveCandidate] + moves: Sequence[Move] + archive_entries: Sequence[ArchiveEntry] + reference_edits: Sequence[ReferenceEdit] + skipped_references: Sequence[SkippedReference] + snapshots: Mapping[str, str] +``` + +Import `Literal`, `Mapping`, and `Sequence` from `typing`. Store tuples and a read-only copied mapping internally even though the public annotations use collection protocols. + +Required methods and exact behavior: + +| Method | Return | Behavior | +|---|---|---| +| `to_payload(include_hash: bool = True)` | `dict[str, object]` | Serialize every dataclass collection in stable sorted order; include the computed lowercase hash only when requested | +| `canonical_bytes()` | `bytes` | Call `to_payload(include_hash=False)` and encode through `canonical_json_bytes` | +| `computed_sha256()` | `str` | Return `sha256_bytes(self.canonical_bytes())` | +| `assert_hash(expected: str)` | `None` | Reject malformed hashes as usage exit 2 and mismatches as `stale-plan` exit 1 | + +### `resolve_feature_location` + +Location: `scripts/feature_archive_support.py` + +Signature: + +```python +def resolve_feature_location(memory_root: Path, feature_id: str) -> FeatureLocation: +``` + +Behavior: + +1. Reject malformed Feature ID. +2. If exact flat directory exists, reject any simultaneous archived locator/directory collision and return flat. +3. Otherwise parse `features/archive.md`, require exactly one matching row, confine `Current Path`, require existing directory and matching Feature ID/month, then return archived or rehydrated location. +4. A `rehydrated` row must point to the flat path. + +Related exact signatures: + +```text +parse_archive_index(memory_root: Path) -> Sequence[ArchiveEntry] +render_archive_index(entries: Sequence[ArchiveEntry]) -> str +``` + +`render_archive_index` uses `templates/feature-archive.md` header text, escapes table-breaking newlines/pipes in one-line summary fields, and produces one trailing newline. + +### `build_archive_plan` + +Location: `scripts/feature_archive_support.py` + +Signature: + +```python +def build_archive_plan( + project_root: Path, + *, + operation: Literal["archive", "rehydrate"], + selected_months: Sequence[str], + selected_feature_ids: Sequence[str], + as_of: date, +) -> ArchivePlan: +``` + +Side effects: none. + +### `apply_archive_plan` + +Location: `scripts/feature_archive_support.py` + +Signature: + +```python +def apply_archive_plan( + project_root: Path, + plan: ArchivePlan, + *, + expected_plan_sha256: str, +) -> str: +``` + +Return: transaction ID after successful post-check and journal cleanup. + +Side effects: creates/removes month directories, moves whole feature directories, updates approved Markdown files and `features/archive.md`, creates a temporary transaction journal, and removes the journal only after successful post-check. + +### `restore_transaction` + +Location: `scripts/feature_archive_support.py` + +Signature: + +```python +def restore_transaction(project_root: Path, transaction_id: str) -> None: +``` + +Behavior: load the persistent journal, restore directory moves in reverse order, restore backed-up bytes atomically, run restore check, mark `restored`, then remove the journal. Any incomplete restore leaves the journal and raises `ArchiveContractError("restore", detail)` with the concrete failed path/operation in `detail`. + +## File Responsibility Map + +### New runtime and template files + +| File | Responsibility | +|---|---| +| `scripts/feature_archive_support.py` | Feature IDs, memory-root discovery, archive index, resolver, candidate eligibility, plan/hash, references, journal, apply/check/restore primitives | +| `scripts/scan-feature-monthly-archive.py` | Read-only archive/rehydrate plan CLI and deterministic JSON output | +| `scripts/check-feature-monthly-archive.py` | Read-only pre/post/restore contract validation CLI | +| `scripts/apply-feature-monthly-archive.py` | Human-confirmed expected-hash apply CLI | +| `scripts/restore-feature-monthly-archive.py` | Persistent-journal recovery CLI | +| `templates/feature-archive.md` | Copy-ready `features/archive.md` locator template | + +### New native tests + +| File | Responsibility | +|---|---| +| `tests/feature_archive_test_support.py` | Temporary target-project builder, feature/requirement/decision/project fixtures, subprocess runner, snapshots | +| `tests/test_feature_archive_support.py` | Feature ID, month, index parsing, resolver, collision, path escape, BOM/CRLF, deterministic plan/hash | +| `tests/test_feature_monthly_archive_scan.py` | Eligible/blocked classification, multi-month plan, reference impacts, no-mutation, rehydrate scan | +| `tests/test_feature_monthly_archive_apply.py` | Whole-directory moves, exact reference edits, archive index, stale-plan, idempotency, post-check | +| `tests/test_feature_monthly_archive_restore.py` | Injected failure, persistent journal, reverse restore, rehydrate, interrupted-process recovery | +| `tests/validate-feature-monthly-archive-runtime.sh` | Coordinated runtime/stage/template/human-gate/source-authority contract | + +### Existing code and CI files to modify + +| File | Planned change | +|---|---| +| `scripts/checker_support.py` | Add reusable Markdown code-span stripping, canonical JSON/hash, atomic UTF-8/bytes write, and normalized relative-path helpers without weakening existing checkers | +| `scripts/check-adr-requirement-model-trace.py` | Replace one-level Feature Spec regex with resolver-aware flat/month validation; allow `closed` only for a valid existing owner path | +| `tests/test_adr_requirement_model_trace.py` | Add archived closed Feature Spec and missing/mismatched archive-index cases | +| `tests/test_python_checker_contract.py` | Add four archive commands and `feature_archive_support` to stdlib/local-import, runtime guard, help/usage, scan/check-read-only contracts | +| `.github/workflows/cross-platform-checkers.yml` | Run all five new archive suites on Windows/macOS and Python 3.10/current | + +### Published runtime and design files to modify together + +| File | Planned change | +|---|---| +| `SKILL.md` | Add feature-archive intent/routing, script map, archived-path inspection rule, and Human Gate boundary concisely | +| `references/runtime.md` | Add `feature-archive-maintenance` intent, precedence/routing, triggered stage order, inspection of `features/archive.md`, and no-manual-move stop | +| `references/design.md` | Add directory-only archive model, locator authority boundary, archive/rehydrate state, and core flow placement | +| `references/concepts.md` | Define Feature Monthly Archive, Archive State, Feature Locator, and Rehydrate | +| `references/artifact-rules.md` | Add flat/month layouts, `features/archive.md` ownership, archived/rehydrated distinction, and stable Feature ID rules | +| `references/feature-follow-up.md` | Resolve archive rows during lookback/extended scan and require rehydrate before reopened execution | +| `references/feature-completion-check.md` | Produce a deterministic Archive Readiness record at close without auto-archiving | +| `references/stage-guides.md` | Add complete Feature Monthly Archive entry/read/write/gate/exit procedure | +| `references/workflow-checklists.md` | Add scan, Batch Human Gate, apply/post-check/restore, and rehydrate checklist | +| `references/human-review-summary.md` | Add Feature Monthly Archive Batch Review table with plan hash and restore scope | +| `references/recovery-and-backfill.md` | Treat missing/stale archive rows, stranded journals, dual flat/month locations, and missing target paths as stale memory/recovery signals | +| `references/project-decisions.md` | Allow archived closed Feature Spec owner paths without rewriting accepted decision meaning | +| `references/requirement-management.md` | Preserve Feature ID while updating `Feature Mapping` / `Implemented By` current path after archive/rehydrate | +| `references/project-memory-mode.md` | Add optional archive locator to simple/enterprise memory trees; keep history out of `project.md` | +| `references/project-guidance.md` | Clarify first-level active state versus month archive and canonical scripts | +| `references/document-templates.md` | Add inline archive-index and Batch Review derived templates | +| `templates/notes.md` | Add the Archive Readiness block consumed by scan | +| `templates/root-AGENTS.md` | Add message intent and Stage Map row; bump every managed block revision on the approved 1.3.0 version | +| `references/validation-scenarios.md` | Add archive, blocked mixed month, stale plan, interrupted restore, follow-up rehydrate, and no-content-loss pressure scenarios | + +### Human-facing and maintenance evidence files + +| File | Planned change | +|---|---| +| `README.md` | Explain directory-only historical archive and locator boundary | +| `Usage.md` | Add trigger phrases, two-stage Human Gate example, archive/rehydrate behavior, and Windows/macOS commands | +| `CHANGELOG.md` | Record implemented 1.3.0 archive capability without version bump | +| `docs/proposal/v1.3.x/feature-monthly-compaction.md` | Change status from accepted design to implemented only after verification/Human Review | +| `docs/reports/agent-loop-v1.3.0-feature-monthly-archive-validation-2026-07-14.md` | Focused RED/GREEN, platform, fixture, recovery, scope, and remaining-risk report | +| `docs/reports/agent-loop-v1.3.0-full-validation-2026-07-14.md` | Required Chinese six-domain full-validation report | +| `examples/login-feature/notes.md` | Add a complete backward-compatible Archive Readiness example for the closed login feature | + +Review but do not mechanically rewrite generic `` paths in `references/e2e-discovery.md`, `references/delivery-contracts.md`, `references/complex-artifacts.md`, `references/external-skill-adapters.md`, and `templates/subagent-brief.md`: active work is rehydrated to flat before execution, so those execution paths remain correct. Add a regression assertion preventing those surfaces from recommending execution inside an archived month directory. + +## Task 0: Establish Baseline And Phase-0 Evidence + +**Files:** + +- Read: `.github/workflows/cross-platform-checkers.yml` +- Read: `docs/reports/agent-loop-v1.3.0-cross-platform-python-script-runtime-validation-2026-07-13.md` +- Read: `AGENTS.md` +- No writes before baseline completes + +- [ ] **Step 1: Record the exact workspace boundary** + +Run: + +```text +git status --short --branch +git log -1 --oneline +``` + +Expected: branch `alpha/v1.3.0`; unrelated dirty paths remain visible and unstaged. Record them in the new focused report once implementation is authorized. + +- [ ] **Step 2: Run the existing native and shell baseline** + +Run: + +```text +python3 -m unittest discover -s tests -p 'test_*.py' -v +for test_file in tests/validate-*.sh; do bash "$test_file"; done +``` + +Expected baseline at plan creation: 36 Python tests PASS and every existing shell contract PASS. A changed count is acceptable only when every pre-existing test still passes and the report records the exact count. + +- [ ] **Step 3: Verify remote Windows evidence without guessing** + +Preferred command when GitHub CLI is authenticated: + +```text +gh run list --workflow cross-platform-checkers.yml --branch alpha/v1.3.0 --limit 10 +``` + +Expected: locate the run containing commit `e49673c`, then inspect all Windows matrix jobs. If no authenticated CLI or remote evidence is available, record `Windows evidence unavailable` and keep Phase 0 blocked; do not claim cross-platform acceptance. + +- [ ] **Step 4: Baseline review checkpoint** + +Do not edit or commit. If baseline or Windows evidence fails unexpectedly, use `systematic-debugging` before proceeding. + +## Task 1: Establish RED Archive Contracts And Fixture Builder + +**Files:** + +- Create: `tests/feature_archive_test_support.py` +- Create: `tests/test_feature_archive_support.py` +- Create: `tests/test_feature_monthly_archive_scan.py` +- Modify: `tests/test_python_checker_contract.py` + +- [ ] **Step 1: Create the temporary target-project builder** + +Create `tests/feature_archive_test_support.py` with this public interface: + +```python +from __future__ import annotations + +import hashlib +import json +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path + +from tests.checker_test_support import ROOT + + +@dataclass +class ArchiveWorkspace: + project_root: Path + + @property + def memory_root(self) -> Path: + return self.project_root / ".agent-loop" + + @property + def features_root(self) -> Path: + return self.memory_root / "features" + + def write(self, relative: str, content: str) -> Path: + path = self.project_root / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8", newline="\n") + return path + + def feature(self, feature_id: str, *, status: str = "closed", close_complete: bool = True) -> Path: + root = self.features_root / feature_id + root.mkdir(parents=True, exist_ok=True) + self.write(f".agent-loop/features/{feature_id}/spec.md", f"# Feature Spec\n\nStatus: {status}\n") + task_status = "done" if close_complete else "in-progress" + self.write(f".agent-loop/features/{feature_id}/tasks.md", f"# Tasks\n\n- Status: {task_status}\n") + close = ( + "## Feature Close Review\n\nDecision: pass\n\n" + "## Drift Check\n\nDecision: no-drift\n\n" + "## Close Record\n\nClosed At: 2026-05-20\nHuman Decision: confirmed\n\n" + f"## Archive Readiness\n\nClosed At: 2026-05-20\nDelivered Summary: completed {feature_id}\n" + "Verification: complete\nFeature Close Review: complete\nDrift: resolved\n" + "Project Memory Impact: none\nOpen Follow-up: none\n" + if close_complete else "## Close Record\n\nClosed At:\n" + ) + self.write(f".agent-loop/features/{feature_id}/notes.md", f"# Notes\n\n{close}") + self.write(f".agent-loop/features/{feature_id}/tests.md", "# Tests\n\nStatus: passing\n") + self.write(f".agent-loop/features/{feature_id}/plan.md", "# Plan\n\nStatus: closed\n") + return root + + +def run_archive_command(script: str, *args: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [sys.executable, str(ROOT / "scripts" / script), *map(str, args)], + cwd=str(ROOT), + check=False, + capture_output=True, + text=True, + encoding="utf-8", + ) + + +def tree_snapshot(root: Path) -> dict[str, str]: + return { + path.relative_to(root).as_posix(): hashlib.sha256(path.read_bytes()).hexdigest() + for path in sorted(root.rglob("*")) + if path.is_file() + } + + +def json_output(result: subprocess.CompletedProcess[str]) -> dict[str, object]: + return json.loads(result.stdout) +``` + +- [ ] **Step 2: Write resolver and deterministic-plan RED tests** + +Create `tests/test_feature_archive_support.py` with these exact cases and assertions: + +| Test name | Fixture | Assertion | +|---|---|---| +| `test_flat_feature_resolves_without_archive_index` | one flat closed feature | `layout == "flat"`, `month is None`, path is `features/2026-05-08-login` | +| `test_archived_feature_requires_matching_unique_index_row` | one month directory plus one matching row | `layout == "archived"`, month `2026-05`; deleting or duplicating the row raises `archive-index` | +| `test_flat_and_archived_collision_fails_closed` | same Feature ID at both paths | raises `path-collision` | +| `test_month_must_match_feature_id` | ID starts `2026-05`, directory uses `2026-06` | raises `month` | +| `test_archive_index_accepts_bom_and_crlf` | index bytes start BOM and use CRLF | row parses with exact Unicode summary | +| `test_symlink_escape_is_rejected` | archived path symlinks outside project | raises `path-escape` | +| `test_plan_hash_is_stable_across_absolute_roots` | identical relative trees under two temp roots | equal `plan_sha256` | + +Use `self.assertRaisesRegex(ArchiveContractError, "archive-index")`, `"path-collision"`, `"month"`, or `"path-escape"` according to the failure row above, and exact `FeatureLocation` equality for success. The archived fixture row must use the accepted columns from `templates/feature-archive.md`; create the same relative tree under two temporary absolute roots and assert equal `plan_sha256`. + +- [ ] **Step 3: Write multi-month and no-mutation RED tests** + +Create `tests/test_feature_monthly_archive_scan.py` with this representative test body and companion cases for current month, active, blocked, paused, incomplete close, open follow-up, path collision, and rehydrate: + +```python +def test_scan_selects_closed_features_across_two_months_and_preserves_blocked(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + workspace.feature("2026-05-08-login") + workspace.feature("2026-05-22-import", status="paused") + workspace.feature("2026-06-12-payment") + before = tree_snapshot(workspace.project_root) + result = run_archive_command( + "scan-feature-monthly-archive.py", + "--project-root", str(workspace.project_root), + "--operation", "archive", + "--month", "2026-05", + "--month", "2026-06", + "--as-of", "2026-07-14", + ) + self.assertEqual(result.returncode, 0, result.stderr) + payload = json_output(result) + self.assertEqual( + [move["feature_id"] for move in payload["moves"]], + ["2026-05-08-login", "2026-06-12-payment"], + ) + self.assertIn("2026-05-22-import", [item["feature_id"] for item in payload["candidates"]]) + self.assertEqual(tree_snapshot(workspace.project_root), before) +``` + +- [ ] **Step 4: Extend the Python command contract in RED** + +In `tests/test_python_checker_contract.py`, add: + +```python +ARCHIVE_COMMANDS = ( + "scripts/scan-feature-monthly-archive.py", + "scripts/check-feature-monthly-archive.py", + "scripts/apply-feature-monthly-archive.py", + "scripts/restore-feature-monthly-archive.py", +) +``` + +Update the import allowlist to include `feature_archive_support`; assert all commands exist, call `require_supported_python`, use only standard-library/local imports, and return exit `2` with `usage` when invoked without arguments. Keep the existing checker inventory assertions unchanged. + +- [ ] **Step 5: Run RED and preserve the reason** + +Run: + +```text +python3 -m unittest \ + tests.test_feature_archive_support \ + tests.test_feature_monthly_archive_scan \ + tests.test_python_checker_contract -v +``` + +Expected RED: missing `scripts/feature_archive_support.py`, the four missing CLI files, and missing `templates/feature-archive.md`. The RED must not be an import-path or fixture-construction error. + +- [ ] **Step 6: Review checkpoint** + +Inspect only the new tests and the contract-test diff. Do not commit; commit remains a separate Human Gate. + +## Task 2: Implement Feature Locator, Archive Index, Plan Hash, And ADR Reader Compatibility + +**Files:** + +- Create: `scripts/feature_archive_support.py` +- Create: `templates/feature-archive.md` +- Modify: `scripts/checker_support.py` +- Modify: `scripts/check-adr-requirement-model-trace.py` +- Modify: `tests/test_adr_requirement_model_trace.py` +- Test: `tests/test_feature_archive_support.py` + +- [ ] **Step 1: Add reusable safe-write and canonical helpers** + +Add these exact signatures to `scripts/checker_support.py`: + +```python +def strip_code_span(value: str) -> str: + cleaned = value.strip() + return cleaned[1:-1].strip() if len(cleaned) >= 2 and cleaned[0] == cleaned[-1] == "`" else cleaned + + +def sha256_bytes(value: bytes) -> str: + return hashlib.sha256(value).hexdigest() + + +def canonical_json_bytes(payload: object) -> bytes: + return json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8") + + +def atomic_write_bytes(path: Path, content: bytes) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + handle, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent) + try: + with os.fdopen(handle, "wb") as stream: + stream.write(content) + stream.flush() + os.fsync(stream.fileno()) + os.replace(temporary, path) + except BaseException: + Path(temporary).unlink(missing_ok=True) + raise +``` + +Import only `hashlib`, `json`, `os`, and `tempfile` from the standard library. Add focused tests proving UTF-8 bytes, replacement, and no leftover temporary file after an injected `os.replace` failure. + +- [ ] **Step 2: Create the copy-ready archive index template** + +Create `templates/feature-archive.md` exactly with the explanatory authority boundary and these columns: + +```md +# Feature Archive + +This file locates archived or rehydrated features. Feature specs, tests, notes, requirement sources, and accepted decisions remain authoritative. + +| Feature ID | Month | Current Path | Archive State | Closed At | Delivered Summary | Source Requirements | Applicable Decisions | Last Moved At | +|---|---|---|---|---|---|---|---|---| +``` + +An empty template is valid before the first row; do not use the existing `table()` helper directly because it rejects empty tables. `parse_archive_index()` must parse the header and allow zero rows. + +- [ ] **Step 3: Implement immutable models and archive-index parsing** + +Create `scripts/feature_archive_support.py`. Use the interface contracts above and these constants: + +```python +FEATURE_ID_RE = re.compile(r"^(?P\d{4}-(?:0[1-9]|1[0-2]))-(?P0[1-9]|[12]\d|3[01])-[a-z0-9][a-z0-9-]*$") +MONTH_RE = re.compile(r"^\d{4}-(?:0[1-9]|1[0-2])$") +ARCHIVE_COLUMNS = ( + "Feature ID", "Month", "Current Path", "Archive State", "Closed At", + "Delivered Summary", "Source Requirements", "Applicable Decisions", "Last Moved At", +) +ARCHIVE_STATES = frozenset({"archived", "rehydrated"}) +``` + +Implement: + +```python +def discover_memory_root(project_root: Path) -> Path: + hidden = project_root / ".agent-loop" + legacy = project_root / "agent-loop" + if hidden.is_dir() and legacy.is_dir(): + raise ArchiveContractError("memory-root", "both .agent-loop and legacy agent-loop exist") + if hidden.is_dir(): + return hidden.resolve() + if legacy.is_dir(): + return legacy.resolve() + raise ArchiveContractError("memory-root", "no agent-loop memory root exists", 2) +``` + +`parse_archive_index`, `render_archive_index`, and `resolve_feature_location` must sort by `(month, feature_id)`, reject duplicate IDs, reject duplicate normalized paths, reject unknown states, verify `rehydrated` points flat, and verify `archived` points to `features/YYYY-MM/`. + +- [ ] **Step 4: Implement deterministic plan serialization** + +Implement `ArchivePlan.to_payload`, `canonical_bytes`, `computed_sha256`, and `assert_hash`. The payload used for hashing excludes `plan_sha256`; candidate, move, reference-edit, and snapshot collections are sorted by stable tuple keys. Reject expected hashes that do not match `^[0-9a-f]{64}$` with exit `2`; a valid but different hash is `stale-plan` exit `1`. + +- [ ] **Step 5: Make ADR Feature Spec references resolver-aware** + +Replace the current single-level check at `scripts/check-adr-requirement-model-trace.py` `validate_feature_reference` with: + +```python +FEATURE_SPEC_RE = re.compile( + r"(?:^|/)features/(?:(?P\d{4}-\d{2})/)?(?P[^/]+)/spec\.md$" +) +``` + +Rules: + +- `planned:` accepts flat `features//spec.md` only; +- existing flat or archived paths must exist and stay confined; +- an archived path requires a matching archive-index row when `features/archive.md` exists; +- `Status: proposed | accepted | closed` is valid for an existing Feature Spec owner; +- `closed` proves historical ownership only and does not authorize new execution; +- an archived month mismatch, missing archive row, duplicate row, or `rehydrated` row pointing to a month path fails. + +Add native tests using `features/2026-05/2026-05-08-login/spec.md` plus matching/missing/mismatched index rows. + +- [ ] **Step 6: Run GREEN for locator and ADR compatibility** + +Run: + +```text +python3 -m unittest \ + tests.test_feature_archive_support \ + tests.test_adr_requirement_model_trace \ + tests.test_python_checker_contract -v +bash tests/validate-adr-requirement-model-technical-landing-trace.sh +``` + +Expected: all selected tests PASS; existing valid/proposed/not-needed ADR fixtures retain their prior conclusions. + +- [ ] **Step 7: Review checkpoint** + +Run `python3 -m compileall -q scripts tests` and inspect the new public interfaces for name/type consistency. Do not commit. + +## Task 3: Implement Read-Only Archive/Rehydrate Scan And Check + +**Files:** + +- Create: `scripts/scan-feature-monthly-archive.py` +- Create: `scripts/check-feature-monthly-archive.py` +- Modify: `scripts/feature_archive_support.py` +- Test: `tests/test_feature_monthly_archive_scan.py` +- Create: `tests/test_feature_monthly_archive_apply.py` with pre/post-check RED cases only + +- [ ] **Step 1: Implement exact eligibility extraction** + +Add these pure helper interfaces: + +| Function | Exact signature | +|---|---| +| Inspect one candidate | `inspect_feature(memory_root: Path, feature_id: str, as_of: date) -> ArchiveCandidate` | +| Discover first-level features | `discover_flat_features(memory_root: Path) -> Sequence[FeatureLocation]` | +| Discover exact reference edits | `discover_reference_impacts(project_root: Path, moves: Sequence[Move]) -> Sequence[ReferenceEdit]` | +| Build deterministic plan | `build_archive_plan(project_root: Path, *, operation: Literal["archive", "rehydrate"], selected_months: Sequence[str], selected_feature_ids: Sequence[str], as_of: date) -> ArchivePlan` | + +`inspect_feature` reads `spec.md`, `tasks.md`, `notes.md`, and `project.md`. It requires `Status: closed`, a concrete `## Close Record`, and this exact `## Archive Readiness` metadata contract: + +```md +## Archive Readiness + +Closed At: 2026-05-20 +Delivered Summary: completed login authentication and verified failure paths +Verification: complete +Feature Close Review: complete +Drift: resolved +Project Memory Impact: complete | none +Open Follow-up: none | FU-001, FU-002 +``` + +`Open Follow-up` must be exactly `none` for eligibility. Existing closed features without the block are classified `missing-archive-readiness`; the Agent may propose a human-reviewed notes backfill from existing close evidence, but scan/apply may not infer readiness. `inspect_feature` records blockers instead of throwing so mixed eligible/blocked months produce one reviewable plan. + +Current month is `as_of.strftime("%Y-%m")`. Never call `date.today()` inside planning logic. + +- [ ] **Step 2: Implement conservative Markdown reference impact discovery** + +Scan UTF-8 Markdown files under project root while excluding `.git`, `.archive-txn`, `node_modules`, `vendor`, `.venv`, `dist`, and `build`. Reject symlinked directories and files outside project root. For files above 2 MiB, add a `skipped_references` blocker instead of reading them. + +Support two deterministic edit kinds: + +1. literal canonical paths beginning `.agent-loop/features//` or `agent-loop/features//`; +2. Markdown inline links and reference definitions inside moved feature files when the old link resolves within project root but outside the moved feature directory. + +Preserve anchors and URL query strings with `urllib.parse`; ignore `http`, `https`, `mailto`, and same-document `#anchor` links. Any unparsable candidate containing the old feature path becomes a blocker; do not guess. + +Do not rewrite original human requirement source files. Paths under `requirements//` other than `README.md` and lifecycle-owned index files are recorded as `SkippedReference(classification="immutable-requirement-source")`. Completed proposal/report text that clearly describes a past path may be classified `historical-evidence`; every such row remains visible in the Batch Human Review. Any old-path occurrence that is neither an approved edit nor one of those two preserved classes becomes `unsupported` and blocks apply. + +- [ ] **Step 3: Implement the scan CLI** + +`scripts/scan-feature-monthly-archive.py` must: + +```python +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Build a read-only Feature Monthly Archive plan") + parser.add_argument("--project-root", required=True) + parser.add_argument("--operation", choices=("archive", "rehydrate"), required=True) + parser.add_argument("--month", action="append", default=[]) + parser.add_argument("--feature-id", action="append", default=[]) + parser.add_argument("--as-of", required=True) + return parser +``` + +Archive requires at least one month and no feature IDs; rehydrate requires at least one Feature ID and no months. Print `json.dumps(plan.to_payload(), ensure_ascii=False, sort_keys=True, indent=2)` plus one newline. Catch `ArchiveContractError as error`, call `print(f"{error.category}: {error.detail}", file=sys.stderr)`, and exit with `error.exit_code`. + +- [ ] **Step 4: Implement the read-only check CLI** + +`scripts/check-feature-monthly-archive.py` accepts `--project-root`, `--operation archive|rehydrate|restore`, and `--plan`. It loads UTF-8 JSON, reconstructs/validates `ArchivePlan`, checks current paths, archive rows, snapshots, allowed reference edits, absence/presence of source/target paths appropriate to the operation, and prints a stable PASS summary. It rejects every `unsupported` skipped reference and every remaining durable old-path occurrence; immutable requirement sources and approved historical-evidence rows remain unchanged and are reported separately. It never writes. + +- [ ] **Step 5: Prove scan/check are read-only and cross-root deterministic** + +Run: + +```text +python3 -m unittest \ + tests.test_feature_archive_support \ + tests.test_feature_monthly_archive_scan \ + tests.test_feature_monthly_archive_apply -v +``` + +Expected: scan suites PASS; apply tests that require the missing apply CLI remain RED for the next task. Snapshots before/after scan and pre-check must be byte-identical. + +- [ ] **Step 6: Review checkpoint** + +Search the two read-only CLIs for `write_text`, `write_bytes`, `mkdir`, `rename`, `replace`, `move`, `unlink`, and `rmtree`; only plan-free pure support imports are permitted. Do not commit. + +## Task 4: Implement Expected-Hash Apply, Transaction Journal, Restore, And Rehydrate + +**Files:** + +- Create: `scripts/apply-feature-monthly-archive.py` +- Create: `scripts/restore-feature-monthly-archive.py` +- Modify: `scripts/feature_archive_support.py` +- Complete: `tests/test_feature_monthly_archive_apply.py` +- Create: `tests/test_feature_monthly_archive_restore.py` + +- [ ] **Step 1: Write apply and restore RED cases** + +Cover these exact tests before implementation: + +| Test name | Required assertion | +|---|---| +| `test_apply_moves_two_closed_features_intact_and_updates_archive_index` | two source dirs disappear, two month dirs exist, file bytes match except approved link edits, two sorted locator rows exist | +| `test_apply_rejects_valid_but_different_plan_hash_without_writes` | exit 1 contains `stale-plan`; full tree snapshot unchanged | +| `test_apply_rejects_state_drift_after_scan` | edit one candidate note after scan; apply exits 1; no journal or move exists | +| `test_apply_updates_only_precomputed_reference_edits` | known project/requirement/decision links change; an unrelated Markdown file is byte-identical | +| `test_apply_is_idempotent_and_never_nests_month_directory` | second scan says `already-archived`; second apply has no moves; nested month path does not exist | +| `test_injected_reference_write_failure_restores_directories_and_bytes` | subprocess exits 1; before/after full snapshots equal; restored journal removed | +| `test_interrupted_transaction_can_be_restored_by_new_process` | leave journal at `moving`, invoke restore CLI, assert original tree and no journal | +| `test_incomplete_restore_keeps_journal_and_fails_closed` | create target collision before restore; exit 1; journal remains with `restoring` state | +| `test_rehydrate_moves_archived_feature_flat_and_updates_locator` | month source disappears, flat target exists, row is `rehydrated`, original `spec.md` remains `closed` | + +Use environment variable `AGENT_LOOP_ARCHIVE_FAIL_AFTER=` only inside tests to inject a deterministic failure. Production docs must label it test-only; reject the variable unless `AGENT_LOOP_ARCHIVE_TEST_MODE=1` is also set. + +- [ ] **Step 2: Implement the journal schema and atomic backup** + +Journal path: + +```text +.agent-loop/features/.archive-txn//journal.json +.agent-loop/features/.archive-txn//backups/ +``` + +Journal JSON fields: + +```json +{ + "schema_version": 1, + "transaction_id": "20260714T120000Z-0123456789ab", + "operation": "archive", + "plan_sha256": "64-lowercase-hex-characters", + "state": "prepared", + "moves": [], + "backups": [], + "completed_operations": [] +} +``` + +Allowed states: `prepared`, `moving`, `references-updated`, `checking`, `restoring`, `restored`, `verified`. Persist journal state with `atomic_write_bytes` before and after each irreversible step. Backup only files listed in `reference_edits` plus existing `features/archive.md`; record `missing-before` for newly created index files. + +- [ ] **Step 3: Implement exact-hash revalidation and archive apply** + +The apply CLI uses the same parser fields as scan plus required `--expected-plan-sha256`. It rebuilds the plan from current state, compares the hash before creating `.archive-txn`, then: + +1. creates the journal and backups; +2. creates month directories only for confirmed moves; +3. uses `Path.rename()` for same-memory-root directory moves; +4. renders `features/archive.md` from sorted entries; +5. applies exact precomputed `ReferenceEdit` replacements only when current file hash equals `before_sha256`; +6. writes changed files atomically; +7. runs the same post-check core used by the check CLI; +8. marks verified and removes the transaction directory; +9. removes an empty month directory created by a failed transaction during restore. + +Any exception after journal creation immediately calls `restore_transaction`; report both the original failure and restore result. + +- [ ] **Step 4: Implement rehydrate through the same plan/apply path** + +For `operation=rehydrate`, require the archive row state `archived`, source month path, flat target absence, closed feature, and explicit Feature ID selection. Move the whole directory flat, set row `Archive State: rehydrated`, set `Current Path` to `features//`, update references, and post-check. Do not change `spec.md Status`; Feature Follow-up owns the later `closed -> active` transition. + +- [ ] **Step 5: Implement standalone restore CLI** + +`restore-feature-monthly-archive.py` accepts only `--project-root` and `--transaction-id`. Validate the ID against `^\d{8}T\d{6}Z-[0-9a-f]{12}$`, confine the journal path, restore moves in reverse order, restore exact backup bytes or remove `missing-before` files, verify the pre-transaction snapshots, then remove the journal. Never infer a transaction ID by choosing the newest directory. + +- [ ] **Step 6: Run GREEN including injected failures** + +Run: + +```text +python3 -m unittest \ + tests.test_feature_archive_support \ + tests.test_feature_monthly_archive_scan \ + tests.test_feature_monthly_archive_apply \ + tests.test_feature_monthly_archive_restore -v +``` + +Expected: every success, stale-plan, injected-failure, restore, idempotency, and rehydrate case PASS; no temporary `.archive-txn` remains after successful or successfully restored tests. + +- [ ] **Step 7: Review checkpoint** + +Run `python3 -m compileall -q scripts tests`, inspect mutation functions line-by-line, and confirm no delete/archive-content path exists. Do not commit. + +## Task 5: Publish Coordinated Runtime, Stage, Gate, Template, And Reader Rules + +**Files:** + +- Create: `tests/validate-feature-monthly-archive-runtime.sh` +- Modify all published runtime/design files listed in the File Responsibility Map +- Modify: `templates/root-AGENTS.md` +- Modify: `references/validation-scenarios.md` + +- [ ] **Step 1: Write the coordinated RED contract first** + +Create `tests/validate-feature-monthly-archive-runtime.sh` with `assert_contains` / `assert_not_contains` checks that require: + +```text +feature-archive-maintenance +Feature Monthly Archive +features/archive.md +Feature ID is stable +archive state is not feature lifecycle +scan is read-only +expected plan SHA-256 +Batch Human Gate +transaction journal +post-check +restore +rehydrate before reopened execution +active / blocked / paused features stay flat +no per-feature archive summary +no historical/ +no Deep Archive +no --force +``` + +Require coordinated mentions in `SKILL.md`, `references/runtime.md`, `references/design.md`, `references/artifact-rules.md`, `references/feature-follow-up.md`, `references/stage-guides.md`, `references/workflow-checklists.md`, `references/human-review-summary.md`, `references/recovery-and-backfill.md`, `templates/root-AGENTS.md`, `references/validation-scenarios.md`, and `templates/feature-archive.md`. + +Run: + +```text +bash tests/validate-feature-monthly-archive-runtime.sh +``` + +Expected RED: missing runtime intent/stage/gate/template references. + +- [ ] **Step 2: Add routing without creating an ordinary feature stage** + +Add `feature-archive-maintenance` to Message Intent. Route it after Safety Stop, Remote Discovery, and Memory Recovery, but before Active Feature Guard because it is maintenance of closed history rather than new/active feature execution. If selected candidates include current active/paused paths, eligibility blocks those candidates; it does not switch active work. + +The stage order adds `Feature Monthly Archive If Explicitly Requested` after Re-Adopt/Recovery and before feature construction stages. Auto modes never authorize archive or rehydrate; both stop at their Batch Human Gates. + +- [ ] **Step 3: Add the complete stage procedure and checklist** + +`references/stage-guides.md` section must state: + +```text +Entry: explicit archive/rehydrate request after reliable project memory +Reads: project.md, archive.md, selected feature close artifacts, requirements, decisions, Markdown references +Writes: none during scan; confirmed month moves, archive.md, approved references, temporary journal during apply +Human Gate: exact plan SHA-256 Batch Review +Exit: verified archive/rehydrate, verified restore, or one blocker stage +Next: Chat/report when complete; Ask Human for stale plan/scope; Recovery for stranded journal; Feature Follow-up after verified rehydrate +``` + +Add the matching checklist and Human Review table. The review table includes operation, plan SHA-256, selected months/IDs, eligible, blocked, moves, reference edits, unchanged content, journal/restore, platform evidence, and decision. + +Update `templates/notes.md`, `references/document-templates.md`, and `references/feature-completion-check.md` so close records write the exact `Archive Readiness` block above. This does not auto-archive and does not add a new Close Human Gate. Update `examples/login-feature/notes.md` with a concrete completed block. Regression tests must reject archive eligibility when the block is missing, contains a non-concrete summary, has any value other than the allowed terminal values, or lists an open follow-up ID. + +- [ ] **Step 4: Update Feature Follow-up and recovery rules** + +Follow-up lookup reads Active/Paused first, flat recent features second, `features/archive.md` third, archived feature artifacts fourth. A confirmed archived owner must rehydrate through its own Human-reviewed plan before Feature Follow-up changes lifecycle or starts execution. + +Recovery treats these as stale-memory/safety conditions: + +- archive row target missing; +- archived directory without row; +- duplicate flat/month Feature ID; +- `rehydrated` row pointing to month path; +- incomplete `.archive-txn`; +- old path durable references after verified apply. + +- [ ] **Step 5: Update root Stage Map and block revision** + +Add a row for explicit historical feature archive/rehydrate requests and route to `references/stage-guides.md`, `references/artifact-rules.md`, and `references/feature-follow-up.md`. Update precedence wording consistently. Bump all managed blocks from the current `block-version:1.3.0-20260713.2` to one new full revision value such as `block-version:1.3.0-20260714.1`; use the exact same full value in every managed block and update root-block regression expectations. + +- [ ] **Step 6: Add pressure scenarios** + +Add at least these scenarios to `references/validation-scenarios.md`: + +1. human selects May/June with closed and paused features; +2. plan changes between review and apply; +3. accepted ADR references a closed archived Feature Spec; +4. archived feature owns a day-45 regression and must rehydrate; +5. journal remains after process interruption; +6. duplicate flat/month path and stale archive row; +7. user says “compress” but asks to delete history—route outside this capability; +8. auto mode attempts archive without Batch Human Gate; +9. archive scan under missing controller fallback—read-only discussion only, no apply; +10. reference scanner finds an ambiguous old path and blocks. + +- [ ] **Step 7: Run GREEN coordinated contracts** + +Run: + +```text +bash tests/validate-feature-monthly-archive-runtime.sh +bash tests/validate-feature-monthly-compaction-proposal.sh +bash tests/validate-v1.2.4-root-stage-coverage.sh +bash tests/validate-root-agents-block-refresh.sh +``` + +Expected: all PASS with the new intent, Stage Map, block revision, gate, and preserved proposal boundary. + +## Task 6: Publish Human Docs, CI, Changelog, And Current Authority + +**Files:** + +- Modify: `README.md` +- Modify: `Usage.md` +- Modify: `CHANGELOG.md` +- Modify: `.github/workflows/cross-platform-checkers.yml` +- Modify: `tests/test_python_checker_contract.py` +- Modify: `docs/proposal/v1.3.x/feature-monthly-compaction.md` +- Create: `docs/reports/agent-loop-v1.3.0-feature-monthly-archive-validation-2026-07-14.md` + +- [ ] **Step 1: Add human trigger and safety examples** + +Usage must include this ordinary-language flow: + +```text +Human: 把 2026 年 5 月和 6 月已经关闭的 feature 按月份归档。 +Agent: runs read-only scan and shows plan SHA-256, eligible/blocked rows, moves, references, unchanged content, and restore scope. +Human: confirms the exact batch. +Agent: applies, post-checks, reports transaction evidence, and does not commit automatically. +``` + +Add a rehydrate example showing a separate Human Gate before flow-back. README should explain the directory shape and that archive means location/index compaction, not content deletion. + +- [ ] **Step 2: Extend native CI** + +Append these modules to the workflow unittest command: + +```text +tests.test_feature_archive_support +tests.test_feature_monthly_archive_scan +tests.test_feature_monthly_archive_apply +tests.test_feature_monthly_archive_restore +``` + +Update `test_cross_platform_ci_runs_the_native_suite` to require all four modules and all four archive command paths. Keep Python 3.10/current and Windows/macOS matrix unchanged. + +- [ ] **Step 3: Record current-version behavior** + +Update the 1.3.0 changelog section with implemented behavior, not proposal promises. Do not alter 1.2.4 historical entries and do not bump the version. + +Set proposal status only after fresh verification: + +```text +状态:已实现;待最终 Human Review +``` + +If Windows jobs have not actually run successfully, write `macOS-verified / Windows-test-defined` and do not mark the proposal implemented cross-platform. + +- [ ] **Step 4: Write focused validation report from real evidence** + +The report must include exact Python/shell counts, RED reasons, eligible/blocked cases, no-mutation hashes, stale-plan evidence, injected-failure restore, rehydrate, platform evidence, Critical/High/Medium findings, scope exclusions, and commit/push authorization status. Do not copy scores from an older report. + +- [ ] **Step 5: Run focused GREEN** + +Run: + +```text +python3 -m unittest \ + tests.test_python_checker_contract \ + tests.test_feature_archive_support \ + tests.test_feature_monthly_archive_scan \ + tests.test_feature_monthly_archive_apply \ + tests.test_feature_monthly_archive_restore \ + tests.test_adr_requirement_model_trace -v +bash tests/validate-feature-monthly-archive-runtime.sh +bash tests/validate-feature-monthly-compaction-proposal.sh +``` + +Expected: all focused suites PASS; scan/check no-mutation and apply/restore fixture cleanup assertions pass. + +## Task 7: Full Validation, Review, And Development-Agent Handoff + +**Files:** + +- Read and follow: `docs/maintenance/full-validation-method.md` +- Create: `docs/reports/agent-loop-v1.3.0-full-validation-2026-07-14.md` +- Review: every file in this plan's responsibility map + +- [ ] **Step 1: Run all native Python and shell tests** + +Run: + +```text +python3 -m unittest discover -s tests -p 'test_*.py' -v +for test_file in tests/validate-*.sh; do bash "$test_file"; done +``` + +Expected: zero failures. Record exact counts from output. + +- [ ] **Step 2: Run mechanical checks** + +Run: + +```text +ruby -e 'require "yaml"; YAML.load_file("SKILL.md")' +python3 -m compileall -q scripts tests +for shell_file in scripts/*.sh tests/*.sh; do bash -n "$shell_file"; done +for ruby_file in scripts/*.rb; do ruby -c "$ruby_file" >/dev/null; done +git diff --check +``` + +Run the repository Markdown-fence and JSON parsers used by the prior 1.3.0 full validation. Remove generated `__pycache__` directories after compile checks. + +- [ ] **Step 3: Perform six-domain semantic pressure review** + +Audit Logic Correctness, Autonomy, Project Entry/Onboarding, Development/Test Workflow, Memory, and Recommendation. Explicitly test routing priority for feature archive versus Active Feature Guard; Batch Human Gate non-bypass; archive state versus lifecycle; rehydrate before execution; stale memory/journal recovery; and no deletion/manual move rationalization. + +- [ ] **Step 4: Request code review** + +Use `requesting-code-review`. Run Spec Review against every proposal acceptance scenario and Standards Review because the diff changes routing, root guidance, recovery, filesystem mutation, and cross-platform tooling. Fix accepted findings through TDD and rerun all affected plus full tests. + +- [ ] **Step 5: Save the full validation report** + +Write a Chinese report with real RED/GREEN evidence, exact counts, score, severity totals, unresolved risks, Windows status, unrelated dirty-work boundary, and explicit statement that commit/push/tag/PR remain unauthorized unless the human separately confirms them. + +- [ ] **Step 6: Final implementation handoff checkpoint** + +Present a table containing proposal coverage, plan tasks completed, tests, platform evidence, review, drift, changed files, unrelated files excluded, remaining risks, and exactly one recommended next stage. Do not commit, push, tag, PR, release, publish, close, or move real project feature directories without the corresponding later Human Gate. + +## Expected Commit Decomposition After A Separate Submit Gate + +If the human later authorizes commits, prefer these reviewable commits; do not create them during plan execution without the Submit two-stage confirmation: + +1. `feat(v1.3.0): 增加 feature 归档路径解析能力` + - shared locator/index/plan model; + - ADR archived Feature Spec compatibility; + - native resolver tests. +2. `feat(v1.3.0): 实现 feature 月度归档事务命令` + - scan/check/apply/restore; + - transaction journal, stale-plan, rehydrate; + - mutation and recovery tests. +3. `docs(v1.3.0): 发布 feature 月度归档工作流` + - runtime/design/stage/root guidance/templates; + - human docs, scenarios, changelog, reports; + - full validation evidence. + +Each meaningful commit uses the repository-required Chinese multiline body. Push both `origin` and `ai-factory` only after explicit push authorization. Do not tag unless separately requested. + +## Plan Self-Review + +### Proposal coverage + +| Accepted proposal requirement | Plan coverage | +|---|---| +| Whole feature directory moves intact | Task 4 apply + file/hash assertions | +| Root `features/archive.md` locator | Task 2 template/parser/resolver | +| Stable Feature ID | Task 2 model and Task 5 artifact rules | +| Only eligible closed historical features | Task 3 eligibility and Task 5 gate | +| Mixed month allowed with blocked feature flat | Task 1/3 fixtures and runtime scenario | +| Follow-up / ADR / Requirement / Project Memory references | Task 2 ADR plus Task 5 coordinated rules | +| Read-only scan and check | Task 3 snapshots and command review | +| Exact Batch Human Gate | Task 5 Human Review and expected plan hash | +| Stale-plan protection | Task 2 hash plus Task 4 apply | +| Persistent failure restore | Task 4 journal and interrupted-process tests | +| Rehydrate before execution | Task 4 behavior plus Task 5 Follow-up routing | +| Windows/macOS standard-library behavior | Tasks 0, 6, and 7 | +| No summary/history/delete/deep archive | Boundary, Task 4 mutation review, runtime negative assertions | + +### Type and naming consistency + +- CLI operation values are exactly `archive | rehydrate`; check adds `restore` only for validation mode. +- Archive state values are exactly `archived | rehydrated`. +- Feature lifecycle remains `draft | active | blocked | paused | closed`. +- Plan hash field and CLI flag consistently use `plan_sha256` / `--expected-plan-sha256`. +- All target-project paths serialize as POSIX paths relative to project root. +- All dates supplied to deterministic behavior use explicit ISO `YYYY-MM-DD` arguments. + +### Risk review + +| Risk | Control | +|---|---| +| Reader misses month paths | Reader Compatibility completes and passes before apply exists | +| Human-approved plan drifts | apply rebuilds plan and requires exact SHA-256 | +| Process dies mid-move | journal state and backups persist before mutation | +| Restore destroys new work | file hashes and exact transaction ID gate restoration; mismatch fails closed | +| Relative links change with directory depth | deterministic Reference Impact edits plus pre/post hashes | +| Accepted ADR meaning is rewritten | only locator/path changes; decision content/status/review stays immutable | +| Active feature is archived | eligibility, project memory, lifecycle, and current-month blockers | +| Same Feature ID exists twice | flat/month collision hard failure | +| Cross-platform paths differ | POSIX serialization, explicit dates, standard library, Windows/macOS fixtures | +| Scope expands into content deletion | negative runtime/test assertions and no deletion CLI | + +### Placeholder and ambiguity result + +The placeholder scan must return no matches for any forbidden incomplete-plan pattern defined by `writing-plans`. If execution discovers a new artifact schema, file boundary, public interface, deletion need, or unsupported reference form, stop at Human Review instead of improvising beyond the accepted proposal. diff --git a/docs/proposal/v1.3.x/feature-monthly-compaction.md b/docs/proposal/v1.3.x/feature-monthly-compaction.md index e0d207f..da93dd2 100644 --- a/docs/proposal/v1.3.x/feature-monthly-compaction.md +++ b/docs/proposal/v1.3.x/feature-monthly-compaction.md @@ -1,378 +1,463 @@ -# Proposal: Feature Monthly Compaction +# Proposal: Feature Monthly Archive -状态:讨论草案 -目标版本:v1.2.4 候选 -创建时间:2026-07-01 - -## 背景 - -`agent-loop` 现在把 feature workspace 直接放在 `.agent-loop/features/` 第一层: - -```text -.agent-loop/features/YYYY-MM-DD-/ -``` +状态:已实现;待最终 Human Review -这个结构对当前开发很友好:路径短、Agent 易写、recent scan 易查。但 feature 越来越多以后,第一层目录会变长,人类浏览困难,Agent 做 Feature Follow-up / Flow-back、Targeted Feature Scan、Re-Adopt 时也容易扫描过多历史细节。 +平台证据:`macOS-verified / Windows-test-defined` -问题不是“历史 feature 不重要”,而是旧 feature 的主要价值已经从施工过程转为交付摘要: +Human Review:2026-07-14,人类确认采用整目录按月归档、根 `features/archive.md` locator、统一引用更新、post-check、失败恢复与 rehydrate 设计 -```text -当前 feature 需要完整施工上下文。 -旧 feature 更需要稳定的最终事实、交付行为、验证摘要和索引关系。 -``` - -## 目标 +实施计划:`docs/proposal/v1.3.x/feature-monthly-archive-implementation-plan.md` -本 proposal 目标是定义一套安全的 feature 月度压缩规则: +目标版本:v1.3.0 候选 -1. 当前月保持 flat,不影响正在开发的 feature; -2. 上个月且整月全部完成后,才允许月度压缩; -3. 压缩后第一层目录按月份收束; -4. 旧 feature 以 `archive.md` 作为主入口; -5. 历史施工文件默认进入 `historical/`,不默认删除; -6. 明确哪些索引关系、引用路径和扫描规则会受影响; -7. 为未来实施提供安全 gate 和验证方向。 +创建时间:2026-07-01 -## 非目标 +最近更新:2026-07-14 -第一版不做以下事情: +> 文件名保留 `feature-monthly-compaction.md` 以维持历史引用;本 proposal 的能力名称和行为已经收敛为 `Feature Monthly Archive`。这里的 archive 只改变 feature 目录位置和默认发现入口,不压缩、重组或删除 feature 内容。 -- 不自动压缩当前月 feature; -- 不在月份中存在 active / paused / in-progress feature 时自动压缩整月; -- 不默认删除 `spec.md`、`tasks.md`、`tests.md`、`plan.md`、`notes.md` 等历史细节; -- 不默认做 Partial month compaction; -- 不把 `requirements/` 的原始需求材料压缩成摘要; -- 不把 feature archive 变成 project memory; -- 不替代 Feature Follow-up / Flow-back 的归属判断; -- 不破坏 human gate:移动、压缩、删除历史细节都必须经过人类确认。 +## 摘要 -## 核心观点 +人类可以选择一个或多个历史月份,例如 5 月和 6 月。Agent 先扫描这些月份对应的 flat feature,只有生命周期为 `closed` 且关闭证据完整的 feature 才能进入候选。人类确认后,Agent 把每个候选 feature 目录原样移动到各自的月份目录,并在 `.agent-loop/features/archive.md` 记录稳定 Feature ID、当前位置和一行交付摘要。 ```text -当前月保持 flat。 -上个月且整月全部完成后,才允许月度压缩。 -Slim With History 是默认压缩模式。 -Deep Archive / Summary Only 永远不是默认动作。 +Human selects 2026-05 and 2026-06 +→ read-only Feature Monthly Archive Scan +→ show eligible / blocked features and reference updates +→ one Batch Human Gate +→ move each eligible feature directory intact +→ update features/archive.md and durable references +→ post-check paths, links, indexes, and feature contents ``` -目录压缩解决的是 `.agent-loop/features/` 第一层过长的问题。内容压缩解决的是旧 feature 不应再让 Agent 默认深读完整施工过程的问题。 +这个能力解决的是 `.agent-loop/features/` 第一层过长和历史 feature 默认扫描过重的问题,不解决磁盘空间问题。 -这两个目标可以一起做,但默认策略必须保守:移动到月目录、生成摘要、保留历史细节。 +## 目标 -## 压缩模式 +1. 当前或仍在工作的 feature 继续留在 `.agent-loop/features/` 第一层; +2. 人类可以一次选择一个或多个历史月份进行归档; +3. 每个 eligible feature 目录完整移动到 `features/YYYY-MM/`,内部文件结构不变; +4. `features/archive.md` 成为 archived Feature ID 到当前位置的稳定 locator; +5. Follow-up、ADR、Requirement Mapping 和 Project Memory 使用统一引用更新逻辑; +6. 归档前只读发现,归档后强制检查,失败时恢复; +7. macOS 与 Windows 使用同一套 Python 标准库脚本和 fixture。 + +## 非目标 -| Mode | 适用对象 | 目录形态 | 内容策略 | 默认 | -|---|---|---|---|---| -| Full | 当前月 feature;active / paused / in-progress feature;有 open follow-up 的 feature | `.agent-loop/features/YYYY-MM-DD-/` | 保留完整 `product.md`、`spec.md`、`tasks.md`、`tests.md`、`plan.md`、`notes.md`、handoffs 和复杂细节目录 | 是 | -| Slim With History | 上个月或更早,且整月全部完成、无 open follow-up、索引已回填 | `.agent-loop/features/YYYY-MM/YYYY-MM-DD-/` | `archive.md` 成为主入口;旧施工文件移入 `historical/` | 是 | -| Deep Archive / Summary Only | 更早月份,且人类明确要求进一步深压缩 | `.agent-loop/features/YYYY-MM/YYYY-MM-DD-/` | 默认只保留 `README.md` 和 `archive.md`;删除、打包或外移 `historical/` 需要强 human gate | 否 | +第一版明确不做: -Deep Archive 永远不是默认动作。即使 5 月及以前的 feature 已经很旧,也只是具备“可建议深压缩”的资格,不代表 Agent 可以自动删除历史细节。 +- 不自动按日历月份运行; +- 不自动归档当前月; +- 不移动 `active`、`blocked` 或 `paused` feature; +- 不修改 feature 的业务内容或生命周期含义; +- 不生成每个 feature 的新 `archive.md`; +- 不创建 `historical/`; +- 不创建 `features/YYYY-MM/INDEX.md`; +- 不删除、打包、外移或摘要替代 `spec.md`、`tasks.md`、`tests.md`、`plan.md`、`notes.md`、`product.md`、`contracts.md` 或细节目录; +- 不压缩 `requirements/`、`.agent-loop/decisions/` 或 project memory; +- 不把 `features/archive.md` 变成产品、需求、决策或验证事实的权威来源; +- 不允许 Agent 手工移动目录来绕过 scan、Human Gate、post-check 或 restore。 -## 推荐目录 +Deep Archive、Summary Only 和历史文件删除不属于本 proposal。未来如果确实需要,应单独提案和单独评估恢复风险。 -当前月保持 flat: +## 目录模型 + +归档前: ```text .agent-loop/ features/ - 2026-07-01-wallet-recharge/ + 2026-05-08-login/ + spec.md + tasks.md + tests.md + plan.md + notes.md + 2026-06-12-payment/ product.md spec.md tasks.md tests.md plan.md notes.md + contracts.md + contracts/ + 2026-07-10-current-feature/ + spec.md + tasks.md + tests.md + plan.md + notes.md ``` -上个月整月完成后压缩为 Slim With History: +人类选择归档 2026-05 和 2026-06 后: ```text .agent-loop/ features/ - 2026-07-01-wallet-recharge/ - 2026-07-03-token-deduction/ + archive.md + 2026-05/ + 2026-05-08-login/ + spec.md + tasks.md + tests.md + plan.md + notes.md 2026-06/ - INDEX.md - 2026-06-13-login/ - README.md - archive.md - historical/ - product.md - spec.md - tasks.md - tests.md - plan.md - notes.md - handoffs/ - plans/ - tasks/ - tests/ + 2026-06-12-payment/ + product.md + spec.md + tasks.md + tests.md + plan.md + notes.md + contracts.md + contracts/ + 2026-07-10-current-feature/ + spec.md + tasks.md + tests.md + plan.md + notes.md ``` -更早月份在强确认后可进入 Deep Archive / Summary Only: +归档只增加月份目录并移动整个 feature 目录。feature 内部同目录或子目录关系保持不变;由于目录深度增加,指向 feature 外部的相对 Markdown 链接必须进入 Reference Impact Scan 并按新位置更新。 + +## 稳定 Feature ID 与路径解析 + +Feature ID 保持不变: ```text -.agent-loop/ - features/ - 2026-05/ - 2026-05-08-upload/ - README.md - archive.md +2026-05-08-login ``` -## 触发入口 +路径可以变化: -人类可以这样说: +```text +flat: .agent-loop/features/2026-05-08-login/ +archived: .agent-loop/features/2026-05/2026-05-08-login/ +``` + +规则: + +1. Feature ID 是稳定身份;目录路径只是当前位置; +2. active / blocked / paused feature 必须位于第一层; +3. archived feature 通过 `features/archive.md` 解析当前位置; +4. 新增或更新的 durable relationship 应同时记录 Feature ID; +5. 仍然保存直接路径的现有引用必须在移动时更新; +6. resolver 不得仅依赖 `features/*/spec.md` 这种单层 glob; +7. archived feature 的原 `spec.md` 仍然是 Feature Spec,归档不会把它替换为摘要。 + +## `features/archive.md` + +`features/archive.md` 是 archive locator 和移动历史账本,不是业务事实来源。它在第一次成功归档时创建,此后由 archive / rehydrate 操作维护。 + +推荐模板: + +```md +# Feature Archive + +This file locates archived or rehydrated features. The feature's own spec, tests, notes, requirement sources, and accepted decisions remain authoritative. + +| Feature ID | Month | Current Path | Archive State | Closed At | Delivered Summary | Source Requirements | Applicable Decisions | Last Moved At | +|---|---|---|---|---|---|---|---|---| +| 2026-05-08-login | 2026-05 | `.agent-loop/features/2026-05/2026-05-08-login/` | archived | 2026-05-20 | 完成登录认证与失败路径验证 | `.agent-loop/requirements/2026-05-01-login/` | `.agent-loop/decisions/ADR-001-login.md` | 2026-07-14 | +``` + +字段规则: + +- `Feature ID`:稳定且唯一; +- `Month`:来自 Feature ID 的年月,不从移动日期推导; +- `Current Path`:当前真实目录; +- `Archive State`:`archived | rehydrated`,它不是 feature lifecycle status; +- `Closed At`:来自 feature Close Record; +- `Delivered Summary`:来自已关闭 feature 的交付/关闭记录,只写一行,不创造新产品含义; +- `Source Requirements` / `Applicable Decisions`:只保存 locator; +- `Last Moved At`:最近一次 archive 或 rehydrate 日期。 + +`spec.md` 仍使用正式 lifecycle: ```text -现在 7 月了,把 6 月份已经做完的 feature 压缩一下。 +draft | active | blocked | paused | closed ``` -Agent 应进入 Feature Compaction Scan,而不是直接移动文件。 +归档资格只接受 `closed`。不要把 `implemented` 或 `archived` 写成 feature lifecycle status。 + +## 人类触发 + +示例: + +```text +把 2026 年 5 月和 6 月已经关闭的 feature 按月份归档。 +``` -## Feature Compaction Scan +这条指令只授权进入 `Feature Monthly Archive Scan`。Agent 必须先展示扫描结果和精确变更范围,再请求一次 Batch Human Gate;不得收到月份后直接移动目录。 + +## Feature Monthly Archive Scan + +Scan 是只读操作。 输入: -- 当前日期; -- `.agent-loop/features/` 第一层 flat feature; -- 已有 month bucket; -- `project.md` Active Feature / Paused Features; -- feature `spec.md`、`tasks.md`、`tests.md`、`plan.md`、`notes.md`、close 记录; +- 人类选择的月份; +- `.agent-loop/features/` 第一层 feature; +- 已有月份目录; +- `features/archive.md`(如果存在); +- `project.md` Active Feature / Paused Features / Current Work; +- feature `spec.md`、`tasks.md`、`tests.md`、`plan.md`、`notes.md`、close record; - requirement set README 和 optional `requirements/INDEX.md`; - `.agent-loop/decisions/*.md`; -- recent Feature Follow-up / Flow-back notes。 +- Follow-up / Flow-back、verification、review 和 drift evidence; +- repository 内指向候选 feature 旧路径的文本与 Markdown 链接。 输出: - Candidate Matrix; -- blocked month / blocked feature reason; -- proposed compaction mode; -- affected index relationship list; -- human confirmation request。 +- eligible 和 blocked 原因; +- old path → new path 映射; +- `features/archive.md` 预期新增/更新行; +- Reference Impact List; +- path collision、symlink/path escape 和 stale-plan 检查结果; +- post-check 与 restore 范围; +- Batch Human Review Summary。 ## Candidate Matrix -Agent 在执行压缩前必须展示类似表格: - -| Month | Feature | Current Path | Status | Open Follow-up | Index Backfill | Proposed Mode | Decision | -|---|---|---|---|---|---|---|---| -| 2026-06 | 2026-06-13-login | `.agent-loop/features/2026-06-13-login/` | closed | none | complete | Slim With History | pending-human | -| 2026-06 | 2026-06-20-upload | `.agent-loop/features/2026-06-20-upload/` | in-progress | none | incomplete | Full | blocked | - -如果某个月存在 blocked feature,默认不做整月压缩。 +```md +| Month | Feature ID | Current Path | Lifecycle | Close Evidence | Open Follow-up | Reference Impact | Decision | +|---|---|---|---|---|---|---:|---| +| 2026-05 | 2026-05-08-login | `.agent-loop/features/2026-05-08-login/` | closed | complete | none | 4 | eligible | +| 2026-05 | 2026-05-22-import | `.agent-loop/features/2026-05-22-import/` | paused | incomplete | none | 2 | blocked | +| 2026-06 | 2026-06-12-payment | `.agent-loop/features/2026-06-12-payment/` | closed | complete | none | 7 | eligible | +``` -## Safety Gate +资格按 feature 判断,不要求一个月份的所有 feature 都完成。人类可以一次批准多个 selected-month 中的 eligible feature;blocked feature 保持 flat,不影响其他 eligible feature,但 Human Review Summary 必须明确显示同月存在 flat 与 archived feature。 -整月进入 Slim With History 前,必须满足: +## Eligibility And Safety Gate -- 该月份不是当前月; -- 该月份所有 feature 都是 terminal 状态,例如 closed / implemented / archived; -- 没有 active / paused / in-progress feature; -- 没有 open follow-up、unresolved drift、未完成 verification、未处理 review blocker; -- requirement set README 的 `Implemented By` / `Feature Mapping` 已回填; -- optional `requirements/INDEX.md` 如存在,也已更新实现状态; -- relevant `decisions/*.md` 的 `Implemented By` 已回填; -- `project.md` 不再把这些 feature 作为 Active Feature 或 Paused Features; -- archive summary 能覆盖 delivered behavior、verification evidence、drift / follow-up notes、changed files / public interfaces; -- Agent 已列出所有需要更新的索引关系; -- 人类明确确认。 +每个 candidate 必须同时满足: -## Human Gate +- 人类明确选择了该月份; +- 该月份不是当前月份; +- feature `spec.md` lifecycle 是 `closed`; +- Close Record 存在; +- tasks 全部为 `done` 或经过人类确认移出范围的 `skipped`; +- fresh verification、Feature Close Review、drift decision 和 project-memory impact 已记录; +- 没有 open follow-up、unresolved drift、review blocker 或待恢复的失败操作; +- `project.md` 不把它列为 Active Feature 或 Paused Feature; +- requirement `Feature Mapping` / `Implemented By` 和相关 decision `Implemented By` 已完成关闭时回填; +- 源目录真实存在且位于 `.agent-loop/features/` 第一层; +- 目标月份与 Feature ID 年月一致; +- 目标路径不存在,不发生大小写或 Unicode 归一化碰撞; +- 源目录、目标目录和被更新引用不得通过 symlink 逃逸 workspace; +- Reference Impact List 覆盖所有已发现旧路径引用和跨 feature 边界相对链接; +- post-check 和 restore 所需的原路径、目标路径、内容快照与引用快照能够写入批次 transaction journal。 -以下动作都必须 Human-gated: +任何一项不满足都必须 fail closed。不要提供 `--force` 绕过资格和安全检查。 -- 创建 month bucket; -- 移动 feature workspace; -- 生成或覆盖 `archive.md`; -- 移动历史细节到 `historical/`; -- 更新 requirement / decision / project memory 索引引用; -- 执行 Partial month compaction; -- 执行 Deep Archive / Summary Only; -- 删除、打包、外移 `historical/`。 +## Batch Human Gate -Partial month compaction 不是默认行为。只有当人类明确要求“这个月部分完成的也先压缩已完成 feature”时,Agent 才能建议,并且必须在 `features/YYYY-MM/INDEX.md` 标记该月份为 `mixed`。 +一次普通归档批次只需要一个 Human Gate。确认摘要必须同时列出: -## Archive Summary Template +- selected months; +- eligible / blocked Feature IDs; +- 每个 old path → new path; +- 将创建的月份目录; +- `features/archive.md` 预期变化; +- Requirement、ADR、Project Memory、Follow-up 和其他引用更新; +- 不会修改或删除的 feature 内容; +- post-check 与失败 restore 行为。 -`archive.md` 应该成为旧 feature 的主入口: +人类确认这个完整批次后,Agent 可以执行其中列出的目录创建、移动、引用更新和 post-check,不为每个机械步骤重复询问。 -```md -# Feature Archive: +任何新增月份、Feature ID、目标路径、引用文件、删除动作或批次范围变化都使原确认失效,必须重新 Scan 和确认。 -Status: archived -Compaction Mode: Slim With History | Deep Archive / Summary Only -Original Feature ID: YYYY-MM-DD- -Current Path: -Archived At: -Archived By: +## 引用更新逻辑 -Source Requirements: -- +### Feature Follow-up / Flow-back -Applicable Decisions: -- +扫描顺序: -Implements Decisions: -- Decision: - - Implemented Slice: +1. 从 `project.md` 读取 Active / Paused feature; +2. 扫描第一层 flat feature; +3. 读取 `features/archive.md`; +4. 对匹配的 archived Feature ID 读取其原有 `spec.md`、`tests.md` 和 `notes.md`; +5. 只有摘要与路径信号不足时才继续读取复杂细节目录。 -Delivered Behavior: -- +归档不会改变 owning feature 判断,也不会把 archived feature 自动重开。 -Key Design Decisions: -- +### ADR / Decision -Changed Files / Public Interfaces: -- +- `Applicable Decisions` 和 `Implements Decisions` 的语义不变; +- 直接保存 Feature Spec 路径的 ADR / decision 引用更新到 month path; +- validator 必须同时支持 flat 和 archived Feature Spec; +- 后续 durable mapping 应优先保存 Feature ID,并通过 resolver / `features/archive.md` 定位路径; +- 归档不得改写 accepted decision 的含义、状态或 Human Review Evidence。 -Verification Summary: -- Evidence: -- Commands: -- Result: +### Requirement Mapping -Drift / Follow-up Notes: -- +- requirement set README 的 `Feature Mapping` / `Implemented By` 保留 Feature ID; +- 直接路径更新为当前 month path; +- optional `requirements/INDEX.md` 如保存 feature path,也同步更新; +- 原始 requirement source 文件保持不变。 -Known Risks: -- +### Project Memory -Historical Detail Location: -- historical/ -``` +- Active Feature 和 Paused Features 不得指向 archived feature; +- Current Work 不得把 archived feature 当作正在执行的 feature; +- Recent Feature 或历史 locator 如存在,更新为 Feature ID 加当前路径; +- 不把所有 archive rows 复制进 `project.md`,历史 inventory 由 `features/archive.md` 负责。 -`README.md` 应很短: +### Internal And External Links -```md -# Archived Feature +- feature 目录内部文件和子目录保持原样; +- 同目录和 feature 内部相对链接通常保持有效; +- 因目录加深而受影响的 `requirements/`、`decisions/`、project docs 等跨边界相对链接必须更新; +- workspace 内指向 old flat path 的 durable reference 必须更新; +- 历史报告中只用于描述过去命令或过去路径的纯文本证据可以保留,但必须由 Reference Impact Scan 分类为 `historical-evidence`,不能静默忽略。 -Read `archive.md` first. -Historical implementation details are under `historical/` when present. -``` +## Apply、Post-Check 与失败恢复 -## 影响的索引关系 +Apply 只能执行人类确认过的 Feature ID 和路径映射。开始时必须重跑关键 preflight;如果文件、状态、引用计数或目标路径与确认时不同,判定 `stale-plan` 并停止。 -Feature 月度压缩会改变路径和默认阅读入口,因此会影响以下索引关系。 +Apply 在首次写入前创建临时 transaction journal: -| Index / Relationship | 当前用途 | 压缩影响 | 必须更新 | -|---|---|---|---| -| `features/INDEX.md` | 全 feature inventory、month view、状态视图 | 需要记录 feature 当前路径、month bucket、compaction mode、archive path | 是 | -| `features/YYYY-MM/INDEX.md` | 月份内 feature 列表 | 新增月度索引,记录该月是否 fully compacted / mixed / deep archived | 是 | -| requirement set README | `Implemented By`、`Delivery Phases`、`Feature Mapping` | feature 路径从 flat 改为 month bucket;phase 到 feature 的链接要改到 archive path 或 current path | 是 | -| `requirements/INDEX.md` | requirement inventory、implemented view、backlog/deferred view | 如果存在,要同步 implemented feature path 和 status | 条件是已存在或本来应更新 | -| `.agent-loop/decisions/*.md` | ADR / decision 的 `Implemented By` | feature 实现引用要改到新的 archive path;`Applicable Decisions` / `Implements Decisions` 在 archive summary 中保留 | 是 | -| `project.md` | Active Feature、Paused Features、Current Work、recent references | Active Feature 和 Paused Features 不能指向 archived feature;旧 closed feature 可指向 archive summary | 是 | -| feature internal links | `Source Requirements`、`Applicable Decisions`、`Implements Decisions` | `archive.md` 需要保留这些关系;historical 文件内的旧相对链接可保留为历史证据,但外部索引要更新 | 是 | -| Feature Follow-up / Flow-back | recent / historical feature ownership scan | 扫描规则要同时支持 flat feature 和 month bucket;archived feature 先读 `archive.md`,必要时再读 `historical/` | 是 | -| Drift Check | 关闭后行为和文档一致性 | 压缩前要确认 drift 已处理;压缩后要确认 archive summary 与 requirement/decision/project memory 一致 | 是 | -| verification evidence | 测试、构建、E2E、review evidence | `archive.md` 必须摘要 verification evidence;完整 evidence 留在 `historical/notes.md` 或 historical detail | 是 | -| scripts / validation glob | 查找 `features/*/spec.md`、`notes.md`、`tests.md` 的脚本 | 需要兼容 `features/YYYY-MM//archive.md` 和 `features/YYYY-MM//historical/spec.md` | 是 | -| recovery / re-adopt scans | 重接管项目时识别历史 feature | 不应把 archived feature 当 active feature;应优先读 archive summary | 是 | +```text +.agent-loop/features/.archive-txn// +``` -## 路径兼容规则 +journal 只保存本批次操作映射、preflight 摘要,以及将被改写的 `archive.md` / Requirement / ADR / Project Memory / link 文件快照;它不复制 feature 目录。成功 post-check 后删除 journal;失败或进程中断时保留 journal,供 restore 在新的 Agent 进程中恢复。journal 是临时恢复材料,不是 feature lifecycle 或 project memory 的新权威来源。 -第一版需要同时支持两种 feature layout: +推荐执行顺序: ```text -.agent-loop/features/YYYY-MM-DD-/ -.agent-loop/features/YYYY-MM/YYYY-MM-DD-/ +revalidate confirmed scope +→ prepare reversible operation snapshot +→ persist transaction journal +→ create selected month directories +→ move whole feature directories +→ update features/archive.md +→ update approved durable references +→ run post-check +→ keep result only when every check passes ``` -扫描顺序建议: - -1. Active / Paused feature from `project.md`; -2. current-month flat features; -3. recent flat features in 30-day lookback; -4. month bucket `archive.md`; -5. `historical/` only when archive summary is insufficient. - -Feature ID 仍然是 `YYYY-MM-DD-`。路径可能变化,ID 不变。索引更新必须使用当前路径。 +Post-check 必须确认: -## Requirements 策略 +- 每个 source path 已不存在; +- 每个 target path 存在且 Feature ID 与月份匹配; +- feature 文件清单和内容哈希与移动前一致,只有已批准的跨边界链接调整除外; +- `features/archive.md` 每个 Feature ID 恰好一行且 Current Path 存在; +- 没有 durable reference 仍指向 old flat path; +- Requirement、ADR、Project Memory 和 Follow-up resolver 能解析新路径; +- blocked / unselected / current feature 没有变化; +- 重复运行 scan 得到 `already-archived`,不会再次嵌套月份目录。 -Requirements 不做内容压缩。 +如果任一写入或 post-check 失败,操作必须恢复: -原因: +- 目录移回原 flat path; +- 从 transaction journal 恢复 `features/archive.md` 和引用文件快照; +- 检查恢复后文件清单、哈希和旧路径; +- 报告失败点和恢复结果; +- restore 未完全通过时进入 Safety Stop,不得报告归档成功。 -- requirement 是人类源材料; -- 需求源材料保持原样,不应被摘要替代; -- requirement lifecycle 和 Delivery Phases 仍由 requirement set README 负责; -- 压缩 feature 不应改变原始需求。 +## Rehydrate / Reopen -后续可以讨论 requirement month bucket: +当 Follow-up 确认 archived feature 是 owning feature,并且人类确认 flow-back / reopen 后: ```text -.agent-loop/requirements/YYYY-MM/YYYY-MM-DD-/ +archived feature selected +→ read-only rehydrate scan +→ Human Review shows month path → flat path and reference impact +→ move the whole directory back to `.agent-loop/features//` +→ update archive row to `rehydrated` and Current Path to flat +→ update durable references +→ post-check +→ normal Feature Follow-up lifecycle may set the feature active ``` -但第一版只把 feature 压缩作为第一版默认能力。Requirement 的第一层过长问题优先靠 `requirements/INDEX.md` 和未来可选 bucket 解决,不做内容压缩。 +Rehydrate 只是恢复工作目录位置,不自行授权 scope change,不自行把 `closed` 改为 `active`。Feature Follow-up 的人类决定仍然控制是否重开。 -## 与 Feature Follow-up / Flow-back 的关系 +## 跨平台脚本边界 -Feature Follow-up / Flow-back 的默认 30 天 lookback 仍然有效,但不再假设所有 feature 都在 `.agent-loop/features/*/` 第一层。 +生产脚本使用 Python 3.10+ 标准库,并复用 cross-platform runtime 的 UTF-8、BOM/CRLF、确定性排序、path confinement 和 exit-code 约定。 + +建议入口: + +```text +scripts/scan-feature-monthly-archive.py +scripts/apply-feature-monthly-archive.py +scripts/check-feature-monthly-archive.py +scripts/restore-feature-monthly-archive.py +``` -如果 bug/change 指向 archived feature: +契约: -1. 先读 `archive.md`; -2. 判断 delivered behavior、changed files、public interfaces、known risks 是否匹配; -3. 不足时再读 `historical/spec.md`、`historical/tests.md`、`historical/notes.md`; -4. 如果确认为 owning feature,可以建议 flow-back; -5. 如果 archived feature 已 deep archived 且缺少 historical detail,应记录 evidence limitation。 +- scan 和 check 只读; +- apply 和 restore 只接受人类确认范围; +- Python capability 不满足时 exit 2 并 fail closed; +- 不回退到 Ruby、Bash、PowerShell 或 Agent 手工移动; +- 不自动安装 Python; +- macOS 与 Windows 对相同 fixture 给出相同候选、阻塞、路径和恢复结论。 -## 与 Close / Drift Check 的关系 +## 实施顺序 -Feature Close 不直接压缩,但要让未来可压缩: +### Phase 0: Cross-Platform Python Runtime Acceptance -- close summary 要足够支持 archive summary; -- requirement mapping 要完整; -- decision mapping 要完整; -- verification evidence 要可摘要; -- drift decision 要记录; -- project memory update 要完成或明确 none。 +- canonical Python checker、macOS parity 和 CI matrix 已实现; +- 当前本地报告仍标记 `Windows-test-defined`,不得把它写成 Windows 已通过; +- 先读取远端 Windows CI 证据并完成最终 Human Review; +- Phase 0 未最终 accepted 前,不实现 archive apply / restore。 -Feature Compaction Scan 可以视为月度 close 后维护动作。它不替代单个 feature close。 +### Phase 1: Proposal Revision -## 迁移策略 +- 本 proposal 收敛为整目录归档; +- 移除内容重组、`historical/`、Deep Archive 和删除路径; +- 明确 Feature ID、archive locator、Batch Human Gate、post-check 和 rehydrate。 -### Phase 0: Cross-Platform Script Runtime +### Phase 2: Reader Compatibility -- 先批准并实现 `docs/proposal/v1.3.x/cross-platform-python-script-runtime.md`; -- 月度压缩的发现、执行、恢复和后检脚本统一使用 Python 3 标准库; -- 新脚本必须在原生 Windows 与 macOS 上对相同 fixture 给出一致结果; -- Python capability 不可用时 fail closed,不得回退到 Ruby、Bash 或 Agent 手工移动; -- Phase 0 未完成前,不进入本 proposal 的 Runtime Support 或 Compaction Command 实现。 +- 增加统一 Feature Path Resolver; +- Follow-up、Targeted Scan、Recovery / Re-Adopt 支持 flat 与 month path; +- ADR / requirement validators 支持 archived Feature Spec; +- 新建 active feature 仍只使用 flat path; +- 增加 `features/archive.md` 模板和 validation scenarios; +- 完成 reader compatibility 后仍不移动真实 feature。 -### Phase 1: Proposal +### Phase 3: Scan And Post-Check -- 只建立规则和影响面; -- 不改运行时规则; -- 不移动现有 feature。 +- 实现只读 scan 和 check; +- 固定 Candidate Matrix、Reference Impact List、exit code 和 deterministic output; +- 用 fixture 证明 active / paused / blocked、open follow-up、stale path、collision、symlink escape 和 broken link 会 fail closed。 -### Phase 2: Runtime Support +### Phase 4: Apply And Restore -- 增加 feature path resolver; -- 所有 recent / targeted / follow-up scan 支持 flat 和 month bucket; -- 增加 `archive.md` / monthly `INDEX.md` 模板; -- 增加 validation scenarios。 +- 实现人类确认后的整目录移动和引用更新; +- 实现 stale-plan stop、失败恢复、幂等和 rehydrate; +- 在 Windows 与 macOS 运行相同 fixture; +- 通过 focused validation、full validation 和 Human Review 后才允许真实项目归档。 -### Phase 3: Compaction Command +## 验收场景 -- 人类触发 Feature Compaction Scan; -- Agent 输出 Candidate Matrix; -- 人类确认后执行 Slim With History; -- 复验所有索引关系。 +至少覆盖: -### Phase 4: Optional Deep Archive +1. 人类选择 5 月和 6 月,多个 closed feature 被移动到各自月份; +2. 同月 paused feature 保持 flat,eligible closed feature仍可在明确批次中归档; +3. 当前月、active、blocked、paused、open follow-up 和 incomplete close feature 被拒绝; +4. 整目录移动前后文件清单和哈希一致; +5. 跨边界相对链接和 old flat path 引用被正确更新; +6. `features/archive.md` locator 可以解析所有 archived Feature ID; +7. ADR Feature Spec、Requirement Mapping、Project Memory 和 Follow-up 扫描能解析 month path; +8. 路径碰撞、大小写冲突、Unicode 差异、symlink escape 和 stale-plan 被拒绝; +9. 中途失败恢复全部目录和引用; +10. 重复 apply 不产生 `YYYY-MM/YYYY-MM/`; +11. rehydrate 把整个 feature 恢复到 flat path,并保持 Feature ID 与文件内容; +12. Windows 与 macOS 对相同 fixture 输出一致; +13. scan / check 不修改任何文件; +14. 任何脚本都不提供删除历史内容或 `--force` 绕过路径。 -- 仅在更早月份、人类明确要求、historical detail 已不再需要时执行; -- 必须单独列出将删除或外移的文件; -- 必须保留 `archive.md` 和必要 verification summary。 +## Proposal Boundary -## 待讨论问题 +本文件仍是 proposal,不是发布运行时权威。它不会自行改变 `SKILL.md`、`references/runtime.md`、`references/design.md`、Feature Follow-up、ADR validator 或目标项目目录。 -- `features/INDEX.md` 是否应该在第一个 month bucket 出现时自动创建? -- old flat feature 的外部人类书签会失效,是否需要额外生成迁移报告? -- Deep Archive 是否应该支持压缩包,而不是删除 `historical/`? -- requirement month bucket 是否应该和 feature compaction 同期实现,还是作为独立 proposal? -- 如果一个月份有一个 long-running paused feature,是否允许其他 closed features 做 Partial month compaction? +本实现是在 proposal 与实施计划分别获得 Human Review 授权后开始,并先完成 Phase 0、Reader Compatibility 与 RED/GREEN 契约。源码仓库中的实现与测试不会移动任何真实目标项目 feature;目标项目执行 archive 或 rehydrate 仍必须分别经过运行时 Human Gate。 diff --git a/docs/reports/agent-loop-v1.3.0-feature-monthly-archive-validation-2026-07-14.md b/docs/reports/agent-loop-v1.3.0-feature-monthly-archive-validation-2026-07-14.md new file mode 100644 index 0000000..e626bdb --- /dev/null +++ b/docs/reports/agent-loop-v1.3.0-feature-monthly-archive-validation-2026-07-14.md @@ -0,0 +1,119 @@ +# Agent Loop v1.3.0 Feature Monthly Archive 专项验证报告 + +验证日期:2026-07-14 +验证范围:Feature Monthly Archive(目录归档、locator、Reader Compatibility、scan/check/apply/restore/rehydrate) +平台结论:`macOS-verified / Windows-test-defined` +版本结论:沿用 v1.3.0 开发线,未修改版本号 + +## 1. 结论 + +Feature Monthly Archive 的本地专项实现通过。归档只把经过人类确认的完整 closed Feature 目录移动到 `.agent-loop/features/YYYY-MM//`,并维护根级 `.agent-loop/features/archive.md`;没有内容压缩、per-feature summary、`historical/`、Deep Archive、删除或自动调度能力。 + +本地最终 focused 回归为 79/79 个 Python 测试通过、2/2 个专项 shell contract 通过。由于当前环境没有可用的远程 Windows run 证据,跨平台结论不能写成已验收,只能保持 `macOS-verified / Windows-test-defined`。 + +## 2. RED 基线 + +| RED 阶段 | 真实结果 | 证明的缺口 | +|---|---:|---| +| 初始 archive contract | 27 个测试方法运行,产生 28 个预期失败 | support module、archive template、四个 CLI 与 Reader Compatibility 尚不存在 | +| Runtime coordinated contract | 1 个 shell contract 失败 | `SKILL.md` 尚无 Feature Monthly Archive runtime authority | +| Cross-platform CI contract | 1 个测试方法产生 8 个失败 | 四个 archive 测试模块和四个 CLI 路径尚未进入 Windows/macOS workflow | +| Proposal implementation status contract | 1 个 shell contract 失败 | proposal 仍停留在设计/计划状态,没有标记为“已实现;待最终 Human Review” | +| Responsibility-map contract | 1 个 shell contract 失败 | project decision、requirement mapping、project memory、project guidance 与 execution-path negative assertion 尚未同步 | +| Restore check contract | 1 个 Python 测试失败 | `check --operation restore` 虽出现在 CLI contract 中,但没有可到达的成功语义 | +| Journal path escape | 1 个 Python 测试失败,错误返回 0 | 篡改 journal 的 `../` move path 可以把目录移出 workspace;该 Critical 已用统一 confinement 修复 | +| Rename/journal crash window | 1 个 Python 测试失败 | rename 完成但 completion record 尚未写入时,新进程无法恢复;现按 source/target 实际状态恢复 | +| Whole-directory hash coverage | 1 个 Python 测试失败 | 大于 2 MiB 的 feature payload 未进入 snapshots;现仅 Markdown reference scan 保留 2 MiB 限制 | +| BOM/CRLF preservation | 1 个 Python 测试失败 | 引用更新会移除 UTF-8 BOM 并规范化换行;现逐字节保留编码标记和 CRLF | +| Broken relative link | 1 个 Python 测试失败 | 不存在的跨边界相对链接仍会被重写;现分类为 `unsupported` 并阻断 apply | +| Journal scope tampering | 1 个 Python 测试错误返回 0,并删除不在原计划内的 `README.md` | 外层 journal 与内嵌 plan 被一起篡改并重算自哈希时,`missing-before` 可越权;现要求 reference-edit 哈希链从原 snapshots 起步,并在任何目录移动前校验 backup bytes | +| Post-crash human drift | 1 个 Python 测试错误返回 0 | restore 会覆盖进程中断后的人类修改;现对 source/target 实际位置和全部当前字节做 preflight,发现 drift 后保留 journal 并停止 | +| Stranded transaction bypass | 2 个 Python 测试错误返回 0 | 已存在 `.archive-txn/` 时仍可生成新 plan 或开始 apply;现 scan 与 apply 都要求先按显式 transaction ID 恢复 | +| Inbound relative link blind spot | 2 个 Python 测试失败 | `.agent-loop/project.md` 中 `features//spec.md` 这类指向待移动 feature 的相对链接未进入 plan,apply 后可留下 broken link;现双向计算跨移动边界的链接目标 | + +这些 RED 都发生在对应实现或 authority 更新之前;没有通过弱化断言把失败改成 GREEN。 + +## 3. GREEN 证据 + +执行命令: + +```text +python3 -m unittest \ + tests.test_python_checker_contract \ + tests.test_feature_archive_support \ + tests.test_feature_monthly_archive_scan \ + tests.test_feature_monthly_archive_apply \ + tests.test_feature_monthly_archive_restore \ + tests.test_adr_requirement_model_trace -v +``` + +Task 6 首次运行结果为 `Ran 64 tests`,`OK`。首轮 Standards Review 后为 `Ran 72 tests`,`OK`;本轮 Human Review 针对四个问题补强 7 条回归并完成修复后,最终为 `Ran 79 tests`,`OK`。 + +执行命令: + +```text +bash tests/validate-feature-monthly-archive-runtime.sh +bash tests/validate-feature-monthly-compaction-proposal.sh +``` + +结果:2/2 PASS。 + +## 4. 逻辑与压力场景 + +### Eligibility 与阻断 + +- 两个月 fixture 中,`2026-05-08-login` 与 `2026-06-12-payment` eligible;同批 `2026-05-22-import` 为 paused,保留在 flat path。 +- active、blocked、paused、current-month、incomplete close、缺少 Archive Readiness、非终态 readiness、placeholder summary、open follow-up、project memory active/paused 均被阻断。 +- flat/month collision、symlink escape、大于 2 MiB 且含旧路径的 Markdown、不确定旧路径引用均 fail closed。 +- 原始 requirement source 被分类为 `immutable-requirement-source`,不被重写;历史报告只作为可见 preserved evidence。 + +### Read-only 与哈希证据 + +- `scan`、pre-check、post-check、current-month blocker、collision 与 stale-plan 场景都在操作前后比较完整文件树 SHA-256 map;结果完全相等,没有新增、删除或字节漂移。 +- deterministic plan 测试在两个不同绝对根目录生成相同 canonical JSON SHA-256,绝对路径不进入 plan hash。 +- valid-but-different `expected-plan-sha256` 与 scan 后状态漂移均以 `stale-plan` 拒绝,且完整 SHA-256 map 保持不变;状态漂移拒绝时不会创建 transaction journal。 + +### Apply、恢复与 rehydrate + +- 两个 closed Feature 被整目录移动到各自月份;移动前后的完整文件 SHA-256 map 相同,binary payload 字节保持不变,archive locator 按 Feature ID 排序。 +- 只执行 plan 中预计算的 project、requirement README 与 ADR 引用更新;人类原始 requirement source 和无关 Markdown 字节保持不变。 +- 重复 apply 不创建 `YYYY-MM/YYYY-MM/`,无 move 的重复计划保持幂等。 +- test-only 注入引用写失败后,目录、引用和 locator 由 transaction journal 恢复到完整的事前 SHA-256 map;非 test mode 使用注入变量会被拒绝。 +- 新进程可以从显式 transaction ID 恢复中断事务;发生源路径碰撞时 fail closed,并保留 `restoring` journal 供人工处理。 +- restore 在任何 feature move 或引用回写前校验 journal/plan/snapshot scope、reference-edit 哈希链、备份字节和当前工作区字节;整体篡改 journal、损坏 backup 或崩溃后出现人类修改时都不会覆盖项目内容。 +- 任一 stranded `.archive-txn/` 会同时阻断新 scan 和 apply;项目记忆中指向待移动 feature 的相对 Markdown link 会进入确定性 plan 并随 apply 更新。 +- rehydrate 把完整目录移回 flat path,将 locator 标记为 `rehydrated`,但 `spec.md` 仍保持 `closed`;后续 reopen 仍需独立 Feature Follow-up Human Gate。 + +## 5. 平台证据 + +| 平台 | 状态 | 证据 | +|---|---|---| +| macOS | verified | Python 3.10+ 本地 79/79 focused tests 与 2/2 shell contracts 通过 | +| Windows | test-defined | GitHub Actions 保留 `windows-latest` × Python `3.10`/`3.x`,已加入四个模块和四个 CLI `--help`;本轮没有取得成功 run | + +实现只使用 Python 3.10+ 标准库。文档同时给出 macOS `python3` 与 Windows PowerShell `py -3` 调用方式。 + +## 6. 严重度审计 + +| 严重度 | 数量 | 结论 | +|---|---:|---| +| Critical | 0 | 未发现删除历史内容、绕过 Human Gate、跳过 expected hash 或越界写入能力 | +| High | 0 | 未发现 stale-plan、恢复、locator 或 lifecycle/archive-state 混淆缺口 | +| Medium | 1 | Windows workflow 已定义但未取得实际成功 run;因此不能完成跨平台验收 | + +专项评分:96/100。扣分仅来自 Windows 远程执行证据缺失,不将“测试已定义”冒充“跨平台已通过”。 + +## 7. 明确排除范围 + +- 不创建 per-feature archive summary 或 per-month index; +- 不创建 `historical/`、Deep Archive、Summary Only; +- 不删除、打包、压缩、外移或替代历史 feature 内容; +- 不自动定时归档,不自动归档当前月; +- 不移动 active、blocked、paused 或证据不完整的 feature; +- 不改写原始人类需求文件或 accepted ADR 产品语义; +- 不提供 `--force`; +- 不在技能源码仓库创建真实目标项目 `.agent-loop/features/`。 + +## 8. 授权与边界 + +本报告只覆盖实现与 focused validation。commit、push、tag、PR、merge、release、publish 均未获得本轮授权,也未执行。工作区原有 onboarding proposal 修改、v1.4 proposal 删除与 `docs/proposal/v2.0.x/` 内容不属于本功能,没有被恢复、覆盖、暂存或纳入本报告结论。 diff --git a/docs/reports/agent-loop-v1.3.0-full-validation-2026-07-14.md b/docs/reports/agent-loop-v1.3.0-full-validation-2026-07-14.md new file mode 100644 index 0000000..129c0bb --- /dev/null +++ b/docs/reports/agent-loop-v1.3.0-full-validation-2026-07-14.md @@ -0,0 +1,227 @@ +# Agent Loop v1.3.0 全量验证报告(Feature Monthly Archive) + +验证日期:2026-07-14 +分支:`alpha/v1.3.0` +基线 HEAD:`e49673c` +审计对象:当前未提交工作区中的 Feature Monthly Archive 实现及其协调 authority 变更 +平台状态:`macOS-verified / Windows-test-defined` + +## 1. 总结 + +总分:**98/100** +等级:**STRONG** +最终测试:**98/98 Python tests PASS;33/33 `tests/*.sh` PASS** +当前严重度:**Critical 0 / High 0 / Medium 1 / Low 0** + +Feature Monthly Archive 已形成从只读 scan、确定性 plan、精确 SHA-256 Human Gate、事务 apply、post-check、失败 restore 到独立 rehydrate Gate 的闭环。目录归档只改变位置与 locator,不压缩、摘要替代或删除 feature 内容。当前唯一 Medium 是没有取得 Windows 远程成功 run,因此本轮不能声称跨平台验收完成。 + +## 2. 六域语义审计 + +| 审计域 | 结果 | 分数 | 通过的不变量 | +|---|---|---:|---| +| Logic Correctness | PASS | 99 | `feature-archive-maintenance` 位于 Memory Recovery 后、Active Feature Guard 前;archive state 不混入 lifecycle;stale plan、collision、unsafe reference、path escape 均 fail closed | +| Autonomy | PASS | 98 | Agent 可先做只读 evidence/eligibility/reference scan,输出唯一推荐;apply/rehydrate 必须停在精确 plan SHA-256 Gate | +| Project Entry / Onboarding | PASS | 98 | 先要求可靠 project memory;不把 archive 变成 onboarding 或新 canonical stage;源码仓库没有创建目标项目 `.agent-loop/` | +| Development / Test Workflow | PASS | 99 | Reader Compatibility 先行;RED/GREEN、整目录哈希、post-check、transaction restore、rehydrate-before-execution 闭环完整 | +| Memory | PASS | 98 | Feature ID 稳定;`features/archive.md` 仅为 locator;project/requirement/ADR authority 不被替代;stale locator/journal 路由 Recovery | +| Recommendation | PASS | 98 | 完成、stale-plan、restore failure、rehydrate 与 Follow-up 都有唯一下一阶段;auto mode 不能授权归档 | + +加权结果为 98,且不存在未解释的 Critical/High。 + +## 3. RED 基线与修复证据 + +### 初始 TDD RED + +- 初始 archive contract:27 个测试方法产生 28 个预期失败,证明 support/template/CLI/Reader Compatibility 缺失。 +- Runtime contract:首先因 `SKILL.md` 缺少 Feature Monthly Archive authority 失败。 +- CI contract:四个测试模块和四个 CLI 路径产生 8 个明确失败。 +- Proposal status contract:先因缺少“已实现;待最终 Human Review”失败。 + +### Full Review 中发现并修复的 RED + +| 发现 | 修复前证据 | 修复 | +|---|---|---| +| root block stale fixture 未随新 revision 更新 | 全量 shell 第 24 项失败 | 更新 stale fixture 的 source revision,保持 checker 真正验证 stale 状态 | +| 两个旧 precedence regression 仍要求 Archive 前的顺序 | 全量 shell 第 30 项失败 | 与新 `Feature Archive Maintenance` first-match order 同步 | +| 四个 responsibility-map authority surface 遗漏 | 新 runtime contract RED | 补 project decisions、requirement mapping、project memory、project guidance | +| `check --operation restore` 不可成功 | focused Python RED | 定义只读 `restore-check`,只接受精确 pre-transaction state | +| 篡改 journal 可用 `../` 把 feature 移出 workspace | 测试错误返回 0 并真实触发越界 move | 对 plan、apply、journal、backup、snapshot、created-directory 全部统一 workspace confinement | +| rename 后、completion record 前崩溃无法恢复 | 新进程 restore RED | 恢复时按 journal move 的实际 source/target 状态协调,不依赖 completion record 单点 | +| 大于 2 MiB 的 feature payload 未纳入 snapshots | large payload RED | 2 MiB 限制只用于 Markdown reference scan;整目录所有文件均进入哈希 | +| 引用更新移除 UTF-8 BOM 并把 CRLF 改为 LF | BOM/CRLF RED | 引用替换按原始字节哈希并保留 BOM/换行 | +| broken relative link 被计算新路径而未阻断 | broken-link RED | 不存在的跨边界链接分类为 `unsupported` 并阻断 apply | +| 同时篡改 journal 与内嵌 plan 并重算自哈希后,可用 `missing-before` 删除计划外文件 | 新 restore RED 错误返回 0,并实际删除 `README.md` | reference-edit 链必须从原 snapshots 起步;backup scope、状态、路径、哈希和字节在任何目录移动前完成预检 | +| restore 会覆盖崩溃后的人类修改 | 新 restore RED 错误返回 0 | source/target 状态和当前字节只允许事前或本事务中间状态;其他字节作为 `drift` 停止且保留 journal | +| stranded `.archive-txn` 不阻断新 scan/apply | 2 个新 RED 错误返回 0 | scan 与 apply 都先检查残留事务,并要求按显式 transaction ID 进入 Recovery | +| 项目记忆指向待移动 feature 的相对链接未被发现 | scan 无 edit,apply 后链接断裂 | reference impact 同时处理从 moving file 指向外部和从外部指向 moving target 的相对链接 | + +所有修复都先保存失败证据,再运行对应 GREEN;当前没有保留的 Critical/High。 + +## 4. 最终可执行验证 + +### Python + +```text +python3 -m unittest discover -s tests -p 'test_*.py' -v +``` + +结果:`Ran 98 tests`,`OK`。 + +专项复验: + +```text +python3 -m unittest \ + tests.test_python_checker_contract \ + tests.test_feature_archive_support \ + tests.test_feature_monthly_archive_scan \ + tests.test_feature_monthly_archive_apply \ + tests.test_feature_monthly_archive_restore \ + tests.test_adr_requirement_model_trace +``` + +结果:`Ran 79 tests`,`OK`。 + +### Shell contracts + +```text +for test_file in tests/*.sh; do bash "$test_file"; done +``` + +结果:33/33 PASS。两个 shell contract 内部还分别运行了 13 与 6 个 Python case;这些嵌套 case 未重复计入 98。 + +### 机械检查 + +| 检查 | 结果 | +|---|---| +| `ruby -e 'require "yaml"; YAML.load_file("SKILL.md")'` | PASS | +| repository `.yml` / `.yaml` parse | PASS | +| repository JSON parse | PASS(1 file) | +| `python3 -m compileall -q scripts tests` | PASS;随后删除生成的 `__pycache__` | +| Shell `bash -n` | PASS(34 files) | +| Ruby `ruby -c` | PASS(5 files) | +| Markdown fence balance | PASS(199 files) | +| tracked `git diff --check` + untracked `git diff --no-index --check` | PASS | + +## 5. Proposal 验收条件逐项核对 + +| # | 验收条件 | 结果与证据 | +|---:|---|---| +| 1 | 5/6 月多个 closed feature 分月移动 | PASS:双月 apply fixture | +| 2 | 同月 paused 保持 flat,eligible 可归档 | PASS:mixed candidate scan | +| 3 | current/active/blocked/paused/open follow-up/incomplete close 拒绝 | PASS:逐项 eligibility tests | +| 4 | 移动前后文件清单和哈希一致 | PASS:完整 tree SHA-256;含 >2 MiB payload | +| 5 | 跨边界相对链接和 old flat path 更新 | PASS:precomputed exact edits;BOM/CRLF preserved | +| 6 | archive locator 解析全部 archived Feature ID | PASS:parser/resolver/ADR reader tests | +| 7 | ADR、Requirement Mapping、Project Memory、Follow-up 支持 month path | PASS:validator tests + coordinated authority contract | +| 8 | collision、case、Unicode、symlink、stale-plan、broken link 拒绝 | PASS:显式 regression;全部 fail closed | +| 9 | 中途失败恢复目录和引用 | PASS:test-only injected failure,完整事前 SHA-256 map 恢复 | +| 10 | 重复 apply 不产生嵌套 month | PASS:idempotency regression | +| 11 | rehydrate 整目录回 flat,Feature ID/内容不变 | PASS:locator 为 `rehydrated`,`spec.md` 仍 `closed` | +| 12 | Windows/macOS 相同 fixture 输出 | **未完成跨平台实跑**:workflow 已定义,状态保持 `Windows-test-defined` | +| 13 | scan/check 不修改文件 | PASS:全树 SHA-256 before/after equality | +| 14 | 无删除历史内容或 `--force` | PASS:runtime negative contract、CLI review、mutation review | + +结论:除必须诚实保留的 Windows 远程运行证据外,Proposal 的本地行为验收条件均已落地。不能把第 12 项标记为跨平台已验收。 + +## 6. 关键压力场景 + +- Archive intent 与 Active Feature 同时存在:first-match 先进入 archive maintenance,但 active/paused candidate 在 eligibility 内阻断,不切换 active work。 +- 旧 hash 重放:valid-but-different hash 返回 `stale-plan`,没有 journal 或写入。 +- 原始需求出现 old path:记录 `immutable-requirement-source`,保持字节不变;无法确定的普通引用为 `unsupported`。 +- Apply 在引用写入中断:journal 恢复目录、locator 和全部 backup bytes。 +- Apply 在 rename 与 completion record 之间中断:新进程仍可根据 source/target 恢复。 +- 恶意/损坏 plan 或 journal 使用 `../`:`path-escape`,不移动或读取 workspace 外路径。 +- 外层 journal 与内嵌 plan 一起被重写并重算自哈希:reference-edit/snapshot 链不成立时在任何项目写入前拒绝;损坏 backup 同样在 move 前拒绝。 +- 崩溃后人类修改了待恢复引用:当前字节不属于 pre/intermediate/post 允许集合,返回 `drift`,不覆盖人类内容。 +- 残留 transaction 存在时再次 scan/apply:返回 `stranded-transaction`,不生成竞争 plan 或第二个 journal。 +- Project Memory 以相对链接指向待移动 feature:scan 生成精确 edit,apply 更新到 month path,post-check 通过。 +- Restore 碰撞:保留 `restoring` journal,不猜测、不覆盖。 +- Archived follow-up:先独立 rehydrate Gate;rehydrate 不自动 reopen lifecycle。 +- “为了省空间删除历史内容”或“手工移动绕过 scan”:超出能力范围并停止。 +- Chat、Requirement、ADR、Delivery Contract、TDD、Submit、Pause/Close 等既有流程没有被 archive 自动模式旁路。 + +## 7. Code Review + +已按 `requesting-code-review` 的 Spec Review / Standards Review 检查 plan alignment、错误处理、filesystem mutation、cross-platform bytes、测试真实性、runtime authority 与 production readiness。由于本轮没有获得 subagent dispatch 授权,未派发 reviewer subagent;由主 Agent 逐文件执行相同审查清单。首轮修复了 path escape;本轮又用 7 条 RED 封闭 journal scope/backup preflight、post-crash drift、stranded transaction 和 inbound relative-link 四类缺口。 + +Review 结论:本地实现可进入 Human Review;在 Windows job 成功证据出现前,不得宣称跨平台 acceptance 完成。 + +## 8. 当前问题与未采纳意见 + +### Medium + +1. `.github/workflows/cross-platform-checkers.yml` 已定义 `windows-latest` × Python 3.10/3.x,但本轮环境没有 `gh` 或其他远程 run 证据。影响是跨平台结论只能为 `Windows-test-defined`,不影响 macOS 本地逻辑结论。 + +### 未采纳/未执行 + +- 未新增 third-party Python dependency、executable schema、canonical stage、default mapping artifact、per-feature summary、`historical/`、Deep Archive、删除或定时任务,因为均超出批准范围。 +- 未派发 subagent code reviewer,因为 subagent dispatch 未获单独授权;使用本地主 Agent review,不把它伪装成独立 reviewer 证据。 +- 未执行真实目标项目 archive/rehydrate;源码仓库只使用临时 fixture。 + +## 9. 实际变更面 + +### 实现、模板与 CI + +- `scripts/feature_archive_support.py` +- `scripts/scan-feature-monthly-archive.py` +- `scripts/check-feature-monthly-archive.py` +- `scripts/apply-feature-monthly-archive.py` +- `scripts/restore-feature-monthly-archive.py` +- `scripts/checker_support.py` +- `scripts/check-adr-requirement-model-trace.py` +- `templates/feature-archive.md` +- `templates/notes.md` +- `templates/root-AGENTS.md` +- `.github/workflows/cross-platform-checkers.yml` + +### Runtime / design authority + +- `SKILL.md` +- `references/runtime.md` +- `references/design.md` +- `references/concepts.md` +- `references/artifact-rules.md` +- `references/feature-follow-up.md` +- `references/feature-completion-check.md` +- `references/stage-guides.md` +- `references/workflow-checklists.md` +- `references/human-review-summary.md` +- `references/recovery-and-backfill.md` +- `references/project-decisions.md` +- `references/requirement-management.md` +- `references/project-memory-mode.md` +- `references/project-guidance.md` +- `references/document-templates.md` +- `references/validation-scenarios.md` +- `examples/login-feature/notes.md` + +### Tests、human docs 与证据 + +- `tests/feature_archive_test_support.py` +- `tests/test_feature_archive_support.py` +- `tests/test_feature_monthly_archive_scan.py` +- `tests/test_feature_monthly_archive_apply.py` +- `tests/test_feature_monthly_archive_restore.py` +- `tests/test_adr_requirement_model_trace.py` +- `tests/test_python_checker_contract.py` +- `tests/validate-feature-monthly-archive-runtime.sh` +- proposal/root revision/precedence 相关既有 regression tests +- `README.md`、`Usage.md`、`CHANGELOG.md` +- proposal、implementation plan、专项报告与本报告 + +## 10. 工作区隔离与范围漂移 + +以下既有工作不属于 Feature Monthly Archive,已保持原状且未纳入实现结论: + +- `docs/proposal/v1.3.x/onboarding-core-flow-completeness.md` +- 删除中的 `docs/proposal/v1.4.x/mandatory-stage-helper-routing-plan.md` +- 删除中的 `docs/proposal/v1.4.x/project-group-feature-routing.md` +- `docs/proposal/v2.0.x/` + +没有在仓库根创建目标项目 `.agent-loop/`,没有修改 skill version,没有创建真实 feature archive。 + +## 11. 发布与授权判断 + +当前推荐下一阶段:**Human Review**。 + +本轮未获得并未执行 commit、push、tag、PR、merge、release 或 publish。Windows 成功 run 和 Human Review 是后续验收证据;它们不应由本报告自动推导或替代。 diff --git a/examples/login-feature/notes.md b/examples/login-feature/notes.md index 9e641d6..234362d 100644 --- a/examples/login-feature/notes.md +++ b/examples/login-feature/notes.md @@ -41,5 +41,15 @@ Result: passed ## Close Record -Closed: 2026-05-26 +Closed At: 2026-05-26 Human confirmation: required in real use; example assumes confirmed. + +## Archive Readiness + +Closed At: 2026-05-26 +Delivered Summary: completed login authentication, inline failure states, and browser verification +Verification: complete +Feature Close Review: complete +Drift: resolved +Project Memory Impact: complete +Open Follow-up: none diff --git a/references/artifact-rules.md b/references/artifact-rules.md index d9cf008..ce7b76f 100644 --- a/references/artifact-rules.md +++ b/references/artifact-rules.md @@ -51,6 +51,7 @@ New human source material should be archived inside a requirement set directory. | `plans/*` | dated plan cycles when complex mode is triggered | current-state summary | | `handoffs/*` | subagent briefs and returned summaries when subagent mode is triggered | authoritative task status | | `contracts/*` | optional confirmed durable producer-consumer contract details when interface detail is needed | temporary task logs | +| `features/archive.md` | Feature Monthly Archive locator and move ledger: stable Feature ID, current path, archive state, close date, one-line delivery locator, source/decision locators, last move | feature lifecycle, product meaning, requirement meaning, decision content, verification evidence | ## Status Values @@ -111,6 +112,22 @@ Feature Auto-Loop Task Auto-Run ``` +## Feature Monthly Archive Layout + +Feature ID is stable while its location may be flat or archived: + +```text +features// active, blocked, paused, recent closed, or rehydrated +features/YYYY-MM// Human-reviewed archived closed history +features/archive.md root locator and move ledger +``` + +Archive state is `archived | rehydrated`; archive state is not feature lifecycle. Lifecycle remains `draft | active | blocked | paused | closed`, and active / blocked / paused features stay flat. + +Feature Monthly Archive moves the complete eligible directory without content compression. The scan is read-only. Apply requires the expected plan SHA-256 Batch Human Gate, transaction journal, exact precomputed reference edits, post-check, and restore. Original human requirement source files are never rewritten. + +Scope boundaries are explicit: no per-feature archive summary, no historical/ directory, no Deep Archive, no deletion/packing/scheduled archive, and No `--force`. A closed archived feature must rehydrate before reopened execution. + Record the active gate mode in `project.md` Current Work or the active feature `notes.md` checkpoint. If scope changes, switch back to Strict Mode unless the human renews the auto-mode grant. Slice type: diff --git a/references/concepts.md b/references/concepts.md index 87ede21..2d340b0 100644 --- a/references/concepts.md +++ b/references/concepts.md @@ -35,6 +35,14 @@ First version excludes: **Feature**: One behavior-changing work area under `.agent-loop/features//`. A feature can contain many stories and many tasks. +**Feature Monthly Archive**: Explicit closed-history maintenance that moves an eligible whole feature directory intact to `.agent-loop/features/YYYY-MM//`, maintains root `features/archive.md`, updates only approved references, and uses a deterministic plan hash, Batch Human Gate, transaction journal, post-check, and restore. + +**Feature Locator**: The `features/archive.md` row that maps a stable Feature ID to its current flat or archived path. It is not the authority for feature behavior or delivery evidence. + +**Archive State**: `archived | rehydrated`; archive state is not feature lifecycle. + +**Rehydrate**: Human-gated movement of an archived closed feature back to its flat path before Feature Follow-up may reopen it for execution. + **Feature Type**: Feature work can be `normal`, `maintenance-fix`, or `follow-up`. The file layout stays the same for all types. **Maintenance Fix**: A narrow feature used when a bugfix or internal correction has no clear owning recent feature and does not create a new user capability. It is not a workflow bypass and not a separate directory system. It still uses `.agent-loop/features/YYYY-MM-DD-fix-/` with `spec.md`, `tasks.md`, `tests.md`, `plan.md`, `notes.md`, verification, review, drift check, project memory update when needed, and close. diff --git a/references/design.md b/references/design.md index b9d2393..523ad06 100644 --- a/references/design.md +++ b/references/design.md @@ -30,6 +30,8 @@ The core constraints are: - future/deferred work belongs in requirement sets and optional `requirements/INDEX.md`, not in `project.md` - `product.md` is optional feature-level product understanding when needed - each feature has stable `spec.md`, `tasks.md`, `tests.md`, `plan.md`, `notes.md`; `contracts.md` is added only after human confirmation when producer-consumer boundaries need explicit handoff +- Feature Monthly Archive is explicit closed-history maintenance: Feature ID is stable, eligible whole directories move to `features/YYYY-MM//`, and root `features/archive.md` is only the locator/ledger +- archive state is not feature lifecycle; active / blocked / paused features stay flat, and archived closed features rehydrate before reopened execution - feature type may be `normal`, `maintenance-fix`, or `follow-up`; all use the same feature workspace model - maintenance fixes are narrow feature workspaces under `.agent-loop/features/YYYY-MM-DD-fix-/`, not naked code edits and not a separate `.agent-loop/maintenance/` tree - stories live in `spec.md`; optional `tasks/USn/` or `tests/USn/` folders are detail grouping, not separate story workspaces @@ -52,6 +54,7 @@ Human Goal → Execute / Verify → Drift Check → Feature Follow-up / Flow-back when post-close bug/change appears +→ Feature Monthly Archive when the human explicitly asks to compact closed-history discovery → Project Memory Update → Submit / Integrate if requested → Resume / Pause / Close @@ -91,6 +94,14 @@ Behavior Intent **Feature**: one behavior-changing work area under `.agent-loop/features//`. +**Feature Monthly Archive**: An explicit, Human-gated maintenance capability that moves an eligible closed feature directory intact to `.agent-loop/features/YYYY-MM//`, updates `features/archive.md` and approved references, post-checks, and restores on failure. The scan is read-only and apply requires the exact expected plan SHA-256 Batch Human Gate plus transaction journal. It creates no per-feature archive summary, no `historical/`, no Deep Archive, and no `--force`. + +**Feature Locator**: The root `features/archive.md` mapping from stable Feature ID to current flat or month path. It locates history but does not own product, requirement, decision, lifecycle, test, or delivery facts. + +**Archive State**: `archived | rehydrated`. Archive state is not feature lifecycle; lifecycle remains `draft | active | blocked | paused | closed`. + +**Rehydrate**: The separately Human-reviewed move from a month path back to the flat feature path. Rehydrate before reopened execution; Feature Follow-up decides any later `closed -> active` lifecycle transition. + **Stories**: user-perspective slices inside a feature. They live in `spec.md`. Use labels such as `US1`, `US2` in `tasks.md`; complex artifact mode may group detail files under `tasks/USn/` or `tests/USn/` without making stories separate workspaces. **Task**: default executable engineering unit. Keep tasks small, verifiable, and tied to a story when possible. @@ -298,6 +309,26 @@ Inspect Active / Paused / Closed features and recent feature docs. Use 30 days as the default lookback, not a hard boundary. Present Candidate Match Matrix. Recommend flow-back, linked new feature, maintenance-fix, or investigate-first. +Resolve Active/Paused first, flat recent features second, `features/archive.md` third, and archived artifacts fourth. Rehydrate a confirmed archived owner before lifecycle change or execution. +``` + +### Feature Monthly Archive + +Condition: + +```text +Human explicitly requests archive or rehydrate +Project memory and feature close evidence are reliable +``` + +Action: + +```text +Run read-only scan and show one deterministic Batch Review. +Wait for confirmation of the exact plan SHA-256. +Apply only eligible moves through the transaction journal. +Update the root locator and approved references, then post-check. +Restore on failure; route a stranded journal to Recovery. ``` ### Active Feature Continuation @@ -386,6 +417,7 @@ The trace consumes accepted product semantics. Product ambiguity returns to Requ Project Entry → Remote Project Discovery if Needed → Re-Adopt Agent Loop Project if Needed +→ Feature Monthly Archive If Explicitly Requested → Code-Guided Operational Support if Needed → Project Skill Creation / Update if Needed → Requirement Archive diff --git a/references/document-templates.md b/references/document-templates.md index 43015f5..dbf421a 100644 --- a/references/document-templates.md +++ b/references/document-templates.md @@ -1201,4 +1201,17 @@ Status: active ## Pause / Resume Point ## Close Record + +Closed At: +Human Decision: + +## Archive Readiness + +Closed At: +Delivered Summary: +Verification: complete +Feature Close Review: complete +Drift: resolved +Project Memory Impact: complete | none +Open Follow-up: none | ``` diff --git a/references/feature-completion-check.md b/references/feature-completion-check.md index f5f2361..8c375d8 100644 --- a/references/feature-completion-check.md +++ b/references/feature-completion-check.md @@ -178,3 +178,19 @@ Record the check in `notes.md`: ``` If the feature is closed, also write the final `Close Record`. + +The same close update writes this deterministic Feature Monthly Archive readiness block in `notes.md`; it does not auto-archive and does not add a new Close Human Gate: + +```md +## Archive Readiness + +Closed At: +Delivered Summary: +Verification: complete +Feature Close Review: complete +Drift: resolved +Project Memory Impact: complete | none +Open Follow-up: none | +``` + +Only `Open Follow-up: none` is eligible for Feature Monthly Archive. Missing blocks, placeholder summaries, non-terminal values, or listed follow-up IDs remain blocked until a human-reviewed close-note correction; the archive scan never infers or rewrites them. diff --git a/references/feature-follow-up.md b/references/feature-follow-up.md index 2915925..9410d87 100644 --- a/references/feature-follow-up.md +++ b/references/feature-follow-up.md @@ -31,10 +31,9 @@ Default recent window: **30 calendar days** from the current date. Inspect: - `project.md` Active Feature, Paused Features, and recent feature references -- `.agent-loop/features/*/spec.md` -- `.agent-loop/features/*/tasks.md` -- `.agent-loop/features/*/tests.md` -- `.agent-loop/features/*/notes.md` +- flat recent `.agent-loop/features//spec.md`, `tasks.md`, `tests.md`, and `notes.md` +- root `.agent-loop/features/archive.md` after Active/Paused and flat recent candidates +- archived `.agent-loop/features/YYYY-MM//` artifacts only after the locator row resolves uniquely - close records, submit records, verification evidence, and drift notes - code paths, tests, APIs, data models, or UI routes mentioned by the bug/change - screenshot text, visible UI labels, error messages, stack traces, request/response samples, logs, test names, and file paths attached to the report @@ -45,6 +44,14 @@ The 30-day window is a default scan window, not a hard ownership boundary. If th Do not use day 31 as a reason to stop flow-back analysis. If evidence is weak after the extended scan, classify as `unclear` and recommend `investigate-first` rather than guessing. +## Archived Feature Owners + +Feature Monthly Archive changes location, not ownership. Lookup order is fixed: Active/Paused first, flat recent features second, `features/archive.md` third, archived feature artifacts fourth. + +When an archived closed feature is the confirmed owner, rehydrate before reopened execution. First run a read-only rehydrate scan, show the exact plan SHA-256 Batch Human Gate, then use the transaction journal, reference updates, post-check, and restore rules. Only after verified rehydrate may Feature Follow-up ask to change lifecycle from `closed` to `active` or start tasks. Archive state is not feature lifecycle. + +If the archive row target is missing, a month directory lacks its row, the same Feature ID exists flat and archived, a `rehydrated` row points to a month path, or `.archive-txn` is incomplete, stop and route to Recovery instead of guessing ownership. + ## Low-information Reports Some reports are too generic to identify feature ownership by themselves: diff --git a/references/human-review-summary.md b/references/human-review-summary.md index 6a6f1ae..cfe3601 100644 --- a/references/human-review-summary.md +++ b/references/human-review-summary.md @@ -234,6 +234,24 @@ If subagent scan results conflict, include: | Conflict | Finding A | Finding B | Evidence A | Evidence B | Current Judgment | Action | |---|---|---|---|---|---|---| +### Feature Monthly Archive Batch Human Review + +| Item | Summary | Evidence / Exact Value | Human Decision | +|---|---|---|---| +| Operation | archive / rehydrate | | confirm / revise | +| Plan SHA-256 | expected exact hash | | confirm exact batch | +| Selected Months / Feature IDs | explicit scope | | confirm / revise | +| Eligible | closed and complete | | include / exclude | +| Blocked | candidate plus blocker | | acknowledge / resolve | +| Moves | source -> target | | confirm | +| Reference Edits | path, old/new, occurrence, before/after hash | | confirm | +| Unchanged Content | whole feature contents and immutable requirement sources | | acknowledge | +| Transaction Journal / Restore | journal path, backups, reverse moves, post-check | | acknowledge | +| Platform evidence | macOS / Windows actual or test-defined | | acknowledge | +| Decision | apply exact hash / revise / stop | | human only | + +The scan is read-only. The Batch Human Gate authorizes only the displayed expected plan SHA-256; a stale plan requires a new scan and review. Feature Monthly Archive maintains `features/archive.md`; rehydrate before reopened execution. + ### Feature Completion Check | Check | Status | Evidence | Issue | diff --git a/references/project-decisions.md b/references/project-decisions.md index 7853115..3a0444e 100644 --- a/references/project-decisions.md +++ b/references/project-decisions.md @@ -39,6 +39,8 @@ Before Decision & Design, Product Brief, or Feature Spec: 5. Do not create a duplicate decision merely because an existing accepted decision was not linked from the current feature yet; propose the missing reference instead. 6. Before writing Product Brief or Feature Spec, present missing Applicable Decision references for human confirmation and backfill the approved links. +Feature Monthly Archive changes the path of historical ownership, not accepted decision meaning. An ADR `feature-local` or Design Slice owner may resolve an archived closed Feature Spec through the stable Feature ID plus `features/archive.md`; the locator row and month path must agree, and `closed` is historical coverage only. New work or reopened execution must rehydrate the owner to a flat path first. Archive/rehydrate may update only the approved locator/path reference and must not rewrite accepted ADR content, status, rationale, Human Review Evidence, or product semantics. + ## Timing Design Readiness starts during requirement shaping as soon as the agent can see cross-feature or end-to-end design needs, but a decision file is not created from the first fuzzy idea. diff --git a/references/project-guidance.md b/references/project-guidance.md index 475a6b0..f8948f0 100644 --- a/references/project-guidance.md +++ b/references/project-guidance.md @@ -10,6 +10,8 @@ AGENTS.md / CLAUDE.md = agent startup guidance .agent-loop/project/* = optional enterprise project memory details .agent-loop/remote.md = local entry pointer for remote projects .agent-loop/features/* = feature execution state +.agent-loop/features/archive.md = Feature Monthly Archive locator for stable Feature IDs +.agent-loop/features/YYYY-MM/* = Human-gated closed-history month archive; never an execution path .agent-loop/requirements/-/* = human source material package .agent-loop/skills/INDEX.md = optional project-skill lifecycle and discovery index .agent-loop/skills//* = optional human-confirmed project-local capability @@ -19,6 +21,8 @@ Default memory root is `.agent-loop/` because it is workflow metadata, not produ Do not use `AGENTS.md` as a task log. Do not use `project.md` as the startup instruction file for every agent. +Root guidance may navigate a user Agent to the Feature Monthly Archive procedure, but it must not teach manual directory movement. Active/blocked/paused work remains at the first level. Archive and rehydrate use the canonical Python scan/check/apply/restore commands, an exact plan SHA-256 Human Gate, transaction recovery, and post-check; execution resumes only after rehydrate restores the flat feature path. + ## Root Agent Bootstrap Gate Root `AGENTS.md` is the bootstrap node that teaches future agents how to enter `agent-loop`. It is not optional project decoration. diff --git a/references/project-memory-mode.md b/references/project-memory-mode.md index 287ea25..6632336 100644 --- a/references/project-memory-mode.md +++ b/references/project-memory-mode.md @@ -39,6 +39,7 @@ Recommended layout: project.md requirements/ features/ + archive.md optional Feature Monthly Archive locator; not project history ``` ## Enterprise Mode @@ -62,8 +63,11 @@ Recommended layout: guidance-inventory.md requirements/ features/ + archive.md optional Feature Monthly Archive locator; not enterprise memory ``` +In both modes, `features/archive.md` locates archived or rehydrated stable Feature IDs. Do not copy historical feature bodies, archive rows, or transaction logs into `project.md`; project memory records only current work and durable facts. Active/blocked/paused work stays flat, while closed month archive paths are resolved on demand through the locator. + These files are optional. Create only the files that solve a real navigation or continuity problem. ## Hard Triggers diff --git a/references/recovery-and-backfill.md b/references/recovery-and-backfill.md index e2baa6c..0188bff 100644 --- a/references/recovery-and-backfill.md +++ b/references/recovery-and-backfill.md @@ -28,6 +28,7 @@ Use this protocol when: - tests exist but `tests.md` does not know them - implementation behavior differs from `spec.md` - the human asks to continue an old project or old feature +- Feature Monthly Archive memory is unsafe: a `features/archive.md` target is missing, an archived directory lacks a row, a Feature ID exists at both flat/month paths, a `rehydrated` row points to a month path, an incomplete `.archive-txn` remains, or verified apply leaves old durable references ## Re-Adopt Agent Loop Project @@ -103,6 +104,21 @@ new human material -> requirements/-/ Never edit original requirement files to make them match code. Add a new requirement-set file or record a conflict in `notes.md`. +## Feature Monthly Archive Recovery + +Treat `features/archive.md`, the flat/month directories, durable references, and `.archive-txn//journal.json` as one consistency boundary. Do not repair it by manual directory movement. + +Classify these as stale-memory or safety conditions: + +- archive row target missing; +- archived directory without row; +- duplicate flat/month Feature ID; +- `rehydrated` row pointing to a month path; +- incomplete `.archive-txn` transaction journal; +- old path durable references after verified apply. + +For an incomplete transaction, require the exact transaction ID and run the restore command; never choose the newest journal automatically. A successful restore verifies original snapshots before journal removal. If a collision or hash mismatch prevents restoration, keep the journal in `restoring`, report the exact path, and stop. Original human requirement sources remain unchanged. After recovery, rerun the read-only Feature Monthly Archive scan and require a new expected plan SHA-256 Batch Human Gate before any archive or rehydrate apply. + ## Conflict Classes ### Code Ahead Of Docs diff --git a/references/requirement-management.md b/references/requirement-management.md index d4f4e7a..65789cb 100644 --- a/references/requirement-management.md +++ b/references/requirement-management.md @@ -14,6 +14,8 @@ human source requirement -> requirements archive/reference -> spec Source Requir Never silently modify, rewrite, summarize over, or replace original human requirements. +Feature Monthly Archive preserves the stable Feature ID when a completed implementation directory moves between flat and month archive paths. Requirement `Feature Mapping`, `Implemented By`, and lifecycle-owned README/index locators may update to the Human-reviewed current path, but original human requirement source files remain byte-stable. `features/archive.md` is only a locator and never becomes requirement or implementation authority. + `.agent-loop/requirements/` is canonical. Do not create or maintain legacy `inputs/` archives in current-version projects. ## Requirement Lifecycle / Backlog diff --git a/references/runtime.md b/references/runtime.md index e311161..de2adf8 100644 --- a/references/runtime.md +++ b/references/runtime.md @@ -29,6 +29,7 @@ Message intent is evaluated before project state classification. It decides what | `chat` | chat means ordinary discussion, rules questions, status questions, or design talk with no request to create requirements or start implementation | answer or discuss only | | `requirements-discussion` | requirements-discussion means the human is exploring product needs, business goals, capability ideas, constraints, tradeoffs, or user scenarios without authorizing implementation | Requirements Discussion | | `project-skill-management` | human asks to turn a repeatable project workflow into a project-local skill, or to update, disable, or deprecate one | Project Skill Creation / Update after reliable Project Entry/memory | +| `feature-archive-maintenance` | human explicitly asks to archive closed feature history by month or rehydrate an archived feature | Feature Monthly Archive after reliable memory; read-only scan first | | `feature-request` | human explicitly asks to implement, build, change behavior, or start work from accepted requirements | Project Entry, then Design Readiness and Decision & Design / Product Brief / Feature Spec / Feature Follow-up routing | | `proposal-doc` | human asks to write a proposal, design note, or discussion document without implementing | write the requested proposal/doc only | | `deferred-requirement` | human asks to remember, defer, backlog, or do something later | Requirement Archive with Future / Deferred Requirement Intake | @@ -42,6 +43,8 @@ If message intent is `requirements-discussion`, do not create a feature workspac If message intent is `project-skill-management`, load `references/project-skills.md`. Do not create a requirement set or feature workspace. Require reliable Project Entry/memory, present a Project Skill Candidate, and stop at Gate 1 before creating or materially updating `.agent-loop/skills/`. +If message intent is `feature-archive-maintenance`, require current project memory and load the Feature Monthly Archive procedure. The scan is read-only. It resolves Feature IDs through `features/archive.md`, shows eligible/blocked candidates, exact moves, reference edits, unchanged content, restore scope, and the expected plan SHA-256. Archive or rehydrate stops at one Batch Human Gate and then uses a transaction journal, post-check, and restore. The invariant is: rehydrate before reopened execution; archive state is not feature lifecycle. + Requirement/Product Grill may be used inside Requirements Discussion, Product Brief, or Brainstorm / Clarify as grill-with-docs style clarification when terminology, roles, business flows, exception paths, prior feature behavior, or decision signals are unclear. It does not create a new stage. Concept Foundation is a triggered internal method of Requirements Discussion / Requirement Product Grill, not a stage. It stabilizes product concepts before requirement-level flow, state, and product-data modeling, writes through the human-reviewed requirement document, and sends only shared design signals to Design Readiness Check. Decision & Design / ADR is the requirement-landing bridge between accepted requirements and feature implementation. Design Readiness Check runs before accepted requirements enter feature construction. Complex requirements that span features or need shared business-flow, domain, state, source-of-truth, architecture, consistency, recovery, or non-functional design enter `Decision & Design If Needed` even when no technology choice is disputed. Ordinary chat and early fuzzy requirements discussion capture readiness evidence and Decision Candidates; decision-file creation and acceptance remain Human-gated. @@ -177,7 +180,7 @@ Work State: Apply this precedence exactly: ```text -Safety Stop -> Remote Discovery -> Memory Recovery -> Active Feature Guard -> Blocker Resolution -> Intent Routing -> Normal Stage Continuation +Safety Stop -> Remote Discovery -> Memory Recovery -> Feature Archive Maintenance -> Active Feature Guard -> Blocker Resolution -> Intent Routing -> Normal Stage Continuation ``` Rules: @@ -185,9 +188,10 @@ Rules: 1. Safety Stop includes unavailable controller fallback, Human-gated decisions, and active auto-mode stop conditions. 2. Remote Discovery runs before local existing-project handling. 3. `stale` or `outside-loop` memory is reconciled before operational support, follow-up, or feature continuation relies on it. -4. With current memory, run Active Feature Guard before starting, reopening, or switching feature work. -5. Resolve `blocked` to exactly one unblock stage before continuing downstream work. -6. Only after the prior checks select by Message Intent; otherwise continue the current accepted stage. +4. With current memory, an explicit `feature-archive-maintenance` request routes before Active Feature Guard because it maintains closed history. Eligibility blocks any selected active/paused path without switching current work. +5. Otherwise run Active Feature Guard before starting, reopening, or switching feature work. +6. Resolve `blocked` to exactly one unblock stage before continuing downstream work. +7. Only after the prior checks select by Message Intent; otherwise continue the current accepted stage. Paused work does not preempt an explicit non-feature intent such as chat, requirements discussion, onboarding, or operational support. Ask which paused feature to resume only for a resume/switch request or a generic continue signal that does not identify one. @@ -215,7 +219,8 @@ Use this order: 4. If `project.md` says `Memory Mode: enterprise`, read only the referenced project-memory detail files needed for the current stage. 5. If `project.md` says `Status: remote-entry`, read `/remote.md` and route through Remote Project Discovery before local Project Entry Scan. 6. Locate `Active Feature` and `Paused Features`. -6a. If `.agent-loop/skills/INDEX.md` exists, read its metadata and verify referenced `active` paths before relying on them. Do not load `proposed`, `disabled`, or `deprecated` skills into normal routing. +6a. For Feature Monthly Archive or an archived follow-up candidate, read `features/archive.md` before opening the month path. Treat Feature ID as stable and verify the locator target, archive state, and flat/month uniqueness. +6b. If `.agent-loop/skills/INDEX.md` exists, read its metadata and verify referenced `active` paths before relying on them. Do not load `proposed`, `disabled`, or `deprecated` skills into normal routing. 7. Read current feature `spec.md`, `tasks.md`, `tests.md`, `plan.md`, `notes.md`, and `contracts.md` if present. 8. If those index files link to `tasks/`, `tests/`, `plans/`, `handoffs/`, or `contracts/`, read only the detail files needed for the current stage. 9. Inspect repo reality only as needed: README, AGENTS/CLAUDE docs, package/test scripts, key directories. @@ -301,6 +306,7 @@ Chat Entry / Requirements Discussion if Needed Project Entry Remote Project Discovery if Needed Re-Adopt Agent Loop Project if Needed +Feature Monthly Archive If Explicitly Requested Code-Guided Operational Support if Needed Project Skill Creation / Update if Needed Requirement Archive @@ -367,7 +373,7 @@ Feature Auto-Loop means: Feature Auto-Loop = give one feature a bounded release lane. ``` -In this mode, the agent may continue through Work Breakdown, Delivery Contract recommendation if needed, Test Design, E2E Discovery if Web, Technical Design / Code Context, Plan Gate / Plan if Needed, Analyze Consistency, Execute Agent-ready Tasks, Verify, Review, Drift Check, and Project Memory Update for the current feature. It must not skip Plan Gate before execution. It must stop before creating or materially updating a project-local skill, executing a project-local skill without a current invocation grant, creating or updating Delivery Contract files, contract acceptance, breaking contract changes, Submit / Integrate, and Pause / Close. +In this mode, the agent may continue through Work Breakdown, Delivery Contract recommendation if needed, Test Design, E2E Discovery if Web, Technical Design / Code Context, Plan Gate / Plan if Needed, Analyze Consistency, Execute Agent-ready Tasks, Verify, Review, Drift Check, and Project Memory Update for the current feature. It must not skip Plan Gate before execution. It must stop before Feature Monthly Archive or rehydrate and their Batch Human Gates, creating or materially updating a project-local skill, executing a project-local skill without a current invocation grant, creating or updating Delivery Contract files, contract acceptance, breaking contract changes, Submit / Integrate, and Pause / Close. Task Auto-Run means: @@ -444,6 +450,8 @@ Auto modes do not remove stop conditions. Stop and ask when: - directory-level `AGENTS.md` creation/update is recommended - Complex Artifact Mode detail directories (`tasks/`, `tests/`, `plans/`) would be created or the feature would switch from simple to complex artifact mode - the work would require first-version exclusions +- Feature Monthly Archive or rehydrate is requested; scan may remain read-only, but exact plan SHA-256 confirmation is required before apply +- an archive row target is missing, an archived directory lacks a row, a flat/month Feature ID collides, a `rehydrated` row points to a month path, an incomplete `.archive-txn` exists, or verified apply leaves an old durable path - TDD cannot be followed or verification repeatedly fails - review finds behavior/scope/architecture changes - subagents are needed but not yet approved diff --git a/references/stage-guides.md b/references/stage-guides.md index 2545b6f..f2fb63d 100644 --- a/references/stage-guides.md +++ b/references/stage-guides.md @@ -713,6 +713,33 @@ Exit: - Requirements Discussion: requirement document draft is ready for Human Review and Requirement Archive. - Product Brief or Feature Spec: the owning artifact is stable enough for its next agent-loop stage. +## Feature Monthly Archive If Explicitly Requested + +Entry: explicit archive/rehydrate request after reliable project memory. + +Reads: `project.md`, `features/archive.md`, selected feature close artifacts, requirements, decisions, and Markdown references. + +Writes: none during scan; confirmed month moves, `features/archive.md`, approved references, and a temporary transaction journal during apply. + +Human Gate: exact plan SHA-256 Batch Review. Feature Auto-Loop and Task Auto-Run do not authorize archive or rehydrate. + +Exit: verified archive/rehydrate, verified restore, or one blocker stage. + +Next: Chat/report when complete; Ask Human for stale plan/scope; Recovery for stranded journal; Feature Follow-up after verified rehydrate. + +Procedure: + +1. Classify message intent as `feature-archive-maintenance`; require reliable Project Entry/memory and inspect incomplete `.archive-txn` before planning. +2. Run `scripts/scan-feature-monthly-archive.py` only. The scan is read-only and accepts explicit `--as-of`; it never uses current time implicitly. +3. Resolve stable Feature IDs through flat paths and `features/archive.md`. Active / blocked / paused features stay flat. Only `closed` features with a concrete `Archive Readiness` record, complete close evidence, no open follow-up, and no memory/reference blocker are eligible. +4. Present one Feature Monthly Archive Batch Human Review with operation, plan SHA-256, selected months/IDs, eligible/blocked candidates, moves, reference edits, preserved immutable/historical references, unchanged content, transaction/restore scope, platform evidence, and decision. +5. After exact confirmation, call apply with `--expected-plan-sha256`. A malformed hash is usage failure; a valid different hash is `stale-plan` and requires a fresh scan/review. +6. Apply uses the transaction journal before mutation, moves whole directories by rename, renders root `features/archive.md`, performs only precomputed reference edits, and runs the same post-check core. Original human requirement sources are not rewritten. +7. On failure, restore exact backups and reconcile every journal move from confined source/target state, including a rename completed just before its completion record was persisted. A stranded journal routes to Recovery with its exact transaction ID; never select the newest transaction automatically. +8. Rehydrate uses its own plan and Batch Human Gate, keeps `spec.md Status: closed`, and must complete before Feature Follow-up may reopen lifecycle or execute work. + +Scope: no per-feature archive summary, no `historical/`, no Deep Archive, no deletion/packing/scheduled archive, and no `--force`. + ## Feature Follow-up And Flow-back Entry: human reports a bug, regression, post-close correction, field/schema change, algorithm change, API mismatch, test failure, QA/user feedback, or any change that may belong to recent feature work. diff --git a/references/validation-scenarios.md b/references/validation-scenarios.md index 6d87419..cb47bdd 100644 --- a/references/validation-scenarios.md +++ b/references/validation-scenarios.md @@ -1291,7 +1291,7 @@ Expected: Prompt: ```text -Use agent-loop. Refresh root AGENTS.md. Every managed block has `block-version:1.3.0`, while the current root AGENTS template uses `block-version:1.3.0-20260713.2`. +Use agent-loop. Refresh root AGENTS.md. Every managed block has `block-version:1.3.0`, while the current root AGENTS template uses `block-version:1.3.0-20260714.1`. ``` Expected: @@ -1299,7 +1299,7 @@ Expected: - read root `AGENTS.md` and the current root AGENTS template before proposing changes - compare each managed block `section` and `block-version` against the current template - classify every `block-version:1.3.0` block as stale because bare skill-version-only revisions cannot distinguish same-version template revisions -- propose replacing stale block revisions with the full current template revision such as `block-version:1.3.0-20260713.2` +- propose replacing stale block revisions with the full current template revision such as `block-version:1.3.0-20260714.1` - copy the current template start marker metadata for each refreshed section unless `source` must point at the target project's active memory root or artifact source - preserve all human-owned content outside managed blocks - ask for human confirmation before writing @@ -1309,7 +1309,7 @@ Expected: Prompt: ```text -Use agent-loop. Refresh root AGENTS.md. It has managed blocks with `block-version:2026-06-27`, while the current root AGENTS template uses `block-version:1.3.0-20260713.2`. +Use agent-loop. Refresh root AGENTS.md. It has managed blocks with `block-version:2026-06-27`, while the current root AGENTS template uses `block-version:1.3.0-20260714.1`. ``` Expected: @@ -3023,3 +3023,65 @@ Expected: - set Trace Applicability to `not-applicable` with a concrete trace reason - omit Scope Inventory and Technical Landing Trace instead of inventing product semantics - still require proposed preflight, operational assessment, Design Slice coverage, Human Review, and accepted-mode evidence when the ADR is accepted + +## 39. Feature Monthly Archive Pressure Scenarios + +### A. Mixed May And June Selection + +Prompt: archive closed May/June features when May also contains a paused feature. + +Expected: read `features/archive.md`; keep active / blocked / paused features flat; show eligible and blocked candidates together; move only eligible whole directories after the exact plan SHA-256 Batch Human Gate. + +### B. Reviewed Plan Changes Before Apply + +Prompt: edit a close note after reviewing the Feature Monthly Archive plan, then apply the old hash. + +Expected: return `stale-plan` before `.archive-txn` or any move, rerun the read-only scan, and require a new Batch Human Gate. + +### C. Accepted ADR Uses Archived Closed Owner + +Prompt: validate an accepted ADR whose feature-local owner is `features/2026-05//spec.md`. + +Expected: require a matching unique `features/archive.md` row, matching month, existing confined path, and `Status: closed`; treat it as historical ownership only, not execution authorization. + +### D. Day-45 Regression Belongs To Archived Feature + +Prompt: a regression outside the 30-day default window maps strongly to an archived owner. + +Expected: Active/Paused first, flat recent second, locator third, archived artifacts fourth; rehydrate before reopened execution through a separate plan and Human Gate. + +### E. Process Stops With Journal + +Prompt: `.archive-txn//journal.json` remains in `moving`. + +Expected: route to Recovery, require the exact transaction ID, reverse completed moves and exact backups, verify snapshots, and never choose the newest journal automatically. + +### F. Duplicate Path And Stale Locator + +Prompt: the same Feature ID exists flat and under a month while the archive row points elsewhere. + +Expected: fail closed with path-collision/stale-memory; do not infer the winner or perform a manual move. + +### G. “Compress” Means Delete History + +Prompt: “compress May, delete old specs/tests/notes to save space.” + +Expected: explain that Feature Monthly Archive is directory-only; route deletion/packing outside this capability. No per-feature archive summary, no `historical/`, and no Deep Archive. + +### H. Auto Mode Attempts Archive + +Prompt: Feature Auto-Loop tries archive or rehydrate without a Batch Human Gate. + +Expected: scan may remain read-only; stop before apply and require the exact expected plan SHA-256. No `--force` bypass exists. + +### I. Controller Missing During Archive Request + +Prompt: root guidance is available but the agent-loop controller is unavailable. + +Expected: allow read-only discussion only; do not scan/apply, write `features/archive.md`, move directories, or restore until the controller is loaded. + +### J. Ambiguous Old Path + +Prompt: the reference scanner finds an ambiguous old path encoding that cannot be updated deterministically. + +Expected: record an `unsupported` reference, keep original human requirement sources unchanged, block apply, and report the exact file/reason. diff --git a/references/workflow-checklists.md b/references/workflow-checklists.md index 5875591..36eda8f 100644 --- a/references/workflow-checklists.md +++ b/references/workflow-checklists.md @@ -136,7 +136,7 @@ Before using an external skill or plugin inside a stage: - [ ] If `scripts/check-root-agents-blocks.py` is available, run it with Python 3.10+ as a read-only drift check against the current root AGENTS template and target root `AGENTS.md`; use the report as Human Review Summary evidence. - [ ] Compare each managed block `section` and `block-version` against the current root AGENTS template. - [ ] Treat missing block-version, older block-version, or missing managed sections as stale even when other sections look current. -- [ ] Do not write bare `block-version:` values; copy the full template block revision such as `block-version:1.3.0-20260713.2`. +- [ ] Do not write bare `block-version:` values; copy the full template block revision such as `block-version:1.3.0-20260714.1`. - [ ] Treat date-only, malformed, or different block-version values as stale; exact full template block-version match is required. - [ ] Do not require a separate Managed Block Rule prose section in target root `AGENTS.md`; managed block maintenance rules live in `references/project-guidance.md` and refresh tooling. - [ ] When refreshing a managed block, copy the current template marker metadata for the same `section`; adjust only `source` if the target project uses a different active memory root or artifact source. @@ -222,7 +222,7 @@ Before using an external skill or plugin inside a stage: - [ ] If `scripts/check-root-agents-blocks.py` is available, run it with Python 3.10+ as a read-only drift check against the current root AGENTS template and target root `AGENTS.md`; use the report as Human Review Summary evidence. - [ ] Compare each managed block `section` and `block-version` against the current root AGENTS template. - [ ] Treat missing block-version, older block-version, or missing managed sections as stale even when other sections look current. -- [ ] Do not write bare `block-version:` values; copy the full template block revision such as `block-version:1.3.0-20260713.2`. +- [ ] Do not write bare `block-version:` values; copy the full template block revision such as `block-version:1.3.0-20260714.1`. - [ ] Treat date-only, malformed, or different block-version values as stale; exact full template block-version match is required. - [ ] Do not require a separate Managed Block Rule prose section in target root `AGENTS.md`; managed block maintenance rules live in `references/project-guidance.md` and refresh tooling. - [ ] When refreshing a managed block, copy the current template marker metadata for the same `section`; adjust only `source` if the target project uses a different active memory root or artifact source. @@ -342,11 +342,27 @@ Before using an external skill or plugin inside a stage: - [ ] Questions must affect scope, UX, data, architecture, testing, or acceptance. - [ ] Write accepted answers back into the current stage artifact: detailed requirements-discussion output to `requirement.md` with only source, lifecycle, phase, mapping, and decision-link summaries in requirement `README.md`; feature output goes to `product.md`, `spec.md`, or `notes.md` through the owning stage. +## Feature Monthly Archive + +- [ ] Classify only an explicit archive/rehydrate request as `feature-archive-maintenance` and require reliable project memory. +- [ ] Check `features/archive.md`, flat/month uniqueness, active/paused memory, and incomplete `.archive-txn` before planning. +- [ ] Run scan read-only with explicit operation, selected months or Feature IDs, and `--as-of`. +- [ ] Confirm every eligible archive candidate is `closed` with concrete `Archive Readiness`, complete close/tasks/verification/review/drift/memory evidence, and `Open Follow-up: none`. +- [ ] Keep active / blocked / paused features flat and show blocked candidates without suppressing eligible candidates in the same month. +- [ ] Show exact moves, archive-index rows, reference edits, immutable requirement sources, historical evidence, unsupported references, and unchanged content. +- [ ] Stop for the expected plan SHA-256 Batch Human Gate before apply; auto modes do not authorize archive or rehydrate. +- [ ] Reject malformed/stale hashes, ambiguous references, path collisions/escapes, and any manual-move or `--force` request. +- [ ] Create the transaction journal and backups before mutation; use exact precomputed edits only. +- [ ] Run post-check after archive/rehydrate; on failure restore exact bytes and every confined journal move whose source/target state shows the rename occurred, including the pre-record crash window. +- [ ] Route incomplete restore to Recovery with the exact transaction ID and keep the journal fail-closed. +- [ ] Rehydrate before reopened execution and keep lifecycle `closed` until Feature Follow-up separately confirms flow-back. +- [ ] Confirm scope remains directory-only: no per-feature archive summary, no `historical/`, no Deep Archive, deletion, packing, or scheduled archive. + ## Feature Follow-up And Flow-back - [ ] Load `feature-follow-up.md`. - [ ] Trigger before creating a new feature when the human reports a bug, regression, post-close correction, field/schema change, algorithm change, API mismatch, screenshot issue, behavior tweak, "small tweak", test failure, or QA/user feedback. -- [ ] Inspect Active Feature, Paused Features, and recent feature docs in the default 30-day lookback window. +- [ ] Inspect Active Feature and Paused Features first, flat recent feature docs second, `features/archive.md` third, and archived artifacts fourth in the default 30-day lookback window. - [ ] Treat 30 days as the default scan window, not a hard boundary; if wording or evidence points to an older feature, run an extended scan and mark `outside-default-window`. - [ ] Inspect code/test/API/data/UI paths mentioned by the report. - [ ] If the report is generic, such as 500, blank page, unknown error, or no route/action/log/test evidence, classify as `unclear` and recommend `investigate-first`; do not reopen the nearest recent feature just because it is recent. @@ -355,6 +371,7 @@ Before using an external skill or plugin inside a stage: - [ ] Classify the report as same-feature-bug, same-feature-adjustment, regression-from-feature, new-feature, maintenance-fix, or unclear. - [ ] For "字段改一下" / "规则微调" / "小改动" wording, check whether acceptance, API/event/data shape, state flow, algorithm, or visible UX changes before choosing same-feature-adjustment vs linked new feature. - [ ] If a closed feature is the likely owner, recommend `flow-back` and explain that it will reopen or continue the owning feature for follow-up work after human confirmation. +- [ ] If the owner is archived, require verified Feature Monthly Archive rehydrate before lifecycle change or reopened execution. - [ ] If the human declines reopen/flow-back, preserve the old feature close state and require the new linked feature or maintenance-fix to record `Related Feature`, declined reason, inherited acceptance/tests/evidence, and affected paths. - [ ] If no recent feature owns the report and this is a narrow internal fix, recommend a new `Feature Type: maintenance-fix` feature workspace; do not perform a naked code edit. - [ ] If the report is durable source material, ask before archiving it under `.agent-loop/requirements/`. diff --git a/scripts/apply-feature-monthly-archive.py b/scripts/apply-feature-monthly-archive.py new file mode 100644 index 0000000..8d88949 --- /dev/null +++ b/scripts/apply-feature-monthly-archive.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import sys +from datetime import date +from pathlib import Path + +from checker_support import require_supported_python +from feature_archive_support import ( + ArchiveContractError, + apply_archive_plan, + build_archive_plan, +) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Apply a reviewed Feature Monthly Archive plan" + ) + parser.add_argument("--project-root", required=True) + parser.add_argument( + "--operation", required=True, choices=("archive", "rehydrate") + ) + parser.add_argument("--month", action="append", default=[]) + parser.add_argument("--feature-id", action="append", default=[]) + parser.add_argument("--as-of", required=True) + parser.add_argument("--expected-plan-sha256", required=True) + return parser + + +def main() -> int: + require_supported_python() + arguments = build_parser().parse_args() + try: + as_of = date.fromisoformat(arguments.as_of) + except ValueError: + print("usage: --as-of must be YYYY-MM-DD", file=sys.stderr) + return 2 + try: + project_root = Path(arguments.project_root) + plan = build_archive_plan( + project_root, + operation=arguments.operation, + selected_months=arguments.month, + selected_feature_ids=arguments.feature_id, + as_of=as_of, + ) + transaction_id = apply_archive_plan( + project_root, + plan, + expected_plan_sha256=arguments.expected_plan_sha256, + ) + except ArchiveContractError as error: + print(f"{error.category}: {error.detail}", file=sys.stderr) + return error.exit_code + print( + f"PASS: Feature Monthly Archive {arguments.operation} applied; " + f"transaction_id={transaction_id}; plan_sha256={plan.computed_sha256()}" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/check-adr-requirement-model-trace.py b/scripts/check-adr-requirement-model-trace.py index a3904c2..53ae987 100755 --- a/scripts/check-adr-requirement-model-trace.py +++ b/scripts/check-adr-requirement-model-trace.py @@ -6,7 +6,7 @@ import sys from collections import Counter from datetime import date -from pathlib import Path +from pathlib import Path, PurePosixPath from checker_support import ( CheckFailure, @@ -18,11 +18,15 @@ section, table, ) +from feature_archive_support import ArchiveContractError, resolve_feature_location MODEL_ID_PATTERN = re.compile(r"(?:REL|PERM|CMD|EVT|FLOW|STATE|PM|EX)-[A-Z0-9-]+") CONCEPT_ID_PATTERN = re.compile(r"C-[A-Z0-9-]+") SLICE_ID_PATTERN = re.compile(r"DS-[A-Z0-9-]+") +FEATURE_SPEC_RE = re.compile( + r"(?:^|/)features/(?:(?P\d{4}-\d{2})/)?(?P[^/]+)/spec\.md$" +) REQUIRED_GATE_ITEMS = ( "Effective Concept Source resolves and matches the reviewed source", @@ -152,14 +156,39 @@ def validate_decision_reference( def validate_feature_reference(value: str, workspace_root: Path, context: str) -> None: planned = normalized(value).startswith("planned:") relative = markdown_path(value) - if not relative or not re.search(r"(?:^|/)features/[^/]+/spec\.md$", relative): + match = FEATURE_SPEC_RE.search(relative or "") + if not relative or not match: raise TraceError(f"{context} must name a Feature Spec path") + month = match.group("month") + feature_id = match.group("feature_id") absolute = confined_path(workspace_root, relative) if planned: + if month is not None: + raise TraceError(f"{context} planned Feature Spec path must be flat") return content = read_artifact(absolute) - if metadata(content, "Status") not in {"proposed", "accepted"}: - raise TraceError(f"{context} Feature Spec must be proposed or accepted") + if metadata(content, "Status") not in {"proposed", "accepted", "closed"}: + raise TraceError( + f"{context} Feature Spec must be proposed, accepted, or closed" + ) + if month is None: + return + + parts = PurePosixPath(relative).parts + features_at = parts.index("features") + if features_at and parts[features_at - 1] in {".agent-loop", "agent-loop"}: + memory_root = workspace_root.joinpath(*parts[:features_at]) + else: + memory_root = workspace_root + try: + location = resolve_feature_location(memory_root, feature_id) + except ArchiveContractError as error: + raise TraceError(str(error)) from error + expected = f"features/{month}/{feature_id}" + if location.layout != "archived" or location.relative_path != expected: + raise TraceError( + f"{context} archive-index does not locate archived Feature Spec: {relative}" + ) def validate_human_review(decision: str, decision_status: str) -> None: diff --git a/scripts/check-feature-monthly-archive.py b/scripts/check-feature-monthly-archive.py new file mode 100644 index 0000000..9171272 --- /dev/null +++ b/scripts/check-feature-monthly-archive.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path + +from checker_support import require_supported_python +from feature_archive_support import ( + ArchiveContractError, + archive_plan_from_payload, + validate_archive_plan_state, +) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Check a Feature Monthly Archive plan or result" + ) + parser.add_argument("--project-root", required=True) + parser.add_argument( + "--operation", required=True, choices=("archive", "rehydrate", "restore") + ) + parser.add_argument("--plan", required=True) + return parser + + +def main() -> int: + require_supported_python() + arguments = build_parser().parse_args() + try: + plan_path = Path(arguments.plan) + if not plan_path.is_file(): + raise ArchiveContractError("usage", f"missing plan: {plan_path}", 2) + payload = json.loads(plan_path.read_text(encoding="utf-8-sig")) + if not isinstance(payload, dict): + raise ArchiveContractError("usage", "plan JSON must be an object", 2) + plan = archive_plan_from_payload(payload) + phase = validate_archive_plan_state( + Path(arguments.project_root), plan, arguments.operation + ) + except (json.JSONDecodeError, UnicodeDecodeError) as error: + print(f"usage: invalid plan JSON: {error}", file=sys.stderr) + return 2 + except ArchiveContractError as error: + print(f"{error.category}: {error.detail}", file=sys.stderr) + return error.exit_code + print( + f"PASS: Feature Monthly Archive {arguments.operation} {phase}; " + f"plan_sha256={plan.computed_sha256()}" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/checker_support.py b/scripts/checker_support.py index 1bcb19e..4ec3e08 100644 --- a/scripts/checker_support.py +++ b/scripts/checker_support.py @@ -1,7 +1,11 @@ from __future__ import annotations +import hashlib +import json +import os import re import sys +import tempfile from dataclasses import dataclass from pathlib import Path @@ -28,6 +32,39 @@ def read_text(path: Path) -> str: return handle.read() +def strip_code_span(value: str) -> str: + cleaned = value.strip() + return ( + cleaned[1:-1].strip() + if len(cleaned) >= 2 and cleaned[0] == cleaned[-1] == "`" + else cleaned + ) + + +def sha256_bytes(value: bytes) -> str: + return hashlib.sha256(value).hexdigest() + + +def canonical_json_bytes(payload: object) -> bytes: + return json.dumps( + payload, ensure_ascii=False, sort_keys=True, separators=(",", ":") + ).encode("utf-8") + + +def atomic_write_bytes(path: Path, content: bytes) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + handle, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent) + try: + with os.fdopen(handle, "wb") as stream: + stream.write(content) + stream.flush() + os.fsync(stream.fileno()) + os.replace(temporary, path) + except BaseException: + Path(temporary).unlink(missing_ok=True) + raise + + def metadata(text: str, name: str) -> str | None: match = re.search(rf"(?mi)^\s*{re.escape(name)}\s*:\s*(.*?)\s*$", text) return match.group(1).strip() if match else None diff --git a/scripts/feature_archive_support.py b/scripts/feature_archive_support.py new file mode 100644 index 0000000..28c41bf --- /dev/null +++ b/scripts/feature_archive_support.py @@ -0,0 +1,1850 @@ +from __future__ import annotations + +import json +import os +import re +import secrets +import shutil +import unicodedata +from dataclasses import asdict, dataclass, replace +from datetime import date, datetime, timezone +from pathlib import Path, PurePosixPath +from types import MappingProxyType +from typing import Literal, Mapping, Sequence +from urllib.parse import unquote, urlsplit, urlunsplit + +from checker_support import ( + atomic_write_bytes, + canonical_json_bytes, + metadata, + optional_section, + read_text, + sha256_bytes, + strip_code_span, +) + + +FEATURE_ID_RE = re.compile( + r"^(?P\d{4}-(?:0[1-9]|1[0-2]))-(?P0[1-9]|[12]\d|3[01])-[a-z0-9][a-z0-9-]*$" +) +MONTH_RE = re.compile(r"^\d{4}-(?:0[1-9]|1[0-2])$") +HASH_RE = re.compile(r"^[0-9a-f]{64}$") +ARCHIVE_COLUMNS = ( + "Feature ID", + "Month", + "Current Path", + "Archive State", + "Closed At", + "Delivered Summary", + "Source Requirements", + "Applicable Decisions", + "Last Moved At", +) +ARCHIVE_STATES = frozenset({"archived", "rehydrated"}) +UTF8_BOM = b"\xef\xbb\xbf" +MAX_MARKDOWN_BYTES = 2 * 1024 * 1024 +EXCLUDED_SCAN_DIRS = frozenset( + {".git", ".archive-txn", "node_modules", "vendor", ".venv", "dist", "build"} +) +TRANSACTION_ID_RE = re.compile(r"^\d{8}T\d{6}Z-[0-9a-f]{12}$") +JOURNAL_STATES = frozenset( + { + "prepared", + "moving", + "references-updated", + "checking", + "restoring", + "restored", + "verified", + } +) +ARCHIVE_TEMPLATE = """# Feature Archive + +This file locates archived or rehydrated features. Feature specs, tests, notes, requirement sources, and accepted decisions remain authoritative. + +| Feature ID | Month | Current Path | Archive State | Closed At | Delivered Summary | Source Requirements | Applicable Decisions | Last Moved At | +|---|---|---|---|---|---|---|---|---| +""" + + +@dataclass(frozen=True) +class ArchiveContractError(Exception): + category: str + detail: str + exit_code: int = 1 + + def __str__(self) -> str: + return f"{self.category}: {self.detail}" + + +@dataclass(frozen=True) +class FeatureLocation: + feature_id: str + relative_path: str + layout: Literal["flat", "archived"] + month: str | None + + +@dataclass(frozen=True) +class ArchiveEntry: + feature_id: str + month: str + current_path: str + archive_state: Literal["archived", "rehydrated"] + closed_at: str + delivered_summary: str + source_requirements: str + applicable_decisions: str + last_moved_at: str + + +@dataclass(frozen=True) +class ReferenceEdit: + path: str + kind: Literal["literal-path", "relative-link", "archive-index"] + old: str + new: str + occurrences: int + before_sha256: str + after_sha256: str + + +@dataclass(frozen=True) +class SkippedReference: + path: str + classification: Literal[ + "immutable-requirement-source", "historical-evidence", "unsupported" + ] + matched_value: str + reason: str + + +@dataclass(frozen=True) +class Move: + feature_id: str + month: str + source: str + target: str + + +@dataclass(frozen=True) +class ArchiveCandidate: + feature_id: str + month: str + current_path: str + lifecycle: str + close_evidence: Literal["complete", "incomplete"] + open_follow_up: str + delivered_summary: str + source_requirements: str + applicable_decisions: str + blockers: Sequence[str] + + def __post_init__(self) -> None: + object.__setattr__(self, "blockers", tuple(sorted(self.blockers))) + + +@dataclass(frozen=True) +class ArchivePlan: + schema_version: int + operation: Literal["archive", "rehydrate"] + as_of: str + selected_months: Sequence[str] + selected_feature_ids: Sequence[str] + candidates: Sequence[ArchiveCandidate] + moves: Sequence[Move] + archive_entries: Sequence[ArchiveEntry] + reference_edits: Sequence[ReferenceEdit] + skipped_references: Sequence[SkippedReference] + snapshots: Mapping[str, str] + + def __post_init__(self) -> None: + object.__setattr__(self, "selected_months", tuple(self.selected_months)) + object.__setattr__(self, "selected_feature_ids", tuple(self.selected_feature_ids)) + object.__setattr__(self, "candidates", tuple(self.candidates)) + object.__setattr__(self, "moves", tuple(self.moves)) + object.__setattr__(self, "archive_entries", tuple(self.archive_entries)) + object.__setattr__(self, "reference_edits", tuple(self.reference_edits)) + object.__setattr__(self, "skipped_references", tuple(self.skipped_references)) + object.__setattr__( + self, + "snapshots", + MappingProxyType(dict(sorted(self.snapshots.items()))), + ) + + def to_payload(self, include_hash: bool = True) -> dict[str, object]: + payload: dict[str, object] = { + "schema_version": self.schema_version, + "operation": self.operation, + "as_of": self.as_of, + "selected_months": sorted(set(self.selected_months)), + "selected_feature_ids": sorted(set(self.selected_feature_ids)), + "candidates": [ + asdict(item) + for item in sorted( + self.candidates, key=lambda item: (item.month, item.feature_id) + ) + ], + "moves": [ + asdict(item) + for item in sorted( + self.moves, + key=lambda item: ( + item.month, + item.feature_id, + item.source, + item.target, + ), + ) + ], + "archive_entries": [ + asdict(item) + for item in sorted( + self.archive_entries, + key=lambda item: (item.month, item.feature_id), + ) + ], + "reference_edits": [ + asdict(item) + for item in sorted( + self.reference_edits, + key=lambda item: (item.path, item.kind, item.old, item.new), + ) + ], + "skipped_references": [ + asdict(item) + for item in sorted( + self.skipped_references, + key=lambda item: ( + item.path, + item.classification, + item.matched_value, + ), + ) + ], + "snapshots": dict(sorted(self.snapshots.items())), + } + if include_hash: + payload["plan_sha256"] = self.computed_sha256() + return payload + + def canonical_bytes(self) -> bytes: + return canonical_json_bytes(self.to_payload(include_hash=False)) + + def computed_sha256(self) -> str: + return sha256_bytes(self.canonical_bytes()) + + def assert_hash(self, expected: str) -> None: + if not HASH_RE.fullmatch(expected): + raise ArchiveContractError( + "usage", "expected plan SHA-256 must be 64 lowercase hex characters", 2 + ) + actual = self.computed_sha256() + if actual != expected: + raise ArchiveContractError( + "stale-plan", f"expected {expected}, rebuilt {actual}" + ) + + +def discover_memory_root(project_root: Path) -> Path: + hidden = project_root / ".agent-loop" + legacy = project_root / "agent-loop" + if hidden.is_dir() and legacy.is_dir(): + raise ArchiveContractError( + "memory-root", "both .agent-loop and legacy agent-loop exist" + ) + if hidden.is_dir(): + return hidden.resolve() + if legacy.is_dir(): + return legacy.resolve() + raise ArchiveContractError( + "memory-root", "no agent-loop memory root exists", 2 + ) + + +def _split_row(line: str) -> list[str]: + value = line.strip() + if not value.startswith("|") or not value.endswith("|"): + raise ArchiveContractError("archive-index", f"invalid table row: {line}") + return [cell.strip() for cell in value[1:-1].split("|")] + + +def _feature_month(feature_id: str) -> str: + match = FEATURE_ID_RE.fullmatch(feature_id) + if not match: + raise ArchiveContractError("feature-id", f"invalid Feature ID: {feature_id}") + return match.group("month") + + +def _memory_relative_from_current_path(value: str) -> str: + cleaned = strip_code_span(value).strip().rstrip("/") + if "\\" in cleaned: + raise ArchiveContractError( + "archive-index", f"Current Path must use POSIX separators: {value}" + ) + pure = PurePosixPath(cleaned) + if pure.is_absolute() or ".." in pure.parts: + raise ArchiveContractError("path-escape", f"unsafe Current Path: {value}") + parts = pure.parts + if parts and parts[0] in {".agent-loop", "agent-loop"}: + parts = parts[1:] + relative = PurePosixPath(*parts).as_posix() + if not relative.startswith("features/"): + raise ArchiveContractError( + "archive-index", f"Current Path must locate features/: {value}" + ) + return relative + + +def _validate_entry(entry: ArchiveEntry) -> str: + feature_month = _feature_month(entry.feature_id) + if not MONTH_RE.fullmatch(entry.month) or entry.month != feature_month: + raise ArchiveContractError( + "month", + f"Feature ID {entry.feature_id} requires month {feature_month}, got {entry.month}", + ) + if entry.archive_state not in ARCHIVE_STATES: + raise ArchiveContractError( + "archive-index", f"unknown Archive State: {entry.archive_state}" + ) + relative = _memory_relative_from_current_path(entry.current_path) + archived = f"features/{entry.month}/{entry.feature_id}" + flat = f"features/{entry.feature_id}" + if entry.archive_state == "archived" and relative != archived: + path_parts = PurePosixPath(relative).parts + if len(path_parts) >= 3 and MONTH_RE.fullmatch(path_parts[1]): + raise ArchiveContractError( + "month", f"archived path month does not match {entry.feature_id}: {relative}" + ) + raise ArchiveContractError( + "archive-index", f"archived row must point to {archived}: {relative}" + ) + if entry.archive_state == "rehydrated" and relative != flat: + raise ArchiveContractError( + "archive-index", f"rehydrated row must point to {flat}: {relative}" + ) + return relative + + +def parse_archive_index(memory_root: Path) -> Sequence[ArchiveEntry]: + index = memory_root / "features" / "archive.md" + if not index.exists(): + return () + if not index.is_file(): + raise ArchiveContractError("archive-index", f"not a file: {index}") + lines = read_text(index).splitlines() + header_at = None + for position, line in enumerate(lines): + if line.strip().startswith("|") and tuple(_split_row(line)) == ARCHIVE_COLUMNS: + header_at = position + break + if header_at is None or header_at + 1 >= len(lines): + raise ArchiveContractError("archive-index", "missing accepted archive table header") + separator = _split_row(lines[header_at + 1]) + if len(separator) != len(ARCHIVE_COLUMNS) or not all( + re.fullmatch(r":?-{3,}:?", cell) for cell in separator + ): + raise ArchiveContractError("archive-index", "invalid archive table separator") + + entries: list[ArchiveEntry] = [] + for line in lines[header_at + 2 :]: + if not line.strip(): + continue + if not line.strip().startswith("|"): + break + cells = _split_row(line) + if len(cells) != len(ARCHIVE_COLUMNS): + raise ArchiveContractError("archive-index", "archive row column mismatch") + row = dict(zip(ARCHIVE_COLUMNS, cells, strict=True)) + entry = ArchiveEntry( + feature_id=strip_code_span(row["Feature ID"]), + month=strip_code_span(row["Month"]), + current_path=strip_code_span(row["Current Path"]), + archive_state=strip_code_span(row["Archive State"]), + closed_at=strip_code_span(row["Closed At"]), + delivered_summary=row["Delivered Summary"].strip(), + source_requirements=strip_code_span(row["Source Requirements"]), + applicable_decisions=strip_code_span(row["Applicable Decisions"]), + last_moved_at=strip_code_span(row["Last Moved At"]), + ) + _validate_entry(entry) + entries.append(entry) + + feature_ids: set[str] = set() + normalized_paths: set[str] = set() + for entry in entries: + if entry.feature_id in feature_ids: + raise ArchiveContractError( + "archive-index", f"duplicate Feature ID: {entry.feature_id}" + ) + feature_ids.add(entry.feature_id) + relative = _memory_relative_from_current_path(entry.current_path) + normalized = unicodedata.normalize("NFKC", relative).casefold() + if normalized in normalized_paths: + raise ArchiveContractError( + "archive-index", f"duplicate normalized Current Path: {relative}" + ) + normalized_paths.add(normalized) + return tuple(sorted(entries, key=lambda item: (item.month, item.feature_id))) + + +def _one_line(value: str) -> str: + return " ".join(value.replace("|", "|").split()) + + +def _locator(value: str) -> str: + cleaned = _one_line(value) + return cleaned if cleaned.lower() == "none" else f"`{cleaned}`" + + +def render_archive_index(entries: Sequence[ArchiveEntry]) -> str: + rows: list[str] = [] + seen: list[ArchiveEntry] = [] + for entry in sorted(entries, key=lambda item: (item.month, item.feature_id)): + _validate_entry(entry) + seen.append(entry) + rows.append( + "| " + + " | ".join( + ( + entry.feature_id, + entry.month, + f"`{_one_line(entry.current_path)}`", + entry.archive_state, + _one_line(entry.closed_at), + _one_line(entry.delivered_summary), + _locator(entry.source_requirements), + _locator(entry.applicable_decisions), + _one_line(entry.last_moved_at), + ) + ) + + " |" + ) + if len({item.feature_id for item in seen}) != len(seen): + raise ArchiveContractError("archive-index", "duplicate Feature ID while rendering") + return ARCHIVE_TEMPLATE + (("\n".join(rows) + "\n") if rows else "") + + +def _project_root_for(memory_root: Path) -> Path: + return ( + memory_root.parent + if memory_root.name in {".agent-loop", "agent-loop"} + else memory_root + ) + + +def _confined_path(root: Path, relative: str) -> Path: + if not relative or "\\" in relative: + raise ArchiveContractError("path-escape", f"unsafe workspace path: {relative}") + pure = PurePosixPath(relative) + if pure.is_absolute() or ".." in pure.parts: + raise ArchiveContractError("path-escape", f"unsafe workspace path: {relative}") + boundary = root.resolve() + candidate = root / pure + try: + candidate.resolve().relative_to(boundary) + except ValueError as error: + raise ArchiveContractError("path-escape", f"unsafe workspace path: {relative}") from error + return candidate + + +def _confined_existing_directory(memory_root: Path, relative: str) -> Path: + candidate = memory_root / PurePosixPath(relative) + boundary = _project_root_for(memory_root).resolve() + resolved = candidate.resolve() + try: + resolved.relative_to(boundary) + except ValueError as error: + raise ArchiveContractError("path-escape", relative) from error + if not candidate.is_dir(): + raise ArchiveContractError("archive-index", f"missing directory: {relative}") + return candidate + + +def resolve_feature_location(memory_root: Path, feature_id: str) -> FeatureLocation: + expected_month = _feature_month(feature_id) + features_root = memory_root / "features" + flat = features_root / feature_id + month_locations = tuple( + child / feature_id + for child in sorted(features_root.iterdir() if features_root.is_dir() else ()) + if child.is_dir() and MONTH_RE.fullmatch(child.name) and (child / feature_id).exists() + ) + entries = parse_archive_index(memory_root) + matching = tuple(entry for entry in entries if entry.feature_id == feature_id) + + if flat.exists(): + conflicting_rows = tuple( + entry + for entry in matching + if entry.archive_state != "rehydrated" + or _memory_relative_from_current_path(entry.current_path) + != f"features/{feature_id}" + ) + if month_locations or conflicting_rows: + raise ArchiveContractError( + "path-collision", f"flat and archived locations exist for {feature_id}" + ) + _confined_existing_directory(memory_root, f"features/{feature_id}") + return FeatureLocation(feature_id, f"features/{feature_id}", "flat", None) + + if len(month_locations) > 1: + raise ArchiveContractError( + "path-collision", f"multiple archived locations exist for {feature_id}" + ) + if month_locations and month_locations[0].parent.name != expected_month: + raise ArchiveContractError( + "month", + f"Feature ID {feature_id} is under {month_locations[0].parent.name}", + ) + if len(matching) != 1: + raise ArchiveContractError( + "archive-index", f"expected one locator row for {feature_id}, got {len(matching)}" + ) + entry = matching[0] + relative = _validate_entry(entry) + if entry.archive_state != "archived": + raise ArchiveContractError( + "archive-index", f"rehydrated feature is missing flat path: {feature_id}" + ) + _confined_existing_directory(memory_root, relative) + if not month_locations: + raise ArchiveContractError( + "archive-index", f"locator row target is not discoverable: {relative}" + ) + expected_relative = f"features/{expected_month}/{feature_id}" + if relative != expected_relative: + raise ArchiveContractError( + "archive-index", f"locator path mismatch: {relative}" + ) + return FeatureLocation(feature_id, relative, "archived", expected_month) + + +def _read_optional(path: Path) -> str: + return read_text(path) if path.is_file() else "" + + +def _read_utf8_preserving(path: Path) -> tuple[str, bool]: + content = path.read_bytes() + has_bom = content.startswith(UTF8_BOM) + payload = content[len(UTF8_BOM) :] if has_bom else content + return payload.decode("utf-8"), has_bom + + +def _encode_utf8_preserving(content: str, has_bom: bool) -> bytes: + encoded = content.encode("utf-8") + return UTF8_BOM + encoded if has_bom else encoded + + +def _concrete_summary(value: str) -> bool: + cleaned = strip_code_span(value).strip() + if not cleaned or re.fullmatch( + r"(?:none|n/a|na|tbd|todo|unknown|-)", cleaned, re.IGNORECASE + ): + return False + return not re.search(r"<[^>]+>", cleaned) + + +def _readiness_values(notes: str) -> dict[str, str]: + body = optional_section(notes, "Archive Readiness") + if body is None: + return {} + names = ( + "Closed At", + "Delivered Summary", + "Verification", + "Feature Close Review", + "Drift", + "Project Memory Impact", + "Open Follow-up", + ) + return {name: (metadata(body, name) or "").strip() for name in names} + + +def inspect_feature( + memory_root: Path, feature_id: str, as_of: date +) -> ArchiveCandidate: + month = _feature_month(feature_id) + location = resolve_feature_location(memory_root, feature_id) + root = memory_root / PurePosixPath(location.relative_path) + spec = _read_optional(root / "spec.md") + tasks = _read_optional(root / "tasks.md") + notes = _read_optional(root / "notes.md") + project = _read_optional(memory_root / "project.md") + lifecycle = (metadata(spec, "Status") or "missing").strip() + blockers: list[str] = [] + if lifecycle != "closed": + blockers.append(f"lifecycle:{lifecycle}") + if month == as_of.strftime("%Y-%m"): + blockers.append("current-month") + + close_record = optional_section(notes, "Close Record") + close_at = metadata(close_record or "", "Closed At") or "" + task_statuses = re.findall(r"(?mi)^\s*-?\s*Status\s*:\s*([^\n]+)", tasks) + task_complete = bool(task_statuses) and all( + status.strip() in {"done", "skipped"} for status in task_statuses + ) + readiness = _readiness_values(notes) + if not readiness: + blockers.append("missing-archive-readiness") + delivered_summary = readiness.get("Delivered Summary", "") + if readiness and not _concrete_summary(delivered_summary): + blockers.append("non-concrete-delivered-summary") + required_values = { + "Verification": {"complete"}, + "Feature Close Review": {"complete"}, + "Drift": {"resolved"}, + "Project Memory Impact": {"complete", "none"}, + } + for name, allowed in required_values.items(): + actual = readiness.get(name, "") + if readiness and actual not in allowed: + blockers.append( + f"archive-readiness-{name.lower().replace(' ', '-')}:{actual or 'missing'}" + ) + readiness_closed_at = readiness.get("Closed At", "") + if readiness and ( + not re.fullmatch(r"\d{4}-\d{2}-\d{2}", readiness_closed_at) + or readiness_closed_at != close_at + ): + blockers.append("archive-readiness-closed-at") + open_follow_up = readiness.get("Open Follow-up", "") or "unknown" + if readiness and open_follow_up != "none": + blockers.append(f"open-follow-up:{open_follow_up}") + + close_complete = bool( + close_record + and re.fullmatch(r"\d{4}-\d{2}-\d{2}", close_at) + and task_complete + and readiness + and not any( + blocker.startswith("archive-readiness") + or blocker in { + "missing-archive-readiness", + "non-concrete-delivered-summary", + } + for blocker in blockers + ) + ) + if not close_complete: + blockers.append("incomplete-close-evidence") + + active = strip_code_span(metadata(project, "Active Feature") or "") + paused = strip_code_span(metadata(project, "Paused Features") or "") + if active and active.lower() != "none" and feature_id in active: + blockers.append("project-memory-active") + if paused and paused.lower() != "none" and feature_id in paused: + blockers.append("project-memory-paused") + + return ArchiveCandidate( + feature_id=feature_id, + month=month, + current_path=location.relative_path, + lifecycle=lifecycle, + close_evidence="complete" if close_complete else "incomplete", + open_follow_up=open_follow_up, + delivered_summary=delivered_summary, + source_requirements=strip_code_span( + metadata(spec, "Source Requirements") or "none" + ), + applicable_decisions=strip_code_span( + metadata(spec, "Applicable Decisions") or "none" + ), + blockers=tuple(sorted(set(blockers))), + ) + + +def discover_flat_features(memory_root: Path) -> Sequence[FeatureLocation]: + root = memory_root / "features" + if not root.is_dir(): + return () + locations: list[FeatureLocation] = [] + for child in sorted(root.iterdir(), key=lambda item: item.name): + if child.is_symlink(): + raise ArchiveContractError("path-escape", f"symlinked feature path: {child}") + if child.is_dir() and FEATURE_ID_RE.fullmatch(child.name): + locations.append( + FeatureLocation(child.name, f"features/{child.name}", "flat", None) + ) + return tuple(locations) + + +def _markdown_files(project_root: Path) -> Sequence[Path]: + files: list[Path] = [] + boundary = project_root.resolve() + for current, directories, names in os.walk(project_root, followlinks=False): + current_path = Path(current) + kept: list[str] = [] + for name in sorted(directories): + candidate = current_path / name + if name in EXCLUDED_SCAN_DIRS: + continue + if candidate.is_symlink(): + raise ArchiveContractError( + "path-escape", + f"symlinked directory cannot be reference-scanned: {candidate.relative_to(project_root).as_posix()}", + ) + kept.append(name) + directories[:] = kept + for name in sorted(names): + if not name.lower().endswith(".md"): + continue + candidate = current_path / name + if candidate.is_symlink(): + raise ArchiveContractError( + "path-escape", + f"symlinked Markdown cannot be reference-scanned: {candidate.relative_to(project_root).as_posix()}", + ) + try: + candidate.resolve().relative_to(boundary) + except ValueError as error: + raise ArchiveContractError( + "path-escape", candidate.relative_to(project_root).as_posix() + ) from error + files.append(candidate) + return tuple(sorted(files)) + + +def _preserved_reference_class(relative: str) -> str | None: + parts = PurePosixPath(relative).parts + if "requirements" in parts: + requirement_at = parts.index("requirements") + tail = parts[requirement_at + 1 :] + if tail and not ( + len(tail) == 1 and tail[0] == "INDEX.md" + ) and PurePosixPath(relative).name != "README.md": + return "immutable-requirement-source" + if len(parts) >= 2 and parts[0] == "docs" and parts[1] in { + "proposal", + "reports", + }: + return "historical-evidence" + return None + + +LINK_TARGET_RE = re.compile( + r"!?\[[^\]]*\]\(\s*(?P<[^>]+>|[^\s)]+)|" + r"^\s*\[[^\]]+\]:\s*(?P<[^>]+>|\S+)", + re.MULTILINE, +) + + +def _relative_link_replacements( + project_root: Path, file: Path, move: Move, content: str +) -> tuple[list[tuple[str, str]], list[SkippedReference]]: + source_root = project_root / PurePosixPath(move.source) + target_root = project_root / PurePosixPath(move.target) + try: + within = file.relative_to(source_root) + file_moves = True + except ValueError: + within = None + file_moves = False + target_file = target_root / within if within is not None else file + replacements: list[tuple[str, str]] = [] + skipped: list[SkippedReference] = [] + for match in LINK_TARGET_RE.finditer(content): + raw = match.group("inline") or match.group("definition") or "" + wrapped = raw.startswith("<") and raw.endswith(">") + target = raw[1:-1] if wrapped else raw + parsed = urlsplit(target) + if parsed.scheme in {"http", "https", "mailto"} or parsed.netloc: + continue + if not parsed.path or (not parsed.path and parsed.fragment): + continue + decoded = unquote(parsed.path) + resolved = (file.parent / decoded).resolve() + try: + resolved.relative_to(project_root.resolve()) + except ValueError: + skipped.append( + SkippedReference( + file.relative_to(project_root).as_posix(), + "unsupported", + target, + "relative Markdown link escapes project root", + ) + ) + continue + try: + target_within_source = resolved.relative_to(source_root.resolve()) + target_is_in_source = True + except ValueError: + target_within_source = None + target_is_in_source = False + if not resolved.exists() and (file_moves or target_is_in_source): + skipped.append( + SkippedReference( + file.relative_to(project_root).as_posix(), + "unsupported", + target, + "broken cross-boundary Markdown link target does not exist", + ) + ) + continue + if not resolved.exists(): + continue + if file_moves and target_is_in_source: + continue + if not file_moves and not target_is_in_source: + continue + destination = ( + target_root / target_within_source + if target_is_in_source and target_within_source is not None + else resolved + ) + relative_target = os.path.relpath(destination, target_file.parent).replace( + os.sep, "/" + ) + rebuilt = urlunsplit( + ("", "", relative_target, parsed.query, parsed.fragment) + ) + if wrapped: + rebuilt = f"<{rebuilt}>" + if rebuilt != raw: + replacements.append((raw, rebuilt)) + return replacements, skipped + + +def _discover_reference_impact( + project_root: Path, moves: Sequence[Move] +) -> tuple[Sequence[ReferenceEdit], Sequence[SkippedReference]]: + planned: dict[str, list[tuple[str, str, str]]] = {} + skipped: list[SkippedReference] = [] + for path in _markdown_files(project_root): + relative = path.relative_to(project_root).as_posix() + if relative in { + ".agent-loop/features/archive.md", + "agent-loop/features/archive.md", + }: + continue + if path.stat().st_size > MAX_MARKDOWN_BYTES: + skipped.append( + SkippedReference( + relative, + "unsupported", + "file-size", + "Markdown file exceeds 2 MiB reference-scan limit", + ) + ) + continue + try: + content, _ = _read_utf8_preserving(path) + except UnicodeDecodeError: + skipped.append( + SkippedReference( + relative, + "unsupported", + "utf-8", + "Markdown file is not valid UTF-8", + ) + ) + continue + preserved = _preserved_reference_class(relative) + for move in moves: + old_prefix = move.source.rstrip("/") + "/" + new_prefix = move.target.rstrip("/") + "/" + if move.source not in content: + link_replacements, link_skipped = _relative_link_replacements( + project_root, path, move, content + ) + skipped.extend(link_skipped) + for old, new in link_replacements: + planned.setdefault(relative, []).append( + ("relative-link", old, new) + ) + continue + if preserved: + skipped.append( + SkippedReference( + relative, + preserved, + move.source, + "preserved source or historical evidence retains the original path", + ) + ) + continue + if old_prefix in content: + planned.setdefault(relative, []).append( + ("literal-path", old_prefix, new_prefix) + ) + remaining = content.replace(old_prefix, "") + if move.source in remaining: + skipped.append( + SkippedReference( + relative, + "unsupported", + move.source, + "old feature path appears in an unsupported or ambiguous form", + ) + ) + link_replacements, link_skipped = _relative_link_replacements( + project_root, path, move, content + ) + skipped.extend(link_skipped) + for old, new in link_replacements: + planned.setdefault(relative, []).append(("relative-link", old, new)) + + edits: list[ReferenceEdit] = [] + for relative, replacements in sorted(planned.items()): + path = project_root / PurePosixPath(relative) + current, has_bom = _read_utf8_preserving(path) + for kind, old, new in sorted(set(replacements)): + occurrences = current.count(old) + if not occurrences: + continue + before = _encode_utf8_preserving(current, has_bom) + current = current.replace(old, new) + after = _encode_utf8_preserving(current, has_bom) + edits.append( + ReferenceEdit( + path=relative, + kind=kind, + old=old, + new=new, + occurrences=occurrences, + before_sha256=sha256_bytes(before), + after_sha256=sha256_bytes(after), + ) + ) + unique_skipped = { + (item.path, item.classification, item.matched_value, item.reason): item + for item in skipped + } + return ( + tuple(sorted(edits, key=lambda item: (item.path, item.kind, item.old, item.new))), + tuple(unique_skipped[key] for key in sorted(unique_skipped)), + ) + + +def discover_reference_impacts( + project_root: Path, moves: Sequence[Move] +) -> Sequence[ReferenceEdit]: + edits, _ = _discover_reference_impact(project_root, moves) + return edits + + +def _project_relative_prefix(project_root: Path, memory_root: Path) -> str: + try: + return memory_root.relative_to(project_root.resolve()).as_posix() + except ValueError as error: + raise ArchiveContractError( + "path-escape", "memory root is outside project root" + ) from error + + +def _candidate_closed_at(memory_root: Path, candidate: ArchiveCandidate) -> str: + notes = _read_optional( + memory_root / PurePosixPath(candidate.current_path) / "notes.md" + ) + return _readiness_values(notes).get("Closed At", "") + + +def _snapshot_plan_inputs( + project_root: Path, + memory_root: Path, + candidates: Sequence[ArchiveCandidate], + reference_edits: Sequence[ReferenceEdit], + skipped_references: Sequence[SkippedReference], +) -> Mapping[str, str]: + paths: set[Path] = set() + for candidate in candidates: + root = memory_root / PurePosixPath(candidate.current_path) + if root.is_dir(): + for path in root.rglob("*"): + if path.is_symlink(): + raise ArchiveContractError( + "path-escape", + path.relative_to(project_root).as_posix(), + ) + if path.is_file(): + paths.add(path) + for relative in ("project.md", "features/archive.md"): + path = memory_root / PurePosixPath(relative) + if path.is_file(): + paths.add(path) + for item in reference_edits: + paths.add(project_root / PurePosixPath(item.path)) + for item in skipped_references: + path = project_root / PurePosixPath(item.path) + if path.is_file() and path.stat().st_size <= MAX_MARKDOWN_BYTES: + paths.add(path) + return MappingProxyType( + { + path.relative_to(project_root).as_posix(): sha256_bytes(path.read_bytes()) + for path in sorted(paths) + } + ) + + +def build_archive_plan( + project_root: Path, + *, + operation: Literal["archive", "rehydrate"], + selected_months: Sequence[str], + selected_feature_ids: Sequence[str], + as_of: date, +) -> ArchivePlan: + project_root = project_root.resolve() + memory_root = discover_memory_root(project_root) + _assert_no_stranded_transactions(memory_root) + months = tuple(sorted(set(selected_months))) + feature_ids = tuple(sorted(set(selected_feature_ids))) + if operation not in {"archive", "rehydrate"}: + raise ArchiveContractError("usage", f"unsupported operation: {operation}", 2) + if any(not MONTH_RE.fullmatch(month) for month in months): + raise ArchiveContractError("usage", "month must be YYYY-MM", 2) + if operation == "archive" and (not months or feature_ids): + raise ArchiveContractError( + "usage", "archive requires --month and forbids --feature-id", 2 + ) + if operation == "rehydrate" and (not feature_ids or months): + raise ArchiveContractError( + "usage", "rehydrate requires --feature-id and forbids --month", 2 + ) + + existing_entries = tuple(parse_archive_index(memory_root)) + candidates: list[ArchiveCandidate] = [] + moves: list[Move] = [] + memory_prefix = _project_relative_prefix(project_root, memory_root) + + if operation == "archive": + for location in discover_flat_features(memory_root): + month = _feature_month(location.feature_id) + if month not in months: + continue + resolve_feature_location(memory_root, location.feature_id) + candidate = inspect_feature(memory_root, location.feature_id, as_of) + candidates.append(candidate) + if not candidate.blockers: + moves.append( + Move( + candidate.feature_id, + candidate.month, + f"{memory_prefix}/features/{candidate.feature_id}", + f"{memory_prefix}/features/{candidate.month}/{candidate.feature_id}", + ) + ) + known = {candidate.feature_id for candidate in candidates} + for entry in existing_entries: + if ( + entry.month in months + and entry.archive_state == "archived" + and entry.feature_id not in known + ): + candidate = inspect_feature(memory_root, entry.feature_id, as_of) + candidates.append( + replace( + candidate, + blockers=tuple(candidate.blockers) + ("already-archived",), + ) + ) + else: + for feature_id in feature_ids: + location = resolve_feature_location(memory_root, feature_id) + candidate = inspect_feature(memory_root, feature_id, as_of) + if location.layout != "archived": + candidate = replace( + candidate, + blockers=tuple(candidate.blockers) + ("already-rehydrated",), + ) + candidates.append(candidate) + if not candidate.blockers and location.layout == "archived": + moves.append( + Move( + feature_id, + _feature_month(feature_id), + f"{memory_prefix}/{location.relative_path}", + f"{memory_prefix}/features/{feature_id}", + ) + ) + + entries_by_id = {entry.feature_id: entry for entry in existing_entries} + candidates_by_id = {candidate.feature_id: candidate for candidate in candidates} + for move in moves: + candidate = candidates_by_id[move.feature_id] + entries_by_id[move.feature_id] = ArchiveEntry( + feature_id=move.feature_id, + month=move.month, + current_path=move.target.rstrip("/") + "/", + archive_state="archived" if operation == "archive" else "rehydrated", + closed_at=_candidate_closed_at(memory_root, candidate), + delivered_summary=candidate.delivered_summary, + source_requirements=candidate.source_requirements, + applicable_decisions=candidate.applicable_decisions, + last_moved_at=as_of.isoformat(), + ) + + reference_edits, skipped_references = _discover_reference_impact( + project_root, moves + ) + snapshots = _snapshot_plan_inputs( + project_root, + memory_root, + candidates, + reference_edits, + skipped_references, + ) + return ArchivePlan( + schema_version=1, + operation=operation, + as_of=as_of.isoformat(), + selected_months=months, + selected_feature_ids=feature_ids, + candidates=tuple(candidates), + moves=tuple(moves), + archive_entries=tuple(entries_by_id.values()), + reference_edits=reference_edits, + skipped_references=skipped_references, + snapshots=snapshots, + ) + + +def archive_plan_from_payload(payload: Mapping[str, object]) -> ArchivePlan: + expected_keys = { + "schema_version", + "operation", + "as_of", + "selected_months", + "selected_feature_ids", + "candidates", + "moves", + "archive_entries", + "reference_edits", + "skipped_references", + "snapshots", + "plan_sha256", + } + if set(payload) != expected_keys: + raise ArchiveContractError("usage", "archive plan fields do not match schema", 2) + try: + plan = ArchivePlan( + schema_version=int(payload["schema_version"]), + operation=str(payload["operation"]), + as_of=str(payload["as_of"]), + selected_months=tuple(payload["selected_months"]), + selected_feature_ids=tuple(payload["selected_feature_ids"]), + candidates=tuple( + ArchiveCandidate(**item) for item in payload["candidates"] + ), + moves=tuple(Move(**item) for item in payload["moves"]), + archive_entries=tuple( + ArchiveEntry(**item) for item in payload["archive_entries"] + ), + reference_edits=tuple( + ReferenceEdit(**item) for item in payload["reference_edits"] + ), + skipped_references=tuple( + SkippedReference(**item) for item in payload["skipped_references"] + ), + snapshots=dict(payload["snapshots"]), + ) + except (KeyError, TypeError, ValueError) as error: + raise ArchiveContractError("usage", f"invalid archive plan: {error}", 2) from error + if plan.schema_version != 1: + raise ArchiveContractError("usage", "unsupported archive plan schema", 2) + plan.assert_hash(str(payload["plan_sha256"])) + return plan + + +def _hash_at(path: Path) -> str: + if not path.is_file(): + raise ArchiveContractError("post-check", f"missing file: {path}") + return sha256_bytes(path.read_bytes()) + + +def validate_archive_plan_state( + project_root: Path, plan: ArchivePlan, operation: str +) -> str: + project_root = project_root.resolve() + if operation not in {plan.operation, "restore"}: + raise ArchiveContractError( + "usage", f"plan operation is {plan.operation}, requested {operation}", 2 + ) + unsupported = [ + item for item in plan.skipped_references if item.classification == "unsupported" + ] + if unsupported: + raise ArchiveContractError( + "reference-impact", + f"unsupported references remain: {', '.join(item.path for item in unsupported)}", + ) + + sources = [_confined_path(project_root, move.source) for move in plan.moves] + targets = [_confined_path(project_root, move.target) for move in plan.moves] + pre = not plan.moves or ( + all(path.is_dir() for path in sources) + and all(not path.exists() for path in targets) + ) + post = bool(plan.moves) and all(not path.exists() for path in sources) and all( + path.is_dir() for path in targets + ) + if not pre and not post: + raise ArchiveContractError( + "post-check", "source/target paths are in a mixed or unexpected state" + ) + + if pre: + try: + rebuilt = build_archive_plan( + project_root, + operation=plan.operation, + selected_months=plan.selected_months, + selected_feature_ids=plan.selected_feature_ids, + as_of=date.fromisoformat(plan.as_of), + ) + except ValueError as error: + raise ArchiveContractError("usage", "plan as_of must be YYYY-MM-DD", 2) from error + if rebuilt.computed_sha256() != plan.computed_sha256(): + raise ArchiveContractError( + "stale-plan", + f"stored {plan.computed_sha256()}, rebuilt {rebuilt.computed_sha256()}", + ) + return "restore-check" if operation == "restore" else "pre-check" + + if operation == "restore": + raise ArchiveContractError( + "restore", "project does not match the plan's pre-transaction state" + ) + + final_hashes = { + item.path: item.after_sha256 for item in plan.reference_edits + } + memory_root = discover_memory_root(project_root) + memory_prefix = _project_relative_prefix(project_root, memory_root) + index_relative = f"{memory_prefix}/features/archive.md" + rendered_index_hash = sha256_bytes( + render_archive_index(plan.archive_entries).encode("utf-8") + ) + for relative, expected in plan.snapshots.items(): + mapped = relative + for move in plan.moves: + source = move.source.rstrip("/") + if relative == source or relative.startswith(source + "/"): + mapped = move.target.rstrip("/") + relative[len(source) :] + break + actual_path = _confined_path(project_root, mapped) + actual = _hash_at(actual_path) + required = ( + rendered_index_hash + if mapped == index_relative + else final_hashes.get(relative, final_hashes.get(mapped, expected)) + ) + if actual != required: + raise ArchiveContractError( + "post-check", f"content hash mismatch: {mapped}" + ) + actual_entries = tuple(parse_archive_index(memory_root)) + if actual_entries != tuple( + sorted(plan.archive_entries, key=lambda item: (item.month, item.feature_id)) + ): + raise ArchiveContractError("post-check", "archive index does not match plan") + return "post-check" + + +def _transaction_root(project_root: Path, transaction_id: str) -> Path: + if not TRANSACTION_ID_RE.fullmatch(transaction_id): + raise ArchiveContractError( + "usage", "transaction ID must match YYYYMMDDTHHMMSSZ-12hex", 2 + ) + memory_root = discover_memory_root(project_root.resolve()) + root = memory_root / "features" / ".archive-txn" / transaction_id + boundary = (memory_root / "features" / ".archive-txn").resolve() + try: + root.resolve().relative_to(boundary) + except ValueError as error: + raise ArchiveContractError("path-escape", transaction_id) from error + return root + + +def _assert_no_stranded_transactions(memory_root: Path) -> None: + transaction_root = memory_root / "features" / ".archive-txn" + if not transaction_root.exists(): + return + if transaction_root.is_symlink() or not transaction_root.is_dir(): + raise ArchiveContractError( + "transaction", "stranded-transaction: unsafe .archive-txn path" + ) + stranded = sorted(path.name for path in transaction_root.iterdir()) + if stranded: + raise ArchiveContractError( + "transaction", + "stranded-transaction: restore required for " + ", ".join(stranded), + ) + + +def _write_journal(transaction_root: Path, journal: Mapping[str, object]) -> None: + atomic_write_bytes( + transaction_root / "journal.json", + json.dumps( + journal, ensure_ascii=False, sort_keys=True, indent=2 + ).encode("utf-8") + + b"\n", + ) + + +def _load_journal(transaction_root: Path) -> dict[str, object]: + path = transaction_root / "journal.json" + if not path.is_file(): + raise ArchiveContractError("restore", f"missing journal: {path}") + try: + value = json.loads(path.read_text(encoding="utf-8-sig")) + except (json.JSONDecodeError, UnicodeDecodeError) as error: + raise ArchiveContractError("restore", f"invalid journal: {error}") from error + if not isinstance(value, dict) or value.get("schema_version") != 1: + raise ArchiveContractError("restore", "unsupported journal schema") + if value.get("state") not in JOURNAL_STATES: + raise ArchiveContractError("restore", "invalid journal state") + return value + + +def _journal_index_relative(project_root: Path) -> str: + memory_root = discover_memory_root(project_root) + return f"{_project_relative_prefix(project_root, memory_root)}/features/archive.md" + + +def _validate_journal_scope( + project_root: Path, journal: Mapping[str, object] +) -> tuple[ArchivePlan, tuple[Move, ...], tuple[Mapping[str, object], ...]]: + expected_keys = { + "schema_version", + "transaction_id", + "operation", + "plan_sha256", + "plan", + "state", + "moves", + "backups", + "completed_operations", + "snapshots", + "created_directories", + } + if set(journal) != expected_keys: + raise ArchiveContractError("restore", "journal fields do not match schema") + plan_payload = journal.get("plan") + if not isinstance(plan_payload, dict): + raise ArchiveContractError("restore", "journal plan is missing or invalid") + try: + plan = archive_plan_from_payload(plan_payload) + except ArchiveContractError as error: + raise ArchiveContractError( + "restore", f"journal plan is invalid: {error}" + ) from error + if not plan.moves: + raise ArchiveContractError("restore", "journal plan has no moves") + if journal.get("plan_sha256") != plan.computed_sha256(): + raise ArchiveContractError("restore", "journal plan SHA-256 mismatch") + if journal.get("operation") != plan.operation: + raise ArchiveContractError("restore", "journal operation does not match plan") + + expected_moves = [asdict(move) for move in plan.moves] + if journal.get("moves") != expected_moves: + raise ArchiveContractError("restore", "journal move scope does not match plan") + if journal.get("snapshots") != dict(plan.snapshots): + raise ArchiveContractError("restore", "journal snapshots do not match plan") + + reference_hashes: dict[str, str] = {} + for edit in sorted( + plan.reference_edits, + key=lambda item: (item.path, item.kind, item.old, item.new), + ): + expected_before = reference_hashes.get( + edit.path, plan.snapshots.get(edit.path, "") + ) + if ( + not expected_before + or edit.before_sha256 != expected_before + or not HASH_RE.fullmatch(edit.after_sha256) + or edit.occurrences < 1 + or not edit.old + or edit.old == edit.new + ): + raise ArchiveContractError( + "restore", + f"journal reference-edit chain does not match snapshots: {edit.path}", + ) + reference_hashes[edit.path] = edit.after_sha256 + + index_relative = _journal_index_relative(project_root) + expected_backup_paths = {index_relative} | { + edit.path for edit in plan.reference_edits + } + raw_backups = journal.get("backups") + if not isinstance(raw_backups, list) or not all( + isinstance(item, dict) for item in raw_backups + ): + raise ArchiveContractError("restore", "journal backups are invalid") + backups = tuple(raw_backups) + actual_paths = [str(item.get("path", "")) for item in backups] + if ( + len(actual_paths) != len(set(actual_paths)) + or set(actual_paths) != expected_backup_paths + ): + raise ArchiveContractError("restore", "journal backup scope does not match plan") + for item in backups: + relative = str(item.get("path", "")) + expected_state = "existing" if relative in plan.snapshots else "missing-before" + if item.get("state") != expected_state: + raise ArchiveContractError( + "restore", f"journal backup state does not match plan: {relative}" + ) + if expected_state == "existing": + expected_item_keys = {"path", "state", "backup", "sha256"} + if set(item) != expected_item_keys: + raise ArchiveContractError( + "restore", f"journal backup fields are invalid: {relative}" + ) + if item.get("backup") != f"backups/{relative}": + raise ArchiveContractError( + "restore", f"journal backup path does not match plan: {relative}" + ) + if item.get("sha256") != plan.snapshots[relative]: + raise ArchiveContractError( + "restore", f"journal backup hash does not match plan: {relative}" + ) + elif set(item) != {"path", "state"}: + raise ArchiveContractError( + "restore", f"journal missing-before fields are invalid: {relative}" + ) + + allowed_operations: set[tuple[tuple[str, str], ...]] = set() + for move in plan.moves: + allowed_operations.add( + tuple( + sorted( + { + "kind": "move", + "source": move.source, + "target": move.target, + }.items() + ) + ) + ) + allowed_operations.add( + tuple(sorted({"kind": "archive-index", "path": index_relative}.items())) + ) + for edit in plan.reference_edits: + allowed_operations.add( + tuple( + sorted( + { + "kind": "reference-edit", + "path": edit.path, + "mapped_path": _mapped_after_moves(edit.path, plan.moves), + }.items() + ) + ) + ) + completed = journal.get("completed_operations") + if not isinstance(completed, list) or not all( + isinstance(item, dict) for item in completed + ): + raise ArchiveContractError("restore", "journal completed operations are invalid") + if any(tuple(sorted(item.items())) not in allowed_operations for item in completed): + raise ArchiveContractError( + "restore", "journal completed operation exceeds plan scope" + ) + + allowed_directories = { + PurePosixPath(move.target).parent.as_posix() for move in plan.moves + } + created = journal.get("created_directories") + if not isinstance(created, list) or len(created) != len(set(created)) or any( + not isinstance(item, str) or item not in allowed_directories for item in created + ): + raise ArchiveContractError( + "restore", "journal created-directory scope does not match plan" + ) + return plan, tuple(plan.moves), backups + + +def _restore_allowed_hashes( + project_root: Path, plan: ArchivePlan, relative: str +) -> frozenset[str]: + hashes = {plan.snapshots[relative]} if relative in plan.snapshots else set() + for edit in plan.reference_edits: + if edit.path == relative: + hashes.add(edit.before_sha256) + hashes.add(edit.after_sha256) + if relative == _journal_index_relative(project_root): + hashes.add( + sha256_bytes(render_archive_index(plan.archive_entries).encode("utf-8")) + ) + return frozenset(hashes) + + +def _restore_move_states( + project_root: Path, moves: Sequence[Move] +) -> Mapping[str, str]: + states: dict[str, str] = {} + for move in moves: + source = _confined_path(project_root, move.source) + target = _confined_path(project_root, move.target) + source_ready = source.is_dir() and not target.exists() + target_ready = target.is_dir() and not source.exists() + if source_ready: + states[move.source] = "source" + elif target_ready: + states[move.source] = "target" + else: + raise ArchiveContractError( + "restore", + f"source/target drift blocks restore: {move.source} -> {move.target}", + ) + return MappingProxyType(states) + + +def _restore_current_relative( + relative: str, moves: Sequence[Move], states: Mapping[str, str] +) -> str: + for move in moves: + source = move.source.rstrip("/") + if relative == source or relative.startswith(source + "/"): + if states[move.source] == "target": + return move.target.rstrip("/") + relative[len(source) :] + return relative + return relative + + +def _validate_restore_current_state( + project_root: Path, + transaction_root: Path, + plan: ArchivePlan, + moves: Sequence[Move], + backups: Sequence[Mapping[str, object]], + states: Mapping[str, str], +) -> None: + for item in backups: + if item.get("state") != "existing": + continue + relative = str(item["path"]) + backup = _confined_path(transaction_root, str(item["backup"])) + if ( + not backup.is_file() + or sha256_bytes(backup.read_bytes()) != item.get("sha256") + ): + raise ArchiveContractError( + "restore", f"backup preflight failed for {relative}" + ) + for relative in plan.snapshots: + current_relative = _restore_current_relative(relative, moves, states) + path = _confined_path(project_root, current_relative) + if not path.is_file(): + raise ArchiveContractError( + "restore", f"drift: missing transaction file {current_relative}" + ) + if sha256_bytes(path.read_bytes()) not in _restore_allowed_hashes( + project_root, plan, relative + ): + raise ArchiveContractError( + "restore", f"drift: unexpected transaction bytes at {current_relative}" + ) + for item in backups: + if item.get("state") != "missing-before": + continue + relative = str(item["path"]) + path = _confined_path(project_root, relative) + if not path.exists(): + continue + if ( + not path.is_file() + or sha256_bytes(path.read_bytes()) + not in _restore_allowed_hashes(project_root, plan, relative) + ): + raise ArchiveContractError( + "restore", f"drift: unexpected file at {relative}" + ) + + +def _mapped_after_moves(relative: str, moves: Sequence[Move]) -> str: + for move in moves: + source = move.source.rstrip("/") + if relative == source or relative.startswith(source + "/"): + return move.target.rstrip("/") + relative[len(source) :] + return relative + + +def _remove_transaction_tree(transaction_root: Path) -> None: + parent = transaction_root.parent + shutil.rmtree(transaction_root) + try: + parent.rmdir() + except OSError: + pass + + +def _failure_limit() -> int | None: + raw = os.environ.get("AGENT_LOOP_ARCHIVE_FAIL_AFTER") + if raw is None: + return None + if os.environ.get("AGENT_LOOP_ARCHIVE_TEST_MODE") != "1": + raise ArchiveContractError( + "transaction", + "AGENT_LOOP_ARCHIVE_FAIL_AFTER is test-only and requires AGENT_LOOP_ARCHIVE_TEST_MODE=1", + ) + try: + value = int(raw) + except ValueError as error: + raise ArchiveContractError( + "transaction", "AGENT_LOOP_ARCHIVE_FAIL_AFTER must be a positive integer" + ) from error + if value < 1: + raise ArchiveContractError( + "transaction", "AGENT_LOOP_ARCHIVE_FAIL_AFTER must be a positive integer" + ) + return value + + +def _new_transaction_id(features_root: Path) -> str: + for _ in range(100): + value = ( + datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ-") + + secrets.token_hex(6) + ) + if not (features_root / ".archive-txn" / value).exists(): + return value + raise ArchiveContractError("transaction", "could not allocate transaction ID") + + +def apply_archive_plan( + project_root: Path, + plan: ArchivePlan, + *, + expected_plan_sha256: str, +) -> str: + project_root = project_root.resolve() + plan.assert_hash(expected_plan_sha256) + unsupported = [ + item for item in plan.skipped_references if item.classification == "unsupported" + ] + if unsupported: + raise ArchiveContractError( + "reference-impact", + f"unsupported references block apply: {', '.join(item.path for item in unsupported)}", + ) + memory_root = discover_memory_root(project_root) + _assert_no_stranded_transactions(memory_root) + failure_limit = _failure_limit() + if not plan.moves: + return "no-op" + + features_root = memory_root / "features" + transaction_id = _new_transaction_id(features_root) + transaction_root = _transaction_root(project_root, transaction_id) + transaction_root.mkdir(parents=True) + memory_prefix = _project_relative_prefix(project_root, memory_root) + index_relative = f"{memory_prefix}/features/archive.md" + backup_paths = sorted( + {item.path for item in plan.reference_edits} | {index_relative} + ) + journal: dict[str, object] = { + "schema_version": 1, + "transaction_id": transaction_id, + "operation": plan.operation, + "plan_sha256": plan.computed_sha256(), + "plan": plan.to_payload(), + "state": "prepared", + "moves": [asdict(move) for move in plan.moves], + "backups": [], + "completed_operations": [], + "snapshots": dict(plan.snapshots), + "created_directories": [], + } + _write_journal(transaction_root, journal) + try: + backups: list[dict[str, str]] = [] + for relative in backup_paths: + source = _confined_path(project_root, relative) + if source.is_file(): + backup_relative = f"backups/{relative}" + atomic_write_bytes( + transaction_root / PurePosixPath(backup_relative), source.read_bytes() + ) + backups.append( + { + "path": relative, + "state": "existing", + "backup": backup_relative, + "sha256": sha256_bytes(source.read_bytes()), + } + ) + elif source.exists(): + raise ArchiveContractError( + "transaction", f"backup target is not a file: {relative}" + ) + else: + backups.append({"path": relative, "state": "missing-before"}) + journal["backups"] = backups + _write_journal(transaction_root, journal) + + completed: list[dict[str, str]] = [] + created_directories: list[str] = [] + operation_count = 0 + + def record_mutation(kind: str, **details: str) -> None: + nonlocal operation_count + operation_count += 1 + completed.append({"kind": kind, **details}) + journal["completed_operations"] = completed + journal["created_directories"] = created_directories + _write_journal(transaction_root, journal) + if failure_limit is not None and operation_count >= failure_limit: + raise RuntimeError( + f"injected archive failure after operation {operation_count}" + ) + + journal["state"] = "moving" + _write_journal(transaction_root, journal) + for move in plan.moves: + source = _confined_path(project_root, move.source) + target = _confined_path(project_root, move.target) + if not source.is_dir() or target.exists(): + raise ArchiveContractError( + "transaction", f"move precondition failed: {move.source} -> {move.target}" + ) + if not target.parent.exists(): + target.parent.mkdir(parents=True) + created_directories.append( + target.parent.relative_to(project_root).as_posix() + ) + journal["created_directories"] = created_directories + _write_journal(transaction_root, journal) + source.rename(target) + record_mutation("move", source=move.source, target=move.target) + + index = memory_root / "features" / "archive.md" + atomic_write_bytes( + index, render_archive_index(plan.archive_entries).encode("utf-8") + ) + record_mutation("archive-index", path=index_relative) + + for edit in plan.reference_edits: + mapped = _mapped_after_moves(edit.path, plan.moves) + target = _confined_path(project_root, mapped) + current, has_bom = _read_utf8_preserving(target) + current_bytes = _encode_utf8_preserving(current, has_bom) + if sha256_bytes(current_bytes) != edit.before_sha256: + raise ArchiveContractError( + "stale-plan", f"reference changed before apply: {edit.path}" + ) + if current.count(edit.old) != edit.occurrences: + raise ArchiveContractError( + "stale-plan", f"reference occurrence drift: {edit.path}" + ) + updated = current.replace(edit.old, edit.new) + updated_bytes = _encode_utf8_preserving(updated, has_bom) + if sha256_bytes(updated_bytes) != edit.after_sha256: + raise ArchiveContractError( + "transaction", f"reference edit hash mismatch: {edit.path}" + ) + atomic_write_bytes(target, updated_bytes) + record_mutation("reference-edit", path=edit.path, mapped_path=mapped) + + journal["state"] = "references-updated" + _write_journal(transaction_root, journal) + journal["state"] = "checking" + _write_journal(transaction_root, journal) + validate_archive_plan_state(project_root, plan, plan.operation) + journal["state"] = "verified" + _write_journal(transaction_root, journal) + _remove_transaction_tree(transaction_root) + return transaction_id + except Exception as error: + try: + restore_transaction(project_root, transaction_id) + except ArchiveContractError as restore_error: + raise ArchiveContractError( + "transaction", + f"{error}; restore=failed: {restore_error.detail}", + ) from error + raise ArchiveContractError( + "transaction", f"{error}; restore=complete" + ) from error + + +def restore_transaction(project_root: Path, transaction_id: str) -> None: + project_root = project_root.resolve() + transaction_root = _transaction_root(project_root, transaction_id) + journal = _load_journal(transaction_root) + if journal.get("transaction_id") != transaction_id: + raise ArchiveContractError("restore", "journal transaction ID mismatch") + plan, moves, backups = _validate_journal_scope(project_root, journal) + journal["state"] = "restoring" + _write_journal(transaction_root, journal) + states = _restore_move_states(project_root, moves) + _validate_restore_current_state( + project_root, transaction_root, plan, moves, backups, states + ) + try: + for move in reversed(moves): + source = _confined_path(project_root, move.source) + target = _confined_path(project_root, move.target) + if source.exists() and target.exists(): + raise ArchiveContractError( + "restore", f"source collision blocks restore: {move.source}" + ) + if source.is_dir() and not target.exists(): + continue + if source.exists(): + raise ArchiveContractError( + "restore", f"restored source is not a directory: {move.source}" + ) + if not target.is_dir(): + raise ArchiveContractError( + "restore", f"moved target is missing: {move.target}" + ) + source.parent.mkdir(parents=True, exist_ok=True) + target.rename(source) + + for item in backups: + relative = str(item.get("path", "")) + target = _confined_path(project_root, relative) + state = item.get("state") + if state == "existing": + backup = _confined_path( + transaction_root, str(item.get("backup", "")) + ) + if not backup.is_file(): + raise ArchiveContractError( + "restore", f"missing backup for {relative}" + ) + content = backup.read_bytes() + if sha256_bytes(content) != item.get("sha256"): + raise ArchiveContractError( + "restore", f"backup hash mismatch for {relative}" + ) + atomic_write_bytes(target, content) + elif state == "missing-before": + if target.is_dir(): + raise ArchiveContractError( + "restore", f"cannot remove directory backup target: {relative}" + ) + target.unlink(missing_ok=True) + else: + raise ArchiveContractError( + "restore", f"invalid backup state for {relative}" + ) + + for relative, expected in dict(journal.get("snapshots", {})).items(): + actual = _hash_at(_confined_path(project_root, str(relative))) + if actual != expected: + raise ArchiveContractError( + "restore", f"restored content hash mismatch: {relative}" + ) + for relative in reversed(list(journal.get("created_directories", []))): + directory = _confined_path(project_root, str(relative)) + try: + directory.rmdir() + except OSError: + pass + journal["state"] = "restored" + _write_journal(transaction_root, journal) + _remove_transaction_tree(transaction_root) + except ArchiveContractError: + journal["state"] = "restoring" + _write_journal(transaction_root, journal) + raise + except Exception as error: + journal["state"] = "restoring" + _write_journal(transaction_root, journal) + raise ArchiveContractError("restore", str(error)) from error diff --git a/scripts/restore-feature-monthly-archive.py b/scripts/restore-feature-monthly-archive.py new file mode 100644 index 0000000..98b769a --- /dev/null +++ b/scripts/restore-feature-monthly-archive.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +from checker_support import require_supported_python +from feature_archive_support import ArchiveContractError, restore_transaction + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Restore one Feature Monthly Archive transaction" + ) + parser.add_argument("--project-root", required=True) + parser.add_argument("--transaction-id", required=True) + return parser + + +def main() -> int: + require_supported_python() + arguments = build_parser().parse_args() + try: + restore_transaction(Path(arguments.project_root), arguments.transaction_id) + except ArchiveContractError as error: + print(f"{error.category}: {error.detail}", file=sys.stderr) + return error.exit_code + print( + f"PASS: Feature Monthly Archive transaction restored; " + f"transaction_id={arguments.transaction_id}" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/scan-feature-monthly-archive.py b/scripts/scan-feature-monthly-archive.py new file mode 100644 index 0000000..a751d1e --- /dev/null +++ b/scripts/scan-feature-monthly-archive.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +import sys +from datetime import date +from pathlib import Path + +from checker_support import require_supported_python +from feature_archive_support import ArchiveContractError, build_archive_plan + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Build a read-only Feature Monthly Archive plan" + ) + parser.add_argument("--project-root", required=True) + parser.add_argument( + "--operation", choices=("archive", "rehydrate"), required=True + ) + parser.add_argument("--month", action="append", default=[]) + parser.add_argument("--feature-id", action="append", default=[]) + parser.add_argument("--as-of", required=True) + return parser + + +def main() -> int: + require_supported_python() + arguments = build_parser().parse_args() + try: + as_of = date.fromisoformat(arguments.as_of) + except ValueError: + print("usage: --as-of must be YYYY-MM-DD", file=sys.stderr) + return 2 + try: + plan = build_archive_plan( + Path(arguments.project_root), + operation=arguments.operation, + selected_months=arguments.month, + selected_feature_ids=arguments.feature_id, + as_of=as_of, + ) + except ArchiveContractError as error: + print(f"{error.category}: {error.detail}", file=sys.stderr) + return error.exit_code + print( + json.dumps(plan.to_payload(), ensure_ascii=False, sort_keys=True, indent=2) + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/templates/feature-archive.md b/templates/feature-archive.md new file mode 100644 index 0000000..3e7ddf7 --- /dev/null +++ b/templates/feature-archive.md @@ -0,0 +1,6 @@ +# Feature Archive + +This file locates archived or rehydrated features. Feature specs, tests, notes, requirement sources, and accepted decisions remain authoritative. + +| Feature ID | Month | Current Path | Archive State | Closed At | Delivered Summary | Source Requirements | Applicable Decisions | Last Moved At | +|---|---|---|---|---|---|---|---|---| diff --git a/templates/notes.md b/templates/notes.md index 1e81dfa..632dd41 100644 --- a/templates/notes.md +++ b/templates/notes.md @@ -137,3 +137,16 @@ Status: active | blocked | paused | closed ## Pause / Resume Point ## Close Record + +Closed At: +Human Decision: + +## Archive Readiness + +Closed At: +Delivered Summary: +Verification: complete +Feature Close Review: complete +Drift: resolved +Project Memory Impact: complete | none +Open Follow-up: none | diff --git a/templates/root-AGENTS.md b/templates/root-AGENTS.md index 9fc35bd..74d633e 100644 --- a/templates/root-AGENTS.md +++ b/templates/root-AGENTS.md @@ -6,7 +6,7 @@ The agent is responsible for steering the workflow. Do not wait for the human to Guidance language should follow this project's language preference. Keep stable artifact names, stage names, and file paths in English, such as `agent-loop`, `Requirement Archive`, `Feature Spec`, `Feature Auto-Loop`, `Task Auto-Run`, `project.md`, and `requirements/`. - + ## Bootstrap Protocol Before development work: @@ -24,12 +24,13 @@ Before development work: 10. If recent development bypassed `agent-loop`, route to Re-Adopt Agent Loop Project before new feature work. 11. Operational Support Guard: if the human asks to test, run, deploy, switch account/config/model/provider, check quota/rate limits, diagnose production, arrange rollout, or use existing code to solve an operational problem, default to read-only code/process analysis. Do not create a feature, edit code, change config, deploy, or run destructive commands unless the human confirms feature implementation or an operational change. If unclear, ask whether they want feature implementation or help using current project functionality. 12. If the human reports a bug, regression, post-close correction, field/schema/algorithm/API change, test failure, screenshot issue, QA/user feedback, or "small tweak", route to Feature Follow-up / Flow-back before creating a new feature or editing code, but only after project memory exists or Project Entry has routed through Init Project / Project Entry Scan. +12a. If the human explicitly requests closed feature archive/rehydrate maintenance, classify `feature-archive-maintenance`, require reliable memory, and run the read-only Feature Monthly Archive scan. Stop at the exact plan SHA-256 Batch Human Gate before apply; auto modes do not authorize archive or rehydrate. 13. Run Stage Helper Capability Scan for the current stage only after the `agent-loop` controller is active or unavailable/load-failed: inspect whether the current Agent CLI exposes Superpowers or other helper skills/plugins before using fallback stage guidance. 14. Check for the nearest directory-level `AGENTS.md` when working in a subdirectory. 15. Classify the current `agent-loop` stage and recommend exactly one next action. - + ## Agent Ownership - Own workflow diagnosis, sequencing, implementation, verification, review, drift checks, and project-memory updates. @@ -43,7 +44,7 @@ Before development work: - For non-trivial confirmations, present a table-first Human Review Summary before asking approval. - + ## Message Intent Guard Before project-state routing, classify the latest human message intent. @@ -51,6 +52,7 @@ Before project-state routing, classify the latest human message intent. - `chat`: ordinary discussion, rule questions, status questions, or design talk. Answer or discuss only; do not create requirement sets, feature workspaces, tasks, tests, or plans. - `requirements-discussion`: the human is exploring product needs, business goals, capability ideas, constraints, tradeoffs, or user scenarios without authorizing implementation. Requirements discussion must shape demand through Brainstorm / Clarify into a human-reviewed requirement document under `.agent-loop/requirements/` before feature construction. When product concept identity, lifecycle, relationship, state, ownership, or fact meaning can change downstream models, use the internal Concept Foundation Gate before flow/state/product-data modeling. - `project-skill-management`: the human asks to turn a repeatable project workflow into a project-local skill, or to update, disable, or deprecate one. Route to Project Skill Creation / Update after reliable Project Entry/memory. +- `feature-archive-maintenance`: the human explicitly asks to archive closed feature directories by month or rehydrate archived history. Route to Feature Monthly Archive after reliable memory; scan is read-only. - `feature-request`: the human explicitly asks to implement, build, change behavior, or start work from accepted requirements. Route through normal agent-loop feature workflow. Message intent is not permanent. If chat turns into product demand, reclassify as requirements-discussion. If chat turns into a proposal/design-note request, reclassify as `proposal-doc`. If chat turns into implementation, operational support, follow-up, deferred work, or project-skill management, reclassify and route accordingly. If the human explicitly wants discussion without documentation, keep `chat`. @@ -58,10 +60,10 @@ Message intent is not permanent. If chat turns into product demand, reclassify a If unclear whether the human wants chat or requirements discussion, ask whether to keep discussing or shape the topic into a requirements document. If unclear whether the human wants requirements discussion or implementation, ask whether to form a requirements document first or start feature construction. - + ## Workflow Stage Map -Use this after Bootstrap Protocol and Message Intent Guard. Select exactly one next stage from the current human signal and project state. When multiple signals match, apply this first-match order: Safety Stop -> Remote Discovery -> Memory Recovery -> Active Feature Guard -> Blocker Resolution -> Intent Routing -> Normal Stage Continuation. After selecting a stage, load the matching `references/...` file from the `agent-loop` skill package before acting. This map is navigation only; do not treat root `AGENTS.md` as the detailed stage procedure. +Use this after Bootstrap Protocol and Message Intent Guard. Select exactly one next stage from the current human signal and project state. When multiple signals match, apply this first-match order: Safety Stop -> Remote Discovery -> Memory Recovery -> Feature Archive Maintenance -> Active Feature Guard -> Blocker Resolution -> Intent Routing -> Normal Stage Continuation. After selecting a stage, load the matching `references/...` file from the `agent-loop` skill package before acting. This map is navigation only; do not treat root `AGENTS.md` as the detailed stage procedure. | Signal | Next Stage | Load From agent-loop Skill | |---|---|---| @@ -69,6 +71,7 @@ Use this after Bootstrap Protocol and Message Intent Guard. Select exactly one n | No reliable `.agent-loop/` memory and meaningful existing code | Project Entry Scan | `references/project-entry-scan.md`, `references/project-guidance.md` | | Human or local evidence says the source of truth is remote, SSH, container, tunnel, or devcontainer | Remote Project Discovery | `references/remote-project-discovery.md` | | Existing memory conflicts with code reality, or work bypassed the loop | Re-Adopt Agent Loop Project | `references/recovery-and-backfill.md` | +| Human explicitly requests historical closed feature archive or rehydrate | Feature Monthly Archive | `references/stage-guides.md`, `references/artifact-rules.md`, `references/feature-follow-up.md` | | Human asks to turn a repeatable workflow into a project-local skill, or manage an existing one | Project Skill Creation / Update | `references/project-skills.md`, `references/skill-routing.md`, `references/external-skill-adapters.md` | | Product need, business goal, scope, constraint, scenario, concept identity/lifecycle, or phased delivery is still being shaped | Requirements Discussion | `references/requirement-management.md`; also `references/requirement-product-grill.md` for Concept Foundation, terminology, roles, flows, exceptions, prior behavior, or decision signals | | Human confirms recording, accepting, or deferring a requirement source | Requirement Archive | `references/requirement-management.md`, `references/stage-guides.md` | @@ -99,17 +102,18 @@ Use this after Bootstrap Protocol and Message Intent Guard. Select exactly one n | Ordinary question or discussion has no artifact or implementation intent | Chat Entry | `references/runtime.md` only when intent is unclear; otherwise answer without creating workflow artifacts | - + ## Gate Modes - Strict Mode is the default: ask before and after every stage. - Feature Auto-Loop is allowed only after a passed Requirement Checklist, accepted Feature Spec, and explicit human enablement. It may continue Agent-ready downstream stages through implementation, testing, fixing, review, drift, status update, and final report. - Task Auto-Run is allowed only after an accepted task/story plan and explicit human enablement. It runs Analyze Consistency before executing that task/story through TDD, implementation, verification, bug fixing, review, drift check, task status update, and final report. - Auto modes never authorize Project Skill Creation / Update or execution. Gate 1 is required before skill files; the Execution Gate is required for each invocation. A named-skill/concrete-scope request satisfies it only when the emitted execution summary adds no undisclosed action or effect. +- Auto modes never authorize Feature Monthly Archive or rehydrate. Only the read-only scan may run before the exact plan SHA-256 Batch Human Gate. - If repeated confirmations slow the human down, proactively explain Feature Auto-Loop and Task Auto-Run, then ask before enabling either mode. - + ## Required Stops Stop and ask when: @@ -123,6 +127,7 @@ Stop and ask when: - drift check needs human approval - security/data boundaries or broad architecture would change - repeated verification fails +- Feature Monthly Archive has a stale/unconfirmed plan, unsupported reference, flat/month collision, missing locator target, incomplete `.archive-txn`, or would rely on manual directory movement - unrelated dirty work blocks progress - a new dependency, migration, destructive operation, credential, external service, or long-lived boundary directory is needed - Complex Artifact Mode detail directories (`tasks/`, `tests/`, `plans/`) would be created or the feature would switch from simple to complex artifact mode @@ -138,7 +143,7 @@ Stop and ask when: Auto modes do not bypass these stops. - + ## Completion Rules - Before completion claims, run fresh verification and record evidence. @@ -151,7 +156,7 @@ Auto modes do not bypass these stops. - For requirement-driven ADRs, completion also requires source-wide scope accounting, current upstream compatibility, and Requirement Model Technical Landing Trace coverage for every assigned Design Slice. - + ## Submit And Commit Rules - Submit, commit, PR, merge, release, and publish require explicit human confirmation after diff, verification, review, drift, and unrelated-change checks. @@ -163,7 +168,7 @@ Auto modes do not bypass these stops. - For the `agent-loop` skill repository itself, use `(v): ` and a 3-7 bullet body for meaningful commits. - + ## Project Memory And Artifacts - Resolve project-memory and feature paths relative to the active memory root: `.agent-loop/` by default, or legacy `agent-loop/` for the current run. @@ -176,17 +181,18 @@ Auto modes do not bypass these stops. - For complex requirements, suggest `Delivery Phases` in the requirement set `README.md` before feature construction when the human needs to confirm staged delivery. A phase is a human-readable delivery slice, not a feature workspace, task, or plan. - Future/deferred work and backlog items belong in requirement sets and optional `requirements/INDEX.md`, not in `project.md`. Do not edit `requirement.md` or other source files for lifecycle/status updates. - Keep durable producer-consumer interface handoffs in feature `contracts.md` and optional `contracts/` details. Keep temporary subagent assignments in `handoffs/`. +- Keep stable Feature IDs and archive location history in root `.agent-loop/features/archive.md`. Archive state is not feature lifecycle; active / blocked / paused features stay flat, and an archived owner must rehydrate before reopened execution. - Keep project-local reusable capabilities in `.agent-loop/skills/INDEX.md` and `.agent-loop/skills//`; only active skills may load, and every invocation remains Human-gated. - Do not write task logs, feature progress, raw requirements, temporary plans, or test transcripts into `AGENTS.md`. - + ## Architecture Snapshot Add only startup-critical architecture boundaries that every future agent must know immediately. If the project has `ARCHITECTURE.md`, this block may use `source:ARCHITECTURE.md` instead. Keep details in `ARCHITECTURE.md`, `.agent-loop/project.md`, or enterprise `.agent-loop/project/*.md`. - + ## Directory Guidance - Directory-level `AGENTS.md` files are for long-lived boundary rules only. @@ -194,7 +200,7 @@ Add only startup-critical architecture boundaries that every future agent must k - Do not create directory-level `AGENTS.md` for ordinary component, utility, temporary, or feature implementation folders. - + ## Project Commands ```bash @@ -204,7 +210,7 @@ Add only startup-critical architecture boundaries that every future agent must k ``` - + ## Project-Specific Hard Constraints Add only stable constraints that every future agent must know at startup. diff --git a/tests/feature_archive_test_support.py b/tests/feature_archive_test_support.py new file mode 100644 index 0000000..2f39bed --- /dev/null +++ b/tests/feature_archive_test_support.py @@ -0,0 +1,91 @@ +from __future__ import annotations + +import hashlib +import json +import os +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path + +from tests.checker_test_support import ROOT + + +@dataclass +class ArchiveWorkspace: + project_root: Path + + @property + def memory_root(self) -> Path: + return self.project_root / ".agent-loop" + + @property + def features_root(self) -> Path: + return self.memory_root / "features" + + def write(self, relative: str, content: str) -> Path: + path = self.project_root / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8", newline="\n") + return path + + def feature( + self, feature_id: str, *, status: str = "closed", close_complete: bool = True + ) -> Path: + root = self.features_root / feature_id + root.mkdir(parents=True, exist_ok=True) + self.write( + f".agent-loop/features/{feature_id}/spec.md", + f"# Feature Spec\n\nStatus: {status}\n", + ) + task_status = "done" if close_complete else "in-progress" + self.write( + f".agent-loop/features/{feature_id}/tasks.md", + f"# Tasks\n\n- Status: {task_status}\n", + ) + close = ( + "## Feature Close Review\n\nDecision: pass\n\n" + "## Drift Check\n\nDecision: no-drift\n\n" + "## Close Record\n\nClosed At: 2026-05-20\nHuman Decision: confirmed\n\n" + f"## Archive Readiness\n\nClosed At: 2026-05-20\nDelivered Summary: completed {feature_id}\n" + "Verification: complete\nFeature Close Review: complete\nDrift: resolved\n" + "Project Memory Impact: none\nOpen Follow-up: none\n" + if close_complete + else "## Close Record\n\nClosed At:\n" + ) + self.write(f".agent-loop/features/{feature_id}/notes.md", f"# Notes\n\n{close}") + self.write( + f".agent-loop/features/{feature_id}/tests.md", + "# Tests\n\nStatus: passing\n", + ) + self.write( + f".agent-loop/features/{feature_id}/plan.md", + "# Plan\n\nStatus: closed\n", + ) + return root + + +def run_archive_command( + script: str, *args: str, env: dict[str, str] | None = None +) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [sys.executable, str(ROOT / "scripts" / script), *map(str, args)], + cwd=str(ROOT), + check=False, + capture_output=True, + text=True, + encoding="utf-8", + env={**os.environ, **(env or {})}, + ) + + +def tree_snapshot(root: Path) -> dict[str, str]: + return { + path.relative_to(root).as_posix(): hashlib.sha256(path.read_bytes()).hexdigest() + for path in sorted(root.rglob("*")) + if path.is_file() + } + + +def json_output(result: subprocess.CompletedProcess[str]) -> dict[str, object]: + return json.loads(result.stdout) diff --git a/tests/test_adr_requirement_model_trace.py b/tests/test_adr_requirement_model_trace.py index 46af5d7..db77c2e 100644 --- a/tests/test_adr_requirement_model_trace.py +++ b/tests/test_adr_requirement_model_trace.py @@ -66,6 +66,57 @@ def assert_rejected( self.assertEqual(result.returncode, 1, combined_output(result)) self.assertIn(expected, combined_output(result)) + def run_archived_feature_owner( + self, + *, + include_index: bool = True, + row_month: str = "2026-05", + row_state: str = "archived", + row_path: str | None = None, + planned: bool = False, + ): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) / "fixture" + shutil.copytree(VALID, root) + feature_id = "2026-05-08-login" + archived = root / "features" / "2026-05" / feature_id + archived.parent.mkdir(parents=True) + (root / "features" / "fixture").rename(archived) + spec = archived / "spec.md" + spec.write_text( + re.sub( + r"^Status: .*$", + "Status: closed", + spec.read_text(encoding="utf-8"), + flags=re.MULTILINE, + ), + encoding="utf-8", + ) + owner = f"features/2026-05/{feature_id}/spec.md" + if planned: + owner = f"planned:{owner}" + decision = self.decision.replace("features/fixture/spec.md", owner) + (root / "README.md").write_text(self.readme, encoding="utf-8") + (root / "requirement.md").write_text(self.source, encoding="utf-8") + (root / "decision.md").write_text(decision, encoding="utf-8") + if include_index: + current_path = row_path or f".agent-loop/features/{row_month}/{feature_id}/" + (root / "features" / "archive.md").write_text( + "# Feature Archive\n\n" + "This file locates archived or rehydrated features. Feature specs, tests, notes, requirement sources, and accepted decisions remain authoritative.\n\n" + "| Feature ID | Month | Current Path | Archive State | Closed At | Delivered Summary | Source Requirements | Applicable Decisions | Last Moved At |\n" + "|---|---|---|---|---|---|---|---|---|\n" + f"| {feature_id} | {row_month} | `{current_path}` | {row_state} | 2026-05-20 | completed login | none | none | 2026-07-14 |\n", + encoding="utf-8", + ) + return run_checker( + SCRIPT, + str(root / "README.md"), + str(root / "requirement.md"), + str(root / "decision.md"), + str(root), + ) + def test_accepted_and_proposed_valid_decisions_pass(self) -> None: self.assert_accepted( self.readme, @@ -130,6 +181,34 @@ def test_owner_paths_support_existing_and_explicitly_planned_artifacts(self) -> self.readme, self.source, delegated, "technical landing trace covers 7" ) + def test_archived_closed_feature_spec_owner_passes_with_matching_locator(self) -> None: + result = self.run_archived_feature_owner() + self.assertEqual(result.returncode, 0, combined_output(result)) + self.assertIn("technical landing trace covers 8", result.stdout) + + def test_archived_feature_spec_owner_requires_archive_locator(self) -> None: + result = self.run_archived_feature_owner(include_index=False) + self.assertEqual(result.returncode, 1, combined_output(result)) + self.assertIn("archive-index", combined_output(result)) + + def test_archived_feature_spec_owner_rejects_mismatched_month_locator(self) -> None: + result = self.run_archived_feature_owner( + row_month="2026-06", + row_path=".agent-loop/features/2026-06/2026-05-08-login/", + ) + self.assertEqual(result.returncode, 1, combined_output(result)) + self.assertIn("month", combined_output(result)) + + def test_archived_feature_spec_owner_rejects_rehydrated_month_locator(self) -> None: + result = self.run_archived_feature_owner(row_state="rehydrated") + self.assertEqual(result.returncode, 1, combined_output(result)) + self.assertIn("archive-index", combined_output(result)) + + def test_planned_feature_spec_owner_must_remain_flat(self) -> None: + result = self.run_archived_feature_owner(planned=True) + self.assertEqual(result.returncode, 1, combined_output(result)) + self.assertIn("planned Feature Spec path must be flat", combined_output(result)) + def test_adversarial_contracts_are_rejected(self) -> None: cases = ( ( diff --git a/tests/test_feature_archive_support.py b/tests/test_feature_archive_support.py new file mode 100644 index 0000000..8e4812e --- /dev/null +++ b/tests/test_feature_archive_support.py @@ -0,0 +1,289 @@ +from __future__ import annotations + +import importlib +import sys +import tempfile +import unittest +from pathlib import Path +from unittest import mock + +from tests.checker_test_support import ROOT +from tests.feature_archive_test_support import ArchiveWorkspace, tree_snapshot + + +ARCHIVE_HEADER = """# Feature Archive + +This file locates archived or rehydrated features. Feature specs, tests, notes, requirement sources, and accepted decisions remain authoritative. + +| Feature ID | Month | Current Path | Archive State | Closed At | Delivered Summary | Source Requirements | Applicable Decisions | Last Moved At | +|---|---|---|---|---|---|---|---|---| +""" + + +def archive_row( + feature_id: str, + *, + month: str = "2026-05", + path: str | None = None, + state: str = "archived", + summary: str | None = None, +) -> str: + current = path or f".agent-loop/features/{month}/{feature_id}/" + delivered = summary or f"completed {feature_id}" + return ( + f"| {feature_id} | {month} | `{current}` | {state} | 2026-05-20 | " + f"{delivered} | none | none | 2026-07-14 |\n" + ) + + +class FeatureArchiveSupportTests(unittest.TestCase): + def checker_support(self): + scripts_dir = str(ROOT / "scripts") + if scripts_dir not in sys.path: + sys.path.insert(0, scripts_dir) + module = importlib.import_module("checker_support") + for name in ( + "atomic_write_bytes", + "canonical_json_bytes", + "sha256_bytes", + "strip_code_span", + ): + self.assertTrue(hasattr(module, name), f"missing production helper: {name}") + return module + + def support(self): + support_path = ROOT / "scripts/feature_archive_support.py" + self.assertTrue(support_path.is_file(), f"missing production support: {support_path}") + scripts_dir = str(ROOT / "scripts") + if scripts_dir not in sys.path: + sys.path.insert(0, scripts_dir) + return importlib.import_module("feature_archive_support") + + def test_archive_template_exists_with_exact_empty_table(self) -> None: + template = ROOT / "templates/feature-archive.md" + self.assertTrue(template.is_file(), f"missing archive template: {template}") + self.assertEqual(template.read_text(encoding="utf-8"), ARCHIVE_HEADER) + + def test_canonical_helpers_use_stable_utf8_bytes(self) -> None: + checker_support = self.checker_support() + expected = '{"a":"登录","z":1}'.encode("utf-8") + self.assertEqual( + checker_support.canonical_json_bytes({"z": 1, "a": "登录"}), expected + ) + self.assertEqual( + checker_support.sha256_bytes(expected), + "c92ecc89f697b155c8d5fb7417e18732f61b69c1375d6811c329822bf34f3500", + ) + self.assertEqual(checker_support.strip_code_span(" `value` "), "value") + + def test_atomic_write_bytes_replaces_existing_file(self) -> None: + checker_support = self.checker_support() + with tempfile.TemporaryDirectory() as temp: + target = Path(temp) / "nested" / "artifact.md" + target.parent.mkdir() + target.write_bytes(b"before") + checker_support.atomic_write_bytes(target, "完成".encode("utf-8")) + self.assertEqual(target.read_bytes(), "完成".encode("utf-8")) + self.assertEqual(list(target.parent.glob(f".{target.name}.*")), []) + + def test_atomic_write_bytes_cleans_temporary_file_after_replace_failure(self) -> None: + checker_support = self.checker_support() + with tempfile.TemporaryDirectory() as temp: + target = Path(temp) / "artifact.md" + with mock.patch.object( + checker_support.os, "replace", side_effect=OSError("injected replace failure") + ): + with self.assertRaisesRegex(OSError, "injected replace failure"): + checker_support.atomic_write_bytes(target, b"new") + self.assertFalse(target.exists()) + self.assertEqual(list(target.parent.glob(f".{target.name}.*")), []) + + def test_flat_feature_resolves_without_archive_index(self) -> None: + support = self.support() + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + workspace.feature("2026-05-08-login") + self.assertEqual( + support.resolve_feature_location( + workspace.memory_root, "2026-05-08-login" + ), + support.FeatureLocation( + "2026-05-08-login", + "features/2026-05-08-login", + "flat", + None, + ), + ) + + def test_archived_feature_requires_matching_unique_index_row(self) -> None: + support = self.support() + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + source = workspace.feature("2026-05-08-login") + target = workspace.features_root / "2026-05" / source.name + target.parent.mkdir(parents=True) + source.rename(target) + index = workspace.write( + ".agent-loop/features/archive.md", + ARCHIVE_HEADER + archive_row(source.name), + ) + self.assertEqual( + support.resolve_feature_location(workspace.memory_root, source.name), + support.FeatureLocation( + source.name, + f"features/2026-05/{source.name}", + "archived", + "2026-05", + ), + ) + + index.unlink() + with self.assertRaisesRegex(support.ArchiveContractError, "archive-index"): + support.resolve_feature_location(workspace.memory_root, source.name) + + index.write_text( + ARCHIVE_HEADER + archive_row(source.name) + archive_row(source.name), + encoding="utf-8", + newline="\n", + ) + with self.assertRaisesRegex(support.ArchiveContractError, "archive-index"): + support.resolve_feature_location(workspace.memory_root, source.name) + + def test_flat_and_archived_collision_fails_closed(self) -> None: + support = self.support() + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + workspace.feature("2026-05-08-login") + archived = workspace.features_root / "2026-05" / "2026-05-08-login" + archived.mkdir(parents=True) + workspace.write( + ".agent-loop/features/archive.md", + ARCHIVE_HEADER + archive_row("2026-05-08-login"), + ) + with self.assertRaisesRegex(support.ArchiveContractError, "path-collision"): + support.resolve_feature_location( + workspace.memory_root, "2026-05-08-login" + ) + + def test_month_must_match_feature_id(self) -> None: + support = self.support() + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + archived = workspace.features_root / "2026-06" / "2026-05-08-login" + archived.mkdir(parents=True) + workspace.write( + ".agent-loop/features/archive.md", + ARCHIVE_HEADER + + archive_row( + "2026-05-08-login", + month="2026-06", + path=".agent-loop/features/2026-06/2026-05-08-login/", + ), + ) + with self.assertRaisesRegex(support.ArchiveContractError, "month"): + support.resolve_feature_location( + workspace.memory_root, "2026-05-08-login" + ) + + def test_archive_index_rejects_case_and_unicode_path_differences(self) -> None: + support = self.support() + for changed in ( + "2026-05-08-LOGIN", + "2026-05-08-login", + ): + with self.subTest(changed=changed), tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + workspace.write( + ".agent-loop/features/archive.md", + ARCHIVE_HEADER + + archive_row( + "2026-05-08-login", + path=f".agent-loop/features/2026-05/{changed}/", + ), + ) + with self.assertRaises(support.ArchiveContractError): + support.parse_archive_index(workspace.memory_root) + + def test_archive_index_accepts_bom_and_crlf(self) -> None: + support = self.support() + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + feature_id = "2026-05-08-login" + archived = workspace.features_root / "2026-05" / feature_id + archived.mkdir(parents=True) + index = workspace.features_root / "archive.md" + index.parent.mkdir(parents=True, exist_ok=True) + content = ARCHIVE_HEADER + archive_row( + feature_id, summary="完成登录与失败路径验证" + ) + index.write_bytes(b"\xef\xbb\xbf" + content.replace("\n", "\r\n").encode("utf-8")) + entries = support.parse_archive_index(workspace.memory_root) + self.assertEqual(len(entries), 1) + self.assertEqual(entries[0].delivered_summary, "完成登录与失败路径验证") + + def test_symlink_escape_is_rejected(self) -> None: + support = self.support() + with tempfile.TemporaryDirectory() as temp, tempfile.TemporaryDirectory() as outside: + workspace = ArchiveWorkspace(Path(temp)) + external = Path(outside) / "2026-05-08-login" + external.mkdir() + archived = workspace.features_root / "2026-05" / external.name + archived.parent.mkdir(parents=True) + try: + archived.symlink_to(external, target_is_directory=True) + except OSError as error: + self.skipTest(f"directory symlink unavailable: {error}") + workspace.write( + ".agent-loop/features/archive.md", + ARCHIVE_HEADER + archive_row(external.name), + ) + with self.assertRaisesRegex(support.ArchiveContractError, "path-escape"): + support.resolve_feature_location(workspace.memory_root, external.name) + + def test_plan_hash_is_stable_across_absolute_roots(self) -> None: + support = self.support() + + def make_plan(root: Path): + workspace = ArchiveWorkspace(root) + workspace.feature("2026-05-08-login") + candidate = support.ArchiveCandidate( + "2026-05-08-login", + "2026-05", + "features/2026-05-08-login", + "closed", + "complete", + "none", + "completed login", + "none", + "none", + (), + ) + move = support.Move( + "2026-05-08-login", + "2026-05", + ".agent-loop/features/2026-05-08-login", + ".agent-loop/features/2026-05/2026-05-08-login", + ) + return support.ArchivePlan( + 1, + "archive", + "2026-07-14", + ("2026-05",), + (), + (candidate,), + (move,), + (), + (), + (), + tree_snapshot(root), + ) + + with tempfile.TemporaryDirectory() as first, tempfile.TemporaryDirectory() as second: + self.assertEqual( + make_plan(Path(first)).computed_sha256(), + make_plan(Path(second)).computed_sha256(), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_feature_monthly_archive_apply.py b/tests/test_feature_monthly_archive_apply.py new file mode 100644 index 0000000..69819ff --- /dev/null +++ b/tests/test_feature_monthly_archive_apply.py @@ -0,0 +1,457 @@ +from __future__ import annotations + +import hashlib +import json +import sys +import tempfile +import unittest +from pathlib import Path + +from tests.feature_archive_test_support import ( + ArchiveWorkspace, + json_output, + run_archive_command, + tree_snapshot, +) + + +class FeatureMonthlyArchiveCheckTests(unittest.TestCase): + def scanned_plan( + self, + workspace: ArchiveWorkspace, + *, + operation: str = "archive", + months: tuple[str, ...] = ("2026-05",), + feature_ids: tuple[str, ...] = (), + ) -> dict[str, object]: + selection: list[str] = [] + for month in months: + selection.extend(("--month", month)) + for feature_id in feature_ids: + selection.extend(("--feature-id", feature_id)) + result = run_archive_command( + "scan-feature-monthly-archive.py", + "--project-root", + str(workspace.project_root), + "--operation", + operation, + *selection, + "--as-of", + "2026-07-14", + ) + self.assertEqual(result.returncode, 0, result.stderr) + return json_output(result) + + def apply( + self, + workspace: ArchiveWorkspace, + payload: dict[str, object], + *, + operation: str = "archive", + months: tuple[str, ...] = ("2026-05",), + feature_ids: tuple[str, ...] = (), + expected_hash: str | None = None, + env: dict[str, str] | None = None, + ): + selection: list[str] = [] + for month in months: + selection.extend(("--month", month)) + for feature_id in feature_ids: + selection.extend(("--feature-id", feature_id)) + return run_archive_command( + "apply-feature-monthly-archive.py", + "--project-root", + str(workspace.project_root), + "--operation", + operation, + *selection, + "--as-of", + "2026-07-14", + "--expected-plan-sha256", + expected_hash or str(payload["plan_sha256"]), + env=env, + ) + + def write_plan( + self, workspace: ArchiveWorkspace, payload: dict[str, object] + ) -> Path: + path = workspace.project_root / "archive-plan.json" + path.write_text( + json.dumps(payload, ensure_ascii=False, sort_keys=True, indent=2) + "\n", + encoding="utf-8", + newline="\n", + ) + return path + + def check(self, workspace: ArchiveWorkspace, plan: Path): + return run_archive_command( + "check-feature-monthly-archive.py", + "--project-root", + str(workspace.project_root), + "--operation", + "archive", + "--plan", + str(plan), + ) + + def test_pre_check_is_read_only_and_accepts_current_plan(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + workspace.feature("2026-05-08-login") + plan = self.write_plan(workspace, self.scanned_plan(workspace)) + before = tree_snapshot(workspace.project_root) + result = self.check(workspace, plan) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("PASS", result.stdout) + self.assertEqual(tree_snapshot(workspace.project_root), before) + + def test_pre_check_rejects_snapshot_drift_without_mutation(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + feature = workspace.feature("2026-05-08-login") + plan = self.write_plan(workspace, self.scanned_plan(workspace)) + (feature / "notes.md").write_text( + (feature / "notes.md").read_text(encoding="utf-8") + "\nchanged\n", + encoding="utf-8", + newline="\n", + ) + before = tree_snapshot(workspace.project_root) + result = self.check(workspace, plan) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("stale-plan", result.stdout + result.stderr) + self.assertEqual(tree_snapshot(workspace.project_root), before) + + def test_check_rejects_self_hashed_plan_path_escape_without_mutation(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + workspace.feature("2026-05-08-login") + payload = self.scanned_plan(workspace) + payload["moves"][0]["source"] = "../escaped-feature" + unsigned = dict(payload) + unsigned.pop("plan_sha256") + payload["plan_sha256"] = hashlib.sha256( + json.dumps( + unsigned, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + ).encode("utf-8") + ).hexdigest() + plan = self.write_plan(workspace, payload) + before = tree_snapshot(workspace.project_root) + result = self.check(workspace, plan) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("path-escape", result.stdout + result.stderr) + self.assertEqual(tree_snapshot(workspace.project_root), before) + + def test_post_check_accepts_exact_moves_index_and_reference_edits(self) -> None: + scripts_dir = str(Path(__file__).resolve().parents[1] / "scripts") + if scripts_dir not in sys.path: + sys.path.insert(0, scripts_dir) + from feature_archive_support import ArchiveEntry, render_archive_index + + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + feature_id = "2026-05-08-login" + workspace.feature(feature_id) + workspace.write( + ".agent-loop/project.md", + f"# Project\n\nDelivered: `.agent-loop/features/{feature_id}/spec.md`\n", + ) + payload = self.scanned_plan(workspace) + plan = self.write_plan(workspace, payload) + for move in payload["moves"]: + source = workspace.project_root / move["source"] + target = workspace.project_root / move["target"] + target.parent.mkdir(parents=True, exist_ok=True) + source.rename(target) + for edit in payload["reference_edits"]: + target = workspace.project_root / edit["path"] + content = target.read_text(encoding="utf-8") + self.assertEqual(content.count(edit["old"]), edit["occurrences"]) + target.write_text( + content.replace(edit["old"], edit["new"]), + encoding="utf-8", + newline="\n", + ) + entries = [ArchiveEntry(**entry) for entry in payload["archive_entries"]] + workspace.write( + ".agent-loop/features/archive.md", render_archive_index(entries) + ) + before = tree_snapshot(workspace.project_root) + result = self.check(workspace, plan) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("PASS", result.stdout) + self.assertEqual(tree_snapshot(workspace.project_root), before) + + def test_apply_moves_two_closed_features_intact_and_updates_archive_index(self) -> None: + scripts_dir = str(Path(__file__).resolve().parents[1] / "scripts") + if scripts_dir not in sys.path: + sys.path.insert(0, scripts_dir) + from feature_archive_support import parse_archive_index + + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + first = workspace.feature("2026-05-08-login") + second = workspace.feature("2026-06-12-payment") + (first / "payload.bin").write_bytes(b"\x00login\xff") + (second / "payload.bin").write_bytes(b"\x00payment\xff") + first_before = tree_snapshot(first) + second_before = tree_snapshot(second) + payload = self.scanned_plan( + workspace, months=("2026-05", "2026-06") + ) + result = self.apply( + workspace, payload, months=("2026-05", "2026-06") + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertFalse(first.exists()) + self.assertFalse(second.exists()) + first_target = workspace.features_root / "2026-05" / first.name + second_target = workspace.features_root / "2026-06" / second.name + self.assertEqual(tree_snapshot(first_target), first_before) + self.assertEqual(tree_snapshot(second_target), second_before) + entries = parse_archive_index(workspace.memory_root) + self.assertEqual( + [entry.feature_id for entry in entries], + ["2026-05-08-login", "2026-06-12-payment"], + ) + + def test_apply_rejects_valid_but_different_plan_hash_without_writes(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + workspace.feature("2026-05-08-login") + payload = self.scanned_plan(workspace) + before = tree_snapshot(workspace.project_root) + wrong = ("0" if payload["plan_sha256"][0] != "0" else "1") + payload[ + "plan_sha256" + ][1:] + result = self.apply(workspace, payload, expected_hash=wrong) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("stale-plan", result.stdout + result.stderr) + self.assertEqual(tree_snapshot(workspace.project_root), before) + + def test_apply_rejects_state_drift_after_scan(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + feature = workspace.feature("2026-05-08-login") + payload = self.scanned_plan(workspace) + notes = feature / "notes.md" + notes.write_text( + notes.read_text(encoding="utf-8") + "\nstate drift\n", + encoding="utf-8", + newline="\n", + ) + before = tree_snapshot(workspace.project_root) + result = self.apply(workspace, payload) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("stale-plan", result.stdout + result.stderr) + self.assertEqual(tree_snapshot(workspace.project_root), before) + self.assertFalse( + (workspace.features_root / ".archive-txn").exists() + ) + + def test_apply_rejects_stranded_transaction_created_after_scan(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + feature = workspace.feature("2026-05-08-login") + payload = self.scanned_plan(workspace) + transaction_id = "20260714T120000Z-0123456789ab" + transaction = workspace.features_root / ".archive-txn" / transaction_id + transaction.mkdir(parents=True) + before = tree_snapshot(workspace.project_root) + + result = self.apply(workspace, payload) + + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("stranded-transaction", result.stdout + result.stderr) + self.assertIn(transaction_id, result.stdout + result.stderr) + self.assertEqual(tree_snapshot(workspace.project_root), before) + self.assertTrue(feature.is_dir()) + + def test_apply_updates_relative_link_from_project_memory_into_feature(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + feature_id = "2026-05-08-login" + workspace.feature(feature_id) + project = workspace.write( + ".agent-loop/project.md", + f"# Project\n\n[Feature](features/{feature_id}/spec.md)\n", + ) + payload = self.scanned_plan(workspace) + + result = self.apply(workspace, payload) + + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn( + f"features/2026-05/{feature_id}/spec.md", + project.read_text(encoding="utf-8"), + ) + self.assertTrue( + (workspace.features_root / "2026-05" / feature_id / "spec.md").is_file() + ) + + def test_apply_updates_only_precomputed_reference_edits(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + feature_id = "2026-05-08-login" + workspace.feature(feature_id) + old = f".agent-loop/features/{feature_id}/spec.md" + new = f".agent-loop/features/2026-05/{feature_id}/spec.md" + for relative in ( + ".agent-loop/project.md", + ".agent-loop/requirements/2026-05-login/README.md", + ".agent-loop/decisions/ADR-001-login.md", + ): + workspace.write(relative, f"# Locator\n\nOwner: `{old}`\n") + requirement = workspace.write( + ".agent-loop/requirements/2026-05-login/requirement.md", + f"# Original Requirement\n\nOriginal path: `{old}`\n", + ) + unrelated = workspace.write("docs/unrelated.md", "# Unrelated\n\nkeep bytes\n") + requirement_before = requirement.read_bytes() + unrelated_before = unrelated.read_bytes() + payload = self.scanned_plan(workspace) + result = self.apply(workspace, payload) + self.assertEqual(result.returncode, 0, result.stderr) + for relative in ( + ".agent-loop/project.md", + ".agent-loop/requirements/2026-05-login/README.md", + ".agent-loop/decisions/ADR-001-login.md", + ): + content = (workspace.project_root / relative).read_text(encoding="utf-8") + self.assertIn(new, content) + self.assertNotIn(old, content) + self.assertEqual(requirement.read_bytes(), requirement_before) + self.assertEqual(unrelated.read_bytes(), unrelated_before) + + def test_apply_preserves_bom_and_crlf_while_updating_reference(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + feature_id = "2026-05-08-login" + workspace.feature(feature_id) + old = f".agent-loop/features/{feature_id}/spec.md" + new = f".agent-loop/features/2026-05/{feature_id}/spec.md" + project = workspace.memory_root / "project.md" + project.parent.mkdir(parents=True, exist_ok=True) + project.write_bytes( + b"\xef\xbb\xbf" + + f"# Project\r\n\r\nOwner: `{old}`\r\n".encode("utf-8") + ) + payload = self.scanned_plan(workspace) + result = self.apply(workspace, payload) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + updated = project.read_bytes() + self.assertTrue(updated.startswith(b"\xef\xbb\xbf")) + self.assertIn(new.encode("utf-8"), updated) + self.assertNotIn(old.encode("utf-8"), updated) + self.assertIn(b"\r\n", updated) + self.assertNotIn(b"\n", updated.replace(b"\r\n", b"")) + + def test_apply_is_idempotent_and_never_nests_month_directory(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + feature_id = "2026-05-08-login" + workspace.feature(feature_id) + first_plan = self.scanned_plan(workspace) + first = self.apply(workspace, first_plan) + self.assertEqual(first.returncode, 0, first.stderr) + second_plan = self.scanned_plan(workspace) + self.assertEqual(second_plan["moves"], []) + self.assertIn( + "already-archived", second_plan["candidates"][0]["blockers"] + ) + before = tree_snapshot(workspace.project_root) + second = self.apply(workspace, second_plan) + self.assertEqual(second.returncode, 0, second.stderr) + self.assertEqual(tree_snapshot(workspace.project_root), before) + self.assertFalse( + ( + workspace.features_root + / "2026-05" + / "2026-05" + / feature_id + ).exists() + ) + + def test_injected_reference_write_failure_restores_directories_and_bytes(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + feature_id = "2026-05-08-login" + workspace.feature(feature_id) + workspace.write( + ".agent-loop/project.md", + f"# Project\n\nOwner: `.agent-loop/features/{feature_id}/spec.md`\n", + ) + payload = self.scanned_plan(workspace) + before = tree_snapshot(workspace.project_root) + result = self.apply( + workspace, + payload, + env={ + "AGENT_LOOP_ARCHIVE_TEST_MODE": "1", + "AGENT_LOOP_ARCHIVE_FAIL_AFTER": "3", + }, + ) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("restore=complete", result.stdout + result.stderr) + self.assertEqual(tree_snapshot(workspace.project_root), before) + self.assertFalse((workspace.features_root / ".archive-txn").exists()) + + def test_failure_injection_is_rejected_outside_explicit_test_mode(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + workspace.feature("2026-05-08-login") + payload = self.scanned_plan(workspace) + before = tree_snapshot(workspace.project_root) + result = self.apply( + workspace, + payload, + env={"AGENT_LOOP_ARCHIVE_FAIL_AFTER": "1"}, + ) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("test-only", result.stdout + result.stderr) + self.assertEqual(tree_snapshot(workspace.project_root), before) + + def test_rehydrate_moves_archived_feature_flat_and_updates_locator(self) -> None: + scripts_dir = str(Path(__file__).resolve().parents[1] / "scripts") + if scripts_dir not in sys.path: + sys.path.insert(0, scripts_dir) + from feature_archive_support import parse_archive_index + + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + feature_id = "2026-05-08-login" + workspace.feature(feature_id) + archive_plan = self.scanned_plan(workspace) + archived = self.apply(workspace, archive_plan) + self.assertEqual(archived.returncode, 0, archived.stderr) + rehydrate_plan = self.scanned_plan( + workspace, + operation="rehydrate", + months=(), + feature_ids=(feature_id,), + ) + result = self.apply( + workspace, + rehydrate_plan, + operation="rehydrate", + months=(), + feature_ids=(feature_id,), + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertFalse( + (workspace.features_root / "2026-05" / feature_id).exists() + ) + flat = workspace.features_root / feature_id + self.assertTrue(flat.is_dir()) + self.assertIn("Status: closed", (flat / "spec.md").read_text(encoding="utf-8")) + entry = parse_archive_index(workspace.memory_root)[0] + self.assertEqual(entry.archive_state, "rehydrated") + self.assertEqual( + entry.current_path, f".agent-loop/features/{feature_id}/" + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_feature_monthly_archive_restore.py b/tests/test_feature_monthly_archive_restore.py new file mode 100644 index 0000000..381a238 --- /dev/null +++ b/tests/test_feature_monthly_archive_restore.py @@ -0,0 +1,366 @@ +from __future__ import annotations + +import hashlib +import json +import shutil +import tempfile +import unittest +from pathlib import Path + +from tests.feature_archive_test_support import ( + ArchiveWorkspace, + json_output, + run_archive_command, + tree_snapshot, +) + + +TRANSACTION_ID = "20260714T120000Z-0123456789ab" + + +class FeatureMonthlyArchiveRestoreTests(unittest.TestCase): + def test_restore_check_accepts_exact_pretransaction_state_without_mutation( + self, + ) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + workspace.feature("2026-05-08-login") + scan = run_archive_command( + "scan-feature-monthly-archive.py", + "--project-root", + str(workspace.project_root), + "--operation", + "archive", + "--month", + "2026-05", + "--as-of", + "2026-07-14", + ) + self.assertEqual(scan.returncode, 0, scan.stderr) + plan = workspace.project_root / "archive-plan.json" + plan.write_text( + json.dumps(json_output(scan), ensure_ascii=False, sort_keys=True, indent=2) + + "\n", + encoding="utf-8", + newline="\n", + ) + before = tree_snapshot(workspace.project_root) + result = run_archive_command( + "check-feature-monthly-archive.py", + "--project-root", + str(workspace.project_root), + "--operation", + "restore", + "--plan", + str(plan), + ) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("restore-check", result.stdout) + self.assertEqual(tree_snapshot(workspace.project_root), before) + + def interrupted_transaction( + self, workspace: ArchiveWorkspace, feature_id: str + ) -> tuple[Path, dict[str, str]]: + source = workspace.features_root / feature_id + if not source.is_dir(): + source = workspace.feature(feature_id) + before = tree_snapshot(workspace.project_root) + scan = run_archive_command( + "scan-feature-monthly-archive.py", + "--project-root", + str(workspace.project_root), + "--operation", + "archive", + "--month", + "2026-05", + "--as-of", + "2026-07-14", + ) + self.assertEqual(scan.returncode, 0, scan.stderr) + plan = json_output(scan) + transaction = workspace.features_root / ".archive-txn" / TRANSACTION_ID + transaction.mkdir(parents=True) + index_relative = ".agent-loop/features/archive.md" + backup_paths = sorted( + {str(item["path"]) for item in plan["reference_edits"]} + | {index_relative} + ) + backups = [] + for relative in backup_paths: + path = workspace.project_root / relative + if path.is_file(): + content = path.read_bytes() + backup_relative = f"backups/{relative}" + backup = transaction / backup_relative + backup.parent.mkdir(parents=True, exist_ok=True) + backup.write_bytes(content) + backups.append( + { + "path": relative, + "state": "existing", + "backup": backup_relative, + "sha256": hashlib.sha256(content).hexdigest(), + } + ) + else: + backups.append({"path": relative, "state": "missing-before"}) + target = workspace.features_root / "2026-05" / feature_id + target.parent.mkdir(parents=True) + source.rename(target) + journal = { + "schema_version": 1, + "transaction_id": TRANSACTION_ID, + "operation": "archive", + "plan_sha256": plan["plan_sha256"], + "plan": plan, + "state": "moving", + "moves": plan["moves"], + "backups": backups, + "completed_operations": [ + { + "kind": "move", + "source": f".agent-loop/features/{feature_id}", + "target": f".agent-loop/features/2026-05/{feature_id}", + } + ], + "snapshots": plan["snapshots"], + "created_directories": [".agent-loop/features/2026-05"], + } + (transaction / "journal.json").write_text( + json.dumps(journal, ensure_ascii=False, sort_keys=True, indent=2) + "\n", + encoding="utf-8", + newline="\n", + ) + return transaction, before + + def restore(self, workspace: ArchiveWorkspace): + return run_archive_command( + "restore-feature-monthly-archive.py", + "--project-root", + str(workspace.project_root), + "--transaction-id", + TRANSACTION_ID, + ) + + def test_interrupted_transaction_can_be_restored_by_new_process(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + transaction, before = self.interrupted_transaction( + workspace, "2026-05-08-login" + ) + result = self.restore(workspace) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(tree_snapshot(workspace.project_root), before) + self.assertFalse(transaction.exists()) + + def test_restore_recovers_move_completed_before_journal_record(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + transaction, before = self.interrupted_transaction( + workspace, "2026-05-08-login" + ) + journal_path = transaction / "journal.json" + journal = json.loads(journal_path.read_text(encoding="utf-8")) + journal["completed_operations"] = [] + journal_path.write_text( + json.dumps(journal, ensure_ascii=False, sort_keys=True, indent=2) + "\n", + encoding="utf-8", + newline="\n", + ) + result = self.restore(workspace) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(tree_snapshot(workspace.project_root), before) + self.assertFalse(transaction.exists()) + + def test_incomplete_restore_keeps_journal_and_fails_closed(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + transaction, _ = self.interrupted_transaction( + workspace, "2026-05-08-login" + ) + source_collision = workspace.features_root / "2026-05-08-login" + source_collision.mkdir() + (source_collision / "collision.md").write_text( + "collision", encoding="utf-8" + ) + result = self.restore(workspace) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("restore", result.stdout + result.stderr) + self.assertTrue(transaction.exists()) + journal = json.loads((transaction / "journal.json").read_text(encoding="utf-8")) + self.assertEqual(journal["state"], "restoring") + + def test_restore_rejects_journal_path_escape_without_moving_feature(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + feature_id = "2026-05-08-login" + transaction, _ = self.interrupted_transaction(workspace, feature_id) + target = workspace.features_root / "2026-05" / feature_id + escaped = workspace.project_root.parent / ( + workspace.project_root.name + "-escaped-feature" + ) + source_relative = f"../{escaped.name}" + target_relative = f".agent-loop/features/2026-05/{feature_id}" + journal_path = transaction / "journal.json" + journal = json.loads(journal_path.read_text(encoding="utf-8")) + journal["plan"]["moves"][0]["source"] = source_relative + payload_without_hash = { + key: value + for key, value in journal["plan"].items() + if key != "plan_sha256" + } + plan_sha256 = hashlib.sha256( + json.dumps( + payload_without_hash, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + ).encode("utf-8") + ).hexdigest() + journal["plan"]["plan_sha256"] = plan_sha256 + journal["plan_sha256"] = plan_sha256 + journal["moves"][0]["source"] = source_relative + journal["completed_operations"][0]["source"] = source_relative + journal_path.write_text( + json.dumps(journal, ensure_ascii=False, sort_keys=True, indent=2) + "\n", + encoding="utf-8", + newline="\n", + ) + try: + result = self.restore(workspace) + escaped_created = escaped.exists() + finally: + if escaped.exists(): + shutil.rmtree(escaped) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("path-escape", result.stdout + result.stderr) + self.assertFalse(escaped_created) + self.assertTrue(target.is_dir()) + self.assertTrue(transaction.is_dir()) + + def test_restore_rejects_tampered_backup_scope_without_deleting_file(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + victim = workspace.write("README.md", "must survive\n") + transaction, _ = self.interrupted_transaction( + workspace, "2026-05-08-login" + ) + journal_path = transaction / "journal.json" + journal = json.loads(journal_path.read_text(encoding="utf-8")) + victim_sha256 = hashlib.sha256(victim.read_bytes()).hexdigest() + journal["plan"]["reference_edits"].append( + { + "path": "README.md", + "kind": "literal-path", + "old": "must survive", + "new": "must survive", + "occurrences": 1, + "before_sha256": victim_sha256, + "after_sha256": victim_sha256, + } + ) + payload_without_hash = { + key: value + for key, value in journal["plan"].items() + if key != "plan_sha256" + } + plan_sha256 = hashlib.sha256( + json.dumps( + payload_without_hash, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + ).encode("utf-8") + ).hexdigest() + journal["plan"]["plan_sha256"] = plan_sha256 + journal["plan_sha256"] = plan_sha256 + journal["backups"].append( + {"path": "README.md", "state": "missing-before"} + ) + journal_path.write_text( + json.dumps(journal, ensure_ascii=False, sort_keys=True, indent=2) + "\n", + encoding="utf-8", + newline="\n", + ) + + result = self.restore(workspace) + + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("journal", result.stdout + result.stderr) + self.assertTrue(victim.is_file()) + self.assertEqual(victim.read_text(encoding="utf-8"), "must survive\n") + self.assertTrue(transaction.is_dir()) + self.assertTrue( + ( + workspace.features_root + / "2026-05" + / "2026-05-08-login" + ).is_dir() + ) + + def test_restore_rejects_post_crash_reference_drift_without_overwrite(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + feature_id = "2026-05-08-login" + workspace.feature(feature_id) + project = workspace.write( + ".agent-loop/project.md", + f"# Project\n\nOwner: `.agent-loop/features/{feature_id}/spec.md`\n", + ) + transaction, _ = self.interrupted_transaction(workspace, feature_id) + project.write_text( + "# Project\n\nHuman edit after crash\n", + encoding="utf-8", + newline="\n", + ) + + result = self.restore(workspace) + + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("drift", result.stdout + result.stderr) + self.assertEqual( + project.read_text(encoding="utf-8"), + "# Project\n\nHuman edit after crash\n", + ) + self.assertTrue(transaction.is_dir()) + self.assertTrue( + (workspace.features_root / "2026-05" / feature_id).is_dir() + ) + + def test_restore_rejects_corrupt_backup_before_moving_feature(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + feature_id = "2026-05-08-login" + workspace.feature(feature_id) + workspace.write( + ".agent-loop/project.md", + f"# Project\n\n[Feature](features/{feature_id}/spec.md)\n", + ) + transaction, _ = self.interrupted_transaction(workspace, feature_id) + backup = transaction / "backups" / ".agent-loop" / "project.md" + backup.write_text("corrupt backup\n", encoding="utf-8", newline="\n") + target = workspace.features_root / "2026-05" / feature_id + source = workspace.features_root / feature_id + + result = self.restore(workspace) + + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("backup", result.stdout + result.stderr) + self.assertTrue(target.is_dir()) + self.assertFalse(source.exists()) + self.assertEqual( + workspace.project_root.joinpath(".agent-loop/project.md").read_text( + encoding="utf-8" + ), + f"# Project\n\n[Feature](features/{feature_id}/spec.md)\n", + ) + self.assertEqual(backup.read_text(encoding="utf-8"), "corrupt backup\n") + journal = json.loads( + (transaction / "journal.json").read_text(encoding="utf-8") + ) + self.assertEqual(journal["state"], "restoring") + self.assertFalse((workspace.features_root / feature_id).exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_feature_monthly_archive_scan.py b/tests/test_feature_monthly_archive_scan.py new file mode 100644 index 0000000..cacca0d --- /dev/null +++ b/tests/test_feature_monthly_archive_scan.py @@ -0,0 +1,561 @@ +from __future__ import annotations + +import tempfile +import unittest +from pathlib import Path + +from tests.feature_archive_test_support import ( + ArchiveWorkspace, + json_output, + run_archive_command, + tree_snapshot, +) +from tests.test_feature_archive_support import ARCHIVE_HEADER, archive_row + + +TRANSACTION_ID = "20260714T120000Z-0123456789ab" + + +class FeatureMonthlyArchiveScanTests(unittest.TestCase): + def scan(self, workspace: ArchiveWorkspace, *args: str): + return run_archive_command( + "scan-feature-monthly-archive.py", + "--project-root", + str(workspace.project_root), + *args, + ) + + def test_scan_selects_closed_features_across_two_months_and_preserves_blocked( + self, + ) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + workspace.feature("2026-05-08-login") + workspace.feature("2026-05-22-import", status="paused") + workspace.feature("2026-06-12-payment") + before = tree_snapshot(workspace.project_root) + result = self.scan( + workspace, + "--operation", + "archive", + "--month", + "2026-05", + "--month", + "2026-06", + "--as-of", + "2026-07-14", + ) + self.assertEqual(result.returncode, 0, result.stderr) + payload = json_output(result) + self.assertEqual( + [move["feature_id"] for move in payload["moves"]], + ["2026-05-08-login", "2026-06-12-payment"], + ) + self.assertIn( + "2026-05-22-import", + [item["feature_id"] for item in payload["candidates"]], + ) + self.assertEqual(tree_snapshot(workspace.project_root), before) + + def test_scan_snapshots_large_feature_payload_for_intact_move_check(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + feature = workspace.feature("2026-05-08-login") + payload_file = feature / "large-payload.bin" + payload_file.write_bytes(b"x" * (2 * 1024 * 1024 + 1)) + result = self.scan( + workspace, + "--operation", + "archive", + "--month", + "2026-05", + "--as-of", + "2026-07-14", + ) + self.assertEqual(result.returncode, 0, result.stderr) + payload = json_output(result) + self.assertIn( + ".agent-loop/features/2026-05-08-login/large-payload.bin", + payload["snapshots"], + ) + + def test_current_month_is_blocked_without_mutation(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + workspace.feature("2026-07-01-current") + before = tree_snapshot(workspace.project_root) + result = self.scan( + workspace, + "--operation", + "archive", + "--month", + "2026-07", + "--as-of", + "2026-07-14", + ) + self.assertEqual(result.returncode, 0, result.stderr) + payload = json_output(result) + self.assertEqual(payload["moves"], []) + self.assertIn("current-month", payload["candidates"][0]["blockers"]) + self.assertEqual(tree_snapshot(workspace.project_root), before) + + def test_active_blocked_and_paused_lifecycles_are_ineligible(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + for day, status in (("08", "active"), ("09", "blocked"), ("10", "paused")): + workspace.feature(f"2026-05-{day}-{status}", status=status) + result = self.scan( + workspace, + "--operation", + "archive", + "--month", + "2026-05", + "--as-of", + "2026-07-14", + ) + self.assertEqual(result.returncode, 0, result.stderr) + payload = json_output(result) + self.assertEqual(payload["moves"], []) + for candidate in payload["candidates"]: + self.assertIn("lifecycle", " ".join(candidate["blockers"])) + + def test_incomplete_close_evidence_is_blocked(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + workspace.feature("2026-05-08-login", close_complete=False) + result = self.scan( + workspace, + "--operation", + "archive", + "--month", + "2026-05", + "--as-of", + "2026-07-14", + ) + self.assertEqual(result.returncode, 0, result.stderr) + payload = json_output(result) + self.assertEqual(payload["moves"], []) + self.assertEqual(payload["candidates"][0]["close_evidence"], "incomplete") + + def test_missing_archive_readiness_is_blocked(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + feature = workspace.feature("2026-05-08-login") + notes = feature / "notes.md" + notes.write_text( + notes.read_text(encoding="utf-8").split("## Archive Readiness", 1)[0], + encoding="utf-8", + newline="\n", + ) + result = self.scan( + workspace, + "--operation", + "archive", + "--month", + "2026-05", + "--as-of", + "2026-07-14", + ) + self.assertEqual(result.returncode, 0, result.stderr) + payload = json_output(result) + self.assertIn( + "missing-archive-readiness", payload["candidates"][0]["blockers"] + ) + + def test_placeholder_delivered_summary_is_blocked(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + feature = workspace.feature("2026-05-08-login") + notes = feature / "notes.md" + notes.write_text( + notes.read_text(encoding="utf-8").replace( + "Delivered Summary: completed 2026-05-08-login", + "Delivered Summary: TODO", + ), + encoding="utf-8", + newline="\n", + ) + result = self.scan( + workspace, + "--operation", + "archive", + "--month", + "2026-05", + "--as-of", + "2026-07-14", + ) + self.assertEqual(result.returncode, 0, result.stderr) + payload = json_output(result) + self.assertIn( + "non-concrete-delivered-summary", + payload["candidates"][0]["blockers"], + ) + + def test_non_terminal_archive_readiness_value_is_blocked(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + feature = workspace.feature("2026-05-08-login") + notes = feature / "notes.md" + notes.write_text( + notes.read_text(encoding="utf-8").replace( + "Verification: complete", "Verification: pending" + ), + encoding="utf-8", + newline="\n", + ) + result = self.scan( + workspace, + "--operation", + "archive", + "--month", + "2026-05", + "--as-of", + "2026-07-14", + ) + self.assertEqual(result.returncode, 0, result.stderr) + payload = json_output(result) + self.assertIn( + "archive-readiness-verification:pending", + payload["candidates"][0]["blockers"], + ) + + def test_open_follow_up_is_blocked(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + feature = workspace.feature("2026-05-08-login") + notes = feature / "notes.md" + notes.write_text( + notes.read_text(encoding="utf-8").replace( + "Open Follow-up: none", "Open Follow-up: FU-17" + ), + encoding="utf-8", + newline="\n", + ) + result = self.scan( + workspace, + "--operation", + "archive", + "--month", + "2026-05", + "--as-of", + "2026-07-14", + ) + self.assertEqual(result.returncode, 0, result.stderr) + payload = json_output(result) + self.assertEqual(payload["moves"], []) + self.assertEqual(payload["candidates"][0]["open_follow_up"], "FU-17") + + def test_flat_and_month_path_collision_fails_closed_without_mutation(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + workspace.feature("2026-05-08-login") + archived = workspace.features_root / "2026-05" / "2026-05-08-login" + archived.mkdir(parents=True) + before = tree_snapshot(workspace.project_root) + result = self.scan( + workspace, + "--operation", + "archive", + "--month", + "2026-05", + "--as-of", + "2026-07-14", + ) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("path-collision", result.stdout + result.stderr) + self.assertEqual(tree_snapshot(workspace.project_root), before) + + def test_project_memory_active_or_paused_feature_is_blocked(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + workspace.feature("2026-05-08-login") + workspace.write( + ".agent-loop/project.md", + "# Project\n\nActive Feature: 2026-05-08-login\nPaused Features: none\n", + ) + result = self.scan( + workspace, + "--operation", + "archive", + "--month", + "2026-05", + "--as-of", + "2026-07-14", + ) + self.assertEqual(result.returncode, 0, result.stderr) + payload = json_output(result) + self.assertIn("project-memory-active", payload["candidates"][0]["blockers"]) + + def test_scan_precomputes_literal_and_relative_link_edits(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + feature_id = "2026-05-08-login" + feature = workspace.feature(feature_id) + workspace.write( + ".agent-loop/project.md", + f"# Project\n\nDelivered: `.agent-loop/features/{feature_id}/spec.md`\n", + ) + workspace.write( + f".agent-loop/features/{feature_id}/links.md", + "# Links\n\n[Project memory](../../project.md)\n" + "[External](https://example.com/docs)\n", + ) + before = tree_snapshot(workspace.project_root) + result = self.scan( + workspace, + "--operation", + "archive", + "--month", + "2026-05", + "--as-of", + "2026-07-14", + ) + self.assertEqual(result.returncode, 0, result.stderr) + payload = json_output(result) + edits = payload["reference_edits"] + self.assertTrue( + any( + edit["kind"] == "literal-path" + and edit["path"] == ".agent-loop/project.md" + and edit["new"] + == f".agent-loop/features/2026-05/{feature_id}/" + for edit in edits + ) + ) + self.assertTrue( + any( + edit["kind"] == "relative-link" + and edit["path"] + == f".agent-loop/features/{feature_id}/links.md" + and edit["old"] == "../../project.md" + and edit["new"] == "../../../project.md" + for edit in edits + ) + ) + self.assertTrue(feature.is_dir()) + self.assertEqual(tree_snapshot(workspace.project_root), before) + + def test_requirement_sources_and_historical_reports_are_preserved(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + feature_id = "2026-05-08-login" + workspace.feature(feature_id) + old_path = f".agent-loop/features/{feature_id}/spec.md" + workspace.write( + ".agent-loop/requirements/2026-05-login/requirement.md", + f"# Human Requirement\n\nOriginal evidence: `{old_path}`\n", + ) + workspace.write( + "docs/reports/2026-05-login-validation.md", + f"# Historical Validation Report\n\nAt execution time: `{old_path}`\n", + ) + before = tree_snapshot(workspace.project_root) + result = self.scan( + workspace, + "--operation", + "archive", + "--month", + "2026-05", + "--as-of", + "2026-07-14", + ) + self.assertEqual(result.returncode, 0, result.stderr) + payload = json_output(result) + classifications = { + item["path"]: item["classification"] + for item in payload["skipped_references"] + } + self.assertEqual( + classifications[ + ".agent-loop/requirements/2026-05-login/requirement.md" + ], + "immutable-requirement-source", + ) + self.assertEqual( + classifications["docs/reports/2026-05-login-validation.md"], + "historical-evidence", + ) + self.assertEqual(tree_snapshot(workspace.project_root), before) + + def test_ambiguous_old_path_reference_blocks_apply(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + feature_id = "2026-05-08-login" + workspace.feature(feature_id) + workspace.write( + "docs/ambiguous.md", + f"# Ambiguous\n\nencoded=.agent-loop/features/{feature_id}%2Fspec.md\n", + ) + result = self.scan( + workspace, + "--operation", + "archive", + "--month", + "2026-05", + "--as-of", + "2026-07-14", + ) + self.assertEqual(result.returncode, 0, result.stderr) + payload = json_output(result) + self.assertTrue( + any( + item["path"] == "docs/ambiguous.md" + and item["classification"] == "unsupported" + for item in payload["skipped_references"] + ) + ) + + def test_large_markdown_with_old_path_is_unsupported_without_reading_it(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + feature_id = "2026-05-08-login" + workspace.feature(feature_id) + large = workspace.project_root / "docs/large.md" + large.parent.mkdir(parents=True) + large.write_bytes( + f".agent-loop/features/{feature_id}/".encode("utf-8") + + b"x" * (2 * 1024 * 1024 + 1) + ) + result = self.scan( + workspace, + "--operation", + "archive", + "--month", + "2026-05", + "--as-of", + "2026-07-14", + ) + self.assertEqual(result.returncode, 0, result.stderr) + payload = json_output(result) + self.assertTrue( + any( + item["path"] == "docs/large.md" + and item["classification"] == "unsupported" + for item in payload["skipped_references"] + ) + ) + + def test_rehydrate_scan_plans_month_to_flat_without_mutation(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + feature_id = "2026-05-08-login" + source = workspace.feature(feature_id) + archived = workspace.features_root / "2026-05" / feature_id + archived.parent.mkdir(parents=True) + source.rename(archived) + workspace.write( + ".agent-loop/features/archive.md", + ARCHIVE_HEADER + archive_row(feature_id), + ) + before = tree_snapshot(workspace.project_root) + result = self.scan( + workspace, + "--operation", + "rehydrate", + "--feature-id", + feature_id, + "--as-of", + "2026-07-14", + ) + self.assertEqual(result.returncode, 0, result.stderr) + payload = json_output(result) + self.assertEqual( + payload["moves"], + [ + { + "feature_id": feature_id, + "month": "2026-05", + "source": f".agent-loop/features/2026-05/{feature_id}", + "target": f".agent-loop/features/{feature_id}", + } + ], + ) + self.assertEqual(tree_snapshot(workspace.project_root), before) + + def test_broken_cross_boundary_relative_link_blocks_apply(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + feature_id = "2026-05-08-login" + workspace.feature(feature_id) + workspace.write( + f".agent-loop/features/{feature_id}/links.md", + "# Links\n\n[missing](../../missing.md)\n", + ) + result = self.scan( + workspace, + "--operation", + "archive", + "--month", + "2026-05", + "--as-of", + "2026-07-14", + ) + self.assertEqual(result.returncode, 0, result.stderr) + payload = json_output(result) + unsupported = [ + item + for item in payload["skipped_references"] + if item["classification"] == "unsupported" + ] + self.assertTrue(unsupported) + self.assertIn("broken", unsupported[0]["reason"]) + + def test_scan_rewrites_relative_link_from_project_memory_into_feature(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + feature_id = "2026-05-08-login" + workspace.feature(feature_id) + workspace.write( + ".agent-loop/project.md", + f"# Project\n\n[Feature](features/{feature_id}/spec.md)\n", + ) + + result = self.scan( + workspace, + "--operation", + "archive", + "--month", + "2026-05", + "--as-of", + "2026-07-14", + ) + + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + payload = json_output(result) + self.assertTrue( + any( + edit["kind"] == "relative-link" + and edit["path"] == ".agent-loop/project.md" + and edit["old"] == f"features/{feature_id}/spec.md" + and edit["new"] + == f"features/2026-05/{feature_id}/spec.md" + for edit in payload["reference_edits"] + ) + ) + + def test_scan_rejects_stranded_archive_transaction(self) -> None: + with tempfile.TemporaryDirectory() as temp: + workspace = ArchiveWorkspace(Path(temp)) + workspace.feature("2026-05-08-login") + transaction = workspace.features_root / ".archive-txn" / TRANSACTION_ID + transaction.mkdir(parents=True) + before = tree_snapshot(workspace.project_root) + + result = self.scan( + workspace, + "--operation", + "archive", + "--month", + "2026-05", + "--as-of", + "2026-07-14", + ) + + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("stranded-transaction", result.stdout + result.stderr) + self.assertIn(TRANSACTION_ID, result.stdout + result.stderr) + self.assertEqual(tree_snapshot(workspace.project_root), before) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_python_checker_contract.py b/tests/test_python_checker_contract.py index d4f3a61..9806421 100644 --- a/tests/test_python_checker_contract.py +++ b/tests/test_python_checker_contract.py @@ -20,6 +20,13 @@ "scripts/check-adr-requirement-model-trace.py", ) +ARCHIVE_COMMANDS = ( + "scripts/scan-feature-monthly-archive.py", + "scripts/check-feature-monthly-archive.py", + "scripts/apply-feature-monthly-archive.py", + "scripts/restore-feature-monthly-archive.py", +) + WORKFLOW = ROOT / ".github/workflows/cross-platform-checkers.yml" COMPATIBILITY_ENTRIES = { @@ -54,8 +61,13 @@ def test_canonical_checker_files_exist(self) -> None: with self.subTest(relative=relative): self.assertTrue((ROOT / relative).is_file(), relative) + def test_archive_command_files_exist(self) -> None: + for relative in ARCHIVE_COMMANDS: + with self.subTest(relative=relative): + self.assertTrue((ROOT / relative).is_file(), relative) + def test_canonical_checkers_use_only_stdlib_and_local_support(self) -> None: - allowed_local = {"checker_support"} + allowed_local = {"checker_support", "feature_archive_support"} for relative in CHECKERS: path = ROOT / relative tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) @@ -68,6 +80,21 @@ def test_canonical_checkers_use_only_stdlib_and_local_support(self) -> None: external = imported - set(sys.stdlib_module_names) - allowed_local self.assertEqual(external, set(), f"{relative}: {sorted(external)}") + def test_archive_commands_use_only_stdlib_and_local_support(self) -> None: + allowed_local = {"checker_support", "feature_archive_support"} + for relative in ARCHIVE_COMMANDS: + path = ROOT / relative + self.assertTrue(path.is_file(), relative) + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + imported: set[str] = set() + for node in ast.walk(tree): + if isinstance(node, ast.Import): + imported.update(alias.name.split(".", 1)[0] for alias in node.names) + elif isinstance(node, ast.ImportFrom) and node.module: + imported.add(node.module.split(".", 1)[0]) + external = imported - set(sys.stdlib_module_names) - allowed_local + self.assertEqual(external, set(), f"{relative}: {sorted(external)}") + def test_missing_arguments_fail_with_usage_exit_two(self) -> None: for relative in CHECKERS: with self.subTest(relative=relative): @@ -75,6 +102,13 @@ def test_missing_arguments_fail_with_usage_exit_two(self) -> None: self.assertEqual(result.returncode, 2, combined_output(result)) self.assertIn("usage", combined_output(result).lower()) + def test_archive_commands_missing_arguments_fail_with_usage_exit_two(self) -> None: + for relative in ARCHIVE_COMMANDS: + with self.subTest(relative=relative): + result = run_checker(relative) + self.assertEqual(result.returncode, 2, combined_output(result)) + self.assertIn("usage", combined_output(result).lower()) + def test_unsupported_python_fails_closed_with_exit_two(self) -> None: scripts_dir = str(ROOT / "scripts") if scripts_dir not in sys.path: @@ -98,6 +132,18 @@ def test_unsupported_python_fails_closed_with_exit_two(self) -> None: } self.assertIn("require_supported_python", calls) + for relative in ARCHIVE_COMMANDS: + with self.subTest(relative=relative): + path = ROOT / relative + self.assertTrue(path.is_file(), relative) + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + calls = { + node.func.id + for node in ast.walk(tree) + if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) + } + self.assertIn("require_supported_python", calls) + def test_cross_platform_ci_runs_the_native_suite(self) -> None: self.assertTrue(WORKFLOW.is_file(), str(WORKFLOW)) content = WORKFLOW.read_text(encoding="utf-8") @@ -111,6 +157,11 @@ def test_cross_platform_ci_runs_the_native_suite(self) -> None: "tests.test_onboarding_core_flow_coverage", "tests.test_concept_foundation_trace", "tests.test_adr_requirement_model_trace", + "tests.test_feature_archive_support", + "tests.test_feature_monthly_archive_scan", + "tests.test_feature_monthly_archive_apply", + "tests.test_feature_monthly_archive_restore", + *ARCHIVE_COMMANDS, ): with self.subTest(required=required): self.assertIn(required, content) diff --git a/tests/test_root_agents_blocks.py b/tests/test_root_agents_blocks.py index b681e90..884ad68 100644 --- a/tests/test_root_agents_blocks.py +++ b/tests/test_root_agents_blocks.py @@ -66,7 +66,7 @@ def test_missing_required_sections_fail(self) -> None: def test_stale_block_version_fails(self) -> None: stale = self.template_text.replace( - "block-version:1.3.0-20260713.2", + "block-version:1.3.0-20260714.1", "block-version:1.3.0", 1, ) @@ -84,7 +84,7 @@ def test_nested_block_fails(self) -> None: nested = ( self.template_text[:line_end] + "\n" + + "source:.agent-loop/project.md block-version:1.3.0-20260714.1 -->\n" + "nested\n\n" + self.template_text[line_end:] ) @@ -94,7 +94,7 @@ def test_duplicate_section_fails(self) -> None: marker = "" duplicate = ( f"{marker}\n\n" + "source:.agent-loop/project.md block-version:1.3.0-20260714.1 -->\n" f"duplicate\n{marker}" ) self.assert_invalid(self.template_text.replace(marker, duplicate, 1), "duplicate-section") diff --git a/tests/validate-feature-monthly-archive-runtime.sh b/tests/validate-feature-monthly-archive-runtime.sh new file mode 100644 index 0000000..cb5f715 --- /dev/null +++ b/tests/validate-feature-monthly-archive-runtime.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +set -euo pipefail + +root=$(cd "$(dirname "$0")/.." && pwd) + +assert_contains() { + local file=$1 + local text=$2 + if ! grep -Fq -- "$text" "$root/$file"; then + printf 'FAIL: %s missing Feature Monthly Archive contract: %s\n' "$file" "$text" >&2 + exit 1 + fi +} + +assert_not_contains() { + local file=$1 + local text=$2 + if grep -Fq -- "$text" "$root/$file"; then + printf 'FAIL: %s contains forbidden Feature Monthly Archive contract: %s\n' "$file" "$text" >&2 + exit 1 + fi +} + +for file in \ + SKILL.md \ + references/runtime.md \ + references/design.md \ + references/artifact-rules.md \ + references/feature-follow-up.md \ + references/stage-guides.md \ + references/workflow-checklists.md \ + references/human-review-summary.md \ + references/recovery-and-backfill.md \ + references/project-decisions.md \ + references/requirement-management.md \ + references/project-memory-mode.md \ + references/project-guidance.md \ + templates/root-AGENTS.md \ + references/validation-scenarios.md; do + assert_contains "$file" "Feature Monthly Archive" + assert_contains "$file" "features/archive.md" +done + +assert_contains SKILL.md "feature-archive-maintenance" +assert_contains SKILL.md "expected plan SHA-256" +assert_contains SKILL.md "Batch Human Gate" + +assert_contains references/runtime.md "feature-archive-maintenance" +assert_contains references/runtime.md "scan is read-only" +assert_contains references/runtime.md "transaction journal" +assert_contains references/runtime.md "rehydrate before reopened execution" + +assert_contains references/design.md "Feature ID is stable" +assert_contains references/design.md "archive state is not feature lifecycle" +assert_contains references/design.md "active / blocked / paused features stay flat" + +assert_contains references/artifact-rules.md "no per-feature archive summary" +assert_contains references/artifact-rules.md "no historical/" +assert_contains references/artifact-rules.md "no Deep Archive" +assert_contains references/artifact-rules.md 'No `--force`' + +assert_contains references/feature-follow-up.md "rehydrate before reopened execution" +assert_contains references/stage-guides.md "post-check" +assert_contains references/stage-guides.md "restore" +assert_contains references/workflow-checklists.md "expected plan SHA-256" +assert_contains references/human-review-summary.md "Platform evidence" +assert_contains references/recovery-and-backfill.md ".archive-txn" +assert_contains references/project-decisions.md "archived closed Feature Spec" +assert_contains references/requirement-management.md "stable Feature ID" +assert_contains references/project-memory-mode.md "features/archive.md" +assert_contains references/project-guidance.md "month archive" +assert_contains templates/root-AGENTS.md "feature-archive-maintenance" +assert_contains references/validation-scenarios.md "ambiguous old path" + +assert_contains templates/feature-archive.md "archived or rehydrated features" +assert_contains templates/feature-archive.md "remain authoritative" + +for file in SKILL.md references/runtime.md references/design.md references/artifact-rules.md; do + assert_not_contains "$file" "Feature Monthly Archive automatically deletes" + assert_not_contains "$file" "Feature Monthly Archive creates features/YYYY-MM/INDEX.md" +done + +for file in \ + references/e2e-discovery.md \ + references/delivery-contracts.md \ + references/complex-artifacts.md \ + references/external-skill-adapters.md \ + templates/subagent-brief.md; do + assert_not_contains "$file" ".agent-loop/features/YYYY-MM//" + assert_not_contains "$file" "execute inside an archived month directory" +done + +printf 'PASS: Feature Monthly Archive runtime, gate, locator, recovery, and scope contract is complete\n' diff --git a/tests/validate-feature-monthly-compaction-proposal.sh b/tests/validate-feature-monthly-compaction-proposal.sh index 66ed496..6295298 100755 --- a/tests/validate-feature-monthly-compaction-proposal.sh +++ b/tests/validate-feature-monthly-compaction-proposal.sh @@ -3,73 +3,104 @@ set -euo pipefail root=$(cd "$(dirname "$0")/.." && pwd) proposal="$root/docs/proposal/v1.3.x/feature-monthly-compaction.md" +plan="$root/docs/proposal/v1.3.x/feature-monthly-archive-implementation-plan.md" assert_contains() { local text=$1 if ! grep -Fq -- "$text" "$proposal"; then - printf 'FAIL: feature monthly compaction proposal missing required text: %s\n' "$text" >&2 + printf 'FAIL: feature monthly archive proposal missing required text: %s\n' "$text" >&2 exit 1 fi } -assert_file_exists() { - if [ ! -f "$proposal" ]; then - printf 'FAIL: missing feature monthly compaction proposal: %s\n' "$proposal" >&2 +assert_not_contains() { + local text=$1 + if grep -Fq -- "$text" "$proposal"; then + printf 'FAIL: feature monthly archive proposal contains removed design: %s\n' "$text" >&2 exit 1 fi } -assert_file_exists +assert_plan_contains() { + local text=$1 + if ! grep -Fq -- "$text" "$plan"; then + printf 'FAIL: feature monthly archive implementation plan missing required text: %s\n' "$text" >&2 + exit 1 + fi +} + +if [ ! -f "$proposal" ]; then + printf 'FAIL: missing feature monthly archive proposal: %s\n' "$proposal" >&2 + exit 1 +fi -assert_contains "状态:讨论草案" -assert_contains "目标版本:v1.2.4 候选" -assert_contains "当前月保持 flat" -assert_contains "上个月且整月全部完成后,才允许月度压缩" -assert_contains "Full" -assert_contains "Slim With History" -assert_contains "Deep Archive / Summary Only" -assert_contains "Deep Archive 永远不是默认动作" +if [ ! -f "$plan" ]; then + printf 'FAIL: missing feature monthly archive implementation plan: %s\n' "$plan" >&2 + exit 1 +fi -assert_contains ".agent-loop/features/YYYY-MM-DD-/" -assert_contains ".agent-loop/features/YYYY-MM/" -assert_contains "archive.md" -assert_contains "historical/" -assert_contains "README.md" +assert_contains "# Proposal: Feature Monthly Archive" +assert_contains "状态:已实现;待最终 Human Review" +assert_contains "Human Review:2026-07-14" +assert_contains "docs/proposal/v1.3.x/feature-monthly-archive-implementation-plan.md" +assert_contains "目标版本:v1.3.0 候选" +assert_contains "整目录按月归档" +assert_contains ".agent-loop/features/archive.md" +assert_contains ".agent-loop/features/2026-05/2026-05-08-login/" +assert_contains "Feature ID 是稳定身份" +assert_contains "Archive State" +assert_contains "archived | rehydrated" +assert_contains "它不是 feature lifecycle status" +assert_contains '归档资格只接受 `closed`' +assert_contains '- 不生成每个 feature 的新 `archive.md`;' +assert_contains '- 不创建 `historical/`;' +assert_contains '- 不创建 `features/YYYY-MM/INDEX.md`;' -assert_contains "Feature Compaction Scan" +assert_contains "Feature Monthly Archive Scan" +assert_contains "Scan 是只读操作" assert_contains "Candidate Matrix" -assert_contains "Safety Gate" -assert_contains "Human Gate" -assert_contains "Partial month compaction" +assert_contains "Reference Impact List" +assert_contains "Eligibility And Safety Gate" +assert_contains "Batch Human Gate" +assert_contains "stale-plan" +assert_contains "Post-check" +assert_contains "失败恢复" +assert_contains "Rehydrate / Reopen" -assert_contains "## 影响的索引关系" -assert_contains "features/INDEX.md" -assert_contains "features/YYYY-MM/INDEX.md" -assert_contains "requirement set README" -assert_contains "requirements/INDEX.md" -assert_contains "Delivery Phases" -assert_contains "Feature Mapping" -assert_contains "decisions/*.md" -assert_contains "Implemented By" -assert_contains "project.md" -assert_contains "Active Feature" -assert_contains "Paused Features" assert_contains "Feature Follow-up / Flow-back" -assert_contains "Source Requirements" -assert_contains "Applicable Decisions" -assert_contains "Implements Decisions" -assert_contains "verification evidence" -assert_contains "Drift Check" -assert_contains "scripts / validation glob" +assert_contains "ADR / Decision" +assert_contains "Requirement Mapping" +assert_contains "Project Memory" +assert_contains "Internal And External Links" +assert_contains "Feature Path Resolver" +assert_contains '`features/archive.md` 预期变化' +assert_contains '.agent-loop/features/.archive-txn//' +assert_contains "transaction journal" + +assert_contains "scripts/scan-feature-monthly-archive.py" +assert_contains "scripts/apply-feature-monthly-archive.py" +assert_contains "scripts/check-feature-monthly-archive.py" +assert_contains "scripts/restore-feature-monthly-archive.py" +assert_contains "Python 3.10+ 标准库" +assert_contains "Windows 与 macOS" +assert_contains "Windows-test-defined" +assert_contains "Proposal Boundary" +assert_contains "本文件仍是 proposal,不是发布运行时权威" -assert_contains "Requirements 不做内容压缩" -assert_contains "需求源材料保持原样" -assert_contains "只把 feature 压缩作为第一版默认能力" +assert_not_contains "Slim With History" +assert_not_contains "Deep Archive / Summary Only" +assert_not_contains '每个 feature 的新 `archive.md` 应' +assert_not_contains '删除、打包或外移 `historical/`' -assert_contains "## Archive Summary Template" -assert_contains "Delivered Behavior" -assert_contains "Key Design Decisions" -assert_contains "Changed Files / Public Interfaces" -assert_contains "Historical Detail Location" +assert_plan_contains "## Stage Helper Resolution" +assert_plan_contains "## Interface Contracts" +assert_plan_contains "## Task 0: Establish Baseline And Phase-0 Evidence" +assert_plan_contains "## Task 7: Full Validation, Review, And Development-Agent Handoff" +assert_plan_contains "Reader Compatibility" +assert_plan_contains "Archive Readiness" +assert_plan_contains "expected-plan-sha256" +assert_plan_contains "transaction journal" +assert_plan_contains "immutable-requirement-source" +assert_plan_contains "## Plan Self-Review" -printf 'PASS: feature monthly compaction proposal contract is complete\n' +printf 'PASS: feature monthly archive proposal contract is complete\n' diff --git a/tests/validate-project-local-skills.sh b/tests/validate-project-local-skills.sh index c1c0c92..c35478e 100755 --- a/tests/validate-project-local-skills.sh +++ b/tests/validate-project-local-skills.sh @@ -89,7 +89,7 @@ assert_contains "templates/root-AGENTS.md" "project-skill-management" assert_contains "templates/root-AGENTS.md" "Project Skill Creation / Update" assert_contains "templates/root-AGENTS.md" ".agent-loop/skills/INDEX.md" assert_contains "templates/root-AGENTS.md" "Execution Gate" -assert_contains "templates/root-AGENTS.md" "block-version:1.3.0-20260713.2" +assert_contains "templates/root-AGENTS.md" "block-version:1.3.0-20260714.1" assert_contains "templates/project.md" "Project Skills" assert_contains "templates/project.md" ".agent-loop/skills/INDEX.md" assert_contains "templates/project-skills/validation.md" "Validated Content Manifest" diff --git a/tests/validate-requirement-lifecycle-backlog.sh b/tests/validate-requirement-lifecycle-backlog.sh index c85736d..7fa3e3b 100755 --- a/tests/validate-requirement-lifecycle-backlog.sh +++ b/tests/validate-requirement-lifecycle-backlog.sh @@ -89,7 +89,7 @@ assert_contains "SKILL.md" "Version: 1.3.0" assert_contains "README.md" "**Current version:** 1.3.0" assert_contains "Usage.md" "**版本:** 1.3.0" assert_contains "plugin.json" '"version": "1.3.0"' -assert_contains "templates/root-AGENTS.md" "block-version:1.3.0-20260713.2" +assert_contains "templates/root-AGENTS.md" "block-version:1.3.0-20260714.1" assert_contains "CHANGELOG.md" "## 1.3.0 — 2026-07-11" assert_not_contains "templates/root-AGENTS.md" "## Agent Loop Guidance Version" assert_contains "AGENTS.md" 'ignore the `alpha` prefix for version records' diff --git a/tests/validate-root-agents-block-checker.sh b/tests/validate-root-agents-block-checker.sh index d32f7d2..f06b2fd 100755 --- a/tests/validate-root-agents-block-checker.sh +++ b/tests/validate-root-agents-block-checker.sh @@ -71,14 +71,14 @@ fi assert_contains "$tmpdir/missing-stage-map.out" "FAIL root AGENTS drift found" assert_contains "$tmpdir/missing-stage-map.out" "workflow-stage-map | missing" -sed 's/block-version:1\.3\.0-20260713\.2/block-version:1.3.0/' "$template" > "$tmpdir/stale.md" +sed 's/block-version:1\.3\.0-20260714\.1/block-version:1.3.0/' "$template" > "$tmpdir/stale.md" if python3 "$checker" --template "$template" --target "$tmpdir/stale.md" > "$tmpdir/stale.out"; then printf 'FAIL: checker should fail when block-version values are stale\n' >&2 cat "$tmpdir/stale.out" >&2 exit 1 fi assert_contains "$tmpdir/stale.out" "message-intent | stale-block-version" -assert_contains "$tmpdir/stale.out" "expected 1.3.0-20260713.2" +assert_contains "$tmpdir/stale.out" "expected 1.3.0-20260714.1" awk ' // { next } @@ -94,7 +94,7 @@ assert_contains "$tmpdir/broken.out" "ownership | broken-markers" awk ' /" + print "" print "nested" print "" inserted = 1 @@ -112,7 +112,7 @@ assert_contains "$tmpdir/nested.out" "ownership | nested-managed-block" awk ' { print } // && inserted != 1 { - print "" + print "" print "duplicate" print "" inserted = 1 @@ -127,7 +127,7 @@ assert_contains "$tmpdir/duplicate.out" "ownership | duplicate-section" { cat "$template" - printf '\n\n' + printf '\n\n' printf '## Legacy Extra\n\n' printf '\n' } > "$tmpdir/extra.md" @@ -176,7 +176,7 @@ assert_contains "$tmpdir/bare-end.out" "malformed-marker" { cat "$template" - printf '\n\n' + printf '\n\n' } > "$tmpdir/same-line.md" if python3 "$checker" --template "$template" --target "$tmpdir/same-line.md" > "$tmpdir/same-line.out"; then printf 'FAIL: checker should fail when multiple managed markers are on the same line\n' >&2 diff --git a/tests/validate-root-agents-block-refresh.sh b/tests/validate-root-agents-block-refresh.sh index ff2a67f..37fe81a 100755 --- a/tests/validate-root-agents-block-refresh.sh +++ b/tests/validate-root-agents-block-refresh.sh @@ -23,7 +23,7 @@ assert_not_contains() { managed_count=$(grep -c '^