agent-method 1.5.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/README.md +256 -0
- package/bin/agent-method.js +58 -0
- package/docs/internal/feature-registry.yaml +1532 -0
- package/lib/cli/check.js +71 -0
- package/lib/cli/helpers.js +151 -0
- package/lib/cli/init.js +60 -0
- package/lib/cli/pipeline.js +163 -0
- package/lib/cli/refine.js +202 -0
- package/lib/cli/route.js +62 -0
- package/lib/cli/scan.js +28 -0
- package/lib/cli/status.js +61 -0
- package/lib/cli/upgrade.js +146 -0
- package/lib/init.js +240 -0
- package/lib/pipeline.js +887 -0
- package/lib/registry.js +108 -0
- package/package.json +39 -0
- package/templates/README.md +293 -0
- package/templates/entry-points/.cursorrules +109 -0
- package/templates/entry-points/AGENT.md +109 -0
- package/templates/entry-points/CLAUDE.md +109 -0
- package/templates/extensions/MANIFEST.md +110 -0
- package/templates/extensions/analytical-system.md +96 -0
- package/templates/extensions/code-project.md +77 -0
- package/templates/extensions/data-exploration.md +117 -0
- package/templates/full/.context/BASE.md +68 -0
- package/templates/full/.context/COMPOSITION.md +47 -0
- package/templates/full/.context/METHODOLOGY.md +84 -0
- package/templates/full/.context/REGISTRY.md +75 -0
- package/templates/full/.cursorrules +128 -0
- package/templates/full/AGENT.md +128 -0
- package/templates/full/CLAUDE.md +128 -0
- package/templates/full/PLAN.md +67 -0
- package/templates/full/PROJECT-PROFILE.md +61 -0
- package/templates/full/PROJECT.md +46 -0
- package/templates/full/REQUIREMENTS.md +30 -0
- package/templates/full/ROADMAP.md +39 -0
- package/templates/full/SESSION-LOG.md +41 -0
- package/templates/full/STATE.md +42 -0
- package/templates/full/SUMMARY.md +24 -0
- package/templates/full/docs/index.md +46 -0
- package/templates/full/todos/backlog.md +19 -0
- package/templates/starter/.context/BASE.md +66 -0
- package/templates/starter/.context/METHODOLOGY.md +70 -0
- package/templates/starter/.cursorrules +113 -0
- package/templates/starter/AGENT.md +113 -0
- package/templates/starter/CLAUDE.md +113 -0
- package/templates/starter/PLAN.md +67 -0
- package/templates/starter/PROJECT-PROFILE.md +44 -0
- package/templates/starter/PROJECT.md +46 -0
- package/templates/starter/ROADMAP.md +39 -0
- package/templates/starter/SESSION-LOG.md +41 -0
- package/templates/starter/STATE.md +42 -0
package/lib/pipeline.js
ADDED
|
@@ -0,0 +1,887 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 8-stage pipeline — Node.js port of agent_method/pipeline.py.
|
|
3
|
+
*
|
|
4
|
+
* Stages:
|
|
5
|
+
* S1 classify Query classification against registry patterns
|
|
6
|
+
* S2 selectWorkflow Workflow selection from query type
|
|
7
|
+
* S3 resolveFeatures Feature resolution for workflow + stage
|
|
8
|
+
* S4 computeFileSets Aggregate read/write sets from features
|
|
9
|
+
* S5 resolveCascade Cascade chain from triggers
|
|
10
|
+
* S6 generateEntryPoint Entry point specification
|
|
11
|
+
* S7 validateEntryPoint Entry point validation
|
|
12
|
+
* S8 detectProjectType Project type detection
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
16
|
+
import { resolve, join, extname, basename } from "node:path";
|
|
17
|
+
import { existsSync } from "node:fs";
|
|
18
|
+
import yaml from "js-yaml";
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
getActivation,
|
|
22
|
+
getDirectives,
|
|
23
|
+
getFeatures,
|
|
24
|
+
getQueryPatterns,
|
|
25
|
+
getWorkflow,
|
|
26
|
+
getWorkflows,
|
|
27
|
+
getVersion,
|
|
28
|
+
} from "./registry.js";
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Helpers
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
function dedupe(items) {
|
|
35
|
+
const seen = new Set();
|
|
36
|
+
const result = [];
|
|
37
|
+
for (const item of items) {
|
|
38
|
+
if (!seen.has(item)) {
|
|
39
|
+
seen.add(item);
|
|
40
|
+
result.push(item);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// S1: Query Classification
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
export function classify(query, projectType, registry) {
|
|
51
|
+
const queryLower = query.toLowerCase();
|
|
52
|
+
const patterns = getQueryPatterns(registry);
|
|
53
|
+
|
|
54
|
+
const scores = [];
|
|
55
|
+
for (const pat of patterns) {
|
|
56
|
+
const patTypes = pat.project_types || ["universal"];
|
|
57
|
+
if (!patTypes.includes("universal") && !patTypes.includes(projectType)) {
|
|
58
|
+
if (projectType !== "mixed") continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const rules = pat.match_rules || {};
|
|
62
|
+
const phrases = (rules.phrases || []).filter((p) => p != null);
|
|
63
|
+
const keywords = (rules.keywords || []).filter((k) => k != null);
|
|
64
|
+
|
|
65
|
+
const phraseHits = phrases.filter((p) => queryLower.includes(p.toLowerCase()));
|
|
66
|
+
const keywordHits = keywords.filter((k) => queryLower.includes(k.toLowerCase()));
|
|
67
|
+
|
|
68
|
+
const phraseScore = phraseHits.reduce((sum, p) => sum + p.length, 0);
|
|
69
|
+
const score = phraseScore * 10 + keywordHits.length;
|
|
70
|
+
if (score > 0) {
|
|
71
|
+
const isSpecific = !patTypes.includes("universal");
|
|
72
|
+
scores.push({
|
|
73
|
+
query_type: pat.query_type,
|
|
74
|
+
score,
|
|
75
|
+
phrase_hits: phraseHits,
|
|
76
|
+
keyword_hits: keywordHits,
|
|
77
|
+
matched: [...phraseHits, ...keywordHits],
|
|
78
|
+
is_specific: isSpecific,
|
|
79
|
+
workflow: pat.workflow || "WF-01",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (scores.length === 0) {
|
|
85
|
+
return {
|
|
86
|
+
query_type: "general_task",
|
|
87
|
+
confidence: "low",
|
|
88
|
+
matched: [],
|
|
89
|
+
source: "default",
|
|
90
|
+
workflow: "WF-01",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
scores.sort((a, b) => {
|
|
95
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
96
|
+
return (b.is_specific ? 1 : 0) - (a.is_specific ? 1 : 0);
|
|
97
|
+
});
|
|
98
|
+
let best = scores[0];
|
|
99
|
+
|
|
100
|
+
let confidence;
|
|
101
|
+
if (best.phrase_hits.length > 0 || best.keyword_hits.length >= 2) {
|
|
102
|
+
confidence = "high";
|
|
103
|
+
} else if (best.keyword_hits.length === 1) {
|
|
104
|
+
confidence = "medium";
|
|
105
|
+
} else {
|
|
106
|
+
confidence = "low";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (scores.length > 1 && scores[0].score === scores[1].score) {
|
|
110
|
+
if (scores[0].is_specific && !scores[1].is_specific) {
|
|
111
|
+
// keep best
|
|
112
|
+
} else if (!scores[0].is_specific && scores[1].is_specific) {
|
|
113
|
+
best = scores[1];
|
|
114
|
+
} else {
|
|
115
|
+
confidence = "low";
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
query_type: best.query_type,
|
|
121
|
+
confidence,
|
|
122
|
+
matched: best.matched,
|
|
123
|
+
source: "registry",
|
|
124
|
+
workflow: best.workflow,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// S2: Workflow Selection
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
const QUERY_WORKFLOW_MAP = {
|
|
133
|
+
planning: "WF-01",
|
|
134
|
+
context_refresh: "WF-03",
|
|
135
|
+
phase_completion: "WF-01",
|
|
136
|
+
backlog: "WF-01",
|
|
137
|
+
general_task: "WF-01",
|
|
138
|
+
code_change: "WF-02",
|
|
139
|
+
bug_fix: "WF-02",
|
|
140
|
+
dependency_update: "WF-02",
|
|
141
|
+
database_work: "WF-02",
|
|
142
|
+
api_work: "WF-02",
|
|
143
|
+
deployment_work: "WF-02",
|
|
144
|
+
data_ingest: "WF-05",
|
|
145
|
+
schema_query: "WF-05",
|
|
146
|
+
explore_entity: "WF-05",
|
|
147
|
+
relationship_query: "WF-05",
|
|
148
|
+
quality_check: "WF-05",
|
|
149
|
+
analytical_query: "WF-05",
|
|
150
|
+
document_search: "WF-05",
|
|
151
|
+
dimension_management: "WF-05",
|
|
152
|
+
add_reference: "WF-05",
|
|
153
|
+
chain_work: "WF-06",
|
|
154
|
+
evaluation: "WF-06",
|
|
155
|
+
composition: "WF-06",
|
|
156
|
+
domain_research: "WF-06",
|
|
157
|
+
spec_writing: "WF-07",
|
|
158
|
+
cross_reference: "WF-07",
|
|
159
|
+
project_discovery: "WF-08",
|
|
160
|
+
dependency_analysis: "WF-08",
|
|
161
|
+
pattern_analysis: "WF-08",
|
|
162
|
+
debt_assessment: "WF-08",
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const WORKFLOW_NAMES = {
|
|
166
|
+
"WF-01": "standard-task",
|
|
167
|
+
"WF-02": "code-change",
|
|
168
|
+
"WF-03": "context-refresh",
|
|
169
|
+
"WF-04": "bootstrap",
|
|
170
|
+
"WF-05": "data-exploration",
|
|
171
|
+
"WF-06": "analytical-system",
|
|
172
|
+
"WF-07": "specification-project",
|
|
173
|
+
"WF-08": "discovery",
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
export function selectWorkflow(queryType, projectType, isFirstSession = false) {
|
|
177
|
+
if (isFirstSession) {
|
|
178
|
+
return { workflow_id: "WF-04", workflow_name: "bootstrap" };
|
|
179
|
+
}
|
|
180
|
+
const wfId = QUERY_WORKFLOW_MAP[queryType] || "WF-01";
|
|
181
|
+
return {
|
|
182
|
+
workflow_id: wfId,
|
|
183
|
+
workflow_name: WORKFLOW_NAMES[wfId] || "standard-task",
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// S3: Feature Resolution
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
export function resolveFeatures(workflowId, stage, registry) {
|
|
192
|
+
const activation = getActivation(registry);
|
|
193
|
+
const workflow = getWorkflow(registry, workflowId);
|
|
194
|
+
|
|
195
|
+
const wfFeatures = [];
|
|
196
|
+
if (workflow) {
|
|
197
|
+
for (const step of workflow.steps || []) {
|
|
198
|
+
if (step.stage === stage) {
|
|
199
|
+
wfFeatures.push(...(step.features || []));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const actFeatures = activation[stage] || [];
|
|
205
|
+
|
|
206
|
+
let merged, source;
|
|
207
|
+
if (wfFeatures.length > 0 && actFeatures.length > 0) {
|
|
208
|
+
merged = dedupe([...wfFeatures, ...actFeatures]);
|
|
209
|
+
source = "merged";
|
|
210
|
+
} else if (wfFeatures.length > 0) {
|
|
211
|
+
merged = dedupe(wfFeatures);
|
|
212
|
+
source = "workflow";
|
|
213
|
+
} else if (actFeatures.length > 0) {
|
|
214
|
+
merged = [...actFeatures];
|
|
215
|
+
source = "activation_map";
|
|
216
|
+
} else {
|
|
217
|
+
merged = [];
|
|
218
|
+
source = "none";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return { features: merged, source };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// S4: File Set Computation
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
export function computeFileSets(features, queryType, registry) {
|
|
229
|
+
const catalog = getFeatures(registry);
|
|
230
|
+
const readSet = [];
|
|
231
|
+
const writeSet = [];
|
|
232
|
+
for (const fid of features) {
|
|
233
|
+
const feat = catalog[fid] || {};
|
|
234
|
+
readSet.push(...(feat.reads || []));
|
|
235
|
+
writeSet.push(...(feat.writes || []));
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
read_set: dedupe(readSet),
|
|
239
|
+
write_set: dedupe(writeSet),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// S5: Cascade Resolution
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
const UNIVERSAL_CASCADE = {
|
|
248
|
+
phase_completion: ["SUMMARY.md", "STATE.md", "ROADMAP.md"],
|
|
249
|
+
requirements_change: ["REQUIREMENTS.md", "ROADMAP.md", "PLAN.md"],
|
|
250
|
+
new_decision: ["STATE.md"],
|
|
251
|
+
open_question_resolved: ["STATE.md"],
|
|
252
|
+
project_structure: [".context/BASE.md"],
|
|
253
|
+
new_domain: [".context/BASE.md"],
|
|
254
|
+
file_exceeds_300_lines: [],
|
|
255
|
+
file_split: [".context/REGISTRY.md"],
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const CODE_CASCADE = {
|
|
259
|
+
database_schema: [".context/DATABASE.md"],
|
|
260
|
+
api_route: [".context/API.md"],
|
|
261
|
+
new_module: [".context/BASE.md"],
|
|
262
|
+
package_change: [".context/BASE.md"],
|
|
263
|
+
env_variable: [".context/BASE.md"],
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const DATA_CASCADE = {
|
|
267
|
+
new_data_source: [".context/BASE.md"],
|
|
268
|
+
schema_change: [
|
|
269
|
+
".context/SCHEMA.md",
|
|
270
|
+
".context/BASE.md",
|
|
271
|
+
".context/RELATIONSHIPS.md",
|
|
272
|
+
],
|
|
273
|
+
new_entity: [".context/BASE.md"],
|
|
274
|
+
new_document: [".context/DOCUMENTS.md", ".context/BASE.md"],
|
|
275
|
+
derived_metric: [".context/SCHEMA.md", ".context/BASE.md"],
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const ANALYTICAL_CASCADE = {
|
|
279
|
+
pipeline_stage: [".context/EXECUTION.md", ".context/BASE.md"],
|
|
280
|
+
evaluation_criteria: [".context/EVALUATION.md"],
|
|
281
|
+
prompt_template: [".context/COMPOSITION.md", ".context/EVALUATION.md"],
|
|
282
|
+
domain_knowledge: [".context/DOMAIN.md", ".context/COMPOSITION.md"],
|
|
283
|
+
orchestration_config: [".context/EXECUTION.md", ".context/BASE.md"],
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
export function resolveCascade(
|
|
287
|
+
writtenFiles,
|
|
288
|
+
trigger = null,
|
|
289
|
+
cascadeContext = "universal",
|
|
290
|
+
maxDepth = 2
|
|
291
|
+
) {
|
|
292
|
+
const table = { ...UNIVERSAL_CASCADE };
|
|
293
|
+
if (cascadeContext === "code" || cascadeContext === "mixed") {
|
|
294
|
+
Object.assign(table, CODE_CASCADE);
|
|
295
|
+
}
|
|
296
|
+
if (cascadeContext === "data" || cascadeContext === "mixed") {
|
|
297
|
+
Object.assign(table, DATA_CASCADE);
|
|
298
|
+
}
|
|
299
|
+
if (cascadeContext === "analytical" || cascadeContext === "mixed") {
|
|
300
|
+
Object.assign(table, ANALYTICAL_CASCADE);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const cascadeFiles = [];
|
|
304
|
+
if (trigger && table[trigger]) {
|
|
305
|
+
cascadeFiles.push(...table[trigger]);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const depth = cascadeFiles.length > 0 ? 1 : 0;
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
cascade_files: dedupe(cascadeFiles),
|
|
312
|
+
depth,
|
|
313
|
+
trigger,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
// S6: Entry Point Generation (summary mode)
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
export function generateEntryPoint(projectType, templateTier, registry) {
|
|
322
|
+
const patterns = getQueryPatterns(registry);
|
|
323
|
+
const directives = getDirectives(registry);
|
|
324
|
+
|
|
325
|
+
const applicablePatterns = [];
|
|
326
|
+
for (const pat of patterns) {
|
|
327
|
+
const pt = pat.project_types || ["universal"];
|
|
328
|
+
if (pt.includes("universal") || pt.includes(projectType) || projectType === "mixed") {
|
|
329
|
+
applicablePatterns.push(pat.query_type);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const applicableWorkflows = [];
|
|
334
|
+
for (const wf of getWorkflows(registry)) {
|
|
335
|
+
const wfTypes = wf.project_types || ["universal"];
|
|
336
|
+
if (
|
|
337
|
+
wfTypes.includes("universal") ||
|
|
338
|
+
wfTypes.includes(projectType) ||
|
|
339
|
+
projectType === "mixed"
|
|
340
|
+
) {
|
|
341
|
+
applicableWorkflows.push({ id: wf.id, name: wf.name });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const extensions = [];
|
|
346
|
+
if (projectType === "code" || projectType === "mixed")
|
|
347
|
+
extensions.push("code-project.md");
|
|
348
|
+
if (projectType === "data" || projectType === "mixed")
|
|
349
|
+
extensions.push("data-exploration.md");
|
|
350
|
+
if (projectType === "analytical" || projectType === "mixed")
|
|
351
|
+
extensions.push("analytical-system.md");
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
project_type: projectType,
|
|
355
|
+
template_tier: templateTier,
|
|
356
|
+
scoping_rules: {
|
|
357
|
+
query_types: applicablePatterns,
|
|
358
|
+
count: applicablePatterns.length,
|
|
359
|
+
},
|
|
360
|
+
cascade_table: {
|
|
361
|
+
universal_rules: Object.keys(UNIVERSAL_CASCADE).length,
|
|
362
|
+
extension_rules: extensions,
|
|
363
|
+
},
|
|
364
|
+
workflows: applicableWorkflows,
|
|
365
|
+
conventions: {
|
|
366
|
+
directives: directives.map((d) => d.id),
|
|
367
|
+
count: directives.length,
|
|
368
|
+
},
|
|
369
|
+
extensions,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ---------------------------------------------------------------------------
|
|
374
|
+
// S7: Entry Point Validation
|
|
375
|
+
// ---------------------------------------------------------------------------
|
|
376
|
+
|
|
377
|
+
export function validateEntryPoint(entryPointPath, projectType, registry) {
|
|
378
|
+
let content;
|
|
379
|
+
try {
|
|
380
|
+
content = readFileSync(resolve(entryPointPath), "utf-8");
|
|
381
|
+
} catch {
|
|
382
|
+
return {
|
|
383
|
+
valid: false,
|
|
384
|
+
checks: {},
|
|
385
|
+
issues: [
|
|
386
|
+
{
|
|
387
|
+
check: "FILE",
|
|
388
|
+
severity: "critical",
|
|
389
|
+
description: `File not found: ${entryPointPath}`,
|
|
390
|
+
},
|
|
391
|
+
],
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const contentLower = content.toLowerCase();
|
|
396
|
+
const checks = {};
|
|
397
|
+
const issues = [];
|
|
398
|
+
|
|
399
|
+
// V-01: Scoping coverage
|
|
400
|
+
const patterns = getQueryPatterns(registry);
|
|
401
|
+
const expectedTypes = new Set();
|
|
402
|
+
for (const pat of patterns) {
|
|
403
|
+
const pt = pat.project_types || ["universal"];
|
|
404
|
+
if (pt.includes("universal") || pt.includes(projectType) || projectType === "mixed") {
|
|
405
|
+
expectedTypes.add(pat.query_type);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const typeLabels = {
|
|
410
|
+
planning: "planning",
|
|
411
|
+
context_refresh: "context refresh",
|
|
412
|
+
phase_completion: "phase completion",
|
|
413
|
+
backlog: "backlog",
|
|
414
|
+
code_change: "code change",
|
|
415
|
+
bug_fix: "bug fix",
|
|
416
|
+
data_ingest: "data ingest",
|
|
417
|
+
schema_query: "schema",
|
|
418
|
+
chain_work: "chain",
|
|
419
|
+
evaluation: "evaluation",
|
|
420
|
+
composition: "composition",
|
|
421
|
+
domain_research: "domain research",
|
|
422
|
+
project_discovery: "project discovery",
|
|
423
|
+
spec_writing: "spec",
|
|
424
|
+
};
|
|
425
|
+
const missingScoping = [];
|
|
426
|
+
for (const qt of expectedTypes) {
|
|
427
|
+
const label = typeLabels[qt] || qt.replace(/_/g, " ");
|
|
428
|
+
if (!contentLower.includes(label)) {
|
|
429
|
+
missingScoping.push(qt);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
checks["V-01_scoping_coverage"] = {
|
|
433
|
+
pass: missingScoping.length === 0,
|
|
434
|
+
missing: missingScoping,
|
|
435
|
+
};
|
|
436
|
+
if (missingScoping.length > 0) {
|
|
437
|
+
issues.push({
|
|
438
|
+
check: "V-01",
|
|
439
|
+
severity: "high",
|
|
440
|
+
description: `Missing scoping rules for: ${missingScoping.join(", ")}`,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// V-04: Cascade completeness
|
|
445
|
+
const cascadeKeywords = [
|
|
446
|
+
"phase completion",
|
|
447
|
+
"requirements",
|
|
448
|
+
"decision",
|
|
449
|
+
"structure",
|
|
450
|
+
"domain",
|
|
451
|
+
"300 line",
|
|
452
|
+
"split",
|
|
453
|
+
];
|
|
454
|
+
const missingCascade = cascadeKeywords.filter((k) => !contentLower.includes(k));
|
|
455
|
+
checks["V-04_cascade_completeness"] = {
|
|
456
|
+
pass: missingCascade.length <= 1,
|
|
457
|
+
missing: missingCascade,
|
|
458
|
+
};
|
|
459
|
+
if (missingCascade.length > 1) {
|
|
460
|
+
issues.push({
|
|
461
|
+
check: "V-04",
|
|
462
|
+
severity: "medium",
|
|
463
|
+
description: `Potentially missing cascade rules for: ${missingCascade.join(", ")}`,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// V-05: Convention coverage
|
|
468
|
+
const directiveThemes = {
|
|
469
|
+
"P-01": "understanding",
|
|
470
|
+
"P-02": "future",
|
|
471
|
+
"P-03": "cascade",
|
|
472
|
+
"P-04": "scope",
|
|
473
|
+
"P-05": "uncertain",
|
|
474
|
+
"P-06": "human",
|
|
475
|
+
"P-07": "300",
|
|
476
|
+
};
|
|
477
|
+
const missingDirectives = Object.entries(directiveThemes)
|
|
478
|
+
.filter(([, theme]) => !contentLower.includes(theme))
|
|
479
|
+
.map(([pid]) => pid);
|
|
480
|
+
checks["V-05_convention_coverage"] = {
|
|
481
|
+
pass: missingDirectives.length === 0,
|
|
482
|
+
missing: missingDirectives,
|
|
483
|
+
};
|
|
484
|
+
if (missingDirectives.length > 0) {
|
|
485
|
+
issues.push({
|
|
486
|
+
check: "V-05",
|
|
487
|
+
severity: "medium",
|
|
488
|
+
description: `Convention themes missing for: ${missingDirectives.join(", ")}`,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// V-06: Workflow coverage
|
|
493
|
+
const gen = generateEntryPoint(projectType, "starter", registry);
|
|
494
|
+
const expectedWfs = gen.workflows.map((wf) => wf.name);
|
|
495
|
+
const missingWfs = expectedWfs.filter(
|
|
496
|
+
(wf) => !contentLower.includes(wf.replace(/-/g, " ")) && !contentLower.includes(wf)
|
|
497
|
+
);
|
|
498
|
+
checks["V-06_workflow_coverage"] = {
|
|
499
|
+
pass: missingWfs.length === 0,
|
|
500
|
+
missing: missingWfs,
|
|
501
|
+
};
|
|
502
|
+
if (missingWfs.length > 0) {
|
|
503
|
+
issues.push({
|
|
504
|
+
check: "V-06",
|
|
505
|
+
severity: "medium",
|
|
506
|
+
description: `Missing workflow references: ${missingWfs.join(", ")}`,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// V-07: Specialist pairing
|
|
511
|
+
const specialistPattern = content.match(
|
|
512
|
+
/\|[^|]*\.context\/\w+\.md[^|]*\.context\/\w+\.md/g
|
|
513
|
+
);
|
|
514
|
+
const violations = specialistPattern ? specialistPattern.length : 0;
|
|
515
|
+
checks["V-07_specialist_pairing"] = {
|
|
516
|
+
pass: violations === 0,
|
|
517
|
+
violations,
|
|
518
|
+
};
|
|
519
|
+
if (violations > 0) {
|
|
520
|
+
issues.push({
|
|
521
|
+
check: "V-07",
|
|
522
|
+
severity: "high",
|
|
523
|
+
description: `Found ${violations} scoping row(s) loading two specialists`,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// V-08: Scale management
|
|
528
|
+
checks["V-08_scale_management"] = { pass: content.includes("300") };
|
|
529
|
+
|
|
530
|
+
const valid = Object.values(checks).every((c) => c.pass);
|
|
531
|
+
|
|
532
|
+
return {
|
|
533
|
+
valid,
|
|
534
|
+
entry_point: entryPointPath,
|
|
535
|
+
project_type: projectType,
|
|
536
|
+
registry_version: getVersion(registry),
|
|
537
|
+
checks,
|
|
538
|
+
issues,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// ---------------------------------------------------------------------------
|
|
543
|
+
// S8: Project Type Detection
|
|
544
|
+
// ---------------------------------------------------------------------------
|
|
545
|
+
|
|
546
|
+
const CODE_INDICATORS = {
|
|
547
|
+
high: [
|
|
548
|
+
"package.json", "Cargo.toml", "go.mod", "pom.xml",
|
|
549
|
+
"requirements.txt", "setup.py", "Gemfile", "build.gradle",
|
|
550
|
+
"pyproject.toml", "Makefile", "CMakeLists.txt",
|
|
551
|
+
],
|
|
552
|
+
medium: [
|
|
553
|
+
"src/", "lib/", "app/", "test/", "tests/",
|
|
554
|
+
"Dockerfile", ".github/", "terraform/",
|
|
555
|
+
],
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
const DATA_INDICATORS = {
|
|
559
|
+
high: [],
|
|
560
|
+
medium: ["data/", "datasets/", "raw/", "schemas/"],
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
const ANALYTICAL_INDICATORS = {
|
|
564
|
+
high: ["prompts/", "chains/", "evaluation/"],
|
|
565
|
+
medium: [
|
|
566
|
+
"pipeline/", "scoring/", "runs/", "compositions/",
|
|
567
|
+
"analysis/", "research/", "experiments/",
|
|
568
|
+
],
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
const CONTEXT_BROWNFIELD_INDICATORS = [
|
|
572
|
+
"STATE.md", ".context/", "CLAUDE.md", ".cursorrules", "AGENT.md",
|
|
573
|
+
"ROADMAP.md", "PLAN.md", "SESSION-LOG.md",
|
|
574
|
+
];
|
|
575
|
+
|
|
576
|
+
const DATA_EXTENSIONS = new Set([
|
|
577
|
+
".csv", ".parquet", ".xlsx", ".jsonl", ".tsv", ".arrow",
|
|
578
|
+
]);
|
|
579
|
+
|
|
580
|
+
export function detectProjectType(directory = ".") {
|
|
581
|
+
const dirPath = resolve(directory);
|
|
582
|
+
if (!existsSync(dirPath)) {
|
|
583
|
+
return {
|
|
584
|
+
project_type: "general",
|
|
585
|
+
confidence: "low",
|
|
586
|
+
indicators: [],
|
|
587
|
+
error: `Directory not found: ${directory}`,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const contents = [];
|
|
592
|
+
try {
|
|
593
|
+
for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
|
|
594
|
+
const name = entry.name;
|
|
595
|
+
if (name.startsWith(".") && name !== ".github" && name !== ".cursorrules")
|
|
596
|
+
continue;
|
|
597
|
+
contents.push(name + (entry.isDirectory() ? "/" : ""));
|
|
598
|
+
}
|
|
599
|
+
} catch {
|
|
600
|
+
// PermissionError
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const dataFiles = [];
|
|
604
|
+
function scanForDataFiles(dir, depth = 0) {
|
|
605
|
+
if (depth > 3 || dataFiles.length >= 5) return;
|
|
606
|
+
try {
|
|
607
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
608
|
+
if (dataFiles.length >= 5) return;
|
|
609
|
+
const fullPath = join(dir, entry.name);
|
|
610
|
+
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
611
|
+
scanForDataFiles(fullPath, depth + 1);
|
|
612
|
+
} else if (DATA_EXTENSIONS.has(extname(entry.name).toLowerCase())) {
|
|
613
|
+
dataFiles.push(entry.name);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
} catch {
|
|
617
|
+
// PermissionError or OSError
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
scanForDataFiles(dirPath);
|
|
621
|
+
|
|
622
|
+
const scores = { code: 0, data: 0, analytical: 0 };
|
|
623
|
+
const indicators = { code: [], data: [], analytical: [] };
|
|
624
|
+
|
|
625
|
+
for (const item of contents) {
|
|
626
|
+
const itemBase = item.replace(/\/$/, "");
|
|
627
|
+
const itemDir = itemBase + "/";
|
|
628
|
+
|
|
629
|
+
// Code indicators
|
|
630
|
+
if (CODE_INDICATORS.high.includes(itemBase)) {
|
|
631
|
+
scores.code += 3;
|
|
632
|
+
indicators.code.push(itemBase);
|
|
633
|
+
} else if (
|
|
634
|
+
CODE_INDICATORS.medium.includes(item) ||
|
|
635
|
+
CODE_INDICATORS.medium.includes(itemDir)
|
|
636
|
+
) {
|
|
637
|
+
scores.code += 1;
|
|
638
|
+
indicators.code.push(itemBase);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Data indicators
|
|
642
|
+
if ((DATA_INDICATORS.high || []).includes(itemBase)) {
|
|
643
|
+
scores.data += 3;
|
|
644
|
+
indicators.data.push(itemBase);
|
|
645
|
+
} else if (
|
|
646
|
+
DATA_INDICATORS.medium.includes(item) ||
|
|
647
|
+
DATA_INDICATORS.medium.includes(itemDir)
|
|
648
|
+
) {
|
|
649
|
+
scores.data += 1;
|
|
650
|
+
indicators.data.push(itemBase);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Analytical indicators
|
|
654
|
+
if (
|
|
655
|
+
ANALYTICAL_INDICATORS.high.includes(item) ||
|
|
656
|
+
ANALYTICAL_INDICATORS.high.includes(itemDir)
|
|
657
|
+
) {
|
|
658
|
+
scores.analytical += 3;
|
|
659
|
+
indicators.analytical.push(itemBase);
|
|
660
|
+
} else if (
|
|
661
|
+
ANALYTICAL_INDICATORS.medium.includes(item) ||
|
|
662
|
+
ANALYTICAL_INDICATORS.medium.includes(itemDir)
|
|
663
|
+
) {
|
|
664
|
+
scores.analytical += 1;
|
|
665
|
+
indicators.analytical.push(itemBase);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Notebooks boost both data and analytical
|
|
669
|
+
if (itemBase.endsWith(".ipynb")) {
|
|
670
|
+
scores.data += 1;
|
|
671
|
+
scores.analytical += 1;
|
|
672
|
+
indicators.data.push(itemBase);
|
|
673
|
+
indicators.analytical.push(itemBase);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Brownfield methodology indicators boost analytical detection
|
|
678
|
+
let brownfieldCount = 0;
|
|
679
|
+
for (const bf of CONTEXT_BROWNFIELD_INDICATORS) {
|
|
680
|
+
const bfBase = bf.replace(/\/$/, "");
|
|
681
|
+
if (contents.includes(bfBase + "/") || contents.includes(bfBase)) {
|
|
682
|
+
brownfieldCount++;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (brownfieldCount >= 2 && scores.code === 0) {
|
|
686
|
+
scores.analytical += 2;
|
|
687
|
+
indicators.analytical.push("methodology-files");
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (dataFiles.length > 0) {
|
|
691
|
+
scores.data += 3;
|
|
692
|
+
indicators.data.push(...dataFiles.slice(0, 3));
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const maxScore = Math.max(...Object.values(scores));
|
|
696
|
+
if (maxScore === 0) {
|
|
697
|
+
return {
|
|
698
|
+
project_type: "general",
|
|
699
|
+
confidence: "low",
|
|
700
|
+
indicators: [],
|
|
701
|
+
recommended_extensions: [],
|
|
702
|
+
recommended_specialists: [],
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const highTypes = Object.entries(scores)
|
|
707
|
+
.filter(([, s]) => s >= maxScore * 0.6 && s > 0)
|
|
708
|
+
.map(([t]) => t);
|
|
709
|
+
|
|
710
|
+
let projectType, confidence, allIndicators;
|
|
711
|
+
if (highTypes.length > 1) {
|
|
712
|
+
projectType = "mixed";
|
|
713
|
+
confidence = "medium";
|
|
714
|
+
allIndicators = [];
|
|
715
|
+
for (const t of highTypes) {
|
|
716
|
+
allIndicators.push(...indicators[t]);
|
|
717
|
+
}
|
|
718
|
+
} else {
|
|
719
|
+
projectType = Object.entries(scores).reduce((a, b) =>
|
|
720
|
+
b[1] > a[1] ? b : a
|
|
721
|
+
)[0];
|
|
722
|
+
confidence = maxScore >= 3 ? "high" : "medium";
|
|
723
|
+
allIndicators = indicators[projectType];
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const extensions = [];
|
|
727
|
+
const specialists = [];
|
|
728
|
+
if (projectType === "code" || projectType === "mixed") {
|
|
729
|
+
extensions.push("code-project.md");
|
|
730
|
+
specialists.push("API.md", "DATABASE.md", "TESTING.md", "INFRASTRUCTURE.md");
|
|
731
|
+
}
|
|
732
|
+
if (projectType === "data" || projectType === "mixed") {
|
|
733
|
+
extensions.push("data-exploration.md");
|
|
734
|
+
specialists.push("SCHEMA.md", "DOCUMENTS.md", "RELATIONSHIPS.md");
|
|
735
|
+
}
|
|
736
|
+
if (projectType === "analytical" || projectType === "mixed") {
|
|
737
|
+
extensions.push("analytical-system.md");
|
|
738
|
+
specialists.push("COMPOSITION.md", "EVALUATION.md", "EXECUTION.md", "DOMAIN.md");
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
return {
|
|
742
|
+
project_type: projectType,
|
|
743
|
+
confidence,
|
|
744
|
+
indicators: dedupe(allIndicators),
|
|
745
|
+
scores,
|
|
746
|
+
recommended_extensions: extensions,
|
|
747
|
+
recommended_specialists: specialists,
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// ---------------------------------------------------------------------------
|
|
752
|
+
// Route: End-to-end pipeline (S1 -> S2 -> S3 -> S4)
|
|
753
|
+
// ---------------------------------------------------------------------------
|
|
754
|
+
|
|
755
|
+
export function route(query, projectType, stage, registry, isFirstSession = false) {
|
|
756
|
+
const classification = classify(query, projectType, registry);
|
|
757
|
+
const selection = selectWorkflow(
|
|
758
|
+
classification.query_type,
|
|
759
|
+
projectType,
|
|
760
|
+
isFirstSession
|
|
761
|
+
);
|
|
762
|
+
const resolution = resolveFeatures(selection.workflow_id, stage, registry);
|
|
763
|
+
const fileSets = computeFileSets(
|
|
764
|
+
resolution.features,
|
|
765
|
+
classification.query_type,
|
|
766
|
+
registry
|
|
767
|
+
);
|
|
768
|
+
return {
|
|
769
|
+
query,
|
|
770
|
+
project_type: projectType,
|
|
771
|
+
stage,
|
|
772
|
+
S1_classify: classification,
|
|
773
|
+
S2_select: selection,
|
|
774
|
+
S3_resolve: resolution,
|
|
775
|
+
S4_compute: fileSets,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// ---------------------------------------------------------------------------
|
|
780
|
+
// Test runner
|
|
781
|
+
// ---------------------------------------------------------------------------
|
|
782
|
+
|
|
783
|
+
export function runFixture(fixturePath, registry) {
|
|
784
|
+
let content = readFileSync(resolve(fixturePath), "utf-8");
|
|
785
|
+
content = content.replace(/\{([^}:]+)\}/g, "_$1_");
|
|
786
|
+
|
|
787
|
+
let fixture;
|
|
788
|
+
try {
|
|
789
|
+
fixture = yaml.load(content);
|
|
790
|
+
} catch (e) {
|
|
791
|
+
return {
|
|
792
|
+
stage: basename(fixturePath, extname(fixturePath)),
|
|
793
|
+
total: 0,
|
|
794
|
+
passed: 0,
|
|
795
|
+
failed: 1,
|
|
796
|
+
details: [
|
|
797
|
+
{ id: "YAML", description: String(e), status: "FAIL", actual: null },
|
|
798
|
+
],
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const metadata = fixture.metadata || {};
|
|
803
|
+
const stageName = metadata.name || "unknown";
|
|
804
|
+
const cases = fixture.cases || [];
|
|
805
|
+
const results = {
|
|
806
|
+
stage: stageName,
|
|
807
|
+
total: cases.length,
|
|
808
|
+
passed: 0,
|
|
809
|
+
failed: 0,
|
|
810
|
+
details: [],
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
for (const testCase of cases) {
|
|
814
|
+
const caseId = testCase.id || "?";
|
|
815
|
+
const desc = testCase.description || "";
|
|
816
|
+
const inp = testCase.input || {};
|
|
817
|
+
const expected = testCase.expected || {};
|
|
818
|
+
|
|
819
|
+
let actual, passed;
|
|
820
|
+
try {
|
|
821
|
+
if (stageName === "classify") {
|
|
822
|
+
actual = classify(
|
|
823
|
+
inp.query || "",
|
|
824
|
+
inp.project_type || "general",
|
|
825
|
+
registry
|
|
826
|
+
);
|
|
827
|
+
passed =
|
|
828
|
+
actual.query_type === expected.query_type &&
|
|
829
|
+
actual.confidence === expected.confidence;
|
|
830
|
+
} else if (stageName === "select") {
|
|
831
|
+
actual = selectWorkflow(
|
|
832
|
+
inp.query_type || "",
|
|
833
|
+
inp.project_type || "general",
|
|
834
|
+
inp.is_first_session || false
|
|
835
|
+
);
|
|
836
|
+
passed = actual.workflow_id === expected.workflow_id;
|
|
837
|
+
} else if (stageName === "resolve") {
|
|
838
|
+
actual = resolveFeatures(
|
|
839
|
+
inp.workflow_id || "",
|
|
840
|
+
inp.stage || "",
|
|
841
|
+
registry
|
|
842
|
+
);
|
|
843
|
+
passed =
|
|
844
|
+
JSON.stringify(actual.features) ===
|
|
845
|
+
JSON.stringify(expected.features || []);
|
|
846
|
+
} else if (stageName === "cascade") {
|
|
847
|
+
actual = resolveCascade(
|
|
848
|
+
inp.written_files || [],
|
|
849
|
+
inp.trigger,
|
|
850
|
+
inp.cascade_context || "universal"
|
|
851
|
+
);
|
|
852
|
+
const actualSet = new Set(actual.cascade_files);
|
|
853
|
+
const expectedSet = new Set(expected.cascade_files || []);
|
|
854
|
+
passed =
|
|
855
|
+
actualSet.size === expectedSet.size &&
|
|
856
|
+
[...actualSet].every((f) => expectedSet.has(f));
|
|
857
|
+
} else if (stageName === "detect") {
|
|
858
|
+
actual = { note: "Detection requires simulated directory" };
|
|
859
|
+
passed = true;
|
|
860
|
+
} else {
|
|
861
|
+
actual = { note: `No test runner for stage: ${stageName}` };
|
|
862
|
+
passed = true;
|
|
863
|
+
}
|
|
864
|
+
} catch (e) {
|
|
865
|
+
actual = { error: String(e) };
|
|
866
|
+
passed = false;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const status = passed ? "PASS" : "FAIL";
|
|
870
|
+
if (passed) results.passed++;
|
|
871
|
+
else results.failed++;
|
|
872
|
+
|
|
873
|
+
results.details.push({
|
|
874
|
+
id: caseId,
|
|
875
|
+
description: desc,
|
|
876
|
+
status,
|
|
877
|
+
expected_type:
|
|
878
|
+
expected.query_type ||
|
|
879
|
+
expected.workflow_id ||
|
|
880
|
+
expected.features ||
|
|
881
|
+
"",
|
|
882
|
+
actual: passed ? null : actual,
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
return results;
|
|
887
|
+
}
|