@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
package/src/lib/risk-detector.js
CHANGED
|
@@ -9,30 +9,28 @@
|
|
|
9
9
|
* - designSystemDivergence: boolean — conflicting design sources detected
|
|
10
10
|
* - standardsViolations: string[] — violations of the plan's forbidden patterns
|
|
11
11
|
* - verificationStatus: string — último `morph-spec verify` ('fail' pausa, qualquer gate)
|
|
12
|
-
* - verificationScope + gate: string — presente e != 'feature' pausa SÓ no ship: o
|
|
13
|
-
* último `verify` foi um recorte de task, não da feature inteira (`null`/ausente
|
|
14
|
-
* não conta — narrow, nunca widen)
|
|
15
12
|
* - e2eStatus + e2eReason + gate — nó e2e do último verify. Um `skip`
|
|
16
|
-
* cuja razão NÃO seja dispensa legítima pausa SÓ no ship (gate 'review')
|
|
17
|
-
* Sem escopo (stamp sem o campo) cai do mesmo lado seguro que `unknown`
|
|
13
|
+
* cuja razão NÃO seja dispensa legítima pausa SÓ no ship (gate 'review')
|
|
18
14
|
* - verificationStale + gate: bool/str — stamp de um commit cujo código já mudou
|
|
19
15
|
* (fora de .morph/) pausa SÓ no ship (gate 'review')
|
|
20
|
-
* -
|
|
21
|
-
*
|
|
16
|
+
* - reviewReports + gate: object — `reviewSignal` (review-reports.js): os
|
|
17
|
+
* relatórios exigidos e a cobertura dos Cn. No ship, só SHIP sem bloqueador
|
|
18
|
+
* em todo exigido, com todo Cn PROVADO, libera
|
|
22
19
|
* - mediaClaims + gate: object — { section, featureGeneratesMedia }
|
|
23
20
|
* - screenChecklist + gate: object — { mockups, legacy, screens, problems, exempt?,
|
|
24
21
|
* section?, uncovered? } derivado do 1-design/mockups.md. No uiux, checklist
|
|
25
22
|
* de tela incompleto pausa; no ship, item decidido sem a conferência do
|
|
26
|
-
*
|
|
27
|
-
* -
|
|
28
|
-
*
|
|
29
|
-
* -
|
|
23
|
+
* verificador pausa. Mockup legado é isento nos dois, com a razão listada
|
|
24
|
+
* - planChecks + gate: string — `readPlanChecks(...).status`: os checks
|
|
25
|
+
* extraídos contra o plano; no gate do plano, tudo que não é `fresh` pausa
|
|
26
|
+
* - sliceCount: number — fatias extraídas (pausa acima de MAX_SLICES)
|
|
30
27
|
*/
|
|
31
28
|
|
|
32
|
-
|
|
29
|
+
import { MAX_SLICES } from './plan/checks.js';
|
|
30
|
+
import { BLOCKER_TYPES, reviewFileName } from './review-reports.js';
|
|
33
31
|
|
|
34
|
-
// Caminho alterado que pede
|
|
35
|
-
const
|
|
32
|
+
// Caminho alterado que pede a revisão de segurança (persona `security`, opus).
|
|
33
|
+
const SECURITY_PATH_RE = /(^|[/._-])(auth|login|senha|password|token|secret|crypto|permiss|rbac|acl|lgpd|pii|privacy|payment|pagamento|billing|cobranca|invoice|asaas|stripe|migrations?)([/._-]|$)/i;
|
|
36
34
|
|
|
37
35
|
// Work types whose ship gate (review) always pauses for a human, regardless
|
|
38
36
|
// of trust mode — a type policy, not a per-feature risk signal. Mirrors
|
|
@@ -53,8 +51,6 @@ const E2E_SKIP_PATTERNS = [
|
|
|
53
51
|
[/^requested$/i, 'requested'],
|
|
54
52
|
// "project does not containerize (no compose file)" — não há app para subir.
|
|
55
53
|
[/does not containerize/i, 'project'],
|
|
56
|
-
// 'task group "docs" does not touch runtime' — escopo da task, não da app.
|
|
57
|
-
[/does not touch runtime/i, 'scope'],
|
|
58
54
|
];
|
|
59
55
|
|
|
60
56
|
// Classes de skip que NÃO são dispensa: o e2e não rodou e nada no repositório
|
|
@@ -63,23 +59,13 @@ const E2E_SKIP_PATTERNS = [
|
|
|
63
59
|
// esta regra existe para eliminar.
|
|
64
60
|
const E2E_SKIP_CLASSES_THAT_PAUSE = ['environment', 'requested', 'unknown'];
|
|
65
61
|
|
|
66
|
-
// O ÚNICO escopo de stamp que responde pela feature inteira. `morph-spec verify
|
|
67
|
-
// {f} {task}` carimba `scope: 'task:{id}'`, e o nó e2e de um verify por task
|
|
68
|
-
// pula com a razão do GRUPO daquela task ('task group "docs" does not touch
|
|
69
|
-
// runtime') — uma afirmação sobre UMA task, nunca sobre a app. Sem este teste,
|
|
70
|
-
// a última task de uma feature (tipicamente `group: docs`) compra o Gate 3 da
|
|
71
|
-
// feature que ela mesma acabou de escrever, numa máquina onde a app nunca
|
|
72
|
-
// subiu. Ausente/desconhecido cai do mesmo lado seguro que `unknown`: recusar
|
|
73
|
-
// é barato, aprovar sem prova não é.
|
|
74
|
-
const FEATURE_SCOPE = 'feature';
|
|
75
|
-
|
|
76
62
|
/**
|
|
77
63
|
* Classifica a razão literal de um `skip` do nó e2e — de QUEM é a ausência de
|
|
78
64
|
* prova. Existe porque `verificationStatus` é o rollup do `verify` e um nó que
|
|
79
65
|
* pula NÃO o reprova: sem classificar a razão, "o e2e rodou e passou" e "o e2e
|
|
80
66
|
* nunca rodou" chegam ao gate como o mesmo `pass`.
|
|
81
67
|
*
|
|
82
|
-
* A assimetria é o ponto: `project
|
|
68
|
+
* A assimetria é o ponto: `project` descreve o REPOSITÓRIO dizendo que
|
|
83
69
|
* não há runtime a exercer (dispensa legítima, registrada com motivo);
|
|
84
70
|
* `environment`/`requested` descrevem a MÁQUINA ou o operador, e nenhum dos
|
|
85
71
|
* dois é evidência sobre o código. `unknown` cai no lado seguro pelo mesmo
|
|
@@ -88,7 +74,7 @@ const FEATURE_SCOPE = 'feature';
|
|
|
88
74
|
* @param {string|null|undefined} reason - `verification.e2eReason` (a razão
|
|
89
75
|
* literal que o `morph-spec verify` carimbou). Vazia/ausente ⇒ 'unknown' —
|
|
90
76
|
* DESCONHECIDO, jamais lido como dispensa.
|
|
91
|
-
* @returns {'environment'|'requested'|'project'|'
|
|
77
|
+
* @returns {'environment'|'requested'|'project'|'unknown'}
|
|
92
78
|
*/
|
|
93
79
|
export function classifyE2eSkip(reason) {
|
|
94
80
|
const text = typeof reason === 'string' ? reason.trim() : '';
|
|
@@ -116,20 +102,6 @@ function e2ePauseReason(klass, reason) {
|
|
|
116
102
|
return `e2e pulado por razão não classificada: "${literal}" — sem classificação, não há dispensa`;
|
|
117
103
|
}
|
|
118
104
|
|
|
119
|
-
/**
|
|
120
|
-
* Mensagem de recusa por ESCOPO do stamp: a razão do skip É uma dispensa
|
|
121
|
-
* legítima (`project`/`scope`), mas quem a carimbou foi um verify que não
|
|
122
|
-
* responde pela feature. Nomeia o escopo real para que o leitor saiba qual
|
|
123
|
-
* comando rodar, em vez de rodar o mesmo de novo.
|
|
124
|
-
*/
|
|
125
|
-
function e2eScopeRefusal(klass, scope, reason) {
|
|
126
|
-
const literal = typeof reason === 'string' && reason.trim() !== '' ? reason.trim() : 'sem motivo registrado';
|
|
127
|
-
const where = typeof scope === 'string' && scope.trim() !== '' ? scope.trim() : 'ausente';
|
|
128
|
-
return `dispensa de e2e [${klass}] veio de um verify de escopo "${where}", não da feature:`
|
|
129
|
-
+ ` "${literal}" — um stamp de task responde por UMA task, não pela app inteira;`
|
|
130
|
-
+ ' rode `morph-spec verify {feature}` (sem task) antes do ship';
|
|
131
|
-
}
|
|
132
|
-
|
|
133
105
|
/** A razão gravada no sinal quando o checklist de tela isenta um mockup legado. */
|
|
134
106
|
export const LEGACY_MOCKUP_EXEMPTION = 'checklist dispensado: mockup no formato antigo';
|
|
135
107
|
|
|
@@ -159,50 +131,115 @@ export function uiuxChecklistPause(screenChecklist) {
|
|
|
159
131
|
+ ' ("não se aplica" vale, se escrito)';
|
|
160
132
|
}
|
|
161
133
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
134
|
+
const ROLE_PERSONA = { verifier: 'morph-verifier', reviewer: 'morph-reviewer', security: 'morph-security' };
|
|
135
|
+
|
|
136
|
+
/** Por que os relatórios de review seguram o ship, ou null. */
|
|
137
|
+
export function reviewPause(signal) {
|
|
138
|
+
const required = signal?.required ?? ['verifier'];
|
|
139
|
+
const reports = signal?.reports ?? {};
|
|
140
|
+
for (const role of required) {
|
|
141
|
+
const r = reports[role];
|
|
142
|
+
const file = `4-review/${reviewFileName(role)}`;
|
|
143
|
+
if (!r || r.status === 'missing') {
|
|
144
|
+
return `Gate 3 sem \`${file}\` — despache o \`${ROLE_PERSONA[role]}\` (contexto fresco);`
|
|
145
|
+
+ ' auto-avaliação de quem implementou não decide o ship';
|
|
146
|
+
}
|
|
147
|
+
if (r.status === 'partial') {
|
|
148
|
+
return `\`${file}\` está PARCIAL — retome o mesmo agente por SendMessage; parcial não é veredito`;
|
|
149
|
+
}
|
|
150
|
+
if (r.status !== 'ok') {
|
|
151
|
+
return `\`${file}\` sem \`**Verdict:** SHIP|FIX\` — o relatório não seguiu o contrato`;
|
|
152
|
+
}
|
|
174
153
|
}
|
|
175
|
-
|
|
176
|
-
|
|
154
|
+
const present = Object.entries(reports).filter(([, r]) => r.status === 'ok');
|
|
155
|
+
const untyped = present.reduce((n, [, r]) => n + (r.untyped ?? 0), 0);
|
|
156
|
+
if (untyped > 0) {
|
|
157
|
+
return `${untyped} bloqueador(es) sem tipo de evidência (${BLOCKER_TYPES.join(' | ')})`
|
|
177
158
|
+ ' — sem evidência tipada não é bloqueador: reclassifique como nota ou complete a evidência';
|
|
178
159
|
}
|
|
179
|
-
|
|
180
|
-
|
|
160
|
+
for (const [role, r] of present) {
|
|
161
|
+
if ((r.blockers ?? 0) > 0) return `${role} abriu ${r.blockers} bloqueador(es): ${firstOf(r.blockerIds ?? [])}`;
|
|
162
|
+
}
|
|
163
|
+
for (const [role, r] of present) {
|
|
164
|
+
if (r.verdict !== 'SHIP') return `veredito do ${role} é ${r.verdict}`;
|
|
165
|
+
}
|
|
166
|
+
const checks = signal?.checks;
|
|
167
|
+
if (!checks || checks.status !== 'ok') {
|
|
168
|
+
return 'sem `2-plan/checks.json` legível — a cobertura dos cenários pelo verificador não é conferível';
|
|
169
|
+
}
|
|
170
|
+
const gaps = [
|
|
171
|
+
...checks.missing.map((id) => `${id} sem linha`),
|
|
172
|
+
...checks.notProven.map(({ id, status }) => `${id} ${status}`),
|
|
173
|
+
];
|
|
174
|
+
if (gaps.length) {
|
|
175
|
+
return `\`## Checks\` do verificador não prova ${firstOf(gaps)}`
|
|
176
|
+
+ ' — todo cenário precisa de uma linha PROVADO com evidência';
|
|
177
|
+
}
|
|
181
178
|
return null;
|
|
182
179
|
}
|
|
183
180
|
|
|
184
|
-
function
|
|
185
|
-
|
|
186
|
-
|
|
181
|
+
function reviewSummary(signal) {
|
|
182
|
+
const reports = signal?.reports ?? {};
|
|
183
|
+
const roles = Object.keys(reports);
|
|
184
|
+
if (roles.length === 0) return 'Gate 3 sem relatórios de review';
|
|
185
|
+
const verdicts = roles.map((role) => `${role} ${reports[role].verdict ?? reports[role].status}`).join(', ');
|
|
186
|
+
const blockers = roles.reduce((n, role) => n + (reports[role].blockers ?? 0), 0);
|
|
187
|
+
const checks = signal.checks?.status === 'ok'
|
|
188
|
+
? `; checks ${signal.checks.total - signal.checks.missing.length - signal.checks.notProven.length}/${signal.checks.total} PROVADO`
|
|
189
|
+
: '';
|
|
190
|
+
return `review: ${verdicts}; ${blockers} bloqueador(es)${checks}`;
|
|
187
191
|
}
|
|
188
192
|
|
|
189
193
|
/**
|
|
190
|
-
* Modelo do
|
|
191
|
-
*
|
|
192
|
-
*
|
|
194
|
+
* Modelo do verificador: `opus` quando a feature mexe em prompt/modelo ou
|
|
195
|
+
* gera mídia; `sonnet` no resto. Caminho sensível não muda o verificador:
|
|
196
|
+
* despacha a persona `security` ({@link securityReviewFor}).
|
|
193
197
|
*
|
|
194
|
-
* @param {{
|
|
198
|
+
* @param {{ promptModelTouched?: boolean, generatesMedia?: boolean }} input
|
|
195
199
|
* @returns {{ model: 'opus'|'sonnet', reasons: string[] }}
|
|
196
200
|
*/
|
|
197
|
-
export function
|
|
201
|
+
export function verifierModelFor({ promptModelTouched = false, generatesMedia = false } = {}) {
|
|
198
202
|
const reasons = [];
|
|
199
|
-
const risky = changedFiles.filter((f) => OPUS_PATH_RE.test(f.replaceAll('\\', '/')));
|
|
200
|
-
if (risky.length) reasons.push(`caminho sensível: ${firstOf(risky)}`);
|
|
201
203
|
if (promptModelTouched) reasons.push('prompt/modelo alterado');
|
|
202
204
|
if (generatesMedia) reasons.push('gera mídia');
|
|
203
205
|
return { model: reasons.length ? 'opus' : 'sonnet', reasons };
|
|
204
206
|
}
|
|
205
207
|
|
|
208
|
+
/**
|
|
209
|
+
* A revisão de segurança é exigida quando o diff toca auth, segredo,
|
|
210
|
+
* permissão, dado pessoal, pagamento ou migration.
|
|
211
|
+
*
|
|
212
|
+
* @param {string[]} changedFiles
|
|
213
|
+
* @returns {{ required: boolean, reasons: string[] }}
|
|
214
|
+
*/
|
|
215
|
+
export function securityReviewFor(changedFiles = []) {
|
|
216
|
+
const risky = changedFiles.filter((f) => SECURITY_PATH_RE.test(f.replaceAll('\\', '/')));
|
|
217
|
+
return { required: risky.length > 0, reasons: risky.length ? [`caminho sensível: ${firstOf(risky)}`] : [] };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const PLAN_CHECKS_REASONS = {
|
|
221
|
+
'no-plan': 'sem 2-plan/plan.md',
|
|
222
|
+
'no-checks': 'sem 2-plan/checks.json',
|
|
223
|
+
unreadable: '2-plan/checks.json ilegível ou de outra versão',
|
|
224
|
+
stale: '2-plan/checks.json não corresponde ao plan.md atual',
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Por que o gate do plano não fecha sem pessoa: os checks extraídos são o que
|
|
229
|
+
* o apply despacha, o report-check confere e a review cobra — um plano sem
|
|
230
|
+
* eles, ou com os de outra versão do plano, aprovaria critérios que ninguém
|
|
231
|
+
* extraiu.
|
|
232
|
+
* Regra única do gate-check (pausa) e do approve/advance automáticos (recusa).
|
|
233
|
+
*
|
|
234
|
+
* @param {string|undefined} status - `readPlanChecks(...).status`
|
|
235
|
+
* @returns {string|null}
|
|
236
|
+
*/
|
|
237
|
+
export function planChecksPause(status) {
|
|
238
|
+
if (status === undefined || status === 'fresh') return null;
|
|
239
|
+
const what = PLAN_CHECKS_REASONS[status] ?? `checks.json em estado "${status}"`;
|
|
240
|
+
return `${what} — rode \`morph-spec plan-check {feature}\` até zero erro antes do gate do plano`;
|
|
241
|
+
}
|
|
242
|
+
|
|
206
243
|
/**
|
|
207
244
|
* @param {object} config - .morph/config/config.json contents (uses `trust`)
|
|
208
245
|
* @param {object} signals
|
|
@@ -252,35 +289,11 @@ export function shouldPauseGate(config, signals) {
|
|
|
252
289
|
};
|
|
253
290
|
}
|
|
254
291
|
|
|
255
|
-
// No gate de SHIP: um `verification` PRESENTE cujo escopo não é o da feature
|
|
256
|
-
// é um recorte parcial — de UMA task, nunca da app inteira. O stamp é
|
|
257
|
-
// SINGULAR, sobrescrito por qualquer verify de qualquer escopo, e vale
|
|
258
|
-
// INDEPENDENTE do que os nós individuais dizem: mesmo com e2e 'pass' dentro
|
|
259
|
-
// daquele recorte, build/tests/validators também rodaram só para a task, não
|
|
260
|
-
// para a feature. Antes desta regra, um `verify {f} {task}` totalmente verde
|
|
261
|
-
// comprava o Gate 3 da feature que ele nem tentou verificar por inteiro — já
|
|
262
|
-
// acontecia manualmente (`--filter`), e o recorte automático de PR B faz
|
|
263
|
-
// acontecer em TODA task. Por isso corre ANTES do bloco de e2e abaixo: ele
|
|
264
|
-
// pausa por si só, sem depender de e2eStatus/e2eReason.
|
|
265
|
-
//
|
|
266
|
-
// `typeof === 'string'` exclui `null`/ausente (features legadas sem stamp) —
|
|
267
|
-
// narrow, nunca widen. O caminho de volta é sempre o mesmo, barato e nomeado:
|
|
268
|
-
// `morph-spec verify {feature}` (suíte inteira) antes do ship.
|
|
269
|
-
if (signals.gate === 'review'
|
|
270
|
-
&& typeof signals.verificationScope === 'string'
|
|
271
|
-
&& signals.verificationScope !== FEATURE_SCOPE) {
|
|
272
|
-
return {
|
|
273
|
-
pause: true,
|
|
274
|
-
reason: `último morph-spec verify foi de escopo "${signals.verificationScope}" (recorte parcial de task)`
|
|
275
|
-
+ ' — rode `morph-spec verify {feature}` (suíte inteira) antes do ship',
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
|
|
279
292
|
// No gate de SHIP apenas: o nó e2e do último verify PULOU. Um skip não
|
|
280
293
|
// reprova o rollup, então `verificationStatus` sai 'pass' e o gate ficava
|
|
281
294
|
// cego — auto-passando um Gate 3 sobre uma app que ninguém subiu. A razão
|
|
282
|
-
// literal decide: dispensa legítima (o projeto não containeriza
|
|
283
|
-
//
|
|
295
|
+
// literal decide: dispensa legítima (o projeto não containeriza) passa e
|
|
296
|
+
// fica REGISTRADA nos signals; ausência de
|
|
284
297
|
// prova (docker indisponível, `--skip-e2e`, razão nenhuma) pausa. É a prosa
|
|
285
298
|
// do morph-review virando regra — "docker indisponível não é dispensa" — e
|
|
286
299
|
// é o que dispensa o `gate-decision --pause false` manual que ela mandava
|
|
@@ -291,26 +304,12 @@ export function shouldPauseGate(config, signals) {
|
|
|
291
304
|
if (E2E_SKIP_CLASSES_THAT_PAUSE.includes(klass)) {
|
|
292
305
|
return { pause: true, reason: e2ePauseReason(klass, signals.e2eReason) };
|
|
293
306
|
}
|
|
294
|
-
// Segunda pergunta, que a razão sozinha não responde: QUEM carimbou. Um
|
|
295
|
-
// escopo de TASK (`task:{id}`) já pausou acima, antes deste bloco — o que
|
|
296
|
-
// sobra a checar aqui é só a ausência de escopo (`null`/indefinido:
|
|
297
|
-
// feature legada, ou stamp anterior à introdução do campo). Cai do mesmo
|
|
298
|
-
// lado seguro que `unknown` pelo mesmo motivo de sempre: recusar é barato,
|
|
299
|
-
// aprovar sem prova de QUEM respondeu pela app não é. O caminho de volta é
|
|
300
|
-
// o mesmo (rodar `verify {feature}`), e é o que morph-review já manda
|
|
301
|
-
// fazer antes do Gate 3.
|
|
302
|
-
if (signals.verificationScope == null) {
|
|
303
|
-
return {
|
|
304
|
-
pause: true,
|
|
305
|
-
reason: e2eScopeRefusal(klass, signals.verificationScope, signals.e2eReason),
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
307
|
}
|
|
309
308
|
|
|
310
309
|
// Furo 2: no gate de SHIP (review) apenas, um stamp de verification cujo
|
|
311
310
|
// sha diverge do HEAD atual significa que código mudou depois do último
|
|
312
311
|
// `morph-spec verify` — o resultado carimbado não cobre mais o que está
|
|
313
|
-
// prestes a ser aprovado. Pausa só aqui, nunca em plan/uiux
|
|
312
|
+
// prestes a ser aprovado. Pausa só aqui, nunca em plan/uiux: essa
|
|
314
313
|
// é uma regra de ship, não uma regra geral de gate. Stamp AUSENTE não entra
|
|
315
314
|
// aqui (verificationStale é fail-open para `false` em collectSignals
|
|
316
315
|
// quando não há stamp) — features legadas sem stamp continuam liberadas.
|
|
@@ -322,7 +321,7 @@ export function shouldPauseGate(config, signals) {
|
|
|
322
321
|
}
|
|
323
322
|
|
|
324
323
|
if (signals.gate === 'review') {
|
|
325
|
-
const reason =
|
|
324
|
+
const reason = reviewPause(signals.reviewReports);
|
|
326
325
|
if (reason) return { pause: true, reason };
|
|
327
326
|
}
|
|
328
327
|
|
|
@@ -331,9 +330,8 @@ export function shouldPauseGate(config, signals) {
|
|
|
331
330
|
// única parte do produto cujo comportamento não é lido por build nem por teste
|
|
332
331
|
// unitário: os dois continuam verdes com o prompt trocado.
|
|
333
332
|
//
|
|
334
|
-
// DEPOIS
|
|
335
|
-
//
|
|
336
|
-
// reportada — é o defeito mais grave dos dois.
|
|
333
|
+
// DEPOIS dos relatórios de review, de propósito: relatório ausente é o
|
|
334
|
+
// defeito mais grave dos dois e é a razão reportada.
|
|
337
335
|
//
|
|
338
336
|
// PAUSA, jamais bloqueia: `approve review` não ganha precondição nova. Uma
|
|
339
337
|
// segunda precondição dura ali tornaria inaprovável, inclusive por decisão
|
|
@@ -371,23 +369,23 @@ export function shouldPauseGate(config, signals) {
|
|
|
371
369
|
&& signals.mediaClaims.section !== true) {
|
|
372
370
|
return {
|
|
373
371
|
pause: true,
|
|
374
|
-
reason: '
|
|
372
|
+
reason: '`4-review/verifier.md` sem a seção `## Mídia gerada` — as quatro afirmações de mídia'
|
|
375
373
|
+ ' (custo, revisão humana, marcação, hot path) não foram provadas'
|
|
376
|
-
+ ' (evals/review-checklist.md, "
|
|
374
|
+
+ ' (evals/review-checklist.md, "Verificador")',
|
|
377
375
|
};
|
|
378
376
|
}
|
|
379
377
|
|
|
380
378
|
// No fecho do DESIGN: o mockup declara telas sem o checklist de tela
|
|
381
379
|
// completo. É ele que faz paginação, ordenação, filtro e os três estados
|
|
382
|
-
// virarem
|
|
383
|
-
// listando tudo, e o implementador entrega exatamente o
|
|
380
|
+
// virarem cenário do plano — sem ele, uma lista de 2 000 linhas é aprovada
|
|
381
|
+
// listando tudo, e o implementador entrega exatamente o cenário.
|
|
384
382
|
if (signals.gate === 'uiux') {
|
|
385
383
|
const reason = uiuxChecklistPause(signals.screenChecklist);
|
|
386
384
|
if (reason) return { pause: true, reason };
|
|
387
385
|
}
|
|
388
386
|
|
|
389
387
|
// No SHIP, depois da cláusula de mídia e pela mesma disciplina: o design
|
|
390
|
-
// decidiu itens de tela e o
|
|
388
|
+
// decidiu itens de tela e o verificador não registrou a conferência deles
|
|
391
389
|
// contra a tela entregue. Cobra-se o item DEVIDO (`uncovered`): tela toda
|
|
392
390
|
// "não se aplica", mockup legado ou ausente não devem nada.
|
|
393
391
|
if (signals.gate === 'review' && (signals.screenChecklist?.uncovered?.length ?? 0) > 0) {
|
|
@@ -395,8 +393,8 @@ export function shouldPauseGate(config, signals) {
|
|
|
395
393
|
if (section !== true) {
|
|
396
394
|
return {
|
|
397
395
|
pause: true,
|
|
398
|
-
reason:
|
|
399
|
-
+ ' e
|
|
396
|
+
reason: `\`4-review/verifier.md\` sem a seção \`## Checklist de tela\` — o design decidiu ${firstOf(uncovered)}`
|
|
397
|
+
+ ' e o verificador não registrou a conferência contra a tela entregue'
|
|
400
398
|
+ ' (evals/review-checklist.md, "Contrato de tela")',
|
|
401
399
|
};
|
|
402
400
|
}
|
|
@@ -407,18 +405,15 @@ export function shouldPauseGate(config, signals) {
|
|
|
407
405
|
};
|
|
408
406
|
}
|
|
409
407
|
|
|
410
|
-
if (signals.gate === 'plan'
|
|
411
|
-
const
|
|
412
|
-
return {
|
|
413
|
-
pause: true,
|
|
414
|
-
reason: `task grande demais para um dispatch: ${list} outputs — fatie por pós-condição`,
|
|
415
|
-
};
|
|
408
|
+
if (signals.gate === 'plan') {
|
|
409
|
+
const reason = planChecksPause(signals.planChecks);
|
|
410
|
+
if (reason) return { pause: true, reason };
|
|
416
411
|
}
|
|
417
412
|
|
|
418
|
-
if (typeof signals.
|
|
413
|
+
if (typeof signals.sliceCount === 'number' && signals.sliceCount > MAX_SLICES) {
|
|
419
414
|
return {
|
|
420
415
|
pause: true,
|
|
421
|
-
reason:
|
|
416
|
+
reason: `${signals.sliceCount} fatias, acima de ${MAX_SLICES} — o plano é grande demais para uma feature`,
|
|
422
417
|
};
|
|
423
418
|
}
|
|
424
419
|
|
|
@@ -444,15 +439,12 @@ export function getRiskSignals(signals) {
|
|
|
444
439
|
const literal = typeof signals.e2eReason === 'string' && signals.e2eReason.trim() !== ''
|
|
445
440
|
? signals.e2eReason.trim()
|
|
446
441
|
: 'sem motivo registrado';
|
|
447
|
-
|
|
448
|
-
? signals.verificationScope.trim()
|
|
449
|
-
: 'escopo ausente';
|
|
450
|
-
out.push(`e2e pulado [${klass}] (stamp de ${where}): ${literal}`);
|
|
442
|
+
out.push(`e2e pulado [${klass}]: ${literal}`);
|
|
451
443
|
}
|
|
452
444
|
if (signals.gate === 'review' && signals.verificationStale === true) {
|
|
453
445
|
out.push('verification stamp desatualizado (stale) no ship');
|
|
454
446
|
}
|
|
455
|
-
if (signals.gate === 'review') out.push(
|
|
447
|
+
if (signals.gate === 'review') out.push(reviewSummary(signals.reviewReports));
|
|
456
448
|
// Listado mesmo quando NÃO pausa, pela mesma razão do e2e: um projeto que não
|
|
457
449
|
// declara caminho de prompt/modelo é indistinguível, no relatório, de um que
|
|
458
450
|
// declara e não tocou em nada — e as duas coisas significam coisas diferentes.
|
|
@@ -464,11 +456,11 @@ export function getRiskSignals(signals) {
|
|
|
464
456
|
out.push(`prompt/modelo tocado (${touched.length}) com eval ${signals.evalsStatus ?? 'não declarado'}`);
|
|
465
457
|
}
|
|
466
458
|
}
|
|
467
|
-
if (signals.gate === 'plan' &&
|
|
468
|
-
out.push(`
|
|
459
|
+
if (signals.gate === 'plan' && signals.planChecks && signals.planChecks !== 'fresh') {
|
|
460
|
+
out.push(`checks.json ${signals.planChecks}`);
|
|
469
461
|
}
|
|
470
|
-
if (typeof signals.
|
|
471
|
-
out.push(`${signals.
|
|
462
|
+
if (typeof signals.sliceCount === 'number' && signals.sliceCount > MAX_SLICES) {
|
|
463
|
+
out.push(`${signals.sliceCount} fatias (acima de ${MAX_SLICES})`);
|
|
472
464
|
}
|
|
473
465
|
// Listado mesmo quando NÃO pausa, pela mesma razão do sinal irmão: a isenção
|
|
474
466
|
// de uma feature que não gera mídia tem de ser VISÍVEL, senão é
|
|
@@ -476,7 +468,7 @@ export function getRiskSignals(signals) {
|
|
|
476
468
|
if (signals.gate === 'review' && signals.mediaClaims?.featureGeneratesMedia === true) {
|
|
477
469
|
out.push(signals.mediaClaims.section === true
|
|
478
470
|
? 'prova de mídia registrada'
|
|
479
|
-
: '
|
|
471
|
+
: 'verifier.md sem a seção `## Mídia gerada` (prova de mídia ausente)');
|
|
480
472
|
}
|
|
481
473
|
|
|
482
474
|
// Listado mesmo quando NÃO pausa: a isenção (sem mockup, legado, nada
|
|
@@ -493,7 +485,7 @@ export function getRiskSignals(signals) {
|
|
|
493
485
|
: `checklist de tela completo (${screens.length} tela(s))`);
|
|
494
486
|
} else if (uncovered.length > 0) {
|
|
495
487
|
out.push(section !== true
|
|
496
|
-
? '
|
|
488
|
+
? 'verifier.md sem a seção `## Checklist de tela`'
|
|
497
489
|
: `checklist de tela sem conferência em ${uncovered.length} tela(s)`);
|
|
498
490
|
} else if (screens.length === 0) {
|
|
499
491
|
// O design passou pela uiux sem tela nenhuma: nada a cobrar no ship, mas
|
|
@@ -510,7 +502,6 @@ export function getRiskSignals(signals) {
|
|
|
510
502
|
}
|
|
511
503
|
|
|
512
504
|
export const RISK_CONSTANTS = {
|
|
513
|
-
SCOPE_TASK_THRESHOLD,
|
|
514
505
|
FORCED_PAUSE_WORK_TYPES,
|
|
515
506
|
E2E_SKIP_CLASSES_THAT_PAUSE,
|
|
516
507
|
};
|
|
@@ -1,36 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The index of the ADRs that still hold across the project:
|
|
3
|
-
* `.morph/specs/adrs/`, one file per ADR,
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* rule, so brainstorming and plan read one folder instead of every archived
|
|
8
|
-
* `decisions.md` — those stay as history, not as instruction.
|
|
9
|
-
*
|
|
10
|
-
* One file per ADR (`{feature}--ADR-{nnn}.md`), like the use-case acervo:
|
|
11
|
-
* parallel features that index different ADRs never touch the same file, so
|
|
12
|
-
* their archive commits merge cleanly — a single shared page conflicted on the
|
|
13
|
-
* second merge.
|
|
14
|
-
*
|
|
15
|
-
* Maintained by finish/archive alone, inside the archive commit:
|
|
16
|
-
* - enters: an ADR of the closing feature whose block carries `**Alcance:** projeto`;
|
|
17
|
-
* - leaves: an ADR the closing feature's `spec.md` revokes in full (file deleted);
|
|
3
|
+
* `.morph/specs/adrs/`, one file per ADR, maintained by finish/archive alone,
|
|
4
|
+
* inside the archive commit, from the closing feature's `2-plan/plan.md`:
|
|
5
|
+
* - enters: an ADR of `## Decisões` with `**Alcance:** projeto`;
|
|
6
|
+
* - leaves: an ADR its `### Conflitos com o acervo` revokes in full (file deleted);
|
|
18
7
|
* - narrows: one it revokes in part keeps its file, with `scope:` where it still holds.
|
|
19
|
-
*
|
|
8
|
+
* One file per ADR so parallel features that index different ADRs merge cleanly.
|
|
20
9
|
*/
|
|
21
10
|
|
|
22
11
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
|
23
12
|
import { basename, join } from 'path';
|
|
13
|
+
import { readFrontmatterField, setFrontmatterField } from './frontmatter.js';
|
|
14
|
+
import { parseRevocations } from './revocations.js';
|
|
15
|
+
import { parsePlan } from '../plan/plan-parser.js';
|
|
24
16
|
import { OUTPUT_SCHEMA } from '../../core/paths/output-schema.js';
|
|
25
|
-
import { readFrontmatterField, setFrontmatterField } from './usecase-promoter.js';
|
|
26
17
|
|
|
27
18
|
/** Project-relative path of the index folder (git pathspec-ready). */
|
|
28
19
|
export const ADR_INDEX_PATHSPEC = join('.morph', 'specs', 'adrs');
|
|
29
20
|
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
/** `**Alcance:** projeto` — also `**Alcance**: projeto`. */
|
|
33
|
-
const PROJECT_SCOPE_RE = /^\s*\*\*Alcance:?\*\*:?\s*projeto\b/i;
|
|
21
|
+
const PLAN = OUTPUT_SCHEMA.plan;
|
|
22
|
+
const INDEX_LABEL = '.morph/specs/adrs/';
|
|
34
23
|
const INDEX_FILE_RE = /^([a-z0-9][a-z0-9-]*)--(ADR-\d+)\.md$/;
|
|
35
24
|
|
|
36
25
|
/** `admin/ADR-001` ↔ `admin--ADR-001.md`. */
|
|
@@ -40,50 +29,19 @@ const refOf = (name) => {
|
|
|
40
29
|
return m ? `${m[1]}/${m[2]}` : null;
|
|
41
30
|
};
|
|
42
31
|
|
|
43
|
-
/**
|
|
44
|
-
* The ADRs of one `decisions.md` marked as project-wide, each with its block.
|
|
45
|
-
*
|
|
46
|
-
* @param {string} content
|
|
47
|
-
* @returns {Array<{ adr: string, title: string, body: string }>}
|
|
48
|
-
*/
|
|
49
|
-
export function projectAdrs(content) {
|
|
50
|
-
const out = [];
|
|
51
|
-
let current = null; // { adr, title, level, marked, lines }
|
|
52
|
-
const close = () => {
|
|
53
|
-
if (current?.marked) out.push({ adr: current.adr, title: current.title, body: current.lines.join('\n').trim() });
|
|
54
|
-
current = null;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
for (const line of String(content ?? '').split(/\r?\n/)) {
|
|
58
|
-
const adr = ADR_HEADING_RE.exec(line);
|
|
59
|
-
if (adr) {
|
|
60
|
-
close();
|
|
61
|
-
current = { adr: adr[2], title: adr[3], level: adr[1].length, marked: false, lines: [] };
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
const heading = ANY_HEADING_RE.exec(line);
|
|
65
|
-
if (current && heading && heading[1].length <= current.level) { close(); continue; }
|
|
66
|
-
if (!current) continue;
|
|
67
|
-
if (PROJECT_SCOPE_RE.test(line)) current.marked = true;
|
|
68
|
-
current.lines.push(line);
|
|
69
|
-
}
|
|
70
|
-
close();
|
|
71
|
-
return out;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
32
|
function renderAdrFile({ ref, title, body }, feature) {
|
|
75
|
-
const
|
|
33
|
+
const source = `.morph/archive/${feature}/${PLAN.phaseDir}/${PLAN.filename}`;
|
|
76
34
|
return [
|
|
77
35
|
'---',
|
|
78
36
|
`adr: ${ref}`,
|
|
79
37
|
`title: ${JSON.stringify(title)}`,
|
|
80
|
-
`source: ${
|
|
38
|
+
`source: ${source}`,
|
|
81
39
|
'---',
|
|
82
40
|
'',
|
|
83
41
|
`# ${ref}: ${title}`,
|
|
84
42
|
'',
|
|
85
|
-
'> Mantido pelo `morph-spec finish` — não edite à mão. Vale no projeto até que o `
|
|
86
|
-
'> feature o revogue
|
|
43
|
+
'> Mantido pelo `morph-spec finish` — não edite à mão. Vale no projeto até que o `plan.md` de uma',
|
|
44
|
+
'> feature o revogue em `### Conflitos com o acervo`: por inteiro, e este arquivo sai; em parte, e `scope:` diz onde ele ainda vale.',
|
|
87
45
|
'',
|
|
88
46
|
body,
|
|
89
47
|
'',
|
|
@@ -112,29 +70,29 @@ export function readAdrIndex(cwd) {
|
|
|
112
70
|
}
|
|
113
71
|
|
|
114
72
|
/**
|
|
115
|
-
* Apply a closing feature to the index: its project-wide ADRs enter, and
|
|
116
|
-
*
|
|
117
|
-
*
|
|
73
|
+
* Apply a closing feature's plan to the index: its project-wide ADRs enter, and
|
|
74
|
+
* the ADRs its conflicts table revokes are deleted (total) or narrowed (parcial).
|
|
75
|
+
* An unreadable or absent plan touches nothing.
|
|
118
76
|
*
|
|
119
77
|
* @param {string} cwd
|
|
120
78
|
* @param {string} featureDir - the feature folder (already archived, at close time)
|
|
121
|
-
* @param {Array<{ id: string, change: 'total'|'parcial', scope: string|null }>} revocations - ADR entries of `parseRevocations`
|
|
122
79
|
* @returns {{ changed: boolean, added: string[], revoked: string[], narrowed: string[],
|
|
123
|
-
* problems: Array<{ id: string, reason: string, message: string }> }}
|
|
80
|
+
* problems: Array<{ id: string|null, reason: string, message: string }> }}
|
|
124
81
|
*/
|
|
125
|
-
export function updateAdrIndex(cwd, featureDir
|
|
82
|
+
export function updateAdrIndex(cwd, featureDir) {
|
|
126
83
|
const result = { changed: false, added: [], revoked: [], narrowed: [], problems: [] };
|
|
127
84
|
const feature = basename(featureDir);
|
|
128
85
|
const dir = join(cwd, ADR_INDEX_PATHSPEC);
|
|
129
86
|
|
|
130
|
-
let
|
|
87
|
+
let plan = null;
|
|
131
88
|
try {
|
|
132
|
-
const
|
|
133
|
-
if (existsSync(
|
|
134
|
-
} catch { /* unreadable
|
|
89
|
+
const planPath = join(featureDir, PLAN.phaseDir, PLAN.filename);
|
|
90
|
+
if (existsSync(planPath)) plan = parsePlan(readFileSync(planPath, 'utf8'));
|
|
91
|
+
} catch { /* unreadable plan.md — nothing enters or leaves */ }
|
|
92
|
+
if (!plan) return result;
|
|
135
93
|
|
|
136
|
-
for (const {
|
|
137
|
-
const ref = `${feature}/${
|
|
94
|
+
for (const { id, title, body } of plan.adrs.filter((a) => a.scope === 'projeto')) {
|
|
95
|
+
const ref = `${feature}/${id}`;
|
|
138
96
|
const path = join(dir, adrFileName(ref));
|
|
139
97
|
const next = renderAdrFile({ ref, title, body }, feature);
|
|
140
98
|
const existed = existsSync(path);
|
|
@@ -145,13 +103,15 @@ export function updateAdrIndex(cwd, featureDir, revocations = []) {
|
|
|
145
103
|
if (!existed) result.added.push(ref);
|
|
146
104
|
}
|
|
147
105
|
|
|
106
|
+
const { entries: revocations, problems } = parseRevocations(plan.conflicts?.text);
|
|
107
|
+
result.problems.push(...problems);
|
|
148
108
|
for (const rev of revocations) {
|
|
149
109
|
const path = join(dir, adrFileName(rev.id));
|
|
150
110
|
if (!existsSync(path)) {
|
|
151
111
|
result.problems.push({
|
|
152
112
|
id: rev.id,
|
|
153
113
|
reason: 'adr-not-indexed',
|
|
154
|
-
message: `${rev.id} não está no índice de ADRs ativos (${
|
|
114
|
+
message: `${rev.id} não está no índice de ADRs ativos (${INDEX_LABEL}) — nada a retirar; o plan.md arquivado segue como história`,
|
|
155
115
|
});
|
|
156
116
|
continue;
|
|
157
117
|
}
|
|
@@ -167,3 +127,24 @@ export function updateAdrIndex(cwd, featureDir, revocations = []) {
|
|
|
167
127
|
|
|
168
128
|
return result;
|
|
169
129
|
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* What the finish would fail to apply from a plan's revocation table, reported
|
|
133
|
+
* while the plan can still change.
|
|
134
|
+
*
|
|
135
|
+
* @param {string} cwd
|
|
136
|
+
* @param {ReturnType<typeof parsePlan>} plan
|
|
137
|
+
* @returns {string[]}
|
|
138
|
+
*/
|
|
139
|
+
export function revocationWarnings(cwd, plan) {
|
|
140
|
+
const { entries, problems } = parseRevocations(plan.conflicts?.text);
|
|
141
|
+
const warnings = problems.map((p) => `### Conflitos com o acervo: ${p.message}`);
|
|
142
|
+
if (entries.length === 0) return warnings;
|
|
143
|
+
const indexed = new Set(readAdrIndex(cwd).map((a) => a.ref));
|
|
144
|
+
for (const e of entries) {
|
|
145
|
+
if (!indexed.has(e.id)) {
|
|
146
|
+
warnings.push(`### Conflitos com o acervo: ${e.id} não está no índice de ADRs ativos (${INDEX_LABEL}) — o finish não terá o que retirar`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return warnings;
|
|
150
|
+
}
|