@smartmemory/compose 0.2.51 → 0.2.53-beta

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 (143) hide show
  1. package/.claude/skills/compose/SKILL.md +0 -8
  2. package/README.md +11 -0
  3. package/bin/compose.js +335 -3
  4. package/dist/assets/App-Bu9KMtTa.js +891 -0
  5. package/dist/assets/abnfDiagram-VRR7QNED-6zp3w9rx.js +1 -0
  6. package/dist/assets/arc-CnLxxuah.js +1 -0
  7. package/dist/assets/architectureDiagram-ZJ3FMSHR-COM46S9k.js +36 -0
  8. package/dist/assets/blockDiagram-677ZJIJ3-wKzgOwF8.js +132 -0
  9. package/dist/assets/{browser-CnKiSnlr.js → browser-Cr0recrN.js} +6 -6
  10. package/dist/assets/{c4Diagram-AHTNJAMY-nBIRLJTv.js → c4Diagram-LMCZKHZV-DIXC_mR6.js} +1 -1
  11. package/dist/assets/channel-DQJPWkb_.js +1 -0
  12. package/dist/assets/{chunk-QZHKN3VN-B4Wfybww.js → chunk-2Q5K7J3B-BqLqZX6m.js} +1 -1
  13. package/dist/assets/{chunk-YZCP3GAM-DE6_5aqh.js → chunk-32BRIVSS-CarSmVlX.js} +1 -1
  14. package/dist/assets/{chunk-FMBD7UC4-tYY8xcjr.js → chunk-5VM5RSS4-D-f_Jn3G.js} +1 -1
  15. package/dist/assets/chunk-EX3LRPZG-DT42xo8E.js +231 -0
  16. package/dist/assets/{chunk-4BX2VUAB-CNmGyhrp.js → chunk-JWPE2WC7-CwY1aZc4.js} +1 -1
  17. package/dist/assets/chunk-MOJQB5TN-DBbZU_KX.js +88 -0
  18. package/dist/assets/chunk-RYQCIY6F-DqxtcLJ3.js +1 -0
  19. package/dist/assets/chunk-V7JOEXUC-Cy4Ixww6.js +206 -0
  20. package/dist/assets/{chunk-EDXVE4YY-TPlt1bS2.js → chunk-VR4S4FIN-CFexfU_a.js} +1 -1
  21. package/dist/assets/{chunk-55IACEB6-D8KuKVLa.js → chunk-XXDRQBXY-uE-zN_vc.js} +1 -1
  22. package/dist/assets/classDiagram-OUVF2IWQ-CTLbAiUK.js +1 -0
  23. package/dist/assets/classDiagram-v2-EOCWNBFH-CTLbAiUK.js +1 -0
  24. package/dist/assets/{cose-bilkent-S5V4N54A-DzzRyJtB.js → cose-bilkent-JH36ORCC-DIjpekos.js} +1 -1
  25. package/dist/assets/cynefin-VYW2F7L2-Ba2gbQow.js +178 -0
  26. package/dist/assets/cynefinDiagram-TSTJHNR4-njGzb7Tg.js +62 -0
  27. package/dist/assets/dagre-VKFMJZFB-DuyZREZ3.js +4 -0
  28. package/dist/assets/diagram-FQU43EPY-Npq-5o3a.js +3 -0
  29. package/dist/assets/diagram-G47NLZAW-fm4k3axC.js +24 -0
  30. package/dist/assets/diagram-NH7WQ7WH-B8EFGrTG.js +24 -0
  31. package/dist/assets/diagram-OA4YK3LP-BC7UHx9Q.js +30 -0
  32. package/dist/assets/diagram-WEI45ONY-BqjHLcCX.js +41 -0
  33. package/dist/assets/ebnfDiagram-CCIWWBDH-D5zXd4Qg.js +1 -0
  34. package/dist/assets/erDiagram-Q63AITRT-DC2FMcra.js +85 -0
  35. package/dist/assets/flowDiagram-23GEKE2U-D7Dx7JU4.js +156 -0
  36. package/dist/assets/ganttDiagram-NO4QXBWP-8sQH2y5K.js +292 -0
  37. package/dist/assets/gitGraphDiagram-IHSO6WYX-Ci_rhxur.js +106 -0
  38. package/dist/assets/graph-C9eacEi8.js +1 -0
  39. package/dist/assets/graph-xkel59g2.js +331 -0
  40. package/dist/assets/index-B7-HQenC.js +119 -0
  41. package/dist/assets/index-BxRamj_i.css +1 -0
  42. package/dist/assets/infoDiagram-FWYZ7A6U-B9FGQ0Cb.js +2 -0
  43. package/dist/assets/{ishikawaDiagram-UXIWVN3A-DWx-7BBy.js → ishikawaDiagram-FXEZZL3T-BEKLyH6A.js} +5 -5
  44. package/dist/assets/{journeyDiagram-VCZTEJTY-C42hXNha.js → journeyDiagram-5HDEW3XC-BM-IDVRo.js} +1 -1
  45. package/dist/assets/{kanban-definition-6JOO6SKY-CEq930ew.js → kanban-definition-HUTT4EX6-Beb2k3pt.js} +7 -7
  46. package/dist/assets/katex-C5jXJg4s.js +257 -0
  47. package/dist/assets/layout-DEXfKzaS.js +1 -0
  48. package/dist/assets/{linear-D158F7OT.js → linear-Dwg7dTpz.js} +1 -1
  49. package/dist/assets/map-Czzmt4hB.js +1 -0
  50. package/dist/assets/{mindmap-definition-QFDTVHPH-d9SSh2nG.js → mindmap-definition-LN4V7U3C-CeaynfcE.js} +7 -7
  51. package/dist/assets/{mobile-Chw8RWyH.js → mobile-CNLMhdFP.js} +2 -2
  52. package/dist/assets/pegDiagram-2B236MQR-7SVdAVqj.js +1 -0
  53. package/dist/assets/pieDiagram-ENE6RG2P-CzqQ4TaE.js +39 -0
  54. package/dist/assets/quadrantDiagram-ABIIQ3AL-B9B6W80N.js +7 -0
  55. package/dist/assets/railroadDiagram-RFXS5EU6-BRNLawsr.js +1 -0
  56. package/dist/assets/{requirementDiagram-MS252O5E-B1ZFIotK.js → requirementDiagram-TGXJPOKE-CmASubeG.js} +3 -3
  57. package/dist/assets/sankeyDiagram-HTMAVEWB-DiReR0OB.js +40 -0
  58. package/dist/assets/sequenceDiagram-DBY2YBRQ-DVn5iZKR.js +162 -0
  59. package/dist/assets/sizeCapture-X5ZJPWSS-C8CuDOdp.js +1 -0
  60. package/dist/assets/stateDiagram-2N3HPSRC-BIdkHglY.js +1 -0
  61. package/dist/assets/stateDiagram-v2-6OUMAXLB-B9yje3Er.js +1 -0
  62. package/dist/assets/swimlanes-5IMT3BWC-BlVsYyyN.js +2 -0
  63. package/dist/assets/swimlanesDiagram-G3AALYLV-BTLpU45n.js +8 -0
  64. package/dist/assets/{timeline-definition-GMOUNBTQ-k_0vJQAK.js → timeline-definition-FHXFAJF6-CnTqJmQ2.js} +3 -3
  65. package/dist/assets/vennDiagram-L72KCM5P-D_qZxRhe.js +34 -0
  66. package/dist/assets/wardleyDiagram-EHGQE667-CT-WHFEO.js +78 -0
  67. package/dist/assets/xychartDiagram-FW5EYKEG-CVRFHwUQ.js +7 -0
  68. package/dist/index.html +3 -3
  69. package/lib/agent-string.js +34 -0
  70. package/lib/build.js +542 -148
  71. package/lib/experiment-judge.js +145 -0
  72. package/lib/experiment-metrics.js +265 -0
  73. package/lib/experiment-pricing.js +60 -0
  74. package/lib/experiment-report.js +306 -0
  75. package/lib/experiment-sandbox.js +147 -0
  76. package/lib/experiment.js +539 -0
  77. package/lib/feature-json.js +1 -1
  78. package/lib/feature-writer.js +30 -0
  79. package/lib/flow-state.js +36 -0
  80. package/lib/gate-prompt.js +66 -6
  81. package/lib/lifecycle-modes.js +213 -0
  82. package/lib/new.js +6 -2
  83. package/lib/roadmap-graph/index.js +26 -28
  84. package/lib/roadmap-graph/vision-adapter.js +126 -0
  85. package/lib/stratum-mcp-client.js +9 -0
  86. package/lib/triage.js +7 -1
  87. package/lib/vision-writer.js +30 -6
  88. package/package.json +1 -1
  89. package/pipelines/plan.stratum.yaml +161 -0
  90. package/scripts/release-manual.sh +98 -0
  91. package/server/artifact-manager.js +30 -3
  92. package/server/compose-mcp-tools.js +11 -9
  93. package/server/compose-mcp.js +4 -0
  94. package/server/feature-scan.js +87 -173
  95. package/server/file-watcher.js +33 -0
  96. package/server/graph-export.js +0 -0
  97. package/server/index.js +10 -5
  98. package/server/lifecycle-guard.js +62 -25
  99. package/server/pipeline-routes.js +773 -3
  100. package/server/roadmap-graph-vision.js +138 -0
  101. package/server/status-snapshot.js +8 -7
  102. package/server/vision-routes.js +64 -33
  103. package/server/vision-store.js +13 -4
  104. package/.claude/skills/compose/references/hermes-tools.md +0 -80
  105. package/dist/assets/App-CT0vXPgd.js +0 -724
  106. package/dist/assets/_baseUniq-CsOHc_iS.js +0 -1
  107. package/dist/assets/arc-BXht3LyE.js +0 -1
  108. package/dist/assets/architectureDiagram-Q4EWVU46-BV1r86-w.js +0 -36
  109. package/dist/assets/blockDiagram-DXYQGD6D-BgcMJjAR.js +0 -132
  110. package/dist/assets/channel-Dd4XaiYv.js +0 -1
  111. package/dist/assets/chunk-4TB4RGXK-MLT7I7w4.js +0 -206
  112. package/dist/assets/chunk-OYMX7WX6-yBPwT1AT.js +0 -231
  113. package/dist/assets/classDiagram-6PBFFD2Q-DshSQDF9.js +0 -1
  114. package/dist/assets/classDiagram-v2-HSJHXN6E-DshSQDF9.js +0 -1
  115. package/dist/assets/clone-CBsbNGAa.js +0 -1
  116. package/dist/assets/dagre-KV5264BT-CTcTVGdU.js +0 -4
  117. package/dist/assets/diagram-5BDNPKRD-qR6mc8kJ.js +0 -10
  118. package/dist/assets/diagram-G4DWMVQ6-CUcGXyNb.js +0 -24
  119. package/dist/assets/diagram-MMDJMWI5-B68cPonH.js +0 -43
  120. package/dist/assets/diagram-TYMM5635-q8Id8eFG.js +0 -24
  121. package/dist/assets/erDiagram-SMLLAGMA-CtxJi57K.js +0 -85
  122. package/dist/assets/flowDiagram-DWJPFMVM-BzzO4JYU.js +0 -162
  123. package/dist/assets/ganttDiagram-T4ZO3ILL-Dn0wIyhR.js +0 -292
  124. package/dist/assets/gitGraphDiagram-UUTBAWPF-tnNFXfMM.js +0 -106
  125. package/dist/assets/graph-DZe55uk8.js +0 -331
  126. package/dist/assets/graph-Tq_bs_r0.js +0 -1
  127. package/dist/assets/index-8UhRLbGq.js +0 -123
  128. package/dist/assets/index-CRqB9els.css +0 -1
  129. package/dist/assets/infoDiagram-42DDH7IO-BXLvLeS0.js +0 -2
  130. package/dist/assets/katex-DkKDou_j.js +0 -257
  131. package/dist/assets/layout-qtUgN9BC.js +0 -1
  132. package/dist/assets/min-hESxN-c0.js +0 -1
  133. package/dist/assets/pieDiagram-DEJITSTG-gglmeMav.js +0 -30
  134. package/dist/assets/quadrantDiagram-34T5L4WZ-CqFC_htU.js +0 -7
  135. package/dist/assets/sankeyDiagram-XADWPNL6-BvItvZt5.js +0 -10
  136. package/dist/assets/sequenceDiagram-FGHM5R23-CAIn5Z3U.js +0 -157
  137. package/dist/assets/stateDiagram-FHFEXIEX-CyJrGzib.js +0 -1
  138. package/dist/assets/stateDiagram-v2-QKLJ7IA2-DprhF_Ho.js +0 -1
  139. package/dist/assets/vennDiagram-DHZGUBPP-TZAW60b0.js +0 -34
  140. package/dist/assets/wardley-RL74JXVD-DzQEoG6X.js +0 -162
  141. package/dist/assets/wardleyDiagram-NUSXRM2D-BAoVVIjZ.js +0 -20
  142. package/dist/assets/xychartDiagram-5P7HB3ND-CynkZcUb.js +0 -7
  143. package/lib/roadmap-graph/collect.js +0 -178
