@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,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MORPH-SPEC State Manager Library
|
|
3
|
-
*
|
|
4
|
-
* Manages state.json for tracking features, progress, agents, and checkpoints.
|
|
5
|
-
* Used both by CLI commands and internal automation.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
1
|
import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync, rmdirSync, statSync, readdirSync } from 'fs';
|
|
9
2
|
import { join, dirname } from 'path';
|
|
10
3
|
import { getAllOutputPaths, getOutputPath } from '../paths/output-schema.js';
|
|
@@ -13,45 +6,24 @@ import { derivePhaseFrom, diskArtifactProbe } from '../../../framework/hooks/sha
|
|
|
13
6
|
const STATE_FILE_NAME = '.morph/state.json';
|
|
14
7
|
const FEATURES_DIR_NAME = '.morph/features';
|
|
15
8
|
const FEATURE_FILE_NAME = 'feature.json';
|
|
16
|
-
/**
|
|
17
|
-
* The one spelling of the committed project config's location, relative to a
|
|
18
|
-
* project root. Exported because three modules used to write it out by hand
|
|
19
|
-
* (`readProjectFromConfig` here, `readMorphConfig` and `morphConfigPath` in
|
|
20
|
-
* src/lib/tasks/) — and `morphConfigPath`'s own JSDoc promised the reader that
|
|
21
|
-
* the writer and the reader "can never disagree about which file is meant",
|
|
22
|
-
* a guarantee three copies of a string cannot give. Now they can't.
|
|
23
|
-
*/
|
|
24
9
|
export const CONFIG_FILE_NAME = '.morph/config/config.json';
|
|
25
10
|
|
|
26
|
-
//
|
|
27
|
-
// is
|
|
28
|
-
// `.morph/features/{name}/feature.json` (authoritative, committed) with the root
|
|
29
|
-
// `.morph/state.json` acting as a thin, derivable, gitignored index. Migration to
|
|
30
|
-
// the split layout is detected by `feature.json` presence, not by this number.
|
|
11
|
+
// Versions the feature-object schema, not the storage layout: the per-feature
|
|
12
|
+
// split is detected by the presence of `feature.json`.
|
|
31
13
|
const STATE_VERSION = '5.0.0';
|
|
32
14
|
|
|
33
|
-
// ============================================================================
|
|
34
|
-
// Core Functions
|
|
35
|
-
// ============================================================================
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Get the state file path (looks in current working directory)
|
|
39
|
-
*/
|
|
40
15
|
export function getStatePath() {
|
|
41
16
|
return join(process.cwd(), STATE_FILE_NAME);
|
|
42
17
|
}
|
|
43
18
|
|
|
44
|
-
/** Absolute path to the per-feature folder root (.morph/features). */
|
|
45
19
|
function getFeaturesDir() {
|
|
46
20
|
return join(process.cwd(), FEATURES_DIR_NAME);
|
|
47
21
|
}
|
|
48
22
|
|
|
49
|
-
/** Absolute path to a feature's authoritative feature.json. */
|
|
50
23
|
function getFeatureJsonPath(featureName) {
|
|
51
24
|
return join(getFeaturesDir(), featureName, FEATURE_FILE_NAME);
|
|
52
25
|
}
|
|
53
26
|
|
|
54
|
-
/** List feature names that have a folder on disk under .morph/features. */
|
|
55
27
|
function listFeatureNamesOnDisk() {
|
|
56
28
|
const dir = getFeaturesDir();
|
|
57
29
|
if (!existsSync(dir)) return [];
|
|
@@ -64,7 +36,6 @@ function listFeatureNamesOnDisk() {
|
|
|
64
36
|
}
|
|
65
37
|
}
|
|
66
38
|
|
|
67
|
-
/** Read a feature's feature.json, or null if missing/corrupt. */
|
|
68
39
|
function readFeatureJson(featureName) {
|
|
69
40
|
const path = getFeatureJsonPath(featureName);
|
|
70
41
|
if (!existsSync(path)) return null;
|
|
@@ -75,7 +46,6 @@ function readFeatureJson(featureName) {
|
|
|
75
46
|
}
|
|
76
47
|
}
|
|
77
48
|
|
|
78
|
-
/** Read project identity from the committed .morph/config/config.json, if present. */
|
|
79
49
|
function readProjectFromConfig() {
|
|
80
50
|
const path = join(process.cwd(), CONFIG_FILE_NAME);
|
|
81
51
|
if (!existsSync(path)) return null;
|
|
@@ -92,10 +62,7 @@ function readProjectFromConfig() {
|
|
|
92
62
|
}
|
|
93
63
|
}
|
|
94
64
|
|
|
95
|
-
|
|
96
|
-
* Atomic JSON write: per-process temp file then rename, with an EPERM retry loop
|
|
97
|
-
* for the brief Windows lock race. Creates the parent directory if needed.
|
|
98
|
-
*/
|
|
65
|
+
// Per-process temp file then rename; the EPERM retry covers the brief Windows lock race.
|
|
99
66
|
export function atomicWriteJson(targetPath, data) {
|
|
100
67
|
const dir = dirname(targetPath);
|
|
101
68
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
@@ -112,41 +79,25 @@ export function atomicWriteJson(targetPath, data) {
|
|
|
112
79
|
if (err.code === 'EPERM' && attempt < MAX_RETRIES) {
|
|
113
80
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50 * (attempt + 1));
|
|
114
81
|
} else {
|
|
115
|
-
try { unlinkSync(tmpPath); } catch {
|
|
82
|
+
try { unlinkSync(tmpPath); } catch { }
|
|
116
83
|
throw err;
|
|
117
84
|
}
|
|
118
85
|
}
|
|
119
86
|
}
|
|
120
87
|
}
|
|
121
88
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
*
|
|
125
|
-
* Skips the write entirely when the on-disk content is already byte-identical to
|
|
126
|
-
* what we'd write. `saveState` hydrates and re-persists EVERY feature, so a CLI
|
|
127
|
-
* mutation on feature A would otherwise rewrite sibling B's file from A's loaded
|
|
128
|
-
* copy. Skipping unchanged siblings means A never touches B's authoritative file
|
|
129
|
-
* in the common case — shrinking the cross-feature clobber window to nothing
|
|
130
|
-
* unless B genuinely changed under A.
|
|
131
|
-
*
|
|
132
|
-
* A formatting mismatch is NOT free, though: the guard compares bytes, so a
|
|
133
|
-
* sibling committed with a trailing newline fails it and gets rewritten. The
|
|
134
|
-
* content is equivalent, but git sees ` M` on a file this caller never meant to
|
|
135
|
-
* touch — enough to fail a whole-tree clean check (see the finish teardown's clearRootWorktreeRegistration).
|
|
136
|
-
* And the guard only skips IDENTICAL content: a concurrent instance's CHANGED
|
|
137
|
-
* write to a sibling would still be clobbered by the sweep.
|
|
138
|
-
*/
|
|
89
|
+
// saveState re-persists every feature; skipping byte-identical content keeps a
|
|
90
|
+
// mutation on one feature from rewriting (and clobbering) its siblings' files.
|
|
139
91
|
function writeFeatureJson(featureName, feature) {
|
|
140
92
|
const path = getFeatureJsonPath(featureName);
|
|
141
93
|
try {
|
|
142
94
|
if (existsSync(path) && readFileSync(path, 'utf8') === JSON.stringify(feature, null, 2)) {
|
|
143
|
-
return;
|
|
95
|
+
return;
|
|
144
96
|
}
|
|
145
|
-
} catch {
|
|
97
|
+
} catch { }
|
|
146
98
|
atomicWriteJson(path, feature);
|
|
147
99
|
}
|
|
148
100
|
|
|
149
|
-
/** A fresh, empty state scaffold (used when no root index exists yet). */
|
|
150
101
|
function scaffoldState() {
|
|
151
102
|
return {
|
|
152
103
|
version: STATE_VERSION,
|
|
@@ -156,13 +107,7 @@ function scaffoldState() {
|
|
|
156
107
|
metadata: { totalFeatures: 0, completedFeatures: 0, totalTimeSpent: 0 },
|
|
157
108
|
};
|
|
158
109
|
}
|
|
159
|
-
|
|
160
|
-
* Derive current phase from filesystem — delegates to the hook-side rule
|
|
161
|
-
* (`derivePhaseFrom`) so the CLI and the hooks never disagree on a feature.
|
|
162
|
-
*
|
|
163
|
-
* @param {string} featurePath - Absolute path to .morph/features/{feature}/
|
|
164
|
-
* @returns {string} Phase name: 'review' | 'implement' | 'uiux' | 'plan'
|
|
165
|
-
*/
|
|
110
|
+
// Delegates to the hook-side rule so the CLI and the hooks never disagree on a phase.
|
|
166
111
|
export function derivePhase(featurePath) {
|
|
167
112
|
let feature = null;
|
|
168
113
|
try {
|
|
@@ -172,14 +117,6 @@ export function derivePhase(featurePath) {
|
|
|
172
117
|
return derivePhaseFrom(feature, diskArtifactProbe(featurePath));
|
|
173
118
|
}
|
|
174
119
|
|
|
175
|
-
/**
|
|
176
|
-
* Derive output existence from filesystem — checks if each output file exists at its expected path.
|
|
177
|
-
* Returns an object matching the old outputs shape for backwards-compatible display.
|
|
178
|
-
*
|
|
179
|
-
* @param {string} featureName - Feature name
|
|
180
|
-
* @param {string} [baseDir] - Project base dir (defaults to cwd)
|
|
181
|
-
* @returns {Object} Map of outputType -> { created: boolean, path: string }
|
|
182
|
-
*/
|
|
183
120
|
export function deriveOutputs(featureName, baseDir = process.cwd()) {
|
|
184
121
|
const outputPaths = getAllOutputPaths(featureName);
|
|
185
122
|
const result = {};
|
|
@@ -190,25 +127,16 @@ export function deriveOutputs(featureName, baseDir = process.cwd()) {
|
|
|
190
127
|
return result;
|
|
191
128
|
}
|
|
192
129
|
|
|
193
|
-
/**
|
|
194
|
-
* Check if state file exists
|
|
195
|
-
*/
|
|
196
130
|
export function stateExists() {
|
|
197
131
|
return existsSync(getStatePath());
|
|
198
132
|
}
|
|
199
133
|
|
|
200
|
-
/**
|
|
201
|
-
* Load state from disk
|
|
202
|
-
* @param {boolean} throwOnError - If false, returns null instead of throwing
|
|
203
|
-
* @returns {Object|null} State object or null
|
|
204
|
-
*/
|
|
205
134
|
export function loadState(throwOnError = true) {
|
|
206
135
|
const statePath = getStatePath();
|
|
207
136
|
const rootExists = existsSync(statePath);
|
|
208
137
|
const featureNames = listFeatureNamesOnDisk();
|
|
209
138
|
const featuresExist = featureNames.some((name) => existsSync(getFeatureJsonPath(name)));
|
|
210
139
|
|
|
211
|
-
// "No state" only when neither the root index nor any per-feature file exists.
|
|
212
140
|
if (!rootExists && !featuresExist) {
|
|
213
141
|
if (throwOnError) {
|
|
214
142
|
throw new Error(`State file not found: ${statePath}\nRun 'morph-spec init' to initialize MORPH in this project.`);
|
|
@@ -233,19 +161,12 @@ export function loadState(throwOnError = true) {
|
|
|
233
161
|
if (fromConfig) state.project = { ...(state.project || {}), ...fromConfig };
|
|
234
162
|
}
|
|
235
163
|
|
|
236
|
-
// Snapshot the
|
|
237
|
-
//
|
|
238
|
-
// predates the split — materialize) from a current-version index entry whose
|
|
239
|
-
// folder is gone (a deleted feature — prune). A missing root index is treated
|
|
240
|
-
// as "no migration" (null): the loop is empty in that case (features hydrate
|
|
241
|
-
// from disk), so the value only matters when the root index exists.
|
|
164
|
+
// Snapshot before the migrations below bump it: the reconcile loop tells a
|
|
165
|
+
// legacy monolith (materialize) from a deleted feature (prune) by this value.
|
|
242
166
|
const fileVersion = rootExists ? state.version : null;
|
|
243
167
|
|
|
244
|
-
// A missing root index means we must (re)generate it from the per-feature
|
|
245
|
-
// files we're about to hydrate — this is the self-heal path after a wipe.
|
|
246
168
|
let mutated = !rootExists;
|
|
247
169
|
|
|
248
|
-
// Migrate v3.0.0 → v4.0.0 (new directory layout) — operates on root features.
|
|
249
170
|
if (state.version === '3.0.0') {
|
|
250
171
|
state.version = '4.0.0';
|
|
251
172
|
for (const [name, feature] of Object.entries(state.features)) {
|
|
@@ -258,8 +179,6 @@ export function loadState(throwOnError = true) {
|
|
|
258
179
|
mutated = true;
|
|
259
180
|
}
|
|
260
181
|
|
|
261
|
-
// Migrate v4.x → v5.0.0: drop derived fields (now filesystem-derived) and
|
|
262
|
-
// workflow selection (v5 has fixed phases).
|
|
263
182
|
if (state.version && state.version.startsWith('4.')) {
|
|
264
183
|
state.version = '5.0.0';
|
|
265
184
|
for (const feature of Object.values(state.features)) {
|
|
@@ -271,57 +190,35 @@ export function loadState(throwOnError = true) {
|
|
|
271
190
|
mutated = true;
|
|
272
191
|
}
|
|
273
192
|
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
// it
|
|
277
|
-
// signal: deleting EVERY folder at once (the reported incident) would leave no
|
|
278
|
-
// survivor and resurrect them all. Version is unconditional:
|
|
279
|
-
//
|
|
280
|
-
// • A pre-v5 root (v3/v4) or an entry rich beyond the thin {status,updatedAt}
|
|
281
|
-
// shape = a legacy monolithic state.json that predates the split. It is the
|
|
282
|
-
// sole copy of its feature — materialize each to its authoritative
|
|
283
|
-
// feature.json (split-on-load migration; idempotent thereafter).
|
|
284
|
-
//
|
|
285
|
-
// • A current-version (v5) thin entry with NO folder = the feature was
|
|
286
|
-
// DELETED and this gitignored index entry is stale. Prune it; NEVER recreate
|
|
287
|
-
// the folder — that turned an intentional delete into a zombie. This is safe
|
|
288
|
-
// and exhaustive because saveState ALWAYS co-writes feature.json + a thin
|
|
289
|
-
// index entry: a v5 thin entry with no feature.json can ONLY mean the folder
|
|
290
|
-
// was removed. The prune sticks — saveState below derives the index from this
|
|
291
|
-
// same in-memory map, and hydration only re-adds features whose folder exists.
|
|
193
|
+
// Index entry without feature.json: a pre-v5 or rich entry is the sole copy of a
|
|
194
|
+
// legacy monolith (materialize it); a v5 thin entry means the folder was deleted
|
|
195
|
+
// (prune it, never resurrect). Safe because saveState always co-writes both.
|
|
292
196
|
const isRichEntry = (f) =>
|
|
293
197
|
f && typeof f === 'object' && Object.keys(f).some((k) => k !== 'status' && k !== 'updatedAt');
|
|
294
198
|
const wasVersionMigration = fileVersion !== null && fileVersion !== STATE_VERSION;
|
|
295
199
|
for (const [name, feature] of Object.entries(state.features)) {
|
|
296
200
|
if (existsSync(getFeatureJsonPath(name))) continue;
|
|
297
201
|
if (wasVersionMigration || isRichEntry(feature)) {
|
|
298
|
-
writeFeatureJson(name, feature);
|
|
202
|
+
writeFeatureJson(name, feature);
|
|
299
203
|
} else {
|
|
300
|
-
delete state.features[name];
|
|
204
|
+
delete state.features[name];
|
|
301
205
|
console.error(`ℹ morph-spec: dropped stale index entry "${name}" (feature folder was deleted).`);
|
|
302
206
|
}
|
|
303
207
|
mutated = true;
|
|
304
208
|
}
|
|
305
209
|
|
|
306
|
-
// Hydrate from per-feature files (authoritative). Also picks up features that
|
|
307
|
-
// exist on disk but are absent from a stale/deleted root index — so a wiped
|
|
308
|
-
// state.json self-heals from the committed feature.json files.
|
|
309
210
|
for (const name of featureNames) {
|
|
310
211
|
const fj = readFeatureJson(name);
|
|
311
212
|
if (fj) state.features[name] = fj;
|
|
312
213
|
}
|
|
313
214
|
|
|
314
215
|
if (mutated) {
|
|
315
|
-
saveState(state);
|
|
216
|
+
saveState(state);
|
|
316
217
|
}
|
|
317
218
|
|
|
318
219
|
return state;
|
|
319
220
|
}
|
|
320
221
|
|
|
321
|
-
/**
|
|
322
|
-
* Save state to disk
|
|
323
|
-
* @param {Object} state - State object to save
|
|
324
|
-
*/
|
|
325
222
|
export function saveState(state) {
|
|
326
223
|
state.metadata = state.metadata || {};
|
|
327
224
|
state.metadata.lastUpdated = new Date().toISOString();
|
|
@@ -329,32 +226,18 @@ export function saveState(state) {
|
|
|
329
226
|
|
|
330
227
|
const features = state.features || {};
|
|
331
228
|
|
|
332
|
-
// Aggregate metadata is derived from the authoritative feature set so the
|
|
333
|
-
// thin root index never drifts from the per-feature files.
|
|
334
229
|
state.metadata.totalFeatures = Object.keys(features).length;
|
|
335
230
|
state.metadata.completedFeatures = Object.values(features)
|
|
336
231
|
.filter((f) => f && f.status === 'completed').length;
|
|
337
232
|
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
// NOTE: this IS a cross-feature sweep. Mutating one feature re-persists
|
|
342
|
-
// every sibling; writeFeatureJson's byte-identical guard makes that a no-op
|
|
343
|
-
// only while a sibling's committed file matches our formatting exactly. A
|
|
344
|
-
// sibling saved with a trailing newline (any editor, prettier) is rewritten
|
|
345
|
-
// and shows up as ` M` — dirtying the tree on behalf of a feature the
|
|
346
|
-
// caller never touched. Callers that need to change ONE feature's field
|
|
347
|
-
// should write that feature.json directly instead of round-tripping state
|
|
348
|
-
// through here (see the finish teardown's clearRootWorktreeRegistration).
|
|
233
|
+
// A cross-feature sweep: a sibling saved in another format (trailing newline) is
|
|
234
|
+
// rewritten and dirties the tree. To change one feature, write its feature.json directly.
|
|
349
235
|
const thinFeatures = {};
|
|
350
236
|
for (const [name, feature] of Object.entries(features)) {
|
|
351
237
|
writeFeatureJson(name, feature);
|
|
352
238
|
thinFeatures[name] = { status: feature.status, updatedAt: feature.updatedAt };
|
|
353
239
|
}
|
|
354
240
|
|
|
355
|
-
// 2) Persist the thin, derivable, gitignored root index (status + updatedAt
|
|
356
|
-
// per feature is all that callers like getActiveFeature need from the root;
|
|
357
|
-
// full feature data is hydrated from feature.json on load).
|
|
358
241
|
atomicWriteJson(getStatePath(), {
|
|
359
242
|
version: state.version,
|
|
360
243
|
project: state.project,
|
|
@@ -364,20 +247,11 @@ export function saveState(state) {
|
|
|
364
247
|
});
|
|
365
248
|
}
|
|
366
249
|
|
|
367
|
-
// ============================================================================
|
|
368
|
-
// File Locking (BUG-002)
|
|
369
|
-
// ============================================================================
|
|
370
|
-
|
|
371
250
|
const LOCK_DIR_NAME = '.morph/state.json.lock';
|
|
372
251
|
const LOCK_MAX_RETRIES = 5;
|
|
373
252
|
const LOCK_BASE_MS = 100;
|
|
374
253
|
const LOCK_STALE_MS = 30_000;
|
|
375
254
|
|
|
376
|
-
/**
|
|
377
|
-
* Wrap a read-modify-write cycle on state.json with a mkdir-based mutex.
|
|
378
|
-
* @param {Function} fn - Async function to execute while holding the lock
|
|
379
|
-
* @returns {Promise<*>} Return value of fn
|
|
380
|
-
*/
|
|
381
255
|
export async function withStateLock(fn) {
|
|
382
256
|
const lockPath = join(process.cwd(), LOCK_DIR_NAME);
|
|
383
257
|
|
|
@@ -408,14 +282,6 @@ export async function withStateLock(fn) {
|
|
|
408
282
|
}
|
|
409
283
|
}
|
|
410
284
|
|
|
411
|
-
/**
|
|
412
|
-
* Initialize new state file
|
|
413
|
-
* @param {Object} options - Options
|
|
414
|
-
* @param {boolean} options.force - Overwrite existing file
|
|
415
|
-
* @param {string} options.projectName - Project name
|
|
416
|
-
* @param {string} options.projectType - Project type (e.g., 'dotnet-nextjs')
|
|
417
|
-
* @returns {Object} Initial state
|
|
418
|
-
*/
|
|
419
285
|
export function initState(options = {}) {
|
|
420
286
|
const { force = false, projectName = '{PROJECT_NAME}', projectType = 'dotnet-nextjs' } = options;
|
|
421
287
|
|
|
@@ -445,27 +311,11 @@ export function initState(options = {}) {
|
|
|
445
311
|
return initialState;
|
|
446
312
|
}
|
|
447
313
|
|
|
448
|
-
// ============================================================================
|
|
449
|
-
// Feature Operations
|
|
450
|
-
// ============================================================================
|
|
451
|
-
|
|
452
|
-
/**
|
|
453
|
-
* Get feature from state
|
|
454
|
-
* @param {string} featureName - Feature name
|
|
455
|
-
* @returns {Object|null} Feature object or null
|
|
456
|
-
*/
|
|
457
314
|
export function getFeature(featureName) {
|
|
458
315
|
const state = loadState();
|
|
459
316
|
return state.features[featureName] || null;
|
|
460
317
|
}
|
|
461
318
|
|
|
462
|
-
/**
|
|
463
|
-
* Create or get feature with default structure.
|
|
464
|
-
* v5: every feature follows the fixed phase sequence
|
|
465
|
-
* (plan → [uiux] → implement → review). No workflow selection.
|
|
466
|
-
* @param {string} featureName - Feature name
|
|
467
|
-
* @returns {Promise<Object>} Feature object
|
|
468
|
-
*/
|
|
469
319
|
async function ensureFeature(featureName) {
|
|
470
320
|
const state = loadState();
|
|
471
321
|
|
|
@@ -492,28 +342,6 @@ async function ensureFeature(featureName) {
|
|
|
492
342
|
return state.features[featureName];
|
|
493
343
|
}
|
|
494
344
|
|
|
495
|
-
/**
|
|
496
|
-
* Explicitly create a feature with the canonical shape (documented in
|
|
497
|
-
* framework/schemas/feature.schema.json). Idempotent: an existing feature is
|
|
498
|
-
* returned untouched (including its `workType` — a later `--type` on a
|
|
499
|
-
* repeat call never overwrites it; see `morph-spec retype` for that). This
|
|
500
|
-
* is the server-side birth path behind `morph-spec create` — replacing the
|
|
501
|
-
* implicit registration on the first Write into .morph/features/ (the
|
|
502
|
-
* state-sync hook remains as fallback).
|
|
503
|
-
*
|
|
504
|
-
* Works even before any state exists (scaffolds the root index), so a
|
|
505
|
-
* feature can be born as the very first morph action in a project.
|
|
506
|
-
*
|
|
507
|
-
* @param {string} featureName - Feature name (kebab-case), also the folder id
|
|
508
|
-
* @param {{ status?: string, workType?: string, workTypeDetection?: object, name?: string, description?: string }} [options]
|
|
509
|
-
* `name` defaults to `featureName` (the slug) — always stamped so a feature
|
|
510
|
-
* is never nameless. `description` is written only when provided (the CLI
|
|
511
|
-
* `create` enforces it; internal callers may omit it). `workType` defaults to
|
|
512
|
-
* 'feature' (v8 routing — see src/core/work-types.js). `workTypeDetection` is
|
|
513
|
-
* only written when provided (source/confidence/matched audit trail from the
|
|
514
|
-
* `--type`/`--request` resolution in `create.js`).
|
|
515
|
-
* @returns {Promise<{ feature: Object, created: boolean }>}
|
|
516
|
-
*/
|
|
517
345
|
export async function createFeature(featureName, options = {}) {
|
|
518
346
|
const { status = 'in_progress', workType, workTypeDetection, name, description } = options;
|
|
519
347
|
const state = loadState(false) || scaffoldState();
|
|
@@ -552,12 +380,6 @@ export async function createFeature(featureName, options = {}) {
|
|
|
552
380
|
return { feature: state.features[featureName], created: true };
|
|
553
381
|
}
|
|
554
382
|
|
|
555
|
-
/**
|
|
556
|
-
* Update feature property (supports nested keys like "approvalGates.plan.approved")
|
|
557
|
-
* @param {string} featureName - Feature name
|
|
558
|
-
* @param {string} key - Property key (supports dot notation)
|
|
559
|
-
* @param {any} value - Value to set
|
|
560
|
-
*/
|
|
561
383
|
export async function updateFeature(featureName, key, value) {
|
|
562
384
|
await ensureFeature(featureName);
|
|
563
385
|
const state = loadState(); // Load AFTER ensuring feature exists
|
|
@@ -579,11 +401,6 @@ export async function updateFeature(featureName, key, value) {
|
|
|
579
401
|
saveState(state);
|
|
580
402
|
}
|
|
581
403
|
|
|
582
|
-
/**
|
|
583
|
-
* Update multiple feature properties at once
|
|
584
|
-
* @param {string} featureName - Feature name
|
|
585
|
-
* @param {Object} updates - Object with key-value pairs to update
|
|
586
|
-
*/
|
|
587
404
|
export async function updateFeatureMultiple(featureName, updates) {
|
|
588
405
|
await ensureFeature(featureName);
|
|
589
406
|
const state = loadState(); // Load AFTER ensuring feature exists
|
|
@@ -607,33 +424,16 @@ export async function updateFeatureMultiple(featureName, updates) {
|
|
|
607
424
|
saveState(state);
|
|
608
425
|
}
|
|
609
426
|
|
|
610
|
-
/**
|
|
611
|
-
* Normalize output type from kebab-case to camelCase (BUG-002 fix - Enhanced)
|
|
612
|
-
* Supports both kebab-case and camelCase input formats for all output types
|
|
613
|
-
* @param {string} type - Output type (e.g., 'ui-design-system', 'stack-scan', or 'uiDesignSystem')
|
|
614
|
-
* @returns {string} Normalized type in camelCase
|
|
615
|
-
*/
|
|
616
427
|
function normalizeOutputType(type) {
|
|
617
|
-
// If string doesn't contain hyphens, assume it's already camelCase
|
|
618
428
|
if (!type.includes('-')) {
|
|
619
429
|
return type;
|
|
620
430
|
}
|
|
621
431
|
|
|
622
|
-
// Convert kebab-case to camelCase automatically
|
|
623
|
-
// Example: 'ui-design-system' → 'uiDesignSystem'
|
|
624
|
-
// Example: 'stack-scan' → 'stackScan'
|
|
625
432
|
const camelCase = type.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
626
433
|
|
|
627
434
|
return camelCase;
|
|
628
435
|
}
|
|
629
436
|
|
|
630
|
-
/**
|
|
631
|
-
* Calculate Levenshtein distance between two strings
|
|
632
|
-
* Used for fuzzy matching and typo detection
|
|
633
|
-
* @param {string} a - First string
|
|
634
|
-
* @param {string} b - Second string
|
|
635
|
-
* @returns {number} Edit distance
|
|
636
|
-
*/
|
|
637
437
|
function levenshteinDistance(a, b) {
|
|
638
438
|
const matrix = [];
|
|
639
439
|
|
|
@@ -651,9 +451,9 @@ function levenshteinDistance(a, b) {
|
|
|
651
451
|
matrix[i][j] = matrix[i - 1][j - 1];
|
|
652
452
|
} else {
|
|
653
453
|
matrix[i][j] = Math.min(
|
|
654
|
-
matrix[i - 1][j - 1] + 1,
|
|
655
|
-
matrix[i][j - 1] + 1,
|
|
656
|
-
matrix[i - 1][j] + 1
|
|
454
|
+
matrix[i - 1][j - 1] + 1,
|
|
455
|
+
matrix[i][j - 1] + 1,
|
|
456
|
+
matrix[i - 1][j] + 1
|
|
657
457
|
);
|
|
658
458
|
}
|
|
659
459
|
}
|
|
@@ -662,12 +462,6 @@ function levenshteinDistance(a, b) {
|
|
|
662
462
|
return matrix[b.length][a.length];
|
|
663
463
|
}
|
|
664
464
|
|
|
665
|
-
/**
|
|
666
|
-
* Find closest matching output type for error suggestions
|
|
667
|
-
* @param {string} inputType - The invalid input type
|
|
668
|
-
* @param {Array<string>} validTypes - List of valid output types
|
|
669
|
-
* @returns {string|null} Closest match or null
|
|
670
|
-
*/
|
|
671
465
|
function findClosestMatch(inputType, validTypes) {
|
|
672
466
|
let closestMatch = null;
|
|
673
467
|
let minDistance = Infinity;
|
|
@@ -677,14 +471,11 @@ function findClosestMatch(inputType, validTypes) {
|
|
|
677
471
|
for (const validType of validTypes) {
|
|
678
472
|
const distance = levenshteinDistance(normalizedInput, validType.toLowerCase());
|
|
679
473
|
|
|
680
|
-
// Consider it a match if distance is less than 3 (likely typo)
|
|
681
|
-
// or if one string contains the other (partial match)
|
|
682
474
|
if (distance < minDistance && distance <= 3) {
|
|
683
475
|
minDistance = distance;
|
|
684
476
|
closestMatch = validType;
|
|
685
477
|
}
|
|
686
478
|
|
|
687
|
-
// Also check for partial matches
|
|
688
479
|
if (validType.toLowerCase().includes(normalizedInput) ||
|
|
689
480
|
normalizedInput.includes(validType.toLowerCase())) {
|
|
690
481
|
if (distance < minDistance) {
|
|
@@ -694,28 +485,20 @@ function findClosestMatch(inputType, validTypes) {
|
|
|
694
485
|
}
|
|
695
486
|
}
|
|
696
487
|
|
|
697
|
-
// Only suggest if similarity is reasonable
|
|
698
488
|
return minDistance <= 3 ? closestMatch : null;
|
|
699
489
|
}
|
|
700
490
|
|
|
701
|
-
/**
|
|
702
|
-
* Mark output as created
|
|
703
|
-
* @param {string} featureName - Feature name
|
|
704
|
-
* @param {string} outputType - Output type (plan, checks, verifierReport, etc.)
|
|
705
|
-
*/
|
|
706
491
|
export async function markOutput(featureName, outputType) {
|
|
707
492
|
await ensureFeature(featureName);
|
|
708
493
|
const state = loadState();
|
|
709
494
|
|
|
710
495
|
const normalized = normalizeOutputType(outputType);
|
|
711
496
|
|
|
712
|
-
// Initialize outputs on demand (not pre-stored)
|
|
713
497
|
if (!state.features[featureName].outputs) {
|
|
714
498
|
state.features[featureName].outputs = getAllOutputPaths(featureName);
|
|
715
499
|
}
|
|
716
500
|
|
|
717
501
|
if (!state.features[featureName].outputs[normalized]) {
|
|
718
|
-
// Try to find closest match for better error message
|
|
719
502
|
const validCamelCaseTypes = Object.keys(state.features[featureName].outputs);
|
|
720
503
|
const suggestion = findClosestMatch(outputType, validCamelCaseTypes);
|
|
721
504
|
|
|
@@ -723,7 +506,6 @@ export async function markOutput(featureName, outputType) {
|
|
|
723
506
|
|
|
724
507
|
if (suggestion) {
|
|
725
508
|
errorMsg += `\n\nDid you mean '${suggestion}'?`;
|
|
726
|
-
// If the suggestion has a kebab-case alternative, show it too
|
|
727
509
|
const kebabAlternative = {
|
|
728
510
|
'uiDesignSystem': 'ui-design-system',
|
|
729
511
|
'uiMockups': 'ui-mockups',
|
|
@@ -745,7 +527,6 @@ export async function markOutput(featureName, outputType) {
|
|
|
745
527
|
state.features[featureName].outputs[normalized].created = true;
|
|
746
528
|
state.features[featureName].updatedAt = new Date().toISOString();
|
|
747
529
|
|
|
748
|
-
// Track the file change automatically
|
|
749
530
|
const outputPath = state.features[featureName].outputs[normalized].path;
|
|
750
531
|
if (outputPath) {
|
|
751
532
|
if (!Array.isArray(state.features[featureName].fileChanges)) {
|
|
@@ -762,13 +543,6 @@ export async function markOutput(featureName, outputType) {
|
|
|
762
543
|
saveState(state);
|
|
763
544
|
}
|
|
764
545
|
|
|
765
|
-
/**
|
|
766
|
-
* Track a file change for a feature
|
|
767
|
-
* @param {string} featureName - Feature name
|
|
768
|
-
* @param {string} filePath - Path of the changed file
|
|
769
|
-
* @param {string} action - 'created' | 'modified' | 'deleted'
|
|
770
|
-
* @param {string} phase - Phase during which the change occurred
|
|
771
|
-
*/
|
|
772
546
|
export async function trackFileChange(featureName, filePath, action, phase) {
|
|
773
547
|
await ensureFeature(featureName);
|
|
774
548
|
const state = loadState();
|
|
@@ -788,13 +562,6 @@ export async function trackFileChange(featureName, filePath, action, phase) {
|
|
|
788
562
|
saveState(state);
|
|
789
563
|
}
|
|
790
564
|
|
|
791
|
-
/**
|
|
792
|
-
* Get file changes for a feature, optionally grouped by phase
|
|
793
|
-
* @param {string} featureName - Feature name
|
|
794
|
-
* @param {Object} options - Options
|
|
795
|
-
* @param {boolean} options.groupByPhase - Group changes by phase
|
|
796
|
-
* @returns {Array|Object} File changes
|
|
797
|
-
*/
|
|
798
565
|
export function getFileChanges(featureName, options = {}) {
|
|
799
566
|
const state = loadState();
|
|
800
567
|
const feature = state.features[featureName];
|
|
@@ -818,12 +585,6 @@ export function getFileChanges(featureName, options = {}) {
|
|
|
818
585
|
return changes;
|
|
819
586
|
}
|
|
820
587
|
|
|
821
|
-
/**
|
|
822
|
-
* Skip a phase and record it
|
|
823
|
-
* @param {string} featureName - Feature name
|
|
824
|
-
* @param {string} phase - Phase to skip
|
|
825
|
-
* @param {string} reason - Reason for skipping
|
|
826
|
-
*/
|
|
827
588
|
export async function skipPhase(featureName, phase, reason = '') {
|
|
828
589
|
await ensureFeature(featureName);
|
|
829
590
|
const state = loadState();
|
|
@@ -832,7 +593,6 @@ export async function skipPhase(featureName, phase, reason = '') {
|
|
|
832
593
|
state.features[featureName].skippedPhases = [];
|
|
833
594
|
}
|
|
834
595
|
|
|
835
|
-
// Don't add duplicates
|
|
836
596
|
const existing = state.features[featureName].skippedPhases.find(s => s.phase === phase);
|
|
837
597
|
if (!existing) {
|
|
838
598
|
state.features[featureName].skippedPhases.push({
|
|
@@ -846,11 +606,6 @@ export async function skipPhase(featureName, phase, reason = '') {
|
|
|
846
606
|
saveState(state);
|
|
847
607
|
}
|
|
848
608
|
|
|
849
|
-
/**
|
|
850
|
-
* Get skipped phases for a feature
|
|
851
|
-
* @param {string} featureName - Feature name
|
|
852
|
-
* @returns {Array} Array of skipped phases
|
|
853
|
-
*/
|
|
854
609
|
export function getSkippedPhases(featureName) {
|
|
855
610
|
const state = loadState();
|
|
856
611
|
if (!state.features[featureName]) {
|
|
@@ -859,19 +614,11 @@ export function getSkippedPhases(featureName) {
|
|
|
859
614
|
return state.features[featureName].skippedPhases || [];
|
|
860
615
|
}
|
|
861
616
|
|
|
862
|
-
/**
|
|
863
|
-
* List all features
|
|
864
|
-
* @returns {Array} Array of [featureName, featureObject] tuples
|
|
865
|
-
*/
|
|
866
617
|
export function listFeatures() {
|
|
867
618
|
const state = loadState();
|
|
868
619
|
return Object.entries(state.features);
|
|
869
620
|
}
|
|
870
621
|
|
|
871
|
-
/**
|
|
872
|
-
* Get project summary
|
|
873
|
-
* @returns {Object} Summary with metadata
|
|
874
|
-
*/
|
|
875
622
|
export function getSummary() {
|
|
876
623
|
const state = loadState();
|
|
877
624
|
return {
|
|
@@ -881,17 +628,6 @@ export function getSummary() {
|
|
|
881
628
|
};
|
|
882
629
|
}
|
|
883
630
|
|
|
884
|
-
// ============================================================================
|
|
885
|
-
// Approval Gates Operations
|
|
886
|
-
// ============================================================================
|
|
887
|
-
|
|
888
|
-
/**
|
|
889
|
-
* Set approval gate status
|
|
890
|
-
* @param {string} featureName - Feature name
|
|
891
|
-
* @param {string} gate - Gate name (plan, uiux, review)
|
|
892
|
-
* @param {boolean} approved - Approval status
|
|
893
|
-
* @param {Object} metadata - Additional metadata (approvedBy, reason, etc.)
|
|
894
|
-
*/
|
|
895
631
|
export async function setApprovalGate(featureName, gate, approved, metadata = {}) {
|
|
896
632
|
const state = loadState();
|
|
897
633
|
const feature = await ensureFeature(featureName);
|
|
@@ -915,12 +651,6 @@ export async function setApprovalGate(featureName, gate, approved, metadata = {}
|
|
|
915
651
|
saveState(state);
|
|
916
652
|
}
|
|
917
653
|
|
|
918
|
-
/**
|
|
919
|
-
* Get approval gate status
|
|
920
|
-
* @param {string} featureName - Feature name
|
|
921
|
-
* @param {string} gate - Gate name
|
|
922
|
-
* @returns {Object|null} Gate object or null
|
|
923
|
-
*/
|
|
924
654
|
export function getApprovalGate(featureName, gate) {
|
|
925
655
|
const state = loadState();
|
|
926
656
|
const feature = state.features[featureName];
|
|
@@ -932,11 +662,6 @@ export function getApprovalGate(featureName, gate) {
|
|
|
932
662
|
return feature.approvalGates[gate] || null;
|
|
933
663
|
}
|
|
934
664
|
|
|
935
|
-
/**
|
|
936
|
-
* Check if feature is pending approval
|
|
937
|
-
* @param {string} featureName - Feature name
|
|
938
|
-
* @returns {boolean} True if any gate is pending approval
|
|
939
|
-
*/
|
|
940
665
|
export function isPendingApproval(featureName) {
|
|
941
666
|
const state = loadState();
|
|
942
667
|
const feature = state.features[featureName];
|
|
@@ -947,7 +672,6 @@ export function isPendingApproval(featureName) {
|
|
|
947
672
|
|
|
948
673
|
const currentPhase = feature.phase || 'plan';
|
|
949
674
|
|
|
950
|
-
// Check if current phase has an approval gate
|
|
951
675
|
const phaseGateMap = {
|
|
952
676
|
'plan': 'plan',
|
|
953
677
|
'review': 'review',
|
|
@@ -963,11 +687,6 @@ export function isPendingApproval(featureName) {
|
|
|
963
687
|
return gate && !gate.approved;
|
|
964
688
|
}
|
|
965
689
|
|
|
966
|
-
/**
|
|
967
|
-
* Get approval history for a feature
|
|
968
|
-
* @param {string} featureName - Feature name
|
|
969
|
-
* @returns {Array} Array of approval events
|
|
970
|
-
*/
|
|
971
690
|
export function getApprovalHistory(featureName) {
|
|
972
691
|
const state = loadState();
|
|
973
692
|
const feature = state.features[featureName];
|
|
@@ -990,6 +709,5 @@ export function getApprovalHistory(featureName) {
|
|
|
990
709
|
}
|
|
991
710
|
});
|
|
992
711
|
|
|
993
|
-
// Sort by timestamp
|
|
994
712
|
return history.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
|
|
995
713
|
}
|