@smartmemory/compose 0.1.1-beta → 0.1.2-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/bug-fix/SKILL.md +143 -0
- package/.claude/skills/compose/SKILL.md +604 -0
- package/.compose-deps.json +89 -0
- package/README.md +14 -3
- package/bin/compose.js +473 -0
- package/contracts/comp-obs-contract.schema.json +362 -0
- package/contracts/cross-model-review-result.json +78 -0
- package/contracts/review-result.json +126 -0
- package/dist/assets/{_baseUniq-CQwX6VLz.js → _baseUniq-D-avYfn5.js} +1 -1
- package/dist/assets/{arc-SxJ2J1sh.js → arc-BC4dfQ-X.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-BykunY1F.js → architectureDiagram-Q4EWVU46-BZmFXnGI.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-ohAKBOUw.js → blockDiagram-DXYQGD6D-DlfWSuux.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-DBDC3ENB.js → c4Diagram-AHTNJAMY-Y__uJrRx.js} +1 -1
- package/dist/assets/channel-LRG9kHqJ.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-Cv93Z7uM.js → chunk-4BX2VUAB-BfMePfTp.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-DE0WBDkj.js → chunk-4TB4RGXK-BdlMSdEA.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-CE1EXenG.js → chunk-55IACEB6-vrQHZTdv.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-DA7Ana6H.js → chunk-EDXVE4YY-B8wioVlW.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-CTDIPA3p.js → chunk-FMBD7UC4-Cd6Hrux2.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-uGBaPaTX.js → chunk-OYMX7WX6-CfrhdQXY.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-CYlnXuUO.js → chunk-QZHKN3VN-B9JQerOU.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-ojGkzcZK.js → chunk-YZCP3GAM-DFN9X99H.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-BC9a6pDE.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-BC9a6pDE.js +1 -0
- package/dist/assets/clone-dRxgFrBv.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-Bktn9hL-.js → cose-bilkent-S5V4N54A-BAn0ap_E.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-DFaSzuRF.js → dagre-KV5264BT-DyxnVq1g.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-DnfmDzEm.js → diagram-5BDNPKRD-XCrzqski.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-Bm8W9YnG.js → diagram-G4DWMVQ6-MBCAXft_.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-B5-TSKvp.js → diagram-MMDJMWI5-DbtB2yS6.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-ls4rqlky.js → diagram-TYMM5635-Bb5NzX61.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-giG6WO-r.js → erDiagram-SMLLAGMA-CpIeCOh2.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-XvlUuz-7.js → flowDiagram-DWJPFMVM-CHyoKnhW.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-hLBV57oV.js → ganttDiagram-T4ZO3ILL-DErKteO_.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-BHu3s_Gn.js → gitGraphDiagram-UUTBAWPF-KFVAtj2F.js} +1 -1
- package/dist/assets/{graph-D0Cfv00Y.js → graph-CRnO_ifT.js} +1 -1
- package/dist/assets/index-DKBsEUJ-.css +1 -0
- package/dist/assets/index-DkRKLuNr.js +1144 -0
- package/dist/assets/{infoDiagram-42DDH7IO-DbqRsOo3.js → infoDiagram-42DDH7IO-BZFnuSp5.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-DnCdx7zb.js → ishikawaDiagram-UXIWVN3A-4Xe2Szde.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-CfD7eNcP.js → journeyDiagram-VCZTEJTY-CZRByfS-.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-BYaO9-mK.js → kanban-definition-6JOO6SKY-B95sk6Fk.js} +1 -1
- package/dist/assets/{layout-Bj72wOEB.js → layout-BqNQzxWT.js} +1 -1
- package/dist/assets/{linear-BRFo114D.js → linear-CUh7qb64.js} +1 -1
- package/dist/assets/{min-GCHnKlJS.js → min-wXgOS3ig.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-n0PMebY4.js → mindmap-definition-QFDTVHPH-DB6iaAbO.js} +1 -1
- package/dist/assets/{pieDiagram-DEJITSTG-pN4CljHF.js → pieDiagram-DEJITSTG-CHkZHrTW.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-DNoAy8-D.js → quadrantDiagram-34T5L4WZ-DoTEO8e3.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-BhtY05PT.js → requirementDiagram-MS252O5E-Dn8peXYp.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-B6AD-16A.js → sankeyDiagram-XADWPNL6-DRXs6Ipb.js} +1 -1
- package/dist/assets/{sequenceDiagram-FGHM5R23-DShHM-uk.js → sequenceDiagram-FGHM5R23-wBBYZ0aq.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-DMxn7HTo.js → stateDiagram-FHFEXIEX-DPlBNGmf.js} +1 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-BW0ezXb4.js +1 -0
- package/dist/assets/{timeline-definition-GMOUNBTQ-Cdu6uq52.js → timeline-definition-GMOUNBTQ-CbbyTlHk.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-CpK29iRe.js → vennDiagram-DHZGUBPP-Bj4GaFfj.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-BQgSkdcO.js → wardley-RL74JXVD-RtNzq8KU.js} +55 -55
- package/dist/assets/{wardleyDiagram-NUSXRM2D-DJHYev6O.js → wardleyDiagram-NUSXRM2D-CDfE3zSj.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-1d75pbaO.js → xychartDiagram-5P7HB3ND-CZXHHYD5.js} +1 -1
- package/dist/index.html +2 -2
- package/lib/budget-ledger.js +45 -0
- package/lib/bug-bisect.js +292 -0
- package/lib/bug-checkpoint.js +191 -0
- package/lib/bug-escalation.js +306 -0
- package/lib/bug-index-gen.js +136 -0
- package/lib/bug-ledger.js +126 -0
- package/lib/build-stream-schema.js +176 -0
- package/lib/build-stream-writer.js +3 -1
- package/lib/build.js +854 -284
- package/lib/connector-factory-shim.js +167 -0
- package/lib/constants.js +18 -0
- package/lib/debug-discipline.js +176 -27
- package/lib/deps.js +205 -0
- package/lib/health-score.js +4 -4
- package/lib/import.js +26 -13
- package/lib/inject-schema.js +21 -0
- package/lib/new.js +27 -53
- package/lib/result-normalizer.js +160 -144
- package/lib/review-lenses.js +5 -5
- package/lib/review-normalize.js +413 -0
- package/lib/review-prompt.js +163 -0
- package/lib/sections.js +325 -0
- package/lib/step-prompt.js +21 -1
- package/lib/step-validator.js +5 -3
- package/lib/stratum-mcp-client.js +172 -7
- package/package.json +14 -3
- package/pipelines/bug-fix.stratum.yaml +39 -1
- package/pipelines/build.stratum.yaml +28 -45
- package/pipelines/review-fix.stratum.yaml +1 -1
- package/presets/team-review.stratum.yaml +21 -14
- package/server/build-stream-bridge.js +28 -0
- package/server/cc-session-feature-resolver.js +111 -0
- package/server/cc-session-reader.js +327 -0
- package/server/cc-session-watcher.js +318 -0
- package/server/compose-mcp-tools.js +0 -125
- package/server/compose-mcp.js +2 -4
- package/server/contract-diff.js +192 -0
- package/server/decision-event-emit.js +175 -0
- package/server/decision-event-id.js +64 -0
- package/server/decision-events-snapshot.js +166 -0
- package/server/design-routes.js +92 -49
- package/server/drift-axes.js +365 -0
- package/server/drift-emit.js +121 -0
- package/server/gate-log-store.js +102 -0
- package/server/lifecycle-phase-history.js +44 -0
- package/server/open-loops-store.js +102 -0
- package/server/schema-validator.js +49 -0
- package/server/status-emit.js +27 -0
- package/server/status-snapshot.js +218 -0
- package/server/vision-routes.js +332 -4
- package/server/vision-server.js +104 -12
- package/server/vision-store.js +21 -0
- package/dist/assets/channel-DGElom1e.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-KqWP9wWZ.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-KqWP9wWZ.js +0 -1
- package/dist/assets/clone-DUJKJXd7.js +0 -1
- package/dist/assets/index-CUd6pFGF.css +0 -1
- package/dist/assets/index-DReRlzZI.js +0 -1144
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-o6PnCs4e.js +0 -1
- package/server/connectors/agent-connector.js +0 -78
- package/server/connectors/claude-sdk-connector.js +0 -198
- package/server/connectors/codex-connector.js +0 -240
- package/server/connectors/connector-discovery.js +0 -18
- package/server/connectors/connector-runtime.js +0 -13
- package/server/connectors/opencode-connector.js +0 -200
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* open-loops-store.js — COMP-OBS-LOOPS persistence layer.
|
|
3
|
+
*
|
|
4
|
+
* Reads/writes item.lifecycle.lifecycle_ext.open_loops[] via the existing
|
|
5
|
+
* updateLifecycleExt path (same as BRANCH for branch_lineage).
|
|
6
|
+
*
|
|
7
|
+
* Design decisions:
|
|
8
|
+
* - Server fills id (UUID v4), created_at, parent_feature from item.lifecycle.featureCode.
|
|
9
|
+
* - parent_feature is REQUIRED by schema — route returns 400 if item lacks featureCode.
|
|
10
|
+
* - Append-only: resolution is set in-place; entries are never deleted.
|
|
11
|
+
* - isStaleLoop is exported here and imported by status-snapshot.js so the
|
|
12
|
+
* panel and STATUS agree on the stale predicate (Decision 3).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { randomUUID } from 'node:crypto';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Returns true if a loop is unresolved and past its TTL.
|
|
19
|
+
* @param {object} loop — OpenLoop object
|
|
20
|
+
* @param {number} nowMs — epoch ms (Date.parse result or Date.now())
|
|
21
|
+
*/
|
|
22
|
+
export function isStaleLoop(loop, nowMs) {
|
|
23
|
+
if (loop.resolution) return false; // resolved — never stale
|
|
24
|
+
const ttl = loop.ttl_days ?? 90;
|
|
25
|
+
const ageMs = nowMs - Date.parse(loop.created_at);
|
|
26
|
+
return ageMs > ttl * 24 * 60 * 60 * 1000;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Add an OpenLoop to an item.
|
|
31
|
+
* The caller must persist the result via store.updateLifecycleExt(item.id, 'open_loops', next).
|
|
32
|
+
*
|
|
33
|
+
* @param {object} item — vision store item (must have lifecycle.featureCode)
|
|
34
|
+
* @param {{ kind, summary, parent_branch?, ttl_days? }} fields
|
|
35
|
+
* @returns {{ loop: OpenLoop, nextLoops: OpenLoop[] }}
|
|
36
|
+
* @throws {Error} if item has no featureCode (400 guard)
|
|
37
|
+
*/
|
|
38
|
+
export function addOpenLoop(item, { kind, summary, parent_branch, ttl_days }) {
|
|
39
|
+
const featureCode = item.lifecycle?.featureCode;
|
|
40
|
+
if (!featureCode) {
|
|
41
|
+
throw Object.assign(new Error('item has no lifecycle.featureCode — cannot create feature-scoped open loop'), { status: 400 });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const loop = {
|
|
45
|
+
id: randomUUID(),
|
|
46
|
+
kind,
|
|
47
|
+
summary,
|
|
48
|
+
created_at: new Date().toISOString(),
|
|
49
|
+
parent_feature: featureCode,
|
|
50
|
+
parent_branch: parent_branch ?? null,
|
|
51
|
+
resolution: null,
|
|
52
|
+
ttl_days: ttl_days ?? 90,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const existing = item.lifecycle?.lifecycle_ext?.open_loops ?? [];
|
|
56
|
+
const nextLoops = [...existing, loop];
|
|
57
|
+
return { loop, nextLoops };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resolve an open loop in place.
|
|
62
|
+
* @param {object} item — vision store item
|
|
63
|
+
* @param {string} loopId
|
|
64
|
+
* @param {{ note: string, resolved_by: string }} resolution
|
|
65
|
+
* @returns {{ loop: OpenLoop, nextLoops: OpenLoop[] }}
|
|
66
|
+
* @throws {Error} if loopId not found (404) or already resolved (400)
|
|
67
|
+
*/
|
|
68
|
+
export function resolveOpenLoop(item, loopId, { note, resolved_by }) {
|
|
69
|
+
const existing = item.lifecycle?.lifecycle_ext?.open_loops ?? [];
|
|
70
|
+
const idx = existing.findIndex(l => l.id === loopId);
|
|
71
|
+
if (idx === -1) {
|
|
72
|
+
throw Object.assign(new Error(`loop not found: ${loopId}`), { status: 404 });
|
|
73
|
+
}
|
|
74
|
+
const loop = existing[idx];
|
|
75
|
+
if (loop.resolution !== null && loop.resolution !== undefined) {
|
|
76
|
+
throw Object.assign(new Error(`loop already resolved: ${loopId}`), { status: 400 });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const resolved = {
|
|
80
|
+
...loop,
|
|
81
|
+
resolution: {
|
|
82
|
+
resolved_at: new Date().toISOString(),
|
|
83
|
+
resolved_by: resolved_by || 'unknown',
|
|
84
|
+
note: note || '',
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const nextLoops = existing.map((l, i) => (i === idx ? resolved : l));
|
|
89
|
+
return { loop: resolved, nextLoops };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* List open loops for an item.
|
|
94
|
+
* @param {object} item
|
|
95
|
+
* @param {{ includeResolved?: boolean }} opts
|
|
96
|
+
* @returns {OpenLoop[]}
|
|
97
|
+
*/
|
|
98
|
+
export function listOpenLoops(item, { includeResolved = false } = {}) {
|
|
99
|
+
const loops = item.lifecycle?.lifecycle_ext?.open_loops ?? [];
|
|
100
|
+
if (includeResolved) return [...loops];
|
|
101
|
+
return loops.filter(l => l.resolution == null);
|
|
102
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import Ajv from 'ajv';
|
|
5
|
+
import addFormats from 'ajv-formats';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const SCHEMA_PATH = resolve(__dirname, '../contracts/comp-obs-contract.schema.json');
|
|
9
|
+
|
|
10
|
+
let cached = null;
|
|
11
|
+
|
|
12
|
+
function load() {
|
|
13
|
+
if (cached) return cached;
|
|
14
|
+
const schema = JSON.parse(readFileSync(SCHEMA_PATH, 'utf8'));
|
|
15
|
+
const ajv = new Ajv({ strict: false, allErrors: true });
|
|
16
|
+
addFormats(ajv);
|
|
17
|
+
ajv.addSchema(schema);
|
|
18
|
+
cached = { schema, ajv };
|
|
19
|
+
return cached;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class SchemaValidator {
|
|
23
|
+
constructor() {
|
|
24
|
+
const { schema, ajv } = load();
|
|
25
|
+
this.schema = schema;
|
|
26
|
+
this.ajv = ajv;
|
|
27
|
+
this._validators = new Map();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_getValidator(defName) {
|
|
31
|
+
if (this._validators.has(defName)) return this._validators.get(defName);
|
|
32
|
+
if (!this.schema.definitions || !(defName in this.schema.definitions)) {
|
|
33
|
+
throw new Error(`unknown schema definition: ${defName}`);
|
|
34
|
+
}
|
|
35
|
+
const ref = `${this.schema.$id}#/definitions/${defName}`;
|
|
36
|
+
let v = this.ajv.getSchema(ref);
|
|
37
|
+
if (!v) v = this.ajv.compile({ $ref: ref });
|
|
38
|
+
this._validators.set(defName, v);
|
|
39
|
+
return v;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
validate(defName, obj) {
|
|
43
|
+
const v = this._getValidator(defName);
|
|
44
|
+
const valid = v(obj);
|
|
45
|
+
return { valid: !!valid, errors: valid ? [] : (v.errors || []) };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const SCHEMA_VERSION = load().schema.version;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* status-emit.js — COMP-OBS-STATUS broadcast dispatcher (A2).
|
|
3
|
+
*
|
|
4
|
+
* Single choke point: emitStatusSnapshot(broadcastMessage, state, featureCode, now)
|
|
5
|
+
* Recomputes the StatusSnapshot from current store state and broadcasts:
|
|
6
|
+
* { type: 'statusSnapshot', featureCode, snapshot }
|
|
7
|
+
*
|
|
8
|
+
* Returns the snapshot for caller convenience (mirrors decision-event-emit.js pattern).
|
|
9
|
+
* No caching — recomputed on every call. The snapshot is cheap (~200 bytes, single scan).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { computeStatusSnapshot } from './status-snapshot.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Emit a StatusSnapshot broadcast.
|
|
16
|
+
*
|
|
17
|
+
* @param {function} broadcastMessage — fn(msg) to dispatch to all WS clients
|
|
18
|
+
* @param {object} state — VisionStore (getItemByFeatureCode, getPendingGates)
|
|
19
|
+
* @param {string|null} featureCode — the feature whose snapshot to recompute
|
|
20
|
+
* @param {string} [now] — ISO timestamp (defaults to Date.now())
|
|
21
|
+
* @returns {StatusSnapshot}
|
|
22
|
+
*/
|
|
23
|
+
export function emitStatusSnapshot(broadcastMessage, state, featureCode, now) {
|
|
24
|
+
const snapshot = computeStatusSnapshot(state, featureCode, now || new Date().toISOString());
|
|
25
|
+
broadcastMessage({ type: 'statusSnapshot', featureCode, snapshot });
|
|
26
|
+
return snapshot;
|
|
27
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* status-snapshot.js — COMP-OBS-STATUS producer (A1).
|
|
3
|
+
*
|
|
4
|
+
* Pure function: computeStatusSnapshot(state, featureCode, now) → StatusSnapshot
|
|
5
|
+
*
|
|
6
|
+
* Decision 2 (design.md): 8 rule branches, first-match wins.
|
|
7
|
+
* 1. No feature selected → "Select a feature to see status."
|
|
8
|
+
* 2. Killed → "{fc} killed. No further action."
|
|
9
|
+
* 3. Complete → "{fc} complete."
|
|
10
|
+
* 4. Pending gate → "Holding {phase}. Next: approve {gateId}."
|
|
11
|
+
* 5. Drift breach → "{phase} — N drift alert(s)."
|
|
12
|
+
* 6. Stale open loops → "{phase}. N loop(s) past TTL."
|
|
13
|
+
* 7. Iteration in flight → "Iterating {loopType} (attempt {count})."
|
|
14
|
+
* 8. Idle baseline → "Building {phase}. Open loops: {count}."
|
|
15
|
+
*
|
|
16
|
+
* Null/unknown phase fallbacks (Decision 2 note):
|
|
17
|
+
* - null phase + feature selected → "{fc}: phase pending."
|
|
18
|
+
* - unknown phase string → "{fc}: {phase} (unrecognized phase)."
|
|
19
|
+
* Both short-circuit branches 4–7.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { readGateLog } from './gate-log-store.js';
|
|
23
|
+
import { isStaleLoop } from './open-loops-store.js';
|
|
24
|
+
|
|
25
|
+
// Known lifecycle phases — must match LIFECYCLE_PHASE_LABELS in constants.js
|
|
26
|
+
const KNOWN_PHASES = new Set([
|
|
27
|
+
'explore_design', 'prd', 'architecture', 'blueprint',
|
|
28
|
+
'verification', 'plan', 'execute', 'report', 'docs', 'ship',
|
|
29
|
+
'complete', 'killed',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const TERMINAL_PHASES = new Set(['complete', 'killed']);
|
|
33
|
+
const MAX_SENTENCE = 280;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Truncate a string to fit within `headroom` chars, appending '…' if cut.
|
|
37
|
+
* If the string already fits, it is returned unchanged.
|
|
38
|
+
*/
|
|
39
|
+
export function truncateForSentence(s, headroom) {
|
|
40
|
+
if (s.length <= headroom) return s;
|
|
41
|
+
if (headroom <= 1) return '…';
|
|
42
|
+
return s.slice(0, headroom - 1) + '…';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build the sentence string from Decision 2 branches.
|
|
47
|
+
*
|
|
48
|
+
* @param {object} params
|
|
49
|
+
* @param {string|null} featureCode
|
|
50
|
+
* @param {string|null} activePhase
|
|
51
|
+
* @param {Array} pendingGates — gate objects with .id
|
|
52
|
+
* @param {Array} driftAlerts — breached DriftAxis objects
|
|
53
|
+
* @param {number} staleLoopCount
|
|
54
|
+
* @param {object|null} iterationState — lifecycle.iterationState
|
|
55
|
+
* @param {boolean} featureExists — whether the feature was found in the store
|
|
56
|
+
*/
|
|
57
|
+
function buildStatusSentence({
|
|
58
|
+
featureCode,
|
|
59
|
+
activePhase,
|
|
60
|
+
pendingGates,
|
|
61
|
+
driftAlerts,
|
|
62
|
+
staleLoopCount,
|
|
63
|
+
iterationState,
|
|
64
|
+
openLoopsCount,
|
|
65
|
+
featureExists,
|
|
66
|
+
}) {
|
|
67
|
+
// Branch 1: no feature selected
|
|
68
|
+
if (!featureCode || !featureExists) {
|
|
69
|
+
return 'Select a feature to see status.';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Null phase guard (Decision 2 note) — short-circuits 4–7
|
|
73
|
+
if (activePhase === null || activePhase === undefined) {
|
|
74
|
+
return `${featureCode}: phase pending.`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Unknown phase guard — short-circuits 4–7
|
|
78
|
+
if (!KNOWN_PHASES.has(activePhase)) {
|
|
79
|
+
return truncateForSentence(`${featureCode}: ${activePhase} (unrecognized phase).`, MAX_SENTENCE);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Branch 2: killed (terminal, checked BEFORE non-terminal branches)
|
|
83
|
+
if (activePhase === 'killed') {
|
|
84
|
+
return `${featureCode} killed. No further action.`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Branch 3: complete (terminal)
|
|
88
|
+
if (activePhase === 'complete') {
|
|
89
|
+
return `${featureCode} complete.`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Branch 4: pending gate
|
|
93
|
+
if (pendingGates.length > 0) {
|
|
94
|
+
const gateId = pendingGates[0].id;
|
|
95
|
+
const prefix = `Holding ${activePhase}. Next: approve `;
|
|
96
|
+
const suffix = '.';
|
|
97
|
+
const headroom = MAX_SENTENCE - prefix.length - suffix.length;
|
|
98
|
+
const gateIdShort = truncateForSentence(gateId, headroom);
|
|
99
|
+
return `${prefix}${gateIdShort}${suffix}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Branch 5: drift breach
|
|
103
|
+
if (driftAlerts.length > 0) {
|
|
104
|
+
const n = driftAlerts.length;
|
|
105
|
+
const s = n === 1 ? '' : 's';
|
|
106
|
+
return truncateForSentence(`${activePhase} — ${n} drift alert${s}.`, MAX_SENTENCE);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Branch 6: stale open loops
|
|
110
|
+
if (staleLoopCount > 0) {
|
|
111
|
+
const n = staleLoopCount;
|
|
112
|
+
const s = n === 1 ? '' : 's';
|
|
113
|
+
return truncateForSentence(`${activePhase}. ${n} loop${s} past TTL.`, MAX_SENTENCE);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Branch 7: iteration in flight
|
|
117
|
+
if (iterationState?.status === 'running') {
|
|
118
|
+
const { loopType, count } = iterationState;
|
|
119
|
+
return truncateForSentence(`Iterating ${loopType} (attempt ${count}).`, MAX_SENTENCE);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Branch 8: idle baseline
|
|
123
|
+
return truncateForSentence(`Building ${activePhase}. Open loops: ${openLoopsCount}.`, MAX_SENTENCE);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Compute a full StatusSnapshot for the given featureCode from current store state.
|
|
128
|
+
*
|
|
129
|
+
* @param {object} state — VisionStore (must expose getItemByFeatureCode, getPendingGates)
|
|
130
|
+
* @param {string|null} featureCode
|
|
131
|
+
* @param {string} now — ISO timestamp (injected for testability)
|
|
132
|
+
* @returns {StatusSnapshot}
|
|
133
|
+
*/
|
|
134
|
+
export function computeStatusSnapshot(state, featureCode, now) {
|
|
135
|
+
const nowStr = now || new Date().toISOString();
|
|
136
|
+
|
|
137
|
+
// No feature selected → branch 1 baseline
|
|
138
|
+
if (!featureCode) {
|
|
139
|
+
return {
|
|
140
|
+
sentence: 'Select a feature to see status.',
|
|
141
|
+
active_goal: null,
|
|
142
|
+
active_phase: null,
|
|
143
|
+
pending_gates: [],
|
|
144
|
+
drift_alerts: [],
|
|
145
|
+
open_loops_count: 0,
|
|
146
|
+
gate_load_24h: 0, // TODO: real value when COMP-OBS-GATELOG ships
|
|
147
|
+
cta: null,
|
|
148
|
+
computed_at: nowStr,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const item = state.getItemByFeatureCode(featureCode);
|
|
153
|
+
|
|
154
|
+
// Feature not found in store — return no-feature snapshot
|
|
155
|
+
if (!item) {
|
|
156
|
+
return {
|
|
157
|
+
sentence: 'Select a feature to see status.',
|
|
158
|
+
active_goal: null,
|
|
159
|
+
active_phase: null,
|
|
160
|
+
pending_gates: [],
|
|
161
|
+
drift_alerts: [],
|
|
162
|
+
open_loops_count: 0,
|
|
163
|
+
gate_load_24h: 0,
|
|
164
|
+
cta: null,
|
|
165
|
+
computed_at: nowStr,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const lc = item.lifecycle;
|
|
170
|
+
const activePhase = lc?.currentPhase ?? null;
|
|
171
|
+
const ext = lc?.lifecycle_ext ?? {};
|
|
172
|
+
|
|
173
|
+
// Pending gates for this item
|
|
174
|
+
const pendingGates = state.getPendingGates(item.id);
|
|
175
|
+
const pendingGateIds = pendingGates.map(g => g.id);
|
|
176
|
+
|
|
177
|
+
// Drift alerts: only axes with breached:true
|
|
178
|
+
const driftAxes = ext.drift_axes ?? [];
|
|
179
|
+
const driftAlerts = driftAxes.filter(a => a.breached === true);
|
|
180
|
+
|
|
181
|
+
// Open loops — only unresolved entries count (COMP-OBS-LOOPS semantic fix)
|
|
182
|
+
const openLoops = ext.open_loops ?? [];
|
|
183
|
+
const openLoopsCount = openLoops.filter(l => l.resolution == null).length;
|
|
184
|
+
|
|
185
|
+
// Stale open loops: unresolved and past TTL — use shared isStaleLoop helper
|
|
186
|
+
const nowMs = Date.parse(nowStr);
|
|
187
|
+
const staleLoops = openLoops.filter(loop => isStaleLoop(loop, nowMs));
|
|
188
|
+
const staleLoopCount = staleLoops.length;
|
|
189
|
+
|
|
190
|
+
const iterationState = lc?.iterationState ?? null;
|
|
191
|
+
|
|
192
|
+
// Is active phase known? (for null/unknown guard)
|
|
193
|
+
const phaseKnown = activePhase === null || KNOWN_PHASES.has(activePhase);
|
|
194
|
+
|
|
195
|
+
// Build sentence — pass iterationState only when phase is non-terminal+known
|
|
196
|
+
const sentence = buildStatusSentence({
|
|
197
|
+
featureCode,
|
|
198
|
+
activePhase,
|
|
199
|
+
pendingGates,
|
|
200
|
+
driftAlerts,
|
|
201
|
+
staleLoopCount,
|
|
202
|
+
iterationState: phaseKnown && !TERMINAL_PHASES.has(activePhase) ? iterationState : null,
|
|
203
|
+
openLoopsCount,
|
|
204
|
+
featureExists: true,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
sentence,
|
|
209
|
+
active_goal: item.title || null,
|
|
210
|
+
active_phase: activePhase,
|
|
211
|
+
pending_gates: pendingGateIds,
|
|
212
|
+
drift_alerts: driftAlerts,
|
|
213
|
+
open_loops_count: openLoopsCount,
|
|
214
|
+
gate_load_24h: readGateLog({ since: nowMs - 86400000 }).length,
|
|
215
|
+
cta: null,
|
|
216
|
+
computed_at: nowStr,
|
|
217
|
+
};
|
|
218
|
+
}
|