@smartmemory/compose 0.2.52-beta → 0.2.54-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 +39 -0
- package/bin/compose.js +378 -3
- package/dist/assets/App-D_tvTs_n.js +896 -0
- package/dist/assets/abnfDiagram-VRR7QNED-Dhb7O5rp.js +1 -0
- package/dist/assets/arc-C4oVdnk2.js +1 -0
- package/dist/assets/architectureDiagram-ZJ3FMSHR-DqHrb5_F.js +36 -0
- package/dist/assets/blockDiagram-677ZJIJ3-DUyBayYK.js +132 -0
- package/dist/assets/{browser-CnKiSnlr.js → browser-Cr0recrN.js} +6 -6
- package/dist/assets/{c4Diagram-AHTNJAMY-DtDSNlOX.js → c4Diagram-LMCZKHZV-CxlkWMRK.js} +1 -1
- package/dist/assets/channel-xb4mQfuL.js +1 -0
- package/dist/assets/{chunk-QZHKN3VN-BMWFLZ8t.js → chunk-2Q5K7J3B-C6QJsyOQ.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-CqjNXooj.js → chunk-32BRIVSS-D9NgYu9W.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-BAa7Edp6.js → chunk-5VM5RSS4-DbD86rhK.js} +1 -1
- package/dist/assets/chunk-EX3LRPZG-E4m4xNMw.js +231 -0
- package/dist/assets/{chunk-4BX2VUAB-oXU2npoL.js → chunk-JWPE2WC7-DHGlRWbw.js} +1 -1
- package/dist/assets/chunk-MOJQB5TN-BblYnFUq.js +88 -0
- package/dist/assets/chunk-RYQCIY6F-CGT-88vL.js +1 -0
- package/dist/assets/chunk-V7JOEXUC-BJJmX0mc.js +206 -0
- package/dist/assets/{chunk-EDXVE4YY-Bse2kdRh.js → chunk-VR4S4FIN-B4ygduCD.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-BEEk2FdH.js → chunk-XXDRQBXY-7APXuI2s.js} +1 -1
- package/dist/assets/classDiagram-OUVF2IWQ-BIvgPTgh.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-BIvgPTgh.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-NYnH3mVW.js → cose-bilkent-JH36ORCC-DnVcT6R6.js} +1 -1
- package/dist/assets/cynefin-VYW2F7L2-C9qiUSfX.js +178 -0
- package/dist/assets/cynefinDiagram-TSTJHNR4-BowkQhEu.js +62 -0
- package/dist/assets/dagre-VKFMJZFB-Y5V-7U3p.js +4 -0
- package/dist/assets/diagram-FQU43EPY-I7wDE-Xp.js +3 -0
- package/dist/assets/diagram-G47NLZAW-DapGa1HI.js +24 -0
- package/dist/assets/diagram-NH7WQ7WH-CHO1JntO.js +24 -0
- package/dist/assets/diagram-OA4YK3LP-mBWP2D1G.js +30 -0
- package/dist/assets/diagram-WEI45ONY-C8KN8YnF.js +41 -0
- package/dist/assets/ebnfDiagram-CCIWWBDH-ZkJG9NOL.js +1 -0
- package/dist/assets/erDiagram-Q63AITRT-DGnL3kW1.js +85 -0
- package/dist/assets/flowDiagram-23GEKE2U-DHYupfOu.js +156 -0
- package/dist/assets/ganttDiagram-NO4QXBWP-B1B2zpwR.js +292 -0
- package/dist/assets/gitGraphDiagram-IHSO6WYX-B9_OP4Fs.js +106 -0
- package/dist/assets/graph-C9eacEi8.js +1 -0
- package/dist/assets/graph-xkel59g2.js +331 -0
- package/dist/assets/index-0Hq9436Q.js +119 -0
- package/dist/assets/index-LIwREYgH.css +1 -0
- package/dist/assets/infoDiagram-FWYZ7A6U-D-a34TqE.js +2 -0
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-CK2IFFAP.js → ishikawaDiagram-FXEZZL3T-h0bUvXGV.js} +5 -5
- package/dist/assets/{journeyDiagram-VCZTEJTY-DAH5Stkf.js → journeyDiagram-5HDEW3XC-B4lm0RBa.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-Bi0aCqkv.js → kanban-definition-HUTT4EX6-msvfouRX.js} +7 -7
- package/dist/assets/katex-C5jXJg4s.js +257 -0
- package/dist/assets/layout-DEXfKzaS.js +1 -0
- package/dist/assets/{linear-ClGEGlS2.js → linear-B0trv2xN.js} +1 -1
- package/dist/assets/map-Czzmt4hB.js +1 -0
- package/dist/assets/{mindmap-definition-QFDTVHPH-Wjr-PgC6.js → mindmap-definition-LN4V7U3C-CvswbRgd.js} +7 -7
- package/dist/assets/{mobile-Chw8RWyH.js → mobile-CNLMhdFP.js} +2 -2
- package/dist/assets/pegDiagram-2B236MQR-DZEmIYC7.js +1 -0
- package/dist/assets/pieDiagram-ENE6RG2P-1ZfqxHkL.js +39 -0
- package/dist/assets/quadrantDiagram-ABIIQ3AL-Owx49uiJ.js +7 -0
- package/dist/assets/railroadDiagram-RFXS5EU6-B7RiaIGf.js +1 -0
- package/dist/assets/{requirementDiagram-MS252O5E-CU9Rlyq5.js → requirementDiagram-TGXJPOKE-fqmHs_5o.js} +3 -3
- package/dist/assets/sankeyDiagram-HTMAVEWB-D0Q3FeKV.js +40 -0
- package/dist/assets/sequenceDiagram-DBY2YBRQ-CnPuB3fy.js +162 -0
- package/dist/assets/sizeCapture-X5ZJPWSS-CsHoR37V.js +1 -0
- package/dist/assets/stateDiagram-2N3HPSRC-GVwpflU5.js +1 -0
- package/dist/assets/stateDiagram-v2-6OUMAXLB-DCGIHFu9.js +1 -0
- package/dist/assets/swimlanes-5IMT3BWC-DRR41dsQ.js +2 -0
- package/dist/assets/swimlanesDiagram-G3AALYLV-C4RhLkZo.js +8 -0
- package/dist/assets/{timeline-definition-GMOUNBTQ-C8SZmaLp.js → timeline-definition-FHXFAJF6-ePaEemL7.js} +3 -3
- package/dist/assets/vennDiagram-L72KCM5P-BAYP8dNF.js +34 -0
- package/dist/assets/wardleyDiagram-EHGQE667-jNVH5Adm.js +78 -0
- package/dist/assets/xychartDiagram-FW5EYKEG-BYDWGXbZ.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-events.js +10 -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/smartmemory-client.js +139 -0
- package/lib/smartmemory-config.js +50 -0
- package/lib/smartmemory-ingest.js +115 -0
- package/lib/smartmemory-sync.js +275 -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/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/gate-log-store.js +11 -1
- package/server/graph-export.js +0 -0
- package/server/index.js +3 -5
- package/server/lifecycle-guard.js +62 -25
- package/server/roadmap-graph-vision.js +138 -0
- package/server/smartmemory-routes.js +113 -0
- package/server/status-snapshot.js +8 -7
- package/server/vision-routes.js +64 -33
- package/server/vision-server.js +6 -0
- package/server/vision-store.js +13 -4
- package/.claude/skills/compose/references/hermes-tools.md +0 -80
- package/dist/assets/App-Ddhfx18X.js +0 -889
- package/dist/assets/_baseUniq-CoEVVYyC.js +0 -1
- package/dist/assets/arc-DFnGxuCB.js +0 -1
- package/dist/assets/architectureDiagram-Q4EWVU46-66rx01Tn.js +0 -36
- package/dist/assets/blockDiagram-DXYQGD6D-AE5PKSak.js +0 -132
- package/dist/assets/channel-CScyWprq.js +0 -1
- package/dist/assets/chunk-4TB4RGXK-5w2lZAjW.js +0 -206
- package/dist/assets/chunk-OYMX7WX6-CGr_YiAc.js +0 -231
- package/dist/assets/classDiagram-6PBFFD2Q-DibVYYdr.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-DibVYYdr.js +0 -1
- package/dist/assets/clone-ZMALIFmw.js +0 -1
- package/dist/assets/dagre-KV5264BT--mrfaTHR.js +0 -4
- package/dist/assets/diagram-5BDNPKRD-DjXvyFzh.js +0 -10
- package/dist/assets/diagram-G4DWMVQ6-BrrexEvm.js +0 -24
- package/dist/assets/diagram-MMDJMWI5-D3Q0OT2M.js +0 -43
- package/dist/assets/diagram-TYMM5635-DKTntmxE.js +0 -24
- package/dist/assets/erDiagram-SMLLAGMA-BWbR0dPj.js +0 -85
- package/dist/assets/flowDiagram-DWJPFMVM-Dj5M5XaY.js +0 -162
- package/dist/assets/ganttDiagram-T4ZO3ILL-DKppFh_5.js +0 -292
- package/dist/assets/gitGraphDiagram-UUTBAWPF-9xQ9O25V.js +0 -106
- package/dist/assets/graph-Cgmhvu1T.js +0 -1
- package/dist/assets/graph-DZe55uk8.js +0 -331
- package/dist/assets/index-BxRamj_i.css +0 -1
- package/dist/assets/index-D8uDfn8y.js +0 -123
- package/dist/assets/infoDiagram-42DDH7IO-DQYWVzVN.js +0 -2
- package/dist/assets/katex-DkKDou_j.js +0 -257
- package/dist/assets/layout-DN13RkA1.js +0 -1
- package/dist/assets/min-CXr-fUFS.js +0 -1
- package/dist/assets/pieDiagram-DEJITSTG-CVipbIlr.js +0 -30
- package/dist/assets/quadrantDiagram-34T5L4WZ-CHJFyhm-.js +0 -7
- package/dist/assets/sankeyDiagram-XADWPNL6-BSULeqST.js +0 -10
- package/dist/assets/sequenceDiagram-FGHM5R23-CCOKXvjj.js +0 -157
- package/dist/assets/stateDiagram-FHFEXIEX-B91gKA7Q.js +0 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-Dc8ur9ta.js +0 -1
- package/dist/assets/vennDiagram-DHZGUBPP-YvEPDlsM.js +0 -34
- package/dist/assets/wardley-RL74JXVD-TMBUq_w1.js +0 -162
- package/dist/assets/wardleyDiagram-NUSXRM2D-DzEkPrri.js +0 -20
- package/dist/assets/xychartDiagram-5P7HB3ND-BIiCeiYk.js +0 -7
- package/lib/roadmap-graph/collect.js +0 -178
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* roadmap-graph-vision.js — COMP-ROADMAP-GRAPH-2 S4.
|
|
3
|
+
*
|
|
4
|
+
* The CANONICAL projection of the one graph model: build the roadmap graph from
|
|
5
|
+
* a freshly, deterministically seeded vision store (committed source only — no
|
|
6
|
+
* persisted cockpit state), through the same adapter + renderer the live
|
|
7
|
+
* cockpit export uses. This is what the headless CLI / MCP / CI call.
|
|
8
|
+
*
|
|
9
|
+
* Lives in server/ (not lib/) because it depends on the vision store + seed,
|
|
10
|
+
* which are server-side; it reuses the pure render/write/check helpers from
|
|
11
|
+
* lib/roadmap-graph/index.js. This is the seam that lets us retire collect.js.
|
|
12
|
+
*/
|
|
13
|
+
import { mkdtempSync, existsSync, readFileSync, rmSync } from 'node:fs';
|
|
14
|
+
import { tmpdir } from 'node:os';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
|
|
17
|
+
import { VisionStore } from './vision-store.js';
|
|
18
|
+
import { scanFeatures, seedFeatures } from './feature-scan.js';
|
|
19
|
+
import { resolveFeaturesPath, resolveRoadmapPath, loadExternalPrefixes } from '../lib/project-paths.js';
|
|
20
|
+
import { parseRoadmap } from '../lib/roadmap-parser.js';
|
|
21
|
+
import { visionToGraphInputs } from '../lib/roadmap-graph/vision-adapter.js';
|
|
22
|
+
import { depsToEdges, DanglingEdgeError } from '../lib/roadmap-graph/model.js';
|
|
23
|
+
import { buildArtifactFromInputs, writeArtifact, checkArtifact } from '../lib/roadmap-graph/index.js';
|
|
24
|
+
|
|
25
|
+
// ROADMAP/feature UPPERCASE status -> vision status. The vision vocab has no
|
|
26
|
+
// `partial`; PARTIAL collapses to in_progress (documented delta — the adapter
|
|
27
|
+
// maps it back to IN_PROGRESS, so a PARTIAL feature renders as in_progress in
|
|
28
|
+
// the canonical graph rather than the static path's `partial` border).
|
|
29
|
+
const ROADMAP_TO_VISION_STATUS = {
|
|
30
|
+
PLANNED: 'planned',
|
|
31
|
+
IN_PROGRESS: 'in_progress',
|
|
32
|
+
PARTIAL: 'in_progress',
|
|
33
|
+
COMPLETE: 'complete',
|
|
34
|
+
BLOCKED: 'blocked',
|
|
35
|
+
PARKED: 'parked',
|
|
36
|
+
KILLED: 'killed',
|
|
37
|
+
SUPERSEDED: 'superseded',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function toVisionStatus(status) {
|
|
41
|
+
return ROADMAP_TO_VISION_STATUS[String(status || 'PLANNED').toUpperCase()] || 'planned';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Seed ROADMAP.md rows that lack a feature folder as fallback feature items,
|
|
46
|
+
* mirroring collect.js section (b) so the node universe matches.
|
|
47
|
+
*/
|
|
48
|
+
function seedRoadmapFallback(store, cwd, externalPrefixes) {
|
|
49
|
+
const roadmapPath = resolveRoadmapPath(cwd);
|
|
50
|
+
if (!existsSync(roadmapPath)) return;
|
|
51
|
+
const isExternal = (code) => externalPrefixes.some((p) => code.startsWith(p));
|
|
52
|
+
const have = new Set(
|
|
53
|
+
[...store.items.values()]
|
|
54
|
+
.filter((i) => i.type === 'feature')
|
|
55
|
+
.map((i) => i.lifecycle?.featureCode || i.title),
|
|
56
|
+
);
|
|
57
|
+
let entries = [];
|
|
58
|
+
try { entries = parseRoadmap(readFileSync(roadmapPath, 'utf-8')); } catch { return; }
|
|
59
|
+
for (const e of entries) {
|
|
60
|
+
if (!e.code || e.code.startsWith('_anon_')) continue;
|
|
61
|
+
if (have.has(e.code) || isExternal(e.code)) continue;
|
|
62
|
+
const item = store.createItem({
|
|
63
|
+
type: 'feature',
|
|
64
|
+
title: e.code,
|
|
65
|
+
description: e.description || '',
|
|
66
|
+
status: toVisionStatus(e.status),
|
|
67
|
+
phase: 'planning',
|
|
68
|
+
confidence: 0,
|
|
69
|
+
group: 'standalone',
|
|
70
|
+
});
|
|
71
|
+
try { store.updateLifecycle(item.id, { featureCode: e.code, currentPhase: 'explore_design' }); } catch { /* optional */ }
|
|
72
|
+
have.add(e.code);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Build the canonical graph inputs from committed source via a throwaway store.
|
|
78
|
+
* @param {string} cwd
|
|
79
|
+
* @returns {{ nodes, rawEdges, knownCodes, warnings }}
|
|
80
|
+
*/
|
|
81
|
+
export function collectVisionInputs(cwd) {
|
|
82
|
+
const dataDir = mkdtempSync(join(tmpdir(), 'rg2-canonical-'));
|
|
83
|
+
try {
|
|
84
|
+
const store = new VisionStore(dataDir);
|
|
85
|
+
const featuresDir = resolveFeaturesPath(cwd);
|
|
86
|
+
const externalPrefixes = loadExternalPrefixes(cwd);
|
|
87
|
+
// Canon: only feature.json-backed folders are managed features (mirrors
|
|
88
|
+
// collect.js's listFeatures universe — doc-only folders are not nodes).
|
|
89
|
+
const features = scanFeatures(featuresDir).filter((f) => f.hasFeatureJson);
|
|
90
|
+
seedFeatures(features, store);
|
|
91
|
+
seedRoadmapFallback(store, cwd, externalPrefixes);
|
|
92
|
+
|
|
93
|
+
// Preserve the dangling-edge refusal (the anti-typo lint kept from the
|
|
94
|
+
// static path). A deps.yaml edge to a code that resolves to no managed
|
|
95
|
+
// feature and isn't an external reference can't become a connection, so it
|
|
96
|
+
// would silently vanish; detect + refuse instead, mirroring collect.js.
|
|
97
|
+
const isExternal = (code) => externalPrefixes.some((p) => code.startsWith(p));
|
|
98
|
+
const known = new Set(
|
|
99
|
+
[...store.items.values()]
|
|
100
|
+
.filter((i) => i.type === 'feature')
|
|
101
|
+
.map((i) => i.lifecycle?.featureCode || i.title),
|
|
102
|
+
);
|
|
103
|
+
const dangling = [];
|
|
104
|
+
for (const f of features) {
|
|
105
|
+
if (!f.deps) continue;
|
|
106
|
+
for (const e of depsToEdges(f.name, f.deps)) {
|
|
107
|
+
const missing = [e.from, e.to].filter((c) => !known.has(c) && !isExternal(c));
|
|
108
|
+
if (missing.length) dangling.push({ from: e.from, to: e.to, kind: e.type, missing });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (dangling.length) throw new DanglingEdgeError(dangling);
|
|
112
|
+
|
|
113
|
+
return visionToGraphInputs(
|
|
114
|
+
[...store.items.values()],
|
|
115
|
+
[...store.connections.values()],
|
|
116
|
+
{ externalPrefixes, includeProseEdges: false },
|
|
117
|
+
);
|
|
118
|
+
} finally {
|
|
119
|
+
try { rmSync(dataDir, { recursive: true, force: true }); } catch { /* best-effort */ }
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Build HTML + stats from the canonical vision projection (no disk write). */
|
|
124
|
+
export function buildRoadmapGraph(cwd) {
|
|
125
|
+
return buildArtifactFromInputs(cwd, collectVisionInputs(cwd));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Generate + atomically write roadmap-graph.html from the canonical projection. */
|
|
129
|
+
export function generateRoadmapGraph(cwd, opts = {}) {
|
|
130
|
+
return writeArtifact(cwd, buildRoadmapGraph(cwd), opts);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Render in-memory and diff vs on-disk, from the canonical projection. */
|
|
134
|
+
export function checkRoadmapGraph(cwd, opts = {}) {
|
|
135
|
+
return checkArtifact(cwd, buildRoadmapGraph(cwd), opts);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export { DanglingEdgeError };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* smartmemory-routes.js — COMP-SMARTMEMORY-RECALL S01.
|
|
3
|
+
*
|
|
4
|
+
* Route:
|
|
5
|
+
* GET /api/smartmemory/recall?featureCode=<CODE> — ranked prior-context
|
|
6
|
+
* lookup for the cockpit's Recall tab, backed by INGEST's SmartMemory
|
|
7
|
+
* client + config (lib/smartmemory-client.js, lib/smartmemory-config.js).
|
|
8
|
+
*
|
|
9
|
+
* Degrade-never-fail (the qa-scope-routes.js model): every branch returns a
|
|
10
|
+
* shaped 200, never a throw. Flag OFF (or no workspace root) answers from
|
|
11
|
+
* config alone — zero client construction, zero SmartMemory-bound traffic.
|
|
12
|
+
*
|
|
13
|
+
* Not on the auth allowlist (server/index.js) — auth-gated in remote mode
|
|
14
|
+
* like the vision routes, since results can carry local project context.
|
|
15
|
+
*/
|
|
16
|
+
import { getSmartmemoryConfig, resolveProjectTag as defaultResolveProjectTag } from '../lib/smartmemory-config.js';
|
|
17
|
+
import { createSmartmemoryClient } from '../lib/smartmemory-client.js';
|
|
18
|
+
import { readFeature as defaultReadFeature } from '../lib/feature-json.js';
|
|
19
|
+
import { resolveFeaturesPath as defaultResolveFeaturesPath } from '../lib/project-paths.js';
|
|
20
|
+
|
|
21
|
+
function defaultCreateClient(cfg) {
|
|
22
|
+
return createSmartmemoryClient(cfg);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function shortReason(e) {
|
|
26
|
+
return e?.message?.slice(0, 200) || 'unreachable';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function safeProjectTag(resolveProjectTag, root) {
|
|
30
|
+
try {
|
|
31
|
+
return resolveProjectTag(root);
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Defensive adapter over the SmartMemory hit shape (multiple field-name fallbacks). */
|
|
38
|
+
function mapHit(hit) {
|
|
39
|
+
const content = hit?.content ?? hit?.text ?? '';
|
|
40
|
+
return {
|
|
41
|
+
id: hit?.id ?? hit?.item_id ?? null,
|
|
42
|
+
snippet: content.length > 280 ? content.slice(0, 280) + '…' : content,
|
|
43
|
+
score: hit?.score ?? null,
|
|
44
|
+
memoryType: hit?.memory_type ?? hit?.memoryType ?? null,
|
|
45
|
+
ts: hit?.context?.event?.ts ?? hit?.context?.ts ?? hit?.ts ?? null,
|
|
46
|
+
project: hit?.context?.project ?? null,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {import('express').Express} app
|
|
52
|
+
* @param {object} [deps]
|
|
53
|
+
* @param {Function} [deps.getConfig] — (cwd) => {} | {enabled,baseUrl,apiKeyEnv,timeoutMs}
|
|
54
|
+
* @param {Function} [deps.createClient] — (cfg) => { search(query, opts) }
|
|
55
|
+
* @param {Function} [deps.readFeature] — (cwd, code, featuresDir) => FeatureJson|null
|
|
56
|
+
* @param {Function} [deps.resolveFeaturesPath] — (cwd) => absolute features dir
|
|
57
|
+
* @param {Function} [deps.resolveProjectTag] — (cwd) => canonical project tag
|
|
58
|
+
*/
|
|
59
|
+
export function attachSmartmemoryRoutes(app, {
|
|
60
|
+
getConfig = getSmartmemoryConfig,
|
|
61
|
+
createClient = defaultCreateClient,
|
|
62
|
+
readFeature = defaultReadFeature,
|
|
63
|
+
resolveFeaturesPath = defaultResolveFeaturesPath,
|
|
64
|
+
resolveProjectTag = defaultResolveProjectTag,
|
|
65
|
+
} = {}) {
|
|
66
|
+
app.get('/api/smartmemory/recall', async (req, res) => {
|
|
67
|
+
const root = req.workspace?.root;
|
|
68
|
+
if (!root) {
|
|
69
|
+
return res.json({ enabled: false });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let cfg;
|
|
73
|
+
try {
|
|
74
|
+
cfg = getConfig(root);
|
|
75
|
+
} catch {
|
|
76
|
+
cfg = {};
|
|
77
|
+
}
|
|
78
|
+
if (cfg?.enabled !== true) {
|
|
79
|
+
return res.json({ enabled: false });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const featureCode = (req.query.featureCode || '').toString().trim();
|
|
83
|
+
if (!featureCode) {
|
|
84
|
+
return res.json({ enabled: true, available: true, results: [], invalidFeatureCode: true });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let feature;
|
|
88
|
+
try {
|
|
89
|
+
feature = readFeature(root, featureCode, resolveFeaturesPath(root));
|
|
90
|
+
} catch {
|
|
91
|
+
feature = null;
|
|
92
|
+
}
|
|
93
|
+
const desc = feature?.description || '';
|
|
94
|
+
const query = desc ? `${featureCode} ${desc}` : featureCode;
|
|
95
|
+
|
|
96
|
+
let hits;
|
|
97
|
+
try {
|
|
98
|
+
const client = createClient(cfg);
|
|
99
|
+
hits = await client.search(query, { top_k: 10 });
|
|
100
|
+
} catch (e) {
|
|
101
|
+
return res.json({ enabled: true, available: false, error: shortReason(e) });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const results = (Array.isArray(hits) ? hits : (hits?.results ?? [])).map(mapHit);
|
|
105
|
+
return res.json({
|
|
106
|
+
enabled: true,
|
|
107
|
+
available: true,
|
|
108
|
+
featureCode,
|
|
109
|
+
project: safeProjectTag(resolveProjectTag, root),
|
|
110
|
+
results,
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
@@ -21,13 +21,14 @@
|
|
|
21
21
|
|
|
22
22
|
import { readGateLog } from './gate-log-store.js';
|
|
23
23
|
import { isStaleLoop } from './open-loops-store.js';
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
import { allKnownPhases } from '../lib/lifecycle-modes.js';
|
|
25
|
+
|
|
26
|
+
// Known lifecycle phases — DERIVED from the mode registry (the union of every
|
|
27
|
+
// mode's phaseOrder ∪ terminal). This is a superset of the legacy build-only
|
|
28
|
+
// list, so non-build-mode phases (e.g. a fix lifecycle's `diagnose`) are no
|
|
29
|
+
// longer flagged "unrecognized". It still includes the terminal phases, which
|
|
30
|
+
// is required because the unknown-phase guard runs BEFORE the terminal branches.
|
|
31
|
+
const KNOWN_PHASES = new Set(allKnownPhases());
|
|
31
32
|
|
|
32
33
|
const TERMINAL_PHASES = new Set(['complete', 'killed']);
|
|
33
34
|
const MAX_SENTENCE = 280;
|
package/server/vision-routes.js
CHANGED
|
@@ -50,10 +50,13 @@ import { anchorBoundary } from '../lib/checkpoint/checkpoint-writer.js';
|
|
|
50
50
|
import { appendGateLogEntry, readGateLog, mapResolveOutcomeToSchema } from './gate-log-store.js';
|
|
51
51
|
import { addOpenLoop, resolveOpenLoop, listOpenLoops } from './open-loops-store.js';
|
|
52
52
|
import {
|
|
53
|
-
|
|
53
|
+
TERMINAL,
|
|
54
54
|
guardedTransition, ensureGuard, projectFeatureStatus,
|
|
55
55
|
verifyCompletionEvidence, guardTestCommand,
|
|
56
56
|
} from './lifecycle-guard.js';
|
|
57
|
+
import {
|
|
58
|
+
transitionsOf, skippableOf, completablePhaseOf, getMode, resolveMode,
|
|
59
|
+
} from '../lib/lifecycle-modes.js';
|
|
57
60
|
import { requireSensitiveOrPaired as requireSensitiveToken } from './security.js';
|
|
58
61
|
|
|
59
62
|
const PROJECT_ROOT = getTargetRoot();
|
|
@@ -214,9 +217,13 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
214
217
|
? resolveFeaturesPathFromConfig(projectRoot, loadComposeConfig(projectRoot))
|
|
215
218
|
: resolveProjectPath('features');
|
|
216
219
|
|
|
217
|
-
// Phase graph +
|
|
218
|
-
// source of truth shared with the STRAT-GUARD graph) — see COMP-MCP-ENFORCE
|
|
219
|
-
|
|
220
|
+
// Phase graph + skippable + terminal are owned by the mode registry (single
|
|
221
|
+
// source of truth shared with the STRAT-GUARD graph) — see COMP-MCP-ENFORCE /
|
|
222
|
+
// COMP-ROADMAP-MODES. Legality is resolved PER-ITEM from its lifecycle mode
|
|
223
|
+
// (build|fix|plan); legacy items with no mode default to build, so build
|
|
224
|
+
// transitions are byte-identical. typeToMode stamps the mode at creation.
|
|
225
|
+
const modeOf = (item) => item?.lifecycle?.mode ?? 'build';
|
|
226
|
+
const typeToMode = (type) => (type === 'bug' ? 'fix' : 'build');
|
|
220
227
|
const ITERATION_TYPES = new Set(['review', 'coverage']);
|
|
221
228
|
|
|
222
229
|
app.get('/api/vision/items/:id/lifecycle', (req, res) => {
|
|
@@ -251,9 +258,18 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
251
258
|
if (!item) return res.status(404).json({ error: `Item not found: ${req.params.id}` });
|
|
252
259
|
if (item.lifecycle) return res.status(400).json({ error: `Item ${req.params.id} already has a lifecycle` });
|
|
253
260
|
|
|
261
|
+
// COMP-ROADMAP-MODES / COMP-ROADMAP-PLAN: stamp the lifecycle mode. Prefer
|
|
262
|
+
// an explicit mode on the start payload (the CLI/VisionWriter forwards
|
|
263
|
+
// `mode` so a plan session is stamped mode='plan'), normalized through
|
|
264
|
+
// resolveMode (feature→build, bug→fix). Fall back to typeToMode(item.type)
|
|
265
|
+
// when the caller does not specify one — byte-identical to pre-PLAN for
|
|
266
|
+
// build AND bug. The genesis phase stays 'explore_design'.
|
|
267
|
+
const mode = req.body.mode != null ? resolveMode(req.body.mode) : typeToMode(item.type);
|
|
268
|
+
const genesis = 'explore_design';
|
|
254
269
|
const now = new Date().toISOString();
|
|
255
270
|
const lifecycle = {
|
|
256
|
-
currentPhase:
|
|
271
|
+
currentPhase: genesis,
|
|
272
|
+
mode,
|
|
257
273
|
featureCode,
|
|
258
274
|
startedAt: now,
|
|
259
275
|
completedAt: null,
|
|
@@ -261,22 +277,26 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
261
277
|
killReason: null,
|
|
262
278
|
};
|
|
263
279
|
// COMP-OBS-TIMELINE: populate phaseHistory before storing
|
|
264
|
-
appendPhaseHistory({ lifecycle }, { from: null, to:
|
|
280
|
+
appendPhaseHistory({ lifecycle }, { from: null, to: genesis, outcome: null, timestamp: now });
|
|
265
281
|
store.updateLifecycle(req.params.id, lifecycle);
|
|
266
282
|
// COMP-MCP-ENFORCE: eager guard registration seeded at the genesis phase
|
|
267
283
|
// (the clean bootstrap path; backfill for pre-rollout items happens lazily
|
|
268
284
|
// on first transition). Best-effort — a registration hiccup must not block
|
|
269
285
|
// starting a lifecycle; the next guarded transition re-attempts ensureGuard.
|
|
270
286
|
if (guardEnabled) {
|
|
271
|
-
try { await ensureGuard(featureCode,
|
|
287
|
+
try { await ensureGuard(featureCode, genesis, projectRoot, mode); }
|
|
272
288
|
catch (e) { console.warn(`[lifecycle/start] guard register for ${featureCode} failed: ${e.message}`); }
|
|
273
|
-
// Slice 2: starting a lifecycle projects
|
|
289
|
+
// Slice 2: starting a lifecycle projects the genesis phase → IN_PROGRESS so
|
|
274
290
|
// the first active phase is not left stuck at PLANNED in feature.json.
|
|
275
|
-
|
|
291
|
+
// COMP-ROADMAP-PLAN: skipped for tracksFeatureJson:false modes (e.g. a
|
|
292
|
+
// plan session has no feature.json to project onto).
|
|
293
|
+
if (getMode(mode).runner.tracksFeatureJson) {
|
|
294
|
+
await projectFeatureStatus({ featureCode, phase: genesis, cwd: projectRoot });
|
|
295
|
+
}
|
|
276
296
|
}
|
|
277
297
|
scheduleBroadcast();
|
|
278
|
-
broadcastMessage({ type: 'lifecycleStarted', itemId: req.params.id, phase:
|
|
279
|
-
emitDecisionEvent(broadcastMessage, buildPhaseTransitionEvent({ featureCode, from: null, to:
|
|
298
|
+
broadcastMessage({ type: 'lifecycleStarted', itemId: req.params.id, phase: genesis, featureCode, timestamp: now });
|
|
299
|
+
emitDecisionEvent(broadcastMessage, buildPhaseTransitionEvent({ featureCode, from: null, to: genesis, outcome: null, timestamp: now }));
|
|
280
300
|
// COMP-OBS-DRIFT: emit drift axes before status so STATUS reads fresh drift_axes
|
|
281
301
|
const startedItem = store.items.get(req.params.id);
|
|
282
302
|
if (startedItem) emitDriftAxes(broadcastMessage, store, startedItem, projectRoot, now);
|
|
@@ -328,12 +348,12 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
328
348
|
if (!item?.lifecycle) return res.status(404).json({ error: 'No lifecycle on this item' });
|
|
329
349
|
const from = item.lifecycle.currentPhase;
|
|
330
350
|
if (TERMINAL.has(from)) return res.status(400).json({ error: `Cannot advance from terminal state: ${from}` });
|
|
331
|
-
const valid =
|
|
351
|
+
const valid = transitionsOf(modeOf(item))[from];
|
|
332
352
|
if (!valid?.includes(targetPhase)) return res.status(400).json({ error: `Invalid transition: ${from} → ${targetPhase}` });
|
|
333
353
|
|
|
334
354
|
// COMP-MCP-ENFORCE: verdict-gate the transition (fail-closed) before mutating.
|
|
335
355
|
if (guardEnabled) {
|
|
336
|
-
const g = await guardedTransition({ featureCode: item.lifecycle.featureCode, from, to: targetPhase, workspaceRoot: projectRoot, resolvedBy: 'agent' });
|
|
356
|
+
const g = await guardedTransition({ featureCode: item.lifecycle.featureCode, from, to: targetPhase, workspaceRoot: projectRoot, resolvedBy: 'agent', mode: modeOf(item) });
|
|
337
357
|
if (!g.applied) return res.status(422).json({ error: 'transition refused by guard', from, to: targetPhase, verdict: g.verdict, guardError: g.error });
|
|
338
358
|
}
|
|
339
359
|
|
|
@@ -344,7 +364,8 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
344
364
|
store.updateLifecycle(req.params.id, item.lifecycle);
|
|
345
365
|
// COMP-MCP-ENFORCE Slice 2 (lifecycle-as-truth): project the new phase onto
|
|
346
366
|
// feature.json STATUS (best-effort; idempotent — only writes on a real change).
|
|
347
|
-
|
|
367
|
+
// COMP-ROADMAP-PLAN: skipped for tracksFeatureJson:false modes (no feature.json).
|
|
368
|
+
if (guardEnabled && getMode(modeOf(item)).runner.tracksFeatureJson) await projectFeatureStatus({ featureCode: item.lifecycle.featureCode, phase: targetPhase, cwd: projectRoot });
|
|
348
369
|
scheduleBroadcast();
|
|
349
370
|
broadcastMessage({ type: 'lifecycleTransition', itemId: req.params.id, from, to: targetPhase, outcome, timestamp: now });
|
|
350
371
|
emitDecisionEvent(broadcastMessage, buildPhaseTransitionEvent({ featureCode: item.lifecycle.featureCode, from, to: targetPhase, outcome, timestamp: now }));
|
|
@@ -368,13 +389,13 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
368
389
|
if (!item?.lifecycle) return res.status(404).json({ error: 'No lifecycle on this item' });
|
|
369
390
|
const from = item.lifecycle.currentPhase;
|
|
370
391
|
if (TERMINAL.has(from)) return res.status(400).json({ error: `Cannot skip from terminal state: ${from}` });
|
|
371
|
-
if (!
|
|
372
|
-
const valid =
|
|
392
|
+
if (!skippableOf(modeOf(item)).includes(from)) return res.status(400).json({ error: `Phase ${from} is not skippable` });
|
|
393
|
+
const valid = transitionsOf(modeOf(item))[from];
|
|
373
394
|
if (!valid?.includes(targetPhase)) return res.status(400).json({ error: `Invalid transition: ${from} → ${targetPhase}` });
|
|
374
395
|
|
|
375
396
|
// COMP-MCP-ENFORCE: verdict-gate the skip (fail-closed) before mutating.
|
|
376
397
|
if (guardEnabled) {
|
|
377
|
-
const g = await guardedTransition({ featureCode: item.lifecycle.featureCode, from, to: targetPhase, workspaceRoot: projectRoot, resolvedBy: 'agent' });
|
|
398
|
+
const g = await guardedTransition({ featureCode: item.lifecycle.featureCode, from, to: targetPhase, workspaceRoot: projectRoot, resolvedBy: 'agent', mode: modeOf(item) });
|
|
378
399
|
if (!g.applied) return res.status(422).json({ error: 'transition refused by guard', from, to: targetPhase, verdict: g.verdict, guardError: g.error });
|
|
379
400
|
}
|
|
380
401
|
|
|
@@ -384,7 +405,8 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
384
405
|
appendPhaseHistory(item, { from, to: targetPhase, outcome: 'skipped', timestamp: now });
|
|
385
406
|
store.updateLifecycle(req.params.id, item.lifecycle);
|
|
386
407
|
// COMP-MCP-ENFORCE Slice 2 (lifecycle-as-truth): project phase → STATUS.
|
|
387
|
-
|
|
408
|
+
// COMP-ROADMAP-PLAN: skipped for tracksFeatureJson:false modes (no feature.json).
|
|
409
|
+
if (guardEnabled && getMode(modeOf(item)).runner.tracksFeatureJson) await projectFeatureStatus({ featureCode: item.lifecycle.featureCode, phase: targetPhase, cwd: projectRoot });
|
|
388
410
|
scheduleBroadcast();
|
|
389
411
|
broadcastMessage({ type: 'lifecycleTransition', itemId: req.params.id, from, to: targetPhase, outcome: 'skipped', timestamp: now });
|
|
390
412
|
emitDecisionEvent(broadcastMessage, buildPhaseTransitionEvent({ featureCode: item.lifecycle.featureCode, from, to: targetPhase, outcome: 'skipped', timestamp: now }));
|
|
@@ -414,7 +436,7 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
414
436
|
// is unreachable — consistent with advance/skip/complete. Authorized
|
|
415
437
|
// bypass remains stratum_guard_override (Slice 3).
|
|
416
438
|
if (guardEnabled) {
|
|
417
|
-
const g = await guardedTransition({ featureCode: item.lifecycle.featureCode, from, to: 'killed', workspaceRoot: projectRoot, resolvedBy: 'agent' });
|
|
439
|
+
const g = await guardedTransition({ featureCode: item.lifecycle.featureCode, from, to: 'killed', workspaceRoot: projectRoot, resolvedBy: 'agent', mode: modeOf(item) });
|
|
418
440
|
if (!g.applied) return res.status(422).json({ error: 'kill refused by guard', from, to: 'killed', verdict: g.verdict, guardError: g.error });
|
|
419
441
|
}
|
|
420
442
|
|
|
@@ -428,7 +450,8 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
428
450
|
store.updateItem(req.params.id, { status: 'killed' });
|
|
429
451
|
// COMP-MCP-ENFORCE Slice 2: project kill → KILLED onto feature.json
|
|
430
452
|
// (closes the COMP-PARITY-7 gap — kill previously wrote vision-state only).
|
|
431
|
-
|
|
453
|
+
// COMP-ROADMAP-PLAN: skipped for tracksFeatureJson:false modes (no feature.json).
|
|
454
|
+
if (guardEnabled && getMode(modeOf(item)).runner.tracksFeatureJson) await projectFeatureStatus({ featureCode: item.lifecycle.featureCode, phase: 'killed', cwd: projectRoot });
|
|
432
455
|
scheduleBroadcast();
|
|
433
456
|
broadcastMessage({ type: 'lifecycleTransition', itemId: req.params.id, from, to: 'killed', outcome: 'killed', timestamp: now });
|
|
434
457
|
emitDecisionEvent(broadcastMessage, buildPhaseTransitionEvent({ featureCode: item.lifecycle.featureCode, from, to: 'killed', outcome: 'killed', timestamp: now }));
|
|
@@ -449,8 +472,11 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
449
472
|
try {
|
|
450
473
|
const item = store.items.get(req.params.id);
|
|
451
474
|
if (!item?.lifecycle) return res.status(404).json({ error: 'No lifecycle on this item' });
|
|
452
|
-
|
|
453
|
-
|
|
475
|
+
// COMP-ROADMAP-MODES: the phase that completes is the mode's completable
|
|
476
|
+
// phase (build → 'ship', byte-identical), not a hard-coded literal.
|
|
477
|
+
const completable = completablePhaseOf(modeOf(item));
|
|
478
|
+
if (item.lifecycle.currentPhase !== completable) {
|
|
479
|
+
return res.status(400).json({ error: `Can only complete from ${completable} phase, currently in: ${item.lifecycle.currentPhase}` });
|
|
454
480
|
}
|
|
455
481
|
|
|
456
482
|
// COMP-MCP-ENFORCE Slice 3: evidence-bound completion. Under the guard,
|
|
@@ -471,15 +497,15 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
471
497
|
}
|
|
472
498
|
verifiedTestsPass = ev.testsAttested ? true : (req.body?.tests_pass === true);
|
|
473
499
|
|
|
474
|
-
const g = await guardedTransition({ featureCode: item.lifecycle.featureCode, from:
|
|
475
|
-
if (!g.applied) return res.status(422).json({ error: 'completion refused by guard', from:
|
|
500
|
+
const g = await guardedTransition({ featureCode: item.lifecycle.featureCode, from: completable, to: 'complete', workspaceRoot: projectRoot, commitSha: req.body?.commit_sha, resolvedBy: 'agent', mode: modeOf(item) });
|
|
501
|
+
if (!g.applied) return res.status(422).json({ error: 'completion refused by guard', from: completable, to: 'complete', verdict: g.verdict, guardError: g.error });
|
|
476
502
|
}
|
|
477
503
|
|
|
478
504
|
const now = new Date().toISOString();
|
|
479
505
|
item.lifecycle.currentPhase = 'complete';
|
|
480
506
|
item.lifecycle.completedAt = now;
|
|
481
507
|
// COMP-OBS-TIMELINE: populate phaseHistory + emit phase_transition DecisionEvent
|
|
482
|
-
appendPhaseHistory(item, { from:
|
|
508
|
+
appendPhaseHistory(item, { from: completable, to: 'complete', outcome: 'approved', timestamp: now });
|
|
483
509
|
store.updateLifecycle(req.params.id, item.lifecycle);
|
|
484
510
|
store.updateItem(req.params.id, { status: 'complete' });
|
|
485
511
|
// COMP-MCP-ENFORCE Slice 2: status projection for the no-commit path is
|
|
@@ -487,8 +513,8 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
487
513
|
// recordCompletion bridge, which is the authority + partial-write reporter
|
|
488
514
|
// on the commit_sha path.
|
|
489
515
|
scheduleBroadcast();
|
|
490
|
-
broadcastMessage({ type: 'lifecycleTransition', itemId: req.params.id, from:
|
|
491
|
-
emitDecisionEvent(broadcastMessage, buildPhaseTransitionEvent({ featureCode: item.lifecycle.featureCode, from:
|
|
516
|
+
broadcastMessage({ type: 'lifecycleTransition', itemId: req.params.id, from: completable, to: 'complete', outcome: 'approved', timestamp: now });
|
|
517
|
+
emitDecisionEvent(broadcastMessage, buildPhaseTransitionEvent({ featureCode: item.lifecycle.featureCode, from: completable, to: 'complete', outcome: 'approved', timestamp: now }));
|
|
492
518
|
// COMP-OBS-DRIFT: emit drift axes before status so STATUS reads fresh drift_axes
|
|
493
519
|
emitDriftAxes(broadcastMessage, store, item, projectRoot, now);
|
|
494
520
|
// COMP-OBS-STATUS: emit status snapshot after lifecycle transition (complete)
|
|
@@ -545,7 +571,8 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
545
571
|
} catch { /* decision event emit best-effort */ }
|
|
546
572
|
// No recordCompletion bridge ran — project complete → COMPLETE so
|
|
547
573
|
// lifecycle-as-truth still reaches feature.json (best-effort).
|
|
548
|
-
|
|
574
|
+
// COMP-ROADMAP-PLAN: skipped for tracksFeatureJson:false modes (no feature.json).
|
|
575
|
+
if (guardEnabled && getMode(modeOf(item)).runner.tracksFeatureJson) await projectFeatureStatus({ featureCode, phase: 'complete', cwd: projectRoot });
|
|
549
576
|
}
|
|
550
577
|
}
|
|
551
578
|
|
|
@@ -756,7 +783,9 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
756
783
|
});
|
|
757
784
|
|
|
758
785
|
// ── Artifact endpoints ───────────────────────────────────────────────
|
|
759
|
-
|
|
786
|
+
// ArtifactManager is constructed per-request from the item's lifecycle mode —
|
|
787
|
+
// the artifact metadata it assesses/scaffolds is mode-scoped (the root stays
|
|
788
|
+
// features-rooted; mode-owned root resolution is a deferred follow-up).
|
|
760
789
|
|
|
761
790
|
app.get('/api/vision/items/:id/artifacts', (req, res) => {
|
|
762
791
|
try {
|
|
@@ -765,7 +794,7 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
765
794
|
if (!item.lifecycle?.featureCode) {
|
|
766
795
|
return res.status(400).json({ error: 'Item has no lifecycle featureCode' });
|
|
767
796
|
}
|
|
768
|
-
const assessment =
|
|
797
|
+
const assessment = new ArtifactManager(featuresPath, item.lifecycle?.mode).assess(item.lifecycle.featureCode);
|
|
769
798
|
res.json(assessment);
|
|
770
799
|
} catch (err) {
|
|
771
800
|
res.status(500).json({ error: err.message });
|
|
@@ -779,7 +808,7 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
779
808
|
if (!item.lifecycle?.featureCode) {
|
|
780
809
|
return res.status(400).json({ error: 'Item has no lifecycle featureCode' });
|
|
781
810
|
}
|
|
782
|
-
const result =
|
|
811
|
+
const result = new ArtifactManager(featuresPath, item.lifecycle?.mode).scaffold(item.lifecycle.featureCode, req.body);
|
|
783
812
|
res.json(result);
|
|
784
813
|
} catch (err) {
|
|
785
814
|
res.status(400).json({ error: err.message });
|
|
@@ -828,9 +857,11 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
828
857
|
if (existing) {
|
|
829
858
|
return res.status(200).json(existing); // idempotent
|
|
830
859
|
}
|
|
831
|
-
// Dedup: if a pending gate already exists for the same item+step
|
|
860
|
+
// Dedup: if a pending gate already exists for the same item+step in THIS
|
|
861
|
+
// flow, reuse it. Scoped to flowId so a fresh run never inherits a stale
|
|
862
|
+
// pending gate from a prior crashed/aborted run for the same feature.
|
|
832
863
|
if (itemId) {
|
|
833
|
-
const dupGate = store.findPendingGate(itemId, stepId);
|
|
864
|
+
const dupGate = store.findPendingGate(itemId, stepId, flowId);
|
|
834
865
|
if (dupGate) {
|
|
835
866
|
return res.status(200).json(dupGate);
|
|
836
867
|
}
|
package/server/vision-server.js
CHANGED
|
@@ -30,6 +30,7 @@ import { attachSettingsRoutes } from './settings-routes.js';
|
|
|
30
30
|
import { attachHealthRoutes } from './health-routes.js';
|
|
31
31
|
import { attachValidateRoutes } from './validate-routes.js';
|
|
32
32
|
import { attachQaScopeRoutes } from './qa-scope-routes.js';
|
|
33
|
+
import { attachSmartmemoryRoutes } from './smartmemory-routes.js';
|
|
33
34
|
import { attachDesignRoutes } from './design-routes.js';
|
|
34
35
|
import { DesignSessionManager } from './design-session.js';
|
|
35
36
|
import { attachPipelineRoutes } from './pipeline-routes.js';
|
|
@@ -102,6 +103,11 @@ export class VisionServer {
|
|
|
102
103
|
// Read-only GET /api/qa-scope?featureCode=…; reads off req.workspace.
|
|
103
104
|
attachQaScopeRoutes(app);
|
|
104
105
|
|
|
106
|
+
// ── SmartMemory recall (COMP-SMARTMEMORY-RECALL, opt-in) ───────────────
|
|
107
|
+
// Read-only GET /api/smartmemory/recall?featureCode=…; degrade-never-fail,
|
|
108
|
+
// answers {enabled:false} from config alone when the flag is OFF.
|
|
109
|
+
attachSmartmemoryRoutes(app);
|
|
110
|
+
|
|
105
111
|
// ── Vision CRUD + lifecycle routes ─────────────────────────────────────
|
|
106
112
|
attachVisionRoutes(app, {
|
|
107
113
|
store: this.store,
|
package/server/vision-store.js
CHANGED
|
@@ -200,7 +200,7 @@ export class VisionStore {
|
|
|
200
200
|
throw new Error(`Invalid phase: ${updates.phase}`);
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
const allowed = ['type', 'title', 'description', 'confidence', 'status', 'phase', 'position', 'parentId', 'summary', 'files', 'featureCode', 'stratumFlowId', 'evidence', 'group'];
|
|
203
|
+
const allowed = ['type', 'title', 'description', 'confidence', 'status', 'phase', 'position', 'parentId', 'summary', 'files', 'featureCode', 'stratumFlowId', 'evidence', 'group', 'priority'];
|
|
204
204
|
for (const key of allowed) {
|
|
205
205
|
if (updates[key] !== undefined) {
|
|
206
206
|
item[key] = updates[key];
|
|
@@ -349,10 +349,19 @@ export class VisionStore {
|
|
|
349
349
|
return this.gates.get(gateId) || null;
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
-
/**
|
|
353
|
-
|
|
352
|
+
/**
|
|
353
|
+
* Find an existing pending gate for the same item+step within the same flow.
|
|
354
|
+
*
|
|
355
|
+
* The flowId guard prevents a fresh run (new flowId) from reusing a stale
|
|
356
|
+
* pending gate left by a prior crashed/aborted run for the same feature — that
|
|
357
|
+
* gate is never resolved, so reusing it would poll until the server TTL
|
|
358
|
+
* expires it and then error. A null flowId falls back to the legacy
|
|
359
|
+
* item+step match (callers that don't track a flow).
|
|
360
|
+
*/
|
|
361
|
+
findPendingGate(itemId, stepId, flowId = null) {
|
|
354
362
|
for (const gate of this.gates.values()) {
|
|
355
|
-
if (gate.status === 'pending' && gate.itemId === itemId && gate.stepId === stepId
|
|
363
|
+
if (gate.status === 'pending' && gate.itemId === itemId && gate.stepId === stepId
|
|
364
|
+
&& (flowId == null || gate.flowId === flowId)) {
|
|
356
365
|
return gate;
|
|
357
366
|
}
|
|
358
367
|
}
|