@polymorphism-tech/morph-spec 7.2.0 → 7.4.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 +1 -1
- package/README.md +2 -2
- package/bin/morph-spec.js +4 -2
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +2 -2
- package/framework/MORPH.md +29 -12
- package/framework/agents.json +132 -132
- package/framework/commands/morph-apply.md +1 -1
- package/framework/commands/morph-proposal.md +1 -1
- package/framework/evals/rubrics/README.md +9 -8
- package/framework/hooks/README.md +3 -1
- package/framework/hooks/claude-code/core/gate-guard.js +160 -0
- package/framework/hooks/claude-code/core/state-sync.js +15 -3
- package/framework/hooks/claude-code/core/validate-completion.js +241 -136
- package/framework/hooks/shared/state-reader.js +236 -221
- package/framework/phases.json +1 -1
- package/framework/skills/README.md +2 -2
- package/framework/skills/core/morph-apply/SKILL.md +63 -21
- package/framework/skills/core/morph-brainstorming/SKILL.md +5 -1
- package/framework/skills/core/morph-eval/SKILL.md +4 -5
- package/framework/skills/core/morph-implement/SKILL.md +47 -19
- package/framework/skills/core/morph-plan/SKILL.md +8 -2
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +13 -6
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/standards/ai-agents/setup.md +258 -244
- package/framework/templates/README.md +708 -709
- package/framework/templates/REGISTRY.json +1 -20
- package/framework/templates/context/CONTEXT-FEATURE.md +276 -276
- package/framework/templates/docs/proposal.md +171 -171
- package/framework/templates/feature/recap.md +145 -145
- package/package.json +1 -1
- package/src/commands/project/doctor.js +941 -946
- package/src/commands/project/init.js +517 -515
- package/src/commands/project/status.js +373 -376
- package/src/commands/state/approve.js +251 -213
- package/src/commands/validation/validate-feature.js +356 -287
- package/src/core/state/state-manager.js +910 -934
- package/src/dashboard/app/App.jsx +29 -31
- package/src/dashboard/app/components/Sidebar.jsx +44 -45
- package/src/dashboard/scanner/index.js +20 -21
- package/src/lib/context/context-builder.js +3 -3
- package/src/lib/generators/context-generator.js +2 -3
- package/src/lib/generators/mandate-generator.js +48 -92
- package/src/lib/generators/metadata-extractor.js +385 -386
- package/src/lib/generators/recap-generator.js +285 -285
- package/src/lib/risk-detector.js +2 -2
- package/src/lib/validators/content/content-validator.js +89 -286
- package/src/lib/validators/content/index.js +0 -1
- package/src/utils/hooks-installer.js +527 -516
- package/framework/templates/feature/tasks.md +0 -199
- package/framework/workflows/README.md +0 -1041
- package/framework/workflows/configs/fast-track.json +0 -46
- package/framework/workflows/configs/full.json +0 -64
- package/framework/workflows/configs/quick.json +0 -32
- package/framework/workflows/configs/standard.json +0 -57
- package/framework/workflows/docs/STORY-DRIVEN-DEVELOPMENT.md +0 -392
- package/framework/workflows/docs/design-impl.md +0 -37
- package/framework/workflows/docs/enforcement-pipeline.md +0 -668
- package/framework/workflows/docs/fast-track.md +0 -29
- package/framework/workflows/docs/full-morph.md +0 -69
- package/framework/workflows/docs/standard.md +0 -49
- package/framework/workflows/docs/ui-refresh.md +0 -39
- package/src/commands/state/index.js +0 -7
- package/src/core/workflows/workflow-detector.js +0 -586
- package/src/core/workflows/workflow-router.js +0 -204
- package/src/dashboard/app/pages/Workflows.jsx +0 -346
- package/src/dashboard/scanner/workflows-scanner.js +0 -18
- package/src/lib/phase-chain/phase-validator.js +0 -359
- package/src/lib/validators/content/cross-phase-validator.js +0 -281
- package/src/lib/validators/spec-validator.js +0 -312
|
@@ -1,586 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Workflow Detection Engine
|
|
3
|
-
*
|
|
4
|
-
* Analyzes user requests to automatically select the appropriate workflow based on:
|
|
5
|
-
* - Keyword matching
|
|
6
|
-
* - Complexity estimation
|
|
7
|
-
* - Workflow criteria
|
|
8
|
-
*
|
|
9
|
-
* @module workflow-detector
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
13
|
-
import { join, dirname, basename } from 'path';
|
|
14
|
-
import { fileURLToPath } from 'url';
|
|
15
|
-
import { getPhaseSequence } from '../state/phase-state-machine.js';
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Load the canonical phase id set from framework/phases.json.
|
|
19
|
-
* Falls back to getPhaseSequence() if the file is unavailable.
|
|
20
|
-
* @returns {Set<string>}
|
|
21
|
-
*/
|
|
22
|
-
function getValidPhaseIds() {
|
|
23
|
-
try {
|
|
24
|
-
const phasesPath = join(__dirname, '..', '..', '..', 'framework', 'phases.json');
|
|
25
|
-
const { phases } = JSON.parse(readFileSync(phasesPath, 'utf8'));
|
|
26
|
-
return new Set(Object.keys(phases));
|
|
27
|
-
} catch {
|
|
28
|
-
// Graceful fallback to state machine list
|
|
29
|
-
return new Set(getPhaseSequence());
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Get framework root directory
|
|
37
|
-
* @param {string} projectPath - Project path
|
|
38
|
-
* @returns {string} Framework root path
|
|
39
|
-
*/
|
|
40
|
-
function getFrameworkRoot(projectPath) {
|
|
41
|
-
// Try npm-installed package first (only if it has the workflow configs)
|
|
42
|
-
const npmPath = join(projectPath, 'node_modules/@polymorphism-tech/morph-spec');
|
|
43
|
-
if (existsSync(npmPath) && existsSync(join(npmPath, 'framework/workflows/configs'))) {
|
|
44
|
-
return npmPath;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Fallback to local development path (from src/core/workflows/ → root)
|
|
48
|
-
return join(__dirname, '..', '..', '..');
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Load all workflow configurations
|
|
53
|
-
* @param {string} projectPath - Project path
|
|
54
|
-
* @returns {Array<Object>} Array of workflow configs
|
|
55
|
-
*/
|
|
56
|
-
function loadWorkflowConfigs(projectPath) {
|
|
57
|
-
const frameworkRoot = getFrameworkRoot(projectPath);
|
|
58
|
-
const configsDir = join(frameworkRoot, 'framework/workflows/configs');
|
|
59
|
-
|
|
60
|
-
if (!existsSync(configsDir)) {
|
|
61
|
-
return [];
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const configFiles = readdirSync(configsDir).filter(f => f.endsWith('.json'));
|
|
65
|
-
const configs = [];
|
|
66
|
-
|
|
67
|
-
for (const file of configFiles) {
|
|
68
|
-
try {
|
|
69
|
-
const configPath = join(configsDir, file);
|
|
70
|
-
const content = readFileSync(configPath, 'utf8');
|
|
71
|
-
configs.push(JSON.parse(content));
|
|
72
|
-
} catch (err) {
|
|
73
|
-
// Skip invalid configs
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return configs;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Match keywords in user request
|
|
83
|
-
* @param {string} userRequest - User request text
|
|
84
|
-
* @param {Array<string>} keywords - Workflow keywords
|
|
85
|
-
* @returns {{matched: Array<string>, score: number}}
|
|
86
|
-
*/
|
|
87
|
-
function matchKeywords(userRequest, keywords) {
|
|
88
|
-
const requestLower = userRequest.toLowerCase();
|
|
89
|
-
const matched = [];
|
|
90
|
-
|
|
91
|
-
for (const keyword of keywords) {
|
|
92
|
-
const keywordLower = keyword.toLowerCase();
|
|
93
|
-
|
|
94
|
-
// Check for partial matches (e.g., "paginação" matches "pagination")
|
|
95
|
-
if (requestLower.includes(keywordLower)) {
|
|
96
|
-
matched.push(keyword);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Score: percentage of keywords matched
|
|
101
|
-
const score = matched.length > 0 ? matched.length / keywords.length : 0;
|
|
102
|
-
|
|
103
|
-
return { matched, score };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Estimate complexity from user request
|
|
108
|
-
* @param {string} userRequest - User request text
|
|
109
|
-
* @returns {{files: number, lines: number, components: number, hasInfra: boolean}}
|
|
110
|
-
*/
|
|
111
|
-
function estimateComplexity(userRequest) {
|
|
112
|
-
const requestLower = userRequest.toLowerCase();
|
|
113
|
-
|
|
114
|
-
// Scope indicators
|
|
115
|
-
const scopeIndicators = {
|
|
116
|
-
trivial: ['button', 'cor', 'color', 'css', 'fix', 'typo', 'edge case', 'one'],
|
|
117
|
-
small: ['add', 'create', 'single', 'simple', 'quick'],
|
|
118
|
-
medium: ['feature', 'pagination', 'filter', 'export', 'refactor', 'optimize'],
|
|
119
|
-
large: ['system', 'complete', 'all', 'multiple', 'integration', 'architecture'],
|
|
120
|
-
huge: ['platform', 'infrastructure', 'subsystem', 'migrate', 'rebuild']
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
// Infrastructure indicators
|
|
124
|
-
const infraKeywords = ['bicep', 'azure', 'deploy', 'docker', 'kubernetes', 'infrastructure', 'terraform'];
|
|
125
|
-
const hasInfra = infraKeywords.some(kw => requestLower.includes(kw));
|
|
126
|
-
|
|
127
|
-
// Component indicators
|
|
128
|
-
const componentKeywords = ['component', 'page', 'screen', 'form', 'table', 'chart', 'modal'];
|
|
129
|
-
const componentMatches = componentKeywords.filter(kw => requestLower.includes(kw)).length;
|
|
130
|
-
|
|
131
|
-
// Entity indicators
|
|
132
|
-
const entityKeywords = ['user', 'order', 'product', 'customer', 'invoice', 'report'];
|
|
133
|
-
const entityMatches = entityKeywords.filter(kw => requestLower.includes(kw)).length;
|
|
134
|
-
|
|
135
|
-
// Determine scope
|
|
136
|
-
let scope = 'medium';
|
|
137
|
-
for (const [level, indicators] of Object.entries(scopeIndicators)) {
|
|
138
|
-
if (indicators.some(indicator => requestLower.includes(indicator))) {
|
|
139
|
-
scope = level;
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Estimate based on scope
|
|
145
|
-
const estimates = {
|
|
146
|
-
trivial: { files: 1, lines: 30, components: 0 },
|
|
147
|
-
small: { files: 2, lines: 100, components: 1 },
|
|
148
|
-
medium: { files: 5, lines: 200, components: 1 },
|
|
149
|
-
large: { files: 12, lines: 800, components: 3 },
|
|
150
|
-
huge: { files: 25, lines: 2000, components: 5 }
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
const base = estimates[scope] || estimates.medium;
|
|
154
|
-
|
|
155
|
-
// Adjust based on entities and components mentioned
|
|
156
|
-
const files = base.files + entityMatches + componentMatches;
|
|
157
|
-
const lines = base.lines + (entityMatches * 50) + (componentMatches * 100);
|
|
158
|
-
const components = base.components + componentMatches;
|
|
159
|
-
|
|
160
|
-
return {
|
|
161
|
-
files,
|
|
162
|
-
lines,
|
|
163
|
-
components,
|
|
164
|
-
hasInfra
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Check if complexity matches workflow criteria
|
|
170
|
-
* @param {Object} complexity - Estimated complexity
|
|
171
|
-
* @param {Object} criteria - Workflow criteria
|
|
172
|
-
* @returns {{matches: boolean, score: number}}
|
|
173
|
-
*/
|
|
174
|
-
function matchComplexity(complexity, criteria) {
|
|
175
|
-
let matches = 0;
|
|
176
|
-
let total = 0;
|
|
177
|
-
|
|
178
|
-
// File count
|
|
179
|
-
if (criteria.maxFiles !== undefined) {
|
|
180
|
-
total++;
|
|
181
|
-
if (complexity.files <= criteria.maxFiles) matches++;
|
|
182
|
-
}
|
|
183
|
-
if (criteria.minFiles !== undefined) {
|
|
184
|
-
total++;
|
|
185
|
-
if (complexity.files >= criteria.minFiles) matches++;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Line count
|
|
189
|
-
if (criteria.maxLines !== undefined) {
|
|
190
|
-
total++;
|
|
191
|
-
if (complexity.lines <= criteria.maxLines) matches++;
|
|
192
|
-
}
|
|
193
|
-
if (criteria.minLines !== undefined) {
|
|
194
|
-
total++;
|
|
195
|
-
if (complexity.lines >= criteria.minLines) matches++;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Component count
|
|
199
|
-
if (criteria.maxComponents !== undefined) {
|
|
200
|
-
total++;
|
|
201
|
-
if (complexity.components <= criteria.maxComponents) matches++;
|
|
202
|
-
}
|
|
203
|
-
if (criteria.minComponents !== undefined) {
|
|
204
|
-
total++;
|
|
205
|
-
if (complexity.components >= criteria.minComponents) matches++;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Infrastructure
|
|
209
|
-
if (criteria.noInfrastructure !== undefined) {
|
|
210
|
-
total++;
|
|
211
|
-
if (criteria.noInfrastructure === !complexity.hasInfra) matches++;
|
|
212
|
-
}
|
|
213
|
-
if (criteria.infrastructure !== undefined) {
|
|
214
|
-
total++;
|
|
215
|
-
if (criteria.infrastructure === complexity.hasInfra) matches++;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const score = total > 0 ? matches / total : 0.5;
|
|
219
|
-
const meetsAll = total > 0 && matches === total;
|
|
220
|
-
|
|
221
|
-
return { matches: meetsAll, score };
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Directories to skip during codebase scanning
|
|
225
|
-
const SCAN_SKIP_DIRS = new Set([
|
|
226
|
-
'node_modules', '.git', '.morph', 'dist', 'out', '.next',
|
|
227
|
-
'obj', 'bin', '.worktrees', '.vs', '.vscode', 'coverage'
|
|
228
|
-
]);
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Quick codebase scan to estimate change complexity from actual files.
|
|
232
|
-
* Reads only filenames (not content). Capped at 2000 files.
|
|
233
|
-
*
|
|
234
|
-
* @param {string} projectPath - Project root
|
|
235
|
-
* @param {string[]} keywords - Keywords extracted from user request
|
|
236
|
-
* @returns {{ affectedFiles: number, hasSharedInfra: boolean, hasTests: boolean, score: number }}
|
|
237
|
-
*/
|
|
238
|
-
export function quickCodebaseScan(projectPath, keywords) {
|
|
239
|
-
if (!projectPath || projectPath === '.' || !existsSync(projectPath)) {
|
|
240
|
-
return { affectedFiles: 0, hasSharedInfra: false, hasTests: false, score: 0.5 };
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const infraPatterns = ['dockerfile', 'docker-compose', '.bicep', '.tf', 'appsettings.json', 'terraform'];
|
|
244
|
-
const testPatterns = ['.test.', '.spec.', '__tests__'];
|
|
245
|
-
const keywordsLower = keywords.map(k => k.toLowerCase());
|
|
246
|
-
|
|
247
|
-
let filesExamined = 0;
|
|
248
|
-
let affectedFiles = 0;
|
|
249
|
-
let hasSharedInfra = false;
|
|
250
|
-
let hasTests = false;
|
|
251
|
-
const MAX_FILES = 2000;
|
|
252
|
-
|
|
253
|
-
function walk(dir) {
|
|
254
|
-
if (filesExamined >= MAX_FILES) return;
|
|
255
|
-
let entries;
|
|
256
|
-
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
257
|
-
|
|
258
|
-
for (const entry of entries) {
|
|
259
|
-
if (filesExamined >= MAX_FILES) return;
|
|
260
|
-
if (entry.isDirectory()) {
|
|
261
|
-
if (!SCAN_SKIP_DIRS.has(entry.name)) walk(join(dir, entry.name));
|
|
262
|
-
continue;
|
|
263
|
-
}
|
|
264
|
-
filesExamined++;
|
|
265
|
-
const nameLower = entry.name.toLowerCase();
|
|
266
|
-
|
|
267
|
-
if (infraPatterns.some(p => nameLower.includes(p))) hasSharedInfra = true;
|
|
268
|
-
if (testPatterns.some(p => nameLower.includes(p))) hasTests = true;
|
|
269
|
-
if (keywordsLower.some(k => k.length >= 3 && nameLower.includes(k))) affectedFiles++;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
walk(projectPath);
|
|
274
|
-
|
|
275
|
-
// Score: more affected files = higher complexity
|
|
276
|
-
let score = 0.5;
|
|
277
|
-
if (affectedFiles === 0) score = 0.3;
|
|
278
|
-
else if (affectedFiles <= 3) score = 0.4;
|
|
279
|
-
else if (affectedFiles <= 8) score = 0.6;
|
|
280
|
-
else if (affectedFiles <= 15) score = 0.75;
|
|
281
|
-
else score = 0.9;
|
|
282
|
-
|
|
283
|
-
if (hasSharedInfra) score = Math.min(1, score + 0.1);
|
|
284
|
-
|
|
285
|
-
return { affectedFiles, hasSharedInfra, hasTests, score };
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Find completed features similar to the current description.
|
|
290
|
-
* Uses Jaccard similarity on tokenized keywords.
|
|
291
|
-
*
|
|
292
|
-
* @param {string} userRequest - Current feature description
|
|
293
|
-
* @param {string} projectPath - Project root (for loading state)
|
|
294
|
-
* @returns {{ similarFeatures: Array<{name: string, workflow: string, similarity: number}>, suggestedWorkflow: string|null, score: number }}
|
|
295
|
-
*/
|
|
296
|
-
export function findSimilarFeatures(userRequest, projectPath) {
|
|
297
|
-
const neutral = { similarFeatures: [], suggestedWorkflow: null, score: 0.5 };
|
|
298
|
-
|
|
299
|
-
let state;
|
|
300
|
-
try {
|
|
301
|
-
const statePath = join(projectPath, '.morph', 'state.json');
|
|
302
|
-
if (!existsSync(statePath)) return neutral;
|
|
303
|
-
state = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
304
|
-
} catch { return neutral; }
|
|
305
|
-
|
|
306
|
-
if (!state?.features) return neutral;
|
|
307
|
-
|
|
308
|
-
const requestTokens = new Set(
|
|
309
|
-
userRequest.toLowerCase().split(/[\s\-_.,;:!?]+/).filter(t => t.length >= 3)
|
|
310
|
-
);
|
|
311
|
-
if (requestTokens.size === 0) return neutral;
|
|
312
|
-
|
|
313
|
-
const candidates = [];
|
|
314
|
-
|
|
315
|
-
for (const [name, feature] of Object.entries(state.features)) {
|
|
316
|
-
if (feature.status !== 'completed') continue;
|
|
317
|
-
if (!feature.workflow || feature.workflow === 'auto') continue;
|
|
318
|
-
|
|
319
|
-
// Build token set from feature name + matched keywords
|
|
320
|
-
const featureTokens = new Set(
|
|
321
|
-
name.toLowerCase().split(/[\s\-_.,;:!?]+/).filter(t => t.length >= 3)
|
|
322
|
-
);
|
|
323
|
-
if (feature.workflowDetection?.matchedKeywords) {
|
|
324
|
-
for (const kw of feature.workflowDetection.matchedKeywords) {
|
|
325
|
-
kw.toLowerCase().split(/[\s\-_]+/).filter(t => t.length >= 3).forEach(t => featureTokens.add(t));
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Jaccard similarity
|
|
330
|
-
const intersection = [...requestTokens].filter(t => featureTokens.has(t)).length;
|
|
331
|
-
const union = new Set([...requestTokens, ...featureTokens]).size;
|
|
332
|
-
const similarity = union > 0 ? intersection / union : 0;
|
|
333
|
-
|
|
334
|
-
if (similarity > 0.3) {
|
|
335
|
-
candidates.push({ name, workflow: feature.workflow, similarity });
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (candidates.length === 0) return neutral;
|
|
340
|
-
|
|
341
|
-
candidates.sort((a, b) => b.similarity - a.similarity);
|
|
342
|
-
const top = candidates.slice(0, 3);
|
|
343
|
-
|
|
344
|
-
// Suggest the workflow used by the most similar feature
|
|
345
|
-
const suggestedWorkflow = top[0].workflow;
|
|
346
|
-
|
|
347
|
-
// Score: higher similarity = more confidence in the suggestion
|
|
348
|
-
const score = Math.min(1, top[0].similarity + 0.2);
|
|
349
|
-
|
|
350
|
-
return { similarFeatures: top, suggestedWorkflow, score };
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* Score based on description length and structure.
|
|
355
|
-
* Short descriptions suggest simple tasks, long ones suggest complex.
|
|
356
|
-
*
|
|
357
|
-
* @param {string} userRequest
|
|
358
|
-
* @returns {number} 0-1 score where higher = more complex
|
|
359
|
-
*/
|
|
360
|
-
export function scoreDescriptionLength(userRequest) {
|
|
361
|
-
const len = userRequest.trim().length;
|
|
362
|
-
const sentences = userRequest.split(/[.!?]+/).filter(s => s.trim().length > 0).length;
|
|
363
|
-
|
|
364
|
-
if (len < 30) return 0.2;
|
|
365
|
-
if (len < 80) return 0.35;
|
|
366
|
-
if (len < 150) return 0.5;
|
|
367
|
-
if (len < 300 && sentences <= 3) return 0.65;
|
|
368
|
-
return 0.8;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* Detect workflow for a feature request
|
|
373
|
-
* @param {Object} options - Detection options
|
|
374
|
-
* @param {string} options.userRequest - User's feature request
|
|
375
|
-
* @param {string} [options.projectPath='.'] - Project path
|
|
376
|
-
* @param {string} [options.featureName] - Feature name (optional)
|
|
377
|
-
* @returns {Object} Detection result
|
|
378
|
-
*/
|
|
379
|
-
export async function detectWorkflow(options) {
|
|
380
|
-
const { userRequest, projectPath = '.', featureName } = options;
|
|
381
|
-
|
|
382
|
-
if (!userRequest || userRequest.trim().length === 0) {
|
|
383
|
-
throw new Error('userRequest is required');
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// Load all workflow configs
|
|
387
|
-
const workflows = loadWorkflowConfigs(projectPath);
|
|
388
|
-
|
|
389
|
-
if (workflows.length === 0) {
|
|
390
|
-
// Fallback if no configs found
|
|
391
|
-
return {
|
|
392
|
-
workflowId: 'standard',
|
|
393
|
-
confidence: 0.5,
|
|
394
|
-
matchedKeywords: [],
|
|
395
|
-
estimatedComplexity: { files: 0, lines: 0, components: 0, hasInfra: false },
|
|
396
|
-
reasoning: 'No workflow configs found - using standard as fallback',
|
|
397
|
-
alternativeWorkflows: []
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// Estimate complexity
|
|
402
|
-
const complexity = estimateComplexity(userRequest);
|
|
403
|
-
|
|
404
|
-
// New signals: codebase scan + feature history + description length
|
|
405
|
-
const codebaseScan = quickCodebaseScan(projectPath, complexity.hasInfra
|
|
406
|
-
? [...userRequest.toLowerCase().split(/\s+/).filter(w => w.length >= 3)]
|
|
407
|
-
: userRequest.toLowerCase().split(/\s+/).filter(w => w.length >= 3)
|
|
408
|
-
);
|
|
409
|
-
const featureHistory = findSimilarFeatures(userRequest, projectPath);
|
|
410
|
-
const descriptionScore = scoreDescriptionLength(userRequest);
|
|
411
|
-
|
|
412
|
-
// Score each workflow
|
|
413
|
-
const scores = [];
|
|
414
|
-
|
|
415
|
-
for (const workflow of workflows) {
|
|
416
|
-
const keywordMatch = matchKeywords(userRequest, workflow.keywords || []);
|
|
417
|
-
const complexityMatch = matchComplexity(complexity, workflow.criteria || {});
|
|
418
|
-
|
|
419
|
-
// Enhanced weighted scoring (6 signals)
|
|
420
|
-
const weights = {
|
|
421
|
-
keywords: 0.25,
|
|
422
|
-
complexity: 0.20,
|
|
423
|
-
codebaseAnalysis: 0.20,
|
|
424
|
-
featureHistory: 0.15,
|
|
425
|
-
priority: 0.10,
|
|
426
|
-
descriptionLength: 0.10,
|
|
427
|
-
};
|
|
428
|
-
|
|
429
|
-
// Priority score (lower number = higher priority = higher score)
|
|
430
|
-
const priorityScore = workflow.priority ? (4 - workflow.priority) / 3 : 0.5;
|
|
431
|
-
|
|
432
|
-
// Codebase analysis score: align scan results with workflow expectations
|
|
433
|
-
let codebaseScore = codebaseScan.score;
|
|
434
|
-
if (workflow.criteria?.noInfrastructure && codebaseScan.hasSharedInfra) {
|
|
435
|
-
codebaseScore *= 0.5; // Penalize simple workflows when infra detected
|
|
436
|
-
}
|
|
437
|
-
if (workflow.criteria?.minFiles && codebaseScan.affectedFiles >= workflow.criteria.minFiles) {
|
|
438
|
-
codebaseScore = Math.min(1, codebaseScore + 0.2);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// Feature history score: boost if similar features used this workflow
|
|
442
|
-
let historyScore = featureHistory.score;
|
|
443
|
-
if (featureHistory.suggestedWorkflow === workflow.id) {
|
|
444
|
-
historyScore = Math.min(1, historyScore + 0.3);
|
|
445
|
-
} else if (featureHistory.suggestedWorkflow && featureHistory.suggestedWorkflow !== workflow.id) {
|
|
446
|
-
historyScore *= 0.6;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// Description length score: align with workflow complexity expectations
|
|
450
|
-
let descScore = descriptionScore;
|
|
451
|
-
if (workflow.priority === 1 && descriptionScore > 0.6) {
|
|
452
|
-
descScore *= 0.7; // Long descriptions less likely to be fast-track
|
|
453
|
-
}
|
|
454
|
-
if (workflow.priority === 3 && descriptionScore < 0.4) {
|
|
455
|
-
descScore *= 0.7; // Short descriptions less likely to be full
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// Calculate total score
|
|
459
|
-
let totalScore =
|
|
460
|
-
(keywordMatch.score * weights.keywords) +
|
|
461
|
-
(complexityMatch.score * weights.complexity) +
|
|
462
|
-
(codebaseScore * weights.codebaseAnalysis) +
|
|
463
|
-
(historyScore * weights.featureHistory) +
|
|
464
|
-
(priorityScore * weights.priority) +
|
|
465
|
-
(descScore * weights.descriptionLength);
|
|
466
|
-
|
|
467
|
-
// Penalty for workflows with no keyword matches
|
|
468
|
-
if (keywordMatch.matched.length === 0) {
|
|
469
|
-
totalScore *= 0.5; // 50% penalty for no keyword match
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// Boost for strong keyword matches (3+ keywords matched)
|
|
473
|
-
if (keywordMatch.matched.length >= 3) {
|
|
474
|
-
totalScore *= 1.2; // 20% boost
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
scores.push({
|
|
478
|
-
workflowId: workflow.id,
|
|
479
|
-
workflowName: workflow.name,
|
|
480
|
-
score: totalScore,
|
|
481
|
-
keywordMatch: keywordMatch.matched,
|
|
482
|
-
complexityMatch: complexityMatch.matches,
|
|
483
|
-
reasoning: []
|
|
484
|
-
});
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// Sort by score (highest first)
|
|
488
|
-
scores.sort((a, b) => b.score - a.score);
|
|
489
|
-
|
|
490
|
-
// Get top result
|
|
491
|
-
const topResult = scores[0];
|
|
492
|
-
const confidence = topResult.score;
|
|
493
|
-
|
|
494
|
-
// Build reasoning
|
|
495
|
-
const reasoning = [];
|
|
496
|
-
if (topResult.keywordMatch.length > 0) {
|
|
497
|
-
reasoning.push(`Matched keyword${topResult.keywordMatch.length > 1 ? 's' : ''}: ${topResult.keywordMatch.join(', ')}`);
|
|
498
|
-
}
|
|
499
|
-
reasoning.push(`Estimated ${complexity.files} files, ~${complexity.lines} lines`);
|
|
500
|
-
if (complexity.hasInfra) {
|
|
501
|
-
reasoning.push('Infrastructure keywords detected');
|
|
502
|
-
}
|
|
503
|
-
if (topResult.complexityMatch) {
|
|
504
|
-
reasoning.push('Meets all complexity criteria');
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Get alternatives (next 2 workflows with score > 0.2)
|
|
508
|
-
const alternatives = scores.slice(1, 3)
|
|
509
|
-
.filter(w => w.score > 0.2)
|
|
510
|
-
.map(w => ({
|
|
511
|
-
workflowId: w.workflowId,
|
|
512
|
-
confidence: w.score,
|
|
513
|
-
reason: `${w.workflowName} (${Math.round(w.score * 100)}% match)`
|
|
514
|
-
}));
|
|
515
|
-
|
|
516
|
-
// Warnings for low confidence or ambiguous results
|
|
517
|
-
const warnings = [];
|
|
518
|
-
if (confidence < 0.4) {
|
|
519
|
-
warnings.push('Low confidence — consider specifying workflow explicitly with --workflow');
|
|
520
|
-
}
|
|
521
|
-
if (alternatives.length > 0 && alternatives[0].confidence > confidence * 0.8) {
|
|
522
|
-
warnings.push(`Close alternative: ${alternatives[0].workflowId} (${Math.round(alternatives[0].confidence * 100)}%) — verify recommendation`);
|
|
523
|
-
}
|
|
524
|
-
if (featureHistory.similarFeatures.length > 0 && featureHistory.suggestedWorkflow !== topResult.workflowId) {
|
|
525
|
-
warnings.push(`Similar features used ${featureHistory.suggestedWorkflow} workflow`);
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
return {
|
|
529
|
-
workflowId: topResult.workflowId,
|
|
530
|
-
confidence,
|
|
531
|
-
matchedKeywords: topResult.keywordMatch,
|
|
532
|
-
estimatedComplexity: complexity,
|
|
533
|
-
reasoning: reasoning.join(' + '),
|
|
534
|
-
alternativeWorkflows: alternatives,
|
|
535
|
-
warnings
|
|
536
|
-
};
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
/**
|
|
540
|
-
* Get workflow configuration by ID
|
|
541
|
-
* @param {string} workflowId - Workflow ID
|
|
542
|
-
* @param {string} [projectPath='.'] - Project path
|
|
543
|
-
* @returns {Object|null} Workflow config or null
|
|
544
|
-
*/
|
|
545
|
-
export function getWorkflowConfig(workflowId, projectPath = '.') {
|
|
546
|
-
const workflows = loadWorkflowConfigs(projectPath);
|
|
547
|
-
const config = workflows.find(w => w.id === workflowId) || null;
|
|
548
|
-
|
|
549
|
-
if (config) {
|
|
550
|
-
const validPhases = getValidPhaseIds();
|
|
551
|
-
const declaredPhases = config.phases?.run || [];
|
|
552
|
-
for (const phase of declaredPhases) {
|
|
553
|
-
if (!validPhases.has(phase)) {
|
|
554
|
-
console.warn(`[workflow] Warning: workflow "${workflowId}" declares unknown phase "${phase}" — ignored`);
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
return config;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
/**
|
|
563
|
-
* List all available workflows
|
|
564
|
-
* @param {string} [projectPath='.'] - Project path
|
|
565
|
-
* @returns {Array<{id: string, name: string, description: string}>}
|
|
566
|
-
*/
|
|
567
|
-
export function listWorkflows(projectPath = '.') {
|
|
568
|
-
const workflows = loadWorkflowConfigs(projectPath);
|
|
569
|
-
return workflows.map(w => ({
|
|
570
|
-
id: w.id,
|
|
571
|
-
name: w.name,
|
|
572
|
-
description: w.description
|
|
573
|
-
}));
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
/**
|
|
577
|
-
* Check zero-touch eligibility for a feature (requires trust-manager)
|
|
578
|
-
* @param {string} feature
|
|
579
|
-
* @returns {Promise<boolean>}
|
|
580
|
-
*/
|
|
581
|
-
export async function checkZeroTouchEligibility(/* feature */) {
|
|
582
|
-
// v5: trust manager removed. The autonomy contract (MORPH.md) replaces it —
|
|
583
|
-
// Claude advances autonomously between gates. Returning false keeps any
|
|
584
|
-
// legacy callers happy without granting unintended privileges.
|
|
585
|
-
return false;
|
|
586
|
-
}
|