@polymorphism-tech/morph-spec 8.47.0 → 9.0.0

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 (222) hide show
  1. package/CLAUDE.md +48 -39
  2. package/README.md +3 -4
  3. package/bin/morph-spec.js +37 -51
  4. package/bin/validate.js +3 -111
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +41 -101
  9. package/framework/MORPH.md +95 -252
  10. package/framework/agents.json +75 -18
  11. package/framework/commands/morph-apply.md +5 -28
  12. package/framework/commands/morph-archive.md +1 -1
  13. package/framework/commands/morph-hotfix.md +8 -8
  14. package/framework/commands/morph-infra.md +1 -1
  15. package/framework/commands/morph-preflight.md +2 -3
  16. package/framework/commands/morph-proposal.md +2 -50
  17. package/framework/commands/morph-status.md +3 -3
  18. package/framework/docs/CLI.md +585 -286
  19. package/framework/evals/review-checklist.md +82 -41
  20. package/framework/hooks/README.md +10 -11
  21. package/framework/hooks/claude-code/core/comment-guard.js +1 -1
  22. package/framework/hooks/claude-code/core/denylist.json +33 -3
  23. package/framework/hooks/claude-code/core/enforce-phase-writes.js +16 -20
  24. package/framework/hooks/claude-code/core/gate-guard.js +2 -11
  25. package/framework/hooks/claude-code/core/pre-bash-denylist.js +38 -0
  26. package/framework/hooks/claude-code/core/protect-spec-files.js +30 -242
  27. package/framework/hooks/claude-code/core/state-sync.js +7 -21
  28. package/framework/hooks/claude-code/core/telemetry-log.js +3 -3
  29. package/framework/hooks/claude-code/core/validate-completion.js +33 -97
  30. package/framework/hooks/claude-code/core/warn-bash-source-write.js +31 -0
  31. package/framework/hooks/claude-code/core/worktree-context.js +5 -4
  32. package/framework/hooks/claude-code/statusline.py +55 -41
  33. package/framework/hooks/package.json +3 -0
  34. package/framework/hooks/shared/activity-logger.js +1 -1
  35. package/framework/hooks/shared/build-check.js +10 -6
  36. package/framework/hooks/shared/command-tokens.js +169 -0
  37. package/framework/hooks/shared/comment-rules.js +1 -1
  38. package/framework/hooks/shared/comment-scanner.js +10 -23
  39. package/framework/hooks/shared/comment-stripper.js +1 -1
  40. package/framework/hooks/shared/e2e-check.js +64 -27
  41. package/framework/hooks/shared/errata.js +72 -0
  42. package/framework/hooks/shared/feature-locator.js +1 -1
  43. package/framework/hooks/shared/frontend-locations.js +6 -0
  44. package/framework/hooks/shared/gate-question.js +18 -0
  45. package/framework/hooks/shared/phase-utils.js +15 -32
  46. package/framework/hooks/shared/slice-progress.js +118 -0
  47. package/framework/hooks/shared/spawn-command.js +23 -0
  48. package/framework/hooks/shared/state-reader.js +61 -87
  49. package/framework/hooks/shared/state-writer.js +2 -3
  50. package/framework/mcp/registry.json +3 -4
  51. package/framework/phases.json +23 -39
  52. package/framework/schemas/config.schema.json +14 -9
  53. package/framework/schemas/feature.schema.json +15 -50
  54. package/framework/skills/README.md +3 -4
  55. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  56. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  57. package/framework/skills/core/morph-apply/SKILL.md +149 -222
  58. package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
  59. package/framework/skills/core/morph-machine/SKILL.md +2 -2
  60. package/framework/skills/core/morph-media/SKILL.md +1 -1
  61. package/framework/skills/core/morph-plan/SKILL.md +137 -299
  62. package/framework/skills/core/morph-review/SKILL.md +111 -136
  63. package/framework/skills/core/morph-standards/SKILL.md +27 -39
  64. package/framework/skills/core/morph-uiux/SKILL.md +77 -152
  65. package/framework/standards/STANDARDS.json +86 -1218
  66. package/framework/standards/STANDARDS.meta.json +1305 -0
  67. package/framework/standards/ai-agents/evals-with-cache.md +1 -2
  68. package/framework/standards/ai-agents/setup.md +1 -1
  69. package/framework/standards/architecture/code-comments.md +6 -6
  70. package/framework/standards/integration/mcp/mcp-tools.md +7 -7
  71. package/framework/templates/README.md +2 -3
  72. package/framework/templates/REGISTRY.json +6 -170
  73. package/framework/templates/docs/plan.md +72 -0
  74. package/framework/templates/dotnet/ai-kit/README.md +1 -9
  75. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
  76. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
  77. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
  78. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
  79. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
  80. package/package.json +1 -1
  81. package/src/commands/e2e/e2e.js +2 -2
  82. package/src/commands/observability/cost.js +18 -60
  83. package/src/commands/project/doctor-procs.js +1 -1
  84. package/src/commands/project/doctor.js +9 -9
  85. package/src/commands/project/fleet.js +4 -4
  86. package/src/commands/project/heavy.js +3 -3
  87. package/src/commands/project/init.js +4 -0
  88. package/src/commands/project/setup-infra-cmd.js +2 -1
  89. package/src/commands/project/stack-scan.js +58 -0
  90. package/src/commands/project/standards.js +79 -25
  91. package/src/commands/project/status.js +29 -69
  92. package/src/commands/project/update.js +17 -1
  93. package/src/commands/project/worktree.js +1 -1
  94. package/src/commands/state/advance.js +62 -46
  95. package/src/commands/state/approve.js +81 -141
  96. package/src/commands/state/archive.js +20 -84
  97. package/src/commands/state/auto-approval.js +68 -0
  98. package/src/commands/state/create.js +28 -1
  99. package/src/commands/state/delete.js +1 -1
  100. package/src/commands/state/finish.js +11 -23
  101. package/src/commands/state/gate-check.js +85 -84
  102. package/src/commands/state/gate-decision.js +6 -4
  103. package/src/commands/state/handoff.js +1 -1
  104. package/src/commands/state/list.js +9 -9
  105. package/src/commands/state/report-check.js +157 -311
  106. package/src/commands/state/retype.js +9 -3
  107. package/src/commands/validation/mutate.js +77 -14
  108. package/src/commands/validation/plan-check.js +81 -0
  109. package/src/commands/validation/validate-feature.js +60 -135
  110. package/src/commands/validation/verify.js +40 -72
  111. package/src/core/paths/output-schema.js +19 -66
  112. package/src/core/state/gate-order.js +5 -6
  113. package/src/core/state/lifecycle-audit.js +2 -2
  114. package/src/core/state/state-manager.js +17 -98
  115. package/src/core/templates/template-registry.js +2 -2
  116. package/src/core/templates/template-seed.js +22 -0
  117. package/src/core/work-types.js +10 -10
  118. package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
  119. package/src/dashboard/app/components/CommandPalette.jsx +5 -5
  120. package/src/dashboard/app/components/EntityLink.jsx +1 -1
  121. package/src/dashboard/app/hooks/useMorphQuery.js +1 -1
  122. package/src/dashboard/app/lib/phases.js +2 -4
  123. package/src/dashboard/app/pages/Activity.jsx +1 -1
  124. package/src/dashboard/app/pages/FeatureDetail.jsx +139 -129
  125. package/src/dashboard/app/pages/Features.jsx +9 -9
  126. package/src/dashboard/app/pages/Guide.jsx +2 -2
  127. package/src/dashboard/app/pages/PhasePipeline.jsx +3 -4
  128. package/src/dashboard/app/pages/Standards.jsx +1 -1
  129. package/src/dashboard/scanner/activity-scanner.js +5 -15
  130. package/src/dashboard/scanner/agents-scanner.js +1 -1
  131. package/src/dashboard/scanner/docs-scanner.js +2 -2
  132. package/src/dashboard/scanner/features-scanner.js +95 -60
  133. package/src/dashboard/scanner/project-api.js +1 -1
  134. package/src/dashboard/scanner/standards-scanner.js +2 -1
  135. package/src/lib/ci/ci-verify.js +17 -1
  136. package/src/lib/ci/github-ci.js +1 -1
  137. package/src/lib/detectors/index.js +3 -8
  138. package/src/lib/generators/handoff-generator.js +34 -65
  139. package/src/lib/generators/settings-generator.js +11 -89
  140. package/src/lib/git/evidence-staleness.js +1 -1
  141. package/src/lib/git/tree-fingerprint.js +19 -7
  142. package/src/lib/git/tree-state.js +2 -2
  143. package/src/lib/heavy/cleanup.js +1 -5
  144. package/src/lib/installers/memory-installer.js +6 -8
  145. package/src/lib/mutation/commands.js +3 -2
  146. package/src/lib/{tasks/dotnet-test-introspection.js → mutation/dotnet-introspection.js} +17 -282
  147. package/src/lib/mutation/plan.js +14 -6
  148. package/src/lib/mutation/runner.js +1 -1
  149. package/src/lib/mutation/subset.js +46 -9
  150. package/src/lib/plan/artifact-vocabulary.js +60 -0
  151. package/src/lib/plan/checks.js +234 -0
  152. package/src/lib/plan/plan-parser.js +209 -0
  153. package/src/lib/plan/plan-template.js +18 -0
  154. package/src/lib/review-reports.js +222 -0
  155. package/src/lib/risk-detector.js +131 -140
  156. package/src/lib/specs/adr-index.js +50 -69
  157. package/src/lib/specs/design-seed.js +18 -0
  158. package/src/lib/specs/frontmatter.js +44 -0
  159. package/src/lib/specs/gfm-table.js +17 -0
  160. package/src/lib/specs/old-format.js +68 -0
  161. package/src/lib/specs/revocations.js +37 -95
  162. package/src/lib/specs/screen-checklist.js +18 -17
  163. package/src/lib/stack/dotnet-build-ignores.js +47 -0
  164. package/src/lib/stack/stack-scan.js +267 -0
  165. package/src/lib/standards/registry.js +43 -0
  166. package/src/lib/standards/sections.js +58 -0
  167. package/src/lib/tasks/eval-node.js +4 -12
  168. package/src/lib/tasks/test-nodes.js +1 -74
  169. package/src/lib/tasks/test-runner.js +75 -117
  170. package/src/lib/telemetry/metrics.js +8 -69
  171. package/src/lib/telemetry/telemetry-reader.js +5 -118
  172. package/src/lib/telemetry/verify-event.js +5 -7
  173. package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
  174. package/src/lib/validators/contracts/contract-compliance-validator.js +2 -2
  175. package/src/lib/validators/reuse/reuse-validator.js +22 -45
  176. package/src/lib/validators/traceability-check.js +85 -129
  177. package/src/lib/validators/verify-runner.js +118 -235
  178. package/src/lib/worktree/docker-audit.js +1 -2
  179. package/src/lib/worktree/fleet-scanner.js +25 -38
  180. package/src/scripts/setup-infra.js +9 -0
  181. package/src/utils/agents-installer.js +76 -9
  182. package/src/utils/claude-settings-manager.js +0 -35
  183. package/src/utils/file-copier.js +27 -1
  184. package/src/utils/hooks-installer.js +16 -9
  185. package/src/utils/skills-installer.js +1 -2
  186. package/framework/evals/trace-schema.md +0 -67
  187. package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
  188. package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
  189. package/framework/hooks/git/pre-commit/specs.sh +0 -50
  190. package/framework/hooks/shared/tasks-validator.js +0 -111
  191. package/framework/rules/morph-task-tracking.md +0 -54
  192. package/framework/rules/morph-workflow.md +0 -244
  193. package/framework/schemas/tasks.schema.json +0 -79
  194. package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
  195. package/framework/skills/core/morph-implement/SKILL.md +0 -194
  196. package/framework/skills/core/morph-proposal/SKILL.md +0 -152
  197. package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
  198. package/framework/templates/docs/proposal.md +0 -192
  199. package/framework/templates/docs/spec.md +0 -57
  200. package/framework/templates/docs/ui-components.md +0 -52
  201. package/framework/templates/docs/ui-design-system.md +0 -76
  202. package/framework/templates/docs/ui-flows.md +0 -167
  203. package/framework/templates/examples/spec-examples.md +0 -91
  204. package/framework/templates/feature/decisions.md +0 -124
  205. package/framework/templates/feature/recap.md +0 -145
  206. package/framework/templates/tasks.template.json +0 -17
  207. package/src/commands/state/usecase.js +0 -143
  208. package/src/commands/tasks/dag.js +0 -181
  209. package/src/lib/agents/output-collector.js +0 -76
  210. package/src/lib/evaluator-report.js +0 -104
  211. package/src/lib/specs/acervo-format.js +0 -70
  212. package/src/lib/specs/acervo-lifecycle.js +0 -264
  213. package/src/lib/specs/done-criteria-check.js +0 -200
  214. package/src/lib/specs/usecase-check.js +0 -301
  215. package/src/lib/specs/usecase-promoter.js +0 -301
  216. package/src/lib/specs/usecase-registry.js +0 -237
  217. package/src/lib/tasks/dispatch-planner.js +0 -319
  218. package/src/lib/tasks/mermaid-renderer.js +0 -132
  219. package/src/lib/tasks/task-classifier.js +0 -30
  220. package/src/lib/tasks/task-parser.js +0 -209
  221. package/src/lib/tasks/tasks-json-parser.js +0 -296
  222. package/src/lib/tasks/test-scope.js +0 -597
