@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
@@ -5,7 +5,7 @@
5
5
  // Requirements: SIG-01
6
6
  //
7
7
  // Usage:
8
- // node bin/detect-coverage-gaps.cjs [--spec=QGSDQuorum] [--log=path]
8
+ // node bin/detect-coverage-gaps.cjs [--spec=NFQuorum] [--log=path]
9
9
  //
10
10
  // Output:
11
11
  // .planning/formal/coverage-gaps.md — structured test backlog when gaps exist
@@ -19,15 +19,15 @@ const path = require('path');
19
19
  // Maps each TLA+ spec to its state variable name and value-to-name mapping.
20
20
  // Source of truth: state comments in the TLA+ spec files.
21
21
  const STATE_MAPS = {
22
- 'QGSDQuorum': {
22
+ 'NFQuorum': {
23
23
  variable: 's',
24
24
  values: { '0': 'COLLECTING_VOTES', '1': 'DECIDED', '2': 'DELIBERATING' },
25
25
  },
26
- 'QGSDStopHook': {
26
+ 'NFStopHook': {
27
27
  variable: 'phase',
28
28
  values: { '0': 'IDLE', '1': 'READING', '2': 'DECIDING', '3': 'BLOCKED' },
29
29
  },
30
- 'QGSDCircuitBreaker': {
30
+ 'NFCircuitBreaker': {
31
31
  variable: 'state',
32
32
  values: { '0': 'MONITORING', '1': 'TRIGGERED', '2': 'RECOVERING' },
33
33
  },
@@ -51,7 +51,7 @@ const ACTION_TO_STATE = {
51
51
 
52
52
  /**
53
53
  * parseTlcStates(specName) — returns the full set of named states for a spec.
54
- * @param {string} specName - TLA+ spec name (e.g. 'QGSDQuorum')
54
+ * @param {string} specName - TLA+ spec name (e.g. 'NFQuorum')
55
55
  * @returns {{ specName: string, states: Set<string>, variable: string } | null}
56
56
  */
57
57
  function parseTlcStates(specName) {
@@ -102,7 +102,7 @@ function parseTraceStates(logPath) {
102
102
  * @returns {{ status: string, gaps?: string[], outputPath?: string, reason?: string }}
103
103
  */
104
104
  function detectCoverageGaps(options = {}) {
105
- const specName = options.specName || 'QGSDQuorum';
105
+ const specName = options.specName || 'NFQuorum';
106
106
  const pp2 = require('./planning-paths.cjs');
107
107
  const logPath = options.logPath || pp2.resolveWithFallback(process.cwd(), 'conformance-events');
108
108
  const outputPath = options.outputPath || path.join(process.cwd(), '.planning', 'formal', 'coverage-gaps.md');
@@ -186,7 +186,7 @@ if (require.main === module) {
186
186
  const specArg = args.find(a => a.startsWith('--spec='));
187
187
  const logArg = args.find(a => a.startsWith('--log='));
188
188
 
189
- const specName = specArg ? specArg.split('=')[1] : 'QGSDQuorum';
189
+ const specName = specArg ? specArg.split('=')[1] : 'NFQuorum';
190
190
  const logPath = logArg ? logArg.split('=')[1] : undefined;
191
191
 
192
192
  const result = detectCoverageGaps({ specName, logPath });
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * failure-mode-catalog.cjs — Failure mode enumeration for Layer 3 (Reasoning).
6
+ *
7
+ * Enumerates concrete failure modes (omission, commission, corruption) per
8
+ * L2 state-event pair. Uses hazard-model.json severity scores for corruption
9
+ * thresholds and observed-fsm.json model_comparison for commission conditions.
10
+ *
11
+ * Requirements: RSN-02
12
+ *
13
+ * Usage:
14
+ * node bin/failure-mode-catalog.cjs # print summary to stdout
15
+ * node bin/failure-mode-catalog.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, 'failure-mode-catalog.json');
25
+
26
+ const JSON_FLAG = process.argv.includes('--json');
27
+
28
+ // ── Severity class mapping ──────────────────────────────────────────────────
29
+
30
+ function classifySeverity(severity, mode) {
31
+ if (mode === 'commission') return 'model_gap';
32
+ if (severity >= 8) return 'critical';
33
+ if (severity >= 6) return 'stalled';
34
+ if (severity >= 4) return 'degraded';
35
+ if (severity >= 2) return 'cosmetic';
36
+ return 'cosmetic';
37
+ }
38
+
39
+ // ── Failure mode descriptions ───────────────────────────────────────────────
40
+
41
+ function omissionDescription(fromState, event, toState) {
42
+ return `Transition ${fromState} --[${event}]--> ${toState} does not fire when expected`;
43
+ }
44
+
45
+ function omissionEffect(fromState) {
46
+ return `System stays in ${fromState}; expected state change does not occur`;
47
+ }
48
+
49
+ function commissionDescription(fromState, event, toState) {
50
+ return `Transition ${fromState} --[${event}]--> ${toState} fires but is not modeled in XState`;
51
+ }
52
+
53
+ function commissionEffect(fromState, event, toState) {
54
+ return `Unmodeled state transition from ${fromState} to ${toState} on ${event}; behavior diverges from spec`;
55
+ }
56
+
57
+ function corruptionDescription(fromState, event, toState) {
58
+ return `Transition ${fromState} --[${event}]--> fires but produces wrong target state (not ${toState})`;
59
+ }
60
+
61
+ function corruptionEffect(fromState, toState) {
62
+ return `System enters incorrect state instead of ${toState}; downstream behavior undefined`;
63
+ }
64
+
65
+ // ── Mismatch enrichment ─────────────────────────────────────────────────────
66
+
67
+ function findMismatches(fromState, event, mismatches) {
68
+ return mismatches.filter(m => {
69
+ // Mismatches have expected/actual states; match if the event context aligns
70
+ // Since mismatches don't have from/event fields directly, we match on state overlap
71
+ return m.expected_state === fromState || m.actual_state === fromState;
72
+ });
73
+ }
74
+
75
+ // ── Core enumeration ────────────────────────────────────────────────────────
76
+
77
+ function enumerateFailureModes(observedFsm, hazardModel, mismatches) {
78
+ const missingInModel = new Set(
79
+ (observedFsm.model_comparison?.missing_in_model || [])
80
+ .map(m => `${m.from}-${m.event}`)
81
+ );
82
+
83
+ // Build severity lookup from hazard model
84
+ const severityMap = {};
85
+ for (const h of (hazardModel?.hazards || [])) {
86
+ severityMap[`${h.state}-${h.event}`] = h.severity;
87
+ }
88
+
89
+ const failureModes = [];
90
+
91
+ for (const [fromState, events] of Object.entries(observedFsm.observed_transitions)) {
92
+ for (const [event, data] of Object.entries(events)) {
93
+ const key = `${fromState}-${event}`;
94
+ const severity = severityMap[key] || 4;
95
+ const toState = data.to_state;
96
+ const relMismatches = findMismatches(fromState, event, mismatches);
97
+ const mismatchNote = relMismatches.length > 0
98
+ ? ` (${relMismatches.length} observed mismatch(es): ${relMismatches.map(m => m.id).join(', ')})`
99
+ : '';
100
+
101
+ // 1. Omission (always)
102
+ failureModes.push({
103
+ id: `FM-${fromState}-${event}-OMISSION`,
104
+ state: fromState,
105
+ event,
106
+ to_state: toState,
107
+ failure_mode: 'omission',
108
+ description: omissionDescription(fromState, event, toState) + mismatchNote,
109
+ effect: omissionEffect(fromState),
110
+ severity_class: classifySeverity(severity, 'omission'),
111
+ derived_from: [
112
+ { layer: 'L2', artifact: 'semantics/observed-fsm.json', ref: `observed_transitions.${fromState}.${event}` },
113
+ { layer: 'L3', artifact: 'reasoning/hazard-model.json', ref: `hazards[id=HAZARD-${fromState}-${event}]` },
114
+ ],
115
+ });
116
+
117
+ // 2. Commission (conditional: only if in missing_in_model)
118
+ if (missingInModel.has(key)) {
119
+ failureModes.push({
120
+ id: `FM-${fromState}-${event}-COMMISSION`,
121
+ state: fromState,
122
+ event,
123
+ to_state: toState,
124
+ failure_mode: 'commission',
125
+ description: commissionDescription(fromState, event, toState) + mismatchNote,
126
+ effect: commissionEffect(fromState, event, toState),
127
+ severity_class: classifySeverity(severity, 'commission'),
128
+ derived_from: [
129
+ { layer: 'L2', artifact: 'semantics/observed-fsm.json', ref: `model_comparison.missing_in_model` },
130
+ { layer: 'L2', artifact: 'semantics/observed-fsm.json', ref: `observed_transitions.${fromState}.${event}` },
131
+ { layer: 'L3', artifact: 'reasoning/hazard-model.json', ref: `hazards[id=HAZARD-${fromState}-${event}]` },
132
+ ],
133
+ });
134
+ }
135
+
136
+ // 3. Corruption (conditional: only if severity >= 6)
137
+ if (severity >= 6) {
138
+ failureModes.push({
139
+ id: `FM-${fromState}-${event}-CORRUPTION`,
140
+ state: fromState,
141
+ event,
142
+ to_state: toState,
143
+ failure_mode: 'corruption',
144
+ description: corruptionDescription(fromState, event, toState) + mismatchNote,
145
+ effect: corruptionEffect(fromState, toState),
146
+ severity_class: classifySeverity(severity, 'corruption'),
147
+ derived_from: [
148
+ { layer: 'L2', artifact: 'semantics/observed-fsm.json', ref: `observed_transitions.${fromState}.${event}` },
149
+ { layer: 'L3', artifact: 'reasoning/hazard-model.json', ref: `hazards[id=HAZARD-${fromState}-${event}]` },
150
+ ],
151
+ });
152
+ }
153
+ }
154
+ }
155
+
156
+ // Count by mode and severity class
157
+ const byMode = { omission: 0, commission: 0, corruption: 0 };
158
+ const bySeverityClass = {};
159
+ for (const fm of failureModes) {
160
+ byMode[fm.failure_mode] = (byMode[fm.failure_mode] || 0) + 1;
161
+ bySeverityClass[fm.severity_class] = (bySeverityClass[fm.severity_class] || 0) + 1;
162
+ }
163
+
164
+ return {
165
+ schema_version: '1',
166
+ generated: new Date().toISOString(),
167
+ failure_modes: failureModes,
168
+ summary: {
169
+ total: failureModes.length,
170
+ by_mode: byMode,
171
+ by_severity_class: bySeverityClass,
172
+ },
173
+ };
174
+ }
175
+
176
+ // ── Entry point ─────────────────────────────────────────────────────────────
177
+
178
+ function main() {
179
+ // Load L2 observed FSM
180
+ const fsmPath = path.join(FORMAL, 'semantics', 'observed-fsm.json');
181
+ if (!fs.existsSync(fsmPath)) {
182
+ console.error('ERROR: observed-fsm.json not found at', fsmPath);
183
+ process.exit(1);
184
+ }
185
+ const observedFsm = JSON.parse(fs.readFileSync(fsmPath, 'utf8'));
186
+
187
+ // Load L3 hazard model (produced by hazard-model.cjs)
188
+ const hazardPath = path.join(REASONING_DIR, 'hazard-model.json');
189
+ if (!fs.existsSync(hazardPath)) {
190
+ console.error('ERROR: hazard-model.json not found at', hazardPath);
191
+ console.error('Run bin/hazard-model.cjs first.');
192
+ process.exit(1);
193
+ }
194
+ const hazardModel = JSON.parse(fs.readFileSync(hazardPath, 'utf8'));
195
+
196
+ // Load mismatch register
197
+ const mismatchPath = path.join(FORMAL, 'semantics', 'mismatch-register.jsonl');
198
+ let mismatches = [];
199
+ if (fs.existsSync(mismatchPath)) {
200
+ mismatches = fs.readFileSync(mismatchPath, 'utf8')
201
+ .trim().split('\n')
202
+ .filter(Boolean)
203
+ .map(line => JSON.parse(line));
204
+ }
205
+
206
+ const output = enumerateFailureModes(observedFsm, hazardModel, mismatches);
207
+
208
+ // Write output
209
+ fs.mkdirSync(REASONING_DIR, { recursive: true });
210
+ fs.writeFileSync(OUT_FILE, JSON.stringify(output, null, 2) + '\n');
211
+
212
+ if (JSON_FLAG) {
213
+ process.stdout.write(JSON.stringify(output));
214
+ } else {
215
+ console.log(`Failure Mode Catalog`);
216
+ console.log(` Total failure modes: ${output.summary.total}`);
217
+ console.log(` By mode: ${JSON.stringify(output.summary.by_mode)}`);
218
+ console.log(` By severity class: ${JSON.stringify(output.summary.by_severity_class)}`);
219
+ console.log(` Output: ${OUT_FILE}`);
220
+ }
221
+
222
+ process.exit(0);
223
+ }
224
+
225
+ if (require.main === module) main();
226
+
227
+ module.exports = { enumerateFailureModes, classifySeverity };
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // bin/failure-taxonomy.cjs
4
+ // Classifies check-result failures into 5 categories:
5
+ // crash, timeout, logic_violation, drift, degradation
6
+ //
7
+ // Requirement: EVID-03
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ const ROOT = process.env.PROJECT_ROOT || path.join(__dirname, '..');
13
+ const EVIDENCE_DIR = path.join(ROOT, '.planning', 'formal', 'evidence');
14
+ const CHECK_RESULTS_PATH = path.join(ROOT, '.planning', 'formal', 'check-results.ndjson');
15
+ const DEBT_PATH = path.join(ROOT, '.planning', 'formal', 'debt.json');
16
+ const OUTPUT_PATH = path.join(EVIDENCE_DIR, 'failure-taxonomy.json');
17
+
18
+ const JSON_FLAG = process.argv.includes('--json');
19
+
20
+ // Timeout threshold in ms (> 60 seconds suggests TLC state explosion)
21
+ const TIMEOUT_THRESHOLD_MS = 60000;
22
+
23
+ // ── Classification rules ────────────────────────────────────────────────────
24
+
25
+ /**
26
+ * Classify a check-result failure into exactly one category.
27
+ * Decision rules per research Pitfall 5:
28
+ *
29
+ * - crash: formalism tool itself crashed (non-zero exit, no structured result, stack trace)
30
+ * - timeout: runtime_ms > threshold AND result=fail (TLC state explosion, model checker timeout)
31
+ * - logic_violation: TLC/Alloy counterexample, assertion failure
32
+ * - drift: validate-traces reports divergence (formalism "trace" with divergence count)
33
+ * - degradation: metrics trending worse (reserved; falls back to logic_violation when no baseline)
34
+ */
35
+ function classifyFailure(entry) {
36
+ // Check for crash indicators
37
+ if (entry.metadata && entry.metadata.stack_trace) {
38
+ return { category: 'crash', reason: 'Stack trace present in metadata' };
39
+ }
40
+ if (entry.summary && /error|crash|exception|ENOENT|spawn/i.test(entry.summary) &&
41
+ !/counterexample|divergence|fail:/i.test(entry.summary)) {
42
+ return { category: 'crash', reason: `Error pattern in summary: ${entry.summary.substring(0, 80)}` };
43
+ }
44
+
45
+ // Check for timeout
46
+ if (entry.runtime_ms && entry.runtime_ms > TIMEOUT_THRESHOLD_MS) {
47
+ return { category: 'timeout', reason: `runtime_ms=${entry.runtime_ms} exceeds ${TIMEOUT_THRESHOLD_MS}ms threshold` };
48
+ }
49
+
50
+ // Check for drift (trace formalism with divergences)
51
+ if (entry.formalism === 'trace') {
52
+ const divMatch = entry.summary && entry.summary.match(/(\d+)\s+divergence/);
53
+ if (divMatch) {
54
+ return { category: 'drift', reason: `Trace divergence: ${divMatch[1]} divergence(s) detected` };
55
+ }
56
+ return { category: 'drift', reason: 'Trace formalism failure (drift)' };
57
+ }
58
+
59
+ // Degradation: reserved, falls back to logic_violation when no baseline
60
+ // (no baseline data exists in current codebase)
61
+
62
+ // Default: logic_violation (counterexample, assertion failure, etc.)
63
+ return { category: 'logic_violation', reason: entry.summary
64
+ ? `Logic violation: ${entry.summary.substring(0, 100)}`
65
+ : 'Logic violation: check-result failure without specific categorization' };
66
+ }
67
+
68
+ // ── Main ────────────────────────────────────────────────────────────────────
69
+
70
+ function main() {
71
+ if (!fs.existsSync(EVIDENCE_DIR)) {
72
+ fs.mkdirSync(EVIDENCE_DIR, { recursive: true });
73
+ }
74
+
75
+ // Read check-results.ndjson
76
+ const failures = [];
77
+
78
+ if (fs.existsSync(CHECK_RESULTS_PATH)) {
79
+ const lines = fs.readFileSync(CHECK_RESULTS_PATH, 'utf8').split('\n').filter(l => l.trim());
80
+ for (const line of lines) {
81
+ try {
82
+ const entry = JSON.parse(line);
83
+ if (entry.result === 'fail') {
84
+ failures.push(entry);
85
+ }
86
+ } catch (_) {}
87
+ }
88
+ }
89
+
90
+ // Read debt.json for additional failure sources
91
+ if (fs.existsSync(DEBT_PATH)) {
92
+ try {
93
+ const debt = JSON.parse(fs.readFileSync(DEBT_PATH, 'utf8'));
94
+ if (Array.isArray(debt.debt_entries)) {
95
+ for (const entry of debt.debt_entries) {
96
+ failures.push({
97
+ ...entry,
98
+ result: 'fail',
99
+ formalism: entry.formalism || 'unknown',
100
+ tool: entry.tool || 'debt-entry',
101
+ summary: entry.summary || entry.description || 'Debt entry',
102
+ timestamp: entry.timestamp || debt.last_updated,
103
+ _source: 'debt.json',
104
+ });
105
+ }
106
+ }
107
+ } catch (_) {}
108
+ }
109
+
110
+ // Classify each failure
111
+ const categories = {
112
+ crash: [],
113
+ timeout: [],
114
+ logic_violation: [],
115
+ drift: [],
116
+ degradation: [],
117
+ };
118
+ const unclassified = [];
119
+
120
+ for (const failure of failures) {
121
+ const { category, reason } = classifyFailure(failure);
122
+ const classified = { ...failure, category, classification_reason: reason };
123
+
124
+ if (category in categories) {
125
+ categories[category].push(classified);
126
+ } else {
127
+ unclassified.push(classified);
128
+ }
129
+ }
130
+
131
+ // Count per category
132
+ const categoryCounts = {};
133
+ for (const [cat, entries] of Object.entries(categories)) {
134
+ categoryCounts[cat] = entries.length;
135
+ }
136
+
137
+ // Build result
138
+ const result = {
139
+ schema_version: '1',
140
+ generated: new Date().toISOString(),
141
+ total_failures: failures.length,
142
+ categories,
143
+ category_counts: categoryCounts,
144
+ degradation_fallback_note: 'When no baseline exists, potential degradation entries are classified as logic_violation',
145
+ unclassified,
146
+ summary: `${failures.length} failures classified: ` +
147
+ Object.entries(categoryCounts).map(([k, v]) => `${k}=${v}`).join(', ') +
148
+ `. ${unclassified.length} unclassified.`,
149
+ };
150
+
151
+ fs.writeFileSync(OUTPUT_PATH, JSON.stringify(result, null, 2) + '\n', 'utf8');
152
+
153
+ if (JSON_FLAG) {
154
+ console.log(JSON.stringify(result, null, 2));
155
+ } else {
156
+ console.log('Failure Taxonomy Generated');
157
+ console.log(` Total failures: ${failures.length}`);
158
+ for (const [cat, count] of Object.entries(categoryCounts)) {
159
+ console.log(` ${cat}: ${count}`);
160
+ }
161
+ if (unclassified.length > 0) {
162
+ console.log(` UNCLASSIFIED: ${unclassified.length}`);
163
+ }
164
+ }
165
+
166
+ // Exit 1 if any unclassified
167
+ if (unclassified.length > 0) {
168
+ process.exit(1);
169
+ }
170
+ }
171
+
172
+ // Export for testing
173
+ module.exports = { classifyFailure, TIMEOUT_THRESHOLD_MS };
174
+
175
+ if (require.main === module) {
176
+ main();
177
+ }
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const SPEC_DIR = path.join(process.cwd(), '.planning', 'formal', 'spec');
8
+
9
+ function printHelp() {
10
+ console.log(`Usage: node bin/formal-scope-scan.cjs --description "text" [options]
11
+
12
+ Options:
13
+ --description "text" Description to match against (required)
14
+ --files file1,file2 Source files to check for overlap (optional)
15
+ --format json|lines Output format (default: json)
16
+ --help Show this help message
17
+
18
+ Matching algorithm (any signal fires):
19
+ 1. Source file overlap: --files matched against module source_files globs
20
+ 2. Concept matching: exact token match against curated concepts
21
+ 3. Module name match: exact token match against module directory name
22
+
23
+ Examples:
24
+ node bin/formal-scope-scan.cjs --description "fix quorum deliberation bug"
25
+ node bin/formal-scope-scan.cjs --description "update breaker" --format lines
26
+ node bin/formal-scope-scan.cjs --files "hooks/nf-stop.js" --description "something"
27
+ `);
28
+ }
29
+
30
+ function parseArgs(argv) {
31
+ const args = { description: '', files: [], format: 'json', help: false };
32
+ for (let i = 2; i < argv.length; i++) {
33
+ if (argv[i] === '--help' || argv[i] === '-h') {
34
+ args.help = true;
35
+ } else if (argv[i] === '--description' && argv[i + 1]) {
36
+ args.description = argv[++i];
37
+ } else if (argv[i] === '--files' && argv[i + 1]) {
38
+ args.files = argv[++i].split(',').map(f => f.trim()).filter(Boolean);
39
+ } else if (argv[i] === '--format' && argv[i + 1]) {
40
+ args.format = argv[++i];
41
+ }
42
+ }
43
+ return args;
44
+ }
45
+
46
+ function globToRegex(glob) {
47
+ let regex = '';
48
+ let i = 0;
49
+ while (i < glob.length) {
50
+ if (glob[i] === '*' && glob[i + 1] === '*') {
51
+ regex += '.*';
52
+ i += 2;
53
+ if (glob[i] === '/') i++; // skip trailing slash after **
54
+ } else if (glob[i] === '*') {
55
+ regex += '[^/]*';
56
+ i++;
57
+ } else if (glob[i] === '?') {
58
+ regex += '[^/]';
59
+ i++;
60
+ } else if ('.+^${}()|[]\\'.includes(glob[i])) {
61
+ regex += '\\' + glob[i];
62
+ i++;
63
+ } else {
64
+ regex += glob[i];
65
+ i++;
66
+ }
67
+ }
68
+ return new RegExp('^' + regex + '$');
69
+ }
70
+
71
+ function matchesSourceFiles(providedFiles, moduleSourceFiles) {
72
+ for (const pf of providedFiles) {
73
+ for (const sf of moduleSourceFiles) {
74
+ const re = globToRegex(sf);
75
+ if (re.test(pf)) return true;
76
+ }
77
+ }
78
+ return false;
79
+ }
80
+
81
+ function matchesConcepts(descLower, tokens, concepts) {
82
+ for (const concept of concepts) {
83
+ const conceptLower = concept.toLowerCase();
84
+ // Exact token match
85
+ if (tokens.includes(conceptLower)) return true;
86
+ // Multi-word concept substring match against raw description
87
+ if (conceptLower.includes('-') || conceptLower.includes(' ')) {
88
+ if (descLower.includes(conceptLower)) return true;
89
+ }
90
+ }
91
+ return false;
92
+ }
93
+
94
+ function matchesModuleName(tokens, moduleName) {
95
+ return tokens.includes(moduleName.toLowerCase());
96
+ }
97
+
98
+ function main() {
99
+ const args = parseArgs(process.argv);
100
+
101
+ if (args.help) {
102
+ printHelp();
103
+ process.exit(0);
104
+ }
105
+
106
+ if (!args.description) {
107
+ console.error('Error: --description is required');
108
+ process.exit(1);
109
+ }
110
+
111
+ // Fail-open: if spec dir doesn't exist, output empty
112
+ if (!fs.existsSync(SPEC_DIR)) {
113
+ if (args.format === 'lines') {
114
+ // no output
115
+ } else {
116
+ console.log('[]');
117
+ }
118
+ process.exit(0);
119
+ }
120
+
121
+ const descLower = args.description.toLowerCase();
122
+ const tokens = descLower.split(/[\s\-_]+/).filter(t => t.length > 0);
123
+
124
+ const modules = fs.readdirSync(SPEC_DIR, { withFileTypes: true })
125
+ .filter(d => d.isDirectory())
126
+ .map(d => d.name);
127
+
128
+ const matches = [];
129
+
130
+ for (const mod of modules) {
131
+ const scopePath = path.join(SPEC_DIR, mod, 'scope.json');
132
+ if (!fs.existsSync(scopePath)) {
133
+ process.stderr.write(`Warning: ${scopePath} not found, skipping module ${mod}\n`);
134
+ continue;
135
+ }
136
+
137
+ let scope;
138
+ try {
139
+ scope = JSON.parse(fs.readFileSync(scopePath, 'utf8'));
140
+ } catch (e) {
141
+ process.stderr.write(`Warning: Failed to parse ${scopePath}: ${e.message}\n`);
142
+ continue;
143
+ }
144
+
145
+ const invariantsPath = `.planning/formal/spec/${mod}/invariants.md`;
146
+ let matchedBy = null;
147
+
148
+ // Priority 1: Source file overlap
149
+ if (args.files.length > 0 && scope.source_files && matchesSourceFiles(args.files, scope.source_files)) {
150
+ matchedBy = 'source_file';
151
+ }
152
+
153
+ // Priority 2: Concept matching
154
+ if (!matchedBy && scope.concepts && matchesConcepts(descLower, tokens, scope.concepts)) {
155
+ matchedBy = 'concept';
156
+ }
157
+
158
+ // Priority 3: Module name match (exact token only)
159
+ if (!matchedBy && matchesModuleName(tokens, mod)) {
160
+ matchedBy = 'module_name';
161
+ }
162
+
163
+ if (matchedBy) {
164
+ matches.push({ module: mod, path: invariantsPath, matched_by: matchedBy });
165
+ }
166
+ }
167
+
168
+ if (args.format === 'lines') {
169
+ for (const m of matches) {
170
+ console.log(`${m.module}\t${m.path}`);
171
+ }
172
+ } else {
173
+ console.log(JSON.stringify(matches, null, 2));
174
+ }
175
+
176
+ process.exit(0);
177
+ }
178
+
179
+ main();