@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.
Files changed (193) hide show
  1. package/README.md +2 -2
  2. package/agents/{qgsd-codebase-mapper.md → nf-codebase-mapper.md} +1 -1
  3. package/agents/{qgsd-debugger.md → nf-debugger.md} +3 -3
  4. package/agents/{qgsd-executor.md → nf-executor.md} +14 -14
  5. package/agents/{qgsd-integration-checker.md → nf-integration-checker.md} +1 -1
  6. package/agents/{qgsd-phase-researcher.md → nf-phase-researcher.md} +6 -6
  7. package/agents/{qgsd-plan-checker.md → nf-plan-checker.md} +9 -9
  8. package/agents/{qgsd-planner.md → nf-planner.md} +9 -9
  9. package/agents/{qgsd-project-researcher.md → nf-project-researcher.md} +2 -2
  10. package/agents/{qgsd-quorum-orchestrator.md → nf-quorum-orchestrator.md} +33 -33
  11. package/agents/{qgsd-quorum-slot-worker.md → nf-quorum-slot-worker.md} +3 -3
  12. package/agents/{qgsd-quorum-synthesizer.md → nf-quorum-synthesizer.md} +3 -3
  13. package/agents/{qgsd-quorum-test-worker.md → nf-quorum-test-worker.md} +1 -1
  14. package/agents/{qgsd-quorum-worker.md → nf-quorum-worker.md} +6 -6
  15. package/agents/{qgsd-research-synthesizer.md → nf-research-synthesizer.md} +5 -5
  16. package/agents/{qgsd-roadmapper.md → nf-roadmapper.md} +3 -3
  17. package/agents/{qgsd-verifier.md → nf-verifier.md} +8 -8
  18. package/bin/accept-debug-invariant.cjs +2 -2
  19. package/bin/account-manager.cjs +10 -10
  20. package/bin/aggregate-requirements.cjs +1 -1
  21. package/bin/analyze-assumptions.cjs +3 -3
  22. package/bin/analyze-state-space.cjs +14 -14
  23. package/bin/assumption-register.cjs +146 -0
  24. package/bin/attribute-trace-divergence.cjs +1 -1
  25. package/bin/auth-drivers/gh-cli.cjs +1 -1
  26. package/bin/auth-drivers/pool.cjs +1 -1
  27. package/bin/autoClosePtoF.cjs +3 -3
  28. package/bin/budget-tracker.cjs +77 -0
  29. package/bin/build-layer-manifest.cjs +153 -0
  30. package/bin/call-quorum-slot.cjs +3 -3
  31. package/bin/ccr-secure-config.cjs +5 -5
  32. package/bin/check-bundled-sdks.cjs +1 -1
  33. package/bin/check-mcp-health.cjs +1 -1
  34. package/bin/check-provider-health.cjs +6 -6
  35. package/bin/check-spec-sync.cjs +26 -26
  36. package/bin/check-trace-schema-drift.cjs +5 -5
  37. package/bin/conformance-schema.cjs +2 -2
  38. package/bin/cross-layer-dashboard.cjs +297 -0
  39. package/bin/design-impact.cjs +377 -0
  40. package/bin/detect-coverage-gaps.cjs +7 -7
  41. package/bin/failure-mode-catalog.cjs +227 -0
  42. package/bin/failure-taxonomy.cjs +177 -0
  43. package/bin/formal-scope-scan.cjs +179 -0
  44. package/bin/gate-a-grounding.cjs +334 -0
  45. package/bin/gate-b-abstraction.cjs +243 -0
  46. package/bin/gate-c-validation.cjs +166 -0
  47. package/bin/generate-formal-specs.cjs +17 -17
  48. package/bin/generate-petri-net.cjs +3 -3
  49. package/bin/generate-tla-cfg.cjs +5 -5
  50. package/bin/git-heatmap.cjs +571 -0
  51. package/bin/harness-diagnostic.cjs +326 -0
  52. package/bin/hazard-model.cjs +261 -0
  53. package/bin/install-formal-tools.cjs +1 -1
  54. package/bin/install.js +184 -139
  55. package/bin/instrumentation-map.cjs +178 -0
  56. package/bin/invariant-catalog.cjs +437 -0
  57. package/bin/issue-classifier.cjs +2 -2
  58. package/bin/load-baseline-requirements.cjs +4 -4
  59. package/bin/manage-agents-core.cjs +32 -32
  60. package/bin/migrate-to-slots.cjs +39 -39
  61. package/bin/mismatch-register.cjs +217 -0
  62. package/bin/nForma.cjs +176 -81
  63. package/bin/{qgsd-solve.cjs → nf-solve.cjs} +327 -14
  64. package/bin/observe-config.cjs +8 -0
  65. package/bin/observe-debt-writer.cjs +1 -1
  66. package/bin/observe-handler-deps.cjs +356 -0
  67. package/bin/observe-handler-grafana.cjs +2 -17
  68. package/bin/observe-handler-internal.cjs +5 -5
  69. package/bin/observe-handler-logstash.cjs +2 -17
  70. package/bin/observe-handler-prometheus.cjs +2 -17
  71. package/bin/observe-handler-upstream.cjs +251 -0
  72. package/bin/observe-handlers.cjs +12 -33
  73. package/bin/observe-render.cjs +68 -22
  74. package/bin/observe-utils.cjs +37 -0
  75. package/bin/observed-fsm.cjs +324 -0
  76. package/bin/planning-paths.cjs +6 -0
  77. package/bin/polyrepo.cjs +1 -1
  78. package/bin/probe-quorum-slots.cjs +1 -1
  79. package/bin/promote-gate-maturity.cjs +274 -0
  80. package/bin/promote-model.cjs +1 -1
  81. package/bin/propose-debug-invariants.cjs +1 -1
  82. package/bin/quorum-cache.cjs +144 -0
  83. package/bin/quorum-consensus-gate.cjs +1 -1
  84. package/bin/quorum-preflight.cjs +89 -0
  85. package/bin/quorum-slot-dispatch.cjs +6 -6
  86. package/bin/requirements-core.cjs +1 -1
  87. package/bin/review-mcp-logs.cjs +1 -1
  88. package/bin/risk-heatmap.cjs +151 -0
  89. package/bin/run-account-manager-tlc.cjs +4 -4
  90. package/bin/run-account-pool-alloy.cjs +2 -2
  91. package/bin/run-alloy.cjs +2 -2
  92. package/bin/run-audit-alloy.cjs +2 -2
  93. package/bin/run-breaker-tlc.cjs +3 -3
  94. package/bin/run-formal-check.cjs +9 -9
  95. package/bin/run-formal-verify.cjs +30 -9
  96. package/bin/run-installer-alloy.cjs +2 -2
  97. package/bin/run-oscillation-tlc.cjs +4 -4
  98. package/bin/run-phase-tlc.cjs +1 -1
  99. package/bin/run-protocol-tlc.cjs +4 -4
  100. package/bin/run-quorum-composition-alloy.cjs +2 -2
  101. package/bin/run-sensitivity-sweep.cjs +2 -2
  102. package/bin/run-stop-hook-tlc.cjs +3 -3
  103. package/bin/run-tlc.cjs +21 -21
  104. package/bin/run-transcript-alloy.cjs +2 -2
  105. package/bin/secrets.cjs +5 -5
  106. package/bin/security-sweep.cjs +238 -0
  107. package/bin/sensitivity-report.cjs +3 -3
  108. package/bin/set-secret.cjs +5 -5
  109. package/bin/setup-telemetry-cron.sh +3 -3
  110. package/bin/stall-detector.cjs +126 -0
  111. package/bin/state-candidates.cjs +206 -0
  112. package/bin/sync-baseline-requirements.cjs +1 -1
  113. package/bin/telemetry-collector.cjs +1 -1
  114. package/bin/test-changed.cjs +111 -0
  115. package/bin/test-recipe-gen.cjs +250 -0
  116. package/bin/trace-corpus-stats.cjs +211 -0
  117. package/bin/unified-mcp-server.mjs +3 -3
  118. package/bin/update-scoreboard.cjs +1 -1
  119. package/bin/validate-memory.cjs +2 -2
  120. package/bin/validate-traces.cjs +10 -10
  121. package/bin/verify-quorum-health.cjs +66 -5
  122. package/bin/xstate-to-tla.cjs +4 -4
  123. package/bin/xstate-trace-walker.cjs +3 -3
  124. package/commands/{qgsd → nf}/add-phase.md +3 -3
  125. package/commands/{qgsd → nf}/add-requirement.md +3 -3
  126. package/commands/{qgsd → nf}/add-todo.md +3 -3
  127. package/commands/{qgsd → nf}/audit-milestone.md +4 -4
  128. package/commands/{qgsd → nf}/check-todos.md +3 -3
  129. package/commands/{qgsd → nf}/cleanup.md +3 -3
  130. package/commands/{qgsd → nf}/close-formal-gaps.md +2 -2
  131. package/commands/{qgsd → nf}/complete-milestone.md +9 -9
  132. package/commands/{qgsd → nf}/debug.md +9 -9
  133. package/commands/{qgsd → nf}/discuss-phase.md +3 -3
  134. package/commands/{qgsd → nf}/execute-phase.md +15 -15
  135. package/commands/{qgsd → nf}/fix-tests.md +3 -3
  136. package/commands/{qgsd → nf}/formal-test-sync.md +1 -1
  137. package/commands/{qgsd → nf}/health.md +3 -3
  138. package/commands/{qgsd → nf}/help.md +3 -3
  139. package/commands/{qgsd → nf}/insert-phase.md +3 -3
  140. package/commands/nf/join-discord.md +18 -0
  141. package/commands/{qgsd → nf}/list-phase-assumptions.md +2 -2
  142. package/commands/{qgsd → nf}/map-codebase.md +7 -7
  143. package/commands/{qgsd → nf}/map-requirements.md +3 -3
  144. package/commands/{qgsd → nf}/mcp-restart.md +3 -3
  145. package/commands/{qgsd → nf}/mcp-set-model.md +8 -8
  146. package/commands/{qgsd → nf}/mcp-setup.md +63 -63
  147. package/commands/{qgsd → nf}/mcp-status.md +3 -3
  148. package/commands/{qgsd → nf}/mcp-update.md +7 -7
  149. package/commands/{qgsd → nf}/new-milestone.md +8 -8
  150. package/commands/{qgsd → nf}/new-project.md +8 -8
  151. package/commands/{qgsd → nf}/observe.md +49 -16
  152. package/commands/{qgsd → nf}/pause-work.md +3 -3
  153. package/commands/{qgsd → nf}/plan-milestone-gaps.md +5 -5
  154. package/commands/{qgsd → nf}/plan-phase.md +6 -6
  155. package/commands/{qgsd → nf}/polyrepo.md +2 -2
  156. package/commands/{qgsd → nf}/progress.md +3 -3
  157. package/commands/{qgsd → nf}/queue.md +2 -2
  158. package/commands/{qgsd → nf}/quick.md +8 -8
  159. package/commands/{qgsd → nf}/quorum-test.md +10 -10
  160. package/commands/{qgsd → nf}/quorum.md +36 -86
  161. package/commands/{qgsd → nf}/reapply-patches.md +2 -2
  162. package/commands/{qgsd → nf}/remove-phase.md +3 -3
  163. package/commands/{qgsd → nf}/research-phase.md +12 -12
  164. package/commands/{qgsd → nf}/resume-work.md +3 -3
  165. package/commands/nf/review-requirements.md +31 -0
  166. package/commands/{qgsd → nf}/set-profile.md +3 -3
  167. package/commands/{qgsd → nf}/settings.md +6 -6
  168. package/commands/{qgsd → nf}/solve.md +35 -35
  169. package/commands/{qgsd → nf}/sync-baselines.md +4 -4
  170. package/commands/{qgsd → nf}/triage.md +10 -10
  171. package/commands/{qgsd → nf}/update.md +3 -3
  172. package/commands/{qgsd → nf}/verify-work.md +5 -5
  173. package/hooks/dist/config-loader.js +188 -32
  174. package/hooks/dist/conformance-schema.cjs +2 -2
  175. package/hooks/dist/gsd-context-monitor.js +118 -13
  176. package/hooks/dist/{qgsd-check-update.js → nf-check-update.js} +5 -5
  177. package/hooks/dist/{qgsd-circuit-breaker.js → nf-circuit-breaker.js} +35 -24
  178. package/hooks/dist/{qgsd-precompact.js → nf-precompact.js} +13 -13
  179. package/hooks/dist/{qgsd-prompt.js → nf-prompt.js} +110 -33
  180. package/hooks/dist/nf-session-start.js +185 -0
  181. package/hooks/dist/{qgsd-slot-correlator.js → nf-slot-correlator.js} +13 -5
  182. package/hooks/dist/{qgsd-spec-regen.js → nf-spec-regen.js} +17 -8
  183. package/hooks/dist/{qgsd-statusline.js → nf-statusline.js} +12 -3
  184. package/hooks/dist/{qgsd-stop.js → nf-stop.js} +152 -18
  185. package/hooks/dist/{qgsd-token-collector.js → nf-token-collector.js} +12 -4
  186. package/hooks/dist/unified-mcp-server.mjs +2 -2
  187. package/package.json +6 -4
  188. package/scripts/build-hooks.js +13 -6
  189. package/scripts/secret-audit.sh +1 -1
  190. package/scripts/verify-hooks-sync.cjs +90 -0
  191. package/templates/{qgsd.json → nf.json} +4 -4
  192. package/commands/qgsd/join-discord.md +0 -18
  193. 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 QGSD formal verification tools:
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