@@ -9,13 +9,13 @@ export async function validateContracts(projectPath, featureName, options = {})
9
9
 
10
10
  const skipped = (message) => ({ status: 'ok', errors: 0, warnings: 0, issues: [], coverage: null, message });
11
11
 
12
- if (!existsSync(contractsPath)) return skipped('No spec.md found — skipping');
12
+ if (!existsSync(contractsPath)) return skipped('No plan.md found — skipping');
13
13
 
14
14
  const contractsContent = readFileSync(contractsPath, 'utf-8');
15
15
  const contracts = parseContracts(contractsContent);
16
16
 
17
17
  if (contracts.interfaces.length === 0 && contracts.records.length === 0 && contracts.enums.length === 0) {
18
- return skipped('spec.md declares no interfaces/records/enums — skipping');
18
+ return skipped('plan.md declares no interfaces/records/enums — skipping');
19
19
  }
20
20
 
21
21
  // Find all .cs files in the project (excluding .morph outputs, bin, obj)
@@ -3,11 +3,11 @@
3
3
  *
4
4
  * A terceira camada do ciclo de reuso:
5
5
  *
6
- * PROSA (morph-plan, personas) ──► ARTEFATO (`reuse` em tasks.json)
6
+ * PROSA (morph-plan, personas) ──► ARTEFATO (seção de reuso do plano)
7
7
  * ──► COBRANÇA (este arquivo)
