@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.
- package/LICENSE +21 -0
- package/README.md +1014 -0
- package/bin/compose.js +1515 -0
- package/dist/assets/_baseUniq-CQwX6VLz.js +1 -0
- package/dist/assets/arc-SxJ2J1sh.js +1 -0
- package/dist/assets/architectureDiagram-Q4EWVU46-BykunY1F.js +36 -0
- package/dist/assets/blockDiagram-DXYQGD6D-ohAKBOUw.js +132 -0
- package/dist/assets/c4Diagram-AHTNJAMY-DBDC3ENB.js +10 -0
- package/dist/assets/channel-DGElom1e.js +1 -0
- package/dist/assets/chunk-4BX2VUAB-Cv93Z7uM.js +1 -0
- package/dist/assets/chunk-4TB4RGXK-DE0WBDkj.js +206 -0
- package/dist/assets/chunk-55IACEB6-CE1EXenG.js +1 -0
- package/dist/assets/chunk-EDXVE4YY-DA7Ana6H.js +1 -0
- package/dist/assets/chunk-FMBD7UC4-CTDIPA3p.js +15 -0
- package/dist/assets/chunk-OYMX7WX6-uGBaPaTX.js +231 -0
- package/dist/assets/chunk-QZHKN3VN-CYlnXuUO.js +1 -0
- package/dist/assets/chunk-YZCP3GAM-ojGkzcZK.js +1 -0
- package/dist/assets/classDiagram-6PBFFD2Q-KqWP9wWZ.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-KqWP9wWZ.js +1 -0
- package/dist/assets/clone-DUJKJXd7.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-Bktn9hL-.js +1 -0
- package/dist/assets/dagre-KV5264BT-DFaSzuRF.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-5BDNPKRD-DnfmDzEm.js +10 -0
- package/dist/assets/diagram-G4DWMVQ6-Bm8W9YnG.js +24 -0
- package/dist/assets/diagram-MMDJMWI5-B5-TSKvp.js +43 -0
- package/dist/assets/diagram-TYMM5635-ls4rqlky.js +24 -0
- package/dist/assets/erDiagram-SMLLAGMA-giG6WO-r.js +85 -0
- package/dist/assets/flowDiagram-DWJPFMVM-XvlUuz-7.js +162 -0
- package/dist/assets/ganttDiagram-T4ZO3ILL-hLBV57oV.js +292 -0
- package/dist/assets/gitGraphDiagram-UUTBAWPF-BHu3s_Gn.js +106 -0
- package/dist/assets/graph-D0Cfv00Y.js +1 -0
- package/dist/assets/index-CUd6pFGF.css +1 -0
- package/dist/assets/index-DReRlzZI.js +1144 -0
- package/dist/assets/infoDiagram-42DDH7IO-DbqRsOo3.js +2 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/ishikawaDiagram-UXIWVN3A-DnCdx7zb.js +70 -0
- package/dist/assets/journeyDiagram-VCZTEJTY-CfD7eNcP.js +139 -0
- package/dist/assets/kanban-definition-6JOO6SKY-BYaO9-mK.js +89 -0
- package/dist/assets/katex-DkKDou_j.js +257 -0
- package/dist/assets/layout-Bj72wOEB.js +1 -0
- package/dist/assets/linear-BRFo114D.js +1 -0
- package/dist/assets/min-GCHnKlJS.js +1 -0
- package/dist/assets/mindmap-definition-QFDTVHPH-n0PMebY4.js +96 -0
- package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/assets/pieDiagram-DEJITSTG-pN4CljHF.js +30 -0
- package/dist/assets/quadrantDiagram-34T5L4WZ-DNoAy8-D.js +7 -0
- package/dist/assets/requirementDiagram-MS252O5E-BhtY05PT.js +84 -0
- package/dist/assets/sankeyDiagram-XADWPNL6-B6AD-16A.js +10 -0
- package/dist/assets/sequenceDiagram-FGHM5R23-DShHM-uk.js +157 -0
- package/dist/assets/stateDiagram-FHFEXIEX-DMxn7HTo.js +1 -0
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-o6PnCs4e.js +1 -0
- package/dist/assets/timeline-definition-GMOUNBTQ-Cdu6uq52.js +120 -0
- package/dist/assets/vennDiagram-DHZGUBPP-CpK29iRe.js +34 -0
- package/dist/assets/wardley-RL74JXVD-BQgSkdcO.js +162 -0
- package/dist/assets/wardleyDiagram-NUSXRM2D-DJHYev6O.js +20 -0
- package/dist/assets/xychartDiagram-5P7HB3ND-1d75pbaO.js +7 -0
- package/dist/index.html +30 -0
- package/lib/agent-chains.js +65 -0
- package/lib/agent-string.js +86 -0
- package/lib/budget-ledger.js +86 -0
- package/lib/build-all.js +162 -0
- package/lib/build-dag.js +120 -0
- package/lib/build-stream-writer.js +190 -0
- package/lib/build.js +2997 -0
- package/lib/capability-checker.js +53 -0
- package/lib/cert-inject.js +38 -0
- package/lib/cli-progress.js +483 -0
- package/lib/constants.js +69 -0
- package/lib/cross-layer-audit.js +84 -0
- package/lib/debug-discipline.js +173 -0
- package/lib/feature-json.js +106 -0
- package/lib/gate-prompt.js +291 -0
- package/lib/gate-tiers.js +194 -0
- package/lib/health-history.js +119 -0
- package/lib/health-score.js +227 -0
- package/lib/ideabox.js +570 -0
- package/lib/import.js +244 -0
- package/lib/migrate-roadmap.js +94 -0
- package/lib/model-pricing.js +67 -0
- package/lib/new.js +413 -0
- package/lib/pipeline-cli.js +489 -0
- package/lib/plan-parser.js +103 -0
- package/lib/qa-scoping.js +474 -0
- package/lib/questionnaire.js +200 -0
- package/lib/resolve-port.js +7 -0
- package/lib/result-normalizer.js +349 -0
- package/lib/review-lenses.js +166 -0
- package/lib/roadmap-gen.js +210 -0
- package/lib/roadmap-parser.js +176 -0
- package/lib/server-probe.js +23 -0
- package/lib/staleness.js +87 -0
- package/lib/step-prompt.js +260 -0
- package/lib/step-validator.js +49 -0
- package/lib/stratum-mcp-client.js +365 -0
- package/lib/team-flag.js +46 -0
- package/lib/test-bootstrap.js +401 -0
- package/lib/triage.js +274 -0
- package/lib/vision-writer.js +391 -0
- package/package.json +111 -0
- package/pipelines/bug-fix.stratum.yaml +230 -0
- package/pipelines/build.stratum.yaml +498 -0
- package/pipelines/content.stratum.yaml +112 -0
- package/pipelines/coverage-sweep.stratum.yaml +52 -0
- package/pipelines/refactor.stratum.yaml +169 -0
- package/pipelines/research.stratum.yaml +88 -0
- package/pipelines/review-fix.stratum.yaml +109 -0
- package/presets/team-feature.stratum.yaml +105 -0
- package/presets/team-research.stratum.yaml +108 -0
- package/presets/team-review.stratum.yaml +106 -0
- package/scripts/agent-activity-hook.sh +31 -0
- package/scripts/agent-error-hook.sh +28 -0
- package/scripts/analyze-orphans.mjs +50 -0
- package/scripts/find-orphans.mjs +26 -0
- package/scripts/fix-phases.mjs +49 -0
- package/scripts/generate-stratum-spec.mjs +137 -0
- package/scripts/import-roadmap.mjs +116 -0
- package/scripts/phase-audit.mjs +33 -0
- package/scripts/run-pipeline.mjs +314 -0
- package/scripts/session-end-hook.sh +18 -0
- package/scripts/session-start-hook.sh +38 -0
- package/scripts/vision-hook.sh +104 -0
- package/scripts/vision-track.mjs +554 -0
- package/scripts/wire-all-orphans.mjs +108 -0
- package/scripts/wire-orphans.mjs +164 -0
- package/server/activity-routes.js +123 -0
- package/server/agent-health.js +197 -0
- package/server/agent-hooks.js +102 -0
- package/server/agent-mcp.js +10 -0
- package/server/agent-registry.js +95 -0
- package/server/agent-server.js +290 -0
- package/server/agent-spawn.js +251 -0
- package/server/agent-templates.js +77 -0
- package/server/artifact-manager.js +247 -0
- package/server/artifact-templates/architecture.md +28 -0
- package/server/artifact-templates/blueprint.md +21 -0
- package/server/artifact-templates/design.md +36 -0
- package/server/artifact-templates/plan.md +25 -0
- package/server/artifact-templates/prd.md +43 -0
- package/server/artifact-templates/report.md +40 -0
- package/server/block-tracker.js +90 -0
- package/server/build-stream-bridge.js +502 -0
- package/server/coalescing-buffer.js +46 -0
- package/server/compose-mcp-tools.js +479 -0
- package/server/compose-mcp.js +324 -0
- package/server/connectors/agent-connector.js +78 -0
- package/server/connectors/claude-sdk-connector.js +198 -0
- package/server/connectors/codex-connector.js +240 -0
- package/server/connectors/connector-discovery.js +18 -0
- package/server/connectors/connector-runtime.js +13 -0
- package/server/connectors/opencode-connector.js +200 -0
- package/server/design-routes.js +540 -0
- package/server/design-session.js +161 -0
- package/server/feature-scan.js +593 -0
- package/server/file-watcher.js +284 -0
- package/server/find-root.js +29 -0
- package/server/graph-export.js +343 -0
- package/server/ideabox-cache.js +77 -0
- package/server/ideabox-routes.js +294 -0
- package/server/index.js +156 -0
- package/server/model-tiers.js +49 -0
- package/server/pipeline-routes.js +288 -0
- package/server/policy-evaluator.js +36 -0
- package/server/project-root.js +122 -0
- package/server/security.js +23 -0
- package/server/session-manager.js +403 -0
- package/server/session-routes.js +190 -0
- package/server/session-store.js +107 -0
- package/server/settings-routes.js +35 -0
- package/server/settings-store.js +234 -0
- package/server/stratum-api.js +102 -0
- package/server/stratum-client.js +192 -0
- package/server/stratum-sync.js +193 -0
- package/server/summarizer.js +139 -0
- package/server/supervisor.js +196 -0
- package/server/vision-routes.js +668 -0
- package/server/vision-server.js +393 -0
- package/server/vision-store.js +360 -0
- package/server/vision-utils.js +179 -0
- package/server/worktree-gc.js +137 -0
- 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
|
+
}
|