@smartmemory/compose 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1014 -0
  3. package/bin/compose.js +1515 -0
  4. package/dist/assets/_baseUniq-CQwX6VLz.js +1 -0
  5. package/dist/assets/arc-SxJ2J1sh.js +1 -0
  6. package/dist/assets/architectureDiagram-Q4EWVU46-BykunY1F.js +36 -0
  7. package/dist/assets/blockDiagram-DXYQGD6D-ohAKBOUw.js +132 -0
  8. package/dist/assets/c4Diagram-AHTNJAMY-DBDC3ENB.js +10 -0
  9. package/dist/assets/channel-DGElom1e.js +1 -0
  10. package/dist/assets/chunk-4BX2VUAB-Cv93Z7uM.js +1 -0
  11. package/dist/assets/chunk-4TB4RGXK-DE0WBDkj.js +206 -0
  12. package/dist/assets/chunk-55IACEB6-CE1EXenG.js +1 -0
  13. package/dist/assets/chunk-EDXVE4YY-DA7Ana6H.js +1 -0
  14. package/dist/assets/chunk-FMBD7UC4-CTDIPA3p.js +15 -0
  15. package/dist/assets/chunk-OYMX7WX6-uGBaPaTX.js +231 -0
  16. package/dist/assets/chunk-QZHKN3VN-CYlnXuUO.js +1 -0
  17. package/dist/assets/chunk-YZCP3GAM-ojGkzcZK.js +1 -0
  18. package/dist/assets/classDiagram-6PBFFD2Q-KqWP9wWZ.js +1 -0
  19. package/dist/assets/classDiagram-v2-HSJHXN6E-KqWP9wWZ.js +1 -0
  20. package/dist/assets/clone-DUJKJXd7.js +1 -0
  21. package/dist/assets/cose-bilkent-S5V4N54A-Bktn9hL-.js +1 -0
  22. package/dist/assets/dagre-KV5264BT-DFaSzuRF.js +4 -0
  23. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  24. package/dist/assets/diagram-5BDNPKRD-DnfmDzEm.js +10 -0
  25. package/dist/assets/diagram-G4DWMVQ6-Bm8W9YnG.js +24 -0
  26. package/dist/assets/diagram-MMDJMWI5-B5-TSKvp.js +43 -0
  27. package/dist/assets/diagram-TYMM5635-ls4rqlky.js +24 -0
  28. package/dist/assets/erDiagram-SMLLAGMA-giG6WO-r.js +85 -0
  29. package/dist/assets/flowDiagram-DWJPFMVM-XvlUuz-7.js +162 -0
  30. package/dist/assets/ganttDiagram-T4ZO3ILL-hLBV57oV.js +292 -0
  31. package/dist/assets/gitGraphDiagram-UUTBAWPF-BHu3s_Gn.js +106 -0
  32. package/dist/assets/graph-D0Cfv00Y.js +1 -0
  33. package/dist/assets/index-CUd6pFGF.css +1 -0
  34. package/dist/assets/index-DReRlzZI.js +1144 -0
  35. package/dist/assets/infoDiagram-42DDH7IO-DbqRsOo3.js +2 -0
  36. package/dist/assets/init-Gi6I4Gst.js +1 -0
  37. package/dist/assets/ishikawaDiagram-UXIWVN3A-DnCdx7zb.js +70 -0
  38. package/dist/assets/journeyDiagram-VCZTEJTY-CfD7eNcP.js +139 -0
  39. package/dist/assets/kanban-definition-6JOO6SKY-BYaO9-mK.js +89 -0
  40. package/dist/assets/katex-DkKDou_j.js +257 -0
  41. package/dist/assets/layout-Bj72wOEB.js +1 -0
  42. package/dist/assets/linear-BRFo114D.js +1 -0
  43. package/dist/assets/min-GCHnKlJS.js +1 -0
  44. package/dist/assets/mindmap-definition-QFDTVHPH-n0PMebY4.js +96 -0
  45. package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  46. package/dist/assets/pieDiagram-DEJITSTG-pN4CljHF.js +30 -0
  47. package/dist/assets/quadrantDiagram-34T5L4WZ-DNoAy8-D.js +7 -0
  48. package/dist/assets/requirementDiagram-MS252O5E-BhtY05PT.js +84 -0
  49. package/dist/assets/sankeyDiagram-XADWPNL6-B6AD-16A.js +10 -0
  50. package/dist/assets/sequenceDiagram-FGHM5R23-DShHM-uk.js +157 -0
  51. package/dist/assets/stateDiagram-FHFEXIEX-DMxn7HTo.js +1 -0
  52. package/dist/assets/stateDiagram-v2-QKLJ7IA2-o6PnCs4e.js +1 -0
  53. package/dist/assets/timeline-definition-GMOUNBTQ-Cdu6uq52.js +120 -0
  54. package/dist/assets/vennDiagram-DHZGUBPP-CpK29iRe.js +34 -0
  55. package/dist/assets/wardley-RL74JXVD-BQgSkdcO.js +162 -0
  56. package/dist/assets/wardleyDiagram-NUSXRM2D-DJHYev6O.js +20 -0
  57. package/dist/assets/xychartDiagram-5P7HB3ND-1d75pbaO.js +7 -0
  58. package/dist/index.html +30 -0
  59. package/lib/agent-chains.js +65 -0
  60. package/lib/agent-string.js +86 -0
  61. package/lib/budget-ledger.js +86 -0
  62. package/lib/build-all.js +162 -0
  63. package/lib/build-dag.js +120 -0
  64. package/lib/build-stream-writer.js +190 -0
  65. package/lib/build.js +2997 -0
  66. package/lib/capability-checker.js +53 -0
  67. package/lib/cert-inject.js +38 -0
  68. package/lib/cli-progress.js +483 -0
  69. package/lib/constants.js +69 -0
  70. package/lib/cross-layer-audit.js +84 -0
  71. package/lib/debug-discipline.js +173 -0
  72. package/lib/feature-json.js +106 -0
  73. package/lib/gate-prompt.js +291 -0
  74. package/lib/gate-tiers.js +194 -0
  75. package/lib/health-history.js +119 -0
  76. package/lib/health-score.js +227 -0
  77. package/lib/ideabox.js +570 -0
  78. package/lib/import.js +244 -0
  79. package/lib/migrate-roadmap.js +94 -0
  80. package/lib/model-pricing.js +67 -0
  81. package/lib/new.js +413 -0
  82. package/lib/pipeline-cli.js +489 -0
  83. package/lib/plan-parser.js +103 -0
  84. package/lib/qa-scoping.js +474 -0
  85. package/lib/questionnaire.js +200 -0
  86. package/lib/resolve-port.js +7 -0
  87. package/lib/result-normalizer.js +349 -0
  88. package/lib/review-lenses.js +166 -0
  89. package/lib/roadmap-gen.js +210 -0
  90. package/lib/roadmap-parser.js +176 -0
  91. package/lib/server-probe.js +23 -0
  92. package/lib/staleness.js +87 -0
  93. package/lib/step-prompt.js +260 -0
  94. package/lib/step-validator.js +49 -0
  95. package/lib/stratum-mcp-client.js +365 -0
  96. package/lib/team-flag.js +46 -0
  97. package/lib/test-bootstrap.js +401 -0
  98. package/lib/triage.js +274 -0
  99. package/lib/vision-writer.js +391 -0
  100. package/package.json +111 -0
  101. package/pipelines/bug-fix.stratum.yaml +230 -0
  102. package/pipelines/build.stratum.yaml +498 -0
  103. package/pipelines/content.stratum.yaml +112 -0
  104. package/pipelines/coverage-sweep.stratum.yaml +52 -0
  105. package/pipelines/refactor.stratum.yaml +169 -0
  106. package/pipelines/research.stratum.yaml +88 -0
  107. package/pipelines/review-fix.stratum.yaml +109 -0
  108. package/presets/team-feature.stratum.yaml +105 -0
  109. package/presets/team-research.stratum.yaml +108 -0
  110. package/presets/team-review.stratum.yaml +106 -0
  111. package/scripts/agent-activity-hook.sh +31 -0
  112. package/scripts/agent-error-hook.sh +28 -0
  113. package/scripts/analyze-orphans.mjs +50 -0
  114. package/scripts/find-orphans.mjs +26 -0
  115. package/scripts/fix-phases.mjs +49 -0
  116. package/scripts/generate-stratum-spec.mjs +137 -0
  117. package/scripts/import-roadmap.mjs +116 -0
  118. package/scripts/phase-audit.mjs +33 -0
  119. package/scripts/run-pipeline.mjs +314 -0
  120. package/scripts/session-end-hook.sh +18 -0
  121. package/scripts/session-start-hook.sh +38 -0
  122. package/scripts/vision-hook.sh +104 -0
  123. package/scripts/vision-track.mjs +554 -0
  124. package/scripts/wire-all-orphans.mjs +108 -0
  125. package/scripts/wire-orphans.mjs +164 -0
  126. package/server/activity-routes.js +123 -0
  127. package/server/agent-health.js +197 -0
  128. package/server/agent-hooks.js +102 -0
  129. package/server/agent-mcp.js +10 -0
  130. package/server/agent-registry.js +95 -0
  131. package/server/agent-server.js +290 -0
  132. package/server/agent-spawn.js +251 -0
  133. package/server/agent-templates.js +77 -0
  134. package/server/artifact-manager.js +247 -0
  135. package/server/artifact-templates/architecture.md +28 -0
  136. package/server/artifact-templates/blueprint.md +21 -0
  137. package/server/artifact-templates/design.md +36 -0
  138. package/server/artifact-templates/plan.md +25 -0
  139. package/server/artifact-templates/prd.md +43 -0
  140. package/server/artifact-templates/report.md +40 -0
  141. package/server/block-tracker.js +90 -0
  142. package/server/build-stream-bridge.js +502 -0
  143. package/server/coalescing-buffer.js +46 -0
  144. package/server/compose-mcp-tools.js +479 -0
  145. package/server/compose-mcp.js +324 -0
  146. package/server/connectors/agent-connector.js +78 -0
  147. package/server/connectors/claude-sdk-connector.js +198 -0
  148. package/server/connectors/codex-connector.js +240 -0
  149. package/server/connectors/connector-discovery.js +18 -0
  150. package/server/connectors/connector-runtime.js +13 -0
  151. package/server/connectors/opencode-connector.js +200 -0
  152. package/server/design-routes.js +540 -0
  153. package/server/design-session.js +161 -0
  154. package/server/feature-scan.js +593 -0
  155. package/server/file-watcher.js +284 -0
  156. package/server/find-root.js +29 -0
  157. package/server/graph-export.js +343 -0
  158. package/server/ideabox-cache.js +77 -0
  159. package/server/ideabox-routes.js +294 -0
  160. package/server/index.js +156 -0
  161. package/server/model-tiers.js +49 -0
  162. package/server/pipeline-routes.js +288 -0
  163. package/server/policy-evaluator.js +36 -0
  164. package/server/project-root.js +122 -0
  165. package/server/security.js +23 -0
  166. package/server/session-manager.js +403 -0
  167. package/server/session-routes.js +190 -0
  168. package/server/session-store.js +107 -0
  169. package/server/settings-routes.js +35 -0
  170. package/server/settings-store.js +234 -0
  171. package/server/stratum-api.js +102 -0
  172. package/server/stratum-client.js +192 -0
  173. package/server/stratum-sync.js +193 -0
  174. package/server/summarizer.js +139 -0
  175. package/server/supervisor.js +196 -0
  176. package/server/vision-routes.js +668 -0
  177. package/server/vision-server.js +393 -0
  178. package/server/vision-store.js +360 -0
  179. package/server/vision-utils.js +179 -0
  180. package/server/worktree-gc.js +137 -0
  181. package/templates/ROADMAP.md +46 -0