8
8
  *
9
- * Prosa sozinha não segura — `morph-brainstorming/SKILL.md:39` já manda olhar
10
- * o que existe, e o ProspectPRO redefiniu `SimpleBuilding` em cinco arquivos da
9
+ * Prosa sozinha não segura — a skill de planejamento já mandava olhar o que
10
+ * existe, e o ProspectPRO redefiniu `SimpleBuilding` em cinco arquivos da
11
11
  * MESMA pasta assim mesmo: só a verificação em código segura o ciclo.
12
12
  *
13
13
  * Regras, na ordem em que reduzem ruído:
@@ -19,9 +19,9 @@
19
19
  * não se resolve por aviso, e fica FORA daqui de propósito.
20
20
  * 3. Mesma feature = achado (`error`); feature diferente = aviso
21
21
  * (`warning`), porque VSA prescreve DTO homônimo entre slices.
22
- * 4. Veredito `CRIAR` declarado no plano (tasks.json `reuse`) rebaixa o
23
- * achado a aviso: a decisão foi tomada no Gate 2, com justificativa em
24
- * `why`. O validador cobra a decisão AUSENTE, não a decisão tomada.
22
+ * 4. Veredito `CRIAR` declarado na tabela de reuso do plano rebaixa o
23
+ * achado a aviso: a decisão foi tomada no gate do plano, com justificativa
24
+ * em `why`. O validador cobra a decisão AUSENTE, não a decisão tomada.
25
25
  *