@@ -15,9 +15,11 @@
15
15
  import fs from 'node:fs';
16
16
  import path from 'node:path';
17
17
 
18
+ import { parse as parseYaml } from 'yaml';
18
19
  import { getTargetRoot, resolveProjectPath } from './project-root.js';
19
20
  import { relForDisplay } from '../lib/project-paths.js';
20
21
  import { assertValidLinkShape } from '../lib/feature-write-guard.js';
22
+ import { depsToEdges } from '../lib/roadmap-graph/model.js';
21
23
 
22
24
  // ---------------------------------------------------------------------------
23
25
  // Metadata extraction
@@ -162,6 +164,9 @@ export function scanFeatures(featuresDir) {
162
164
  predecessors: [],
163
165
  successors: [],
164
166
  group: null, // explicit group from feature.json overrides derivation
167
+ priority: null, // track/priority from design.md frontmatter (COMP-ROADMAP-GRAPH-2)
168
+ deps: null, // { depends_on?, blocks?, concurrent_with? } from deps.yaml
169
+ hasFeatureJson: false, // feature.json present = a managed feature (canon)
165
170
  };
166
171
 
167
172
  // Read feature.json if present — supplies optional explicit `group`,
@@ -169,13 +174,48 @@ export function scanFeatures(featuresDir) {
169
174
  try {
170
175
  const specPath = path.join(featureDir, 'feature.json');
171
176
  if (fs.existsSync(specPath)) {
177
+ feature.hasFeatureJson = true;
172
178
  const spec = JSON.parse(fs.readFileSync(specPath, 'utf-8'));
173
179
  if (typeof spec.group === 'string' && spec.group.trim()) {
174
180
  feature.group = spec.group.trim();
175
181
  }
182
+ // feature.json is canon for status. Set it first (highest precedence) so
183
+ // the design.md prose loop below cannot override it — the vision store
184
+ // is a managed projection of feature.json, not a thing that drifts from
185
+ // it (COMP-ROADMAP-GRAPH-2: kills the feature.json/cockpit status de-sync).
186
+ if (typeof spec.status === 'string' && spec.status.trim()) {
187
+ feature.status = normalizeStatus(spec.status);
188
+ }
176
189
  }
177
190
  } catch { /* ignore malformed feature.json */ }
178
191
 
192
+ // COMP-ROADMAP-GRAPH-2 (S3): absorb the static collector's edge + track
193
+ // metadata so a seeded store renders the same canonical graph.
194
+ // deps.yaml -> typed dependency edges.
195
+ try {
196
+ const depsPath = path.join(featureDir, 'deps.yaml');
197
+ if (fs.existsSync(depsPath)) {
198
+ const deps = parseYaml(fs.readFileSync(depsPath, 'utf-8')) || {};
199
+ if (deps && typeof deps === 'object' && !Array.isArray(deps)) feature.deps = deps;
200
+ }
201
+ } catch { /* unparseable deps.yaml — skip edges for this feature */ }
202
+
203
+ // design.md YAML frontmatter `track`/`priority` (collect.js precedence:
204
+ // frontmatter track > feature.json group). Rare, but kept faithful.
205
+ try {
206
+ const designPath = path.join(featureDir, 'design.md');
207
+ if (fs.existsSync(designPath)) {
208
+ const m = fs.readFileSync(designPath, 'utf-8').match(/^---\n([\s\S]*?)\n---/);
209
+ if (m) {
210
+ const fm = parseYaml(m[1]) || {};
211
+ if (fm && typeof fm === 'object' && !Array.isArray(fm)) {
212
+ if (typeof fm.track === 'string' && fm.track.trim()) feature.group = fm.track.trim();
213
+ if (typeof fm.priority === 'string' && fm.priority.trim()) feature.priority = fm.priority.trim();
214
+ }
215
+ }
216
+ }
217
+ } catch { /* invalid frontmatter — ignore */ }
218
+
179
219
  // List artifacts
180
220
  try {
181
221
  feature.artifacts = fs.readdirSync(featureDir)
@@ -420,179 +460,6 @@ export function writeFeatureGroupToDisk(item, newGroup, featuresDir) {
420
460
  }
421
461
  }
422
462
 
423
- // ---------------------------------------------------------------------------
424
- // Roadmap graph import
425
- // ---------------------------------------------------------------------------
426
-
427
- /**
428
- * Parse a roadmap-graph.html file (Cytoscape-based dependency graph).
429
- * Extracts the `nodes` and `edges` JS arrays from the <script> block.
430
- *
431
- * @param {string} htmlPath — absolute path to roadmap-graph.html
432
- * @returns {{ nodes: Array, edges: Array }}
433
- */
434
- export function parseRoadmapGraph(htmlPath) {
435
- if (!fs.existsSync(htmlPath)) return { nodes: [], edges: [] };
436
-
437
- const raw = fs.readFileSync(htmlPath, 'utf-8');
438
-
439
- // Extract nodes array
440
- const nodesMatch = raw.match(/const\s+nodes\s*=\s*\[([\s\S]*?)\];\s*\n/);
441
- const edgesMatch = raw.match(/const\s+edges\s*=\s*\[([\s\S]*?)\];\s*\n/);
442
-
443
- let nodes = [];
444
- let edges = [];
445
-
446
- if (nodesMatch) {
447
- try {
448
- // Wrap in array brackets and evaluate as JSON-ish JS
449
- // The data uses single quotes and unquoted keys, so we need to eval
450
- nodes = Function(`"use strict"; return [${nodesMatch[1]}]`)();
451
- } catch (e) {
452
- console.error('[feature-scan] Failed to parse roadmap-graph nodes:', e.message);
453
- }
454
- }
455
-
456
- if (edgesMatch) {
457
- try {
458
- edges = Function(`"use strict"; return [${edgesMatch[1]}]`)();
459
- } catch (e) {
460
- console.error('[feature-scan] Failed to parse roadmap-graph edges:', e.message);
461
- }
462
- }
463
-
464
- return { nodes, edges };
465
- }
466
-
467
- /**
468
- * Find roadmap-graph.html in common locations within a project.
469
- */
470
- function findRoadmapGraph() {
471
- const root = getTargetRoot();
472
- const candidates = [
473
- path.join(root, 'docs', 'roadmap-graph.html'),
474
- path.join(root, 'roadmap-graph.html'),
475
- ];
476
-
477
- // Search one level of subdirs for docs/roadmap-graph.html
478
- try {
479
- const entries = fs.readdirSync(root, { withFileTypes: true });
480
- for (const entry of entries) {
481
- if (!entry.isDirectory()) continue;
482
- if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
483
- candidates.push(path.join(root, entry.name, 'docs', 'roadmap-graph.html'));
484
- candidates.push(path.join(root, entry.name, 'roadmap-graph.html'));
485
- }
486
- } catch { /* skip */ }
487
-
488
- for (const p of candidates) {
489
- if (fs.existsSync(p)) return p;
490
- }
491
- return null;
492
- }
493
-
494
- const GRAPH_STATUS_MAP = {
495
- planned: 'planned',
496
- parked: 'parked',
497
- partial: 'in_progress',
498
- open: 'in_progress',
499
- complete: 'complete',
500
- };
501
-
502
- const GRAPH_EDGE_MAP = {
503
- dep: 'blocks',
504
- concurrent: 'supports',
505
- };
506
-
507
- /**
508
- * Seed vision store from a roadmap-graph.html file.
509
- * Creates items for nodes and connections for edges.
510
- */
511
- export function seedFromRoadmapGraph(store) {
512
- const graphPath = findRoadmapGraph();
513
- if (!graphPath) return { items: 0, connections: 0 };
514
-
515
- const { nodes, edges } = parseRoadmapGraph(graphPath);
516
- if (nodes.length === 0) return { items: 0, connections: 0 };
517
-
518
- console.log(`[vision] Roadmap graph: ${nodes.length} nodes, ${edges.length} edges from ${graphPath}`);
519
-
520
- const seeded = { items: 0, connections: 0 };
521
- const idMap = new Map(); // graphNodeId → visionItemId
522
-
523
- // Create/update items from nodes
524
- for (const node of nodes) {
525
- // Look for existing item by featureCode or title
526
- let item = Array.from(store.items.values()).find(
527
- i => i.lifecycle?.featureCode === node.id || i.title === node.id
528
- );
529
-
530
- const status = GRAPH_STATUS_MAP[node.status] || 'planned';
531
- const description = [
532
- node.name || '',
533
- node.desc || '',
534
- node.track ? `Track: ${node.track}` : '',
535
- node.priority ? `Priority: ${node.priority}` : '',
536
- ].filter(Boolean).join('\n');
537
-
538
- if (!item) {
539
- item = store.createItem({
540
- type: 'feature',
541
- title: node.id,
542
- description,
543
- status,
544
- phase: 'planning',
545
- confidence: status === 'complete' ? 3 : node.priority === 'high' ? 1 : 0,
546
- });
547
- try {
548
- store.updateLifecycle(item.id, { featureCode: node.id });
549
- } catch { /* skip */ }
550
- item = store.items.get(item.id);
551
- seeded.items++;
552
- } else {
553
- // Update description and status if richer
554
- const updates = {};
555
- if (description.length > (item.description || '').length) {
556
- updates.description = description;
557
- }
558
- if (status !== item.status && status !== 'planned') {
559
- updates.status = status;
560
- }
561
- if (Object.keys(updates).length > 0) {
562
- store.updateItem(item.id, updates);
563
- }
564
- }
565
-
566
- idMap.set(node.id, item.id);
567
- }
568
-
569
- // Create connections from edges
570
- for (const edge of edges) {
571
- const fromId = idMap.get(edge.source);
572
- const toId = idMap.get(edge.target);
573
- if (!fromId || !toId) continue;
574
-
575
- const type = GRAPH_EDGE_MAP[edge.type] || 'informs';
576
-
577
- // Check existing
578
- const exists = Array.from(store.connections.values()).some(
579
- c => (c.fromId === fromId && c.toId === toId) ||
580
- (c.fromId === toId && c.toId === fromId)
581
- );
582
- if (exists) continue;
583
-
584
- try {
585
- store.createConnection({ fromId, toId, type });
586
- seeded.connections++;
587
- } catch { /* skip */ }
588
- }
589
-
590
- if (seeded.items || seeded.connections) {
591
- console.log(`[vision] Roadmap graph: ${seeded.items} items, ${seeded.connections} connections seeded`);
592
- }
593
- return seeded;
594
- }
595
-
596
463
  // ---------------------------------------------------------------------------
597
464
  // Seed
598
465
  // ---------------------------------------------------------------------------
@@ -631,6 +498,7 @@ export function seedFeatures(features, store) {
631
498
  confidence: feature.confidence,
632
499
  files: feature.artifacts.map(a => artifactPath(feature, a)),
633
500
  ...(feature.group ? { group: feature.group } : {}),
501
+ ...(feature.priority ? { priority: feature.priority } : {}),
634
502
  });