@@ -0,0 +1,194 @@
1
+ /**
2
+ * gate-tiers.js — COMP-OBS-GATES: tiered gate evaluation with short-circuit.
3
+ *
4
+ * Defines the five evaluation tiers used by the review pipeline.
5
+ * Tiers are ordered cheapest → most expensive. When a tier fails, all
6
+ * subsequent (more expensive) tiers are skipped — short-circuit.
7
+ *
8
+ * Tiers:
9
+ * T0 schema — output contract validation (free, instant)
10
+ * T1 lint — lint/format checks (fast)
11
+ * T2 tests — test suite execution (medium)
12
+ * T3 llm-review — Claude multi-lens review (expensive)
13
+ * T4 cross-model — Codex cross-model review (very expensive)
14
+ */
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Tier definitions
18
+ // ---------------------------------------------------------------------------
19
+
20
+ export const GATE_TIERS = [
21
+ {
22
+ id: 'T0',
23
+ name: 'schema',
24
+ cost: 'free',
25
+ description: 'Output contract validation',
26
+ },
27
+ {
28
+ id: 'T1',
29
+ name: 'lint',
30
+ cost: 'fast',
31
+ description: 'Lint/format checks',
32
+ },
33
+ {
34
+ id: 'T2',
35
+ name: 'tests',
36
+ cost: 'medium',
37
+ description: 'Test suite execution',
38
+ },
39
+ {
40
+ id: 'T3',
41
+ name: 'llm-review',
42
+ cost: 'expensive',
43
+ description: 'Claude multi-lens review',
44
+ },
45
+ {
46
+ id: 'T4',
47
+ name: 'cross-model',
48
+ cost: 'very-expensive',
49
+ description: 'Codex cross-model review',
50
+ },
51
+ ];
52
+
53
+ // Map tier ID → index for fast ordering lookups
54
+ const TIER_ORDER = new Map(GATE_TIERS.map((t, i) => [t.id, i]));
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Cost estimates (rough USD per invocation)
58
+ // ---------------------------------------------------------------------------
59
+
60
+ // Base cost constants (USD)
61
+ const TIER_BASE_COST_USD = {
62
+ T0: 0.00, // free — purely local contract validation
63
+ T1: 0.00, // free — local lint process
64
+ T2: 0.05, // test suite: estimate varies; use conservative floor
65
+ T3: 0.50, // Opus multi-lens: ~$0.50 per review pass
66
+ T4: 0.30, // Codex synthesis: ~$0.30 per cross-model pass
67
+ };
68
+
69
+ /**
70
+ * Estimate the cost of running a tier in USD.
71
+ *
72
+ * @param {string} tierId Tier ID (e.g. 'T0', 'T3')
73
+ * @param {object} [context] Optional context for future scaling (e.g. file count, lens count)
74
+ * @param {number} [context.lensCount] Number of review lenses (scales T3 cost)
75
+ * @returns {number} Estimated cost in USD
76
+ */
77
+ export function estimateTierCost(tierId, context = {}) {
78
+ const base = TIER_BASE_COST_USD[tierId] ?? 0;
79
+ if (tierId === 'T3' && typeof context.lensCount === 'number' && context.lensCount > 1) {
80
+ // Each additional lens beyond 1 adds ~50% of base cost
81
+ return base + (base * 0.5 * (context.lensCount - 1));
82
+ }
83
+ return base;
84
+ }
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // Step → tier classification
88
+ // ---------------------------------------------------------------------------
89
+
90
+ /**
91
+ * Map of step IDs to their tier.
92
+ * Step IDs come from .stratum.yaml pipeline specs.
93
+ */
94
+ const STEP_TIER_MAP = {
95
+ // T0: schema validation — happens implicitly via Stratum output_contract
96
+ output_contract: 'T0',
97
+ schema_check: 'T0',
98
+ validate: 'T0',
99
+
100
+ // T1: lint / format
101
+ lint: 'T1',
102
+ format: 'T1',
103
+ typecheck: 'T1',
104
+
105
+ // T2: test suite
106
+ run_tests: 'T2',
107
+ coverage: 'T2',
108
+ coverage_check: 'T2',
109
+ test: 'T2',
110
+
111
+ // T3: LLM review (Claude multi-lens)
112
+ review: 'T3',
113
+ parallel_review: 'T3',
114
+ triage: 'T3',
115
+ merge: 'T3',
116
+
117
+ // T4: cross-model review (Codex)
118
+ codex_review: 'T4',
119
+ cross_model: 'T4',
120
+ cross_model_review: 'T4',
121
+ };
122
+
123
+ /**
124
+ * Classify a pipeline step ID as a tier.
125
+ *
126
+ * @param {string} stepId Step ID from the pipeline spec
127
+ * @returns {string|null} Tier ID (e.g. 'T3') or null if unmapped
128
+ */
129
+ export function classifyStepAsTier(stepId) {
130
+ if (!stepId || typeof stepId !== 'string') return null;
131
+ return STEP_TIER_MAP[stepId] ?? null;
132
+ }
133
+
134
+ // ---------------------------------------------------------------------------
135
+ // Tiered evaluator with short-circuit
136
+ // ---------------------------------------------------------------------------
137
+
138
+ /**
139
+ * Evaluate tier results and short-circuit on the first failure.
140
+ *
141
+ * @param {object} tierResults Map of tier IDs to pass/fail booleans or null (not run).
142
+ * Example: { T0: true, T1: true, T2: false, T3: null, T4: null }
143
+ * @param {object} [costContext] Context for cost estimation (e.g. { lensCount: 3 })
144
+ * @returns {{
145
+ * passed: boolean,
146
+ * tierThatFailed: string|null,
147
+ * tiersRun: string[],
148
+ * tiersSkipped: string[],
149
+ * costSaved: number
150
+ * }}
151
+ */
152
+ export function evaluateTiers(tierResults, costContext = {}) {
153
+ // Sort tiers by their canonical order
154
+ const orderedTiers = GATE_TIERS.map(t => t.id);
155
+
156
+ const tiersRun = [];
157
+ const tiersSkipped = [];
158
+ let tierThatFailed = null;
159
+ let shortCircuiting = false;
160
+
161
+ for (const tierId of orderedTiers) {
162
+ const result = tierResults[tierId];
163
+
164
+ if (shortCircuiting || result === null || result === undefined) {
165
+ // Either we already failed, or this tier was never run
166
+ if (shortCircuiting) {
167
+ tiersSkipped.push(tierId);
168
+ }
169
+ // null/undefined = not run, not explicitly skipped — don't count either way
170
+ continue;
171
+ }
172
+
173
+ tiersRun.push(tierId);
174
+
175
+ if (result === false) {
176
+ tierThatFailed = tierId;
177
+ shortCircuiting = true;
178
+ }
179
+ }
180
+
181
+ // Cost saved = sum of estimated costs for skipped tiers
182
+ const costSaved = tiersSkipped.reduce(
183
+ (sum, tierId) => sum + estimateTierCost(tierId, costContext),
184
+ 0
185
+ );
186
+
187
+ return {
188
+ passed: tierThatFailed === null,
189
+ tierThatFailed,
190
+ tiersRun,
191
+ tiersSkipped,
192
+ costSaved,
193
+ };
194
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * health-history.js — COMP-HEALTH: Score history persistence (item 120).
3
+ *
4
+ * Appends health score records to .compose/data/health-scores.json.
5
+ * Provides read and trend APIs for UI and gate integration.
6
+ */
7
+
8
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+
11
+ /**
12
+ * Path to health-scores.json for a given project root.
13
+ * @param {string} cwd Project root (must contain .compose/)
14
+ * @returns {string}
15
+ */
16
+ function historyPath(cwd) {
17
+ return join(cwd, '.compose', 'data', 'health-scores.json');
18
+ }
19
+
20
+ /**
21
+ * Load the full history array from disk. Returns [] on missing/corrupt file.
22
+ * @param {string} cwd
23
+ * @returns {Array<object>}
24
+ */
25
+ function _load(cwd) {
26
+ const p = historyPath(cwd);
27
+ if (!existsSync(p)) return [];
28
+ try {
29
+ const raw = readFileSync(p, 'utf-8');
30
+ const parsed = JSON.parse(raw);
31
+ return Array.isArray(parsed) ? parsed : [];
32
+ } catch {
33
+ return [];
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Persist the full history array to disk (atomic-ish via direct write).
39
+ * @param {string} cwd
40
+ * @param {Array<object>} entries
41
+ */
42
+ function _save(cwd, entries) {
43
+ const p = historyPath(cwd);
44
+ mkdirSync(join(cwd, '.compose', 'data'), { recursive: true });
45
+ writeFileSync(p, JSON.stringify(entries, null, 2), 'utf-8');
46
+ }
47
+
48
+ /**
49
+ * Record a health score entry for a feature.
50
+ *
51
+ * @param {string} cwd
52
+ * @param {object} entry
53
+ * @param {string} entry.featureCode Feature code (e.g. 'FEAT-1')
54
+ * @param {string} [entry.phase] Build phase at scoring time
55
+ * @param {number} entry.score Composite score 0-100
56
+ * @param {object} entry.breakdown Per-dimension scores
57
+ * @param {string} [entry.timestamp] ISO timestamp (default: now)
58
+ */
59
+ export function recordScore(cwd, { featureCode, phase = null, score, breakdown = {}, timestamp = null }) {
60
+ const entries = _load(cwd);
61
+ entries.push({
62
+ featureCode,
63
+ phase,
64
+ score,
65
+ breakdown,
66
+ timestamp: timestamp ?? new Date().toISOString(),
67
+ });
68
+ _save(cwd, entries);
69
+ }
70
+
71
+ /**
72
+ * Read all history entries for a feature, sorted chronologically (oldest first).
73
+ *
74
+ * @param {string} cwd
75
+ * @param {string} featureCode
76
+ * @returns {Array<object>}
77
+ */
78
+ export function readHistory(cwd, featureCode) {
79
+ const all = _load(cwd);
80
+ return all
81
+ .filter(e => e.featureCode === featureCode)
82
+ .sort((a, b) => {
83
+ const ta = a.timestamp ? new Date(a.timestamp).getTime() : 0;
84
+ const tb = b.timestamp ? new Date(b.timestamp).getTime() : 0;
85
+ return ta - tb;
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Compute the trend for a feature based on its two most recent scores.
91
+ *
92
+ * @param {string} cwd
93
+ * @param {string} featureCode
94
+ * @returns {{ latest: object|null, previous: object|null, delta: number|null, direction: 'improving'|'declining'|'stable'|null }}
95
+ */
96
+ export function getTrend(cwd, featureCode) {
97
+ const history = readHistory(cwd, featureCode);
98
+ if (history.length === 0) {
99
+ return { latest: null, previous: null, delta: null, direction: null };
100
+ }
101
+ if (history.length === 1) {
102
+ return { latest: history[0], previous: null, delta: null, direction: null };
103
+ }
104
+
105
+ const latest = history[history.length - 1];
106
+ const previous = history[history.length - 2];
107
+ const delta = latest.score - previous.score;
108
+
109
+ let direction;
110
+ if (delta > 1) {
111
+ direction = 'improving';
112
+ } else if (delta < -1) {
113
+ direction = 'declining';
114
+ } else {
115
+ direction = 'stable';
116
+ }
117
+
118
+ return { latest, previous, delta, direction };
119
+ }
@@ -0,0 +1,227 @@
1
+ /**
2
+ * health-score.js — COMP-HEALTH: Quantified Quality Score for Gates (items 117-120).
3
+ *
4
+ * Aggregates signals from existing build artifacts into a 0-100 composite
5
+ * health score. No new data sources — only aggregates what already exists:
6
+ * - Review findings from parallel_review's MergedReviewResult
7
+ * - Test pass/fail from coverage_check result
8
+ * - Contract compliance from Stratum ensure results
9
+ * - Doc freshness from lib/staleness.js
10
+ * - Plan completion from plan_completion ensure builtin
11
+ * - Runtime errors from build_step_done violations
12
+ */
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Dimension definitions
16
+ // ---------------------------------------------------------------------------
17
+
18
+ /**
19
+ * Default dimension weights. Weights sum to 1.0.
20
+ * @type {Record<string, {weight: number, name: string}>}
21
+ */
22
+ export const DIMENSIONS = {
23
+ test_coverage: { weight: 0.225, name: 'Test Coverage' },
24
+ review_findings: { weight: 0.225, name: 'Review Findings' },
25
+ contract_compliance:{ weight: 0.135, name: 'Contract Compliance' },
26
+ runtime_errors: { weight: 0.135, name: 'Runtime Errors' },
27
+ doc_freshness: { weight: 0.09, name: 'Doc Freshness' },
28
+ plan_completion: { weight: 0.09, name: 'Plan Completion' },
29
+ debug_discipline: { weight: 0.10, name: 'Debug Discipline' },
30
+ };
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Per-dimension scorers
34
+ // ---------------------------------------------------------------------------
35
+
36
+ /**
37
+ * Score test coverage from a coverage_check result.
38
+ *
39
+ * @param {object|null} testResult Coverage result: { passing, failures }
40
+ * @returns {number} 0-100
41
+ */
42
+ export function scoreTestCoverage(testResult) {
43
+ if (testResult == null) return 50; // neutral — no data
44
+ // Truthy passing with no failures = full pass
45
+ if (testResult.passing === true && (!testResult.failures || testResult.failures.length === 0)) {
46
+ return 100;
47
+ }
48
+ // Explicitly failing
49
+ if (testResult.passing === false) return 0;
50
+ // Mixed or unknown — treat partial
51
+ return 50;
52
+ }
53
+
54
+ /**
55
+ * Score review findings from a MergedReviewResult.
56
+ *
57
+ * Severity breakdown:
58
+ * must-fix → -20 per finding
59
+ * should-fix → -5 per finding
60
+ * nit → -1 per finding
61
+ *
62
+ * @param {object|null} mergedResult { findings: [{severity, ...}] }
63
+ * @returns {number} 0-100 (floored at 0)
64
+ */
65
+ export function scoreReviewFindings(mergedResult) {
66
+ if (mergedResult == null) return 50; // neutral — no data
67
+ const findings = mergedResult.findings ?? mergedResult.all_findings ?? [];
68
+ if (findings.length === 0) return 100;
69
+
70
+ let score = 100;
71
+ for (const f of findings) {
72
+ const sev = (f.severity ?? '').toLowerCase();
73
+ if (sev === 'must-fix' || sev === 'must_fix') {
74
+ score -= 20;
75
+ } else if (sev === 'should-fix' || sev === 'should_fix') {
76
+ score -= 5;
77
+ } else if (sev === 'nit') {
78
+ score -= 1;
79
+ }
80
+ }
81
+ return Math.max(0, score);
82
+ }
83
+
84
+ /**
85
+ * Score contract compliance from Stratum ensure results.
86
+ * Each failed ensure costs -10 points.
87
+ *
88
+ * @param {Array<{passed: boolean}>|null} ensureResults
89
+ * @returns {number} 0-100 (floored at 0)
90
+ */
91
+ export function scoreContractCompliance(ensureResults) {
92
+ if (ensureResults == null || !Array.isArray(ensureResults)) return 50;
93
+ if (ensureResults.length === 0) return 100;
94
+
95
+ const failed = ensureResults.filter(e => !e.passed).length;
96
+ return Math.max(0, 100 - failed * 10);
97
+ }
98
+
99
+ /**
100
+ * Score runtime errors from capability violations or build violations.
101
+ * Each violation costs -15 points.
102
+ *
103
+ * @param {Array<string|object>|null} violations
104
+ * @returns {number} 0-100 (floored at 0)
105
+ */
106
+ export function scoreRuntimeErrors(violations) {
107
+ if (violations == null || !Array.isArray(violations)) return 50;
108
+ if (violations.length === 0) return 100;
109
+ return Math.max(0, 100 - violations.length * 15);
110
+ }
111
+
112
+ /**
113
+ * Score doc freshness from staleness check results.
114
+ * Each stale doc costs -20 points.
115
+ *
116
+ * @param {Array<{stale: boolean}>|null} stalenessResults
117
+ * @returns {number} 0-100 (floored at 0)
118
+ */
119
+ export function scoreDocFreshness(stalenessResults) {
120
+ if (stalenessResults == null || !Array.isArray(stalenessResults)) return 50;
121
+ if (stalenessResults.length === 0) return 100;
122
+
123
+ const staleCount = stalenessResults.filter(r => r.stale).length;
124
+ if (staleCount === 0) return 100;
125
+ return Math.max(0, 100 - staleCount * 20);
126
+ }
127
+
128
+ /**
129
+ * Score plan completion from a plan_completion ensure result.
130
+ * Uses planCompletionPct directly, or 50 if no plan data is available.
131
+ *
132
+ * @param {object|null} planResult { planCompletionPct: number } or null
133
+ * @returns {number} 0-100
134
+ */
135
+ export function scorePlanCompletion(planResult) {
136
+ if (planResult == null) return 50;
137
+ const pct = planResult.planCompletionPct ?? planResult.completion_pct;
138
+ if (typeof pct !== 'number') return 50;
139
+ return Math.max(0, Math.min(100, pct));
140
+ }
141
+
142
+ /**
143
+ * Score debug discipline from debug ledger signals.
144
+ *
145
+ * Penalty breakdown:
146
+ * fix_chain_count → -15 per chain
147
+ * untraced_fixes → -20 per untraced fix
148
+ * escalation_count → -10 per escalation
149
+ *
150
+ * @param {object|null} signals { fix_chain_count, untraced_fixes, escalation_count }
151
+ * @returns {number} 0-100 (floored at 0)
152
+ */
153
+ export function scoreDebugDiscipline(signals) {
154
+ if (signals == null) return 50;
155
+ let score = 100;
156
+ score -= (signals.fix_chain_count ?? 0) * 15;
157
+ score -= (signals.untraced_fixes ?? 0) * 20;
158
+ score -= (signals.escalation_count ?? 0) * 10;
159
+ return Math.max(0, score);
160
+ }
161
+
162
+ // ---------------------------------------------------------------------------
163
+ // Composite scorer
164
+ // ---------------------------------------------------------------------------
165
+
166
+ /**
167
+ * Compute a composite health score from a set of signals.
168
+ *
169
+ * Missing dimensions are scored as 50 (neutral) but their weight is
170
+ * re-normalized out so they don't artificially lower the score — we only
171
+ * penalize for data we actually have.
172
+ *
173
+ * @param {object} signals Map of dimension key → raw signal data
174
+ * @param {object} [weights] Override weights (same shape as DIMENSIONS)
175
+ * @returns {{ score: number, breakdown: Record<string,number>, missing: string[] }}
176
+ */
177
+ export function computeCompositeScore(signals = {}, weights = {}) {
178
+ const dimWeights = { ...DIMENSIONS };
179
+ // Apply any user-supplied weight overrides
180
+ for (const [key, w] of Object.entries(weights)) {
181
+ if (dimWeights[key] != null && typeof w === 'number') {
182
+ dimWeights[key] = { ...dimWeights[key], weight: w };
183
+ }
184
+ }
185
+
186
+ const scorers = {
187
+ test_coverage: () => scoreTestCoverage(signals.test_coverage),
188
+ review_findings: () => scoreReviewFindings(signals.review_findings),
189
+ contract_compliance: () => scoreContractCompliance(signals.contract_compliance),
190
+ runtime_errors: () => scoreRuntimeErrors(signals.runtime_errors),
191
+ doc_freshness: () => scoreDocFreshness(signals.doc_freshness),
192
+ plan_completion: () => scorePlanCompletion(signals.plan_completion),
193
+ debug_discipline: () => scoreDebugDiscipline(signals.debug_discipline),
194
+ };
195
+
196
+ const breakdown = {};
197
+ const missing = [];
198
+
199
+ // Score each dimension. A dimension is "present" if its key is in signals.
200
+ for (const dim of Object.keys(dimWeights)) {
201
+ if (Object.prototype.hasOwnProperty.call(signals, dim)) {
202
+ breakdown[dim] = scorers[dim]();
203
+ } else {
204
+ missing.push(dim);
205
+ // Score neutral — excluded from weight normalization below
206
+ }
207
+ }
208
+
209
+ const presentDims = Object.keys(breakdown);
210
+
211
+ if (presentDims.length === 0) {
212
+ // No signals at all — return neutral
213
+ return { score: 50, breakdown: {}, missing };
214
+ }
215
+
216
+ // Re-normalize: sum weights of present dimensions only
217
+ const totalWeight = presentDims.reduce((s, d) => s + dimWeights[d].weight, 0);
218
+ const score = presentDims.reduce((s, d) => {
219
+ return s + (breakdown[d] * dimWeights[d].weight) / totalWeight;
220
+ }, 0);
221
+
222
+ return {
223
+ score: Math.round(score * 10) / 10, // 1 decimal
224
+ breakdown,
225
+ missing,
226
+ };
227
+ }