26
26
  * Sem grafo o validador não roda (ADR-002: ausência de grafo nunca bloqueia).
27
27
  *
@@ -34,13 +34,13 @@ import { join, isAbsolute } from 'path';
34
34
 
35
35
  import { loadGraph } from '../../graph/graph-loader.js';
36
36
  import { languageOf } from '../../graph/sanitize.js';
37
- import { getAbsoluteOutputPath } from '../../../core/paths/output-schema.js';
38
37
  import {
39
38
  featureOf,
40
39
  isIdiomaticLocalName,
41
40
  isScannableFile,
42
41
  scanDeclarations,
43
42
  } from './symbol-scanner.js';
43
+ import { readPlanChecks } from '../../plan/checks.js';
44
44
 
45
45
  /** Quantos locais citar por achado antes de resumir com "(+N)". */
46
46
  const MAX_CITED = 4;
@@ -154,7 +154,7 @@ export function findDuplicateSymbols(declarations, existing, opts = {}) {
154
154
  ? `Reuse ou estenda o tipo existente em vez de redeclarar. Confirme com ` +
155
155
  `\`morph-spec graph explain ${decls[0].name}\` (sem grafo: ` +
156
156
  `\`grep -rn "${decls[0].name}"\`) e, se a criação for mesmo necessária, ` +
157
- 'registre o veredito CRIAR com a justificativa em "why" no tasks[].reuse da task.'
157
+ 'registre o veredito CRIAR com a justificativa em "why" na tabela ## Reuso do plan.md.'
158
158
  : `VSA tolera DTO homônimo entre slices — confirme que são conceitos ` +
159
159
  `diferentes e não a mesma coisa escrita duas vezes.`,
160
160
  });
@@ -167,9 +167,7 @@ export function findDuplicateSymbols(declarations, existing, opts = {}) {
167
167
  }
168
168
 
