@polymorphism-tech/morph-spec 4.8.19 → 4.9.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 +21 -0
- package/README.md +2 -2
- package/bin/morph-spec.js +15 -56
- package/bin/task-manager.js +115 -14
- package/bin/validate.js +67 -33
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +201 -203
- package/docs/QUICKSTART.md +2 -2
- package/framework/CLAUDE.md +21 -0
- package/framework/agents.json +698 -176
- package/framework/hooks/claude-code/post-tool-use/context-refresh.js +1 -1
- package/framework/hooks/claude-code/post-tool-use/dispatch.js +2 -2
- package/framework/hooks/claude-code/post-tool-use/skill-reminder.js +155 -0
- package/framework/hooks/claude-code/pre-tool-use/protect-spec-files.js +1 -1
- package/framework/hooks/claude-code/session-start/inject-morph-context.js +71 -2
- package/framework/hooks/claude-code/statusline.py +76 -30
- package/framework/hooks/claude-code/user-prompt/set-terminal-title.js +14 -6
- package/framework/hooks/shared/activity-logger.js +0 -24
- package/framework/hooks/shared/phase-utils.js +3 -0
- package/framework/hooks/shared/skill-reminder-helpers.js +79 -0
- package/framework/hooks/shared/stale-task-reset.js +57 -0
- package/framework/hooks/shared/state-reader.js +2 -2
- package/framework/hooks/shared/worktree-helpers.js +53 -0
- package/framework/phases.json +40 -8
- package/framework/skills/level-0-meta/brainstorming/SKILL.md +1 -1
- package/framework/skills/level-0-meta/code-review/SKILL.md +1 -1
- package/framework/skills/level-0-meta/code-review-nextjs/SKILL.md +163 -163
- package/framework/skills/level-0-meta/frontend-review/SKILL.md +5 -5
- package/framework/skills/level-0-meta/morph-checklist/SKILL.md +2 -2
- package/framework/skills/level-0-meta/morph-init/SKILL.md +5 -5
- package/framework/skills/level-0-meta/morph-replicate/SKILL.md +4 -4
- package/framework/skills/level-0-meta/morph-replicate/references/blazor-html-mapping.md +1 -1
- package/framework/skills/level-0-meta/post-implementation/SKILL.md +59 -12
- package/framework/skills/level-0-meta/simulation-checklist/SKILL.md +1 -1
- package/framework/skills/level-0-meta/terminal-title/SKILL.md +1 -1
- package/framework/skills/level-0-meta/tool-usage-guide/SKILL.md +1 -1
- package/framework/skills/level-0-meta/tool-usage-guide/references/tools-per-phase.md +6 -5
- package/framework/skills/level-0-meta/verification-before-completion/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +215 -189
- package/framework/skills/level-1-workflows/phase-codebase-analysis/SKILL.md +251 -251
- package/framework/skills/level-1-workflows/phase-design/SKILL.md +382 -365
- package/framework/skills/level-1-workflows/phase-implement/SKILL.md +492 -450
- package/framework/skills/level-1-workflows/phase-setup/SKILL.md +194 -190
- package/framework/skills/level-1-workflows/phase-tasks/SKILL.md +270 -270
- package/framework/skills/level-1-workflows/phase-uiux/SKILL.md +285 -285
- package/framework/standards/STANDARDS.json +640 -88
- package/framework/standards/infrastructure/vercel/vercel-database.md +106 -0
- package/framework/templates/REGISTRY.json +1825 -1909
- package/framework/templates/context/CONTEXT-FEATURE.md +276 -276
- package/framework/templates/docs/onboarding.md +1 -5
- package/package.json +2 -6
- package/src/commands/agents/dispatch-agents.js +55 -4
- package/src/commands/project/doctor.js +16 -47
- package/src/commands/project/init.js +1 -1
- package/src/commands/project/status.js +2 -2
- package/src/commands/project/update.js +381 -365
- package/src/commands/project/worktree.js +154 -0
- package/src/commands/state/advance-phase.js +120 -30
- package/src/commands/state/approve.js +2 -2
- package/src/commands/state/index.js +7 -8
- package/src/commands/state/phase-runner.js +1 -1
- package/src/commands/state/state.js +61 -6
- package/src/commands/tasks/task.js +78 -99
- package/src/commands/templates/template-render.js +93 -173
- package/src/commands/trust/trust.js +26 -21
- package/src/core/paths/output-schema.js +15 -0
- package/src/core/state/state-manager.js +28 -54
- package/src/core/workflows/workflow-detector.js +9 -87
- package/src/lib/phase-chain/phase-validator.js +330 -0
- package/src/lib/stack/stack-profile.js +88 -0
- package/src/lib/tasks/task-classifier.js +16 -0
- package/src/lib/tasks/test-runner.js +77 -0
- package/src/lib/trust/trust-manager.js +32 -144
- package/src/lib/validators/spec-validator.js +58 -4
- package/src/lib/validators/validation-runner.js +23 -11
- package/src/scripts/setup-infra.js +240 -224
- package/src/utils/agents-installer.js +2 -2
- package/src/utils/banner.js +1 -1
- package/src/utils/claude-settings-manager.js +1 -1
- package/src/utils/file-copier.js +1 -0
- package/src/utils/hooks-installer.js +258 -8
- package/framework/hooks/dev/check-sync-health.js +0 -117
- package/framework/hooks/dev/guard-version-numbers.js +0 -57
- package/framework/hooks/dev/sync-standards-registry.js +0 -60
- package/framework/hooks/dev/sync-template-registry.js +0 -60
- package/framework/hooks/dev/validate-skill-format.js +0 -70
- package/framework/hooks/dev/validate-standard-format.js +0 -73
- package/framework/templates/meta-prompts/hops/hop-retry.md +0 -78
- package/framework/templates/meta-prompts/hops/hop-validation.md +0 -97
- package/framework/templates/meta-prompts/hops/hop-wrapper.md +0 -36
- package/framework/workflows/configs/design-impl.json +0 -49
- package/framework/workflows/configs/express.json +0 -45
- package/framework/workflows/configs/fast-track.json +0 -42
- package/framework/workflows/configs/full-morph.json +0 -79
- package/framework/workflows/configs/fusion.json +0 -39
- package/framework/workflows/configs/long-running.json +0 -33
- package/framework/workflows/configs/spec-only.json +0 -43
- package/framework/workflows/configs/ui-refresh.json +0 -49
- package/framework/workflows/configs/zero-touch.json +0 -82
- package/src/commands/project/monitor.js +0 -295
- package/src/commands/project/tutorial.js +0 -115
- package/src/commands/state/validate-phase.js +0 -238
- package/src/commands/templates/generate-contracts.js +0 -445
- package/src/core/orchestrator.js +0 -171
- package/src/core/registry/command-registry.js +0 -28
- package/src/core/registry/index.js +0 -8
- package/src/core/registry/validator-registry.js +0 -204
- package/src/core/templates/template-validator.js +0 -296
- package/src/generator/config-generator.js +0 -206
- package/src/generator/templates/config.json.template +0 -40
- package/src/generator/templates/project.md.template +0 -67
- package/src/lib/agents/micro-agent-factory.js +0 -161
- package/src/lib/analysis/complexity-analyzer.js +0 -441
- package/src/lib/analysis/index.js +0 -7
- package/src/lib/analytics/analytics-engine.js +0 -345
- package/src/lib/checkpoints/checkpoint-hooks.js +0 -298
- package/src/lib/checkpoints/index.js +0 -7
- package/src/lib/context/context-bundler.js +0 -241
- package/src/lib/context/context-optimizer.js +0 -212
- package/src/lib/context/context-tracker.js +0 -273
- package/src/lib/context/core-four-tracker.js +0 -201
- package/src/lib/context/mcp-optimizer.js +0 -200
- package/src/lib/execution/fusion-executor.js +0 -304
- package/src/lib/execution/parallel-executor.js +0 -270
- package/src/lib/hooks/stop-hook-executor.js +0 -286
- package/src/lib/hops/hop-composer.js +0 -221
- package/src/lib/phase-chain/eligibility-checker.js +0 -243
- package/src/lib/threads/thread-coordinator.js +0 -238
- package/src/lib/threads/thread-manager.js +0 -317
- package/src/lib/tracking/artifact-trail.js +0 -202
- package/src/scanner/project-scanner.js +0 -242
- package/src/ui/diff-display.js +0 -91
- package/src/ui/interactive-wizard.js +0 -96
- package/src/ui/user-review.js +0 -211
- package/src/ui/wizard-questions.js +0 -188
- package/src/utils/color-utils.js +0 -70
- package/src/utils/process-handler.js +0 -97
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Phase Eligibility Checker
|
|
3
|
-
*
|
|
4
|
-
* Determines whether a feature is eligible to auto-advance to the next phase.
|
|
5
|
-
* Used by phase-runner.js and the dispatch.js PostToolUse hook.
|
|
6
|
-
*
|
|
7
|
-
* Eligibility criteria (all must pass):
|
|
8
|
-
* 1. Required outputs for the current phase exist on disk
|
|
9
|
-
* 2. No tasks in state with status = 'blocked'
|
|
10
|
-
* 3. validationHistory passRate >= minPassRate (from workflow config)
|
|
11
|
-
* 4. Trust level meets the workflow's trustLevel requirement
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { existsSync } from 'fs';
|
|
15
|
-
import { join } from 'path';
|
|
16
|
-
import { loadState } from '../../core/state/state-manager.js';
|
|
17
|
-
import { getWorkflowConfig } from '../../core/workflows/workflow-detector.js';
|
|
18
|
-
import { derivePhase } from '../../core/state/state-manager.js';
|
|
19
|
-
import { PHASES } from '../../commands/state/validate-phase.js';
|
|
20
|
-
import { shouldAutoApprove } from '../trust/trust-manager.js';
|
|
21
|
-
|
|
22
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
23
|
-
// Types (JSDoc)
|
|
24
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* @typedef {Object} EligibilityBlocker
|
|
28
|
-
* @property {'missing_outputs'|'blocked_tasks'|'low_pass_rate'|'trust_too_low'|'state_error'} type
|
|
29
|
-
* @property {string[]} [items] - Specific items causing the block
|
|
30
|
-
* @property {number} [current] - Current value (for rate/level comparisons)
|
|
31
|
-
* @property {number|string} [required] - Required value
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* @typedef {Object} EligibilityResult
|
|
36
|
-
* @property {boolean} eligible
|
|
37
|
-
* @property {EligibilityBlocker[]} blockers
|
|
38
|
-
* @property {string} currentPhase
|
|
39
|
-
* @property {string|null} nextPhase
|
|
40
|
-
* @property {number|null} passRate - Current validation pass rate (null if no data)
|
|
41
|
-
*/
|
|
42
|
-
|
|
43
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
44
|
-
// Helpers
|
|
45
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
46
|
-
|
|
47
|
-
/** Phase order (canonical from advance-phase.js) */
|
|
48
|
-
const PHASE_ORDER = ['proposal', 'setup', 'uiux', 'design', 'clarify', 'tasks', 'implement', 'sync'];
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Get the next phase in the standard sequence.
|
|
52
|
-
* Returns null when at the last phase.
|
|
53
|
-
*/
|
|
54
|
-
function getNextPhase(currentPhase) {
|
|
55
|
-
const idx = PHASE_ORDER.indexOf(currentPhase);
|
|
56
|
-
if (idx === -1 || idx >= PHASE_ORDER.length - 1) return null;
|
|
57
|
-
return PHASE_ORDER[idx + 1];
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Compute the overall validation pass rate from validationHistory.
|
|
62
|
-
* Returns null when no history exists.
|
|
63
|
-
*
|
|
64
|
-
* @param {Object} validationHistory - feature.validationHistory
|
|
65
|
-
* @returns {number|null}
|
|
66
|
-
*/
|
|
67
|
-
export function computePassRate(validationHistory) {
|
|
68
|
-
if (!validationHistory || Object.keys(validationHistory).length === 0) return null;
|
|
69
|
-
|
|
70
|
-
const entries = Object.values(validationHistory);
|
|
71
|
-
const total = entries.length;
|
|
72
|
-
const passed = entries.filter(e => e.status === 'passed').length;
|
|
73
|
-
|
|
74
|
-
return total > 0 ? passed / total : null;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Check if required outputs exist on disk for the given phase.
|
|
79
|
-
*
|
|
80
|
-
* @param {string} featureName
|
|
81
|
-
* @param {string} phase
|
|
82
|
-
* @param {string} projectPath
|
|
83
|
-
* @returns {string[]} Array of missing output descriptions
|
|
84
|
-
*/
|
|
85
|
-
function getMissingRequiredOutputs(featureName, phase, projectPath) {
|
|
86
|
-
const phaseDef = PHASES[phase];
|
|
87
|
-
if (!phaseDef?.requiredOutputs) return [];
|
|
88
|
-
|
|
89
|
-
const missing = [];
|
|
90
|
-
const featureBase = join(projectPath, '.morph', 'features', featureName);
|
|
91
|
-
|
|
92
|
-
// Map output type names to common file paths
|
|
93
|
-
const outputPathMap = {
|
|
94
|
-
'proposal': '0-proposal/proposal.md',
|
|
95
|
-
'spec': '1-design/spec.md',
|
|
96
|
-
'contracts': '1-design/contracts.cs',
|
|
97
|
-
'tasks': '3-tasks/tasks.md',
|
|
98
|
-
'schemaAnalysis': '1-design/schema-analysis.md',
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
for (const output of phaseDef.requiredOutputs) {
|
|
102
|
-
const relPath = outputPathMap[output];
|
|
103
|
-
if (relPath && !existsSync(join(featureBase, relPath))) {
|
|
104
|
-
missing.push(output);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return missing;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Get tasks that are blocked (attempt >= 3, no resolution).
|
|
113
|
-
*
|
|
114
|
-
* @param {Object} feature - Feature state object
|
|
115
|
-
* @returns {string[]} Array of blocked task IDs
|
|
116
|
-
*/
|
|
117
|
-
function getBlockedTasks(feature) {
|
|
118
|
-
const history = feature.validationHistory || {};
|
|
119
|
-
return Object.entries(history)
|
|
120
|
-
.filter(([, entry]) => entry.status === 'blocked')
|
|
121
|
-
.map(([taskId]) => taskId);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
125
|
-
// Main export
|
|
126
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Check whether a feature is eligible to auto-advance to the next phase.
|
|
130
|
-
*
|
|
131
|
-
* @param {string} featureName
|
|
132
|
-
* @param {Object} [opts]
|
|
133
|
-
* @param {string} [opts.projectPath] - Project root (defaults to cwd)
|
|
134
|
-
* @param {boolean} [opts.dryRun] - Only compute, don't enforce
|
|
135
|
-
* @returns {EligibilityResult}
|
|
136
|
-
*/
|
|
137
|
-
export function checkPhaseEligibility(featureName, opts = {}) {
|
|
138
|
-
const projectPath = opts.projectPath || process.cwd();
|
|
139
|
-
|
|
140
|
-
// ── Load state ────────────────────────────────────────────────────────────
|
|
141
|
-
const state = loadState(false);
|
|
142
|
-
if (!state) {
|
|
143
|
-
return {
|
|
144
|
-
eligible: false,
|
|
145
|
-
blockers: [{ type: 'state_error', items: ['state.json not found'] }],
|
|
146
|
-
currentPhase: 'unknown',
|
|
147
|
-
nextPhase: null,
|
|
148
|
-
passRate: null,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const feature = state.features?.[featureName];
|
|
153
|
-
if (!feature) {
|
|
154
|
-
return {
|
|
155
|
-
eligible: false,
|
|
156
|
-
blockers: [{ type: 'state_error', items: [`Feature '${featureName}' not found in state.json`] }],
|
|
157
|
-
currentPhase: 'unknown',
|
|
158
|
-
nextPhase: null,
|
|
159
|
-
passRate: null,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// ── Derive current phase ──────────────────────────────────────────────────
|
|
164
|
-
const featureFolderPath = join(projectPath, '.morph', 'features', featureName);
|
|
165
|
-
const currentPhase = feature.phase || derivePhase(featureFolderPath);
|
|
166
|
-
const nextPhase = getNextPhase(currentPhase);
|
|
167
|
-
|
|
168
|
-
if (!nextPhase) {
|
|
169
|
-
return {
|
|
170
|
-
eligible: false,
|
|
171
|
-
blockers: [{ type: 'state_error', items: ['Feature is at the final phase'] }],
|
|
172
|
-
currentPhase,
|
|
173
|
-
nextPhase: null,
|
|
174
|
-
passRate: null,
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// ── Load workflow config ──────────────────────────────────────────────────
|
|
179
|
-
let workflowConfig = null;
|
|
180
|
-
if (feature.workflow && feature.workflow !== 'auto') {
|
|
181
|
-
try {
|
|
182
|
-
workflowConfig = getWorkflowConfig(feature.workflow);
|
|
183
|
-
} catch {
|
|
184
|
-
// Non-blocking: fall through to defaults
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const minPassRate = workflowConfig?.phaseChain?.pauseOn
|
|
189
|
-
? 0.80 // Default minimum
|
|
190
|
-
: workflowConfig?.eligibility?.minPassRate ?? 0.80;
|
|
191
|
-
|
|
192
|
-
const blockers = [];
|
|
193
|
-
|
|
194
|
-
// ── Check 1: Missing required outputs ────────────────────────────────────
|
|
195
|
-
const missingOutputs = getMissingRequiredOutputs(featureName, currentPhase, projectPath);
|
|
196
|
-
if (missingOutputs.length > 0) {
|
|
197
|
-
blockers.push({ type: 'missing_outputs', items: missingOutputs });
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// ── Check 2: Blocked tasks ────────────────────────────────────────────────
|
|
201
|
-
const blockedTasks = getBlockedTasks(feature);
|
|
202
|
-
if (blockedTasks.length > 0) {
|
|
203
|
-
blockers.push({ type: 'blocked_tasks', items: blockedTasks });
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// ── Check 3: Validation pass rate ────────────────────────────────────────
|
|
207
|
-
const passRate = computePassRate(feature.validationHistory);
|
|
208
|
-
if (passRate !== null && passRate < minPassRate) {
|
|
209
|
-
blockers.push({
|
|
210
|
-
type: 'low_pass_rate',
|
|
211
|
-
current: passRate,
|
|
212
|
-
required: minPassRate,
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// ── Check 4: Trust level ─────────────────────────────────────────────────
|
|
217
|
-
try {
|
|
218
|
-
const requiredGateMap = { design: 'design', tasks: 'tasks', uiux: 'uiux' };
|
|
219
|
-
const gate = requiredGateMap[currentPhase];
|
|
220
|
-
if (gate) {
|
|
221
|
-
const trustResult = shouldAutoApprove(featureName, gate);
|
|
222
|
-
if (!trustResult.autoApprove) {
|
|
223
|
-
const gateStatus = feature.approvalGates?.[gate];
|
|
224
|
-
if (!gateStatus?.approved) {
|
|
225
|
-
blockers.push({
|
|
226
|
-
type: 'trust_too_low',
|
|
227
|
-
items: [`Gate '${gate}' requires trust level ${trustResult.level || 'medium+'}`],
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
} catch {
|
|
233
|
-
// Trust manager unavailable — non-blocking, don't add blocker
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return {
|
|
237
|
-
eligible: blockers.length === 0,
|
|
238
|
-
blockers,
|
|
239
|
-
currentPhase,
|
|
240
|
-
nextPhase,
|
|
241
|
-
passRate,
|
|
242
|
-
};
|
|
243
|
-
}
|
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Thread Coordinator — Dependency analysis and wait strategies
|
|
3
|
-
*
|
|
4
|
-
* Parses tasks.json dependency graphs and identifies:
|
|
5
|
-
* - Tasks that can run in parallel (no dependencies on each other)
|
|
6
|
-
* - Tasks that must run sequentially
|
|
7
|
-
* - Circular dependencies
|
|
8
|
-
*
|
|
9
|
-
* Returns an execution plan: phases of parallel groups.
|
|
10
|
-
*
|
|
11
|
-
* Example output:
|
|
12
|
-
* Phase 1 (parallel): [T001, T002, T005] ← no dependencies
|
|
13
|
-
* Phase 2 (parallel): [T003, T004] ← depend only on phase 1
|
|
14
|
-
* Phase 3 (sequential): [T006] ← depends on both T003, T004
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { readFileSync, existsSync } from 'fs';
|
|
18
|
-
import { join } from 'path';
|
|
19
|
-
|
|
20
|
-
// ============================================================================
|
|
21
|
-
// Dependency Analysis
|
|
22
|
-
// ============================================================================
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Build a dependency graph from an in-memory task array
|
|
26
|
-
* @param {Array} tasks - Array of { id, dependencies: [] } task objects
|
|
27
|
-
* @returns {Object} { nodes: Map<id, task>, edges: Map<id, Set<depId>>, tasks }
|
|
28
|
-
*/
|
|
29
|
-
export function buildGraph(tasks) {
|
|
30
|
-
const nodes = new Map();
|
|
31
|
-
const edges = new Map();
|
|
32
|
-
|
|
33
|
-
for (const task of tasks) {
|
|
34
|
-
nodes.set(task.id, task);
|
|
35
|
-
edges.set(task.id, new Set(task.dependencies || []));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return { nodes, edges, tasks };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Parse tasks.json and build a dependency graph
|
|
43
|
-
* @param {string} feature - Feature name
|
|
44
|
-
* @returns {Object} { nodes: Map<id, task>, edges: Map<id, Set<depId>> }
|
|
45
|
-
*/
|
|
46
|
-
export function analyzeDependencies(feature) {
|
|
47
|
-
const tasksPath = join(process.cwd(), `.morph/features/${feature}/tasks.json`);
|
|
48
|
-
|
|
49
|
-
if (!existsSync(tasksPath)) {
|
|
50
|
-
throw new Error(`tasks.json not found for feature: ${feature}`);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
let tasksData;
|
|
54
|
-
try {
|
|
55
|
-
tasksData = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
|
56
|
-
} catch (err) {
|
|
57
|
-
throw new Error(`Failed to parse tasks.json: ${err.message}`);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return buildGraph(tasksData.tasks || []);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Identify tasks that can run in parallel (no dependencies between them)
|
|
65
|
-
* @param {Object} graph - { nodes, edges } from buildGraph/analyzeDependencies
|
|
66
|
-
* @returns {Array} Groups of task IDs that can run in parallel
|
|
67
|
-
*/
|
|
68
|
-
export function identifyParallelizable(graph) {
|
|
69
|
-
const { nodes, edges } = graph;
|
|
70
|
-
const plan = planExecution(nodes, edges);
|
|
71
|
-
return plan.filter(group => group.length > 1);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Identify tasks that must run sequentially
|
|
76
|
-
* @param {Object} graph - { nodes, edges } from buildGraph/analyzeDependencies
|
|
77
|
-
* @returns {Array} Task IDs that must run sequentially
|
|
78
|
-
*/
|
|
79
|
-
export function identifySequential(graph) {
|
|
80
|
-
const { nodes, edges } = graph;
|
|
81
|
-
const plan = planExecution(nodes, edges);
|
|
82
|
-
return plan.filter(group => group.length === 1).flat();
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Detect circular dependencies in the task graph
|
|
87
|
-
* @param {Map} nodes - Task nodes
|
|
88
|
-
* @param {Map} edges - Dependency edges
|
|
89
|
-
* @returns {Array} Array of circular dependency chains, or [] if none
|
|
90
|
-
*/
|
|
91
|
-
export function detectCircular(nodes, edges) {
|
|
92
|
-
const cycles = [];
|
|
93
|
-
const visited = new Set();
|
|
94
|
-
const recursionStack = new Set();
|
|
95
|
-
|
|
96
|
-
function dfs(nodeId, path) {
|
|
97
|
-
visited.add(nodeId);
|
|
98
|
-
recursionStack.add(nodeId);
|
|
99
|
-
|
|
100
|
-
for (const dep of (edges.get(nodeId) || [])) {
|
|
101
|
-
if (!nodes.has(dep)) continue;
|
|
102
|
-
|
|
103
|
-
if (!visited.has(dep)) {
|
|
104
|
-
dfs(dep, [...path, nodeId]);
|
|
105
|
-
} else if (recursionStack.has(dep)) {
|
|
106
|
-
// Found a cycle
|
|
107
|
-
const cycleStart = path.indexOf(dep);
|
|
108
|
-
const cycle = cycleStart >= 0
|
|
109
|
-
? [...path.slice(cycleStart), nodeId, dep]
|
|
110
|
-
: [...path, nodeId, dep];
|
|
111
|
-
cycles.push(cycle);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
recursionStack.delete(nodeId);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
for (const nodeId of nodes.keys()) {
|
|
119
|
-
if (!visited.has(nodeId)) {
|
|
120
|
-
dfs(nodeId, []);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return cycles;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Plan execution: return phases of parallel task groups
|
|
129
|
-
* @param {Map} nodes - Task nodes
|
|
130
|
-
* @param {Map} edges - Dependency edges
|
|
131
|
-
* @returns {Array} Array of phases, each phase is an array of task IDs
|
|
132
|
-
*
|
|
133
|
-
* Algorithm: Kahn's topological sort, grouping by "wave"
|
|
134
|
-
*/
|
|
135
|
-
export function planExecution(nodes, edges) {
|
|
136
|
-
// Check for cycles first
|
|
137
|
-
const cycles = detectCircular(nodes, edges);
|
|
138
|
-
if (cycles.length > 0) {
|
|
139
|
-
throw new Error(
|
|
140
|
-
`Circular dependencies detected: ${cycles.map(c => c.join(' → ')).join('; ')}`
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Build reverse edges (dependents)
|
|
145
|
-
const inDegree = new Map();
|
|
146
|
-
const dependents = new Map(); // id → Set of task IDs that depend on it
|
|
147
|
-
|
|
148
|
-
for (const [id] of nodes) {
|
|
149
|
-
inDegree.set(id, 0);
|
|
150
|
-
dependents.set(id, new Set());
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
for (const [id, deps] of edges) {
|
|
154
|
-
for (const dep of deps) {
|
|
155
|
-
if (!inDegree.has(dep)) continue;
|
|
156
|
-
inDegree.set(id, (inDegree.get(id) || 0) + 1);
|
|
157
|
-
dependents.get(dep).add(id);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Correct in-degree: count only deps that exist in nodes
|
|
162
|
-
for (const [id, deps] of edges) {
|
|
163
|
-
let count = 0;
|
|
164
|
-
for (const dep of deps) {
|
|
165
|
-
if (nodes.has(dep)) count++;
|
|
166
|
-
}
|
|
167
|
-
inDegree.set(id, count);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const phases = [];
|
|
171
|
-
const remaining = new Set(nodes.keys());
|
|
172
|
-
|
|
173
|
-
while (remaining.size > 0) {
|
|
174
|
-
// Find all nodes with no unsatisfied dependencies
|
|
175
|
-
const ready = [...remaining].filter(id => {
|
|
176
|
-
const deps = edges.get(id) || new Set();
|
|
177
|
-
return [...deps].every(dep => !remaining.has(dep));
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
if (ready.length === 0 && remaining.size > 0) {
|
|
181
|
-
// Shouldn't happen if no cycles, but safeguard
|
|
182
|
-
throw new Error(`Execution plan stalled with ${remaining.size} remaining tasks: ${[...remaining].join(', ')}`);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
phases.push(ready);
|
|
186
|
-
for (const id of ready) {
|
|
187
|
-
remaining.delete(id);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return phases;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// ============================================================================
|
|
195
|
-
// Full Analysis
|
|
196
|
-
// ============================================================================
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Get full parallel execution plan for a feature
|
|
200
|
-
* @param {string} feature - Feature name
|
|
201
|
-
* @returns {Object} Execution plan with phases, parallelizable groups, and stats
|
|
202
|
-
*/
|
|
203
|
-
export function getExecutionPlan(feature) {
|
|
204
|
-
const { nodes, edges, tasks } = analyzeDependencies(feature);
|
|
205
|
-
|
|
206
|
-
const cycles = detectCircular(nodes, edges);
|
|
207
|
-
if (cycles.length > 0) {
|
|
208
|
-
return {
|
|
209
|
-
feature,
|
|
210
|
-
valid: false,
|
|
211
|
-
cycles,
|
|
212
|
-
error: `Circular dependencies found: ${cycles.map(c => c.join(' → ')).join('; ')}`
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const phases = planExecution(nodes, edges);
|
|
217
|
-
const parallelPhases = phases.filter(p => p.length > 1);
|
|
218
|
-
const totalParallelizable = parallelPhases.reduce((sum, p) => sum + p.length, 0);
|
|
219
|
-
|
|
220
|
-
return {
|
|
221
|
-
feature,
|
|
222
|
-
valid: true,
|
|
223
|
-
totalTasks: tasks.length,
|
|
224
|
-
phases: phases.map((group, i) => ({
|
|
225
|
-
phase: i + 1,
|
|
226
|
-
tasks: group,
|
|
227
|
-
canRunParallel: group.length > 1,
|
|
228
|
-
count: group.length
|
|
229
|
-
})),
|
|
230
|
-
stats: {
|
|
231
|
-
totalPhases: phases.length,
|
|
232
|
-
parallelPhases: parallelPhases.length,
|
|
233
|
-
parallelizableTasks: totalParallelizable,
|
|
234
|
-
sequentialTasks: tasks.length - totalParallelizable,
|
|
235
|
-
parallelizationRatio: Math.round(totalParallelizable / Math.max(tasks.length, 1) * 100)
|
|
236
|
-
}
|
|
237
|
-
};
|
|
238
|
-
}
|