635
503
  try {
636
504
  store.updateLifecycle(featureItem.id, { featureCode: feature.name, currentPhase: 'explore_design' });
@@ -656,6 +524,9 @@ export function seedFeatures(features, store) {
656
524
  if (feature.group && feature.group !== featureItem.group) {
657
525
  updates.group = feature.group;
658
526
  }
527
+ if (feature.priority && feature.priority !== featureItem.priority) {
528
+ updates.priority = feature.priority;
529
+ }
659
530
  if (Object.keys(updates).length > 0) {
660
531
  store.updateItem(featureItem.id, updates);
661
532
  seeded.updated++;
@@ -713,6 +584,49 @@ export function seedFeatures(features, store) {
713
584
  }
714
585
  }
715
586
 
587
+ // Fourth pass (COMP-ROADMAP-GRAPH-2 S3a): deps.yaml dependency edges as typed
588
+ // connections — `dep` -> blocks (directed), `concurrent` -> supports. These
589
+ // structural edges are RECONCILED, not appended: deps.yaml is canon, so a
590
+ // removed/retargeted dep must drop the stale edge from the live store too
591
+ // (else the cockpit export drifts from the canonical projection). Endpoints
592
+ // that resolve to no local item (external/missing) are skipped — buildGraph
593
+ // would drop their edges anyway.
594
+ const DEP_EDGE_CONN_TYPE = { dep: 'blocks', concurrent: 'supports' };
595
+ const STRUCTURAL_TYPES = new Set(['blocks', 'supports']);
596
+ const managedIds = new Set(featureItemMap.values());
597
+ const desiredStructural = new Set(); // `${fromId}|${toId}|${type}`
598
+ for (const feature of features) {
599
+ if (!feature.deps) continue;
600
+ for (const edge of depsToEdges(feature.name, feature.deps)) {
601
+ const fromId = featureItemMap.get(edge.from);
602
+ const toId = featureItemMap.get(edge.to);
603
+ const connType = DEP_EDGE_CONN_TYPE[edge.type];
604
+ if (!fromId || !toId || !connType) continue;
605
+ desiredStructural.add(`${fromId}|${toId}|${connType}`);
606
+ }
607
+ }
608
+ // Remove stale structural edges between two managed features that deps.yaml no
609
+ // longer declares (feature relationships are source-managed, not hand-edited).
610
+ for (const c of Array.from(store.connections.values())) {
611
+ if (!STRUCTURAL_TYPES.has(c.type)) continue;
612
+ if (!managedIds.has(c.fromId) || !managedIds.has(c.toId)) continue;
613
+ if (!desiredStructural.has(`${c.fromId}|${c.toId}|${c.type}`)) {
614
+ store.deleteConnection(c.id);
615
+ }
616
+ }
617
+ // Add any missing desired structural edges.
618
+ for (const key of desiredStructural) {
619
+ const [fromId, toId, connType] = key.split('|');
620
+ const exists = Array.from(store.connections.values()).some(
621
+ c => c.fromId === fromId && c.toId === toId && c.type === connType
622
+ );
623
+ if (exists) continue;
624
+ try {
625
+ store.createConnection({ fromId, toId, type: connType });
626
+ seeded.connections++;
627
+ } catch { /* skip invalid */ }
628
+ }
629
+
716
630
  if (seeded.features || seeded.updated || seeded.connections) {
717
631
  console.log(`[vision] Feature scan: ${seeded.features} new, ${seeded.updated} updated, ${seeded.connections} connections`);
718
632
  }
@@ -14,6 +14,24 @@ import { resolveDocsPathFromConfig, resolveFeaturesPathFromConfig } from '../lib
14
14
 
15
15
  const PROJECT_ROOT = getTargetRoot();
16
16
 
17
+ /**
18
+ * fs.watch fileFilter for the pipelines/ watch (COMP-PIPE-EDIT-6): only
19
+ * *.stratum.yaml files trigger a specChanged. `filename` is what fs.watch hands
20
+ * us (a name, possibly with a subdir prefix on recursive watches).
21
+ */
22
+ export function isStratumSpecFile(filename) {
23
+ return typeof filename === 'string' && filename.endsWith('.stratum.yaml');
24
+ }
25
+
26
+ /**
27
+ * Shape of the vision-WS `specChanged` broadcast (COMP-PIPE-EDIT-6). `file` is a
28
+ * BASENAME (editorSpecFile is a bare filename, so the store compares on basename);
29
+ * `path` carries the prefixed relative path for diagnostics.
30
+ */
31
+ export function buildSpecChangedMessage(file, relativePath) {
32
+ return { type: 'specChanged', file, path: relativePath };
33
+ }
34
+
17
35
  export class FileWatcherServer {
18
36
  constructor() {
19
37
  this.clients = new Set();
@@ -215,6 +233,21 @@ export class FileWatcherServer {
215
233
  }
216
234
  });
217
235
 
236
+ // Watch pipelines/ for *.stratum.yaml changes (COMP-PIPE-EDIT-6). A spec the
237
+ // pipeline editor has open can be edited externally (another writer, the user
238
+ // editing the file directly). The editor store lives on the VISION WS, not
239
+ // /ws/files, so emit a DEDICATED `specChanged` message via the onSpecChanged
240
+ // callback (wired in server/index.js to visionServer.broadcastMessage), NOT a
241
+ // `fileChanged` on this server's /ws/files. The payload carries `file` as a
242
+ // BASENAME because editorSpecFile is a bare filename (a prefixed relative path
243
+ // would never match the store's compare).
244
+ const pipelinesDir = path.join(PROJECT_ROOT, 'pipelines');
245
+ watchDir(pipelinesDir, 'pipelines', (relativePath, fullPath) => {
246
+ if (typeof this.onSpecChanged === 'function') {
247
+ this.onSpecChanged(buildSpecChangedMessage(path.basename(fullPath), relativePath));
248
+ }
249
+ }, isStratumSpecFile);
250
+
218
251
  // Watch .compose/data/ for active-build.json changes
219
252
  const self = this;
220
253
  const dataDir = path.join(PROJECT_ROOT, '.compose', 'data');
Binary file
package/server/index.js CHANGED
@@ -8,7 +8,7 @@ import { FileWatcherServer } from './file-watcher.js';
8
8
  import { VisionStore } from './vision-store.js';
9
9
  import { VisionServer } from './vision-server.js';
10
10
  import { SessionManager } from './session-manager.js';
11
- import { scanFeatures, seedFeatures, scanSubPackages, seedSubPackages, seedFromRoadmapGraph } from './feature-scan.js';
11
+ import { scanFeatures, seedFeatures, scanSubPackages, seedSubPackages } from './feature-scan.js';
12
12
  import { attachGraphExportRoutes } from './graph-export.js';
13
13
  import { attachWorkspaceRoutes } from './workspace-routes.js';
14
14
  import { attachGraphLayoutRoutes } from './graph-layout-routes.js';
@@ -168,13 +168,12 @@ app.post('/api/project/switch', (req, res) => {
168
168
  ensureDataDir();
169
169
  // Reload store from new data directory
170
170
  visionStore.reloadFrom(result.dataDir);
171
- // Re-scan features, sub-packages, and roadmap graph from new project
171
+ // Re-scan features and sub-packages from new project
172
172
  try {
173
173
  const features = scanFeatures();
174
174
  if (features.length > 0) seedFeatures(features, visionStore);
175
175
  const packages = scanSubPackages();
176
176
  if (packages.length > 0) seedSubPackages(packages, visionStore);
177
- seedFromRoadmapGraph(visionStore);
178
177
  } catch (err) {
179
178
  console.error('[compose] Feature scan after switch:', err.message);
180
179
  }
@@ -201,13 +200,12 @@ const sessionManager = new SessionManager({
201
200
  const visionServer = new VisionServer(visionStore, sessionManager, { config: projectConfig });
202
201
  visionServer.attach(server, app);
203
202
 
204
- // Seed feature folders, sub-packages, and roadmap graph into vision store on startup
203
+ // Seed feature folders and sub-packages into vision store on startup
205
204
  try {
206
205
  const features = scanFeatures();
207
206
  if (features.length > 0) seedFeatures(features, visionStore);
208
207
  const packages = scanSubPackages();
209
208
  if (packages.length > 0) seedSubPackages(packages, visionStore);
210
- seedFromRoadmapGraph(visionStore);
211
209
  } catch (err) {
212
210
  console.error('[compose] Feature scan startup error:', err.message);
213
211
  }
@@ -231,6 +229,13 @@ fileWatcher.onBuildStateChanged = (state) => {
231
229
  }
232
230
  };
233
231
 
232
+ // Wire pipelines/*.stratum.yaml external changes → broadcast `specChanged` on
233
+ // the VISION WS (COMP-PIPE-EDIT-6 — the channel the pipeline editor store uses,
234
+ // not /ws/files). The message already carries { type:'specChanged', file, path }.
235
+ fileWatcher.onSpecChanged = (message) => {
236
+ visionServer.broadcastMessage(message);
237
+ };
238
+
234
239
  // ---------------------------------------------------------------------------
235
240
  // Static serving + SPA fallback (BOTH modes — additive)
236
241
  // Mounted AFTER all API routes so /api/* is never shadowed.
@@ -22,6 +22,14 @@ import {
22
22
  guardTransition as _guardTransition,
23
23
  } from './stratum-client.js';
24
24
  import { setFeatureStatus as _setFeatureStatus } from '../lib/feature-writer.js';
25
+ import {
26
+ resolveMode,
27
+ getMode,
28
+ transitionsOf,
29
+ completablePhaseOf,
30
+ terminalOf,
31
+ edgeEvidenceOf,
32
+ } from '../lib/lifecycle-modes.js';
25
33
 
26
34
  // ---------------------------------------------------------------------------
27
35
  // Canonical phase graph (compose-owned data — single source of truth)
@@ -54,7 +62,10 @@ export const TERMINAL = new Set(['complete', 'killed']);
54
62
  * be a superset of every edge vision-routes will legally request, or the guard
55
63
  * would reject a transition the app considers valid.
56
64
  */
57
- export function buildPhaseGraph(transitions = BASE_TRANSITIONS) {
65
+ export function buildPhaseGraph(mode = 'build') {
66
+ const transitions = transitionsOf(mode);
67
+ const completable = completablePhaseOf(mode);
68
+ const terminal = new Set(terminalOf(mode));
58
69
  const graph = {};
59
70
  const nodes = new Set();
60
71
  for (const [from, tos] of Object.entries(transitions)) {
@@ -62,18 +73,18 @@ export function buildPhaseGraph(transitions = BASE_TRANSITIONS) {
62
73
  nodes.add(from);
63
74
  for (const t of tos) nodes.add(t);
64
75
  }
65
- // ship → complete (the highest-consequence edge; implemented separately in
66
- // vision-routes at /lifecycle/complete, so absent from BASE_TRANSITIONS).
67
- graph.ship = [...(graph.ship || []), 'complete'];
76
+ // <completable phase> → complete (the highest-consequence edge; implemented
77
+ // separately in vision-routes at /lifecycle/complete, so absent from the mode's
78
+ // forward transitions). For build this is `ship complete`.
79
+ graph[completable] = [...(graph[completable] || []), 'complete'];
68
80
  // Every non-terminal phase → killed (vision-routes /lifecycle/kill allows
69
- // kill from any non-terminal phase, including ship).
81
+ // kill from any non-terminal phase, including the completable phase).
70
82
  for (const s of nodes) {
71
- if (TERMINAL.has(s)) continue;
83
+ if (terminal.has(s)) continue;
72
84
  graph[s] = graph[s] || [];
73
85
  if (!graph[s].includes('killed')) graph[s].push('killed');
74
86
  }
75
- graph.complete = [];
76
- graph.killed = [];
87
+ for (const t of terminal) graph[t] = [];
77
88
  return graph;
78
89
  }
79
90
 
@@ -86,17 +97,19 @@ export function buildPhaseGraph(transitions = BASE_TRANSITIONS) {
86
97
  *
87
98
  * @param {string} featureRelDir e.g. "docs/features/FEAT-1"
88
99
  */
89
- export function edgePredicates(featureRelDir) {
100
+ export function edgePredicates(featureRelDir, mode = 'build') {
90
101
  const det = (id, file) => ({
91
102
  id,
92
103
  type: 'deterministic',
93
104
  statement: `server_file_exists('${featureRelDir}/${file}')`,
94
105
  });
95
- return {
96
- 'explore_design->blueprint': [det('design_md', 'design.md')],
97
- 'blueprint->verification': [det('blueprint_md', 'blueprint.md')],
98
- 'plan->execute': [det('plan_md', 'plan.md')],
99
- };
106
+ const out = {};
107
+ // edgeEvidence is `{ 'from->to': 'file.md' }` per mode (build's is the legacy
108
+ // 3 edges verbatim). The predicate id matches the legacy naming: design.md → design_md.
109
+ for (const [edge, file] of Object.entries(edgeEvidenceOf(mode))) {
110
+ out[edge] = [det(file.replace(/\.md$/, '_md'), file)];
111
+ }
112
+ return out;
100
113
  }
101
114
 
102
115
  /**
@@ -105,9 +118,15 @@ export function edgePredicates(featureRelDir) {
105
118
  * `compose:<FC>` would let two compose projects sharing a feature code collide on
106
119
  * one ledger/current-state. The project-path hash prevents that.
107
120
  */
108
- export function resourceId(featureCode, workspaceRoot) {
121
+ export function resourceId(featureCode, workspaceRoot, mode = 'build') {
109
122
  const hash = createHash('sha256').update(path.resolve(workspaceRoot)).digest('hex').slice(0, 12);
110
- return `compose:${hash}:${featureCode}`;
123
+ const m = resolveMode(mode);
124
+ // build keeps the LEGACY id (no mode segment) so in-flight build guards are
125
+ // never orphaned; non-build modes get a namespaced id so two modes on one
126
+ // feature can't collide on a single guard ledger/current-state.
127
+ return m === 'build'
128
+ ? `compose:${hash}:${featureCode}`
129
+ : `compose:${hash}:${m}:${featureCode}`;
111
130
  }
112
131
 
113
132
  // ---------------------------------------------------------------------------
@@ -224,8 +243,21 @@ export function _testOnly_resetGuardCache() { _registered.clear(); }
224
243
  * which is pinned to getTargetRoot() and would drift for a non-current project
225
244
  * root). The relative dir is baked into the immutable guard registration, so it
226
245
  * must reflect the tree the routes actually serve.
246
+ *
247
+ * COMP-ROADMAP-PLAN S7: mode-aware, mirroring `resolveItemDir` (build.js:670-674).
248
+ * The mode's `artifactRoot` is a token: `'features'` resolves the (override-aware)
249
+ * config-backed `paths.features` dir — build stays byte-identical; any other token
250
+ * (`docs/bugs`, `docs/plans`) is a literal repo-relative root used as `<root>/<code>`.
251
+ * Without this, plan/bug evidence would be looked up under `docs/features` and the
252
+ * guard would block `advance` (C11).
227
253
  */
228
- function _featureRelDir(featureCode, workspaceRoot) {
254
+ function _featureRelDir(featureCode, workspaceRoot, mode = 'build') {
255
+ const artifactRoot = getMode(mode).runner.artifactRoot;
256
+ if (artifactRoot !== 'features') {
257
+ // Literal repo-relative root (docs/bugs, docs/plans) — no config override.
258
+ return `${artifactRoot}/${featureCode}`;
259
+ }
260
+ // Build: keep the config-backed paths.features resolution.
229
261
  let featuresRel = 'docs/features';
230
262
  try {
231
263
  const cfg = JSON.parse(readFileSync(path.join(workspaceRoot, '.compose', 'compose.json'), 'utf-8'));
@@ -234,6 +266,11 @@ function _featureRelDir(featureCode, workspaceRoot) {
234
266
  return `${featuresRel}/${featureCode}`;
235
267
  }
236
268
 
269
+ /** Test-only seam for the mode-aware feature-dir resolver. */
270
+ export function _testOnly_featureRelDir(featureCode, workspaceRoot, mode = 'build') {
271
+ return _featureRelDir(featureCode, workspaceRoot, mode);
272
+ }
273
+
237
274
  /**
238
275
  * The configured evidence-bound-completion test command (array form, e.g.
239
276
  * ["npm","test"]) from `<workspaceRoot>/.compose/compose.json` `guard.testCommand`,
@@ -259,18 +296,18 @@ export function guardTestCommand(workspaceRoot) {
259
296
  *
260
297
  * @returns the register result, or {error} on guard failure.
261
298
  */
262
- export async function ensureGuard(featureCode, currentPhase, workspaceRoot) {
263
- const rid = resourceId(featureCode, workspaceRoot);
299
+ export async function ensureGuard(featureCode, currentPhase, workspaceRoot, mode = 'build') {
300
+ const rid = resourceId(featureCode, workspaceRoot, mode);
264
301
  if (_registered.has(rid)) return { guard_id: rid, status: 'cached' };
265
302
 
266
303
  let res;
267
304
  try {
268
305
  res = await _client.register({
269
306
  resourceId: rid,
270
- graph: buildPhaseGraph(),
271
- edgePredicates: edgePredicates(_featureRelDir(featureCode, workspaceRoot)),
307
+ graph: buildPhaseGraph(mode),
308
+ edgePredicates: edgePredicates(_featureRelDir(featureCode, workspaceRoot, mode), mode),
272
309
  initial: currentPhase,
273
- terminal: ['complete', 'killed'],
310
+ terminal: terminalOf(mode),
274
311
  stakes: {},
275
312
  workspaceRoot,
276
313
  });
@@ -293,13 +330,13 @@ export async function ensureGuard(featureCode, currentPhase, workspaceRoot) {
293
330
  * @returns {Promise<{applied:boolean, refused?:boolean, verdict?:object,
294
331
  * ledgerRef?:string, currentState?:string, error?:object}>}
295
332
  */
296
- export async function guardedTransition({ featureCode, from, to, workspaceRoot, commitSha, resolvedBy = 'agent' }) {
297
- const reg = await ensureGuard(featureCode, from, workspaceRoot);
333
+ export async function guardedTransition({ featureCode, from, to, workspaceRoot, commitSha, resolvedBy = 'agent', mode = 'build' }) {
334
+ const reg = await ensureGuard(featureCode, from, workspaceRoot, mode);
298
335
  if (reg && (reg.error || reg.status === 'error')) {
299
336
  return { applied: false, error: reg.error || reg };
300
337
  }
301
338
 
302
- const rid = resourceId(featureCode, workspaceRoot);
339
+ const rid = resourceId(featureCode, workspaceRoot, mode);
303
340
  const artifacts = commitSha ? { commit_sha: commitSha } : {};
304
341
  // No idempotency_key: a refuse→fix→retry is a NEW logical attempt that must
305
342
  // re-evaluate evidence, but it carries an identical (from,to,artifacts)