169
169
  /**
170
- * Vereditos de reuso carimbados no plano: `tasks[].reuse[]`.
171
- *
172
- * O veredito mora só no `tasks.json`: dado estruturado, lido aqui e no Gate 2.
170
+ * Vereditos de reuso do plano: a tabela `## Reuso` que o plan-check extraiu.
173
171
  *
174
172
  * @param {string} projectPath
175
173
  * @param {string} featureName
@@ -178,24 +176,13 @@ export function findDuplicateSymbols(declarations, existing, opts = {}) {
178
176
  export function readReuseVerdicts(projectPath, featureName) {
179
177
  const verdicts = new Map();
180
178
  if (!featureName) return verdicts;
181
-
182
- try {
183
- const tasksPath = getAbsoluteOutputPath(projectPath, featureName, 'tasks');
184
- if (!existsSync(tasksPath)) return verdicts;
185
- const doc = JSON.parse(readFileSync(tasksPath, 'utf8').replace(/^/, ''));
186
- for (const task of doc.tasks ?? []) {
187
- for (const entry of task.reuse ?? []) {
188
- if (!entry?.concept) continue;
189
- verdicts.set(String(entry.concept).toLowerCase(), {
190
- verdict: String(entry.verdict || '').toUpperCase(),
191
- target: entry.target,
192
- });
193
- }
194
- }
195
- } catch {
196
- // Plano ilegível não é problema deste validador — o tasks-json-guard já
197
- // cobra o schema, e aqui a ausência de veredito só torna a cobrança mais
198
- // rígida, nunca mais frouxa.
179
+ // Sem extração, nenhum veredito: a cobrança só fica mais rígida, nunca mais frouxa.
180
+ for (const entry of readPlanChecks(projectPath, featureName).checks?.reuse ?? []) {
181
+ if (!entry?.concept) continue;
182
+ verdicts.set(String(entry.concept).toLowerCase(), {
183
+ verdict: String(entry.verdict || '').toUpperCase(),
184
+ target: entry.target,
185
+ });
199
186
  }
200
187
  return verdicts;
201
188
  }
@@ -204,9 +191,8 @@ export function readReuseVerdicts(projectPath, featureName) {
204
191
  * Arquivos que esta feature criou ou alterou.
205
192
  *
206
193
  * Duas fontes, unidas, porque cada uma é cega no momento em que a outra
207
- * enxerga: `outputs` do `tasks.json` é o contrato do framework (morph-implement
208
- * §8 exige listar TODO arquivo produzido) e sobrevive ao commit; `git status`
209
- * pega o que ainda não foi registrado numa task. Sem git, sem drama.
194
+ * enxerga: os `arquivos:` das fatias são o contrato do plano e sobrevivem ao
195
+ * commit; `git status` pega o que o plano não previu. Sem git, sem drama.
210
196
  *
211
197
  * @param {string} projectPath
212
198
  * @param {string} featureName
@@ -214,19 +200,10 @@ export function readReuseVerdicts(projectPath, featureName) {
214
200
  */
