@nforma.ai/nforma 0.2.1 → 0.29.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-preflight.cjs +89 -0
- 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 +36 -86
- 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/{qgsd-precompact.js → nf-precompact.js} +13 -13
- package/hooks/dist/{qgsd-prompt.js → nf-prompt.js} +110 -33
- package/hooks/dist/nf-session-start.js +185 -0
- package/hooks/dist/{qgsd-slot-correlator.js → nf-slot-correlator.js} +13 -5
- package/hooks/dist/{qgsd-spec-regen.js → nf-spec-regen.js} +17 -8
- package/hooks/dist/{qgsd-statusline.js → nf-statusline.js} +12 -3
- package/hooks/dist/{qgsd-stop.js → nf-stop.js} +152 -18
- package/hooks/dist/{qgsd-token-collector.js → nf-token-collector.js} +12 -4
- package/hooks/dist/unified-mcp-server.mjs +2 -2
- package/package.json +6 -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,326 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* bin/harness-diagnostic.cjs — Unified harness diagnostic tool for nForma.
|
|
6
|
+
*
|
|
7
|
+
* Cross-references scoreboard, conformance events, token usage, stall detector,
|
|
8
|
+
* and circuit breaker state into a single structured health report with
|
|
9
|
+
* actionable recommendations.
|
|
10
|
+
*
|
|
11
|
+
* Exports: generateReport, formatTerminalReport
|
|
12
|
+
* CLI: node bin/harness-diagnostic.cjs [--json] [--cwd /path]
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
// ─── Module resolution helpers ──────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
function tryRequire(modulePath) {
|
|
21
|
+
try { return require(modulePath); } catch (_) { return null; }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function findModule(name) {
|
|
25
|
+
return tryRequire(path.join(__dirname, name)) ||
|
|
26
|
+
tryRequire(path.join(__dirname, '..', 'bin', name));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─── generateReport ─────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Generate a structured harness diagnostic report.
|
|
33
|
+
* @param {string} cwd - Project root directory.
|
|
34
|
+
* @param {Object} [options]
|
|
35
|
+
* @param {Object} [options.config] - Pre-loaded config (optional).
|
|
36
|
+
* @returns {Object} Structured report object.
|
|
37
|
+
*/
|
|
38
|
+
function generateReport(cwd, options = {}) {
|
|
39
|
+
const report = {
|
|
40
|
+
timestamp: new Date().toISOString(),
|
|
41
|
+
slot_availability: [],
|
|
42
|
+
pass_at_k: { total: 0, pass_at_1: 0, pass_at_3: 0, avg_k: 0 },
|
|
43
|
+
token_spend: {
|
|
44
|
+
total_records: 0,
|
|
45
|
+
total_input: 0,
|
|
46
|
+
total_output: 0,
|
|
47
|
+
by_slot: {},
|
|
48
|
+
note: 'Token values are currently unreliable (all zeros in Claude Code subagent transcripts)',
|
|
49
|
+
},
|
|
50
|
+
stall_events: [],
|
|
51
|
+
circuit_breaker: {
|
|
52
|
+
active: false,
|
|
53
|
+
disabled: false,
|
|
54
|
+
last_triggered: null,
|
|
55
|
+
},
|
|
56
|
+
recommendations: [],
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const planningPaths = findModule('planning-paths.cjs');
|
|
60
|
+
|
|
61
|
+
// ── Section 1: Slot Availability from scoreboard ──────────────────────────
|
|
62
|
+
try {
|
|
63
|
+
let scoreboardPath;
|
|
64
|
+
if (planningPaths) {
|
|
65
|
+
scoreboardPath = planningPaths.resolveWithFallback(cwd, 'quorum-scoreboard');
|
|
66
|
+
} else {
|
|
67
|
+
scoreboardPath = path.join(cwd, '.planning', 'quorum', 'scoreboard.json');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (fs.existsSync(scoreboardPath)) {
|
|
71
|
+
const scoreboard = JSON.parse(fs.readFileSync(scoreboardPath, 'utf8'));
|
|
72
|
+
const slots = scoreboard.slots || {};
|
|
73
|
+
const slotAgg = {};
|
|
74
|
+
|
|
75
|
+
for (const [key, data] of Object.entries(slots)) {
|
|
76
|
+
const slotName = key.split(':')[0];
|
|
77
|
+
if (!slotAgg[slotName]) slotAgg[slotName] = { tp: 0, fn: 0 };
|
|
78
|
+
slotAgg[slotName].tp += (data.tp || 0);
|
|
79
|
+
slotAgg[slotName].fn += (data.fn || 0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const [slotName, agg] of Object.entries(slotAgg)) {
|
|
83
|
+
const total = agg.tp + agg.fn;
|
|
84
|
+
const rate = total > 0 ? agg.tp / total : 0;
|
|
85
|
+
let status = 'unknown';
|
|
86
|
+
if (total > 0) {
|
|
87
|
+
if (rate >= 0.8) status = 'healthy';
|
|
88
|
+
else if (rate >= 0.5) status = 'degraded';
|
|
89
|
+
else status = 'critical';
|
|
90
|
+
}
|
|
91
|
+
report.slot_availability.push({
|
|
92
|
+
slot: slotName,
|
|
93
|
+
total_rounds: total,
|
|
94
|
+
successes: agg.tp,
|
|
95
|
+
failures: agg.fn,
|
|
96
|
+
success_rate: Math.round(rate * 1000) / 1000,
|
|
97
|
+
status,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} catch (_) {}
|
|
102
|
+
|
|
103
|
+
// ── Section 2: Pass@k from conformance events ────────────────────────────
|
|
104
|
+
try {
|
|
105
|
+
const vqh = findModule('verify-quorum-health.cjs');
|
|
106
|
+
if (vqh && vqh.computePassAtKRates) {
|
|
107
|
+
let conformancePath;
|
|
108
|
+
if (planningPaths) {
|
|
109
|
+
conformancePath = planningPaths.resolveWithFallback(cwd, 'conformance-events');
|
|
110
|
+
} else {
|
|
111
|
+
conformancePath = path.join(cwd, '.planning', 'telemetry', 'conformance-events.jsonl');
|
|
112
|
+
}
|
|
113
|
+
report.pass_at_k = vqh.computePassAtKRates(conformancePath);
|
|
114
|
+
}
|
|
115
|
+
} catch (_) {}
|
|
116
|
+
|
|
117
|
+
// ── Section 3: Token spend from token-usage.jsonl ─────────────────────────
|
|
118
|
+
try {
|
|
119
|
+
let tokenPath;
|
|
120
|
+
if (planningPaths) {
|
|
121
|
+
tokenPath = planningPaths.resolveWithFallback(cwd, 'token-usage');
|
|
122
|
+
} else {
|
|
123
|
+
tokenPath = path.join(cwd, '.planning', 'telemetry', 'token-usage.jsonl');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (fs.existsSync(tokenPath)) {
|
|
127
|
+
const lines = fs.readFileSync(tokenPath, 'utf8').split('\n').filter(l => l.trim());
|
|
128
|
+
report.token_spend.total_records = lines.length;
|
|
129
|
+
const bySlot = {};
|
|
130
|
+
for (const line of lines) {
|
|
131
|
+
try {
|
|
132
|
+
const r = JSON.parse(line);
|
|
133
|
+
const slot = r.slot || 'unknown';
|
|
134
|
+
if (!bySlot[slot]) bySlot[slot] = { input: 0, output: 0, rounds: 0 };
|
|
135
|
+
bySlot[slot].input += (r.input_tokens || 0);
|
|
136
|
+
bySlot[slot].output += (r.output_tokens || 0);
|
|
137
|
+
bySlot[slot].rounds++;
|
|
138
|
+
report.token_spend.total_input += (r.input_tokens || 0);
|
|
139
|
+
report.token_spend.total_output += (r.output_tokens || 0);
|
|
140
|
+
} catch (_) {}
|
|
141
|
+
}
|
|
142
|
+
report.token_spend.by_slot = bySlot;
|
|
143
|
+
}
|
|
144
|
+
} catch (_) {}
|
|
145
|
+
|
|
146
|
+
// ── Section 4: Stall events from stall-detector ──────────────────────────
|
|
147
|
+
try {
|
|
148
|
+
const stallDetector = findModule('stall-detector.cjs');
|
|
149
|
+
if (stallDetector && stallDetector.detectStalledSlots) {
|
|
150
|
+
let config = options.config;
|
|
151
|
+
if (!config) {
|
|
152
|
+
try {
|
|
153
|
+
const configLoader = tryRequire(path.join(__dirname, '..', 'hooks', 'config-loader'));
|
|
154
|
+
if (configLoader) config = configLoader.loadConfig(cwd);
|
|
155
|
+
} catch (_) {}
|
|
156
|
+
}
|
|
157
|
+
report.stall_events = stallDetector.detectStalledSlots(cwd, config || {}) || [];
|
|
158
|
+
}
|
|
159
|
+
} catch (_) {}
|
|
160
|
+
|
|
161
|
+
// ── Section 5: Circuit breaker status ─────────────────────────────────────
|
|
162
|
+
try {
|
|
163
|
+
const cbPath = path.join(cwd, '.claude', 'circuit-breaker-state.json');
|
|
164
|
+
if (fs.existsSync(cbPath)) {
|
|
165
|
+
const cb = JSON.parse(fs.readFileSync(cbPath, 'utf8'));
|
|
166
|
+
report.circuit_breaker.active = !!cb.active;
|
|
167
|
+
report.circuit_breaker.disabled = !!cb.disabled;
|
|
168
|
+
report.circuit_breaker.last_triggered = cb.last_triggered || cb.lastTriggered || null;
|
|
169
|
+
}
|
|
170
|
+
} catch (_) {}
|
|
171
|
+
|
|
172
|
+
// ── Section 6: Recommendations ────────────────────────────────────────────
|
|
173
|
+
const hasData = report.slot_availability.length > 0 ||
|
|
174
|
+
report.pass_at_k.total > 0 ||
|
|
175
|
+
report.token_spend.total_records > 0;
|
|
176
|
+
|
|
177
|
+
if (!hasData) {
|
|
178
|
+
report.recommendations.push('No quorum data available yet -- run quorum rounds to populate diagnostic data.');
|
|
179
|
+
} else {
|
|
180
|
+
// Slot health recommendations
|
|
181
|
+
for (const slot of report.slot_availability) {
|
|
182
|
+
if (slot.success_rate < 0.5 && slot.total_rounds > 0) {
|
|
183
|
+
const pct = Math.round(slot.success_rate * 100);
|
|
184
|
+
report.recommendations.push(
|
|
185
|
+
`Slot ${slot.slot} has ${pct}% success rate -- consider removing from quorum_active or checking provider health`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Stall recommendations
|
|
191
|
+
for (const stall of report.stall_events) {
|
|
192
|
+
if ((stall.consecutiveTimeouts || 0) >= 2) {
|
|
193
|
+
report.recommendations.push(
|
|
194
|
+
`Slot ${stall.slot} has ${stall.consecutiveTimeouts} consecutive timeouts -- run \`node bin/check-mcp-health.cjs\``
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Pass@k recommendations
|
|
200
|
+
if (report.pass_at_k.total > 0 && report.pass_at_k.pass_at_1 < 0.7) {
|
|
201
|
+
const pct = Math.round(report.pass_at_k.pass_at_1 * 100);
|
|
202
|
+
report.recommendations.push(
|
|
203
|
+
`pass@1 rate at ${pct}% -- quorum requires multiple rounds frequently. Consider adjusting maxDeliberation.`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Circuit breaker recommendations
|
|
208
|
+
if (report.circuit_breaker.active) {
|
|
209
|
+
report.recommendations.push(
|
|
210
|
+
'Circuit breaker is ACTIVE -- oscillation detected. Follow resolution procedure.'
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (report.recommendations.length === 0) {
|
|
215
|
+
report.recommendations.push('No issues detected. Harness is healthy.');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return report;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ─── formatTerminalReport ───────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Format report object into a terminal-friendly string.
|
|
226
|
+
* @param {Object} report - Report from generateReport().
|
|
227
|
+
* @returns {string}
|
|
228
|
+
*/
|
|
229
|
+
function formatTerminalReport(report) {
|
|
230
|
+
const lines = [];
|
|
231
|
+
|
|
232
|
+
lines.push('nForma Harness Diagnostic Report');
|
|
233
|
+
lines.push('=================================');
|
|
234
|
+
lines.push(`Generated: ${report.timestamp}`);
|
|
235
|
+
lines.push('');
|
|
236
|
+
|
|
237
|
+
// Slot Availability
|
|
238
|
+
lines.push('## Slot Availability');
|
|
239
|
+
if (report.slot_availability.length === 0) {
|
|
240
|
+
lines.push(' No slot data available.');
|
|
241
|
+
} else {
|
|
242
|
+
lines.push(' slot rounds success rate status');
|
|
243
|
+
lines.push(' -----------------------------------------------');
|
|
244
|
+
for (const s of report.slot_availability) {
|
|
245
|
+
const rate = (s.success_rate * 100).toFixed(1) + '%';
|
|
246
|
+
lines.push(
|
|
247
|
+
' ' +
|
|
248
|
+
s.slot.padEnd(15) +
|
|
249
|
+
String(s.total_rounds).padStart(6) +
|
|
250
|
+
String(s.successes).padStart(10) +
|
|
251
|
+
rate.padStart(9) +
|
|
252
|
+
' ' + s.status
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
lines.push('');
|
|
257
|
+
|
|
258
|
+
// Pass@k
|
|
259
|
+
lines.push('## Pass@k Consensus Efficiency');
|
|
260
|
+
if (report.pass_at_k.total === 0) {
|
|
261
|
+
lines.push(' No pass@k data available.');
|
|
262
|
+
} else {
|
|
263
|
+
lines.push(` pass@1: ${(report.pass_at_k.pass_at_1 * 100).toFixed(1)}%`);
|
|
264
|
+
lines.push(` pass@3: ${(report.pass_at_k.pass_at_3 * 100).toFixed(1)}%`);
|
|
265
|
+
lines.push(` avg rounds: ${report.pass_at_k.avg_k.toFixed(1)}`);
|
|
266
|
+
lines.push(` total events: ${report.pass_at_k.total}`);
|
|
267
|
+
}
|
|
268
|
+
lines.push('');
|
|
269
|
+
|
|
270
|
+
// Token Spend
|
|
271
|
+
lines.push('## Token Spend');
|
|
272
|
+
lines.push(' Note: Token values unreliable (all zeros)');
|
|
273
|
+
lines.push(` total records: ${report.token_spend.total_records}`);
|
|
274
|
+
lines.push('');
|
|
275
|
+
|
|
276
|
+
// Stall Events
|
|
277
|
+
lines.push('## Stall Events');
|
|
278
|
+
if (!report.stall_events || report.stall_events.length === 0) {
|
|
279
|
+
lines.push(' No stalled slots detected.');
|
|
280
|
+
} else {
|
|
281
|
+
for (const s of report.stall_events) {
|
|
282
|
+
lines.push(` ${s.slot}: ${s.consecutiveTimeouts} consecutive timeouts (last: ${s.lastSeen || 'unknown'})`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
lines.push('');
|
|
286
|
+
|
|
287
|
+
// Circuit Breaker
|
|
288
|
+
lines.push('## Circuit Breaker');
|
|
289
|
+
if (report.circuit_breaker.active) {
|
|
290
|
+
lines.push(' Status: ACTIVE (oscillation detected)');
|
|
291
|
+
} else if (report.circuit_breaker.disabled) {
|
|
292
|
+
lines.push(' Status: disabled');
|
|
293
|
+
} else {
|
|
294
|
+
lines.push(' Status: inactive');
|
|
295
|
+
}
|
|
296
|
+
lines.push('');
|
|
297
|
+
|
|
298
|
+
// Recommendations
|
|
299
|
+
lines.push('## Recommendations');
|
|
300
|
+
for (const r of report.recommendations) {
|
|
301
|
+
lines.push(` - ${r}`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return lines.join('\n');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ─── CLI ─────────────────────────────────────────────────────────────────────
|
|
308
|
+
|
|
309
|
+
if (require.main === module) {
|
|
310
|
+
const args = process.argv.slice(2);
|
|
311
|
+
const jsonFlag = args.includes('--json');
|
|
312
|
+
const cwdIdx = args.indexOf('--cwd');
|
|
313
|
+
const cwd = cwdIdx >= 0 && args[cwdIdx + 1] ? args[cwdIdx + 1] : process.cwd();
|
|
314
|
+
|
|
315
|
+
const report = generateReport(cwd);
|
|
316
|
+
|
|
317
|
+
if (jsonFlag) {
|
|
318
|
+
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
319
|
+
} else {
|
|
320
|
+
process.stdout.write(formatTerminalReport(report) + '\n');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
process.exit(0);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
module.exports = { generateReport, formatTerminalReport };
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* hazard-model.cjs — FMEA hazard model for Layer 3 (Reasoning).
|
|
6
|
+
*
|
|
7
|
+
* Applies FMEA scoring (Severity x Occurrence x Detection = RPN) to every
|
|
8
|
+
* state-event pair in the L2 observed FSM. Outputs hazard-model.json with
|
|
9
|
+
* derived_from traceability links.
|
|
10
|
+
*
|
|
11
|
+
* Requirements: RSN-01
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* node bin/hazard-model.cjs # print summary to stdout
|
|
15
|
+
* node bin/hazard-model.cjs --json # print full results JSON to stdout
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
|
|
21
|
+
const ROOT = process.env.PROJECT_ROOT || path.join(__dirname, '..');
|
|
22
|
+
const FORMAL = path.join(ROOT, '.planning', 'formal');
|
|
23
|
+
const REASONING_DIR = path.join(FORMAL, 'reasoning');
|
|
24
|
+
const OUT_FILE = path.join(REASONING_DIR, 'hazard-model.json');
|
|
25
|
+
|
|
26
|
+
const JSON_FLAG = process.argv.includes('--json');
|
|
27
|
+
|
|
28
|
+
// ── Severity lookup table (domain judgment) ─────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Compute severity score (1-10) for a (fromState, event) pair.
|
|
32
|
+
* Based on domain impact if the transition fails.
|
|
33
|
+
*/
|
|
34
|
+
function computeSeverity(fromState, event) {
|
|
35
|
+
// DECIDED -> any self-loop: already terminal, harmless (check first — overrides event rules)
|
|
36
|
+
if (fromState === 'DECIDED') return 2;
|
|
37
|
+
|
|
38
|
+
// DELIBERATING -> DECIDE: wrong verdict = incorrect PASS/BLOCK
|
|
39
|
+
if (fromState === 'DELIBERATING' && event === 'DECIDE') return 8;
|
|
40
|
+
|
|
41
|
+
// COLLECTING_VOTES -> VOTES_COLLECTED: incomplete votes = stalled quorum
|
|
42
|
+
if (fromState === 'COLLECTING_VOTES' && event === 'VOTES_COLLECTED') return 6;
|
|
43
|
+
|
|
44
|
+
// Any -> CIRCUIT_BREAK: false positive break = workflow interruption
|
|
45
|
+
if (event === 'CIRCUIT_BREAK') return 6;
|
|
46
|
+
|
|
47
|
+
// IDLE -> QUORUM_START: start failure = degraded, can retry
|
|
48
|
+
if (fromState === 'IDLE' && event === 'QUORUM_START') return 4;
|
|
49
|
+
|
|
50
|
+
// Default: moderate
|
|
51
|
+
return 4;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Occurrence computation from trace data ──────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Compute occurrence score (1-10) based on transition frequency.
|
|
58
|
+
* ratio = transition.count / totalSessions
|
|
59
|
+
*/
|
|
60
|
+
function computeOccurrenceScore(transitionCount, totalSessions) {
|
|
61
|
+
if (totalSessions === 0) return 1;
|
|
62
|
+
const ratio = transitionCount / totalSessions;
|
|
63
|
+
if (ratio > 0.8) return 10;
|
|
64
|
+
if (ratio > 0.5) return 8;
|
|
65
|
+
if (ratio > 0.2) return 6;
|
|
66
|
+
if (ratio > 0.05) return 4;
|
|
67
|
+
if (ratio > 0) return 2;
|
|
68
|
+
return 1;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Detection computation from formalism + test coverage ────────────────────
|
|
72
|
+
|
|
73
|
+
// Mapping of states/events to related requirement prefixes for formalism lookup
|
|
74
|
+
const STATE_REQ_MAP = {
|
|
75
|
+
'IDLE': ['STOP', 'SPEC'],
|
|
76
|
+
'COLLECTING_VOTES': ['PLAN', 'LOOP'],
|
|
77
|
+
'DELIBERATING': ['PLAN', 'IMPR', 'LOOP'],
|
|
78
|
+
'DECIDED': ['ORES', 'DETECT'],
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const EVENT_REQ_MAP = {
|
|
82
|
+
'QUORUM_START': ['PLAN', 'CRED'],
|
|
83
|
+
'VOTES_COLLECTED': ['PLAN', 'LOOP'],
|
|
84
|
+
'DECIDE': ['PLAN', 'IMPR', 'SAFE'],
|
|
85
|
+
'CIRCUIT_BREAK': ['DETECT', 'ORES'],
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Compute detection score (1-10) based on formalism and test coverage.
|
|
90
|
+
* - Has formalism AND tests: 2
|
|
91
|
+
* - Has formalism OR tests: 4
|
|
92
|
+
* - Neither (only conformance events): 8
|
|
93
|
+
* - Nothing at all: 10
|
|
94
|
+
*/
|
|
95
|
+
function computeDetectionScore(fromState, event, failureTaxonomy, unitTestCoverage) {
|
|
96
|
+
// Check if any formal check covers related requirements
|
|
97
|
+
const relatedPrefixes = [
|
|
98
|
+
...(STATE_REQ_MAP[fromState] || []),
|
|
99
|
+
...(EVENT_REQ_MAP[event] || []),
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
const logicViolations = failureTaxonomy?.categories?.logic_violation || [];
|
|
103
|
+
const hasFormalism = logicViolations.some(f =>
|
|
104
|
+
(f.requirement_ids || []).some(rid =>
|
|
105
|
+
relatedPrefixes.some(pfx => rid.startsWith(pfx))
|
|
106
|
+
)
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// Check if there are tests covering related requirement IDs
|
|
110
|
+
const coveredReqs = unitTestCoverage?.requirements || {};
|
|
111
|
+
const hasTests = relatedPrefixes.some(pfx =>
|
|
112
|
+
Object.keys(coveredReqs).some(rid => rid.startsWith(pfx) && coveredReqs[rid]?.covered)
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (hasFormalism && hasTests) return 2;
|
|
116
|
+
if (hasFormalism || hasTests) return 4;
|
|
117
|
+
|
|
118
|
+
// Check if at least conformance events cover this transition
|
|
119
|
+
// (all transitions in observed-fsm.json have conformance traces by definition)
|
|
120
|
+
return 8;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── Main generation ─────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
function generateHazardModel(observedFsm, traceStats, failureTaxonomy, unitTestCoverage) {
|
|
126
|
+
const totalSessions = traceStats?.sessions?.length || 349;
|
|
127
|
+
const hazards = [];
|
|
128
|
+
|
|
129
|
+
for (const [fromState, events] of Object.entries(observedFsm.observed_transitions)) {
|
|
130
|
+
for (const [event, data] of Object.entries(events)) {
|
|
131
|
+
const severity = computeSeverity(fromState, event);
|
|
132
|
+
const occurrence = computeOccurrenceScore(data.count, totalSessions);
|
|
133
|
+
const detection = computeDetectionScore(fromState, event, failureTaxonomy, unitTestCoverage);
|
|
134
|
+
const rpn = severity * occurrence * detection;
|
|
135
|
+
|
|
136
|
+
// Build derived_from links
|
|
137
|
+
const derivedFrom = [
|
|
138
|
+
{
|
|
139
|
+
layer: 'L2',
|
|
140
|
+
artifact: 'semantics/observed-fsm.json',
|
|
141
|
+
ref: `observed_transitions.${fromState}.${event}`,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
layer: 'L1',
|
|
145
|
+
artifact: 'evidence/trace-corpus-stats.json',
|
|
146
|
+
ref: `sessions[*].actions`,
|
|
147
|
+
},
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
// Add invariant link if any invariant relates to this state
|
|
151
|
+
const invariantConfigs = {
|
|
152
|
+
'IDLE': 'MCQGSDQuorum',
|
|
153
|
+
'COLLECTING_VOTES': 'MCQGSDQuorum',
|
|
154
|
+
'DELIBERATING': 'MCdeliberation',
|
|
155
|
+
'DECIDED': 'MCQGSDQuorum',
|
|
156
|
+
};
|
|
157
|
+
const config = invariantConfigs[fromState];
|
|
158
|
+
if (config) {
|
|
159
|
+
derivedFrom.push({
|
|
160
|
+
layer: 'L2',
|
|
161
|
+
artifact: 'semantics/invariant-catalog.json',
|
|
162
|
+
ref: `invariants[config=${config}]`,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
hazards.push({
|
|
167
|
+
id: `HAZARD-${fromState}-${event}`,
|
|
168
|
+
state: fromState,
|
|
169
|
+
event,
|
|
170
|
+
to_state: data.to_state,
|
|
171
|
+
severity,
|
|
172
|
+
occurrence,
|
|
173
|
+
detection,
|
|
174
|
+
rpn,
|
|
175
|
+
derived_from: derivedFrom,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Sort by RPN descending
|
|
181
|
+
hazards.sort((a, b) => b.rpn - a.rpn);
|
|
182
|
+
|
|
183
|
+
const criticalCount = hazards.filter(h => h.rpn >= 200).length;
|
|
184
|
+
const highCount = hazards.filter(h => h.rpn >= 100 && h.rpn < 200).length;
|
|
185
|
+
const maxRpn = hazards.length > 0 ? hazards[0].rpn : 0;
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
schema_version: '1',
|
|
189
|
+
generated: new Date().toISOString(),
|
|
190
|
+
methodology: 'FMEA (IEC 60812)',
|
|
191
|
+
scoring_scale: {
|
|
192
|
+
severity: '1-10: 1=no impact, 2=cosmetic, 4=degraded, 6=stalled/interrupted, 8=incorrect verdict, 10=crash/data loss',
|
|
193
|
+
occurrence: '1-10: based on transition count / total sessions (349). >80%=10, >50%=8, >20%=6, >5%=4, >0%=2, 0=1',
|
|
194
|
+
detection: '1-10: 2=formalism+tests, 4=formalism OR tests, 8=conformance events only, 10=nothing',
|
|
195
|
+
rpn: 'Severity x Occurrence x Detection (range 1-1000)',
|
|
196
|
+
},
|
|
197
|
+
hazards,
|
|
198
|
+
summary: {
|
|
199
|
+
total: hazards.length,
|
|
200
|
+
max_rpn: maxRpn,
|
|
201
|
+
critical_count: criticalCount,
|
|
202
|
+
high_count: highCount,
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ── Entry point ─────────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
function main() {
|
|
210
|
+
// Load L2 observed FSM
|
|
211
|
+
const fsmPath = path.join(FORMAL, 'semantics', 'observed-fsm.json');
|
|
212
|
+
if (!fs.existsSync(fsmPath)) {
|
|
213
|
+
console.error('ERROR: observed-fsm.json not found at', fsmPath);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
const observedFsm = JSON.parse(fs.readFileSync(fsmPath, 'utf8'));
|
|
217
|
+
|
|
218
|
+
// Load L1 trace corpus stats
|
|
219
|
+
const tracePath = path.join(FORMAL, 'evidence', 'trace-corpus-stats.json');
|
|
220
|
+
let traceStats = { sessions: [] };
|
|
221
|
+
if (fs.existsSync(tracePath)) {
|
|
222
|
+
traceStats = JSON.parse(fs.readFileSync(tracePath, 'utf8'));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Load failure taxonomy
|
|
226
|
+
const taxPath = path.join(FORMAL, 'evidence', 'failure-taxonomy.json');
|
|
227
|
+
let failureTaxonomy = { categories: { logic_violation: [] } };
|
|
228
|
+
if (fs.existsSync(taxPath)) {
|
|
229
|
+
failureTaxonomy = JSON.parse(fs.readFileSync(taxPath, 'utf8'));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Load unit test coverage
|
|
233
|
+
const covPath = path.join(FORMAL, 'unit-test-coverage.json');
|
|
234
|
+
let unitTestCoverage = { requirements: {} };
|
|
235
|
+
if (fs.existsSync(covPath)) {
|
|
236
|
+
unitTestCoverage = JSON.parse(fs.readFileSync(covPath, 'utf8'));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const output = generateHazardModel(observedFsm, traceStats, failureTaxonomy, unitTestCoverage);
|
|
240
|
+
|
|
241
|
+
// Write output
|
|
242
|
+
fs.mkdirSync(REASONING_DIR, { recursive: true });
|
|
243
|
+
fs.writeFileSync(OUT_FILE, JSON.stringify(output, null, 2) + '\n');
|
|
244
|
+
|
|
245
|
+
if (JSON_FLAG) {
|
|
246
|
+
process.stdout.write(JSON.stringify(output));
|
|
247
|
+
} else {
|
|
248
|
+
console.log(`Hazard Model (FMEA)`);
|
|
249
|
+
console.log(` Total hazards: ${output.summary.total}`);
|
|
250
|
+
console.log(` Max RPN: ${output.summary.max_rpn}`);
|
|
251
|
+
console.log(` Critical (RPN>=200): ${output.summary.critical_count}`);
|
|
252
|
+
console.log(` High (100<=RPN<200): ${output.summary.high_count}`);
|
|
253
|
+
console.log(` Output: ${OUT_FILE}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
process.exit(0);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (require.main === module) main();
|
|
260
|
+
|
|
261
|
+
module.exports = { computeSeverity, computeOccurrenceScore, computeDetectionScore, generateHazardModel, main };
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
/**
|
|
5
5
|
* install-formal-tools.cjs
|
|
6
6
|
*
|
|
7
|
-
* Cross-platform installer for
|
|
7
|
+
* Cross-platform installer for nForma formal verification tools:
|
|
8
8
|
* TLA+ — downloads tla2tools.jar into .planning/formal/tla/
|
|
9
9
|
* Alloy — downloads org.alloytools.alloy.dist.jar into .planning/formal/alloy/
|
|
10
10
|
* PRISM — downloads and installs platform-specific binary
|