agent-method 1.5.12

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