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.
Files changed (52) hide show
  1. package/README.md +256 -0
  2. package/bin/agent-method.js +58 -0
  3. package/docs/internal/feature-registry.yaml +1532 -0
  4. package/lib/cli/check.js +71 -0
  5. package/lib/cli/helpers.js +151 -0
  6. package/lib/cli/init.js +60 -0
  7. package/lib/cli/pipeline.js +163 -0
  8. package/lib/cli/refine.js +202 -0
  9. package/lib/cli/route.js +62 -0
  10. package/lib/cli/scan.js +28 -0
  11. package/lib/cli/status.js +61 -0
  12. package/lib/cli/upgrade.js +146 -0
  13. package/lib/init.js +240 -0
  14. package/lib/pipeline.js +887 -0
  15. package/lib/registry.js +108 -0
  16. package/package.json +39 -0
  17. package/templates/README.md +293 -0
  18. package/templates/entry-points/.cursorrules +109 -0
  19. package/templates/entry-points/AGENT.md +109 -0
  20. package/templates/entry-points/CLAUDE.md +109 -0
  21. package/templates/extensions/MANIFEST.md +110 -0
  22. package/templates/extensions/analytical-system.md +96 -0
  23. package/templates/extensions/code-project.md +77 -0
  24. package/templates/extensions/data-exploration.md +117 -0
  25. package/templates/full/.context/BASE.md +68 -0
  26. package/templates/full/.context/COMPOSITION.md +47 -0
  27. package/templates/full/.context/METHODOLOGY.md +84 -0
  28. package/templates/full/.context/REGISTRY.md +75 -0
  29. package/templates/full/.cursorrules +128 -0
  30. package/templates/full/AGENT.md +128 -0
  31. package/templates/full/CLAUDE.md +128 -0
  32. package/templates/full/PLAN.md +67 -0
  33. package/templates/full/PROJECT-PROFILE.md +61 -0
  34. package/templates/full/PROJECT.md +46 -0
  35. package/templates/full/REQUIREMENTS.md +30 -0
  36. package/templates/full/ROADMAP.md +39 -0
  37. package/templates/full/SESSION-LOG.md +41 -0
  38. package/templates/full/STATE.md +42 -0
  39. package/templates/full/SUMMARY.md +24 -0
  40. package/templates/full/docs/index.md +46 -0
  41. package/templates/full/todos/backlog.md +19 -0
  42. package/templates/starter/.context/BASE.md +66 -0
  43. package/templates/starter/.context/METHODOLOGY.md +70 -0
  44. package/templates/starter/.cursorrules +113 -0
  45. package/templates/starter/AGENT.md +113 -0
  46. package/templates/starter/CLAUDE.md +113 -0
  47. package/templates/starter/PLAN.md +67 -0
  48. package/templates/starter/PROJECT-PROFILE.md +44 -0
  49. package/templates/starter/PROJECT.md +46 -0
  50. package/templates/starter/ROADMAP.md +39 -0
  51. package/templates/starter/SESSION-LOG.md +41 -0
  52. package/templates/starter/STATE.md +42 -0
@@ -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
+ }