@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { resolve } from 'path';
|
|
3
3
|
import { getFeature, updateFeature } from '../../core/state/state-manager.js';
|
|
4
|
-
import { runVerify,
|
|
4
|
+
import { runVerify, VerifyUsageError } from '../../lib/validators/verify-runner.js';
|
|
5
5
|
import { resolvePrimaryRoot } from '../project/worktree.js';
|
|
6
6
|
import { resolveFeatureTree } from '../../lib/worktree/feature-truth.js';
|
|
7
7
|
import { canonicalPath, readMorphConfig } from '../../lib/tasks/test-runner.js';
|
|
@@ -12,7 +12,7 @@ import { treeFingerprint } from '../../lib/git/tree-fingerprint.js';
|
|
|
12
12
|
import { HeavyBusyError } from '../../lib/heavy/gate.js';
|
|
13
13
|
import { afterHeavyRun } from '../../lib/heavy/cleanup.js';
|
|
14
14
|
import { readHeavyConfig } from '../../../framework/hooks/shared/heavy-config.js';
|
|
15
|
-
import { runCiVerify, CiUsageError } from '../../lib/ci/ci-verify.js';
|
|
15
|
+
import { runCiVerify, CiUsageError, reusableLocalStamp } from '../../lib/ci/ci-verify.js';
|
|
16
16
|
|
|
17
17
|
/** ✓ pass · ✗ fail · – skip */
|
|
18
18
|
function statusIcon(status) {
|
|
@@ -21,34 +21,11 @@ function statusIcon(status) {
|
|
|
21
21
|
return chalk.gray('–');
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
/**
|
|
25
|
-
* One line per test run naming what the task-scope recorte did to it (#87):
|
|
26
|
-
* narrowed (how many selectors, from how many test files), whole, out of scope
|
|
27
|
-
* or full, each with its reason. A green `tests` line alone cannot say that the
|
|
28
|
-
* task ran 12 tests instead of 3.082. Feature-scope runs carry no selection and
|
|
29
|
-
* print nothing extra; `feedback[]` is untouched either way.
|
|
30
|
-
*
|
|
31
|
-
* @param {Array<{id: string, selection: object|null}>} [runs]
|
|
32
|
-
* @returns {string[]}
|
|
33
|
-
*/
|
|
34
|
-
function selectionLines(runs = []) {
|
|
35
|
-
return runs
|
|
36
|
-
.filter((run) => run.selection)
|
|
37
|
-
.map((run) => {
|
|
38
|
-
const { mode, reason, selectors = [], files = [] } = run.selection;
|
|
39
|
-
const detail = mode === 'narrowed'
|
|
40
|
-
? `${selectors.length} selector(s) from ${files.length} test file(s)`
|
|
41
|
-
: (reason ?? '');
|
|
42
|
-
return chalk.gray(` ↳ ${run.id.padEnd(10)} ${mode.padEnd(12)} ${detail}`);
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
24
|
function formatVerifyResult(result) {
|
|
47
25
|
const lines = [];
|
|
48
|
-
const scopeLabel = result.scope === 'task' ? `${result.feature}/${result.task}` : result.feature;
|
|
49
26
|
|
|
50
27
|
lines.push('');
|
|
51
|
-
lines.push(chalk.bold(`Verify — ${
|
|
28
|
+
lines.push(chalk.bold(`Verify — ${result.feature}`));
|
|
52
29
|
if (result.stack) lines.push(chalk.gray(`stack: ${result.stack}`));
|
|
53
30
|
lines.push('');
|
|
54
31
|
lines.push(result.status === 'pass' ? chalk.green.bold('✓ PASS') : chalk.red.bold('✗ FAIL'));
|
|
@@ -72,7 +49,6 @@ function formatVerifyResult(result) {
|
|
|
72
49
|
detail = `${node.durationMs}ms`;
|
|
73
50
|
}
|
|
74
51
|
lines.push(` ${icon} ${name.padEnd(11)} ${node.status.padEnd(5)} ${chalk.gray(detail)}`);
|
|
75
|
-
if (name === 'tests') lines.push(...selectionLines(node.runs));
|
|
76
52
|
}
|
|
77
53
|
|
|
78
54
|
if (result.feedback.length > 0) {
|
|
@@ -132,7 +108,7 @@ function baselineStampBlock(baseline) {
|
|
|
132
108
|
|
|
133
109
|
/**
|
|
134
110
|
* The per-node status map, in the shape `feature.schema.json` fixes for BOTH
|
|
135
|
-
* writers of it — the
|
|
111
|
+
* writers of it — the history entry (`verifications.feature`) and the
|
|
136
112
|
* singular gate stamp (`verification.nodes`). One function because they were
|
|
137
113
|
* two literal copies twenty-five lines apart: a fifth node meant remembering
|
|
138
114
|
* both, and a stamp missing a node the history has is precisely the kind of
|
|
@@ -156,7 +132,7 @@ function nodeStatuses(result) {
|
|
|
156
132
|
*
|
|
157
133
|
* The rollup cannot answer the question Gate 3 asks. A red eval and a red unit
|
|
158
134
|
* test both arrive as `tests: 'fail'`, and — worse — an eval node that was
|
|
159
|
-
* SKIPPED (
|
|
135
|
+
* SKIPPED (`--filter`) leaves the rollup green, so a gate reading `tests` alone
|
|
160
136
|
* cannot tell "the eval ran and passed" from "the eval never ran".
|
|
161
137
|
*
|
|
162
138
|
* Four values, and the distinction between the last two is the whole point:
|
|
@@ -179,7 +155,7 @@ function evalsStatusOf(result) {
|
|
|
179
155
|
}
|
|
180
156
|
|
|
181
157
|
/**
|
|
182
|
-
* The options that make a
|
|
158
|
+
* The options that make a run partial, each with the `reason` it
|
|
183
159
|
* reports when it keeps the stamp from being written — first match wins.
|
|
184
160
|
*/
|
|
185
161
|
const PARTIAL_RUN_FLAGS = [['filter', 'filtered'], ['skipTests', 'skip-tests'], ['skipBuild', 'skip-build']];
|
|
@@ -190,9 +166,9 @@ const CI_EXCLUSIVE_FLAGS = [
|
|
|
190
166
|
['noPersist', '--no-persist'],
|
|
191
167
|
];
|
|
192
168
|
|
|
193
|
-
async function runLocalVerify(featureName,
|
|
169
|
+
async function runLocalVerify(featureName, options) {
|
|
194
170
|
try {
|
|
195
|
-
return await runVerify(process.cwd(), featureName,
|
|
171
|
+
return await runVerify(process.cwd(), featureName, {
|
|
196
172
|
timeout: options.timeout,
|
|
197
173
|
skipTests: options.skipTests,
|
|
198
174
|
skipBuild: options.skipBuild,
|
|
@@ -203,12 +179,9 @@ async function runLocalVerify(featureName, taskId, options) {
|
|
|
203
179
|
refreshCache: options.refreshCache,
|
|
204
180
|
linux: options.linux,
|
|
205
181
|
linuxImage: options.linuxImage,
|
|
206
|
-
// Task scope: switch the recorte off (#87). At feature scope nothing is
|
|
207
|
-
// ever narrowed, so the flag is a documented no-op there, not an error.
|
|
208
|
-
full: options.full,
|
|
209
182
|
});
|
|
210
183
|
} catch (err) {
|
|
211
|
-
// A wrong invocation is exit 2, like an unknown feature
|
|
184
|
+
// A wrong invocation is exit 2, like an unknown feature — never a
|
|
212
185
|
// verdict. `--filter` that selects nothing must not print green (or red):
|
|
213
186
|
// there is no verification to report, only a command to fix.
|
|
214
187
|
if (err instanceof VerifyUsageError) {
|
|
@@ -232,7 +205,7 @@ async function runLocalVerify(featureName, taskId, options) {
|
|
|
232
205
|
}
|
|
233
206
|
|
|
234
207
|
/**
|
|
235
|
-
* `morph-spec verify <feature
|
|
208
|
+
* `morph-spec verify <feature>` — the deterministic code-before-judge
|
|
236
209
|
* gate: build + tests + validators, BEFORE the LLM eval. See verify-runner.js
|
|
237
210
|
* for the node cadence; this command wraps it with CLI ergonomics
|
|
238
211
|
* (human/JSON output, exit codes) and context-aware persistence.
|
|
@@ -243,16 +216,14 @@ async function runLocalVerify(featureName, taskId, options) {
|
|
|
243
216
|
* Exit 2 is "the command cannot mean what you typed", and there are four
|
|
244
217
|
* paths to it — never a verdict, because a verdict about nothing is worse
|
|
245
218
|
* than a refusal:
|
|
246
|
-
* - unknown feature
|
|
247
|
-
* decide the scope, so it is a hard usage error here);
|
|
219
|
+
* - unknown feature;
|
|
248
220
|
* - `--filter` that matches no declared node (a filtered-to-empty run is
|
|
249
221
|
* green and proves nothing);
|
|
250
|
-
* - `--record-baseline` with nowhere to write (no `config.json`)
|
|
251
|
-
* task id (a recorte de task is never a fleet baseline, #87);
|
|
222
|
+
* - `--record-baseline` with nowhere to write (no `config.json`);
|
|
252
223
|
* - `--linux` without Docker.
|
|
253
224
|
* The last three arrive as `VerifyUsageError` from the runner.
|
|
254
225
|
*/
|
|
255
|
-
export async function verifyCommand(featureName,
|
|
226
|
+
export async function verifyCommand(featureName, options = {}) {
|
|
256
227
|
let feature;
|
|
257
228
|
try {
|
|
258
229
|
feature = getFeature(featureName);
|
|
@@ -265,22 +236,13 @@ export async function verifyCommand(featureName, taskId, options = {}) {
|
|
|
265
236
|
}
|
|
266
237
|
|
|
267
238
|
if (options.ci) {
|
|
268
|
-
const conflict =
|
|
239
|
+
const conflict = CI_EXCLUSIVE_FLAGS.find(([flag]) => options[flag])?.[1];
|
|
269
240
|
if (conflict) {
|
|
270
241
|
console.error(chalk.red(`\n❌ --ci roda a suíte inteira da feature no CI; não combina com ${conflict}\n`));
|
|
271
242
|
process.exit(2);
|
|
272
243
|
}
|
|
273
244
|
}
|
|
274
245
|
|
|
275
|
-
// The runner's own reader (one reader of tasks.json for verify, #87): null
|
|
276
|
-
// means tasks.json or the task is missing, and both are this usage error.
|
|
277
|
-
if (taskId) {
|
|
278
|
-
if (!readTask(process.cwd(), featureName, taskId)) {
|
|
279
|
-
console.error(chalk.red(`\n❌ Task "${taskId}" not found in tasks.json for feature "${featureName}"\n`));
|
|
280
|
-
process.exit(2);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
246
|
let ci = null;
|
|
285
247
|
if (options.ci) {
|
|
286
248
|
try {
|
|
@@ -294,6 +256,17 @@ export async function verifyCommand(featureName, taskId, options = {}) {
|
|
|
294
256
|
console.error(chalk.yellow(`\n⏳ ${outcome.reason}. Não é vermelho e não há stamp: não repita em laço — siga e deixe o gate-check decidir.\n`));
|
|
295
257
|
process.exit(3);
|
|
296
258
|
} else {
|
|
259
|
+
const reused = reusableLocalStamp(process.cwd(), feature.verification);
|
|
260
|
+
if (reused) {
|
|
261
|
+
console.error(chalk.green(`\n↪ CI indisponível (${outcome.reason}) — stamp fresco reaproveitado (sha ${reused.sha.slice(0, 8)}, ${reused.ts}): nada mudou fora de .morph/ desde aquela rodada verde.\n`));
|
|
262
|
+
if (options.json) {
|
|
263
|
+
console.log(JSON.stringify({
|
|
264
|
+
feature: featureName, status: reused.status, nodes: reused.nodes, e2eReason: reused.e2eReason ?? null,
|
|
265
|
+
reused: { sha: reused.sha, ts: reused.ts, source: reused.source ?? 'local' }, persisted: false, reason: 'stamp-reused',
|
|
266
|
+
}, null, 2));
|
|
267
|
+
}
|
|
268
|
+
process.exit(0);
|
|
269
|
+
}
|
|
297
270
|
console.error(chalk.yellow(`\n↪ CI indisponível (${outcome.reason}) — verify local.\n`));
|
|
298
271
|
}
|
|
299
272
|
} catch (err) {
|
|
@@ -308,31 +281,27 @@ export async function verifyCommand(featureName, taskId, options = {}) {
|
|
|
308
281
|
// Taken before the run: it is the tree the run is about to read.
|
|
309
282
|
const fingerprint = treeFingerprint(process.cwd());
|
|
310
283
|
|
|
311
|
-
const result = ci ? ci.result : await runLocalVerify(featureName,
|
|
284
|
+
const result = ci ? ci.result : await runLocalVerify(featureName, options);
|
|
312
285
|
|
|
313
286
|
const cleanup = ci ? { status: 'skipped', reason: 'ci' } : afterHeavyRun({
|
|
314
287
|
projectPath: process.cwd(),
|
|
315
288
|
config: readHeavyConfig(process.cwd()),
|
|
316
|
-
scope: taskId ? 'task' : 'feature',
|
|
317
289
|
});
|
|
318
290
|
|
|
319
|
-
const
|
|
291
|
+
const historyKey = 'verifications.feature';
|
|
320
292
|
// A run that measured only part of the suite must not stand in for the proof a gate asks for:
|
|
321
|
-
// --filter and --skip
|
|
322
|
-
|
|
323
|
-
const partialFeatureRun = (!options.noPersist && !taskId)
|
|
324
|
-
? PARTIAL_RUN_FLAGS.find(([flag]) => options[flag])?.[1] ?? null
|
|
325
|
-
: null;
|
|
293
|
+
// --filter and --skip-*, or a --record-baseline that recorded nothing; --no-persist keeps its own reason.
|
|
294
|
+
const partialRun = options.noPersist ? null : PARTIAL_RUN_FLAGS.find(([flag]) => options[flag])?.[1] ?? null;
|
|
326
295
|
const baselineBlock = options.recordBaseline ? baselineStampBlock(result.baseline) : null;
|
|
327
296
|
const { persist, reason } = baselineBlock
|
|
328
297
|
? { persist: false, reason: baselineBlock }
|
|
329
|
-
:
|
|
330
|
-
? { persist: false, reason:
|
|
298
|
+
: partialRun
|
|
299
|
+
? { persist: false, reason: partialRun }
|
|
331
300
|
: decidePersistence({ cwd: process.cwd(), featureName, noPersist: options.noPersist });
|
|
332
301
|
|
|
333
302
|
let compact = null;
|
|
334
303
|
if (persist) {
|
|
335
|
-
const prevEntry =
|
|
304
|
+
const prevEntry = feature.verifications?.feature;
|
|
336
305
|
const now = new Date().toISOString();
|
|
337
306
|
compact = {
|
|
338
307
|
status: result.status,
|
|
@@ -340,12 +309,12 @@ export async function verifyCommand(featureName, taskId, options = {}) {
|
|
|
340
309
|
attempts: (prevEntry?.attempts ?? 0) + 1,
|
|
341
310
|
updatedAt: now,
|
|
342
311
|
};
|
|
343
|
-
await updateFeature(featureName,
|
|
312
|
+
await updateFeature(featureName, historyKey, compact);
|
|
344
313
|
|
|
345
|
-
// Singular
|
|
346
|
-
//
|
|
314
|
+
// Singular "latest stamp" — distinct from the `verifications.feature`
|
|
315
|
+
// history above. `sha` is what lets a
|
|
347
316
|
// consumer (morph-spec gate-check) correlate this pass/fail with the
|
|
348
|
-
// EXACT commit it covers, the same way review
|
|
317
|
+
// EXACT commit it covers, the same way a review report's
|
|
349
318
|
// `**Reviewed-SHA:**` token lets approve.js detect a stale review. null
|
|
350
319
|
// in a non-git checkout (or before the first commit) — fail-open, never
|
|
351
320
|
// blocks persistence.
|
|
@@ -364,18 +333,17 @@ export async function verifyCommand(featureName, taskId, options = {}) {
|
|
|
364
333
|
e2eReason: result.nodes.e2e.status === 'skip' ? (result.nodes.e2e.reason ?? null) : null,
|
|
365
334
|
sha: ci ? ci.run.headSha : getHeadSha(process.cwd()),
|
|
366
335
|
ts: now,
|
|
367
|
-
scope: taskId ? `task:${taskId}` : 'feature',
|
|
368
336
|
...(ci ? { source: 'ci', ciRun: ci.run.url } : {}),
|
|
369
337
|
};
|
|
370
338
|
await updateFeature(featureName, 'verification', stamp);
|
|
371
339
|
}
|
|
372
340
|
|
|
373
|
-
// A
|
|
341
|
+
// A run over the exact tree of the previous one proves nothing
|
|
374
342
|
// new. Advisory only: the stamp and the exit code are the same either way.
|
|
375
343
|
let repeatOf = null;
|
|
376
344
|
try {
|
|
377
345
|
const cwd = process.cwd();
|
|
378
|
-
if (
|
|
346
|
+
if (persist && !ci) repeatOf = findRepeatedVerify(cwd, { feature: featureName, fingerprint });
|
|
379
347
|
const tree = resolve(cwd).replace(/\\/g, '/') === resolvePrimaryRoot(cwd) ? 'root' : 'worktree';
|
|
380
348
|
recordVerifyEvent(cwd, buildVerifyEvent(result, { sha: ci ? ci.run.headSha : getHeadSha(cwd), tree, persisted: persist, reason, fingerprint, source: ci ? 'ci' : 'local' }));
|
|
381
349
|
} catch { /* telemetry never changes the verdict */ }
|
|
@@ -406,7 +374,7 @@ export async function verifyCommand(featureName, taskId, options = {}) {
|
|
|
406
374
|
console.log(chalk.gray(`${mark} dotnet build-server shutdown: ${cleanup.status}${cleanup.reason ? ` (${cleanup.reason})` : ''}\n`));
|
|
407
375
|
}
|
|
408
376
|
if (repeatOf) {
|
|
409
|
-
console.log(chalk.yellow(`⚠️ Mesma árvore do verify de
|
|
377
|
+
console.log(chalk.yellow(`⚠️ Mesma árvore do verify de ${repeatOf.ts} (${repeatOf.status}): nada mudou fora de .morph/ desde então. `
|
|
410
378
|
+ 'Esta rodada não prova nada novo — reuse o resultado anterior, a menos que o ambiente (Docker, banco, serviço externo) tenha mudado.\n'));
|
|
411
379
|
}
|
|
412
380
|
if (result.baseline?.recorded) {
|
|
@@ -417,7 +385,7 @@ export async function verifyCommand(featureName, taskId, options = {}) {
|
|
|
417
385
|
`));
|
|
418
386
|
}
|
|
419
387
|
if (persist) {
|
|
420
|
-
console.log(chalk.gray(`Persisted: ${
|
|
388
|
+
console.log(chalk.gray(`Persisted: ${historyKey} (attempts: ${compact.attempts})\n`));
|
|
421
389
|
} else {
|
|
422
390
|
console.log(chalk.yellow(`⚠️ Not persisted (${reason})\n`));
|
|
423
391
|
}
|
|
@@ -15,28 +15,24 @@ import { join } from 'path';
|
|
|
15
15
|
* Schema for output types produced by morph-spec NLH phases.
|
|
16
16
|
*
|
|
17
17
|
* Fields:
|
|
18
|
-
* filename - The file's basename (e.g. '
|
|
18
|
+
* filename - The file's basename (e.g. 'plan.md'). Single-file types only.
|
|
19
19
|
* phaseDir - Subdirectory within .morph/features/{feature}/
|
|
20
20
|
* phase - The morph-spec phase that produces this output
|
|
21
21
|
* protected - Whether the file requires an approval gate before editing
|
|
22
22
|
* approvalGate - (optional) Which gate protects this file
|
|
23
|
-
* mutableFields- (optional) For protected JSON files: top-level array-item
|
|
24
|
-
* field names that MAY still change after the gate is approved.
|
|
25
|
-
* Lets the implement phase update task progress without
|
|
26
|
-
* unfreezing the plan contract. When present, the protect
|
|
27
|
-
* hook does a semantic diff instead of a blanket block.
|
|
28
23
|
*
|
|
29
24
|
* Multi-file types (`multi: true`) hold N files in a directory instead of one
|
|
30
25
|
* fixed basename. They carry `dirname` + `filenamePattern` + `displayFilename`
|
|
31
26
|
* INSTEAD of `filename`, and every derivation keyed by basename
|
|
32
|
-
* (FILENAME_TO_OUTPUT, PROTECTED_SPEC_FILES
|
|
27
|
+
* (FILENAME_TO_OUTPUT, PROTECTED_SPEC_FILES) skips them —
|
|
33
28
|
* there is no single basename to key on. `getOutputPath` returns the DIRECTORY
|
|
34
29
|
* for these types.
|
|
35
30
|
*/
|
|
36
31
|
export const OUTPUT_SCHEMA = {
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
32
|
+
// 2-plan/ — the plan gate freezes both: plan.md accepts only an append in
|
|
33
|
+
// `## Errata`, checks.json only a rewrite by `morph-spec plan-check`.
|
|
34
|
+
plan: { filename: 'plan.md', phaseDir: '2-plan', phase: 'plan', protected: true, approvalGate: 'plan' },
|
|
35
|
+
checks: { filename: 'checks.json', phaseDir: '2-plan', phase: 'plan', protected: true, approvalGate: 'plan' },
|
|
40
36
|
|
|
41
37
|
// 1-design/ (optional)
|
|
42
38
|
uiDesignSystem: { filename: 'design-system.md', phaseDir: '1-design', phase: 'uiux', protected: true, approvalGate: 'uiux' },
|
|
@@ -44,47 +40,20 @@ export const OUTPUT_SCHEMA = {
|
|
|
44
40
|
uiComponents: { filename: 'components.md', phaseDir: '1-design', phase: 'uiux', protected: true, approvalGate: 'uiux' },
|
|
45
41
|
uiFlows: { filename: 'flows.md', phaseDir: '1-design', phase: 'uiux', protected: true, approvalGate: 'uiux' },
|
|
46
42
|
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
decisions: { filename: 'decisions.md', phaseDir: '2-plan', phase: 'plan', protected: false },
|
|
51
|
-
// Veredito REUSAR/ESTENDER/CRIAR por conceito que a feature introduz, com a
|
|
52
|
-
// justificativa de cada CRIAR. É o ARTEFATO do ciclo de reuso (a prosa está
|
|
53
|
-
// em morph-plan §2b, a cobrança em src/lib/validators/reuse/). NÃO protegido,
|
|
54
|
-
// como decisions.md: é registro vivo — o implementador que descobre um alvo
|
|
55
|
-
// melhor no meio da task precisa poder anotar, e congelá-lo transformaria a
|
|
56
|
-
// correção num bloqueio de gate.
|
|
57
|
-
// N files, one per system use case — the behaviour layer the plan distils
|
|
58
|
-
// BEFORE spec.md/tasks.json. Deliberately NOT protected: editing a use case
|
|
59
|
-
// after Gate 2 is the supported way to change behaviour and re-run implement;
|
|
60
|
-
// the audit trail lives in git.
|
|
61
|
-
usecases: {
|
|
62
|
-
dirname: 'usecases',
|
|
63
|
-
filenamePattern: '^UC-\\d+-[a-z0-9-]+\\.md$',
|
|
64
|
-
displayFilename: 'UC-{nn}-{slug}.md',
|
|
65
|
-
phaseDir: '2-plan',
|
|
66
|
-
phase: 'plan',
|
|
67
|
-
protected: false,
|
|
68
|
-
multi: true,
|
|
69
|
-
},
|
|
70
|
-
|
|
71
|
-
// 3-implement/
|
|
72
|
-
recap: { filename: 'recap.md', phaseDir: '3-implement', phase: 'implement', protected: false },
|
|
73
|
-
// N files, one per task whose doneCriteria judges what a MODEL answers
|
|
74
|
-
// (morph-plan §5). Runtime evidence: input, model, output, verdict — pointing
|
|
43
|
+
// 3-implement/ — the slice reports (`reports/F{n}.md`, see getTaskReportPath) are the record.
|
|
44
|
+
// N files, one per scenario whose proof judges what a MODEL answers
|
|
45
|
+
// (`prova: eval` in the plan). Runtime evidence: input, model, output, verdict — pointing
|
|
75
46
|
// at the `runner: "evals"` node when the stack declares one. CONDITIONAL by
|
|
76
47
|
// construction: never in phases.json requiredOutputs. Readers: morph-review
|
|
77
48
|
// dispatch prompt.
|
|
78
|
-
evidencias: { dirname: 'evidencias', filenamePattern: '^
|
|
49
|
+
evidencias: { dirname: 'evidencias', filenamePattern: '^C\\d+\\.md$', displayFilename: '{Cn}.md',
|
|
79
50
|
phaseDir: '3-implement', phase: 'implement', protected: false, multi: true },
|
|
80
51
|
|
|
81
52
|
// 4-review/
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
// the other is the orchestrator's curated Gate 3 presentation.
|
|
87
|
-
evaluatorReport:{ filename: 'evaluator-report.md', phaseDir: '4-review', phase: 'review', protected: false },
|
|
53
|
+
// Um relatório por papel; o canônico da fase é o do verificador.
|
|
54
|
+
verifierReport: { filename: 'verifier.md', phaseDir: '4-review', phase: 'review', protected: false },
|
|
55
|
+
reviewerReport: { filename: 'reviewer.md', phaseDir: '4-review', phase: 'review', protected: false },
|
|
56
|
+
securityReport: { filename: 'security.md', phaseDir: '4-review', phase: 'review', protected: false },
|
|
88
57
|
};
|
|
89
58
|
|
|
90
59
|
// ============================================================================
|
|
@@ -104,11 +73,11 @@ function leafSegment(entry) {
|
|
|
104
73
|
* Get the morph-relative path for a specific output type of a feature.
|
|
105
74
|
*
|
|
106
75
|
* For `multi` types this is the DIRECTORY holding the N files
|
|
107
|
-
* (".morph/features/my-feature/
|
|
76
|
+
* (".morph/features/my-feature/3-implement/evidencias"), not a single file.
|
|
108
77
|
*
|
|
109
78
|
* @param {string} featureName
|
|
110
79
|
* @param {string} outputType - camelCase output type key from OUTPUT_SCHEMA
|
|
111
|
-
* @returns {string} e.g. ".morph/features/my-feature/2-plan/
|
|
80
|
+
* @returns {string} e.g. ".morph/features/my-feature/2-plan/plan.md"
|
|
112
81
|
*/
|
|
113
82
|
export function getOutputPath(featureName, outputType) {
|
|
114
83
|
const entry = OUTPUT_SCHEMA[outputType];
|
|
@@ -205,27 +174,11 @@ export const PROTECTED_SPEC_FILES = Object.fromEntries(
|
|
|
205
174
|
.map(([, entry]) => [entry.filename, entry.approvalGate])
|
|
206
175
|
);
|
|
207
176
|
|
|
208
|
-
/**
|
|
209
|
-
* For protected files that carry mutable progress fields: filename → the
|
|
210
|
-
* top-level array-item fields that may change after the gate is approved.
|
|
211
|
-
* The protect-spec-files hook uses this to allow progress updates (e.g.
|
|
212
|
-
* task status) while still freezing the plan contract.
|
|
213
|
-
*/
|
|
214
|
-
export const MUTABLE_SPEC_FIELDS = Object.fromEntries(
|
|
215
|
-
Object.entries(OUTPUT_SCHEMA)
|
|
216
|
-
.filter(([, entry]) => entry.protected && entry.approvalGate && entry.mutableFields && !entry.multi)
|
|
217
|
-
.map(([, entry]) => [entry.filename, entry.mutableFields])
|
|
218
|
-
);
|
|
219
|
-
|
|
220
177
|
// ============================================================================
|
|
221
|
-
//
|
|
178
|
+
// Contracts
|
|
222
179
|
// ============================================================================
|
|
223
180
|
|
|
224
|
-
/**
|
|
225
|
-
* v4 had separate contracts.cs / contracts.ts / contracts-vsa.cs output types.
|
|
226
|
-
* In v5 contracts live inside spec.md (or in source files for VSA). This
|
|
227
|
-
* function keeps callers happy until they're updated.
|
|
228
|
-
*/
|
|
181
|
+
/** The output type whose code blocks declare the contracts the code must honour. */
|
|
229
182
|
export function resolveContractsOutputType() {
|
|
230
|
-
return '
|
|
183
|
+
return 'plan';
|
|
231
184
|
}
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* Gate monotonicity — single source of truth.
|
|
3
3
|
*
|
|
4
4
|
* A gate may only be approved once its predecessors are approved. The strict
|
|
5
|
-
* chain is
|
|
6
|
-
*
|
|
5
|
+
* chain is plan → review. uiux is optional: it requires plan but gates nothing
|
|
6
|
+
* downstream, so a feature that skips UI/UX is never blocked.
|
|
7
7
|
*
|
|
8
8
|
* Policy-stamped gates (approvedBy: "policy:{type}", written at birth by
|
|
9
9
|
* `morph-spec create` for a work type's skippedGates) count as approved, so
|
|
10
|
-
* chore/hotfix (
|
|
10
|
+
* chore/hotfix (plan pre-stamped) satisfy prereqs naturally.
|
|
11
11
|
*
|
|
12
12
|
* Pure module — no filesystem/CLI side effects. Consumed by
|
|
13
13
|
* `approve`/`advance` (refuse), the state-sync hook (warn), and `list`/`doctor`
|
|
@@ -15,9 +15,8 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
export const GATE_PREREQ = {
|
|
18
|
-
|
|
19
|
-
uiux: ['
|
|
20
|
-
plan: ['proposal'],
|
|
18
|
+
plan: [],
|
|
19
|
+
uiux: ['plan'],
|
|
21
20
|
review: ['plan'],
|
|
22
21
|
};
|
|
23
22
|
|
|
@@ -103,7 +103,7 @@ function lastTouched(fdir, fj) {
|
|
|
103
103
|
* Does the feature dir hold any FILE other than its own `feature.json`?
|
|
104
104
|
*
|
|
105
105
|
* Files, not entries: `create` scaffolds the phase folders empty, so an empty
|
|
106
|
-
* `
|
|
106
|
+
* `2-plan/` is the absence of work, not evidence of it.
|
|
107
107
|
*/
|
|
108
108
|
function hasArtifacts(fdir) {
|
|
109
109
|
let entries;
|
|
@@ -159,7 +159,7 @@ function ownCommitCount(baseDir, branch) {
|
|
|
159
159
|
* mean nobody ever came back to it.
|
|
160
160
|
*
|
|
161
161
|
* The artifact test is deliberately broader than the "0 tasks" the report
|
|
162
|
-
* named: a
|
|
162
|
+
* named: a plan drafted and waiting on its gate also has zero tasks and is
|
|
163
163
|
* very much alive. What a dead scaffold has is NOTHING — checked in the root
|
|
164
164
|
* and, if the feature registered one, in its worktree, where uncommitted work
|
|
165
165
|
* would otherwise be invisible to both the disk scan and `rev-list`.
|