cabloy 5.1.201 → 5.1.203

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +17 -0
  3. package/CONTRIBUTING.md +18 -17
  4. package/README.md +2 -2
  5. package/package.json +2 -3
  6. package/repo-agent-governance/managed-assets.json +33 -33
  7. package/repo-agent-governance/policies/repository.md +1 -0
  8. package/repo-agent-governance/skills/cabloy-spec-execution/SKILL.md +3 -2
  9. package/repo-agent-governance/skills/cabloy-spec-execution/references/execution-protocol.md +1 -1
  10. package/repo-agent-governance/skills/cabloy-spec-execution/references/status-and-evidence.md +11 -10
  11. package/repo-agent-governance/skills/cabloy-spec-generation/SKILL.md +1 -1
  12. package/repo-agent-governance/skills/cabloy-spec-generation/references/canonical-spec-input.md +4 -2
  13. package/repo-agent-governance/skills/cabloy-spec-generation/references/repo-specs-document-set.md +16 -13
  14. package/repo-agent-governance/skills/cabloy-spec-generation/references/traceability-and-status-rules.md +18 -11
  15. package/repo-agent-governance/skills/cabloy-workflow/SKILL.md +2 -2
  16. package/repo-agent-governance/skills/cabloy-workflow/evals/evals.json +1 -1
  17. package/repo-agent-governance/skills/cabloy-worktree-environment/SKILL.md +5 -5
  18. package/repo-agent-governance/tests/spec-audit.test.mjs +323 -0
  19. package/repo-agent-governance/tools/spec-audit/audit.mjs +129 -3
  20. package/repo-agent-governance/tools/spec-charts/generate-implementation-charts.mjs +26 -7
  21. package/repo-agent-governance/tools/spec-charts/generate-implementation-charts.test.mjs +82 -0
  22. package/repo-agent-governance/tools/spec-charts/spec-parser.mjs +40 -1
  23. package/repo-docs/ai/ai-spec-driven-development.md +1 -0
  24. package/repo-docs/ai/playbook-spec-execution.md +3 -2
  25. package/repo-docs/ai/playbook-spec-generation.md +1 -1
  26. package/repo-docs/assets/img/cabloy-admin-ssr-cover-en.png +0 -0
  27. package/repo-docs/assets/img/cabloy-admin-tabs-cover-en.png +0 -0
  28. package/repo-docs/assets/img/cabloy-ai-crud-cover-en.png +0 -0
  29. package/repo-docs/demo.md +14 -0
  30. package/repo-docs/frontend/scripts.md +10 -18
  31. package/repo-docs/fullstack/parallel-worktree-environment.md +2 -2
  32. package/repo-docs/index.md +2 -2
  33. package/repo-docs/reference/repo-scripts.md +15 -31
  34. package/repo-e2e/config/playwright.config.ts +14 -19
  35. package/repo-e2e/scripts/e2e.test.ts +17 -0
  36. package/repo-e2e/scripts/e2e.ts +5 -1
  37. package/repo-e2e/scripts/runE2e.ts +3 -15
  38. package/repo-e2e/scripts/runE2eArgs.test.ts +21 -17
  39. package/repo-e2e/scripts/runE2eArgs.ts +16 -29
  40. package/repo-e2e/scripts/startE2eVona.ts +20 -9
  41. package/repo-e2e/specs/a-commerce.spec.ts +232 -31
  42. package/repo-e2e/specs/cabloy-basic.spec.ts +39 -12
  43. package/repo-e2e/specs/home-user-account.spec.ts +9 -2
  44. package/scripts/upgrade.test.ts +147 -0
  45. package/scripts/upgrade.ts +49 -27
  46. package/vona/packages-cli/cli/package.json +1 -1
  47. package/vona/packages-cli/cli-set-api/package.json +1 -1
  48. package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.dev.ts +51 -5
  49. package/vona/packages-cli/cli-set-api/src/lib/command/bin.dev.ts +6 -1
  50. package/vona/packages-cli/cli-set-api/test/binDevMode.test.ts +52 -0
  51. package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +41 -0
  52. package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/e2eFixture.ts +52 -0
  53. package/vona/src/suite/a-commerce/modules/commerce-trade/src/lib/e2eFixture.ts +224 -0
  54. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/e2eFixture.ts +694 -0
  55. package/vona/src/suite/a-commerce/modules/commerce-trade/test/e2eFixture.test.ts +434 -0
@@ -49,6 +49,12 @@ Minimum cardinality for a material in-scope requirement:
49
49
 
50
50
  A single ATP may prove several related requirements, but the matrices must make the relationship explicit. A WBS item may cover several contracts, but it still needs bounded completion checks. The full-chain minimum applies to material in-scope product requirements; a separately sourced engineering obligation or a documentary coordination gate need not invent a product parent or an executable ATP. Use the narrowly scoped declaration-local `Traceability exception` classifications in `canonical-spec-input.md` only for those genuine cases. The unaffected direction still needs an exact-ID link, and review/closure still needs applicable retained proof. An exception is not a deferred status, evidence waiver, acceptance result, or release approval.