215
201
  export function collectChangedFiles(projectPath, featureName) {
216
202
  const files = new Set();
217
-
218
- try {
219
- const tasksPath = getAbsoluteOutputPath(projectPath, featureName, 'tasks');
220
- if (existsSync(tasksPath)) {
221
- const doc = JSON.parse(readFileSync(tasksPath, 'utf8').replace(/^/, ''));
222
- for (const task of doc.tasks ?? []) {
223
- for (const out of task.outputs ?? []) {
224
- if (typeof out === 'string') files.add(out.replace(/\\/g, '/'));
225
- }
226
- }
203
+ for (const slice of readPlanChecks(projectPath, featureName).checks?.slices ?? []) {
204
+ for (const file of slice.files ?? []) {
205
+ if (typeof file === 'string') files.add(file.replace(/\\/g, '/'));
227
206
  }
228
- } catch {
229
- // tasks.json ausente/ilegível: sobra o git.
230
207
  }
231
208
 
232
209
  try {
@@ -247,7 +224,7 @@ export function collectChangedFiles(projectPath, featureName) {
247
224
  if (path) files.add(path.replace(/\\/g, '/'));
248
225
  }
249
226
  } catch {
250
- // Fora de um repo git, ou git indisponível: o tasks.json já respondeu.
227
+ // Fora de um repo git, ou git indisponível: o plano já respondeu.
251
228
  }
252
229
 
253
230
  return [...files];
@@ -1,89 +1,69 @@
1
1
  /**
2
2
  * traceability-check.js — advisory, feature-scope-only check: do the files
3
- * cited in recap.md's "**Files:**" bullet lists have an owning task in
4
- * tasks.json's `outputs`?
3
+ * cited under `**Arquivos:**` in the slice reports (`3-implement/reports/F{n}.md`)
4
+ * belong to some slice of the plan (its `arquivos:`, as extracted by plan-check)?
5
5
  *
6
- * This is the DETERMINISTIC half of a two-layer fix. The NLH half lives in
7
- * `framework/skills/core/morph-implement/SKILL.md` §7 ("outputs DEVE listar
8
- * TODO arquivo criado ou modificado pela task") and targets the root cause: a
9
- * file shipped without EVER being declared anywhere (not in outputs, not in
10
- * recap.md — the real-world case that motivated this module, see the finding
11
- * below). This module cannot catch THAT class of drift — a file absent from
12
- * both documents is invisible to a check that only compares the two documents
13
- * against each other. What it DOES catch is a narrower, complementary class:
14
- * recap.md and tasks.json DISAGREEING — an author cited a file in the recap's
15
- * prose but forgot (or a later task re-touched it without) declaring it in any
16
- * task's `outputs`. That disagreement is real signal even though it is not the
17
- * same defect the SKILL.md rule targets.
6
+ * It catches the reports and the plan DISAGREEING: a file the implementation
7
+ * says it touched that no slice declared. A file absent from both documents
8
+ * is invisible to it by construction — that drift is the reuse/contract
9
+ * reviewer's job.
18
10
  *
19
- * Motivating evidence: run 2026-08-12T133732Z-vet-visit-followup-reminder —
20
- * `ApplicationDbContextFactory.cs` shipped in production with no owning task
21
- * in `outputs` AND no mention in recap.md (out of this module's reach by
22
- * construction). Separately, `src/VetClinic.Api/appsettings.json` and
23
- * `Properties/launchSettings.json` WERE cited in T1's recap `**Files:**` list
24
- * but never appear in any task's `outputs` — exactly the disagreement this
25
- * module flags.
11
+ * Advisory, never blocking: `status` is only ever 'pass' | 'warn' | 'skip'.
12
+ * `runVerify()` keeps this result OUTSIDE `nodes` so it can never flip the
13
+ * overall build/tests/validators/e2e status.
26
14
  *
27
- * Advisory, never blocking: the returned `status` is only ever 'pass' | 'warn'
28
- * | 'skip' — never 'fail'. `runVerify()` keeps this result OUTSIDE `nodes` on
29
- * purpose so it can never flip the overall build/tests/validators/e2e status
30
- * computed from `nodes` alone (see verify-runner.js).
31
- *
32
- * Fail-open throughout: no recap.md, no tasks.json, or a parse error all
33
- * degrade to `{ status: 'skip' }` rather than throwing or warning about the
34
- * absence itself — this is rastreabilidade tooling, not a completeness gate.
15
+ * Fail-open throughout: no slice report, no extracted plan or a parse error
16
+ * all degrade to `{ status: 'skip' }`.
35
17
  *
36
18
  * @module traceability-check
37
19
  */
38
20
 
39
21
  import { existsSync, readFileSync } from 'fs';
40
- import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
22
+ import { join } from 'path';
23
+ import { readPlanChecks } from '../plan/checks.js';
24
+ import { sliceProgress, sliceReportPath } from '../../../framework/hooks/shared/slice-progress.js';
41
25
 
42
- /** Matches a `**Files:**` marker followed by one or more `- ` bullet lines. */
43
- const FILES_BLOCK_RE = /\*\*Files:\*\*[ \t]*\r?\n((?:^[ \t]*-[ \t].*\r?\n?)+)/gm;
26
+ /**
27
+ * An `**Arquivos:**` marker and what follows it: the rest of its own line
28
+ * (inline list) plus any `- ` bullet lines right below.
29
+ */
30
+ const FILES_BLOCK_RE = /\*\*Arquivos:\*\*([^\r\n]*)\r?\n?((?:^[ \t]*-[ \t].*\r?\n?)*)/gm;
44
31
 
45
- /** Backtick-quoted spans within a bullet line. */
32
+ /** Backtick-quoted spans within a line. */
46
33
  const BACKTICK_RE = /`([^`]+)`/g;
47
34
 
48
35
  /**
49
- * Extract the set of file paths cited under every "**Files:**" block in a
50
- * recap.md's text, applying a conservative decomposition of the natural
51
- * language shorthand real recaps use:
36
+ * Extract the set of file paths cited under every `**Arquivos:**` marker of a
37
+ * slice report, applying a conservative decomposition of the shorthand real
38
+ * reports use:
52
39
  *
53
40
  * - A line with exactly one backtick span: that span is a candidate,
54
- * whether or not it contains a `/` (a legitimate top-level file, e.g.
55
- * `` `VetClinic.sln` ``, has no slash).
56
- * - A line with multiple comma-separated backtick spans (e.g.
57
- * `` `a/b.json`, `c.json`, `d/e.json` ``): only the FIRST span and any
41
+ * whether or not it contains a `/`.
42
+ * - A line with several comma-separated spans: only the FIRST span and any
58
43
  * LATER span that itself contains `/` are candidates. A later span with
59
- * no `/` is ambiguous shorthand (a bare filename implicitly relative to
60
- * the first span's directory, or a suffix like `` `.Designer.cs` ``) and
61
- * is skipped rather than guessed — false negatives are the acceptable
62
- * failure mode for advisory tooling, false positives are not.
63
- * - Any span containing `{`/`}` (brace-expansion shorthand, e.g.
64
- * `` `Foo/{A,B,C}.cs` ``) is skipped entirely — decomposing it correctly
65
- * would require guessing at delimiters this format doesn't standardize.
44
+ * no `/` is ambiguous shorthand and is skipped rather than guessed —
45
+ * false negatives are acceptable for advisory tooling, false positives
46
+ * are not.
47
+ * - Any span containing `{`/`}` (brace expansion) is skipped.
66
48
  * - Any span ending in `/` is a directory reference, not a file — skipped.
67
49
  *
68
- * @param {string} recapText
50
+ * @param {string} reportText
69
51
  * @returns {string[]} de-duplicated, normalized (forward-slash) candidate paths
70
52
  */
71
- export function extractCitedFiles(recapText) {
53
+ export function extractCitedFiles(reportText) {
72
54
  const cited = new Set();
73
- if (!recapText) return [];
55
+ if (!reportText) return [];
74
56
 
75
57
  FILES_BLOCK_RE.lastIndex = 0;
76
58
  let blockMatch;
77
- while ((blockMatch = FILES_BLOCK_RE.exec(recapText)) !== null) {
78
- const block = blockMatch[1];
79
- const lines = block.split(/\r?\n/).filter((l) => /^[ \t]*-[ \t]/.test(l));
80
-
81
- for (const line of lines) {
59
+ while ((blockMatch = FILES_BLOCK_RE.exec(reportText)) !== null) {
60
+ const bullets = blockMatch[2].split(/\r?\n/).filter((l) => /^[ \t]*-[ \t]/.test(l));
61
+ for (const line of [blockMatch[1], ...bullets]) {
82
62
  const spans = [...line.matchAll(BACKTICK_RE)].map((m) => m[1].trim()).filter(Boolean);
83
63
  spans.forEach((span, idx) => {
84
- if (idx > 0 && !span.includes('/')) return; // ambiguous shorthand continuation
85
- if (span.includes('{') || span.includes('}')) return; // brace-expansion, too ambiguous to decompose
86
- if (span.endsWith('/')) return; // directory reference, not a file
64
+ if (idx > 0 && !span.includes('/')) return;
65
+ if (span.includes('{') || span.includes('}')) return;
66
+ if (span.endsWith('/')) return;
87
67
  cited.add(normalizePath(span));
88
68
  });
89
69
  }
@@ -92,114 +72,90 @@ export function extractCitedFiles(recapText) {
92
72
  return [...cited];
93
73
  }
94
74
 
95
- /**
96
- * Backslashes to forward slashes, strip a leading `./`.
97
- *
98
- * Exported because `src/lib/tasks/test-scope.js` normalises the SAME thing —
99
- * a task's `outputs` entry — before deciding which tests a task-scoped verify
100
- * runs. One normaliser for `outputs`, so the traceability check and the test
101
- * recorte never disagree about which file a task delivered.
102
- */
75
+ /** Backslashes to forward slashes, strip a leading `./`. */
103
76
  export function normalizePath(p) {
104
77
  return String(p).replace(/\\/g, '/').replace(/^\.\//, '');
105
78
  }
106
79
 
107
80
  /**
108
- * Union of `outputs` across every `done` task in tasks.json, normalized.
81
+ * Union of the `arquivos:` of every slice of the plan, normalized.
109
82
  *
110
- * @param {string} projectPath
111
- * @param {string} featureName
112
- * @returns {string[]|null} null when tasks.json is missing/unreadable/invalid
113
- * (the caller treats null as "cannot judge coverage" and skips, not warns)
83
+ * @returns {string[]|null} null when the plan has no readable extraction
114
84
  */
115
- export function collectDoneOutputs(projectPath, featureName) {
116
- let tasksPath;
117
- try {
118
- tasksPath = getAbsoluteOutputPath(projectPath, featureName, 'tasks');
119
- } catch {
120
- return null;
121
- }
122
- if (!existsSync(tasksPath)) return null;
123
-
124
- let data;
125
- try {
126
- data = JSON.parse(readFileSync(tasksPath, 'utf8'));
127
- } catch {
128
- return null;
129
- }
130
-
131
- const tasks = Array.isArray(data?.tasks) ? data.tasks : [];
132
- const outputs = [];
133
- for (const task of tasks) {
134
- if (task?.status !== 'done') continue;
135
- for (const output of task.outputs || []) {
136
- if (typeof output === 'string' && output.trim()) outputs.push(normalizePath(output));
137
- }
138
- }
139
- return outputs;
85
+ export function collectSliceFiles(projectPath, featureName) {
86
+ const checks = readPlanChecks(projectPath, featureName).checks;
87
+ if (!checks) return null;
88
+ return checks.slices.flatMap((slice) => (slice.files ?? [])
89
+ .filter((f) => typeof f === 'string' && f.trim())
90
+ .map(normalizePath));
140
91
  }
141
92
 
142
93
  /**
143
- * A cited file is covered if some done task's `outputs` names it exactly, or
144
- * names a directory (an entry ending in `/`) that the cited file falls under.
94
+ * A cited file is covered if some slice names it exactly, or names a
95
+ * directory (an entry ending in `/`) that the cited file falls under.
145
96
  *
146
97
  * @param {string} file
147
- * @param {string[]} outputs
98
+ * @param {string[]} declared
148
99
  * @returns {boolean}
149
100
  */
150
- function isCovered(file, outputs) {
151
- for (const output of outputs) {
152
- if (output === file) return true;
153
- if (output.endsWith('/') && file.startsWith(output)) return true;
101
+ function isCovered(file, declared) {
102
+ for (const entry of declared) {
103
+ if (entry === file) return true;
104
+ if (entry.endsWith('/') && file.startsWith(entry)) return true;
154
105
  }
155
106
  return false;
156
107
  }
157
108
 
109
+ function readOrNull(path) {
110
+ try {
111
+ return existsSync(path) ? readFileSync(path, 'utf8') : null;
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
116
+
158
117
  /**
159
- * Run the recap-vs-outputs traceability check for a feature.
118
+ * Run the reports-vs-slices traceability check for a feature.
160
119
  *
161
120
  * @param {string} projectPath
162
121
  * @param {string} featureName
163
122
  * @returns {{ status: 'pass'|'warn'|'skip', warnings: string[], reason: string|null, citedCount: number }}
164
123
  */
165
124
  export function checkOutputsTraceability(projectPath, featureName) {
166
- let recapPath;
167
- try {
168
- recapPath = getAbsoluteOutputPath(projectPath, featureName, 'recap');
169
- } catch {
170
- return { status: 'skip', warnings: [], reason: 'unknown feature', citedCount: 0 };
171
- }
172
-
173
- if (!existsSync(recapPath)) {
174
- return { status: 'skip', warnings: [], reason: 'no recap.md', citedCount: 0 };
125
+ const featureDir = join(projectPath, '.morph', 'features', featureName);
126
+ const progress = sliceProgress(featureDir);
127
+ const reports = (progress?.slices ?? [])
128
+ .map((slice) => ({ id: slice.id, text: readOrNull(sliceReportPath(featureDir, slice.id)) }))
129
+ .filter((r) => r.text !== null);
130
+ if (reports.length === 0) {
131
+ return { status: 'skip', warnings: [], reason: 'no slice reports', citedCount: 0 };
175
132
  }
176
133
 
177
- let recapText;
178
- try {
179
- recapText = readFileSync(recapPath, 'utf8');
180
- } catch {
181
- return { status: 'skip', warnings: [], reason: 'recap.md unreadable', citedCount: 0 };
134
+ const citedBy = new Map();
135
+ for (const { id, text } of reports) {
136
+ for (const file of extractCitedFiles(text)) {
137
+ if (!citedBy.has(file)) citedBy.set(file, []);
138
+ citedBy.get(file).push(id);
139
+ }
182
140
  }
183
-
184
- const cited = extractCitedFiles(recapText);
185
- if (cited.length === 0) {
186
- return { status: 'skip', warnings: [], reason: 'no Files: entries found in recap.md', citedCount: 0 };
141
+ if (citedBy.size === 0) {
142
+ return { status: 'skip', warnings: [], reason: 'no Arquivos: entries found in the slice reports', citedCount: 0 };
187
143
  }
188
144
 
189
- const outputs = collectDoneOutputs(projectPath, featureName);
190
- if (outputs === null) {
191
- return { status: 'skip', warnings: [], reason: 'no tasks.json', citedCount: cited.length };
145
+ const declared = collectSliceFiles(projectPath, featureName);
146
+ if (declared === null) {
147
+ return { status: 'skip', warnings: [], reason: 'no checks.json', citedCount: citedBy.size };
192
148
  }
193
149
 
194
- const warnings = cited
195
- .filter((file) => !isCovered(file, outputs))
150
+ const warnings = [...citedBy.keys()]
151
+ .filter((file) => !isCovered(file, declared))
196
152
  .sort()
197
- .map((file) => `${file} — cited in recap.md but not listed in any done task's outputs`);
153
+ .map((file) => `${file} — cited in reports/${citedBy.get(file).join(', ')} but declared in no slice's arquivos:`);
198
154
 
199
155
  return {
200
156
  status: warnings.length > 0 ? 'warn' : 'pass',
201
157
  warnings,
202
158
  reason: null,
203
- citedCount: cited.length,
159
+ citedCount: citedBy.size,
204
160
  };
205
161
  }