@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.
- package/CLAUDE.md +48 -39
- package/README.md +3 -4
- package/bin/morph-spec.js +37 -51
- package/bin/validate.js +3 -111
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +41 -101
- package/framework/MORPH.md +95 -252
- package/framework/agents.json +75 -18
- package/framework/commands/morph-apply.md +5 -28
- package/framework/commands/morph-archive.md +1 -1
- package/framework/commands/morph-hotfix.md +8 -8
- package/framework/commands/morph-infra.md +1 -1
- package/framework/commands/morph-preflight.md +2 -3
- package/framework/commands/morph-proposal.md +2 -50
- package/framework/commands/morph-status.md +3 -3
- package/framework/docs/CLI.md +585 -286
- package/framework/evals/review-checklist.md +82 -41
- package/framework/hooks/README.md +10 -11
- package/framework/hooks/claude-code/core/comment-guard.js +1 -1
- package/framework/hooks/claude-code/core/denylist.json +33 -3
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +16 -20
- package/framework/hooks/claude-code/core/gate-guard.js +2 -11
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +38 -0
- package/framework/hooks/claude-code/core/protect-spec-files.js +30 -242
- package/framework/hooks/claude-code/core/state-sync.js +7 -21
- package/framework/hooks/claude-code/core/telemetry-log.js +3 -3
- package/framework/hooks/claude-code/core/validate-completion.js +33 -97
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +31 -0
- package/framework/hooks/claude-code/core/worktree-context.js +5 -4
- package/framework/hooks/claude-code/statusline.py +55 -41
- package/framework/hooks/package.json +3 -0
- package/framework/hooks/shared/activity-logger.js +1 -1
- package/framework/hooks/shared/build-check.js +10 -6
- package/framework/hooks/shared/command-tokens.js +169 -0
- package/framework/hooks/shared/comment-rules.js +1 -1
- package/framework/hooks/shared/comment-scanner.js +10 -23
- package/framework/hooks/shared/comment-stripper.js +1 -1
- package/framework/hooks/shared/e2e-check.js +64 -27
- package/framework/hooks/shared/errata.js +72 -0
- package/framework/hooks/shared/feature-locator.js +1 -1
- package/framework/hooks/shared/frontend-locations.js +6 -0
- package/framework/hooks/shared/gate-question.js +18 -0
- package/framework/hooks/shared/phase-utils.js +15 -32
- package/framework/hooks/shared/slice-progress.js +118 -0
- package/framework/hooks/shared/spawn-command.js +23 -0
- package/framework/hooks/shared/state-reader.js +61 -87
- package/framework/hooks/shared/state-writer.js +2 -3
- package/framework/mcp/registry.json +3 -4
- package/framework/phases.json +23 -39
- package/framework/schemas/config.schema.json +14 -9
- package/framework/schemas/feature.schema.json +15 -50
- package/framework/skills/README.md +3 -4
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +149 -222
- package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
- package/framework/skills/core/morph-machine/SKILL.md +2 -2
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +137 -299
- package/framework/skills/core/morph-review/SKILL.md +111 -136
- package/framework/skills/core/morph-standards/SKILL.md +27 -39
- package/framework/skills/core/morph-uiux/SKILL.md +77 -152
- package/framework/standards/STANDARDS.json +86 -1218
- package/framework/standards/STANDARDS.meta.json +1305 -0
- package/framework/standards/ai-agents/evals-with-cache.md +1 -2
- package/framework/standards/ai-agents/setup.md +1 -1
- package/framework/standards/architecture/code-comments.md +6 -6
- package/framework/standards/integration/mcp/mcp-tools.md +7 -7
- package/framework/templates/README.md +2 -3
- package/framework/templates/REGISTRY.json +6 -170
- package/framework/templates/docs/plan.md +72 -0
- package/framework/templates/dotnet/ai-kit/README.md +1 -9
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
- package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
- package/package.json +1 -1
- package/src/commands/e2e/e2e.js +2 -2
- package/src/commands/observability/cost.js +18 -60
- package/src/commands/project/doctor-procs.js +1 -1
- package/src/commands/project/doctor.js +9 -9
- package/src/commands/project/fleet.js +4 -4
- package/src/commands/project/heavy.js +3 -3
- package/src/commands/project/init.js +4 -0
- package/src/commands/project/setup-infra-cmd.js +2 -1
- package/src/commands/project/stack-scan.js +58 -0
- package/src/commands/project/standards.js +79 -25
- package/src/commands/project/status.js +29 -69
- package/src/commands/project/update.js +17 -1
- package/src/commands/project/worktree.js +1 -1
- package/src/commands/state/advance.js +62 -46
- package/src/commands/state/approve.js +81 -141
- package/src/commands/state/archive.js +20 -84
- package/src/commands/state/auto-approval.js +68 -0
- package/src/commands/state/create.js +28 -1
- package/src/commands/state/delete.js +1 -1
- package/src/commands/state/finish.js +11 -23
- package/src/commands/state/gate-check.js +85 -84
- package/src/commands/state/gate-decision.js +6 -4
- package/src/commands/state/handoff.js +1 -1
- package/src/commands/state/list.js +9 -9
- package/src/commands/state/report-check.js +157 -311
- package/src/commands/state/retype.js +9 -3
- package/src/commands/validation/mutate.js +77 -14
- package/src/commands/validation/plan-check.js +81 -0
- package/src/commands/validation/validate-feature.js +60 -135
- package/src/commands/validation/verify.js +40 -72
- package/src/core/paths/output-schema.js +19 -66
- package/src/core/state/gate-order.js +5 -6
- package/src/core/state/lifecycle-audit.js +2 -2
- package/src/core/state/state-manager.js +17 -98
- package/src/core/templates/template-registry.js +2 -2
- package/src/core/templates/template-seed.js +22 -0
- package/src/core/work-types.js +10 -10
- package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
- package/src/dashboard/app/components/CommandPalette.jsx +5 -5
- package/src/dashboard/app/components/EntityLink.jsx +1 -1
- package/src/dashboard/app/hooks/useMorphQuery.js +1 -1
- package/src/dashboard/app/lib/phases.js +2 -4
- package/src/dashboard/app/pages/Activity.jsx +1 -1
- package/src/dashboard/app/pages/FeatureDetail.jsx +139 -129
- package/src/dashboard/app/pages/Features.jsx +9 -9
- package/src/dashboard/app/pages/Guide.jsx +2 -2
- package/src/dashboard/app/pages/PhasePipeline.jsx +3 -4
- package/src/dashboard/app/pages/Standards.jsx +1 -1
- package/src/dashboard/scanner/activity-scanner.js +5 -15
- package/src/dashboard/scanner/agents-scanner.js +1 -1
- package/src/dashboard/scanner/docs-scanner.js +2 -2
- package/src/dashboard/scanner/features-scanner.js +95 -60
- package/src/dashboard/scanner/project-api.js +1 -1
- package/src/dashboard/scanner/standards-scanner.js +2 -1
- package/src/lib/ci/ci-verify.js +17 -1
- package/src/lib/ci/github-ci.js +1 -1
- package/src/lib/detectors/index.js +3 -8
- package/src/lib/generators/handoff-generator.js +34 -65
- package/src/lib/generators/settings-generator.js +11 -89
- package/src/lib/git/evidence-staleness.js +1 -1
- package/src/lib/git/tree-fingerprint.js +19 -7
- package/src/lib/git/tree-state.js +2 -2
- package/src/lib/heavy/cleanup.js +1 -5
- package/src/lib/installers/memory-installer.js +6 -8
- package/src/lib/mutation/commands.js +3 -2
- package/src/lib/{tasks/dotnet-test-introspection.js → mutation/dotnet-introspection.js} +17 -282
- package/src/lib/mutation/plan.js +14 -6
- package/src/lib/mutation/runner.js +1 -1
- package/src/lib/mutation/subset.js +46 -9
- package/src/lib/plan/artifact-vocabulary.js +60 -0
- package/src/lib/plan/checks.js +234 -0
- package/src/lib/plan/plan-parser.js +209 -0
- package/src/lib/plan/plan-template.js +18 -0
- package/src/lib/review-reports.js +222 -0
- package/src/lib/risk-detector.js +131 -140
- package/src/lib/specs/adr-index.js +50 -69
- package/src/lib/specs/design-seed.js +18 -0
- package/src/lib/specs/frontmatter.js +44 -0
- package/src/lib/specs/gfm-table.js +17 -0
- package/src/lib/specs/old-format.js +68 -0
- package/src/lib/specs/revocations.js +37 -95
- package/src/lib/specs/screen-checklist.js +18 -17
- package/src/lib/stack/dotnet-build-ignores.js +47 -0
- package/src/lib/stack/stack-scan.js +267 -0
- package/src/lib/standards/registry.js +43 -0
- package/src/lib/standards/sections.js +58 -0
- package/src/lib/tasks/eval-node.js +4 -12
- package/src/lib/tasks/test-nodes.js +1 -74
- package/src/lib/tasks/test-runner.js +75 -117
- package/src/lib/telemetry/metrics.js +8 -69
- package/src/lib/telemetry/telemetry-reader.js +5 -118
- package/src/lib/telemetry/verify-event.js +5 -7
- package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
- package/src/lib/validators/contracts/contract-compliance-validator.js +2 -2
- package/src/lib/validators/reuse/reuse-validator.js +22 -45
- package/src/lib/validators/traceability-check.js +85 -129
- package/src/lib/validators/verify-runner.js +118 -235
- package/src/lib/worktree/docker-audit.js +1 -2
- package/src/lib/worktree/fleet-scanner.js +25 -38
- package/src/scripts/setup-infra.js +9 -0
- package/src/utils/agents-installer.js +76 -9
- package/src/utils/claude-settings-manager.js +0 -35
- package/src/utils/file-copier.js +27 -1
- package/src/utils/hooks-installer.js +16 -9
- package/src/utils/skills-installer.js +1 -2
- package/framework/evals/trace-schema.md +0 -67
- package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
- package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
- package/framework/hooks/git/pre-commit/specs.sh +0 -50
- package/framework/hooks/shared/tasks-validator.js +0 -111
- package/framework/rules/morph-task-tracking.md +0 -54
- package/framework/rules/morph-workflow.md +0 -244
- package/framework/schemas/tasks.schema.json +0 -79
- package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
- package/framework/skills/core/morph-implement/SKILL.md +0 -194
- package/framework/skills/core/morph-proposal/SKILL.md +0 -152
- package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
- package/framework/templates/docs/proposal.md +0 -192
- package/framework/templates/docs/spec.md +0 -57
- package/framework/templates/docs/ui-components.md +0 -52
- package/framework/templates/docs/ui-design-system.md +0 -76
- package/framework/templates/docs/ui-flows.md +0 -167
- package/framework/templates/examples/spec-examples.md +0 -91
- package/framework/templates/feature/decisions.md +0 -124
- package/framework/templates/feature/recap.md +0 -145
- package/framework/templates/tasks.template.json +0 -17
- package/src/commands/state/usecase.js +0 -143
- package/src/commands/tasks/dag.js +0 -181
- package/src/lib/agents/output-collector.js +0 -76
- package/src/lib/evaluator-report.js +0 -104
- package/src/lib/specs/acervo-format.js +0 -70
- package/src/lib/specs/acervo-lifecycle.js +0 -264
- package/src/lib/specs/done-criteria-check.js +0 -200
- package/src/lib/specs/usecase-check.js +0 -301
- package/src/lib/specs/usecase-promoter.js +0 -301
- package/src/lib/specs/usecase-registry.js +0 -237
- package/src/lib/tasks/dispatch-planner.js +0 -319
- package/src/lib/tasks/mermaid-renderer.js +0 -132
- package/src/lib/tasks/task-classifier.js +0 -30
- package/src/lib/tasks/task-parser.js +0 -209
- package/src/lib/tasks/tasks-json-parser.js +0 -296
- 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
|
|
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('
|
|
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 (
|
|
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 —
|
|
10
|
-
*
|
|
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
|
|
23
|
-
* achado a aviso: a decisão foi tomada no
|
|
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"
|
|
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
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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: `
|
|
208
|
-
*
|
|
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
|
-
|
|
219
|
-
|
|
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
|
|
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
|
|
4
|
-
*
|
|
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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
20
|
-
* `
|
|
21
|
-
*
|
|
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
|
-
*
|
|
28
|
-
*
|
|
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 {
|
|
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
|
-
/**
|
|
43
|
-
|
|
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
|
|
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
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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
|
|
55
|
-
*
|
|
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
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
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}
|
|
50
|
+
* @param {string} reportText
|
|
69
51
|
* @returns {string[]} de-duplicated, normalized (forward-slash) candidate paths
|
|
70
52
|
*/
|
|
71
|
-
export function extractCitedFiles(
|
|
53
|
+
export function extractCitedFiles(reportText) {
|
|
72
54
|
const cited = new Set();
|
|
73
|
-
if (!
|
|
55
|
+
if (!reportText) return [];
|
|
74
56
|
|
|
75
57
|
FILES_BLOCK_RE.lastIndex = 0;
|
|
76
58
|
let blockMatch;
|
|
77
|
-
while ((blockMatch = FILES_BLOCK_RE.exec(
|
|
78
|
-
const
|
|
79
|
-
const
|
|
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;
|
|
85
|
-
if (span.includes('{') || span.includes('}')) return;
|
|
86
|
-
if (span.endsWith('/')) return;
|
|
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 `
|
|
81
|
+
* Union of the `arquivos:` of every slice of the plan, normalized.
|
|
109
82
|
*
|
|
110
|
-
* @
|
|
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
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
144
|
-
*
|
|
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[]}
|
|
98
|
+
* @param {string[]} declared
|
|
148
99
|
* @returns {boolean}
|
|
149
100
|
*/
|
|
150
|
-
function isCovered(file,
|
|
151
|
-
for (const
|
|
152
|
-
if (
|
|
153
|
-
if (
|
|
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
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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
|
|
190
|
-
if (
|
|
191
|
-
return { status: 'skip', warnings: [], reason: 'no
|
|
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 =
|
|
195
|
-
.filter((file) => !isCovered(file,
|
|
150
|
+
const warnings = [...citedBy.keys()]
|
|
151
|
+
.filter((file) => !isCovered(file, declared))
|
|
196
152
|
.sort()
|
|
197
|
-
.map((file) => `${file} — cited in
|
|
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:
|
|
159
|
+
citedCount: citedBy.size,
|
|
204
160
|
};
|
|
205
161
|
}
|