51
51
 
52
+ ### Planning-baseline review gate
53
+
54
+ A WBS task that solely reviews and approves the whole PRD/SRS/WBS/ATP baseline before feature work may declare `Traceability mode: planning-baseline-review` in its own Phase 10 task body. It must name a local accepted boundary ADR under `decisions/` as `Review authority: [ADR](./decisions/<file>.md)`, and retain substantive Tasks and Acceptance checks for the baseline review. It must not claim to implement a specific SRS contract or execute a formal ATP, including in its title, Tasks, or Acceptance checks. The ADR may use the existing `## Status` followed by `Accepted` or `Accepted.` convention; the auditor does not require new owner or acceptance fields in the ADR, and acceptance of the ADR alone does not prove the review occurred. This explicit process gate alone is exempt from the WBS-specific SRS incoming and ATP outgoing exact-ID checks in the complete audit; all product-chain definitions, ordinary WBS tasks, references, dependencies, and progress remain checked. It cannot classify implementation, migration, contract-loop, release, or mixed-scope work. Do not infer this type from a title, task number, review status, or a summary table, and do not combine it with `Traceability exception: authority-only`.
55
+
56
+ This differs from `authority-only`, which still requires an exact SRS incoming link and exempts only the ATP outgoing link. Both traceability classifications are independent of `Completion mode: planning-only.`; neither makes a task eligible for `planning-complete`. An audit exception is not approval or proof: the ADR decision owner must separately accept the baseline, and the progress record must retain that review's attributable approval before `verified` is appropriate. It does not create an ATP execution record, change any business traceability, or relax the evidence rule for other WBS work.
57
+
52
58
  ## Authority-first updates
53
59
 
54
60
  When a requirement or durable boundary changes:
@@ -67,17 +73,18 @@ Downstream records do not silently override authority. Charts cannot authorize s
67
73
 
68
74
  ## Status semantics
69
75
 
70
- | Status | Meaning |
71
- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
72
- | `not-started` | Defined, but implementation or acceptance evidence has not started. |
73
- | `in-progress` | Work or verification has started, but closure checks are incomplete. |
74
- | `implementation-complete` | Source work is reported complete, but required ATP or release evidence is incomplete. |
75
- | `verified` | Applicable WBS checks and ATPs passed, and durable evidence contains revision, environment, exact procedure, result, and redacted artifact location. |
76
- | `blocked` | A failed gate, dependency, or unresolved decision prevents closure. |
77
- | `waived` | A temporary exception explicitly approved with owner, reason, and expiry. |
78
- | `deferred` | Explicitly postponed scope; it is not complete or verified. |
79
-
80
- For a newly created plan, initialize delivery rows as `not-started`, `deferred`, or `blocked` as appropriate. Creating Markdown files never makes implementation `implementation-complete` or `verified`.
76
+ | Status | Meaning |
77
+ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
78
+ | `not-started` | Defined, but implementation or acceptance evidence has not started. |
79
+ | `in-progress` | Work or verification has started, but closure checks are incomplete. |
80
+ | `planning-complete` | Opted-in documentary/design-only WBS checks have a named-reviewer, revision-scoped closure disposition and retained planning proof; source and ATP remain unproven. |
81
+ | `implementation-complete` | Source work is reported complete, but required ATP or release evidence is incomplete. |
82
+ | `verified` | Applicable WBS checks and ATPs passed, and durable evidence contains revision, environment, exact procedure, result, and redacted artifact location. |
83
+ | `blocked` | A failed gate, dependency, or unresolved decision prevents closure. |
84
+ | `waived` | A temporary exception explicitly approved with owner, reason, and expiry. |
85
+ | `deferred` | Explicitly postponed scope; it is not complete or verified. |
86
+
87
+ For a newly created plan, initialize delivery rows as `not-started`, `deferred`, or `blocked` as appropriate. Creating Markdown files never makes a task `planning-complete`, implementation `implementation-complete`, or ATP `verified`. Only a formally declared `Completion mode: planning-only.` task with documentary acceptance checks, revision-scoped evidence and an explicit named-reviewer closure disposition may reach `planning-complete`; `authority-only` traceability does not establish this eligibility. A planning-complete predecessor satisfies its WBS dependency edge, but blocked/deferred/waived successors stay gated and require their own decisions and approvals. `implementation-complete`, `waived` and `deferred` do not satisfy predecessor completion. Only `verified` contributes to verified totals and the verified-based burndown remainder.
81
88
 
82
89
  ### README and ADR decision-status consistency
83
90
 
@@ -107,11 +107,11 @@ The reason is simple: these files are where Cabloy already encodes its real work
107
107
 
108
108
  ### Parallel worktree environment setup
109
109
 
110
- When a request involves a second worktree, concurrent Vona/Zova development, isolated ordinary tests, or managed clean E2E, classify it as fullstack workflow setup. Read [Parallel Worktree Environment](../../../repo-docs/fullstack/parallel-worktree-environment.md), the canonical shared recipe maintained in Cabloy Basic for both editions.
110
+ When a request involves a second worktree, concurrent Vona/Zova development, isolated ordinary tests, or managed E2E, classify it as fullstack workflow setup. Read [Parallel Worktree Environment](../../../repo-docs/fullstack/parallel-worktree-environment.md), the canonical shared recipe maintained in Cabloy Basic for both editions.
111
111
 
