@polymorphism-tech/morph-spec 8.29.0 → 8.32.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 +11 -1
- package/README.md +13 -2
- package/bin/morph-spec.js +67 -1
- package/bin/validate.js +379 -379
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +4 -1
- package/framework/MORPH.md +4 -3
- package/framework/agents.json +6 -6
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +68 -45
- package/framework/evals/rubrics/code-quality.md +17 -0
- package/framework/evals/rubrics/condensed.md +3 -1
- package/framework/hooks/claude-code/statusline.sh +11 -11
- package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
- package/framework/hooks/git/pre-commit/agents.sh +25 -25
- package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
- package/framework/hooks/git/pre-commit/specs.sh +50 -50
- package/framework/hooks/git/pre-push/run-tests.sh +44 -44
- package/framework/hooks/shared/phase-utils.js +2 -0
- package/framework/hooks/shared/stdin-reader.js +26 -26
- package/framework/mcp/registry.json +39 -4
- package/framework/rubrics/backend.json +2 -2
- package/framework/rubrics/default.json +2 -2
- package/framework/rubrics/frontend.json +2 -2
- package/framework/schemas/tasks.schema.json +25 -0
- 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 +14 -4
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
- package/framework/skills/core/morph-implement/SKILL.md +12 -2
- package/framework/skills/core/morph-plan/SKILL.md +45 -3
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +12 -4
- package/framework/skills/core/morph-stack-scan/SKILL.md +13 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +1 -1
- package/framework/standards/ai-agents/agent-session.md +160 -160
- package/framework/standards/ai-agents/batching.md +193 -193
- package/framework/standards/ai-agents/context-providers.md +188 -188
- package/framework/standards/ai-agents/mcp-server.md +250 -250
- package/framework/standards/ai-agents/mcp-tools.md +197 -197
- package/framework/standards/ai-agents/middleware-patterns.md +207 -207
- package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
- package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
- package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
- package/framework/standards/ai-agents/modalities-vision.md +201 -201
- package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
- package/framework/standards/ai-agents/production.md +231 -231
- package/framework/standards/ai-agents/prompts-in-db.md +214 -214
- package/framework/standards/ai-agents/providers/model-registry.md +158 -158
- package/framework/standards/ai-agents/service-tier-flex.md +135 -135
- package/framework/standards/ai-agents/structured-output.md +249 -249
- package/framework/standards/ai-agents/sweet-spot.md +109 -109
- package/framework/templates/.idea/morph-templates.xml +92 -0
- package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
- package/framework/templates/code/dotnet/database/migration.cs +83 -83
- package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
- package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/code/typescript/contracts.ts +167 -167
- package/framework/templates/context/ARCHITECTURE.md +20 -0
- package/framework/templates/docs/clarifications.md +253 -253
- package/framework/templates/docs/schema-analysis.md +119 -119
- package/framework/templates/docs/ui-components.md +52 -52
- package/framework/templates/docs/ui-design-system.md +76 -76
- package/framework/templates/docs/ui-flows.md +167 -167
- package/framework/templates/docs/ui-mockups.md +98 -98
- package/framework/templates/feature/decisions.md +122 -122
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
- package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/github/README.md +56 -56
- package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
- package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
- package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
- package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
- package/framework/templates/integrations/asaas-client.cs +387 -387
- package/framework/templates/integrations/asaas-webhook.cs +351 -351
- package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
- package/framework/templates/project/validate.js +120 -120
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- package/package.json +1 -1
- package/src/commands/dashboard/dashboard.js +53 -53
- package/src/commands/graph/affected.js +89 -0
- package/src/commands/graph/build.js +94 -0
- package/src/commands/graph/explain.js +96 -0
- package/src/commands/graph/status.js +93 -0
- package/src/commands/index.js +13 -13
- package/src/commands/project/branch.js +170 -0
- package/src/commands/project/checkpoint.js +209 -209
- package/src/commands/project/doctor.js +148 -9
- package/src/commands/project/fleet.js +10 -3
- package/src/commands/project/worktree.js +73 -50
- package/src/commands/state/finish.js +41 -26
- package/src/commands/state/list.js +79 -6
- package/src/core/paths/output-schema.js +7 -0
- package/src/core/state/lifecycle-audit.js +127 -0
- package/src/core/state/phase-state-machine.js +197 -197
- package/src/core/templates/template-renderer.js +477 -477
- package/src/dashboard/app/components/DetailPanel.jsx +11 -11
- package/src/dashboard/app/components/FlowGraph.jsx +17 -17
- package/src/dashboard/app/components/SearchFilter.jsx +22 -22
- package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/AgentMap.jsx +370 -370
- package/src/dashboard/app/pages/HookInspector.jsx +418 -418
- package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
- package/src/dashboard/app/pages/RulesMap.jsx +273 -273
- package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
- package/src/dashboard/app/pages/Standards.jsx +332 -332
- package/src/dashboard/app/styles/index.css +47 -47
- package/src/dashboard/index.html +12 -12
- package/src/dashboard/package.json +24 -24
- package/src/dashboard/scanner/phases-scanner.js +15 -15
- package/src/dashboard/scanner/rubrics-scanner.js +12 -12
- package/src/dashboard/scanner/rules-scanner.js +37 -37
- package/src/dashboard/scanner/skills-scanner.js +43 -43
- package/src/dashboard/scanner/squads-scanner.js +13 -13
- package/src/dashboard/vite-plugin-morph-data.js +17 -17
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/detectors/design-system-detector.js +188 -188
- package/src/lib/detectors/index.js +217 -217
- package/src/lib/generators/architecture-scanner.js +45 -4
- package/src/lib/generators/design-system-generator.js +508 -508
- package/src/lib/generators/settings-generator.js +209 -209
- package/src/lib/git/branch-classifier.js +177 -0
- package/src/lib/git/gh.js +30 -0
- package/src/lib/git/pending-prs.js +89 -0
- package/src/lib/git/pr-index.js +70 -0
- package/src/lib/graph/god-nodes.js +75 -0
- package/src/lib/graph/graph-health.js +61 -0
- package/src/lib/graph/graph-loader.js +86 -0
- package/src/lib/graph/graphify-runtime.js +100 -0
- package/src/lib/graph/graphifyignore.js +86 -0
- package/src/lib/graph/sanitize.js +280 -0
- package/src/lib/hooks/hook-executor.js +167 -167
- package/src/lib/installers/mcp-installer.js +719 -719
- package/src/lib/tasks/task-classifier.js +30 -30
- package/src/lib/tasks/task-parser.js +209 -209
- package/src/lib/tasks/tasks-json-parser.js +26 -0
- package/src/lib/validators/checkpoint-persistence.js +39 -39
- package/src/lib/validators/css/css-validator.js +340 -340
- package/src/lib/validators/reuse/index.js +10 -0
- package/src/lib/validators/reuse/reuse-validator.js +324 -0
- package/src/lib/validators/reuse/symbol-scanner.js +144 -0
- package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
- package/src/lib/validators/shared/issue-counter.js +14 -14
- package/src/lib/validators/validation-runner.js +40 -1
- package/src/lib/worktree/fleet-scanner.js +51 -21
- package/src/lib/worktree/ignored-triage.js +197 -0
- package/src/lib/worktree/worktree-audit.js +200 -35
- package/src/scripts/setup-infra.js +60 -1
- package/src/utils/agent-id-mapper.js +11 -11
- package/src/utils/claude-settings-manager.js +300 -300
- package/src/utils/file-copier.js +40 -0
- package/src/utils/skills-installer.js +187 -187
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
* the next loadState, but surfaced so it's not a mystery)
|
|
13
13
|
* • branch-drift — the root's copy of a feature.json disagrees with the copy
|
|
14
14
|
* committed on `morph/{f}` (B5 — the root's view lags)
|
|
15
|
+
* • abandoned-scaffold — an `in_progress` feature that never produced
|
|
16
|
+
* anything: no artifacts, no approved gate, and a branch
|
|
17
|
+
* with no commit of its own (see abandonedScaffold)
|
|
15
18
|
* • corrupt — a feature.json that won't parse
|
|
16
19
|
*/
|
|
17
20
|
|
|
@@ -20,6 +23,8 @@ import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
|
|
|
20
23
|
import { join } from 'path';
|
|
21
24
|
import { gateOrderViolations } from './gate-order.js';
|
|
22
25
|
import { missingIdentity } from './feature-identity.js';
|
|
26
|
+
import { detectDefaultBranch } from '../../lib/git/default-branch.js';
|
|
27
|
+
import { commitsAhead, resolveBaseRef } from '../../lib/git/branch-classifier.js';
|
|
23
28
|
|
|
24
29
|
/** The operational truth of a feature.json: its status and which gates are approved. */
|
|
25
30
|
function operationalShape(fj) {
|
|
@@ -72,6 +77,125 @@ function branchDrift(baseDir, feature, rootFj) {
|
|
|
72
77
|
}
|
|
73
78
|
}
|
|
74
79
|
|
|
80
|
+
/**
|
|
81
|
+
* How long a scaffold has to sit untouched before it counts as abandoned.
|
|
82
|
+
*
|
|
83
|
+
* A dead scaffold and a feature born five minutes ago have the SAME shape:
|
|
84
|
+
* `create` forks `morph/{f}` off the default branch without committing, so at
|
|
85
|
+
* birth the branch has no commit of its own and the folder holds nothing but
|
|
86
|
+
* `feature.json`. Only elapsed time separates the two — and a check that fires
|
|
87
|
+
* on every fresh `create` is one people learn to scroll past.
|
|
88
|
+
*/
|
|
89
|
+
const ABANDONED_SCAFFOLD_MIN_AGE_MS = 24 * 60 * 60 * 1000;
|
|
90
|
+
|
|
91
|
+
/** Newest timestamp the feature carries, in ms; falls back to the folder's mtime. */
|
|
92
|
+
function lastTouched(fdir, fj) {
|
|
93
|
+
let newest = 0;
|
|
94
|
+
for (const ts of [fj?.updatedAt, fj?.createdAt]) {
|
|
95
|
+
const t = ts ? Date.parse(ts) : NaN;
|
|
96
|
+
if (!Number.isNaN(t)) newest = Math.max(newest, t);
|
|
97
|
+
}
|
|
98
|
+
if (newest) return newest;
|
|
99
|
+
try { return statSync(fdir).mtimeMs; } catch { return 0; }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Does the feature dir hold any FILE other than its own `feature.json`?
|
|
104
|
+
*
|
|
105
|
+
* Files, not entries: `create` scaffolds the phase folders empty, so an empty
|
|
106
|
+
* `0-proposal/` is the absence of work, not evidence of it.
|
|
107
|
+
*/
|
|
108
|
+
function hasArtifacts(fdir) {
|
|
109
|
+
let entries;
|
|
110
|
+
try { entries = readdirSync(fdir, { withFileTypes: true }); } catch { return false; }
|
|
111
|
+
for (const e of entries) {
|
|
112
|
+
if (e.isDirectory()) {
|
|
113
|
+
if (hasArtifacts(join(fdir, e.name))) return true;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (e.name === 'feature.json') continue;
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Commits on the feature's branch that the base does not already contain, or
|
|
124
|
+
* `null` when git cannot answer (no repo, no resolvable base, no branch, no
|
|
125
|
+
* rev-list). "Cannot prove" must never read as "proved zero" — the predicate
|
|
126
|
+
* below hinges on this number, so an unanswerable repo has to stay silent.
|
|
127
|
+
*
|
|
128
|
+
* The counting itself is `branch-classifier.js`'s `commitsAhead` against its
|
|
129
|
+
* `resolveBaseRef`, NOT a second copy: `origin/{default}` is the authority on
|
|
130
|
+
* what actually landed, and two drifting answers to "does this branch carry
|
|
131
|
+
* anything of its own?" is exactly what the `doctor`/`branch prune` split was
|
|
132
|
+
* unified to avoid.
|
|
133
|
+
*
|
|
134
|
+
* Note the field evidence this exists for: the branch pointed at the merge
|
|
135
|
+
* commit of somebody else's PR. That is the base's own tip — reachable from
|
|
136
|
+
* the base, and therefore zero commits of its own.
|
|
137
|
+
*/
|
|
138
|
+
function ownCommitCount(baseDir, branch) {
|
|
139
|
+
try {
|
|
140
|
+
execFileSync('git', ['rev-parse', '--git-dir'], { cwd: baseDir, stdio: 'pipe' });
|
|
141
|
+
} catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
const baseRef = resolveBaseRef(detectDefaultBranch(baseDir), baseDir);
|
|
145
|
+
if (!baseRef) return null;
|
|
146
|
+
try {
|
|
147
|
+
execFileSync('git', ['show-ref', '--verify', '--quiet', `refs/heads/${branch}`], { cwd: baseDir, stdio: 'pipe' });
|
|
148
|
+
} catch {
|
|
149
|
+
return 0; // the branch never existed — nothing was ever committed for it
|
|
150
|
+
}
|
|
151
|
+
return commitsAhead(baseRef, branch, baseDir);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The orphan skeleton `create --worktree` can leave behind: a feature.json on
|
|
156
|
+
* its own, `status: in_progress`, no tasks, no gates, and a branch parked on
|
|
157
|
+
* somebody else's commit. Every one of those alone is normal at some point in
|
|
158
|
+
* a feature's life; together, and only after the feature has gone stale, they
|
|
159
|
+
* mean nobody ever came back to it.
|
|
160
|
+
*
|
|
161
|
+
* The artifact test is deliberately broader than the "0 tasks" the report
|
|
162
|
+
* named: a proposal drafted and waiting on Gate 1 also has zero tasks and is
|
|
163
|
+
* very much alive. What a dead scaffold has is NOTHING — checked in the root
|
|
164
|
+
* and, if the feature registered one, in its worktree, where uncommitted work
|
|
165
|
+
* would otherwise be invisible to both the disk scan and `rev-list`.
|
|
166
|
+
*
|
|
167
|
+
* @returns {string|null} the issue detail, or null when the feature is alive
|
|
168
|
+
*/
|
|
169
|
+
function abandonedScaffold(baseDir, feature, fdir, fj) {
|
|
170
|
+
if (fj?.status !== 'in_progress') return null;
|
|
171
|
+
|
|
172
|
+
// "0 gates": a gate stamped `policy:{workType}` at birth (chore/hotfix) was
|
|
173
|
+
// never a decision anyone took, so it must not read as progress.
|
|
174
|
+
const decided = Object.values(fj.approvalGates || {}).some(
|
|
175
|
+
(g) => g?.approved === true && !String(g?.approvedBy ?? '').startsWith('policy:'),
|
|
176
|
+
);
|
|
177
|
+
if (decided) return null;
|
|
178
|
+
|
|
179
|
+
if (hasArtifacts(fdir)) return null;
|
|
180
|
+
const wtFeatureDir = fj.worktree?.path
|
|
181
|
+
? join(fj.worktree.path, '.morph', 'features', feature)
|
|
182
|
+
: null;
|
|
183
|
+
if (wtFeatureDir && existsSync(wtFeatureDir) && hasArtifacts(wtFeatureDir)) return null;
|
|
184
|
+
|
|
185
|
+
const age = Date.now() - lastTouched(fdir, fj);
|
|
186
|
+
if (age < ABANDONED_SCAFFOLD_MIN_AGE_MS) return null;
|
|
187
|
+
|
|
188
|
+
// The registered branch when the feature has one, `morph/{f}` otherwise —
|
|
189
|
+
// same name `branchDrift` above assumes.
|
|
190
|
+
const branch = fj.worktree?.branch || `morph/${feature}`;
|
|
191
|
+
if (ownCommitCount(baseDir, branch) !== 0) return null;
|
|
192
|
+
|
|
193
|
+
const days = Math.floor(age / (24 * 60 * 60 * 1000));
|
|
194
|
+
return `in_progress há ${days}d sem nada produzido: 0 artefatos, 0 gates aprovados `
|
|
195
|
+
+ `e \`${branch}\` sem commit próprio — rode \`morph-spec delete ${feature} --archive\` `
|
|
196
|
+
+ 'ou retome a feature';
|
|
197
|
+
}
|
|
198
|
+
|
|
75
199
|
/**
|
|
76
200
|
* @param {string} [baseDir] - project root (defaults to cwd)
|
|
77
201
|
* @returns {Array<{ feature: string, type: string, detail: string }>} issues
|
|
@@ -106,6 +230,9 @@ export function auditFeatureLifecycle(baseDir = process.cwd()) {
|
|
|
106
230
|
const drift = branchDrift(baseDir, name, fj);
|
|
107
231
|
if (drift) issues.push({ feature: name, type: 'branch-drift', detail: drift });
|
|
108
232
|
|
|
233
|
+
const dead = abandonedScaffold(baseDir, name, fdir, fj);
|
|
234
|
+
if (dead) issues.push({ feature: name, type: 'abandoned-scaffold', detail: dead });
|
|
235
|
+
|
|
109
236
|
const miss = missingIdentity(fj);
|
|
110
237
|
if (miss.length) {
|
|
111
238
|
issues.push({ feature: name, type: 'identity', detail: `missing ${miss.join(', ')}` });
|
|
@@ -1,197 +1,197 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Phase State Machine - Enforces valid phase transitions
|
|
3
|
-
*
|
|
4
|
-
* Prevents invalid workflow jumps and ensures sequential phase progression.
|
|
5
|
-
* Based on the MORPH 5-phase workflow.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Valid phase transitions map
|
|
10
|
-
* Each phase can only transition to specific next phases
|
|
11
|
-
*/
|
|
12
|
-
const VALID_TRANSITIONS = {
|
|
13
|
-
'proposal': ['uiux', 'plan'],
|
|
14
|
-
'uiux': ['plan'],
|
|
15
|
-
'plan': ['implement'],
|
|
16
|
-
'implement': ['review'],
|
|
17
|
-
'review': []
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Phase display names for error messages
|
|
22
|
-
*/
|
|
23
|
-
const PHASE_NAMES = {
|
|
24
|
-
'proposal': 'Proposal (Phase 0)',
|
|
25
|
-
'uiux': 'UI/UX Design (Phase 1)',
|
|
26
|
-
'plan': 'Plan (Phase 2)',
|
|
27
|
-
'implement': 'Implement (Phase 3)',
|
|
28
|
-
'review': 'Quality Review (Phase 4)'
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Optional phases that can be skipped
|
|
33
|
-
*/
|
|
34
|
-
const OPTIONAL_PHASES = ['uiux'];
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Validate if a phase transition is allowed
|
|
38
|
-
* @param {string} fromPhase - Current phase
|
|
39
|
-
* @param {string} toPhase - Target phase
|
|
40
|
-
* @returns {boolean} True if transition is valid
|
|
41
|
-
*/
|
|
42
|
-
export function isValidTransition(fromPhase, toPhase) {
|
|
43
|
-
if (!fromPhase || !toPhase) {
|
|
44
|
-
return false;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const validNextPhases = VALID_TRANSITIONS[fromPhase];
|
|
48
|
-
return validNextPhases ? validNextPhases.includes(toPhase) : false;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Get list of valid next phases for a given phase
|
|
53
|
-
* @param {string} currentPhase - Current phase
|
|
54
|
-
* @returns {string[]} Array of valid next phase names
|
|
55
|
-
*/
|
|
56
|
-
export function getValidNextPhases(currentPhase) {
|
|
57
|
-
return VALID_TRANSITIONS[currentPhase] || [];
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Get display name for a phase
|
|
62
|
-
* @param {string} phase - Phase identifier
|
|
63
|
-
* @returns {string} Human-readable phase name
|
|
64
|
-
*/
|
|
65
|
-
export function getPhaseDisplayName(phase) {
|
|
66
|
-
return PHASE_NAMES[phase] || phase;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Check if a phase is optional
|
|
71
|
-
* @param {string} phase - Phase identifier
|
|
72
|
-
* @returns {boolean} True if phase is optional
|
|
73
|
-
*/
|
|
74
|
-
export function isOptionalPhase(phase) {
|
|
75
|
-
return OPTIONAL_PHASES.includes(phase);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Validate transition and throw error if invalid
|
|
80
|
-
* @param {string} fromPhase - Current phase
|
|
81
|
-
* @param {string} toPhase - Target phase
|
|
82
|
-
* @throws {Error} If transition is invalid
|
|
83
|
-
*/
|
|
84
|
-
export function validateTransition(fromPhase, toPhase) {
|
|
85
|
-
if (!fromPhase) {
|
|
86
|
-
throw new Error('Current phase is undefined. Cannot validate transition.');
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (!toPhase) {
|
|
90
|
-
throw new Error('Target phase is undefined. Cannot validate transition.');
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (!isValidTransition(fromPhase, toPhase)) {
|
|
94
|
-
const validPhases = getValidNextPhases(fromPhase);
|
|
95
|
-
const fromDisplay = getPhaseDisplayName(fromPhase);
|
|
96
|
-
const toDisplay = getPhaseDisplayName(toPhase);
|
|
97
|
-
|
|
98
|
-
throw new Error(
|
|
99
|
-
`Invalid phase transition: ${fromDisplay} → ${toDisplay}\n\n` +
|
|
100
|
-
`Valid next phases from ${fromDisplay}:\n` +
|
|
101
|
-
validPhases.map(p => ` • ${getPhaseDisplayName(p)}`).join('\n') +
|
|
102
|
-
'\n\n' +
|
|
103
|
-
'You cannot skip phases in the MORPH workflow.\n' +
|
|
104
|
-
'To override this check, use the --force flag (not recommended).'
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Get the sequential order of phases
|
|
111
|
-
* @returns {string[]} Ordered list of phases
|
|
112
|
-
*/
|
|
113
|
-
export function getPhaseSequence() {
|
|
114
|
-
return ['proposal', 'uiux', 'plan', 'implement', 'review'];
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Get phase order index (for comparison)
|
|
119
|
-
* @param {string} phase - Phase identifier
|
|
120
|
-
* @returns {number} Index in sequence (-1 if not found)
|
|
121
|
-
*/
|
|
122
|
-
export function getPhaseOrder(phase) {
|
|
123
|
-
return getPhaseSequence().indexOf(phase);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Check if target phase is ahead of current phase
|
|
128
|
-
* @param {string} currentPhase - Current phase
|
|
129
|
-
* @param {string} targetPhase - Target phase
|
|
130
|
-
* @returns {boolean} True if target is ahead
|
|
131
|
-
*/
|
|
132
|
-
export function isPhaseAhead(currentPhase, targetPhase) {
|
|
133
|
-
return getPhaseOrder(targetPhase) > getPhaseOrder(currentPhase);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Check if target phase is behind current phase (rollback scenario)
|
|
138
|
-
* @param {string} currentPhase - Current phase
|
|
139
|
-
* @param {string} targetPhase - Target phase
|
|
140
|
-
* @returns {boolean} True if target is behind
|
|
141
|
-
*/
|
|
142
|
-
export function isPhaseRollback(currentPhase, targetPhase) {
|
|
143
|
-
return getPhaseOrder(targetPhase) < getPhaseOrder(currentPhase);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Get all skipped phases between two phases
|
|
148
|
-
* @param {string} fromPhase - Starting phase
|
|
149
|
-
* @param {string} toPhase - Ending phase
|
|
150
|
-
* @returns {string[]} Array of skipped phases
|
|
151
|
-
*/
|
|
152
|
-
export function getSkippedPhases(fromPhase, toPhase) {
|
|
153
|
-
const sequence = getPhaseSequence();
|
|
154
|
-
const fromIndex = sequence.indexOf(fromPhase);
|
|
155
|
-
const toIndex = sequence.indexOf(toPhase);
|
|
156
|
-
|
|
157
|
-
if (fromIndex === -1 || toIndex === -1 || toIndex <= fromIndex) {
|
|
158
|
-
return [];
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return sequence.slice(fromIndex + 1, toIndex);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Validate that no required phases were skipped
|
|
166
|
-
* @param {string} fromPhase - Starting phase
|
|
167
|
-
* @param {string} toPhase - Ending phase
|
|
168
|
-
* @throws {Error} If required phases were skipped
|
|
169
|
-
*/
|
|
170
|
-
export function validateNoRequiredPhasesSkipped(fromPhase, toPhase) {
|
|
171
|
-
const skipped = getSkippedPhases(fromPhase, toPhase);
|
|
172
|
-
const requiredSkipped = skipped.filter(p => !isOptionalPhase(p));
|
|
173
|
-
|
|
174
|
-
if (requiredSkipped.length > 0) {
|
|
175
|
-
throw new Error(
|
|
176
|
-
`Cannot skip required phases:\n` +
|
|
177
|
-
requiredSkipped.map(p => ` • ${getPhaseDisplayName(p)}`).join('\n') +
|
|
178
|
-
'\n\nYou must complete each required phase in sequence.'
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Get comprehensive phase information
|
|
185
|
-
* @param {string} phase - Phase identifier
|
|
186
|
-
* @returns {Object} Phase metadata
|
|
187
|
-
*/
|
|
188
|
-
export function getPhaseInfo(phase) {
|
|
189
|
-
return {
|
|
190
|
-
id: phase,
|
|
191
|
-
displayName: getPhaseDisplayName(phase),
|
|
192
|
-
order: getPhaseOrder(phase),
|
|
193
|
-
isOptional: isOptionalPhase(phase),
|
|
194
|
-
validNextPhases: getValidNextPhases(phase),
|
|
195
|
-
canTransitionTo: (targetPhase) => isValidTransition(phase, targetPhase)
|
|
196
|
-
};
|
|
197
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Phase State Machine - Enforces valid phase transitions
|
|
3
|
+
*
|
|
4
|
+
* Prevents invalid workflow jumps and ensures sequential phase progression.
|
|
5
|
+
* Based on the MORPH 5-phase workflow.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Valid phase transitions map
|
|
10
|
+
* Each phase can only transition to specific next phases
|
|
11
|
+
*/
|
|
12
|
+
const VALID_TRANSITIONS = {
|
|
13
|
+
'proposal': ['uiux', 'plan'],
|
|
14
|
+
'uiux': ['plan'],
|
|
15
|
+
'plan': ['implement'],
|
|
16
|
+
'implement': ['review'],
|
|
17
|
+
'review': []
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Phase display names for error messages
|
|
22
|
+
*/
|
|
23
|
+
const PHASE_NAMES = {
|
|
24
|
+
'proposal': 'Proposal (Phase 0)',
|
|
25
|
+
'uiux': 'UI/UX Design (Phase 1)',
|
|
26
|
+
'plan': 'Plan (Phase 2)',
|
|
27
|
+
'implement': 'Implement (Phase 3)',
|
|
28
|
+
'review': 'Quality Review (Phase 4)'
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Optional phases that can be skipped
|
|
33
|
+
*/
|
|
34
|
+
const OPTIONAL_PHASES = ['uiux'];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Validate if a phase transition is allowed
|
|
38
|
+
* @param {string} fromPhase - Current phase
|
|
39
|
+
* @param {string} toPhase - Target phase
|
|
40
|
+
* @returns {boolean} True if transition is valid
|
|
41
|
+
*/
|
|
42
|
+
export function isValidTransition(fromPhase, toPhase) {
|
|
43
|
+
if (!fromPhase || !toPhase) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const validNextPhases = VALID_TRANSITIONS[fromPhase];
|
|
48
|
+
return validNextPhases ? validNextPhases.includes(toPhase) : false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get list of valid next phases for a given phase
|
|
53
|
+
* @param {string} currentPhase - Current phase
|
|
54
|
+
* @returns {string[]} Array of valid next phase names
|
|
55
|
+
*/
|
|
56
|
+
export function getValidNextPhases(currentPhase) {
|
|
57
|
+
return VALID_TRANSITIONS[currentPhase] || [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get display name for a phase
|
|
62
|
+
* @param {string} phase - Phase identifier
|
|
63
|
+
* @returns {string} Human-readable phase name
|
|
64
|
+
*/
|
|
65
|
+
export function getPhaseDisplayName(phase) {
|
|
66
|
+
return PHASE_NAMES[phase] || phase;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Check if a phase is optional
|
|
71
|
+
* @param {string} phase - Phase identifier
|
|
72
|
+
* @returns {boolean} True if phase is optional
|
|
73
|
+
*/
|
|
74
|
+
export function isOptionalPhase(phase) {
|
|
75
|
+
return OPTIONAL_PHASES.includes(phase);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Validate transition and throw error if invalid
|
|
80
|
+
* @param {string} fromPhase - Current phase
|
|
81
|
+
* @param {string} toPhase - Target phase
|
|
82
|
+
* @throws {Error} If transition is invalid
|
|
83
|
+
*/
|
|
84
|
+
export function validateTransition(fromPhase, toPhase) {
|
|
85
|
+
if (!fromPhase) {
|
|
86
|
+
throw new Error('Current phase is undefined. Cannot validate transition.');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!toPhase) {
|
|
90
|
+
throw new Error('Target phase is undefined. Cannot validate transition.');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!isValidTransition(fromPhase, toPhase)) {
|
|
94
|
+
const validPhases = getValidNextPhases(fromPhase);
|
|
95
|
+
const fromDisplay = getPhaseDisplayName(fromPhase);
|
|
96
|
+
const toDisplay = getPhaseDisplayName(toPhase);
|
|
97
|
+
|
|
98
|
+
throw new Error(
|
|
99
|
+
`Invalid phase transition: ${fromDisplay} → ${toDisplay}\n\n` +
|
|
100
|
+
`Valid next phases from ${fromDisplay}:\n` +
|
|
101
|
+
validPhases.map(p => ` • ${getPhaseDisplayName(p)}`).join('\n') +
|
|
102
|
+
'\n\n' +
|
|
103
|
+
'You cannot skip phases in the MORPH workflow.\n' +
|
|
104
|
+
'To override this check, use the --force flag (not recommended).'
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get the sequential order of phases
|
|
111
|
+
* @returns {string[]} Ordered list of phases
|
|
112
|
+
*/
|
|
113
|
+
export function getPhaseSequence() {
|
|
114
|
+
return ['proposal', 'uiux', 'plan', 'implement', 'review'];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get phase order index (for comparison)
|
|
119
|
+
* @param {string} phase - Phase identifier
|
|
120
|
+
* @returns {number} Index in sequence (-1 if not found)
|
|
121
|
+
*/
|
|
122
|
+
export function getPhaseOrder(phase) {
|
|
123
|
+
return getPhaseSequence().indexOf(phase);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if target phase is ahead of current phase
|
|
128
|
+
* @param {string} currentPhase - Current phase
|
|
129
|
+
* @param {string} targetPhase - Target phase
|
|
130
|
+
* @returns {boolean} True if target is ahead
|
|
131
|
+
*/
|
|
132
|
+
export function isPhaseAhead(currentPhase, targetPhase) {
|
|
133
|
+
return getPhaseOrder(targetPhase) > getPhaseOrder(currentPhase);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Check if target phase is behind current phase (rollback scenario)
|
|
138
|
+
* @param {string} currentPhase - Current phase
|
|
139
|
+
* @param {string} targetPhase - Target phase
|
|
140
|
+
* @returns {boolean} True if target is behind
|
|
141
|
+
*/
|
|
142
|
+
export function isPhaseRollback(currentPhase, targetPhase) {
|
|
143
|
+
return getPhaseOrder(targetPhase) < getPhaseOrder(currentPhase);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get all skipped phases between two phases
|
|
148
|
+
* @param {string} fromPhase - Starting phase
|
|
149
|
+
* @param {string} toPhase - Ending phase
|
|
150
|
+
* @returns {string[]} Array of skipped phases
|
|
151
|
+
*/
|
|
152
|
+
export function getSkippedPhases(fromPhase, toPhase) {
|
|
153
|
+
const sequence = getPhaseSequence();
|
|
154
|
+
const fromIndex = sequence.indexOf(fromPhase);
|
|
155
|
+
const toIndex = sequence.indexOf(toPhase);
|
|
156
|
+
|
|
157
|
+
if (fromIndex === -1 || toIndex === -1 || toIndex <= fromIndex) {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return sequence.slice(fromIndex + 1, toIndex);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Validate that no required phases were skipped
|
|
166
|
+
* @param {string} fromPhase - Starting phase
|
|
167
|
+
* @param {string} toPhase - Ending phase
|
|
168
|
+
* @throws {Error} If required phases were skipped
|
|
169
|
+
*/
|
|
170
|
+
export function validateNoRequiredPhasesSkipped(fromPhase, toPhase) {
|
|
171
|
+
const skipped = getSkippedPhases(fromPhase, toPhase);
|
|
172
|
+
const requiredSkipped = skipped.filter(p => !isOptionalPhase(p));
|
|
173
|
+
|
|
174
|
+
if (requiredSkipped.length > 0) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`Cannot skip required phases:\n` +
|
|
177
|
+
requiredSkipped.map(p => ` • ${getPhaseDisplayName(p)}`).join('\n') +
|
|
178
|
+
'\n\nYou must complete each required phase in sequence.'
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get comprehensive phase information
|
|
185
|
+
* @param {string} phase - Phase identifier
|
|
186
|
+
* @returns {Object} Phase metadata
|
|
187
|
+
*/
|
|
188
|
+
export function getPhaseInfo(phase) {
|
|
189
|
+
return {
|
|
190
|
+
id: phase,
|
|
191
|
+
displayName: getPhaseDisplayName(phase),
|
|
192
|
+
order: getPhaseOrder(phase),
|
|
193
|
+
isOptional: isOptionalPhase(phase),
|
|
194
|
+
validNextPhases: getValidNextPhases(phase),
|
|
195
|
+
canTransitionTo: (targetPhase) => isValidTransition(phase, targetPhase)
|
|
196
|
+
};
|
|
197
|
+
}
|