@polymorphism-tech/morph-spec 9.0.0 → 9.1.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 +8 -9
- package/README.md +38 -36
- package/bin/morph-spec.js +4 -62
- package/bin/validate.js +1 -39
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +1 -1
- package/framework/MORPH.md +1 -1
- package/framework/agents.json +12 -4
- package/framework/commands/morph-apply.md +1 -1
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +6 -17
- package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
- package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
- package/framework/hooks/claude-code/core/chain.js +4 -65
- package/framework/hooks/claude-code/core/comment-guard.js +2 -16
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +6 -50
- package/framework/hooks/claude-code/core/gate-guard.js +7 -67
- package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
- package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
- package/framework/hooks/claude-code/core/heavy-release.js +2 -13
- package/framework/hooks/claude-code/core/loop-detect.js +2 -31
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +0 -4
- package/framework/hooks/claude-code/core/protect-spec-files.js +7 -67
- package/framework/hooks/claude-code/core/state-sync.js +11 -67
- package/framework/hooks/claude-code/core/telemetry-log.js +12 -93
- package/framework/hooks/claude-code/core/validate-completion.js +11 -57
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +1 -9
- package/framework/hooks/claude-code/core/worktree-context.js +10 -110
- package/framework/hooks/claude-code/core/worktree-create.js +6 -70
- package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
- package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
- package/framework/hooks/shared/activity-logger.js +9 -89
- package/framework/hooks/shared/agent-reap.js +2 -17
- package/framework/hooks/shared/build-check.js +6 -75
- package/framework/hooks/shared/claim-registry.js +9 -119
- package/framework/hooks/shared/command-tokens.js +6 -51
- package/framework/hooks/shared/comment-rules.js +5 -20
- package/framework/hooks/shared/comment-scanner.js +6 -25
- package/framework/hooks/shared/comment-stripper.js +11 -85
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/errata.js +2 -15
- package/framework/hooks/shared/feature-locator.js +11 -95
- package/framework/hooks/shared/frontend-locations.js +1 -3
- package/framework/hooks/shared/gate-question.js +1 -7
- package/framework/hooks/shared/git-exclude.js +3 -20
- package/framework/hooks/shared/git-paths.js +8 -104
- package/framework/hooks/shared/graph-freshness.js +22 -166
- package/framework/hooks/shared/graph-refresh.js +21 -149
- package/framework/hooks/shared/heavy-command.js +4 -12
- package/framework/hooks/shared/hook-response.js +8 -83
- package/framework/hooks/shared/morph-cli.js +8 -52
- package/framework/hooks/shared/payload-utils.js +10 -132
- package/framework/hooks/shared/phase-utils.js +1 -42
- package/framework/hooks/shared/redact-secrets.js +4 -15
- package/framework/hooks/shared/reparse-scan.js +6 -66
- package/framework/hooks/shared/slice-progress.js +4 -23
- package/framework/hooks/shared/source-files.js +4 -17
- package/framework/hooks/shared/state-reader.js +13 -183
- package/framework/hooks/shared/state-writer.js +8 -52
- package/framework/hooks/shared/stdin-reader.js +1 -11
- package/framework/hooks/shared/telemetry-logger.js +7 -67
- package/framework/hooks/shared/transcript-usage.js +7 -29
- package/framework/hooks/shared/worktree-root.js +7 -29
- package/framework/rules/csharp-standards.md +1 -1
- package/framework/rules/frontend-standards.md +1 -0
- package/framework/rules/hangfire-standards.md +1 -1
- package/framework/rules/neon-standards.md +1 -1
- 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 +2 -2
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-machine/SKILL.md +1 -1
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +2 -2
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +29 -12
- package/framework/standards/STANDARDS.json +34 -12
- package/framework/standards/STANDARDS.meta.json +16 -2
- package/framework/standards/ai-agents/agent-archetypes.md +2 -2
- package/framework/standards/ai-agents/agent-session.md +1 -1
- package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
- package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
- package/framework/standards/ai-agents/guardrails.md +1 -1
- package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
- package/framework/standards/ai-agents/observability-patterns.md +1 -1
- package/framework/standards/ai-agents/production.md +1 -1
- package/framework/standards/ai-agents/prompt-sources.md +2 -2
- package/framework/standards/ai-agents/providers/model-registry.md +1 -1
- package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
- package/framework/standards/ai-agents/setup.md +2 -2
- package/framework/standards/ai-agents/sweet-spot.md +2 -2
- package/framework/standards/ai-agents/tool-adapters.md +0 -2
- package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
- package/framework/standards/ai-agents/workflows.md +1 -1
- package/framework/standards/architecture/code-comments.md +1 -1
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
- package/framework/standards/backend/api/validation.md +1 -1
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
- package/framework/standards/data/nosql/cache/redis.md +1 -1
- package/framework/standards/data/nosql/cosmos-db.md +3 -3
- package/framework/standards/frontend/admin-app/forms.md +1 -1
- package/framework/standards/frontend/design-system/art-direction.md +88 -0
- package/framework/standards/frontend/design-system/premium-finish.md +77 -2
- package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
- package/framework/templates/CLAUDE.md.core.template +3 -3
- package/framework/templates/REGISTRY.json +9 -9
- package/framework/templates/docs/ui-mockups.md +4 -0
- package/package.json +1 -1
- package/src/commands/context/context-refresh.js +0 -16
- package/src/commands/dashboard/dashboard.js +2 -8
- package/src/commands/e2e/e2e.js +17 -104
- package/src/commands/graph/affected.js +0 -14
- package/src/commands/graph/build.js +11 -50
- package/src/commands/graph/explain.js +0 -16
- package/src/commands/graph/status.js +1 -12
- package/src/commands/mcp/mcp-setup.js +4 -62
- package/src/commands/observability/cost.js +5 -57
- package/src/commands/observability/telemetry.js +1 -14
- package/src/commands/project/branch.js +4 -45
- package/src/commands/project/claim.js +4 -48
- package/src/commands/project/doctor.js +48 -302
- package/src/commands/project/env.js +1 -27
- package/src/commands/project/fleet.js +3 -32
- package/src/commands/project/init.js +4 -68
- package/src/commands/project/install-plugin-cmd.js +0 -1
- package/src/commands/project/standards.js +1 -62
- package/src/commands/project/status.js +0 -53
- package/src/commands/project/update.js +31 -239
- package/src/commands/project/worktree.js +108 -882
- package/src/commands/scaffold/scaffold.js +0 -25
- package/src/commands/state/advance.js +14 -85
- package/src/commands/state/approve.js +14 -96
- package/src/commands/state/archive.js +12 -64
- package/src/commands/state/auto-approval.js +2 -5
- package/src/commands/state/create.js +22 -109
- package/src/commands/state/delete.js +11 -63
- package/src/commands/state/finish.js +98 -475
- package/src/commands/state/gate-check.js +22 -132
- package/src/commands/state/gate-decision.js +3 -39
- package/src/commands/state/handoff.js +3 -30
- package/src/commands/state/list.js +14 -84
- package/src/commands/state/report-check.js +8 -58
- package/src/commands/state/retype.js +4 -26
- package/src/commands/validation/index.js +0 -3
- package/src/commands/validation/mutate.js +2 -5
- package/src/commands/validation/validate-feature.js +5 -94
- package/src/commands/validation/verify.js +15 -98
- package/src/core/paths/output-schema.js +2 -101
- package/src/core/state/feature-identity.js +1 -25
- package/src/core/state/gate-order.js +0 -27
- package/src/core/state/lifecycle-audit.js +8 -100
- package/src/core/state/state-manager.js +22 -304
- package/src/core/templates/template-registry.js +2 -97
- package/src/core/templates/template-renderer.js +13 -93
- package/src/core/templates/template-seed.js +0 -1
- package/src/core/work-types.js +1 -36
- package/src/dashboard/app/App.jsx +1 -8
- package/src/dashboard/app/components/ActivityFeed.jsx +0 -5
- package/src/dashboard/app/components/CommandPalette.jsx +0 -7
- package/src/dashboard/app/components/DetailPanel.jsx +1 -5
- package/src/dashboard/app/components/EntityLink.jsx +0 -12
- package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
- package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
- package/src/dashboard/app/components/Markdown.jsx +0 -9
- package/src/dashboard/app/components/PhaseRail.jsx +0 -11
- package/src/dashboard/app/components/SearchFilter.jsx +0 -4
- package/src/dashboard/app/components/Sidebar.jsx +0 -5
- package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
- package/src/dashboard/app/hooks/usePref.js +1 -5
- package/src/dashboard/app/lib/format.js +0 -2
- package/src/dashboard/app/lib/phases.js +1 -5
- package/src/dashboard/app/lib/project.js +0 -12
- package/src/dashboard/app/pages/AgentMap.jsx +1 -8
- package/src/dashboard/app/pages/FeatureDetail.jsx +1 -5
- package/src/dashboard/app/pages/Features.jsx +1 -8
- package/src/dashboard/app/pages/Guide.jsx +0 -1
- package/src/dashboard/app/pages/HookInspector.jsx +2 -23
- package/src/dashboard/app/pages/PhasePipeline.jsx +0 -3
- package/src/dashboard/app/pages/Portfolio.jsx +0 -1
- package/src/dashboard/app/pages/RulesMap.jsx +1 -16
- package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
- package/src/dashboard/app/pages/Standards.jsx +2 -17
- package/src/dashboard/scanner/activity-scanner.js +10 -69
- package/src/dashboard/scanner/agents-scanner.js +1 -23
- package/src/dashboard/scanner/docs-scanner.js +0 -6
- package/src/dashboard/scanner/feature-status.js +2 -5
- package/src/dashboard/scanner/features-scanner.js +10 -66
- package/src/dashboard/scanner/hooks-scanner.js +5 -28
- package/src/dashboard/scanner/index.js +0 -4
- package/src/dashboard/scanner/portfolio.js +5 -29
- package/src/dashboard/scanner/project-api.js +8 -54
- package/src/dashboard/scanner/rules-scanner.js +1 -2
- package/src/dashboard/scanner/skills-scanner.js +2 -12
- package/src/dashboard/scanner/standards-scanner.js +2 -6
- package/src/dashboard/vite-plugin-morph-data.js +5 -19
- package/src/lib/ai/ai-checks.js +7 -52
- package/src/lib/ai/ai-project-scan.js +14 -108
- package/src/lib/ai/code-audit.js +7 -63
- package/src/lib/ai/pin-drift.js +8 -51
- package/src/lib/ai/registry-audit.js +11 -90
- package/src/lib/ci/ci-verify.js +4 -19
- package/src/lib/ci/github-ci.js +9 -37
- package/src/lib/ci/workflow-scaffold.js +2 -10
- package/src/lib/detectors/claude-config-detector.js +13 -74
- package/src/lib/detectors/index.js +4 -50
- package/src/lib/e2e/compose-env.js +5 -66
- package/src/lib/e2e/stack-teardown.js +6 -40
- package/src/lib/generators/architecture-scanner.js +18 -156
- package/src/lib/generators/handoff-generator.js +9 -53
- package/src/lib/generators/scaffold-generator.js +5 -101
- package/src/lib/generators/settings-generator.js +4 -37
- package/src/lib/git/branch-classifier.js +11 -84
- package/src/lib/git/default-branch.js +14 -169
- package/src/lib/git/evidence-staleness.js +1 -0
- package/src/lib/git/gh.js +2 -17
- package/src/lib/git/pending-prs.js +6 -37
- package/src/lib/git/pr-files.js +3 -28
- package/src/lib/git/pr-index.js +3 -34
- package/src/lib/git/tree-fingerprint.js +3 -15
- package/src/lib/git/tree-state.js +6 -33
- package/src/lib/graph/god-nodes.js +2 -22
- package/src/lib/graph/graph-health.js +3 -24
- package/src/lib/graph/graph-loader.js +5 -56
- package/src/lib/graph/graphify-runtime.js +7 -37
- package/src/lib/graph/graphifyignore.js +8 -74
- package/src/lib/graph/sanitize.js +16 -136
- package/src/lib/heavy/cleanup.js +1 -1
- package/src/lib/installers/effects.js +8 -93
- package/src/lib/installers/mcp-installer.js +9 -169
- package/src/lib/installers/memory-installer.js +4 -27
- package/src/lib/installers/templates-reconcile.js +5 -60
- package/src/lib/mutation/dotnet-introspection.js +20 -106
- package/src/lib/mutation/subset.js +1 -4
- package/src/lib/plan/artifact-vocabulary.js +5 -25
- package/src/lib/plan/checks.js +5 -31
- package/src/lib/plan/plan-parser.js +1 -6
- package/src/lib/projects/registry.js +7 -44
- package/src/lib/review-reports.js +5 -23
- package/src/lib/risk-detector.js +39 -173
- package/src/lib/specs/adr-index.js +6 -34
- package/src/lib/specs/frontmatter.js +2 -15
- package/src/lib/specs/gfm-table.js +1 -7
- package/src/lib/specs/revocations.js +1 -14
- package/src/lib/specs/screen-checklist.js +47 -124
- package/src/lib/stack/dotnet-build-ignores.js +2 -6
- package/src/lib/stack/stack-profile.js +1 -28
- package/src/lib/stack/stack-scan.js +1 -8
- package/src/lib/stack-filter.js +4 -27
- package/src/lib/standards/registry.js +1 -7
- package/src/lib/standards/sections.js +2 -13
- package/src/lib/standards/verified-against.js +19 -191
- package/src/lib/tasks/eval-node.js +21 -169
- package/src/lib/tasks/linux-runner.js +18 -142
- package/src/lib/tasks/test-nodes.js +43 -240
- package/src/lib/tasks/test-runner.js +43 -341
- package/src/lib/telemetry/metrics.js +7 -36
- package/src/lib/telemetry/telemetry-reader.js +13 -125
- package/src/lib/telemetry/verify-event.js +2 -30
- package/src/lib/validators/code-budget/code-budget-validator.js +3 -43
- package/src/lib/validators/code-budget/index.js +0 -7
- package/src/lib/validators/contracts/contract-compliance-validator.js +2 -24
- package/src/lib/validators/contracts/index.js +0 -6
- package/src/lib/validators/nextjs/index.js +0 -5
- package/src/lib/validators/nextjs/next-component-validator.js +4 -81
- package/src/lib/validators/packages/index.js +0 -6
- package/src/lib/validators/packages/package-validator.js +6 -56
- package/src/lib/validators/reuse/index.js +0 -8
- package/src/lib/validators/reuse/reuse-validator.js +11 -101
- package/src/lib/validators/reuse/symbol-scanner.js +6 -62
- package/src/lib/validators/traceability-check.js +5 -63
- package/src/lib/validators/validation-runner.js +20 -125
- package/src/lib/validators/verify-runner.js +52 -358
- package/src/lib/work-type-detector.js +5 -67
- package/src/lib/worktree/docker-audit.js +17 -136
- package/src/lib/worktree/env-allocator.js +10 -98
- package/src/lib/worktree/feature-truth.js +12 -115
- package/src/lib/worktree/finish-backup.js +32 -242
- package/src/lib/worktree/fleet-scanner.js +13 -95
- package/src/lib/worktree/ignored-triage.js +15 -91
- package/src/lib/worktree/vscode-settings.js +2 -3
- package/src/lib/worktree/worktree-audit.js +21 -213
- package/src/lib/worktree/worktree-include.js +3 -5
- package/src/scripts/global-install.js +3 -9
- package/src/scripts/install-plugin.js +1 -13
- package/src/scripts/setup-infra.js +17 -147
- package/src/utils/agents-installer.js +28 -275
- package/src/utils/banner.js +4 -25
- package/src/utils/claude-md-injector.js +1 -37
- package/src/utils/claude-settings-manager.js +1 -52
- package/src/utils/file-copier.js +11 -95
- package/src/utils/hooks-installer.js +21 -298
- package/src/utils/skills-installer.js +4 -101
- package/src/utils/version-checker.js +4 -83
- package/framework/commands/morph-proposal.md +0 -9
- package/src/commands/utils/index.js +0 -3
- package/src/lib/detectors/design-system-detector.js +0 -188
- package/src/lib/generators/design-system-generator.js +0 -508
- package/src/lib/specs/old-format.js +0 -68
- package/src/utils/agent-id-mapper.js +0 -11
|
@@ -1,29 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* half of the missing-report protocol.
|
|
4
|
-
*
|
|
5
|
-
* The slice report file is the sub-agent's authoritative return value; the
|
|
6
|
-
* final message is a courtesy. This command answers what the orchestrator
|
|
7
|
-
* needs when it resumes control, from evidence rather than from the agent's
|
|
8
|
-
* own word: **did the WORK land, independently of whether the REPORT did?**
|
|
9
|
-
*
|
|
10
|
-
* verdict action
|
|
11
|
-
* ───────────────────────────── ──────────────────
|
|
12
|
-
* ok proceed
|
|
13
|
-
* report-stale refresh-from-evidence
|
|
14
|
-
* report-incomplete resume | complete-degraded
|
|
15
|
-
* work-landed-report-missing accept-degraded
|
|
16
|
-
* no-work-no-report redispatch
|
|
17
|
-
* report-missing-evidence-unknown redispatch (conservative default)
|
|
18
|
-
*
|
|
19
|
-
* The observed evidence is keyed by the dispatch: the agent whose `agentId`
|
|
20
|
-
* wrote the report owns the window, and its writes after the report make the
|
|
21
|
-
* report stale. A report written inline (no sub-agent) is judged by its
|
|
22
|
-
* markers only.
|
|
23
|
-
*
|
|
24
|
-
* Exit 0 only for `ok`. The output also carries `tree` (see tree-state.js),
|
|
25
|
-
* information only — it never changes the verdict.
|
|
26
|
-
*/
|
|
1
|
+
// Judged from observed evidence, never the agent's word: did the WORK land, independently of
|
|
2
|
+
// the REPORT? The `tree` block is information only and never changes the verdict.
|
|
27
3
|
|
|
28
4
|
import chalk from 'chalk';
|
|
29
5
|
import { existsSync, readFileSync, statSync } from 'fs';
|
|
@@ -54,7 +30,7 @@ export function inspectReviewReports(cwd, feature) {
|
|
|
54
30
|
return out;
|
|
55
31
|
}
|
|
56
32
|
|
|
57
|
-
|
|
33
|
+
// The verifier routes to opus on prompt/model or media; security is dispatched only when required.
|
|
58
34
|
function reviewDispatch(cwd, feature, slices) {
|
|
59
35
|
let changedFiles = [];
|
|
60
36
|
try { changedFiles = changedPathsAgainst(detectDefaultBranch(cwd), cwd); } catch { changedFiles = []; }
|
|
@@ -74,20 +50,12 @@ function reviewDispatch(cwd, feature, slices) {
|
|
|
74
50
|
};
|
|
75
51
|
}
|
|
76
52
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
* from the telemetry hook. Two seconds absorb their skew and are far less
|
|
80
|
-
* than any real work done after reporting.
|
|
81
|
-
*/
|
|
53
|
+
// Two clocks: the report mtime comes from the filesystem, the event `ts` from the hook. Two
|
|
54
|
+
// seconds absorb their skew and are far less than any real work done after reporting.
|
|
82
55
|
export const STALE_TOLERANCE_MS = 2000;
|
|
83
56
|
|
|
84
57
|
const slash = (p) => String(p || '').replace(/\\/g, '/');
|
|
85
58
|
|
|
86
|
-
/**
|
|
87
|
-
* The event target relative to the tree that logged it, the same shape as the
|
|
88
|
-
* slice's `arquivos:`. `Write`/`Edit` targets are absolute; a target outside
|
|
89
|
-
* that tree comes back as it was.
|
|
90
|
-
*/
|
|
91
59
|
function toTreeRelative(target, treePath) {
|
|
92
60
|
const norm = slash(target);
|
|
93
61
|
const root = slash(treePath).replace(/\/+$/, '');
|
|
@@ -98,7 +66,6 @@ function toTreeRelative(target, treePath) {
|
|
|
98
66
|
|
|
99
67
|
const reportSuffix = (feature, sliceId) => `.morph/features/${feature}/3-implement/reports/${sliceId}.md`.toLowerCase();
|
|
100
68
|
|
|
101
|
-
/** Every successful Write/Edit of every tree of this repo, oldest first. */
|
|
102
69
|
function loadWrites(cwd) {
|
|
103
70
|
let trees = [];
|
|
104
71
|
try { trees = loadTelemetryAllTrees(cwd); } catch { trees = []; }
|
|
@@ -115,14 +82,8 @@ function loadWrites(cwd) {
|
|
|
115
82
|
return { available: trees.some((t) => t.events.length > 0), writes: dedupeEvents(events) };
|
|
116
83
|
}
|
|
117
84
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
*
|
|
121
|
-
* The dispatch that owns the slice is the sub-agent that wrote its report —
|
|
122
|
-
* the latest such write carrying an `agentId`; an orchestrator edit made
|
|
123
|
-
* afterwards does not hide it. Without a report, any write to one of the
|
|
124
|
-
* slice's files says the work landed.
|
|
125
|
-
*/
|
|
85
|
+
// The owning dispatch is the latest sub-agent write of the report; a later orchestrator edit
|
|
86
|
+
// does not hide it. Without a report, any write to one of the slice files says the work landed.
|
|
126
87
|
function observedEvidence(telemetry, feature, slice) {
|
|
127
88
|
if (!telemetry.available) return { available: false };
|
|
128
89
|
const suffix = reportSuffix(feature, slice.id);
|
|
@@ -150,7 +111,6 @@ function observedEvidence(telemetry, feature, slice) {
|
|
|
150
111
|
};
|
|
151
112
|
}
|
|
152
113
|
|
|
153
|
-
/** Is the report older than the last work its own dispatch observed? */
|
|
154
114
|
function isReportStale(reportFsPath, observed) {
|
|
155
115
|
if (!observed?.agentId || !observed.lastWriteAt) return false;
|
|
156
116
|
try {
|
|
@@ -160,7 +120,6 @@ function isReportStale(reportFsPath, observed) {
|
|
|
160
120
|
}
|
|
161
121
|
}
|
|
162
122
|
|
|
163
|
-
/** Combine report state + evidence into a verdict and the action it implies. */
|
|
164
123
|
function decide(report, observed, stale) {
|
|
165
124
|
if (report.exists && report.complete) {
|
|
166
125
|
// Structural integrity answers "is the report complete?", never "is it
|
|
@@ -200,11 +159,7 @@ function snapshotPath(cwd, feature) {
|
|
|
200
159
|
return join(cwd, '.morph', 'features', feature, '4-review', 'tree-before-eval.json');
|
|
201
160
|
}
|
|
202
161
|
|
|
203
|
-
|
|
204
|
-
* The tree block, compared with the snapshot taken before the review agents were
|
|
205
|
-
* dispatched when there is one. `beforeEval` takes a new snapshot after the
|
|
206
|
-
* comparison, so the output still shows what changed since the previous one.
|
|
207
|
-
*/
|
|
162
|
+
// `beforeEval` snapshots after comparing, so the output still shows what changed since the last one.
|
|
208
163
|
function treeFor(cwd, feature, beforeEval, slices = []) {
|
|
209
164
|
const path = snapshotPath(cwd, feature);
|
|
210
165
|
let snapshot = null;
|
|
@@ -218,11 +173,6 @@ function treeFor(cwd, feature, beforeEval, slices = []) {
|
|
|
218
173
|
return { tree, beforeEval: { at: next.at, files: Object.keys(next.files).length }, reviewModels };
|
|
219
174
|
}
|
|
220
175
|
|
|
221
|
-
/**
|
|
222
|
-
* @param {string} feature
|
|
223
|
-
* @param {string[]|undefined} sliceIds - audits only these slices; none audits every slice
|
|
224
|
-
* @param {{ json?: boolean, cwd?: string, beforeEval?: boolean }} [options]
|
|
225
|
-
*/
|
|
226
176
|
export function reportCheckCommand(feature, sliceIds, options = {}) {
|
|
227
177
|
const cwd = options.cwd || process.cwd();
|
|
228
178
|
const emit = (obj, code) => {
|
|
@@ -5,28 +5,8 @@ import { getFeature, setApprovalGate, updateFeature } from '../../core/state/sta
|
|
|
5
5
|
import { WORK_TYPES, isValidWorkType, POLICY_APPROVER_PREFIX } from '../../core/work-types.js';
|
|
6
6
|
import { renderPlanSkeleton } from '../../lib/plan/plan-template.js';
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* asymmetric gate rules that preserve GENUINE human/auto approvals and only
|
|
11
|
-
* ever undo policy stamps:
|
|
12
|
-
*
|
|
13
|
-
* - Blocked outright once Gate 3 (review) is approved — the feature already
|
|
14
|
-
* shipped its pipeline, retyping it retroactively would be revisionist.
|
|
15
|
-
* - Downgrade (fewer skipped gates → more skipped gates, e.g. feature/bug
|
|
16
|
-
* → chore/hotfix) is blocked once the plan gate carries a genuine
|
|
17
|
-
* (non-policy) approval — the plan was already accepted under the old
|
|
18
|
-
* type's scrutiny; retyping around it would erase that judgment call.
|
|
19
|
-
* - Upgrade (more skipped gates → fewer, e.g. chore/hotfix → feature/bug)
|
|
20
|
-
* is always allowed before review: it only ever ADDS scrutiny.
|
|
21
|
-
*
|
|
22
|
-
* Applying a retype resets exactly the gates that were stamped
|
|
23
|
-
* `policy:{oldType}` and the NEW type no longer skips (so the human is asked
|
|
24
|
-
* again), stamps `policy:{newType}` on any of the new type's skippedGates
|
|
25
|
-
* that aren't already approved (a prior genuine approval is left standing —
|
|
26
|
-
* that's the "preserves human approvals" half of the contract), scaffolds
|
|
27
|
-
* any phase folders the new type needs that don't exist yet, and appends an
|
|
28
|
-
* audit entry to `workTypeHistory`.
|
|
29
|
-
*/
|
|
8
|
+
// Retype only ever undoes `policy:` stamps: blocked once review is approved; a downgrade
|
|
9
|
+
// is blocked once the plan carries a genuine approval; an upgrade only adds scrutiny.
|
|
30
10
|
export async function retypeCommand(featureName, newType, options = {}) {
|
|
31
11
|
try {
|
|
32
12
|
if (!isValidWorkType(newType)) {
|
|
@@ -69,8 +49,7 @@ export async function retypeCommand(featureName, newType, options = {}) {
|
|
|
69
49
|
|
|
70
50
|
const now = new Date().toISOString();
|
|
71
51
|
|
|
72
|
-
//
|
|
73
|
-
// longer skips — a genuine (non-policy) approval is never touched.
|
|
52
|
+
// A genuine (non-policy) approval is never reset.
|
|
74
53
|
const resetGates = [];
|
|
75
54
|
for (const [gate, data] of Object.entries(feature.approvalGates || {})) {
|
|
76
55
|
const isPolicyStamped = String(data?.approvedBy || '').startsWith(POLICY_APPROVER_PREFIX);
|
|
@@ -80,8 +59,7 @@ export async function retypeCommand(featureName, newType, options = {}) {
|
|
|
80
59
|
}
|
|
81
60
|
}
|
|
82
61
|
|
|
83
|
-
//
|
|
84
|
-
// approved (preserves a prior genuine approval untouched).
|
|
62
|
+
// A gate already approved keeps its genuine approval.
|
|
85
63
|
const stampedGates = [];
|
|
86
64
|
for (const gate of newWorkType.skippedGates) {
|
|
87
65
|
if (!feature.approvalGates?.[gate]?.approved) {
|
|
@@ -23,11 +23,8 @@ const EXIT = { ok: 0, failed: 1, usage: 2, busy: 3, unsafe: 4 };
|
|
|
23
23
|
const KEEP_RUNS = 2;
|
|
24
24
|
const DEFAULT_JOBS = 2;
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
* are taken only if free right now. The run uses as many workers as it got,
|
|
29
|
-
* so a busy machine degrades to sequential instead of queueing.
|
|
30
|
-
*/
|
|
26
|
+
// One heavy slot per worker: the first waits, the others are taken only if free now,
|
|
27
|
+
// so a busy machine degrades to sequential instead of queueing.
|
|
31
28
|
export function workerGate({ root, feature, jobs }) {
|
|
32
29
|
const config = readHeavyConfig(root);
|
|
33
30
|
const gates = [createHeavyGate({ projectPath: root, label: 'mutate', feature, config })];
|
|
@@ -1,34 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MORPH-SPEC Feature Validation Command
|
|
3
|
-
*
|
|
4
|
-
* Content-aware validation of feature outputs at each phase.
|
|
5
|
-
* Checks section presence, schema consistency, and contract correctness.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* morph-spec validate <feature-name> [--phase <phase>] [--json] [--verbose]
|
|
9
|
-
*
|
|
10
|
-
* Example:
|
|
11
|
-
* morph-spec validate user-portal --phase plan
|
|
12
|
-
* morph-spec validate user-portal --json
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
1
|
import fs from 'fs';
|
|
16
2
|
import path from 'path';
|
|
17
3
|
import chalk from 'chalk';
|
|
18
4
|
import { parsePlan } from '../../lib/plan/plan-parser.js';
|
|
19
5
|
import { knownPersonas, planHash, validatePlan } from '../../lib/plan/checks.js';
|
|
20
6
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
* (headings and line-start bold labels), not whole-file substrings; see
|
|
24
|
-
* `extractSectionMarkers`. plan.md is judged by the plan parser instead.
|
|
25
|
-
*
|
|
26
|
-
* required → ERROR if absent.
|
|
27
|
-
* recommended → WARNING if absent.
|
|
28
|
-
* optional → silent. Tracked when present, never penalized.
|
|
29
|
-
*
|
|
30
|
-
* Each entry is a string or an array of acceptable aliases (any-of).
|
|
31
|
-
*/
|
|
7
|
+
// Matched against section markers (headings and line-start bold labels), never whole-file
|
|
8
|
+
// substrings. required → ERROR, recommended → WARNING, optional → never penalized; an array is any-of.
|
|
32
9
|
const OUTPUT_SECTIONS = {
|
|
33
10
|
'verifier.md': {
|
|
34
11
|
required: [
|
|
@@ -60,9 +37,6 @@ const OUTPUT_SECTIONS = {
|
|
|
60
37
|
},
|
|
61
38
|
};
|
|
62
39
|
|
|
63
|
-
/**
|
|
64
|
-
* Phase-to-output mapping (v5 NLH)
|
|
65
|
-
*/
|
|
66
40
|
const PHASE_OUTPUTS = {
|
|
67
41
|
plan: ['plan.md', 'checks.json'],
|
|
68
42
|
uiux: ['design-system.md', 'mockups.md', 'components.md', 'flows.md'],
|
|
@@ -70,9 +44,6 @@ const PHASE_OUTPUTS = {
|
|
|
70
44
|
review: ['verifier.md', 'reviewer.md'],
|
|
71
45
|
};
|
|
72
46
|
|
|
73
|
-
/**
|
|
74
|
-
* Phase-to-folder mapping (v5 filesystem convention)
|
|
75
|
-
*/
|
|
76
47
|
const PHASE_FOLDERS = {
|
|
77
48
|
plan: '2-plan',
|
|
78
49
|
uiux: '1-design',
|
|
@@ -80,20 +51,7 @@ const PHASE_FOLDERS = {
|
|
|
80
51
|
review: '4-review',
|
|
81
52
|
};
|
|
82
53
|
|
|
83
|
-
|
|
84
|
-
* Extract section markers from markdown content.
|
|
85
|
-
*
|
|
86
|
-
* A marker is a line that structurally introduces a section:
|
|
87
|
-
* - a markdown heading → `## Arquitetura da solução`
|
|
88
|
-
* - a line-start bold label → `**Status:** Aceito`
|
|
89
|
-
*
|
|
90
|
-
* Bold labels are included on purpose (deviation from heading-only): reports
|
|
91
|
-
* write `**Verdict:**` as a line-start bold label, not a heading. They are
|
|
92
|
-
* structural, not prose, so this avoids the accidental-prose-match problem that
|
|
93
|
-
* whole-file substring matching had.
|
|
94
|
-
*
|
|
95
|
-
* Returns lowercased marker texts.
|
|
96
|
-
*/
|
|
54
|
+
// Line-start bold labels count: reports write `**Verdict:**` as a label, not a heading.
|
|
97
55
|
function extractSectionMarkers(content) {
|
|
98
56
|
const markers = [];
|
|
99
57
|
for (const rawLine of content.split('\n')) {
|
|
@@ -111,9 +69,6 @@ function extractSectionMarkers(content) {
|
|
|
111
69
|
return markers;
|
|
112
70
|
}
|
|
113
71
|
|
|
114
|
-
/**
|
|
115
|
-
* Validate a single output file
|
|
116
|
-
*/
|
|
117
72
|
function validateOutput(featurePath, outputFile) {
|
|
118
73
|
const filePath = path.join(featurePath, outputFile);
|
|
119
74
|
const result = {
|
|
@@ -143,7 +98,6 @@ function validateOutput(featurePath, outputFile) {
|
|
|
143
98
|
return result;
|
|
144
99
|
}
|
|
145
100
|
|
|
146
|
-
// Skip section checks for files without defined sections
|
|
147
101
|
const sections = OUTPUT_SECTIONS[outputFile];
|
|
148
102
|
if (!sections) {
|
|
149
103
|
return result;
|
|
@@ -151,7 +105,6 @@ function validateOutput(featurePath, outputFile) {
|
|
|
151
105
|
|
|
152
106
|
validateMarkdownSections(content, sections, result);
|
|
153
107
|
|
|
154
|
-
// Size warnings
|
|
155
108
|
if (result.lineCount < 10) {
|
|
156
109
|
result.warnings.push(`File is very short (${result.lineCount} lines) — may be incomplete`);
|
|
157
110
|
}
|
|
@@ -159,10 +112,7 @@ function validateOutput(featurePath, outputFile) {
|
|
|
159
112
|
return result;
|
|
160
113
|
}
|
|
161
114
|
|
|
162
|
-
|
|
163
|
-
* plan.md is judged by the same parser and rules as `morph-spec plan-check`.
|
|
164
|
-
* `planDir` is `.morph/features/{f}/2-plan`, so the project is four levels up.
|
|
165
|
-
*/
|
|
115
|
+
// `planDir` is `.morph/features/{f}/2-plan`, so the project is four levels up.
|
|
166
116
|
function validatePlanOutput(content, planDir, result) {
|
|
167
117
|
const plan = parsePlan(content);
|
|
168
118
|
const { errors, warnings } = validatePlan(plan, { personas: knownPersonas(path.resolve(planDir, '..', '..', '..', '..')) });
|
|
@@ -171,7 +121,6 @@ function validatePlanOutput(content, planDir, result) {
|
|
|
171
121
|
result.sections.found.push(...plan.sections.filter((x) => x.key).map((x) => x.name));
|
|
172
122
|
}
|
|
173
123
|
|
|
174
|
-
/** checks.json must parse and carry the hash of the plan.md beside it. */
|
|
175
124
|
function validateChecksOutput(content, planDir, result) {
|
|
176
125
|
let doc;
|
|
177
126
|
try {
|
|
@@ -189,16 +138,6 @@ function validateChecksOutput(content, planDir, result) {
|
|
|
189
138
|
result.sections.found.push(`${doc.checks?.length ?? 0} check(s), ${doc.slices?.length ?? 0} slice(s)`);
|
|
190
139
|
}
|
|
191
140
|
|
|
192
|
-
/**
|
|
193
|
-
* Validate markdown sections against section markers (headings + bold labels).
|
|
194
|
-
*
|
|
195
|
-
* Each `entry` is a string or array of acceptable aliases (any-of). A section
|
|
196
|
-
* is present when at least one alias is a substring of at least one marker.
|
|
197
|
-
*
|
|
198
|
-
* required → missing produces an ERROR
|
|
199
|
-
* recommended → missing produces a WARNING
|
|
200
|
-
* optional → tracked when present, never penalized
|
|
201
|
-
*/
|
|
202
141
|
function validateMarkdownSections(content, sections, result) {
|
|
203
142
|
const markers = extractSectionMarkers(content);
|
|
204
143
|
const match = (entry) => {
|
|
@@ -234,13 +173,6 @@ function validateMarkdownSections(content, sections, result) {
|
|
|
234
173
|
}
|
|
235
174
|
}
|
|
236
175
|
|
|
237
|
-
/**
|
|
238
|
-
* Resolve the list of `{ file, dir }` outputs to validate.
|
|
239
|
-
*
|
|
240
|
-
* @param {string} featurePath Absolute path to the feature directory.
|
|
241
|
-
* @param {string|null} phase Phase key, or null to validate everything.
|
|
242
|
-
* @returns {{ outputFiles?: Array<{file:string, dir:string}>, error?: string }}
|
|
243
|
-
*/
|
|
244
176
|
function resolveOutputFiles(featurePath, phase) {
|
|
245
177
|
if (phase) {
|
|
246
178
|
const files = PHASE_OUTPUTS[phase];
|
|
@@ -250,7 +182,6 @@ function resolveOutputFiles(featurePath, phase) {
|
|
|
250
182
|
return { outputFiles: files.map(file => ({ file, dir: searchPath })) };
|
|
251
183
|
}
|
|
252
184
|
|
|
253
|
-
// Validate every existing output across the feature root + phase folders.
|
|
254
185
|
const outputFiles = [];
|
|
255
186
|
const isOutput = name => name.endsWith('.md') || name.endsWith('.cs') || name === 'checks.json';
|
|
256
187
|
const entries = fs.readdirSync(featurePath, { withFileTypes: true });
|
|
@@ -267,17 +198,7 @@ function resolveOutputFiles(featurePath, phase) {
|
|
|
267
198
|
return { outputFiles };
|
|
268
199
|
}
|
|
269
200
|
|
|
270
|
-
/**
|
|
271
|
-
* Pure validation core — no console output, no process.exit.
|
|
272
|
-
*
|
|
273
|
-
* @param {string} featurePath Absolute path to the feature directory.
|
|
274
|
-
* @param {{ phase?: string }} [options]
|
|
275
|
-
* @returns {{
|
|
276
|
-
* feature: string, phase: string, valid: boolean, error?: string,
|
|
277
|
-
* totalErrors: number, totalWarnings: number, totalFound: number,
|
|
278
|
-
* results: object[]
|
|
279
|
-
* }}
|
|
280
|
-
*/
|
|
201
|
+
/** Pure core: no console output, no process.exit. */
|
|
281
202
|
export function runFeatureValidation(featurePath, options = {}) {
|
|
282
203
|
const phase = options.phase || null;
|
|
283
204
|
const feature = path.basename(featurePath);
|
|
@@ -312,9 +233,6 @@ export function runFeatureValidation(featurePath, options = {}) {
|
|
|
312
233
|
};
|
|
313
234
|
}
|
|
314
235
|
|
|
315
|
-
/**
|
|
316
|
-
* Main validate command (CLI wrapper around `runFeatureValidation`)
|
|
317
|
-
*/
|
|
318
236
|
export async function validateFeatureCommand(featureName, options = {}) {
|
|
319
237
|
if (!featureName) {
|
|
320
238
|
console.error(chalk.red('Error: Feature name required'));
|
|
@@ -340,7 +258,6 @@ export async function validateFeatureCommand(featureName, options = {}) {
|
|
|
340
258
|
const { results, totalErrors, totalWarnings, totalFound } = validation;
|
|
341
259
|
const schemaWarnings = [];
|
|
342
260
|
|
|
343
|
-
// JSON output mode
|
|
344
261
|
if (options.json) {
|
|
345
262
|
const output = {
|
|
346
263
|
feature: featureName,
|
|
@@ -367,7 +284,6 @@ export async function validateFeatureCommand(featureName, options = {}) {
|
|
|
367
284
|
return;
|
|
368
285
|
}
|
|
369
286
|
|
|
370
|
-
// Visual output
|
|
371
287
|
console.log(chalk.cyan('\n┌────────────────────────────────────────────────────────────┐'));
|
|
372
288
|
console.log(chalk.cyan('│ MORPH-SPEC FEATURE VALIDATOR │'));
|
|
373
289
|
console.log(chalk.cyan('└────────────────────────────────────────────────────────────┘\n'));
|
|
@@ -376,7 +292,6 @@ export async function validateFeatureCommand(featureName, options = {}) {
|
|
|
376
292
|
if (phase) console.log(chalk.gray(`Phase: ${phase}`));
|
|
377
293
|
console.log('');
|
|
378
294
|
|
|
379
|
-
// File-by-file results
|
|
380
295
|
for (const result of results) {
|
|
381
296
|
const symbol = result.exists
|
|
382
297
|
? (result.errors.length === 0 ? '✅' : '❌')
|
|
@@ -387,25 +302,21 @@ export async function validateFeatureCommand(featureName, options = {}) {
|
|
|
387
302
|
|
|
388
303
|
console.log(`${symbol} ${name}${result.exists ? chalk.gray(` (${result.lineCount} lines)`) : ''}`);
|
|
389
304
|
|
|
390
|
-
// Sections found
|
|
391
305
|
if (result.sections.found.length > 0 && (options.verbose || result.errors.length > 0)) {
|
|
392
306
|
result.sections.found.forEach(s => {
|
|
393
307
|
console.log(chalk.green(` ✓ ${s}`));
|
|
394
308
|
});
|
|
395
309
|
}
|
|
396
310
|
|
|
397
|
-
// Errors
|
|
398
311
|
result.errors.forEach(err => {
|
|
399
312
|
console.log(chalk.red(` ✗ ${err}`));
|
|
400
313
|
});
|
|
401
314
|
|
|
402
|
-
// Warnings
|
|
403
315
|
result.warnings.forEach(warn => {
|
|
404
316
|
console.log(chalk.yellow(` ⚠ ${warn}`));
|
|
405
317
|
});
|
|
406
318
|
}
|
|
407
319
|
|
|
408
|
-
// Summary
|
|
409
320
|
console.log('');
|
|
410
321
|
console.log(chalk.gray('─'.repeat(60)));
|
|
411
322
|
|
|
@@ -14,7 +14,6 @@ import { afterHeavyRun } from '../../lib/heavy/cleanup.js';
|
|
|
14
14
|
import { readHeavyConfig } from '../../../framework/hooks/shared/heavy-config.js';
|
|
15
15
|
import { runCiVerify, CiUsageError, reusableLocalStamp } from '../../lib/ci/ci-verify.js';
|
|
16
16
|
|
|
17
|
-
/** ✓ pass · ✗ fail · – skip */
|
|
18
17
|
function statusIcon(status) {
|
|
19
18
|
if (status === 'pass') return chalk.green('✓');
|
|
20
19
|
if (status === 'fail') return chalk.red('✗');
|
|
@@ -69,27 +68,9 @@ function formatVerifyResult(result) {
|
|
|
69
68
|
|
|
70
69
|
const samePath = (a, b) => canonicalPath(a).replace(/\\/g, '/') === canonicalPath(b).replace(/\\/g, '/');
|
|
71
70
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
* the PRIMARY ROOT so every tree gets the same answer. Any other tree (a
|
|
76
|
-
* sub-agent's `morph-task/*` tree branched from `morph/{f}`, a scratch
|
|
77
|
-
* checkout) must never write feature.json: its copy is not the one the gates
|
|
78
|
-
* read, and the write would merge-conflict with the owner's.
|
|
79
|
-
*
|
|
80
|
-
* The old rule read only the root's `worktree.path` pointer, which a feature
|
|
81
|
-
* created INSIDE its worktree never has: a `worktrees/homolog-v2` on
|
|
82
|
-
* `morph/chat-demo-simulacao` measured `unregistered-worktree` on 11/11 runs,
|
|
83
|
-
* and `repeatOf` (computed only when persisting) never fired. Git's worktree
|
|
84
|
-
* registry names that tree as the owner.
|
|
85
|
-
*
|
|
86
|
-
* The root is not special: with a live worktree owning the feature, the
|
|
87
|
-
* root's copy is the birth-instant skeleton and a stamp there is drift —
|
|
88
|
-
* `owner-tree-elsewhere`, the verify side of `assertOwnerTree`.
|
|
89
|
-
*
|
|
90
|
-
* @param {{ cwd: string, featureName: string, noPersist: boolean }} params
|
|
91
|
-
* @returns {{ persist: boolean, reason: string|null }}
|
|
92
|
-
*/
|
|
71
|
+
// Persists only in the tree that OWNS the feature (`resolveFeatureTree`, asked from the PRIMARY
|
|
72
|
+
// ROOT so every tree agrees): another tree's copy is not what the gates read, and writing it
|
|
73
|
+
// would merge-conflict with the owner's.
|
|
93
74
|
function decidePersistence({ cwd, featureName, noPersist }) {
|
|
94
75
|
if (noPersist) return { persist: false, reason: 'no-persist-flag' };
|
|
95
76
|
|
|
@@ -106,17 +87,8 @@ function baselineStampBlock(baseline) {
|
|
|
106
87
|
return `record-baseline: a contagem caiu em ${fell.map((u) => `${u.id} (${u.from} → ${u.to})`).join(', ')}`;
|
|
107
88
|
}
|
|
108
89
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
* writers of it — the history entry (`verifications.feature`) and the
|
|
112
|
-
* singular gate stamp (`verification.nodes`). One function because they were
|
|
113
|
-
* two literal copies twenty-five lines apart: a fifth node meant remembering
|
|
114
|
-
* both, and a stamp missing a node the history has is precisely the kind of
|
|
115
|
-
* half-truth the gate reads as fact.
|
|
116
|
-
*
|
|
117
|
-
* @param {{ nodes: Record<string, { status: string }> }} result - runVerify() output
|
|
118
|
-
* @returns {{ build: string, tests: string, validators: string, e2e: string }}
|
|
119
|
-
*/
|
|
90
|
+
// The single writer of both node maps the schema fixes (`verification.nodes` and the history
|
|
91
|
+
// entry): a stamp missing a node the history has is a half-truth the gate reads as fact.
|
|
120
92
|
function nodeStatuses(result) {
|
|
121
93
|
return {
|
|
122
94
|
build: result.nodes.build.status,
|
|
@@ -127,25 +99,8 @@ function nodeStatuses(result) {
|
|
|
127
99
|
};
|
|
128
100
|
}
|
|
129
101
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
*
|
|
133
|
-
* The rollup cannot answer the question Gate 3 asks. A red eval and a red unit
|
|
134
|
-
* test both arrive as `tests: 'fail'`, and — worse — an eval node that was
|
|
135
|
-
* SKIPPED (`--filter`) leaves the rollup green, so a gate reading `tests` alone
|
|
136
|
-
* cannot tell "the eval ran and passed" from "the eval never ran".
|
|
137
|
-
*
|
|
138
|
-
* Four values, and the distinction between the last two is the whole point:
|
|
139
|
-
* - `'pass'` / `'fail'`: every declared eval node agreed, or at least one did
|
|
140
|
-
* not. Same pessimistic rollup rule the tests node already uses;
|
|
141
|
-
* - `'skip'`: eval nodes ARE declared, but this run did not execute them;
|
|
142
|
-
* - `null`: this project declares NO eval node. Not a skip — there is nothing
|
|
143
|
-
* to skip. `risk-detector.js` pauses on the two for different reasons,
|
|
144
|
-
* because they call for different actions.
|
|
145
|
-
*
|
|
146
|
-
* @param {{nodes: {tests: {runs?: Array}}}} result - runVerify() output
|
|
147
|
-
* @returns {'pass'|'fail'|'skip'|null}
|
|
148
|
-
*/
|
|
102
|
+
// `'skip'` = eval nodes declared but not run; `null` = no eval node declared. A skipped eval
|
|
103
|
+
// leaves the tests rollup green, so the gate needs this split to tell them apart.
|
|
149
104
|
function evalsStatusOf(result) {
|
|
150
105
|
const runs = (result.nodes.tests?.runs || []).filter((run) => run.runner === EVAL_RUNNER);
|
|
151
106
|
if (runs.length === 0) return null;
|
|
@@ -154,10 +109,7 @@ function evalsStatusOf(result) {
|
|
|
154
109
|
return 'skip';
|
|
155
110
|
}
|
|
156
111
|
|
|
157
|
-
|
|
158
|
-
* The options that make a run partial, each with the `reason` it
|
|
159
|
-
* reports when it keeps the stamp from being written — first match wins.
|
|
160
|
-
*/
|
|
112
|
+
// First match wins; each keeps the stamp from being written, with its `reason`.
|
|
161
113
|
const PARTIAL_RUN_FLAGS = [['filter', 'filtered'], ['skipTests', 'skip-tests'], ['skipBuild', 'skip-build']];
|
|
162
114
|
|
|
163
115
|
const CI_EXCLUSIVE_FLAGS = [
|
|
@@ -181,9 +133,7 @@ async function runLocalVerify(featureName, options) {
|
|
|
181
133
|
linuxImage: options.linuxImage,
|
|
182
134
|
});
|
|
183
135
|
} catch (err) {
|
|
184
|
-
//
|
|
185
|
-
// verdict. `--filter` that selects nothing must not print green (or red):
|
|
186
|
-
// there is no verification to report, only a command to fix.
|
|
136
|
+
// Exit 2, never a verdict: a `--filter` that selects nothing must not print green or red.
|
|
187
137
|
if (err instanceof VerifyUsageError) {
|
|
188
138
|
console.error(chalk.red(`\n❌ ${err.message}\n`));
|
|
189
139
|
process.exit(2);
|
|
@@ -204,25 +154,8 @@ async function runLocalVerify(featureName, options) {
|
|
|
204
154
|
}
|
|
205
155
|
}
|
|
206
156
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
* gate: build + tests + validators, BEFORE the LLM eval. See verify-runner.js
|
|
210
|
-
* for the node cadence; this command wraps it with CLI ergonomics
|
|
211
|
-
* (human/JSON output, exit codes) and context-aware persistence.
|
|
212
|
-
*
|
|
213
|
-
* Exit codes: 0 = pass (including all-skip) · 1 = fail (>=1 red node, or a
|
|
214
|
-
* `--record-baseline` the run refused to write) · 2 = usage error.
|
|
215
|
-
*
|
|
216
|
-
* Exit 2 is "the command cannot mean what you typed", and there are four
|
|
217
|
-
* paths to it — never a verdict, because a verdict about nothing is worse
|
|
218
|
-
* than a refusal:
|
|
219
|
-
* - unknown feature;
|
|
220
|
-
* - `--filter` that matches no declared node (a filtered-to-empty run is
|
|
221
|
-
* green and proves nothing);
|
|
222
|
-
* - `--record-baseline` with nowhere to write (no `config.json`);
|
|
223
|
-
* - `--linux` without Docker.
|
|
224
|
-
* The last three arrive as `VerifyUsageError` from the runner.
|
|
225
|
-
*/
|
|
157
|
+
// Exit 2 is "the command cannot mean what you typed" (unknown feature, `--filter` matching
|
|
158
|
+
// nothing, `--record-baseline` without config.json, `--linux` without Docker): never a verdict.
|
|
226
159
|
export async function verifyCommand(featureName, options = {}) {
|
|
227
160
|
let feature;
|
|
228
161
|
try {
|
|
@@ -311,22 +244,9 @@ export async function verifyCommand(featureName, options = {}) {
|
|
|
311
244
|
};
|
|
312
245
|
await updateFeature(featureName, historyKey, compact);
|
|
313
246
|
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
// EXACT commit it covers, the same way a review report's
|
|
318
|
-
// `**Reviewed-SHA:**` token lets approve.js detect a stale review. null
|
|
319
|
-
// in a non-git checkout (or before the first commit) — fail-open, never
|
|
320
|
-
// blocks persistence.
|
|
321
|
-
//
|
|
322
|
-
// `nodes` + `e2eReason` são acréscimo de campo, não mudança de forma: o
|
|
323
|
-
// rollup `status` continua onde estava. Existem porque um nó e2e que PULA
|
|
324
|
-
// não reprova o rollup — sem o status do nó e a razão LITERAL do skip, o
|
|
325
|
-
// gate-check lê 'pass' tanto quando o e2e rodou e passou quanto quando ele
|
|
326
|
-
// nunca rodou, e não tem como separar dispensa legítima ("o projeto não
|
|
327
|
-
// containeriza") de ausência de prova ("docker indisponível"). `e2eReason`
|
|
328
|
-
// é null quando o nó não pulou — e um null NUM SKIP é lido como dispensa
|
|
329
|
-
// sem motivo, que risk-detector.js recusa.
|
|
247
|
+
// `sha` ties the stamp to the exact commit (null without git: fail-open). `nodes` and `e2eReason`
|
|
248
|
+
// exist because a skipped e2e does not fail the rollup; a null reason ON A SKIP reads as a
|
|
249
|
+
// waiver without motive, which risk-detector.js refuses.
|
|
330
250
|
const stamp = {
|
|
331
251
|
status: result.status,
|
|
332
252
|
nodes: nodeStatuses(result),
|
|
@@ -391,10 +311,7 @@ export async function verifyCommand(featureName, options = {}) {
|
|
|
391
311
|
}
|
|
392
312
|
}
|
|
393
313
|
|
|
394
|
-
// A recording
|
|
395
|
-
// invocation even when the run itself was green (a filtered run is green and
|
|
396
|
-
// unrecordable): exiting 0 there would report success for something that did
|
|
397
|
-
// not happen.
|
|
314
|
+
// A recording asked for and not made fails the invocation, even when the run was green.
|
|
398
315
|
const baselineRefused = Boolean(result.baseline && !result.baseline.recorded);
|
|
399
316
|
process.exit(result.status === 'pass' && !baselineRefused ? 0 : 1);
|
|
400
317
|
}
|