112
112
  This routing skill provides read-only guidance only. Do not infer an `APP_NAME` or ports, edit local overrides, or run `npm run init` from a generic worktree request. When the user wants confirmation-gated local environment setup, ask them to explicitly invoke `/cabloy-worktree-environment`.
113
113
 
114
- The invoked skill detects Basic or Start to select the active root scripts and managed clean-E2E command. It uses Git worktree metadata, fixed Vona/Zova port baselines, and the worktree basename to recommend the complete standard tuple for confirmation: `APP_NAME`, `SERVER_LISTEN_PORT`, `DEV_SERVER_PORT`, `DEV_SERVER_HMR_PORT`, and an `API_BASE_URL` derived from the Vona port. After confirmation, it writes both `vona/env/.env.local` and `zova/env/.env.local`; never use flavor-, mode-, app-mode-, or runtime-specific `.env.*.local` files. It does not inspect environment-file content or allocate/reserve a live port. The shared environment is Vona + Zova development: Admin and Web are alternative commands using the same configuration, and they must not run concurrently in one worktree. Concurrent use requires separately configured linked worktrees.
114
+ The invoked skill detects Basic or Start to select the active root scripts and managed E2E command. It uses Git worktree metadata, fixed Vona/Zova port baselines, and the worktree basename to recommend the complete standard tuple for confirmation: `APP_NAME`, `SERVER_LISTEN_PORT`, `DEV_SERVER_PORT`, `DEV_SERVER_HMR_PORT`, and an `API_BASE_URL` derived from the Vona port. After confirmation, it writes both `vona/env/.env.local` and `zova/env/.env.local`; never use flavor-, mode-, app-mode-, or runtime-specific `.env.*.local` files. It does not inspect environment-file content or allocate/reserve a live port. The shared environment is Vona + Zova development: Admin and Web are alternative commands using the same configuration, and they must not run concurrently in one worktree. Concurrent use requires separately configured linked worktrees.
115
115
 
116
116
  ## Step 4: Prefer CLI-first workflows
117
117
 
@@ -39,7 +39,7 @@
39
39
  },
40
40
  {
41
41
  "id": 7,
42
- "prompt": "I created a linked Cabloy Start worktree and need isolated Vona, Zova Web, and managed clean E2E. Which workflow should I use?",
42
+ "prompt": "I created a linked Cabloy Start worktree and need isolated Vona, Zova Web, and managed E2E. Which workflow should I use?",
43
43
  "expected_output": "Routes to the canonical shared guide and explicitly invoked /cabloy-worktree-environment skill. It recognizes that the invoked skill selects the Start command npm run test:e2e while retaining read-only generic routing, a deterministic Git-metadata recommendation, no environment-content inspection, no automatic initialization, and the universal five-setting tuple written to both permitted broad local files only after confirmation.",
44
44
  "files": []
45
45
  },
@@ -29,10 +29,10 @@ Before proposing configuration values or writing files:
29
29
 
30
30
  Interpretation:
31
31
 
32
- | Marker | Edition | Managed clean E2E command |
33
- | ----------------------- | ------------ | ------------------------- |
34
- | `__CABLOY_BASIC__` only | Cabloy Basic | `npm run test:e2e` |
35
- | `__CABLOY_START__` only | Cabloy Start | `npm run test:e2e` |
32
+ | Marker | Edition | Managed E2E command |
33
+ | ----------------------- | ------------ | ------------------- |
34
+ | `__CABLOY_BASIC__` only | Cabloy Basic | `npm run test:e2e` |
35
+ | `__CABLOY_START__` only | Cabloy Start | `npm run test:e2e` |
36
36
 
37
37
  Stop without edits when both markers are present, neither marker is present, the checkout is primary/unregistered, required environment directories are missing, or the selected edition's required scripts are unavailable.
38
38
 
@@ -178,7 +178,7 @@ npm run dev:zova:web
178
178
  npm run test
179
179
  ```
180
180
 
181
- Run the edition-appropriate managed clean E2E command:
181
+ Run the edition-appropriate managed E2E command:
182
182
 
183
183
  ```bash
184
184
  # Cabloy Basic or Cabloy Start
@@ -57,6 +57,267 @@ test('complete baseline checks exact traceability without treating a Proposed AD
57
57
  assert.match(result.limitations, /approval.*evidence authenticity/);
58
58
  });
59
59
 
