@smartmemory/compose 0.1.0 → 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,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* connector-factory-shim.js — Backward-compatibility adapter for tests that
|
|
3
|
+
* pass a legacy `connectorFactory(agentType, opts)` returning an object with
|
|
4
|
+
* an async-generator `run(prompt)` that yields `{type: 'assistant'|'tool_use'
|
|
5
|
+
* |'tool_use_summary'|'usage'|'error'|'result', ...}` events.
|
|
6
|
+
*
|
|
7
|
+
* After STRAT-DEDUP-AGENTRUN-V3 the consumer pipeline calls
|
|
8
|
+
* `stratum.agentRun(...)` and consumes BuildStreamEvent envelopes via
|
|
9
|
+
* `stratum.onEvent(correlationId, '_agent_run', handler)`. This shim adapts
|
|
10
|
+
* the legacy factory to dispatch envelopes through the StratumMcpClient's
|
|
11
|
+
* onEvent pathway so existing tests continue to assert wire-level behavior.
|
|
12
|
+
*
|
|
13
|
+
* Used only when `opts.stratum` is NOT injected directly. Production paths
|
|
14
|
+
* never instantiate this shim.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Install fake `agentRun`, `runAgentText`, `cancelAgentRun` methods on a
|
|
19
|
+
* StratumMcpClient instance backed by a legacy connector factory.
|
|
20
|
+
*
|
|
21
|
+
* @param {object} stratum - StratumMcpClient (uses its #dispatchEvent path indirectly via internal subscribers).
|
|
22
|
+
* @param {Function} factory - legacy `factory(agentType, opts)` returning {run(prompt), interrupt(), isRunning}
|
|
23
|
+
* @param {string} defaultCwd
|
|
24
|
+
*/
|
|
25
|
+
export function installFactoryShim(stratum, factory, defaultCwd) {
|
|
26
|
+
// We dispatch via the public onEvent subscribers map. There's no public
|
|
27
|
+
// emit method, so we synthesize the same JSON-string-via-progress path the
|
|
28
|
+
// real client uses by directly invoking subscribed handlers.
|
|
29
|
+
function emit(correlationId, kind, metadata, seq) {
|
|
30
|
+
// Replicate the dispatch path: lookup `${flow}::${step}` subscribers and
|
|
31
|
+
// hand them a parsed envelope. This mirrors `#dispatchEvent` (private)
|
|
32
|
+
// but works against the public `onEvent` registry implicitly because we
|
|
33
|
+
// only run inside a single tool-call lifecycle.
|
|
34
|
+
const env = {
|
|
35
|
+
schema_version: '0.2.5',
|
|
36
|
+
flow_id: correlationId,
|
|
37
|
+
step_id: '_agent_run',
|
|
38
|
+
task_id: null,
|
|
39
|
+
seq,
|
|
40
|
+
ts: new Date().toISOString(),
|
|
41
|
+
kind,
|
|
42
|
+
metadata,
|
|
43
|
+
};
|
|
44
|
+
// Lean on the stratum client's existing #makeProgressHandler-style path:
|
|
45
|
+
// re-encode and feed via a shadow handler is overkill — we instead reach
|
|
46
|
+
// into the onEvent subscribers via a dispatch closure attached on first
|
|
47
|
+
// install.
|
|
48
|
+
if (!stratum._shimDispatch) {
|
|
49
|
+
// Fallback: directly walk subscribers if dispatch closure not wired.
|
|
50
|
+
// Here we expose a temporary dispatcher by hijacking onEvent's contract:
|
|
51
|
+
// each subscribe call adds to a map; we mirror lookups via a private
|
|
52
|
+
// tracker. The simpler approach is to wrap `onEvent` to also register
|
|
53
|
+
// with our own map. Done at install time below.
|
|
54
|
+
}
|
|
55
|
+
stratum._shimDispatch(env);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Wrap onEvent to keep a parallel registry we can dispatch into.
|
|
59
|
+
if (!stratum._shimSubs) {
|
|
60
|
+
const subs = new Map();
|
|
61
|
+
stratum._shimSubs = subs;
|
|
62
|
+
const realOnEvent = stratum.onEvent.bind(stratum);
|
|
63
|
+
stratum.onEvent = (flowId, stepId, handler) => {
|
|
64
|
+
const key = `${flowId}::${stepId}`;
|
|
65
|
+
let set = subs.get(key);
|
|
66
|
+
if (!set) { set = new Set(); subs.set(key, set); }
|
|
67
|
+
set.add(handler);
|
|
68
|
+
const realUnsub = realOnEvent(flowId, stepId, handler);
|
|
69
|
+
return () => {
|
|
70
|
+
const s = subs.get(key);
|
|
71
|
+
if (s) { s.delete(handler); if (s.size === 0) subs.delete(key); }
|
|
72
|
+
realUnsub();
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
stratum._shimDispatch = (env) => {
|
|
76
|
+
const set = subs.get(`${env.flow_id}::${env.step_id}`);
|
|
77
|
+
if (!set) return;
|
|
78
|
+
for (const h of set) {
|
|
79
|
+
try { h(env); } catch (err) { console.error('[shim] handler threw:', err); }
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
stratum.agentRun = async (agentType, prompt, agentOpts = {}) => {
|
|
85
|
+
const correlationId = agentOpts.correlationId ?? `mock-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
86
|
+
const connector = factory(agentType, { cwd: agentOpts.cwd ?? defaultCwd });
|
|
87
|
+
const parts = [];
|
|
88
|
+
let seq = 0;
|
|
89
|
+
let interruptHook = null;
|
|
90
|
+
if (typeof connector.interrupt === 'function') {
|
|
91
|
+
interruptHook = () => { try { connector.interrupt(); } catch { /* best-effort */ } };
|
|
92
|
+
}
|
|
93
|
+
// Hook for cancelAgentRun: stash the interrupt function under correlationId.
|
|
94
|
+
if (!stratum._shimInterrupts) stratum._shimInterrupts = new Map();
|
|
95
|
+
if (interruptHook) stratum._shimInterrupts.set(correlationId, interruptHook);
|
|
96
|
+
try {
|
|
97
|
+
for await (const ev of connector.run(prompt, {})) {
|
|
98
|
+
if (ev.type === 'assistant' && ev.content) {
|
|
99
|
+
parts.push(ev.content);
|
|
100
|
+
stratum._shimDispatch({
|
|
101
|
+
schema_version: '0.2.5',
|
|
102
|
+
flow_id: correlationId, step_id: '_agent_run', task_id: null,
|
|
103
|
+
seq: seq++, ts: new Date().toISOString(),
|
|
104
|
+
kind: 'agent_relay',
|
|
105
|
+
metadata: { role: 'assistant', text: ev.content },
|
|
106
|
+
});
|
|
107
|
+
} else if (ev.type === 'result' && ev.content && parts.length === 0) {
|
|
108
|
+
parts.push(ev.content);
|
|
109
|
+
stratum._shimDispatch({
|
|
110
|
+
schema_version: '0.2.5',
|
|
111
|
+
flow_id: correlationId, step_id: '_agent_run', task_id: null,
|
|
112
|
+
seq: seq++, ts: new Date().toISOString(),
|
|
113
|
+
kind: 'agent_relay',
|
|
114
|
+
metadata: { role: 'assistant', text: ev.content },
|
|
115
|
+
});
|
|
116
|
+
} else if (ev.type === 'tool_use' && ev.tool) {
|
|
117
|
+
stratum._shimDispatch({
|
|
118
|
+
schema_version: '0.2.5',
|
|
119
|
+
flow_id: correlationId, step_id: '_agent_run', task_id: null,
|
|
120
|
+
seq: seq++, ts: new Date().toISOString(),
|
|
121
|
+
kind: 'tool_use_summary',
|
|
122
|
+
metadata: { tool: ev.tool, input: ev.input ?? {}, summary: '', output: '' },
|
|
123
|
+
});
|
|
124
|
+
} else if (ev.type === 'tool_use_summary') {
|
|
125
|
+
stratum._shimDispatch({
|
|
126
|
+
schema_version: '0.2.5',
|
|
127
|
+
flow_id: correlationId, step_id: '_agent_run', task_id: null,
|
|
128
|
+
seq: seq++, ts: new Date().toISOString(),
|
|
129
|
+
kind: 'tool_use_summary',
|
|
130
|
+
metadata: { tool: ev.tool ?? '', input: ev.input ?? {}, summary: ev.summary ?? '', output: ev.output ?? '' },
|
|
131
|
+
});
|
|
132
|
+
} else if (ev.type === 'usage') {
|
|
133
|
+
stratum._shimDispatch({
|
|
134
|
+
schema_version: '0.2.5',
|
|
135
|
+
flow_id: correlationId, step_id: '_agent_run', task_id: null,
|
|
136
|
+
seq: seq++, ts: new Date().toISOString(),
|
|
137
|
+
kind: 'step_usage',
|
|
138
|
+
metadata: {
|
|
139
|
+
input_tokens: ev.input_tokens ?? 0,
|
|
140
|
+
output_tokens: ev.output_tokens ?? 0,
|
|
141
|
+
cache_creation_input_tokens: ev.cache_creation_input_tokens ?? 0,
|
|
142
|
+
cache_read_input_tokens: ev.cache_read_input_tokens ?? 0,
|
|
143
|
+
cost_usd: ev.cost_usd ?? null,
|
|
144
|
+
model: ev.model ?? null,
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
} else if (ev.type === 'error') {
|
|
148
|
+
throw new Error(ev.message);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
} finally {
|
|
152
|
+
stratum._shimInterrupts?.delete(correlationId);
|
|
153
|
+
}
|
|
154
|
+
return { text: parts.join(''), correlation_id: correlationId };
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
stratum.runAgentText = async (agentType, prompt, agentOpts = {}) => {
|
|
158
|
+
const r = await stratum.agentRun(agentType, prompt, agentOpts);
|
|
159
|
+
return r.text;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
stratum.cancelAgentRun = async (correlationId) => {
|
|
163
|
+
const hook = stratum._shimInterrupts?.get(correlationId);
|
|
164
|
+
if (hook) hook();
|
|
165
|
+
return { status: hook ? 'cancelled' : 'not_found', correlation_id: correlationId };
|
|
166
|
+
};
|
|
167
|
+
}
|
package/lib/constants.js
CHANGED
|
@@ -43,6 +43,24 @@ export const GATE_ARTIFACTS = {
|
|
|
43
43
|
report_gate: 'report.md',
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Subdirectory under a feature folder where per-task section files live.
|
|
48
|
+
* Emitted only when a plan exceeds the section threshold.
|
|
49
|
+
*/
|
|
50
|
+
export const SECTIONS_DIR = 'sections';
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Read the section-emission threshold from env.
|
|
54
|
+
* Default 5; unparseable falls back to default; finite is clamped to >= 1.
|
|
55
|
+
*/
|
|
56
|
+
export function getSectionsThreshold() {
|
|
57
|
+
const envVal = process.env.COMPOSE_PLAN_SECTIONS_THRESHOLD;
|
|
58
|
+
if (envVal === undefined || envVal === null || envVal === '') return 5;
|
|
59
|
+
const raw = parseInt(envVal, 10);
|
|
60
|
+
if (!Number.isFinite(raw)) return 5;
|
|
61
|
+
return Math.max(1, raw);
|
|
62
|
+
}
|
|
63
|
+
|
|
46
64
|
/**
|
|
47
65
|
* Title-case a step ID for display when no label exists.
|
|
48
66
|
* e.g. "some_step" -> "Some Step"
|
package/lib/debug-discipline.js
CHANGED
|
@@ -14,25 +14,44 @@ import { join } from 'node:path';
|
|
|
14
14
|
// Fix-Chain Detector
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
16
|
|
|
17
|
+
const FEATURE_MODE_KEY = '__feature_mode__';
|
|
18
|
+
const LEGACY_KEY = '__legacy__';
|
|
19
|
+
|
|
17
20
|
/**
|
|
18
21
|
* Tracks which files are modified across fix iterations.
|
|
19
22
|
* Detects thrashing: same file touched in multiple iterations.
|
|
23
|
+
*
|
|
24
|
+
* Per-bug keying (COMP-FIX-HARD T9): state is namespaced by bug code.
|
|
25
|
+
* Legacy flat API (recordIteration/detect/iteration) delegates to a
|
|
26
|
+
* synthetic `__feature_mode__` key for feature-mode builds.
|
|
20
27
|
*/
|
|
21
28
|
export class FixChainDetector {
|
|
22
29
|
constructor() {
|
|
23
|
-
|
|
24
|
-
this.
|
|
30
|
+
/** @type {Map<string, { iteration: number, fileHits: Map<string, number> }>} */
|
|
31
|
+
this.byBug = new Map();
|
|
25
32
|
}
|
|
26
33
|
|
|
27
|
-
|
|
28
|
-
this.
|
|
34
|
+
_slot(bugCode) {
|
|
35
|
+
let s = this.byBug.get(bugCode);
|
|
36
|
+
if (!s) {
|
|
37
|
+
s = { iteration: 0, fileHits: new Map() };
|
|
38
|
+
this.byBug.set(bugCode, s);
|
|
39
|
+
}
|
|
40
|
+
return s;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
recordIterationForBug(bugCode, filesChanged) {
|
|
44
|
+
const s = this._slot(bugCode);
|
|
45
|
+
s.iteration++;
|
|
29
46
|
for (const file of filesChanged) {
|
|
30
|
-
|
|
47
|
+
s.fileHits.set(file, (s.fileHits.get(file) ?? 0) + 1);
|
|
31
48
|
}
|
|
32
49
|
}
|
|
33
50
|
|
|
34
|
-
|
|
35
|
-
|
|
51
|
+
detectForBug(bugCode) {
|
|
52
|
+
const s = this.byBug.get(bugCode);
|
|
53
|
+
if (!s) return [];
|
|
54
|
+
return [...s.fileHits.entries()]
|
|
36
55
|
.filter(([, count]) => count >= 2)
|
|
37
56
|
.map(([file, count]) => ({
|
|
38
57
|
file,
|
|
@@ -41,17 +60,74 @@ export class FixChainDetector {
|
|
|
41
60
|
}));
|
|
42
61
|
}
|
|
43
62
|
|
|
63
|
+
getIterationForBug(bugCode) {
|
|
64
|
+
return this.byBug.get(bugCode)?.iteration ?? 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
resetForBug(bugCode) {
|
|
68
|
+
this.byBug.delete(bugCode);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// --- Legacy global API (feature mode) ----------------------------------
|
|
72
|
+
|
|
73
|
+
recordIteration(filesChanged) {
|
|
74
|
+
this.recordIterationForBug(FEATURE_MODE_KEY, filesChanged);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
detect() {
|
|
78
|
+
return this.detectForBug(FEATURE_MODE_KEY);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get iteration() {
|
|
82
|
+
return this.getIterationForBug(FEATURE_MODE_KEY);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
set iteration(v) {
|
|
86
|
+
this._slot(FEATURE_MODE_KEY).iteration = v;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
get fileHits() {
|
|
90
|
+
return this._slot(FEATURE_MODE_KEY).fileHits;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
set fileHits(map) {
|
|
94
|
+
this._slot(FEATURE_MODE_KEY).fileHits = map instanceof Map ? map : new Map(Object.entries(map ?? {}));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// --- Serialization ------------------------------------------------------
|
|
98
|
+
|
|
44
99
|
toJSON() {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
100
|
+
const out = {};
|
|
101
|
+
for (const [key, s] of this.byBug.entries()) {
|
|
102
|
+
out[key] = {
|
|
103
|
+
iteration: s.iteration,
|
|
104
|
+
fileHits: Object.fromEntries(s.fileHits),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
49
108
|
}
|
|
50
109
|
|
|
51
110
|
static fromJSON(json) {
|
|
52
111
|
const d = new FixChainDetector();
|
|
53
|
-
|
|
54
|
-
|
|
112
|
+
if (!json || typeof json !== 'object') return d;
|
|
113
|
+
// Legacy detection: flat shape has top-level `iteration` and/or `fileHits`
|
|
114
|
+
// and no per-bug sub-objects with the per-bug shape.
|
|
115
|
+
// Salvage any top-level legacy fields. Fold into __feature_mode__ so the
|
|
116
|
+
// existing global-API getters (recordIteration / detect / iteration getter)
|
|
117
|
+
// continue to surface it. If there's already a per-bug subkey for
|
|
118
|
+
// __feature_mode__ in the same JSON, the explicit subkey wins.
|
|
119
|
+
if ('iteration' in json || 'fileHits' in json) {
|
|
120
|
+
const slot = d._slot(FEATURE_MODE_KEY);
|
|
121
|
+
slot.iteration = json.iteration ?? 0;
|
|
122
|
+
slot.fileHits = new Map(Object.entries(json.fileHits ?? {}));
|
|
123
|
+
}
|
|
124
|
+
for (const [key, sub] of Object.entries(json)) {
|
|
125
|
+
if (key === 'iteration' || key === 'fileHits') continue; // top-level legacy, already folded
|
|
126
|
+
if (!sub || typeof sub !== 'object') continue;
|
|
127
|
+
const slot = d._slot(key);
|
|
128
|
+
slot.iteration = sub.iteration ?? 0;
|
|
129
|
+
slot.fileHits = new Map(Object.entries(sub.fileHits ?? {}));
|
|
130
|
+
}
|
|
55
131
|
return d;
|
|
56
132
|
}
|
|
57
133
|
}
|
|
@@ -65,42 +141,115 @@ const VISUAL_EXTENSIONS = /\.(css|scss|jsx|tsx)$/i;
|
|
|
65
141
|
/**
|
|
66
142
|
* Tracks fix attempts and enforces thresholds with escalation.
|
|
67
143
|
* Visual bugs escalate at attempt 2; all bugs escalate at attempt 5.
|
|
144
|
+
*
|
|
145
|
+
* Per-bug keying (COMP-FIX-HARD T9): state is namespaced by bug code.
|
|
146
|
+
* Legacy flat API (record/getIntervention/count/isVisual) delegates to a
|
|
147
|
+
* synthetic `__feature_mode__` key for feature-mode builds.
|
|
68
148
|
*/
|
|
69
149
|
export class AttemptCounter {
|
|
70
150
|
constructor() {
|
|
71
|
-
|
|
72
|
-
this.
|
|
151
|
+
/** @type {Map<string, { count: number, isVisual: boolean }>} */
|
|
152
|
+
this.byBug = new Map();
|
|
73
153
|
}
|
|
74
154
|
|
|
75
|
-
|
|
76
|
-
this.
|
|
155
|
+
_slot(bugCode) {
|
|
156
|
+
let s = this.byBug.get(bugCode);
|
|
157
|
+
if (!s) {
|
|
158
|
+
s = { count: 0, isVisual: false };
|
|
159
|
+
this.byBug.set(bugCode, s);
|
|
160
|
+
}
|
|
161
|
+
return s;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
recordForBug(bugCode, { filesChanged = [], isVisual = null } = {}) {
|
|
165
|
+
const s = this._slot(bugCode);
|
|
166
|
+
s.count++;
|
|
77
167
|
if (isVisual !== null) {
|
|
78
|
-
|
|
168
|
+
s.isVisual = isVisual;
|
|
79
169
|
} else if (filesChanged.some(f => AttemptCounter.isVisualFile(f))) {
|
|
80
|
-
|
|
170
|
+
s.isVisual = true;
|
|
81
171
|
}
|
|
82
172
|
}
|
|
83
173
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
174
|
+
getCountForBug(bugCode) {
|
|
175
|
+
return this.byBug.get(bugCode)?.count ?? 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
getInterventionForBug(bugCode) {
|
|
179
|
+
const s = this.byBug.get(bugCode);
|
|
180
|
+
if (!s) return null;
|
|
181
|
+
if (s.count >= 5) return 'escalate';
|
|
182
|
+
if (s.count >= 3 && !s.isVisual) return 'trace_refresh';
|
|
183
|
+
if (s.count >= 2 && s.isVisual) return 'escalate';
|
|
184
|
+
if (s.count >= 2) return 'trace_reminder';
|
|
89
185
|
return null;
|
|
90
186
|
}
|
|
91
187
|
|
|
188
|
+
resetForBug(bugCode) {
|
|
189
|
+
this.byBug.delete(bugCode);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// --- Legacy global API (feature mode) ----------------------------------
|
|
193
|
+
|
|
194
|
+
record(opts) {
|
|
195
|
+
this.recordForBug(FEATURE_MODE_KEY, opts ?? {});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
getIntervention() {
|
|
199
|
+
return this.getInterventionForBug(FEATURE_MODE_KEY);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
get count() {
|
|
203
|
+
return this.getCountForBug(FEATURE_MODE_KEY);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
set count(v) {
|
|
207
|
+
this._slot(FEATURE_MODE_KEY).count = v;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
get isVisual() {
|
|
211
|
+
return this.byBug.get(FEATURE_MODE_KEY)?.isVisual ?? false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
set isVisual(v) {
|
|
215
|
+
this._slot(FEATURE_MODE_KEY).isVisual = !!v;
|
|
216
|
+
}
|
|
217
|
+
|
|
92
218
|
static isVisualFile(file) {
|
|
93
219
|
return VISUAL_EXTENSIONS.test(file);
|
|
94
220
|
}
|
|
95
221
|
|
|
222
|
+
// --- Serialization ------------------------------------------------------
|
|
223
|
+
|
|
96
224
|
toJSON() {
|
|
97
|
-
|
|
225
|
+
const out = {};
|
|
226
|
+
for (const [key, s] of this.byBug.entries()) {
|
|
227
|
+
out[key] = { count: s.count, isVisual: s.isVisual };
|
|
228
|
+
}
|
|
229
|
+
return out;
|
|
98
230
|
}
|
|
99
231
|
|
|
100
232
|
static fromJSON(json) {
|
|
101
233
|
const c = new AttemptCounter();
|
|
102
|
-
|
|
103
|
-
|
|
234
|
+
if (!json || typeof json !== 'object') return c;
|
|
235
|
+
// Legacy detection: flat shape has top-level `count`/`isVisual` and
|
|
236
|
+
// no per-bug sub-objects with the per-bug shape.
|
|
237
|
+
// Salvage any top-level legacy fields. Fold into __feature_mode__ so the
|
|
238
|
+
// existing global-API getters (count getter, getIntervention, etc.)
|
|
239
|
+
// continue to surface it. If an explicit __feature_mode__ subkey is also
|
|
240
|
+
// present, it wins (loop below).
|
|
241
|
+
if ('count' in json || 'isVisual' in json) {
|
|
242
|
+
const slot = c._slot(FEATURE_MODE_KEY);
|
|
243
|
+
slot.count = json.count ?? 0;
|
|
244
|
+
slot.isVisual = json.isVisual ?? false;
|
|
245
|
+
}
|
|
246
|
+
for (const [key, sub] of Object.entries(json)) {
|
|
247
|
+
if (key === 'count' || key === 'isVisual') continue; // top-level legacy, already handled
|
|
248
|
+
if (!sub || typeof sub !== 'object') continue;
|
|
249
|
+
const slot = c._slot(key);
|
|
250
|
+
slot.count = sub.count ?? 0;
|
|
251
|
+
slot.isVisual = sub.isVisual ?? false;
|
|
252
|
+
}
|
|
104
253
|
return c;
|
|
105
254
|
}
|
|
106
255
|
}
|
package/lib/deps.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* COMP-DEPS-PACKAGE — external skill dependency manifest helpers.
|
|
3
|
+
*
|
|
4
|
+
* Three exported functions used by `bin/compose.js` (and tested in
|
|
5
|
+
* `test/comp-deps-package.test.js`):
|
|
6
|
+
*
|
|
7
|
+
* loadDeps(packageRoot) — load and validate .compose-deps.json
|
|
8
|
+
* checkExternalSkills(deps, home?) — scan disk, return present/missing dep records
|
|
9
|
+
* printDepReport(result, opts?) — human or JSON output, returns true if all required deps present
|
|
10
|
+
*
|
|
11
|
+
* Manifest schema is documented in docs/features/COMP-DEPS-PACKAGE/blueprint.md.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
14
|
+
import { join } from 'node:path'
|
|
15
|
+
import { homedir } from 'node:os'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Load .compose-deps.json from `packageRoot`. Returns the parsed manifest with
|
|
19
|
+
* invalid entries filtered out (skip-and-warn), or null if the manifest file
|
|
20
|
+
* is missing/unparseable/structurally invalid.
|
|
21
|
+
*/
|
|
22
|
+
export function loadDeps(packageRoot) {
|
|
23
|
+
const manifestPath = join(packageRoot, '.compose-deps.json')
|
|
24
|
+
if (!existsSync(manifestPath)) return null
|
|
25
|
+
let raw
|
|
26
|
+
try {
|
|
27
|
+
raw = JSON.parse(readFileSync(manifestPath, 'utf-8'))
|
|
28
|
+
} catch (e) {
|
|
29
|
+
console.warn(`Warning: failed to parse .compose-deps.json: ${e.message}`)
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
if (raw.version !== 1) {
|
|
33
|
+
console.warn(`Warning: .compose-deps.json version ${raw.version} unsupported (expected 1)`)
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
if (!Array.isArray(raw.external_skills)) {
|
|
37
|
+
console.warn('Warning: .compose-deps.json external_skills must be an array')
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
const valid = []
|
|
41
|
+
for (const dep of raw.external_skills) {
|
|
42
|
+
const idOk = typeof dep?.id === 'string'
|
|
43
|
+
const reqOk = Array.isArray(dep?.required_for) && dep.required_for.every(v => typeof v === 'string')
|
|
44
|
+
const installOk = typeof dep?.install === 'string'
|
|
45
|
+
const fallbackOk = dep?.fallback === null || typeof dep?.fallback === 'string'
|
|
46
|
+
const optOk = typeof dep?.optional === 'boolean'
|
|
47
|
+
if (!(idOk && reqOk && installOk && fallbackOk && optOk)) {
|
|
48
|
+
console.warn(`Warning: skipping invalid dep entry in .compose-deps.json: ${JSON.stringify(dep)}`)
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
valid.push(dep)
|
|
52
|
+
}
|
|
53
|
+
return { ...raw, external_skills: valid }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Scan disk for installed skills and commands matching the manifest's deps.
|
|
58
|
+
* Returns { present: [...], missing: [...], scannedPaths: [...] }.
|
|
59
|
+
*
|
|
60
|
+
* Scans:
|
|
61
|
+
* - <home>/.claude/skills/<id>/SKILL.md (bare-name skills)
|
|
62
|
+
* - <home>/.claude/plugins/marketplaces/<m>/plugins/<p>/skills/<s>/SKILL.md (pattern A)
|
|
63
|
+
* - <home>/.claude/plugins/marketplaces/<m>/plugins/<p>/commands/<n>.md (pattern A')
|
|
64
|
+
* - <home>/.claude/plugins/marketplaces/<m>/.claude/skills/<s>/SKILL.md (pattern B)
|
|
65
|
+
* - <home>/.claude/plugins/marketplaces/<m>/.claude/commands/<n>.md (pattern B')
|
|
66
|
+
* - <home>/.claude/plugins/cache/<marketplace>/<plugin>/<version>/skills/<s>/SKILL.md (pattern C)
|
|
67
|
+
*/
|
|
68
|
+
export function checkExternalSkills(deps, home = homedir()) {
|
|
69
|
+
const userSkillsRoot = join(home, '.claude', 'skills')
|
|
70
|
+
const marketplacesRoot = join(home, '.claude', 'plugins', 'marketplaces')
|
|
71
|
+
const cacheRoot = join(home, '.claude', 'plugins', 'cache')
|
|
72
|
+
const scannedPaths = [userSkillsRoot, marketplacesRoot, cacheRoot]
|
|
73
|
+
|
|
74
|
+
const bareInstalled = new Set()
|
|
75
|
+
const namespacedInstalled = new Set()
|
|
76
|
+
const addNs = (ns, leaf) => namespacedInstalled.add(`${ns}:${leaf}`)
|
|
77
|
+
|
|
78
|
+
// Helpers: list only entries of a given dirent kind, swallow scandir errors.
|
|
79
|
+
const listDirs = (path) => {
|
|
80
|
+
if (!existsSync(path)) return []
|
|
81
|
+
try {
|
|
82
|
+
return readdirSync(path, { withFileTypes: true })
|
|
83
|
+
.filter(e => e.isDirectory())
|
|
84
|
+
.map(e => e.name)
|
|
85
|
+
} catch { return [] }
|
|
86
|
+
}
|
|
87
|
+
const listFiles = (path) => {
|
|
88
|
+
if (!existsSync(path)) return []
|
|
89
|
+
try {
|
|
90
|
+
return readdirSync(path, { withFileTypes: true })
|
|
91
|
+
.filter(e => e.isFile())
|
|
92
|
+
.map(e => e.name)
|
|
93
|
+
} catch { return [] }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 1. Bare-name user skills
|
|
97
|
+
for (const entry of listDirs(userSkillsRoot)) {
|
|
98
|
+
if (existsSync(join(userSkillsRoot, entry, 'SKILL.md'))) bareInstalled.add(entry)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 2. Marketplaces — skills (A, B) and commands (A', B')
|
|
102
|
+
for (const m of listDirs(marketplacesRoot)) {
|
|
103
|
+
// Pattern A / A': marketplaces/<m>/plugins/<p>/{skills,commands}/...
|
|
104
|
+
const pluginsDir = join(marketplacesRoot, m, 'plugins')
|
|
105
|
+
for (const p of listDirs(pluginsDir)) {
|
|
106
|
+
const skillsDir = join(pluginsDir, p, 'skills')
|
|
107
|
+
for (const s of listDirs(skillsDir)) {
|
|
108
|
+
if (existsSync(join(skillsDir, s, 'SKILL.md'))) addNs(p, s)
|
|
109
|
+
}
|
|
110
|
+
const commandsDir = join(pluginsDir, p, 'commands')
|
|
111
|
+
for (const f of listFiles(commandsDir)) {
|
|
112
|
+
if (f.endsWith('.md')) addNs(p, f.slice(0, -3))
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Pattern B / B': marketplaces/<m>/.claude/{skills,commands}/...
|
|
116
|
+
const dotSkillsDir = join(marketplacesRoot, m, '.claude', 'skills')
|
|
117
|
+
for (const s of listDirs(dotSkillsDir)) {
|
|
118
|
+
if (existsSync(join(dotSkillsDir, s, 'SKILL.md'))) addNs(m, s)
|
|
119
|
+
}
|
|
120
|
+
const dotCommandsDir = join(marketplacesRoot, m, '.claude', 'commands')
|
|
121
|
+
for (const f of listFiles(dotCommandsDir)) {
|
|
122
|
+
if (f.endsWith('.md')) addNs(m, f.slice(0, -3))
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 3. Cache — pattern C: cache/<marketplace>/<plugin>/<version>/skills/<s>/SKILL.md
|
|
127
|
+
for (const marketplace of listDirs(cacheRoot)) {
|
|
128
|
+
for (const plugin of listDirs(join(cacheRoot, marketplace))) {
|
|
129
|
+
for (const version of listDirs(join(cacheRoot, marketplace, plugin))) {
|
|
130
|
+
const skillsDir = join(cacheRoot, marketplace, plugin, version, 'skills')
|
|
131
|
+
for (const s of listDirs(skillsDir)) {
|
|
132
|
+
if (existsSync(join(skillsDir, s, 'SKILL.md'))) addNs(plugin, s)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const present = []
|
|
139
|
+
const missing = []
|
|
140
|
+
for (const dep of deps.external_skills) {
|
|
141
|
+
const isNamespaced = dep.id.includes(':')
|
|
142
|
+
const found = isNamespaced ? namespacedInstalled.has(dep.id) : bareInstalled.has(dep.id)
|
|
143
|
+
if (found) present.push(dep); else missing.push(dep)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { present, missing, scannedPaths }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Print human or JSON dep report. Returns true if all required (non-optional) deps present.
|
|
151
|
+
*
|
|
152
|
+
* opts:
|
|
153
|
+
* json — emit JSON with full dep records (id, required_for, install, fallback, optional)
|
|
154
|
+
* verbose — also list scanned paths in human mode
|
|
155
|
+
*/
|
|
156
|
+
export function printDepReport(result, opts = {}) {
|
|
157
|
+
const projectDep = (d) => ({
|
|
158
|
+
id: d.id,
|
|
159
|
+
required_for: d.required_for,
|
|
160
|
+
install: d.install,
|
|
161
|
+
fallback: d.fallback ?? null,
|
|
162
|
+
optional: d.optional,
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
if (opts.json) {
|
|
166
|
+
console.log(JSON.stringify({
|
|
167
|
+
present: result.present.map(projectDep),
|
|
168
|
+
missing: result.missing.map(projectDep),
|
|
169
|
+
scannedPaths: result.scannedPaths,
|
|
170
|
+
}, null, 2))
|
|
171
|
+
return result.missing.every(d => d.optional)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
console.log('\nExternal skill dependencies:')
|
|
175
|
+
for (const dep of result.present) {
|
|
176
|
+
console.log(` ✓ ${dep.id}`)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const missingRequired = result.missing.filter(d => !d.optional)
|
|
180
|
+
const missingOptional = result.missing.filter(d => d.optional)
|
|
181
|
+
|
|
182
|
+
for (const dep of missingRequired) {
|
|
183
|
+
console.log(` ✗ ${dep.id} — install: ${dep.install}`)
|
|
184
|
+
}
|
|
185
|
+
for (const dep of missingOptional) {
|
|
186
|
+
console.log(` ○ ${dep.id} (optional) — install: ${dep.install}`)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const total = result.present.length + result.missing.length
|
|
190
|
+
if (result.missing.length === 0) {
|
|
191
|
+
console.log(`\nAll ${total} deps present.`)
|
|
192
|
+
} else {
|
|
193
|
+
console.log(`\n${result.missing.length} of ${total} deps missing (${missingRequired.length} required, ${missingOptional.length} optional).`)
|
|
194
|
+
if (missingRequired.length > 0) {
|
|
195
|
+
console.log('Lifecycle will run in degraded mode for affected phases. See SKILL.md §Dependencies for fallback paths.')
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (opts.verbose) {
|
|
200
|
+
console.log('\nScanned paths:')
|
|
201
|
+
for (const p of result.scannedPaths) console.log(` ${p}`)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return missingRequired.length === 0
|
|
205
|
+
}
|
package/lib/health-score.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Aggregates signals from existing build artifacts into a 0-100 composite
|
|
5
5
|
* health score. No new data sources — only aggregates what already exists:
|
|
6
|
-
* - Review findings from parallel_review's
|
|
6
|
+
* - Review findings from parallel_review's ReviewResult (canonical — STRAT-CLAUDE-EFFORT-PARITY)
|
|
7
7
|
* - Test pass/fail from coverage_check result
|
|
8
8
|
* - Contract compliance from Stratum ensure results
|
|
9
9
|
* - Doc freshness from lib/staleness.js
|
|
@@ -52,19 +52,19 @@ export function scoreTestCoverage(testResult) {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
|
-
* Score review findings from a
|
|
55
|
+
* Score review findings from a ReviewResult.
|
|
56
56
|
*
|
|
57
57
|
* Severity breakdown:
|
|
58
58
|
* must-fix → -20 per finding
|
|
59
59
|
* should-fix → -5 per finding
|
|
60
60
|
* nit → -1 per finding
|
|
61
61
|
*
|
|
62
|
-
* @param {object|null} mergedResult { findings: [{severity, ...}] }
|
|
62
|
+
* @param {object|null} mergedResult ReviewResult: { findings: [{severity, ...}] }
|
|
63
63
|
* @returns {number} 0-100 (floored at 0)
|
|
64
64
|
*/
|
|
65
65
|
export function scoreReviewFindings(mergedResult) {
|
|
66
66
|
if (mergedResult == null) return 50; // neutral — no data
|
|
67
|
-
const findings = mergedResult.findings ??
|
|
67
|
+
const findings = mergedResult.findings ?? [];
|
|
68
68
|
if (findings.length === 0) return 100;
|
|
69
69
|
|
|
70
70
|
let score = 100;
|