@nforma.ai/nforma 0.2.1 → 0.28.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/README.md +2 -2
- package/agents/{qgsd-codebase-mapper.md → nf-codebase-mapper.md} +1 -1
- package/agents/{qgsd-debugger.md → nf-debugger.md} +3 -3
- package/agents/{qgsd-executor.md → nf-executor.md} +14 -14
- package/agents/{qgsd-integration-checker.md → nf-integration-checker.md} +1 -1
- package/agents/{qgsd-phase-researcher.md → nf-phase-researcher.md} +6 -6
- package/agents/{qgsd-plan-checker.md → nf-plan-checker.md} +9 -9
- package/agents/{qgsd-planner.md → nf-planner.md} +9 -9
- package/agents/{qgsd-project-researcher.md → nf-project-researcher.md} +2 -2
- package/agents/{qgsd-quorum-orchestrator.md → nf-quorum-orchestrator.md} +33 -33
- package/agents/{qgsd-quorum-slot-worker.md → nf-quorum-slot-worker.md} +3 -3
- package/agents/{qgsd-quorum-synthesizer.md → nf-quorum-synthesizer.md} +3 -3
- package/agents/{qgsd-quorum-test-worker.md → nf-quorum-test-worker.md} +1 -1
- package/agents/{qgsd-quorum-worker.md → nf-quorum-worker.md} +6 -6
- package/agents/{qgsd-research-synthesizer.md → nf-research-synthesizer.md} +5 -5
- package/agents/{qgsd-roadmapper.md → nf-roadmapper.md} +3 -3
- package/agents/{qgsd-verifier.md → nf-verifier.md} +8 -8
- package/bin/accept-debug-invariant.cjs +2 -2
- package/bin/account-manager.cjs +10 -10
- package/bin/aggregate-requirements.cjs +1 -1
- package/bin/analyze-assumptions.cjs +3 -3
- package/bin/analyze-state-space.cjs +14 -14
- package/bin/assumption-register.cjs +146 -0
- package/bin/attribute-trace-divergence.cjs +1 -1
- package/bin/auth-drivers/gh-cli.cjs +1 -1
- package/bin/auth-drivers/pool.cjs +1 -1
- package/bin/autoClosePtoF.cjs +3 -3
- package/bin/budget-tracker.cjs +77 -0
- package/bin/build-layer-manifest.cjs +153 -0
- package/bin/call-quorum-slot.cjs +3 -3
- package/bin/ccr-secure-config.cjs +5 -5
- package/bin/check-bundled-sdks.cjs +1 -1
- package/bin/check-mcp-health.cjs +1 -1
- package/bin/check-provider-health.cjs +6 -6
- package/bin/check-spec-sync.cjs +26 -26
- package/bin/check-trace-schema-drift.cjs +5 -5
- package/bin/conformance-schema.cjs +2 -2
- package/bin/cross-layer-dashboard.cjs +297 -0
- package/bin/design-impact.cjs +377 -0
- package/bin/detect-coverage-gaps.cjs +7 -7
- package/bin/failure-mode-catalog.cjs +227 -0
- package/bin/failure-taxonomy.cjs +177 -0
- package/bin/formal-scope-scan.cjs +179 -0
- package/bin/gate-a-grounding.cjs +334 -0
- package/bin/gate-b-abstraction.cjs +243 -0
- package/bin/gate-c-validation.cjs +166 -0
- package/bin/generate-formal-specs.cjs +17 -17
- package/bin/generate-petri-net.cjs +3 -3
- package/bin/generate-tla-cfg.cjs +5 -5
- package/bin/git-heatmap.cjs +571 -0
- package/bin/harness-diagnostic.cjs +326 -0
- package/bin/hazard-model.cjs +261 -0
- package/bin/install-formal-tools.cjs +1 -1
- package/bin/install.js +184 -139
- package/bin/instrumentation-map.cjs +178 -0
- package/bin/invariant-catalog.cjs +437 -0
- package/bin/issue-classifier.cjs +2 -2
- package/bin/load-baseline-requirements.cjs +4 -4
- package/bin/manage-agents-core.cjs +32 -32
- package/bin/migrate-to-slots.cjs +39 -39
- package/bin/mismatch-register.cjs +217 -0
- package/bin/nForma.cjs +176 -81
- package/bin/{qgsd-solve.cjs → nf-solve.cjs} +327 -14
- package/bin/observe-config.cjs +8 -0
- package/bin/observe-debt-writer.cjs +1 -1
- package/bin/observe-handler-deps.cjs +356 -0
- package/bin/observe-handler-grafana.cjs +2 -17
- package/bin/observe-handler-internal.cjs +5 -5
- package/bin/observe-handler-logstash.cjs +2 -17
- package/bin/observe-handler-prometheus.cjs +2 -17
- package/bin/observe-handler-upstream.cjs +251 -0
- package/bin/observe-handlers.cjs +12 -33
- package/bin/observe-render.cjs +68 -22
- package/bin/observe-utils.cjs +37 -0
- package/bin/observed-fsm.cjs +324 -0
- package/bin/planning-paths.cjs +6 -0
- package/bin/polyrepo.cjs +1 -1
- package/bin/probe-quorum-slots.cjs +1 -1
- package/bin/promote-gate-maturity.cjs +274 -0
- package/bin/promote-model.cjs +1 -1
- package/bin/propose-debug-invariants.cjs +1 -1
- package/bin/quorum-cache.cjs +144 -0
- package/bin/quorum-consensus-gate.cjs +1 -1
- package/bin/quorum-slot-dispatch.cjs +6 -6
- package/bin/requirements-core.cjs +1 -1
- package/bin/review-mcp-logs.cjs +1 -1
- package/bin/risk-heatmap.cjs +151 -0
- package/bin/run-account-manager-tlc.cjs +4 -4
- package/bin/run-account-pool-alloy.cjs +2 -2
- package/bin/run-alloy.cjs +2 -2
- package/bin/run-audit-alloy.cjs +2 -2
- package/bin/run-breaker-tlc.cjs +3 -3
- package/bin/run-formal-check.cjs +9 -9
- package/bin/run-formal-verify.cjs +30 -9
- package/bin/run-installer-alloy.cjs +2 -2
- package/bin/run-oscillation-tlc.cjs +4 -4
- package/bin/run-phase-tlc.cjs +1 -1
- package/bin/run-protocol-tlc.cjs +4 -4
- package/bin/run-quorum-composition-alloy.cjs +2 -2
- package/bin/run-sensitivity-sweep.cjs +2 -2
- package/bin/run-stop-hook-tlc.cjs +3 -3
- package/bin/run-tlc.cjs +21 -21
- package/bin/run-transcript-alloy.cjs +2 -2
- package/bin/secrets.cjs +5 -5
- package/bin/security-sweep.cjs +238 -0
- package/bin/sensitivity-report.cjs +3 -3
- package/bin/set-secret.cjs +5 -5
- package/bin/setup-telemetry-cron.sh +3 -3
- package/bin/stall-detector.cjs +126 -0
- package/bin/state-candidates.cjs +206 -0
- package/bin/sync-baseline-requirements.cjs +1 -1
- package/bin/telemetry-collector.cjs +1 -1
- package/bin/test-changed.cjs +111 -0
- package/bin/test-recipe-gen.cjs +250 -0
- package/bin/trace-corpus-stats.cjs +211 -0
- package/bin/unified-mcp-server.mjs +3 -3
- package/bin/update-scoreboard.cjs +1 -1
- package/bin/validate-memory.cjs +2 -2
- package/bin/validate-traces.cjs +10 -10
- package/bin/verify-quorum-health.cjs +66 -5
- package/bin/xstate-to-tla.cjs +4 -4
- package/bin/xstate-trace-walker.cjs +3 -3
- package/commands/{qgsd → nf}/add-phase.md +3 -3
- package/commands/{qgsd → nf}/add-requirement.md +3 -3
- package/commands/{qgsd → nf}/add-todo.md +3 -3
- package/commands/{qgsd → nf}/audit-milestone.md +4 -4
- package/commands/{qgsd → nf}/check-todos.md +3 -3
- package/commands/{qgsd → nf}/cleanup.md +3 -3
- package/commands/{qgsd → nf}/close-formal-gaps.md +2 -2
- package/commands/{qgsd → nf}/complete-milestone.md +9 -9
- package/commands/{qgsd → nf}/debug.md +9 -9
- package/commands/{qgsd → nf}/discuss-phase.md +3 -3
- package/commands/{qgsd → nf}/execute-phase.md +15 -15
- package/commands/{qgsd → nf}/fix-tests.md +3 -3
- package/commands/{qgsd → nf}/formal-test-sync.md +1 -1
- package/commands/{qgsd → nf}/health.md +3 -3
- package/commands/{qgsd → nf}/help.md +3 -3
- package/commands/{qgsd → nf}/insert-phase.md +3 -3
- package/commands/nf/join-discord.md +18 -0
- package/commands/{qgsd → nf}/list-phase-assumptions.md +2 -2
- package/commands/{qgsd → nf}/map-codebase.md +7 -7
- package/commands/{qgsd → nf}/map-requirements.md +3 -3
- package/commands/{qgsd → nf}/mcp-restart.md +3 -3
- package/commands/{qgsd → nf}/mcp-set-model.md +8 -8
- package/commands/{qgsd → nf}/mcp-setup.md +63 -63
- package/commands/{qgsd → nf}/mcp-status.md +3 -3
- package/commands/{qgsd → nf}/mcp-update.md +7 -7
- package/commands/{qgsd → nf}/new-milestone.md +8 -8
- package/commands/{qgsd → nf}/new-project.md +8 -8
- package/commands/{qgsd → nf}/observe.md +49 -16
- package/commands/{qgsd → nf}/pause-work.md +3 -3
- package/commands/{qgsd → nf}/plan-milestone-gaps.md +5 -5
- package/commands/{qgsd → nf}/plan-phase.md +6 -6
- package/commands/{qgsd → nf}/polyrepo.md +2 -2
- package/commands/{qgsd → nf}/progress.md +3 -3
- package/commands/{qgsd → nf}/queue.md +2 -2
- package/commands/{qgsd → nf}/quick.md +8 -8
- package/commands/{qgsd → nf}/quorum-test.md +10 -10
- package/commands/{qgsd → nf}/quorum.md +40 -40
- package/commands/{qgsd → nf}/reapply-patches.md +2 -2
- package/commands/{qgsd → nf}/remove-phase.md +3 -3
- package/commands/{qgsd → nf}/research-phase.md +12 -12
- package/commands/{qgsd → nf}/resume-work.md +3 -3
- package/commands/nf/review-requirements.md +31 -0
- package/commands/{qgsd → nf}/set-profile.md +3 -3
- package/commands/{qgsd → nf}/settings.md +6 -6
- package/commands/{qgsd → nf}/solve.md +35 -35
- package/commands/{qgsd → nf}/sync-baselines.md +4 -4
- package/commands/{qgsd → nf}/triage.md +10 -10
- package/commands/{qgsd → nf}/update.md +3 -3
- package/commands/{qgsd → nf}/verify-work.md +5 -5
- package/hooks/dist/config-loader.js +188 -32
- package/hooks/dist/conformance-schema.cjs +2 -2
- package/hooks/dist/gsd-context-monitor.js +118 -13
- package/hooks/dist/{qgsd-check-update.js → nf-check-update.js} +5 -5
- package/hooks/dist/{qgsd-circuit-breaker.js → nf-circuit-breaker.js} +35 -24
- package/hooks/dist/nf-circuit-breaker.test.js +1002 -0
- package/hooks/dist/{qgsd-precompact.js → nf-precompact.js} +13 -13
- package/hooks/dist/nf-precompact.test.js +227 -0
- package/hooks/dist/{qgsd-prompt.js → nf-prompt.js} +110 -33
- package/hooks/dist/nf-prompt.test.js +698 -0
- package/hooks/dist/nf-session-start.js +185 -0
- package/hooks/dist/nf-session-start.test.js +354 -0
- package/hooks/dist/{qgsd-slot-correlator.js → nf-slot-correlator.js} +13 -5
- package/hooks/dist/nf-slot-correlator.test.js +85 -0
- package/hooks/dist/{qgsd-spec-regen.js → nf-spec-regen.js} +17 -8
- package/hooks/dist/nf-spec-regen.test.js +73 -0
- package/hooks/dist/{qgsd-statusline.js → nf-statusline.js} +12 -3
- package/hooks/dist/nf-statusline.test.js +157 -0
- package/hooks/dist/{qgsd-stop.js → nf-stop.js} +152 -18
- package/hooks/dist/nf-stop.test.js +1388 -0
- package/hooks/dist/{qgsd-token-collector.js → nf-token-collector.js} +12 -4
- package/hooks/dist/nf-token-collector.test.js +262 -0
- package/hooks/dist/unified-mcp-server.mjs +2 -2
- package/package.json +4 -4
- package/scripts/build-hooks.js +13 -6
- package/scripts/secret-audit.sh +1 -1
- package/scripts/verify-hooks-sync.cjs +90 -0
- package/templates/{qgsd.json → nf.json} +4 -4
- package/commands/qgsd/join-discord.md +0 -18
- package/hooks/dist/qgsd-session-start.js +0 -122
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* cross-layer-dashboard.cjs — Cross-Layer Alignment Dashboard.
|
|
6
|
+
*
|
|
7
|
+
* Aggregates L1 coverage, Gate A, Gate B, and Gate C scores into a single
|
|
8
|
+
* terminal view. Re-runs gate scripts by default for freshness.
|
|
9
|
+
*
|
|
10
|
+
* Requirements: INTG-04
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* node bin/cross-layer-dashboard.cjs # re-run gates, display terminal dashboard
|
|
14
|
+
* node bin/cross-layer-dashboard.cjs --cached # read existing gate JSON files (fast)
|
|
15
|
+
* node bin/cross-layer-dashboard.cjs --json # output aggregated JSON
|
|
16
|
+
* node bin/cross-layer-dashboard.cjs --cached --json
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const { spawnSync } = require('child_process');
|
|
22
|
+
|
|
23
|
+
const ROOT = process.env.PROJECT_ROOT
|
|
24
|
+
|| (process.argv.find(a => a.startsWith('--project-root=')) || '').replace('--project-root=', '')
|
|
25
|
+
|| path.join(__dirname, '..');
|
|
26
|
+
const SCRIPT_DIR = __dirname;
|
|
27
|
+
const FORMAL = path.join(ROOT, '.planning', 'formal');
|
|
28
|
+
const EVIDENCE_DIR = path.join(FORMAL, 'evidence');
|
|
29
|
+
const GATES_DIR = path.join(FORMAL, 'gates');
|
|
30
|
+
|
|
31
|
+
const args = process.argv.slice(2);
|
|
32
|
+
const JSON_FLAG = args.includes('--json');
|
|
33
|
+
const CACHED_FLAG = args.includes('--cached');
|
|
34
|
+
|
|
35
|
+
// ── Health indicator logic ──────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Returns [PASS], [WARN], or [FAIL] based on score vs target.
|
|
39
|
+
* PASS: score >= target
|
|
40
|
+
* WARN: score >= target * 0.8
|
|
41
|
+
* FAIL: score < target * 0.8
|
|
42
|
+
*/
|
|
43
|
+
function healthIndicator(score, target) {
|
|
44
|
+
if (score == null || target == null) return '[N/A]';
|
|
45
|
+
if (score >= target) return '[PASS]';
|
|
46
|
+
if (score >= target * 0.8) return '[WARN]';
|
|
47
|
+
return '[FAIL]';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── spawnTool (adapted from nf-solve.cjs) ───────────────────────────────────
|
|
51
|
+
|
|
52
|
+
function spawnTool(script, spawnArgs) {
|
|
53
|
+
const scriptPath = path.join(SCRIPT_DIR, path.basename(script));
|
|
54
|
+
const childArgs = [...spawnArgs];
|
|
55
|
+
if (!childArgs.some(a => a.startsWith('--project-root='))) {
|
|
56
|
+
childArgs.push('--project-root=' + ROOT);
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const result = spawnSync(process.execPath, [scriptPath, ...childArgs], {
|
|
60
|
+
encoding: 'utf8',
|
|
61
|
+
cwd: ROOT,
|
|
62
|
+
timeout: 60000,
|
|
63
|
+
stdio: 'pipe',
|
|
64
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
65
|
+
});
|
|
66
|
+
if (result.error) {
|
|
67
|
+
return { ok: false, stdout: '', stderr: result.error.message };
|
|
68
|
+
}
|
|
69
|
+
// Gate scripts exit 1 when target not met but still produce valid JSON
|
|
70
|
+
return {
|
|
71
|
+
ok: result.status === 0 || result.status === 1,
|
|
72
|
+
stdout: result.stdout || '',
|
|
73
|
+
stderr: result.stderr || '',
|
|
74
|
+
exitCode: result.status,
|
|
75
|
+
};
|
|
76
|
+
} catch (err) {
|
|
77
|
+
return { ok: false, stdout: '', stderr: err.message };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Data collection ─────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
function readJsonFile(filePath) {
|
|
84
|
+
try {
|
|
85
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
86
|
+
} catch (err) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function collectGateData(script, cachedFile, jsonFlag) {
|
|
92
|
+
if (CACHED_FLAG) {
|
|
93
|
+
const cached = readJsonFile(path.join(GATES_DIR, cachedFile));
|
|
94
|
+
if (cached) return cached;
|
|
95
|
+
process.stderr.write(`[dashboard] WARN: cached file ${cachedFile} not found, gate unavailable\n`);
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const result = spawnTool(script, ['--json']);
|
|
99
|
+
if (!result.ok) {
|
|
100
|
+
process.stderr.write(`[dashboard] WARN: ${script} failed: ${result.stderr.slice(0, 200)}\n`);
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
return JSON.parse(result.stdout);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
process.stderr.write(`[dashboard] WARN: ${script} produced invalid JSON\n`);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function collectL1Coverage() {
|
|
112
|
+
const imap = readJsonFile(path.join(EVIDENCE_DIR, 'instrumentation-map.json'));
|
|
113
|
+
if (!imap || !imap.coverage) return null;
|
|
114
|
+
return imap.coverage.coverage_pct;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function collectMaturityData() {
|
|
118
|
+
if (CACHED_FLAG) {
|
|
119
|
+
// No cached file for maturity; skip in cached mode
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
const result = spawnTool('promote-gate-maturity.cjs', ['--check', '--json']);
|
|
123
|
+
if (!result.ok) return null;
|
|
124
|
+
try {
|
|
125
|
+
return JSON.parse(result.stdout);
|
|
126
|
+
} catch { return null; }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function collectAll() {
|
|
130
|
+
const gateA = collectGateData('gate-a-grounding.cjs', 'gate-a-grounding.json');
|
|
131
|
+
const gateB = collectGateData('gate-b-abstraction.cjs', 'gate-b-abstraction.json');
|
|
132
|
+
const gateC = collectGateData('gate-c-validation.cjs', 'gate-c-validation.json');
|
|
133
|
+
const l1Pct = collectL1Coverage();
|
|
134
|
+
const maturity = collectMaturityData();
|
|
135
|
+
|
|
136
|
+
return { gateA, gateB, gateC, l1Pct, maturity };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── Build aggregated result ─────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
function buildResult(data) {
|
|
142
|
+
const { gateA, gateB, gateC, l1Pct, maturity } = data;
|
|
143
|
+
|
|
144
|
+
const result = {
|
|
145
|
+
generated: new Date().toISOString(),
|
|
146
|
+
l1_coverage_pct: l1Pct != null ? l1Pct : null,
|
|
147
|
+
gate_a: gateA ? {
|
|
148
|
+
score: gateA.grounding_score,
|
|
149
|
+
target: gateA.target,
|
|
150
|
+
target_met: gateA.target_met,
|
|
151
|
+
explained: gateA.explained,
|
|
152
|
+
total: gateA.total,
|
|
153
|
+
} : null,
|
|
154
|
+
gate_b: gateB ? {
|
|
155
|
+
score: gateB.gate_b_score,
|
|
156
|
+
target: gateB.target,
|
|
157
|
+
target_met: gateB.target_met,
|
|
158
|
+
grounded_entries: gateB.grounded_entries,
|
|
159
|
+
total_entries: gateB.total_entries,
|
|
160
|
+
} : null,
|
|
161
|
+
gate_c: gateC ? {
|
|
162
|
+
score: gateC.gate_c_score,
|
|
163
|
+
target: gateC.target,
|
|
164
|
+
target_met: gateC.target_met,
|
|
165
|
+
validated_entries: gateC.validated_entries,
|
|
166
|
+
total_entries: gateC.total_entries,
|
|
167
|
+
} : null,
|
|
168
|
+
maturity: maturity ? {
|
|
169
|
+
total: maturity.total,
|
|
170
|
+
by_level: maturity.by_level,
|
|
171
|
+
} : null,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Determine overall health
|
|
175
|
+
const allMet = [
|
|
176
|
+
result.gate_a?.target_met,
|
|
177
|
+
result.gate_b?.target_met,
|
|
178
|
+
result.gate_c?.target_met,
|
|
179
|
+
];
|
|
180
|
+
result.all_targets_met = allMet.every(v => v === true);
|
|
181
|
+
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Terminal rendering ──────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
function pct(value, decimals = 1) {
|
|
188
|
+
if (value == null) return 'N/A';
|
|
189
|
+
return (value * 100).toFixed(decimals) + '%';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function renderTerminal(result) {
|
|
193
|
+
const lines = [];
|
|
194
|
+
const W = 64;
|
|
195
|
+
const hr = '─'.repeat(W);
|
|
196
|
+
const dhr = '═'.repeat(W);
|
|
197
|
+
|
|
198
|
+
lines.push('╔' + dhr + '╗');
|
|
199
|
+
lines.push('║' + ' Cross-Layer Alignment Dashboard'.padEnd(W) + '║');
|
|
200
|
+
lines.push('╚' + dhr + '╝');
|
|
201
|
+
lines.push('');
|
|
202
|
+
|
|
203
|
+
// L1 Coverage
|
|
204
|
+
const l1Target = 50; // percentage
|
|
205
|
+
const l1Val = result.l1_coverage_pct;
|
|
206
|
+
const l1Health = l1Val != null
|
|
207
|
+
? healthIndicator(l1Val / 100, l1Target / 100)
|
|
208
|
+
: '[N/A]';
|
|
209
|
+
lines.push('┌' + hr + '┐');
|
|
210
|
+
lines.push('│' + ' Layer Health'.padEnd(W) + '│');
|
|
211
|
+
lines.push('├' + hr + '┤');
|
|
212
|
+
lines.push('│' + ` L1 Coverage: ${l1Val != null ? l1Val.toFixed(1) + '%' : 'N/A'}`.padEnd(W - 8) + l1Health.padStart(8) + '│');
|
|
213
|
+
|
|
214
|
+
// Gate A
|
|
215
|
+
const gaScore = result.gate_a?.score;
|
|
216
|
+
const gaTarget = result.gate_a?.target ?? 0.8;
|
|
217
|
+
const gaHealth = gaScore != null ? healthIndicator(gaScore, gaTarget) : '[N/A]';
|
|
218
|
+
const gaDetail = result.gate_a
|
|
219
|
+
? `${result.gate_a.explained}/${result.gate_a.total} traces explained`
|
|
220
|
+
: '';
|
|
221
|
+
lines.push('│' + ` Gate A: ${pct(gaScore)}`.padEnd(W - 8) + gaHealth.padStart(8) + '│');
|
|
222
|
+
if (gaDetail) {
|
|
223
|
+
lines.push('│' + ` ${gaDetail}`.padEnd(W) + '│');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Gate B
|
|
227
|
+
const gbScore = result.gate_b?.score;
|
|
228
|
+
const gbTarget = result.gate_b?.target ?? 1.0;
|
|
229
|
+
const gbHealth = gbScore != null ? healthIndicator(gbScore, gbTarget) : '[N/A]';
|
|
230
|
+
const gbDetail = result.gate_b
|
|
231
|
+
? `${result.gate_b.grounded_entries}/${result.gate_b.total_entries} entries grounded`
|
|
232
|
+
: '';
|
|
233
|
+
lines.push('│' + ` Gate B: ${pct(gbScore)}`.padEnd(W - 8) + gbHealth.padStart(8) + '│');
|
|
234
|
+
if (gbDetail) {
|
|
235
|
+
lines.push('│' + ` ${gbDetail}`.padEnd(W) + '│');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Gate C
|
|
239
|
+
const gcScore = result.gate_c?.score;
|
|
240
|
+
const gcTarget = result.gate_c?.target ?? 0.8;
|
|
241
|
+
const gcHealth = gcScore != null ? healthIndicator(gcScore, gcTarget) : '[N/A]';
|
|
242
|
+
const gcDetail = result.gate_c
|
|
243
|
+
? `${result.gate_c.validated_entries}/${result.gate_c.total_entries} entries validated`
|
|
244
|
+
: '';
|
|
245
|
+
lines.push('│' + ` Gate C: ${pct(gcScore)}`.padEnd(W - 8) + gcHealth.padStart(8) + '│');
|
|
246
|
+
if (gcDetail) {
|
|
247
|
+
lines.push('│' + ` ${gcDetail}`.padEnd(W) + '│');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
lines.push('└' + hr + '┘');
|
|
251
|
+
|
|
252
|
+
// Maturity distribution
|
|
253
|
+
if (result.maturity) {
|
|
254
|
+
lines.push('');
|
|
255
|
+
lines.push('┌' + hr + '┐');
|
|
256
|
+
lines.push('│' + ' Gate Maturity Distribution'.padEnd(W) + '│');
|
|
257
|
+
lines.push('├' + hr + '┤');
|
|
258
|
+
const by = result.maturity.by_level || {};
|
|
259
|
+
lines.push('│' + ` ADVISORY: ${by.ADVISORY ?? 0}`.padEnd(W) + '│');
|
|
260
|
+
lines.push('│' + ` SOFT_GATE: ${by.SOFT_GATE ?? 0}`.padEnd(W) + '│');
|
|
261
|
+
lines.push('│' + ` HARD_GATE: ${by.HARD_GATE ?? 0}`.padEnd(W) + '│');
|
|
262
|
+
lines.push('│' + ` Total: ${result.maturity.total ?? 0}`.padEnd(W) + '│');
|
|
263
|
+
lines.push('└' + hr + '┘');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Composite summary
|
|
267
|
+
lines.push('');
|
|
268
|
+
const status = result.all_targets_met ? 'ALL TARGETS MET' : 'TARGETS NOT MET';
|
|
269
|
+
const statusIndicator = result.all_targets_met ? '[PASS]' : '[FAIL]';
|
|
270
|
+
lines.push(` Composite: ${status} ${statusIndicator}`);
|
|
271
|
+
lines.push('');
|
|
272
|
+
|
|
273
|
+
return lines.join('\n');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ── Main ────────────────────────────────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
function main() {
|
|
279
|
+
const data = collectAll();
|
|
280
|
+
const result = buildResult(data);
|
|
281
|
+
|
|
282
|
+
if (JSON_FLAG) {
|
|
283
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
284
|
+
} else {
|
|
285
|
+
process.stdout.write(renderTerminal(result));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Exit 0 if all gates met, 1 otherwise
|
|
289
|
+
process.exit(result.all_targets_met ? 0 : 1);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Export for testing
|
|
293
|
+
module.exports = { healthIndicator, buildResult, renderTerminal };
|
|
294
|
+
|
|
295
|
+
if (require.main === module) {
|
|
296
|
+
main();
|
|
297
|
+
}
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// bin/design-impact.cjs
|
|
4
|
+
// Three-layer git diff impact analysis — traces changes through L1 (instrumentation),
|
|
5
|
+
// L2 (state transitions), and L3 (hazards) of the formal verification architecture.
|
|
6
|
+
//
|
|
7
|
+
// Usage:
|
|
8
|
+
// node bin/design-impact.cjs # analyze HEAD~1..HEAD
|
|
9
|
+
// node bin/design-impact.cjs --diff=HEAD~3..HEAD # specific commit range
|
|
10
|
+
// node bin/design-impact.cjs --stdin # read diff from stdin
|
|
11
|
+
// node bin/design-impact.cjs --json # JSON output
|
|
12
|
+
// node bin/design-impact.cjs --project-root=/path # cross-repo usage
|
|
13
|
+
//
|
|
14
|
+
// Exit codes:
|
|
15
|
+
// 0 — success
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const { spawnSync } = require('child_process');
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Resolve project root
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
const ROOT = (() => {
|
|
25
|
+
const args = process.argv.slice(2);
|
|
26
|
+
const rootArg = args.find(a => a.startsWith('--project-root='));
|
|
27
|
+
if (rootArg) return rootArg.split('=').slice(1).join('=');
|
|
28
|
+
if (process.env.PROJECT_ROOT) return process.env.PROJECT_ROOT;
|
|
29
|
+
return path.resolve(__dirname, '..');
|
|
30
|
+
})();
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// CLI flags
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
const ARGS = process.argv.slice(2);
|
|
36
|
+
const JSON_OUTPUT = ARGS.includes('--json');
|
|
37
|
+
const STDIN_MODE = ARGS.includes('--stdin');
|
|
38
|
+
const DIFF_ARG = ARGS.find(a => a.startsWith('--diff='));
|
|
39
|
+
const DIFF_REF = DIFF_ARG ? DIFF_ARG.split('=').slice(1).join('=') : 'HEAD~1..HEAD';
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Diff parsing
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Parse a unified diff to extract changed file names.
|
|
47
|
+
* Only looks at --- / +++ lines and 'diff --git' headers.
|
|
48
|
+
* Returns deduplicated array of file paths (relative to repo root).
|
|
49
|
+
*/
|
|
50
|
+
function parseGitDiff(diffText) {
|
|
51
|
+
const files = new Set();
|
|
52
|
+
const lines = diffText.split('\n');
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
// Handle 'diff --git a/foo b/foo' header
|
|
55
|
+
const gitMatch = line.match(/^diff --git a\/(.+) b\/(.+)$/);
|
|
56
|
+
if (gitMatch) {
|
|
57
|
+
files.add(gitMatch[2]);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
// Skip binary file markers
|
|
61
|
+
if (line.startsWith('Binary files ')) continue;
|
|
62
|
+
// Parse +++ b/path (new file path)
|
|
63
|
+
if (line.startsWith('+++ b/')) {
|
|
64
|
+
files.add(line.slice(6));
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return [...files];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Parse diff hunk headers to extract changed line ranges per file.
|
|
73
|
+
* Returns Map<filePath, Array<{start, count}>> for the new-side (+) ranges.
|
|
74
|
+
*/
|
|
75
|
+
function parseDiffLineRanges(diffText) {
|
|
76
|
+
const ranges = new Map();
|
|
77
|
+
let currentFile = null;
|
|
78
|
+
const lines = diffText.split('\n');
|
|
79
|
+
for (const line of lines) {
|
|
80
|
+
// Track current file from +++ header
|
|
81
|
+
if (line.startsWith('+++ b/')) {
|
|
82
|
+
currentFile = line.slice(6);
|
|
83
|
+
if (!ranges.has(currentFile)) ranges.set(currentFile, []);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (line.startsWith('+++ ')) {
|
|
87
|
+
// /dev/null or similar — skip
|
|
88
|
+
currentFile = null;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
// Parse @@ hunk header
|
|
92
|
+
if (currentFile && line.startsWith('@@')) {
|
|
93
|
+
const match = line.match(/@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
|
|
94
|
+
if (match) {
|
|
95
|
+
const start = parseInt(match[1], 10);
|
|
96
|
+
const count = match[2] !== undefined ? parseInt(match[2], 10) : 1;
|
|
97
|
+
ranges.get(currentFile).push({ start, count });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return ranges;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Check if a line number falls within any of the changed ranges for a file.
|
|
106
|
+
*/
|
|
107
|
+
function isLineInRanges(lineNum, ranges) {
|
|
108
|
+
if (!ranges || ranges.length === 0) return false;
|
|
109
|
+
for (const r of ranges) {
|
|
110
|
+
if (lineNum >= r.start && lineNum < r.start + r.count) return true;
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Layer analysis
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* L1: Instrumentation impact — find emission points in changed files.
|
|
121
|
+
*/
|
|
122
|
+
function analyzeL1(changedFiles, lineRanges, instrumentationMap) {
|
|
123
|
+
const points = instrumentationMap.emission_points || [];
|
|
124
|
+
const affected = [];
|
|
125
|
+
for (const ep of points) {
|
|
126
|
+
if (!changedFiles.includes(ep.file)) continue;
|
|
127
|
+
const fileRanges = lineRanges.get(ep.file);
|
|
128
|
+
const impactType = isLineInRanges(ep.line_number, fileRanges) ? 'direct' : 'file_level';
|
|
129
|
+
affected.push({
|
|
130
|
+
file: ep.file,
|
|
131
|
+
line_number: ep.line_number,
|
|
132
|
+
action: ep.action,
|
|
133
|
+
xstate_event: ep.xstate_event,
|
|
134
|
+
impact_type: impactType,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
affected_emission_points: affected.length,
|
|
139
|
+
direct_hits: affected.filter(a => a.impact_type === 'direct').length,
|
|
140
|
+
details: affected,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* L2: State transition impact — find transitions for affected xstate events.
|
|
146
|
+
*/
|
|
147
|
+
function analyzeL2(l1Impact, observedFsm) {
|
|
148
|
+
const affectedEvents = new Set();
|
|
149
|
+
for (const ep of l1Impact.details) {
|
|
150
|
+
if (ep.xstate_event) affectedEvents.add(ep.xstate_event);
|
|
151
|
+
}
|
|
152
|
+
const transitions = [];
|
|
153
|
+
const observed = observedFsm.observed_transitions || {};
|
|
154
|
+
for (const [state, events] of Object.entries(observed)) {
|
|
155
|
+
for (const [event, info] of Object.entries(events)) {
|
|
156
|
+
if (affectedEvents.has(event)) {
|
|
157
|
+
transitions.push({
|
|
158
|
+
state,
|
|
159
|
+
event,
|
|
160
|
+
to_state: info.to_state,
|
|
161
|
+
count: info.count,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
affected_transitions: transitions.length,
|
|
168
|
+
affected_events: [...affectedEvents],
|
|
169
|
+
details: transitions,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* L3: Hazard impact — find hazards and failure modes for affected state-event pairs.
|
|
175
|
+
*/
|
|
176
|
+
function analyzeL3(l2Impact, hazardModel, failureModeCatalog) {
|
|
177
|
+
// Build set of affected (state, event) pairs from L2
|
|
178
|
+
const affectedPairs = new Set();
|
|
179
|
+
for (const t of l2Impact.details) {
|
|
180
|
+
affectedPairs.add(`${t.state}|${t.event}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const hazards = (hazardModel.hazards || []).filter(h =>
|
|
184
|
+
affectedPairs.has(`${h.state}|${h.event}`)
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const failureModes = (failureModeCatalog.failure_modes || []).filter(fm =>
|
|
188
|
+
affectedPairs.has(`${fm.state}|${fm.event}`)
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const maxRpn = hazards.length > 0 ? Math.max(...hazards.map(h => h.rpn)) : 0;
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
affected_hazards: hazards.length,
|
|
195
|
+
max_rpn: maxRpn,
|
|
196
|
+
affected_failure_modes: failureModes.length,
|
|
197
|
+
details: hazards.map(h => ({
|
|
198
|
+
hazard_id: h.id,
|
|
199
|
+
state: h.state,
|
|
200
|
+
event: h.event,
|
|
201
|
+
rpn: h.rpn,
|
|
202
|
+
severity: h.severity,
|
|
203
|
+
})),
|
|
204
|
+
failure_mode_details: failureModes.map(fm => ({
|
|
205
|
+
id: fm.id,
|
|
206
|
+
state: fm.state,
|
|
207
|
+
event: fm.event,
|
|
208
|
+
failure_mode: fm.failure_mode,
|
|
209
|
+
severity_class: fm.severity_class,
|
|
210
|
+
})),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// Main analysis entry point
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Run complete three-layer impact analysis.
|
|
220
|
+
* @param {Object} opts
|
|
221
|
+
* @param {string[]} opts.changedFiles - list of changed file paths
|
|
222
|
+
* @param {Map<string, Array<{start:number, count:number}>>} opts.lineRanges - per-file line ranges
|
|
223
|
+
* @param {Object} opts.instrumentationMap - parsed instrumentation-map.json
|
|
224
|
+
* @param {Object} opts.observedFsm - parsed observed-fsm.json
|
|
225
|
+
* @param {Object} opts.hazardModel - parsed hazard-model.json
|
|
226
|
+
* @param {Object} opts.failureModeCatalog - parsed failure-mode-catalog.json
|
|
227
|
+
*/
|
|
228
|
+
function analyzeImpact(opts) {
|
|
229
|
+
const { changedFiles, lineRanges, instrumentationMap, observedFsm, hazardModel, failureModeCatalog } = opts;
|
|
230
|
+
|
|
231
|
+
const l1 = analyzeL1(changedFiles, lineRanges, instrumentationMap);
|
|
232
|
+
const l2 = l1.affected_emission_points > 0
|
|
233
|
+
? analyzeL2(l1, observedFsm)
|
|
234
|
+
: { affected_transitions: 0, affected_events: [], details: [] };
|
|
235
|
+
const l3 = l2.affected_transitions > 0
|
|
236
|
+
? analyzeL3(l2, hazardModel, failureModeCatalog)
|
|
237
|
+
: { affected_hazards: 0, max_rpn: 0, affected_failure_modes: 0, details: [], failure_mode_details: [] };
|
|
238
|
+
|
|
239
|
+
let summary;
|
|
240
|
+
if (l1.affected_emission_points === 0) {
|
|
241
|
+
summary = 'No instrumented files affected by this change';
|
|
242
|
+
} else {
|
|
243
|
+
summary = `${l1.affected_emission_points} emission point(s) affected (${l1.direct_hits} direct), ` +
|
|
244
|
+
`${l2.affected_transitions} state transition(s), ` +
|
|
245
|
+
`${l3.affected_hazards} hazard(s) (max RPN: ${l3.max_rpn})`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
schema_version: '1',
|
|
250
|
+
generated: new Date().toISOString(),
|
|
251
|
+
changed_files: changedFiles.length,
|
|
252
|
+
l1_impact: l1,
|
|
253
|
+
l2_impact: l2,
|
|
254
|
+
l3_impact: l3,
|
|
255
|
+
summary,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
// File loaders
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
function loadJSON(relPath) {
|
|
264
|
+
const fullPath = path.join(ROOT, relPath);
|
|
265
|
+
try {
|
|
266
|
+
return JSON.parse(fs.readFileSync(fullPath, 'utf8'));
|
|
267
|
+
} catch (e) {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
// Human-readable output
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
|
|
276
|
+
function printReport(report) {
|
|
277
|
+
console.log('=== Design Impact Report ===\n');
|
|
278
|
+
console.log(`Changed files: ${report.changed_files}`);
|
|
279
|
+
console.log(`Summary: ${report.summary}\n`);
|
|
280
|
+
|
|
281
|
+
if (report.l1_impact.affected_emission_points === 0) {
|
|
282
|
+
console.log('No instrumented files affected — L2/L3 analysis skipped.\n');
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// L1
|
|
287
|
+
console.log('--- L1: Instrumentation Impact ---');
|
|
288
|
+
console.log(` Affected emission points: ${report.l1_impact.affected_emission_points} (${report.l1_impact.direct_hits} direct)`);
|
|
289
|
+
for (const ep of report.l1_impact.details) {
|
|
290
|
+
console.log(` [${ep.impact_type}] ${ep.file}:${ep.line_number} — ${ep.action} (${ep.xstate_event || 'no event'})`);
|
|
291
|
+
}
|
|
292
|
+
console.log('');
|
|
293
|
+
|
|
294
|
+
// L2
|
|
295
|
+
console.log('--- L2: State Transition Impact ---');
|
|
296
|
+
console.log(` Affected transitions: ${report.l2_impact.affected_transitions}`);
|
|
297
|
+
console.log(` Affected events: ${report.l2_impact.affected_events.join(', ')}`);
|
|
298
|
+
for (const t of report.l2_impact.details) {
|
|
299
|
+
console.log(` ${t.state} --[${t.event}]--> ${t.to_state} (${t.count} observed)`);
|
|
300
|
+
}
|
|
301
|
+
console.log('');
|
|
302
|
+
|
|
303
|
+
// L3
|
|
304
|
+
console.log('--- L3: Hazard Impact ---');
|
|
305
|
+
console.log(` Affected hazards: ${report.l3_impact.affected_hazards} (max RPN: ${report.l3_impact.max_rpn})`);
|
|
306
|
+
console.log(` Affected failure modes: ${report.l3_impact.affected_failure_modes}`);
|
|
307
|
+
for (const h of report.l3_impact.details) {
|
|
308
|
+
console.log(` ${h.hazard_id}: ${h.state} --[${h.event}]--> RPN=${h.rpn} severity=${h.severity}`);
|
|
309
|
+
}
|
|
310
|
+
console.log('');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
// CLI main
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
async function main() {
|
|
318
|
+
let diffText;
|
|
319
|
+
|
|
320
|
+
if (STDIN_MODE) {
|
|
321
|
+
// Read diff from stdin
|
|
322
|
+
diffText = fs.readFileSync(0, 'utf8');
|
|
323
|
+
} else {
|
|
324
|
+
// Run git diff
|
|
325
|
+
const [ref1, ref2] = DIFF_REF.includes('..')
|
|
326
|
+
? DIFF_REF.split('..')
|
|
327
|
+
: [DIFF_REF + '~1', DIFF_REF];
|
|
328
|
+
|
|
329
|
+
const diffResult = spawnSync('git', ['diff', '--unified=0', ref1, ref2], {
|
|
330
|
+
cwd: ROOT,
|
|
331
|
+
encoding: 'utf8',
|
|
332
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
if (diffResult.error) {
|
|
336
|
+
console.error(`Error running git diff: ${diffResult.error.message}`);
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
diffText = diffResult.stdout || '';
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const changedFiles = parseGitDiff(diffText);
|
|
344
|
+
const lineRanges = parseDiffLineRanges(diffText);
|
|
345
|
+
|
|
346
|
+
// Load formal verification artifacts
|
|
347
|
+
const instrumentationMap = loadJSON('.planning/formal/evidence/instrumentation-map.json') || { emission_points: [] };
|
|
348
|
+
const observedFsm = loadJSON('.planning/formal/semantics/observed-fsm.json') || { observed_transitions: {} };
|
|
349
|
+
const hazardModel = loadJSON('.planning/formal/reasoning/hazard-model.json') || { hazards: [] };
|
|
350
|
+
const failureModeCatalog = loadJSON('.planning/formal/reasoning/failure-mode-catalog.json') || { failure_modes: [] };
|
|
351
|
+
|
|
352
|
+
const report = analyzeImpact({
|
|
353
|
+
changedFiles,
|
|
354
|
+
lineRanges,
|
|
355
|
+
instrumentationMap,
|
|
356
|
+
observedFsm,
|
|
357
|
+
hazardModel,
|
|
358
|
+
failureModeCatalog,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
if (JSON_OUTPUT) {
|
|
362
|
+
console.log(JSON.stringify(report, null, 2));
|
|
363
|
+
} else {
|
|
364
|
+
printReport(report);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Export for testing
|
|
369
|
+
module.exports = { analyzeImpact, parseGitDiff, parseDiffLineRanges, analyzeL1, analyzeL2, analyzeL3, isLineInRanges };
|
|
370
|
+
|
|
371
|
+
// Run CLI if invoked directly
|
|
372
|
+
if (require.main === module) {
|
|
373
|
+
main().catch(err => {
|
|
374
|
+
console.error(err);
|
|
375
|
+
process.exit(1);
|
|
376
|
+
});
|
|
377
|
+
}
|