60
+ const PLANNING_GATE = {
61
+ ...BASELINE,
62
+ 'srs.md': BASELINE['srs.md'].replace(', `WBS-DEMO-10-01`', ''),
63
+ 'test-plan.md': BASELINE['test-plan.md'].replace('Traceability: `WBS-DEMO-10-01`', ''),
64
+ 'pdp-wbs.md':
65
+ '# Delivery\n\n### Phase 10: Baseline\n\nDependencies: none.\n\n#### WBS-DEMO-10-01: Review planning baseline\n\nTraceability mode: planning-baseline-review\n\nReview authority: [ADR](./decisions/0001-boundary.md).\n\nTasks:\n\n- Reconcile the whole requirements, contracts, work and acceptance catalogue.\n\nAcceptance checks:\n\n- Confirm the boundary ADR was accepted by its decision owner.\n',
66
+ 'decisions/0001-boundary.md':
67
+ '# Boundary\n\n## Status\n\nAccepted.\n\n## Decision\n\nAccept the planning boundary.\n',
68
+ };
69
+
70
+ test('explicit planning-baseline gate omits only its WBS adjacency checks', async t => {
71
+ const result = await auditPlanning(await fixture(t, PLANNING_GATE));
72
+ assert.deepEqual(
73
+ result.diagnostics.map(diagnostic => [diagnostic.id, diagnostic.code]),
74
+ [
75
+ ['SRS-DEMO-01', 'traceability-outgoing'],
76
+ ['ATP-DEMO-01', 'traceability-incoming'],
77
+ ],
78
+ );
79
+ assert.equal(result.ok, false);
80
+ });
81
+
82
+ test('a planning gate coexists with a fully connected product chain', async t => {
83
+ const records = {
84
+ ...BASELINE,
85
+ 'pdp-wbs.md': `${PLANNING_GATE['pdp-wbs.md']}\n#### WBS-DEMO-10-02: Implement contract\n\nTraceability: \`SRS-DEMO-01\`, \`ATP-DEMO-01\`.\n`,
86
+ 'srs.md': BASELINE['srs.md'].replace('WBS-DEMO-10-01', 'WBS-DEMO-10-02'),
87
+ 'test-plan.md': BASELINE['test-plan.md'].replace('WBS-DEMO-10-01', 'WBS-DEMO-10-02'),
88
+ 'progress.md': `${BASELINE['progress.md']}| WBS-DEMO-10-02 | Implement contract | not-started |\n`,
89
+ 'decisions/0001-boundary.md': PLANNING_GATE['decisions/0001-boundary.md'],
90
+ };
91
+ const result = await auditPlanning(await fixture(t, records));
92
+ assert.deepEqual(result.diagnostics, []);
93
+ assert.equal(result.ok, true);
94
+ });
95
+
96
+ test('planning closure and baseline traceability classification remain independent', async t => {
97
+ const records = {
98
+ ...BASELINE,
99
+ 'pdp-wbs.md': `${PLANNING_GATE['pdp-wbs.md']}\n#### WBS-DEMO-10-02: Implement contract\n\nTraceability: \`SRS-DEMO-01\`, \`ATP-DEMO-01\`.\n`,
100
+ 'srs.md': BASELINE['srs.md'].replace('WBS-DEMO-10-01', 'WBS-DEMO-10-02'),
101
+ 'test-plan.md': BASELINE['test-plan.md'].replace('WBS-DEMO-10-01', 'WBS-DEMO-10-02'),
102
+ 'progress.md': `${BASELINE['progress.md'].replace('not-started |', 'planning-complete |')}| WBS-DEMO-10-02 | Implement contract | not-started |\n`,
103
+ 'decisions/0001-boundary.md': PLANNING_GATE['decisions/0001-boundary.md'],
104
+ };
105
+ const missingMode = await auditPlanning(await fixture(t, records));
106
+ assert.deepEqual(codes(missingMode), ['completion-mode']);
107
+ const withMode = {
108
+ ...records,
109
+ 'pdp-wbs.md': records['pdp-wbs.md'].replace(
110
+ 'Traceability mode: planning-baseline-review',
111
+ 'Traceability mode: planning-baseline-review\n\nCompletion mode: planning-only.',
112
+ ),
113
+ };
114
+ const eligible = await auditPlanning(await fixture(t, withMode));
115
+ assert.equal(eligible.ok, true, JSON.stringify(eligible.diagnostics));
116
+ const invalidGate = await auditPlanning(
117
+ await fixture(t, {
118
+ ...withMode,
119
+ 'pdp-wbs.md': withMode['pdp-wbs.md'].replace(
120
+ 'Traceability mode: planning-baseline-review',
121
+ 'Traceability mode: unknown',
122
+ ),
123
+ }),
124
+ );
125
+ assert.ok(codes(invalidGate).includes('traceability-mode'));
126
+ assert.ok(!codes(invalidGate).includes('completion-mode'));
127
+ });
128
+
129
+ test('planning review accepts existing and legacy accepted ADR status formats', async t => {
130
+ for (const status of [
131
+ '## Status\n\nAccepted.\n',
132
+ '## Status\n\nAccepted\n',
133
+ '- **Status:** Accepted\n',
134
+ ]) {
135
+ await t.test(status.trim().replace(/\n/g, ' '), async subtest => {
136
+ const records = {
137
+ ...PLANNING_GATE,
138
+ 'decisions/0001-boundary.md': `# Boundary\n\n${status}`,
139
+ };
140
+ const result = await auditPlanning(await fixture(subtest, records));
141
+ assert.ok(!codes(result).includes('traceability-mode'), JSON.stringify(result.diagnostics));
142
+ });
143
+ }
144
+ });
145
+
146
+ test('baseline review prose may refer to the work catalogue without doing delivery work', async t => {
147
+ const records = {
148
+ ...PLANNING_GATE,
149
+ 'pdp-wbs.md': PLANNING_GATE['pdp-wbs.md'].replace(
150
+ '- Reconcile the whole requirements, contracts, work and acceptance catalogue.',
151
+ '- Review the implementation plan, build risks and migration strategy as planning inputs.',
152
+ ),
153
+ };
154
+ const result = await auditPlanning(await fixture(t, records));
155
+ assert.ok(!codes(result).includes('traceability-mode'), JSON.stringify(result.diagnostics));
156
+ });
157
+
158
+ test('baseline classification must be declared inside a bounded Phase 10 WBS task', async t => {
159
+ const cases = [
160
+ [
161
+ 'unknown',
162
+ { 'pdp-wbs.md': PLANNING_GATE['pdp-wbs.md'].replace('planning-baseline-review', 'skip') },
163
+ ],
164
+ [
165
+ 'duplicate',
166
+ {
167
+ 'pdp-wbs.md': PLANNING_GATE['pdp-wbs.md'].replace(
168
+ 'Tasks:',
169
+ 'Traceability mode: planning-baseline-review\n\nTasks:',
170
+ ),
171
+ },
172
+ ],
173
+ ['wrong phase', { 'pdp-wbs.md': PLANNING_GATE['pdp-wbs.md'].replace('Phase 10', 'Phase 20') }],
174
+ ['no tasks', { 'pdp-wbs.md': PLANNING_GATE['pdp-wbs.md'].replace('Tasks:', 'Work:') }],
175
+ [
176
+ 'no checks',
177
+ { 'pdp-wbs.md': PLANNING_GATE['pdp-wbs.md'].replace('Acceptance checks:', 'Proof:') },
178
+ ],
179
+ [
180
+ 'mixed scope',
181
+ {
182
+ 'pdp-wbs.md': `${PLANNING_GATE['pdp-wbs.md']}\n- Implement SRS-DEMO-01 through ATP-DEMO-01.\n`,
183
+ },
184
+ ],
185
+ [
186
+ 'no authority',
187
+ { 'pdp-wbs.md': PLANNING_GATE['pdp-wbs.md'].replace('Review authority:', 'Reviewer:') },
188
+ ],
189
+ [
190
+ 'unaccepted authority',
191
+ {
192
+ 'decisions/0001-boundary.md': PLANNING_GATE['decisions/0001-boundary.md'].replace(
193
+ 'Accepted.',
194
+ 'Proposed.',
195
+ ),
196
+ },
197
+ ],
198
+ [
199
+ 'implementation in title',
200
+ {
201
+ 'pdp-wbs.md': PLANNING_GATE['pdp-wbs.md'].replace(
202
+ 'Review planning baseline',
203
+ 'Review baseline and implement SRS-DEMO-01',
204
+ ),
205
+ },
206
+ ],
207
+ [
208
+ 'release task',
209
+ {
210
+ 'pdp-wbs.md': PLANNING_GATE['pdp-wbs.md'].replace(
211
+ '- Reconcile the whole requirements, contracts, work and acceptance catalogue.',
212
+ '- Review the baseline and release the feature.',
213
+ ),
214
+ },
215
+ ],
216
+ [
217
+ 'implementation task',
218
+ {
219
+ 'pdp-wbs.md': PLANNING_GATE['pdp-wbs.md'].replace(
220
+ '- Reconcile the whole requirements, contracts, work and acceptance catalogue.',
221
+ '- Review the baseline and implement the feature.',
222
+ ),
223
+ },
224
+ ],
225
+ [
226
+ 'implementation acceptance check',
227
+ {
228
+ 'pdp-wbs.md': PLANNING_GATE['pdp-wbs.md'].replace(
229
+ '- Confirm the boundary ADR was accepted by its decision owner.',
230
+ '- Confirm the boundary ADR was accepted and deploy the feature.',
231
+ ),
232
+ },
233
+ ],
234
+ ...['Build the checkout backend.', 'Create a service.', 'Run a migration.'].map(instruction => [
235
+ instruction,
236
+ {
237
+ 'pdp-wbs.md': PLANNING_GATE['pdp-wbs.md'].replace(
238
+ '- Reconcile the whole requirements, contracts, work and acceptance catalogue.',
239
+ `- ${instruction}`,
240
+ ),
241
+ },
242
+ ]),
243
+ [
244
+ 'unmapped contract in title',
245
+ {
246
+ 'pdp-wbs.md': PLANNING_GATE['pdp-wbs.md'].replace(
247
+ 'Review planning baseline',
248
+ 'Review planning baseline for SRS-DEMO-01',
249
+ ),
250
+ },
251
+ ],
252
+ [
253
+ 'contradictory ADR statuses',
254
+ {
255
+ 'decisions/0001-boundary.md': '## Status\n\nAccepted.\n\n- **Status:** Proposed\n',
256
+ },
257
+ ],
258
+ [
259
+ 'ADR status inside unrelated section',
260
+ {
261
+ 'decisions/0001-boundary.md': '# Boundary\n\n## Background\n\n- **Status:** Accepted\n',
262
+ },
263
+ ],
264
+ ['mapped from SRS', { 'srs.md': BASELINE['srs.md'] }],
265
+ ['mapped to ATP', { 'test-plan.md': BASELINE['test-plan.md'] }],
266
+ [
267
+ 'misplaced',
268
+ {
269
+ 'README.md': `${PLANNING_GATE['README.md']}\nTraceability mode: planning-baseline-review\n`,
270
+ },
271
+ ],
272
+ [
273
+ 'competing exception',
274
+ {
275
+ 'pdp-wbs.md': PLANNING_GATE['pdp-wbs.md'].replace(
276
+ 'Tasks:',
277
+ 'Traceability exception: authority-only — Review the whole baseline.\n\nTasks:',
278
+ ),
279
+ },
280
+ ],
281
+ ];
282
+ for (const [name, changed] of cases) {
283
+ await t.test(name, async subtest => {
284
+ const result = await auditPlanning(await fixture(subtest, { ...PLANNING_GATE, ...changed }));
285
+ assert.ok(codes(result).includes('traceability-mode'), JSON.stringify(result.diagnostics));
286
+ if (name !== 'misplaced' && name !== 'mapped from SRS') {
287
+ assert.ok(
288
+ result.diagnostics.some(
289
+ diagnostic =>
290
+ diagnostic.id === 'WBS-DEMO-10-01' && diagnostic.code === 'traceability-incoming',
291
+ ),
292
+ );
293
+ }
294
+ if (name !== 'misplaced' && name !== 'mapped to ATP' && name !== 'competing exception') {
295
+ assert.ok(
296
+ result.diagnostics.some(
297
+ diagnostic =>
298
+ diagnostic.id === 'WBS-DEMO-10-01' && diagnostic.code === 'traceability-outgoing',
299
+ ),
300
+ );
301
+ }
302
+ });
303
+ }
304
+ });
305
+
306
+ test('ordinary unmapped WBS retains its adjacency diagnostics beside a planning gate', async t => {
307
+ const records = {
308
+ ...PLANNING_GATE,
309
+ 'pdp-wbs.md': `${PLANNING_GATE['pdp-wbs.md']}\n#### WBS-DEMO-10-02: Implement item\n\nTasks:\n\n- Implement the item.\n`,
310
+ 'progress.md': `${PLANNING_GATE['progress.md']}| WBS-DEMO-10-02 | Implement item | not-started |\n`,
311
+ };
312
+ const result = await auditPlanning(await fixture(t, records));
313
+ assert.deepEqual(
314
+ result.diagnostics
315
+ .filter(diagnostic => diagnostic.id === 'WBS-DEMO-10-02')
316
+ .map(diagnostic => diagnostic.code),
317
+ ['traceability-incoming', 'traceability-outgoing'],
318
+ );
319
+ });
320
+
60
321
  test('canonical atomic bullets carry their own inline traceability', async t => {
61
322
  const records = {
62
323
  ...BASELINE,
@@ -361,6 +622,68 @@ test('completed tasks cannot hide unfinished recorded prerequisites', async t =>
361
622
  );
362
623
  });
363
624
 
625
+ test('planning-complete requires a task-local mode in full and lightweight audits', async t => {
626
+ const progress = BASELINE['progress.md'].replace('not-started |', 'planning-complete |');
627
+ for (const lightweight of [false, true]) {
628
+ const rejected = await auditPlanning(
629
+ await fixture(t, { ...BASELINE, 'progress.md': progress }),
630
+ { lightweight },
631
+ );
632
+ assert.ok(codes(rejected).includes('completion-mode'));
633
+ const approved = await auditPlanning(
634
+ await fixture(t, {
635
+ ...BASELINE,
636
+ 'pdp-wbs.md': BASELINE['pdp-wbs.md'].replace(
637
+ 'Completion checks:',
638
+ 'Completion mode: planning-only.\n\nCompletion checks:',
639
+ ),
640
+ 'progress.md': progress,
641
+ }),
642
+ { lightweight },
643
+ );
644
+ assert.equal(approved.ok, true, JSON.stringify(approved.diagnostics));
645
+ }
646
+ });
647
+
648
+ test('planning-complete prerequisites allow recorded closure but never unblock successors', async t => {
649
+ const wbs = `${BASELINE['pdp-wbs.md'].replace('Completion checks:', 'Completion mode: planning-only.\n\nCompletion checks:')}\n#### WBS-DEMO-10-02: Dependent review\n\nDependencies: WBS-DEMO-10-01.\n\nTraceability exception: authority-only — Retain the review decision.\n\nTraceability: \`SRS-DEMO-01\`.\n`;
650
+ const progress = `${BASELINE['progress.md'].replace('not-started |', 'planning-complete |')}| WBS-DEMO-10-02 | Dependent | planning-complete |\n`;
651
+ const records = { ...BASELINE, 'pdp-wbs.md': wbs, 'progress.md': progress };
652
+ const completed = await auditPlanning(await fixture(t, records));
653
+ assert.ok(codes(completed).includes('completion-mode'));
654
+ assert.ok(!codes(completed).includes('dependency-state'));
655
+ const blocked = await auditPlanning(
656
+ await fixture(t, {
657
+ ...records,
658
+ 'progress.md': progress.replace('Dependent | planning-complete', 'Dependent | blocked'),
659
+ }),
660
+ );
661
+ assert.ok(!codes(blocked).includes('dependency-state'));
662
+ assert.ok(!codes(blocked).includes('completion-mode'));
663
+ const unfinished = await auditPlanning(
664
+ await fixture(t, {
665
+ ...records,
666
+ 'progress.md': progress.replace('planning-complete |', 'implementation-complete |'),
667
+ }),
668
+ );
669
+ assert.ok(codes(unfinished).includes('dependency-state'));
670
+ const waived = await auditPlanning(
671
+ await fixture(t, {
672
+ ...records,
673
+ 'progress.md': progress.replace('planning-complete |', 'waived |'),
674
+ }),
675
+ );
676
+ assert.ok(codes(waived).includes('dependency-state'));
677
+ const noMode = await auditPlanning(
678
+ await fixture(t, {
679
+ ...records,
680
+ 'pdp-wbs.md': wbs.replace('Completion mode: planning-only.\n\n', ''),
681
+ }),
682
+ );
683
+ assert.ok(codes(noMode).includes('completion-mode'));
684
+ assert.ok(codes(noMode).includes('dependency-state'));
685
+ });
686
+
364
687
  test('local Markdown destinations, fragments and reference links are checked', async t => {
365
688
  const records = {
366
689
  ...BASELINE,
@@ -4,6 +4,8 @@ import { fileURLToPath } from 'node:url';
4
4
 
5
5
  import {
6
6
  findIdentifiers,
7
+ isPlanningCompleteEligible,
8
+ isPrerequisiteSatisfied,
7
9
  markdownLines,
8
10
  parseDefinitions,
9
11
  parseProgress,
@@ -160,6 +162,116 @@ function collectMappings(records, definitions) {
160
162
  return edges;
161
163
  }
162
164
 
165
+ function acceptedDecision(markdown) {
166
+ if (!markdown) return false;
167
+ const lines = markdownLines(markdown);
168
+ const statuses = [];
169
+ let section = null;
170
+ for (const [index, { text, heading }] of lines.entries()) {
171
+ if (heading?.level === 2) section = heading.text;
172
+ const field = text.match(/^\s*-\s*\*\*Status:\*\*/i);
173
+ if (field && (section === null || /^Status$/i.test(section)))
174
+ statuses.push(text.slice(field[0].length).trim());
175
+ if (!heading || !/^Status$/i.test(heading.text)) continue;
176
+ const value = lines.slice(index + 1).find(record => {
177
+ if (record.heading && record.heading.level <= heading.level) return true;
178
+ return record.text.trim();
179
+ });
180
+ if (value && !(value.heading && value.heading.level <= heading.level))
181
+ statuses.push(value.text.trim());
182
+ }
183
+ return statuses.length > 0 && statuses.every(status => /^Accepted\.?$/i.test(status));
184
+ }
185
+
186
+ function planningBaselineGates(records, wbs, edges, exceptions, report) {
187
+ const gates = new Set();
188
+ const field = /^\s*(?:-\s*)?(?:\*\*)?Traceability mode(?:\*\*)?\s*:/i;
189
+ const declarations = new Map();
190
+ for (const [file, markdown] of records) {
191
+ for (const { text, line } of markdownLines(markdown)) {
192
+ if (!field.test(text)) continue;
193
+ const task =
194
+ file === 'pdp-wbs.md'
195
+ ? wbs?.tasks.find(candidate => candidate.line < line && candidate.endLine >= line)
196
+ : null;
197
+ if (!task) {
198
+ report(
199
+ 'traceability-mode',
200
+ file,
201
+ line,
202
+ 'Traceability mode must be declared inside a WBS task.',
203
+ );
204
+ continue;
205
+ }
206
+ if (!declarations.has(task.id)) declarations.set(task.id, []);
207
+ declarations.get(task.id).push({ text, line });
208
+ }
209
+ }
210
+ for (const task of wbs?.tasks ?? []) {
211
+ const declared = declarations.get(task.id);
212
+ if (!declared) continue;
213
+ const [{ text, line }] = declared;
214
+ const mode = text.replace(field, '').trim().replace(/`/g, '');
215
+ const body = markdownLines(task.body).map(record => record.text);
216
+ const deliveryAction =
217
+ /(?:^|[,;]\s*|\b(?:and|then|to)\s+)(?:implement|deploy|migrate|release|regenerate|develop|ship|code|build\b(?!\s+risks?\b)|create|run\s+(?:an?\s+)?(?:migrations?|tests?|ATP-|scenarios?)|execute\s+(?:an?\s+)?(?:ATP-|tests?|scenarios?))\b/i;
218
+ const mixedScope =
219
+ deliveryAction.test(task.title) ||
220
+ body.some(value => {
221
+ const bullet = value.match(/^\s*[-*]\s+(.*)/);
222
+ return bullet && deliveryAction.test(bullet[1].replace(/^\*\*[^*]+\*\*\s*/, ''));
223
+ });
224
+ const hasItems = label =>
225
+ body.some((value, index) => {
226
+ if (value.trim().toLowerCase() !== `${label}:`) return false;
227
+ const next = body.slice(index + 1).find(line => line.trim());
228
+ return Boolean(next?.trim().match(/^[-*] \S/));
229
+ });
230
+ const authorities = body.flatMap(value => {
231
+ const match = value.match(/^\s*(?:-\s*)?(?:\*\*)?Review authority(?:\*\*)?\s*:/i);
232
+ return match ? [value.replace(match[0], '').trim()] : [];
233
+ });
234
+ const authority = authorities[0]?.match(
235
+ /^\[([^\]]+)\]\((\.\/decisions\/[^/()]+\.md)\)(?:\s*\([^\n]*\))?\s*\.?$/,
236
+ );
237
+ const adr = authority?.[2].slice(2);
238
+ const decision = adr && records.get(adr);
239
+ const mappedIncoming = [...edges].some(
240
+ ([id, targets]) => id.startsWith('SRS-') && targets.has(task.id),
241
+ );
242
+ const valid =
243
+ declared.length === 1 &&
244
+ mode === 'planning-baseline-review' &&
245
+ task.phase.number === '10' &&
246
+ /\b(?:review|freeze)\b/i.test(task.title) &&
247
+ /\bbaseline\b/i.test(task.title) &&
248
+ hasItems('tasks') &&
249
+ hasItems('acceptance checks') &&
250
+ !mixedScope &&
251
+ !findIdentifiers(task.title, 'SRS-').length &&
252
+ !findIdentifiers(task.title, 'ATP-').length &&
253
+ !findIdentifiers(task.body, 'SRS-').length &&
254
+ !findIdentifiers(task.body, 'ATP-').length &&
255
+ !mappedIncoming &&
256
+ !edges.get(task.id)?.size &&
257
+ !exceptions.has(task.id) &&
258
+ authorities.length === 1 &&
259
+ acceptedDecision(decision);
260
+ if (!valid) {
261
+ report(
262
+ 'traceability-mode',
263
+ 'pdp-wbs.md',
264
+ line,
265
+ 'Planning-baseline review requires one valid mode, Phase 10 baseline review, substantive review-only Tasks and Acceptance checks, no individual SRS/ATP mapping or other traceability exception, and one accepted local ADR review authority.',
266
+ task.id,
267
+ );
268
+ continue;
269
+ }
270
+ gates.add(task.id);
271
+ }
272
+ return gates;
273
+ }
274
+
163
275
  function headingIds(markdown) {
164
276
  const slugs = new Set();
165
277
  const counts = new Map();
@@ -362,10 +474,23 @@ export async function auditPlanning(
362
474
  task.id,
363
475
  );
364
476
  }
365
- if (['implementation-complete', 'verified'].includes(rows.get(task.id))) {
477
+ if (rows.get(task.id) === 'planning-complete' && !isPlanningCompleteEligible(task)) {
478
+ report(
479
+ 'completion-mode',
480
+ 'progress.md',
481
+ locations?.get(task.id)?.line ?? 1,
482
+ 'Planning-complete requires a declaration-local Completion mode: planning-only field.',
483
+ task.id,
484
+ );
485
+ }
486
+ if (
487
+ ['planning-complete', 'implementation-complete', 'verified'].includes(rows.get(task.id))
488
+ ) {
366
489
  for (const dependency of task.dependencyIds) {
490
+ const predecessor = wbs.tasks.find(item => item.id === dependency);
367
491
  if (
368
- ['not-started', 'in-progress', 'blocked', 'deferred'].includes(rows.get(dependency))
492
+ rows.has(dependency) &&
493
+ !isPrerequisiteSatisfied(predecessor, rows.get(dependency))
369
494
  ) {
370
495
  report(
371
496
  'dependency-state',
@@ -398,9 +523,10 @@ export async function auditPlanning(
398
523
  }
399
524
  if (!lightweight) {
400
525
  const edges = collectMappings(records, definitions);
526
+ const gates = planningBaselineGates(records, wbs, edges, exceptions, report);
401
527
  const incoming = new Set([...edges.values()].flatMap(values => [...values]));
402
528
  for (const [id, definition] of definitions) {
403
- if (definition.status === 'deferred' || definition.deferred) continue;
529
+ if (definition.status === 'deferred' || definition.deferred || gates.has(id)) continue;
404
530
  if (!id.startsWith('PRD-') && !incoming.has(id) && exceptions.get(id) !== 'technical-only') {
405
531
  report(
406
532
  'traceability-incoming',