@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.
- package/.claude/skills/compose/SKILL.md +0 -8
- package/README.md +11 -0
- package/bin/compose.js +335 -3
- package/dist/assets/App-Bu9KMtTa.js +891 -0
- package/dist/assets/abnfDiagram-VRR7QNED-6zp3w9rx.js +1 -0
- package/dist/assets/arc-CnLxxuah.js +1 -0
- package/dist/assets/architectureDiagram-ZJ3FMSHR-COM46S9k.js +36 -0
- package/dist/assets/blockDiagram-677ZJIJ3-wKzgOwF8.js +132 -0
- package/dist/assets/{browser-CnKiSnlr.js → browser-Cr0recrN.js} +6 -6
- package/dist/assets/{c4Diagram-AHTNJAMY-nBIRLJTv.js → c4Diagram-LMCZKHZV-DIXC_mR6.js} +1 -1
- package/dist/assets/channel-DQJPWkb_.js +1 -0
- package/dist/assets/{chunk-QZHKN3VN-B4Wfybww.js → chunk-2Q5K7J3B-BqLqZX6m.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-DE6_5aqh.js → chunk-32BRIVSS-CarSmVlX.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-tYY8xcjr.js → chunk-5VM5RSS4-D-f_Jn3G.js} +1 -1
- package/dist/assets/chunk-EX3LRPZG-DT42xo8E.js +231 -0
- package/dist/assets/{chunk-4BX2VUAB-CNmGyhrp.js → chunk-JWPE2WC7-CwY1aZc4.js} +1 -1
- package/dist/assets/chunk-MOJQB5TN-DBbZU_KX.js +88 -0
- package/dist/assets/chunk-RYQCIY6F-DqxtcLJ3.js +1 -0
- package/dist/assets/chunk-V7JOEXUC-Cy4Ixww6.js +206 -0
- package/dist/assets/{chunk-EDXVE4YY-TPlt1bS2.js → chunk-VR4S4FIN-CFexfU_a.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-D8KuKVLa.js → chunk-XXDRQBXY-uE-zN_vc.js} +1 -1
- package/dist/assets/classDiagram-OUVF2IWQ-CTLbAiUK.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-CTLbAiUK.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-DzzRyJtB.js → cose-bilkent-JH36ORCC-DIjpekos.js} +1 -1
- package/dist/assets/cynefin-VYW2F7L2-Ba2gbQow.js +178 -0
- package/dist/assets/cynefinDiagram-TSTJHNR4-njGzb7Tg.js +62 -0
- package/dist/assets/dagre-VKFMJZFB-DuyZREZ3.js +4 -0
- package/dist/assets/diagram-FQU43EPY-Npq-5o3a.js +3 -0
- package/dist/assets/diagram-G47NLZAW-fm4k3axC.js +24 -0
- package/dist/assets/diagram-NH7WQ7WH-B8EFGrTG.js +24 -0
- package/dist/assets/diagram-OA4YK3LP-BC7UHx9Q.js +30 -0
- package/dist/assets/diagram-WEI45ONY-BqjHLcCX.js +41 -0
- package/dist/assets/ebnfDiagram-CCIWWBDH-D5zXd4Qg.js +1 -0
- package/dist/assets/erDiagram-Q63AITRT-DC2FMcra.js +85 -0
- package/dist/assets/flowDiagram-23GEKE2U-D7Dx7JU4.js +156 -0
- package/dist/assets/ganttDiagram-NO4QXBWP-8sQH2y5K.js +292 -0
- package/dist/assets/gitGraphDiagram-IHSO6WYX-Ci_rhxur.js +106 -0
- package/dist/assets/graph-C9eacEi8.js +1 -0
- package/dist/assets/graph-xkel59g2.js +331 -0
- package/dist/assets/index-B7-HQenC.js +119 -0
- package/dist/assets/index-BxRamj_i.css +1 -0
- package/dist/assets/infoDiagram-FWYZ7A6U-B9FGQ0Cb.js +2 -0
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-DWx-7BBy.js → ishikawaDiagram-FXEZZL3T-BEKLyH6A.js} +5 -5
- package/dist/assets/{journeyDiagram-VCZTEJTY-C42hXNha.js → journeyDiagram-5HDEW3XC-BM-IDVRo.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-CEq930ew.js → kanban-definition-HUTT4EX6-Beb2k3pt.js} +7 -7
- package/dist/assets/katex-C5jXJg4s.js +257 -0
- package/dist/assets/layout-DEXfKzaS.js +1 -0
- package/dist/assets/{linear-D158F7OT.js → linear-Dwg7dTpz.js} +1 -1
- package/dist/assets/map-Czzmt4hB.js +1 -0
- package/dist/assets/{mindmap-definition-QFDTVHPH-d9SSh2nG.js → mindmap-definition-LN4V7U3C-CeaynfcE.js} +7 -7
- package/dist/assets/{mobile-Chw8RWyH.js → mobile-CNLMhdFP.js} +2 -2
- package/dist/assets/pegDiagram-2B236MQR-7SVdAVqj.js +1 -0
- package/dist/assets/pieDiagram-ENE6RG2P-CzqQ4TaE.js +39 -0
- package/dist/assets/quadrantDiagram-ABIIQ3AL-B9B6W80N.js +7 -0
- package/dist/assets/railroadDiagram-RFXS5EU6-BRNLawsr.js +1 -0
- package/dist/assets/{requirementDiagram-MS252O5E-B1ZFIotK.js → requirementDiagram-TGXJPOKE-CmASubeG.js} +3 -3
- package/dist/assets/sankeyDiagram-HTMAVEWB-DiReR0OB.js +40 -0
- package/dist/assets/sequenceDiagram-DBY2YBRQ-DVn5iZKR.js +162 -0
- package/dist/assets/sizeCapture-X5ZJPWSS-C8CuDOdp.js +1 -0
- package/dist/assets/stateDiagram-2N3HPSRC-BIdkHglY.js +1 -0
- package/dist/assets/stateDiagram-v2-6OUMAXLB-B9yje3Er.js +1 -0
- package/dist/assets/swimlanes-5IMT3BWC-BlVsYyyN.js +2 -0
- package/dist/assets/swimlanesDiagram-G3AALYLV-BTLpU45n.js +8 -0
- package/dist/assets/{timeline-definition-GMOUNBTQ-k_0vJQAK.js → timeline-definition-FHXFAJF6-CnTqJmQ2.js} +3 -3
- package/dist/assets/vennDiagram-L72KCM5P-D_qZxRhe.js +34 -0
- package/dist/assets/wardleyDiagram-EHGQE667-CT-WHFEO.js +78 -0
- package/dist/assets/xychartDiagram-FW5EYKEG-CVRFHwUQ.js +7 -0
- package/dist/index.html +3 -3
- package/lib/agent-string.js +34 -0
- package/lib/build.js +542 -148
- package/lib/experiment-judge.js +145 -0
- package/lib/experiment-metrics.js +265 -0
- package/lib/experiment-pricing.js +60 -0
- package/lib/experiment-report.js +306 -0
- package/lib/experiment-sandbox.js +147 -0
- package/lib/experiment.js +539 -0
- package/lib/feature-json.js +1 -1
- package/lib/feature-writer.js +30 -0
- package/lib/flow-state.js +36 -0
- package/lib/gate-prompt.js +66 -6
- package/lib/lifecycle-modes.js +213 -0
- package/lib/new.js +6 -2
- package/lib/roadmap-graph/index.js +26 -28
- package/lib/roadmap-graph/vision-adapter.js +126 -0
- package/lib/stratum-mcp-client.js +9 -0
- package/lib/triage.js +7 -1
- package/lib/vision-writer.js +30 -6
- package/package.json +1 -1
- package/pipelines/plan.stratum.yaml +161 -0
- package/scripts/release-manual.sh +98 -0
- package/server/artifact-manager.js +30 -3
- package/server/compose-mcp-tools.js +11 -9
- package/server/compose-mcp.js +4 -0
- package/server/feature-scan.js +87 -173
- package/server/file-watcher.js +33 -0
- package/server/graph-export.js +0 -0
- package/server/index.js +10 -5
- package/server/lifecycle-guard.js +62 -25
- package/server/pipeline-routes.js +773 -3
- package/server/roadmap-graph-vision.js +138 -0
- package/server/status-snapshot.js +8 -7
- package/server/vision-routes.js +64 -33
- package/server/vision-store.js +13 -4
- package/.claude/skills/compose/references/hermes-tools.md +0 -80
- package/dist/assets/App-CT0vXPgd.js +0 -724
- package/dist/assets/_baseUniq-CsOHc_iS.js +0 -1
- package/dist/assets/arc-BXht3LyE.js +0 -1
- package/dist/assets/architectureDiagram-Q4EWVU46-BV1r86-w.js +0 -36
- package/dist/assets/blockDiagram-DXYQGD6D-BgcMJjAR.js +0 -132
- package/dist/assets/channel-Dd4XaiYv.js +0 -1
- package/dist/assets/chunk-4TB4RGXK-MLT7I7w4.js +0 -206
- package/dist/assets/chunk-OYMX7WX6-yBPwT1AT.js +0 -231
- package/dist/assets/classDiagram-6PBFFD2Q-DshSQDF9.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-DshSQDF9.js +0 -1
- package/dist/assets/clone-CBsbNGAa.js +0 -1
- package/dist/assets/dagre-KV5264BT-CTcTVGdU.js +0 -4
- package/dist/assets/diagram-5BDNPKRD-qR6mc8kJ.js +0 -10
- package/dist/assets/diagram-G4DWMVQ6-CUcGXyNb.js +0 -24
- package/dist/assets/diagram-MMDJMWI5-B68cPonH.js +0 -43
- package/dist/assets/diagram-TYMM5635-q8Id8eFG.js +0 -24
- package/dist/assets/erDiagram-SMLLAGMA-CtxJi57K.js +0 -85
- package/dist/assets/flowDiagram-DWJPFMVM-BzzO4JYU.js +0 -162
- package/dist/assets/ganttDiagram-T4ZO3ILL-Dn0wIyhR.js +0 -292
- package/dist/assets/gitGraphDiagram-UUTBAWPF-tnNFXfMM.js +0 -106
- package/dist/assets/graph-DZe55uk8.js +0 -331
- package/dist/assets/graph-Tq_bs_r0.js +0 -1
- package/dist/assets/index-8UhRLbGq.js +0 -123
- package/dist/assets/index-CRqB9els.css +0 -1
- package/dist/assets/infoDiagram-42DDH7IO-BXLvLeS0.js +0 -2
- package/dist/assets/katex-DkKDou_j.js +0 -257
- package/dist/assets/layout-qtUgN9BC.js +0 -1
- package/dist/assets/min-hESxN-c0.js +0 -1
- package/dist/assets/pieDiagram-DEJITSTG-gglmeMav.js +0 -30
- package/dist/assets/quadrantDiagram-34T5L4WZ-CqFC_htU.js +0 -7
- package/dist/assets/sankeyDiagram-XADWPNL6-BvItvZt5.js +0 -10
- package/dist/assets/sequenceDiagram-FGHM5R23-CAIn5Z3U.js +0 -157
- package/dist/assets/stateDiagram-FHFEXIEX-CyJrGzib.js +0 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-DprhF_Ho.js +0 -1
- package/dist/assets/vennDiagram-DHZGUBPP-TZAW60b0.js +0 -34
- package/dist/assets/wardley-RL74JXVD-DzQEoG6X.js +0 -162
- package/dist/assets/wardleyDiagram-NUSXRM2D-BAoVVIjZ.js +0 -20
- package/dist/assets/xychartDiagram-5P7HB3ND-CynkZcUb.js +0 -7
- package/lib/roadmap-graph/collect.js +0 -178
package/server/feature-scan.js
CHANGED
|
@@ -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
|
}
|
package/server/file-watcher.js
CHANGED
|
@@ -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');
|
package/server/graph-export.js
CHANGED
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
//
|
|
66
|
-
// vision-routes at /lifecycle/complete, so absent from
|
|
67
|
-
|
|
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
|
|
81
|
+
// kill from any non-terminal phase, including the completable phase).
|
|
70
82
|
for (const s of nodes) {
|
|
71
|
-
if (
|
|
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
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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:
|
|
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)
|