@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,334 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * gate-a-grounding.cjs — Gate A grounding score computation.
6
+ *
7
+ * Measures alignment between L1 evidence (conformance traces) and L2 semantics
8
+ * (operational model). A trace event is "explained" iff:
9
+ * 1. Its action maps to a known entry in event-vocabulary.json (vocabulary_mapped = true)
10
+ * 2. Its XState transition is EITHER:
11
+ * a. Validated by fresh actor replay (xstate_valid = true), OR
12
+ * b. Correctly skipped as a mid-session event under H1 methodology (methodology_skip = true)
13
+ *
14
+ * Unexplained traces are classified into:
15
+ * - instrumentation_bug: action NOT in vocabulary
16
+ * - model_gap: action in vocabulary but XState replay fails
17
+ * - genuine_violation: model_gap event that violates a declared observed invariant
18
+ *
19
+ * Requirements: GATE-01
20
+ *
21
+ * Usage:
22
+ * node bin/gate-a-grounding.cjs # print summary to stdout
23
+ * node bin/gate-a-grounding.cjs --json # print full results JSON to stdout
24
+ */
25
+
26
+ const fs = require('fs');
27
+ const path = require('path');
28
+
29
+ const ROOT = process.env.PROJECT_ROOT || path.join(__dirname, '..');
30
+ const FORMAL = path.join(ROOT, '.planning', 'formal');
31
+ const GATES_DIR = path.join(FORMAL, 'gates');
32
+ const OUT_FILE = path.join(GATES_DIR, 'gate-a-grounding.json');
33
+
34
+ const JSON_FLAG = process.argv.includes('--json');
35
+
36
+ // ── Helpers ─────────────────────────────────────────────────────────────────
37
+
38
+ /**
39
+ * H1 methodology skip: mid-session events (phase !== 'IDLE' AND action !== 'quorum_start')
40
+ * cannot be validated with a fresh actor from IDLE.
41
+ */
42
+ function isMethodologySkip(event) {
43
+ if (!event) return false;
44
+ if (event.action === 'quorum_start') return false;
45
+ if (event.phase && event.phase !== 'IDLE') return true;
46
+ return false;
47
+ }
48
+
49
+ /**
50
+ * Check if a model_gap event violates an observed invariant.
51
+ * Returns the violated invariant name or null.
52
+ */
53
+ function checkGenuineViolation(event, observedInvariants, sessionContext) {
54
+ if (!observedInvariants || observedInvariants.length === 0) return null;
55
+
56
+ for (const inv of observedInvariants) {
57
+ const expr = (inv.property_expression || '').toLowerCase();
58
+
59
+ // Match invariant property_expression keywords against event action
60
+ if (inv.name === 'quorum_start_precedes_complete' || expr.includes('quorum_start always precedes quorum_complete')) {
61
+ if (event.action === 'quorum_complete' && sessionContext && !sessionContext.seenQuorumStart) {
62
+ return inv.name;
63
+ }
64
+ }
65
+
66
+ if (inv.name === 'circuit_break_within_quorum_session' || expr.includes('circuit_break events only occur during active quorum')) {
67
+ if (event.action === 'circuit_break' && sessionContext && !sessionContext.seenQuorumStart) {
68
+ return inv.name;
69
+ }
70
+ }
71
+ }
72
+
73
+ return null;
74
+ }
75
+
76
+ // ── Core computation ────────────────────────────────────────────────────────
77
+
78
+ function computeGateA(conformanceEvents, vocabulary, invariantCatalog, mismatchRegister) {
79
+ const warnings = [];
80
+
81
+ // Load mapToXStateEvent
82
+ const { mapToXStateEvent } = require(path.join(__dirname, 'validate-traces.cjs'));
83
+
84
+ // Load XState machine
85
+ const machinePath = (() => {
86
+ const repoDist = path.join(__dirname, '..', 'dist', 'machines', 'nf-workflow.machine.js');
87
+ const installDist = path.join(__dirname, 'dist', 'machines', 'nf-workflow.machine.js');
88
+ return fs.existsSync(repoDist) ? repoDist : installDist;
89
+ })();
90
+ const { createActor, nfWorkflowMachine } = require(machinePath);
91
+
92
+ // Extract vocabulary actions set
93
+ const vocabActions = new Set();
94
+ if (vocabulary && vocabulary.vocabulary) {
95
+ for (const key of Object.keys(vocabulary.vocabulary)) {
96
+ vocabActions.add(key);
97
+ }
98
+ }
99
+
100
+ // Extract observed invariants for genuine_violation check
101
+ let observedInvariants = [];
102
+ if (invariantCatalog && invariantCatalog.invariants) {
103
+ observedInvariants = invariantCatalog.invariants.filter(i => i.type === 'observed');
104
+ } else {
105
+ warnings.push('invariant-catalog.json not found or invalid, skipping genuine_violation reclassification');
106
+ }
107
+
108
+ if (!mismatchRegister) {
109
+ warnings.push('mismatch-register.jsonl not found, skipping mismatch incorporation');
110
+ }
111
+
112
+ // Counters
113
+ let explained = 0;
114
+ let xstateValidated = 0;
115
+ let methodologySkips = 0;
116
+ let vocabularyMapped = 0;
117
+
118
+ const unexplainedCounts = { instrumentation_bug: 0, model_gap: 0, genuine_violation: 0 };
119
+ const unexplainedActions = { instrumentation_bug: {}, model_gap: {}, genuine_violation: {} };
120
+ const violatedInvariants = {};
121
+
122
+ // Simple session context tracking for genuine violation checks
123
+ // We track per-session whether quorum_start has been seen
124
+ let sessionContext = { seenQuorumStart: false };
125
+
126
+ for (let i = 0; i < conformanceEvents.length; i++) {
127
+ const event = conformanceEvents[i];
128
+
129
+ // Reset session context on IDLE phase events
130
+ if (event.phase === 'IDLE' && event.action === 'quorum_start') {
131
+ sessionContext = { seenQuorumStart: true };
132
+ }
133
+
134
+ // Step 1: Is the action in the vocabulary?
135
+ const inVocab = vocabActions.has(event.action);
136
+
137
+ if (!inVocab) {
138
+ // NOT in vocabulary -> instrumentation_bug
139
+ unexplainedCounts.instrumentation_bug++;
140
+ unexplainedActions.instrumentation_bug[event.action] = (unexplainedActions.instrumentation_bug[event.action] || 0) + 1;
141
+ continue;
142
+ }
143
+
144
+ vocabularyMapped++;
145
+
146
+ // Step 2: Map to XState event
147
+ const xstateEvent = mapToXStateEvent(event);
148
+ if (!xstateEvent) {
149
+ // In vocab but no XState mapping -> instrumentation_bug
150
+ unexplainedCounts.instrumentation_bug++;
151
+ unexplainedActions.instrumentation_bug[event.action + ':no_xstate_map'] = (unexplainedActions.instrumentation_bug[event.action + ':no_xstate_map'] || 0) + 1;
152
+ continue;
153
+ }
154
+
155
+ // Step 3: H1 methodology skip?
156
+ if (isMethodologySkip(event)) {
157
+ methodologySkips++;
158
+ explained++;
159
+ continue;
160
+ }
161
+
162
+ // Step 4: Fresh actor replay
163
+ const actor = createActor(nfWorkflowMachine);
164
+ actor.start();
165
+ const beforeState = actor.getSnapshot().value;
166
+ actor.send(xstateEvent);
167
+ const afterState = actor.getSnapshot().value;
168
+ actor.stop();
169
+
170
+ // Check if the transition was accepted (state changed or self-loop is valid)
171
+ // A "valid" transition means the machine processed it without error
172
+ // For fresh-from-IDLE replay, the key test is: did the machine move to a state
173
+ // consistent with the event type?
174
+ const stateStr = typeof afterState === 'string' ? afterState : JSON.stringify(afterState);
175
+
176
+ // Consider the transition valid if the machine accepted the event
177
+ // (any state change from IDLE or valid self-loop like CIRCUIT_BREAK)
178
+ const transitionAccepted = (stateStr !== 'IDLE' || xstateEvent.type === 'CIRCUIT_BREAK');
179
+
180
+ if (transitionAccepted) {
181
+ xstateValidated++;
182
+ explained++;
183
+ } else {
184
+ // model_gap: in vocab, mapped, but replay fails
185
+ // Check for genuine_violation reclassification
186
+ const violatedInvariant = checkGenuineViolation(event, observedInvariants, sessionContext);
187
+ if (violatedInvariant) {
188
+ unexplainedCounts.genuine_violation++;
189
+ unexplainedActions.genuine_violation[event.action] = (unexplainedActions.genuine_violation[event.action] || 0) + 1;
190
+ violatedInvariants[violatedInvariant] = (violatedInvariants[violatedInvariant] || 0) + 1;
191
+ } else {
192
+ unexplainedCounts.model_gap++;
193
+ unexplainedActions.model_gap[event.action] = (unexplainedActions.model_gap[event.action] || 0) + 1;
194
+ }
195
+ }
196
+ }
197
+
198
+ const total = conformanceEvents.length;
199
+ const groundingScore = total > 0 ? explained / total : 0;
200
+
201
+ // Build top actions lists for unexplained summary
202
+ const topActions = (obj) => Object.entries(obj).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([k, v]) => ({ action: k, count: v }));
203
+
204
+ return {
205
+ schema_version: '1',
206
+ generated: new Date().toISOString(),
207
+ grounding_score: groundingScore,
208
+ target: 0.80,
209
+ target_met: groundingScore >= 0.80,
210
+ explained,
211
+ total,
212
+ unexplained_counts: unexplainedCounts,
213
+ unexplained_summary: {
214
+ instrumentation_bug: { top_actions: topActions(unexplainedActions.instrumentation_bug), total: unexplainedCounts.instrumentation_bug },
215
+ model_gap: { top_mismatches: topActions(unexplainedActions.model_gap), total: unexplainedCounts.model_gap },
216
+ genuine_violation: { violated_invariants: Object.entries(violatedInvariants).map(([k, v]) => ({ invariant: k, count: v })), total: unexplainedCounts.genuine_violation }
217
+ },
218
+ methodology: {
219
+ explains_definition: 'vocabulary_mapped AND (xstate_valid OR methodology_skip)',
220
+ h1_methodology_skips: methodologySkips,
221
+ xstate_validated: xstateValidated,
222
+ vocabulary_mapped: vocabularyMapped
223
+ },
224
+ warnings
225
+ };
226
+ }
227
+
228
+ // ── CLI ─────────────────────────────────────────────────────────────────────
229
+
230
+ if (require.main === module) {
231
+ // Read conformance events
232
+ const pp = require(path.join(__dirname, 'planning-paths.cjs'));
233
+ const logPath = pp.resolveWithFallback(ROOT, 'conformance-events');
234
+ let conformanceEvents = [];
235
+ if (fs.existsSync(logPath)) {
236
+ const raw = fs.readFileSync(logPath, 'utf8');
237
+ const lines = raw.split('\n').filter(l => l.trim().length > 0);
238
+ for (const line of lines) {
239
+ try { conformanceEvents.push(JSON.parse(line)); } catch (_) { /* skip */ }
240
+ }
241
+ }
242
+
243
+ // Read vocabulary
244
+ const vocabPath = path.join(FORMAL, 'evidence', 'event-vocabulary.json');
245
+ let vocabulary = null;
246
+ if (fs.existsSync(vocabPath)) {
247
+ try { vocabulary = JSON.parse(fs.readFileSync(vocabPath, 'utf8')); } catch (_) { /* fail-open */ }
248
+ }
249
+
250
+ // Try to read invariant catalog (graceful degradation)
251
+ let invariantCatalog = null;
252
+ const invCatPath = path.join(FORMAL, 'semantics', 'invariant-catalog.json');
253
+ if (fs.existsSync(invCatPath)) {
254
+ try {
255
+ const parsed = JSON.parse(fs.readFileSync(invCatPath, 'utf8'));
256
+ if (parsed.schema_version && Array.isArray(parsed.invariants)) {
257
+ invariantCatalog = parsed;
258
+ } else {
259
+ process.stderr.write('WARNING: invariant-catalog.json has invalid schema, skipping\n');
260
+ }
261
+ } catch (_) {
262
+ process.stderr.write('WARNING: invariant-catalog.json parse error, skipping\n');
263
+ }
264
+ } else {
265
+ process.stderr.write('WARNING: invariant-catalog.json not found, skipping genuine_violation reclassification\n');
266
+ }
267
+
268
+ // Try to read mismatch register (graceful degradation)
269
+ let mismatchRegister = null;
270
+ const mmPath = path.join(FORMAL, 'semantics', 'mismatch-register.jsonl');
271
+ if (fs.existsSync(mmPath)) {
272
+ try {
273
+ const lines = fs.readFileSync(mmPath, 'utf8').trim().split('\n').filter(l => l.trim());
274
+ mismatchRegister = lines.map(l => {
275
+ const parsed = JSON.parse(l);
276
+ if (!parsed.resolution) throw new Error('Missing resolution field');
277
+ return parsed;
278
+ });
279
+ } catch (e) {
280
+ process.stderr.write('WARNING: mismatch-register.jsonl parse error, skipping mismatch incorporation\n');
281
+ }
282
+ } else {
283
+ process.stderr.write('WARNING: mismatch-register.jsonl not found, skipping mismatch incorporation\n');
284
+ }
285
+
286
+ const result = computeGateA(conformanceEvents, vocabulary, invariantCatalog, mismatchRegister);
287
+
288
+ // EARLY WARNING check
289
+ if (result.grounding_score < 0.50) {
290
+ process.stderr.write(`\nEARLY WARNING: grounding_score is ${(result.grounding_score * 100).toFixed(1)}% (< 50%). Re-examine explains definition!\n`);
291
+ }
292
+
293
+ // Write output
294
+ fs.mkdirSync(GATES_DIR, { recursive: true });
295
+ fs.writeFileSync(OUT_FILE, JSON.stringify(result, null, 2) + '\n');
296
+
297
+ // Emit check result via write-check-result
298
+ try {
299
+ const { writeCheckResult } = require(path.join(__dirname, 'write-check-result.cjs'));
300
+ const startTime = Date.now();
301
+ writeCheckResult({
302
+ tool: 'gate-a-grounding',
303
+ formalism: 'trace',
304
+ result: result.target_met ? 'pass' : 'fail',
305
+ check_id: 'gate-a:grounding-score',
306
+ surface: 'trace',
307
+ property: `Gate A grounding score: ${(result.grounding_score * 100).toFixed(1)}% (target: 80%)`,
308
+ runtime_ms: Date.now() - startTime,
309
+ summary: `${result.target_met ? 'pass' : 'fail'}: grounding ${(result.grounding_score * 100).toFixed(1)}%, ${result.explained}/${result.total} explained`,
310
+ requirement_ids: ['GATE-01'],
311
+ metadata: { grounding_score: result.grounding_score, target: result.target, target_met: result.target_met }
312
+ });
313
+ } catch (e) {
314
+ process.stderr.write('WARNING: Could not write check result: ' + e.message + '\n');
315
+ }
316
+
317
+ if (JSON_FLAG) {
318
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
319
+ } else {
320
+ console.log(`Gate A Grounding Score: ${(result.grounding_score * 100).toFixed(1)}%`);
321
+ console.log(` Target: >= 80% | Met: ${result.target_met}`);
322
+ console.log(` Explained: ${result.explained} / ${result.total}`);
323
+ console.log(` Unexplained: instrumentation_bug=${result.unexplained_counts.instrumentation_bug} model_gap=${result.unexplained_counts.model_gap} genuine_violation=${result.unexplained_counts.genuine_violation}`);
324
+ console.log(` Methodology: xstate_validated=${result.methodology.xstate_validated} h1_skips=${result.methodology.h1_methodology_skips} vocab_mapped=${result.methodology.vocabulary_mapped}`);
325
+ if (result.warnings.length > 0) {
326
+ console.log(` Warnings: ${result.warnings.join('; ')}`);
327
+ }
328
+ console.log(` Output: ${OUT_FILE}`);
329
+ }
330
+
331
+ process.exit(0);
332
+ }
333
+
334
+ module.exports = { computeGateA, isMethodologySkip, checkGenuineViolation };
@@ -0,0 +1,243 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * gate-b-abstraction.cjs — Gate B L2-L3 traceability verification.
6
+ *
7
+ * Verifies every L3 reasoning artifact entry has valid derived_from links
8
+ * to L2 semantics sources. Reports orphaned hazards.
9
+ *
10
+ * Requirements: GATE-02
11
+ *
12
+ * Usage:
13
+ * node bin/gate-b-abstraction.cjs # print summary to stdout
14
+ * node bin/gate-b-abstraction.cjs --json # print full results JSON to stdout
15
+ */
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+
20
+ const ROOT = process.env.PROJECT_ROOT || path.join(__dirname, '..');
21
+ const FORMAL = path.join(ROOT, '.planning', 'formal');
22
+ const GATES_DIR = path.join(FORMAL, 'gates');
23
+ const REASONING_DIR = path.join(FORMAL, 'reasoning');
24
+ const SEMANTICS_DIR = path.join(FORMAL, 'semantics');
25
+ const EVIDENCE_DIR = path.join(FORMAL, 'evidence');
26
+ const OUT_FILE = path.join(GATES_DIR, 'gate-b-abstraction.json');
27
+
28
+ const JSON_FLAG = process.argv.includes('--json');
29
+
30
+ // ── L2 artifact cache ──────────────────────────────────────────────────────
31
+
32
+ const l2Cache = {};
33
+
34
+ function loadL2Artifact(artifactRelPath) {
35
+ if (l2Cache[artifactRelPath]) return l2Cache[artifactRelPath];
36
+
37
+ const fullPath = path.join(FORMAL, artifactRelPath);
38
+ if (!fs.existsSync(fullPath)) return null;
39
+
40
+ let data;
41
+ if (fullPath.endsWith('.jsonl')) {
42
+ data = fs.readFileSync(fullPath, 'utf8')
43
+ .trim().split('\n').filter(Boolean).map(line => JSON.parse(line));
44
+ } else {
45
+ data = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
46
+ }
47
+
48
+ l2Cache[artifactRelPath] = data;
49
+ return data;
50
+ }
51
+
52
+ // ── Link resolution ─────────────────────────────────────────────────────────
53
+
54
+ /**
55
+ * Resolve a derived_from link by loading the referenced L2 artifact and
56
+ * navigating to the ref path. Returns true if the link is valid.
57
+ */
58
+ function resolveL2Link(link) {
59
+ if (!link || !link.artifact || !link.ref) return false;
60
+
61
+ const data = loadL2Artifact(link.artifact);
62
+ if (data === null) return false;
63
+
64
+ // For JSONL (mismatch-register), just verify the file loaded (array)
65
+ if (link.artifact.endsWith('.jsonl')) {
66
+ return Array.isArray(data) && data.length > 0;
67
+ }
68
+
69
+ // For L3 self-references (reasoning/ artifacts), verify file exists
70
+ if (link.layer === 'L3') {
71
+ const l3Path = path.join(FORMAL, link.artifact);
72
+ return fs.existsSync(l3Path);
73
+ }
74
+
75
+ const ref = link.ref;
76
+
77
+ // Handle array filter refs: invariants[name=X,config=Y] or invariants[config=X]
78
+ const arrayFilterMatch = ref.match(/^(\w+)\[(.+)\]$/);
79
+ if (arrayFilterMatch) {
80
+ const arrayKey = arrayFilterMatch[1];
81
+ const filterStr = arrayFilterMatch[2];
82
+ const arr = data[arrayKey];
83
+ if (!Array.isArray(arr)) return false;
84
+
85
+ // Parse filter conditions: key=value pairs
86
+ const conditions = filterStr.split(',').map(c => {
87
+ const [k, v] = c.trim().split('=');
88
+ return { key: k, value: v };
89
+ });
90
+
91
+ // Find at least one item matching all conditions
92
+ return arr.some(item =>
93
+ conditions.every(cond => String(item[cond.key]) === cond.value)
94
+ );
95
+ }
96
+
97
+ // Handle dot-path navigation: observed_transitions.IDLE.QUORUM_START
98
+ const parts = ref.split('.');
99
+ let current = data;
100
+ for (const part of parts) {
101
+ // Handle wildcard: sessions[*].actions
102
+ if (part === '*' || part.includes('[*]')) {
103
+ // For wildcard refs, just check the parent array/object exists
104
+ return current !== undefined && current !== null;
105
+ }
106
+ if (current === undefined || current === null) return false;
107
+ if (typeof current !== 'object') return false;
108
+ current = current[part];
109
+ }
110
+
111
+ return current !== undefined && current !== null;
112
+ }
113
+
114
+ // ── Gate B check ────────────────────────────────────────────────────────────
115
+
116
+ function checkGateB(l3Artifacts) {
117
+ let totalEntries = 0;
118
+ let groundedEntries = 0;
119
+ const orphans = [];
120
+
121
+ for (const { name, entries } of l3Artifacts) {
122
+ for (const entry of entries) {
123
+ totalEntries++;
124
+
125
+ if (!entry.derived_from || !Array.isArray(entry.derived_from) || entry.derived_from.length === 0) {
126
+ orphans.push({
127
+ artifact: name,
128
+ entry_id: entry.id || 'unknown',
129
+ reason: 'missing or empty derived_from',
130
+ });
131
+ continue;
132
+ }
133
+
134
+ let allValid = true;
135
+ for (const link of entry.derived_from) {
136
+ if (!resolveL2Link(link)) {
137
+ allValid = false;
138
+ orphans.push({
139
+ artifact: name,
140
+ entry_id: entry.id || 'unknown',
141
+ reason: `broken link: ${link.artifact}#${link.ref}`,
142
+ });
143
+ break; // Report first broken link only
144
+ }
145
+ }
146
+
147
+ if (allValid) groundedEntries++;
148
+ }
149
+ }
150
+
151
+ const gateBScore = totalEntries > 0 ? groundedEntries / totalEntries : 0;
152
+
153
+ return {
154
+ schema_version: '1',
155
+ generated: new Date().toISOString(),
156
+ gate_b_score: Math.round(gateBScore * 10000) / 10000, // 4 decimal places
157
+ total_entries: totalEntries,
158
+ grounded_entries: groundedEntries,
159
+ orphaned_entries: orphans.length,
160
+ orphans,
161
+ target: 1.0,
162
+ target_met: gateBScore >= 1.0,
163
+ };
164
+ }
165
+
166
+ // ── Entry point ─────────────────────────────────────────────────────────────
167
+
168
+ function main() {
169
+ // Load L3 reasoning artifacts
170
+ const l3Artifacts = [];
171
+
172
+ const hazardPath = path.join(REASONING_DIR, 'hazard-model.json');
173
+ if (fs.existsSync(hazardPath)) {
174
+ const hm = JSON.parse(fs.readFileSync(hazardPath, 'utf8'));
175
+ l3Artifacts.push({ name: 'hazard-model.json', entries: hm.hazards || [] });
176
+ }
177
+
178
+ const fmPath = path.join(REASONING_DIR, 'failure-mode-catalog.json');
179
+ if (fs.existsSync(fmPath)) {
180
+ const fm = JSON.parse(fs.readFileSync(fmPath, 'utf8'));
181
+ l3Artifacts.push({ name: 'failure-mode-catalog.json', entries: fm.failure_modes || [] });
182
+ }
183
+
184
+ const rhPath = path.join(REASONING_DIR, 'risk-heatmap.json');
185
+ if (fs.existsSync(rhPath)) {
186
+ const rh = JSON.parse(fs.readFileSync(rhPath, 'utf8'));
187
+ l3Artifacts.push({ name: 'risk-heatmap.json', entries: rh.transitions || [] });
188
+ }
189
+
190
+ if (l3Artifacts.length === 0) {
191
+ console.error('ERROR: No L3 reasoning artifacts found in', REASONING_DIR);
192
+ process.exit(1);
193
+ }
194
+
195
+ const result = checkGateB(l3Artifacts);
196
+
197
+ // Write output
198
+ fs.mkdirSync(GATES_DIR, { recursive: true });
199
+ fs.writeFileSync(OUT_FILE, JSON.stringify(result, null, 2) + '\n');
200
+
201
+ // Emit check result
202
+ try {
203
+ const { writeCheckResult } = require(path.join(__dirname, 'write-check-result.cjs'));
204
+ writeCheckResult({
205
+ tool: 'gate-b-abstraction',
206
+ formalism: 'trace',
207
+ result: result.target_met ? 'pass' : 'fail',
208
+ check_id: 'gate-b:abstraction-score',
209
+ surface: 'trace',
210
+ property: `Gate B traceability score: ${(result.gate_b_score * 100).toFixed(1)}% (target: 100%)`,
211
+ runtime_ms: 0,
212
+ summary: `${result.target_met ? 'pass' : 'fail'}: traceability ${(result.gate_b_score * 100).toFixed(1)}%, ${result.grounded_entries}/${result.total_entries} grounded`,
213
+ requirement_ids: ['GATE-02'],
214
+ metadata: { gate_b_score: result.gate_b_score, target: result.target, target_met: result.target_met },
215
+ });
216
+ } catch (e) {
217
+ // write-check-result.cjs not available; skip
218
+ }
219
+
220
+ if (JSON_FLAG) {
221
+ process.stdout.write(JSON.stringify(result));
222
+ } else {
223
+ console.log(`Gate B: L2-L3 Traceability`);
224
+ console.log(` Score: ${(result.gate_b_score * 100).toFixed(1)}%`);
225
+ console.log(` Total entries: ${result.total_entries}`);
226
+ console.log(` Grounded: ${result.grounded_entries}`);
227
+ console.log(` Orphaned: ${result.orphaned_entries}`);
228
+ console.log(` Target met: ${result.target_met}`);
229
+ if (result.orphans.length > 0) {
230
+ console.log(` Orphan details:`);
231
+ for (const o of result.orphans) {
232
+ console.log(` - ${o.artifact} / ${o.entry_id}: ${o.reason}`);
233
+ }
234
+ }
235
+ console.log(` Output: ${OUT_FILE}`);
236
+ }
237
+
238
+ process.exit(result.target_met ? 0 : 1);
239
+ }
240
+
241
+ if (require.main === module) main();
242
+
243
+ module.exports = { checkGateB, resolveL2Link, main };