@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,324 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * observed-fsm.cjs — Derives an observed-behavior FSM from traces using
6
+ * dual-mode replay (per-event isolation + per-session chains).
7
+ *
8
+ * Requirements: SEM-04
9
+ *
10
+ * Usage:
11
+ * node bin/observed-fsm.cjs # print summary to stdout
12
+ * node bin/observed-fsm.cjs --json # print full FSM JSON to stdout
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ const ROOT = process.env.PROJECT_ROOT || path.join(__dirname, '..');
19
+ const FORMAL = path.join(ROOT, '.planning', 'formal');
20
+ const OUT_DIR = path.join(FORMAL, 'semantics');
21
+ const OUT_FILE = path.join(OUT_DIR, 'observed-fsm.json');
22
+
23
+ const JSON_FLAG = process.argv.includes('--json');
24
+
25
+ // ── Helpers ─────────────────────────────────────────────────────────────────
26
+
27
+ function stateToString(value) {
28
+ if (typeof value === 'string') return value;
29
+ if (typeof value === 'object' && value !== null) return JSON.stringify(value);
30
+ return String(value);
31
+ }
32
+
33
+ // ── Model extraction ────────────────────────────────────────────────────────
34
+
35
+ function extractModelTransitions() {
36
+ const machinePath = (() => {
37
+ const repoDist = path.join(__dirname, '..', 'dist', 'machines', 'nf-workflow.machine.js');
38
+ const installDist = path.join(__dirname, 'dist', 'machines', 'nf-workflow.machine.js');
39
+ return fs.existsSync(repoDist) ? repoDist : installDist;
40
+ })();
41
+ const { nfWorkflowMachine } = require(machinePath);
42
+
43
+ const config = nfWorkflowMachine.config;
44
+ const transitions = [];
45
+
46
+ for (const [stateName, stateDef] of Object.entries(config.states)) {
47
+ const on = stateDef.on || {};
48
+ for (const [eventName, targets] of Object.entries(on)) {
49
+ if (Array.isArray(targets)) {
50
+ for (const t of targets) {
51
+ if (t.target) transitions.push({ from: stateName, event: eventName, to: t.target });
52
+ }
53
+ } else if (typeof targets === 'string') {
54
+ transitions.push({ from: stateName, event: eventName, to: targets });
55
+ } else if (targets && targets.target) {
56
+ transitions.push({ from: stateName, event: eventName, to: targets.target });
57
+ }
58
+ }
59
+ }
60
+
61
+ return { states: Object.keys(config.states), transitions };
62
+ }
63
+
64
+ // ── Mode A: Per-event isolation ─────────────────────────────────────────────
65
+
66
+ function replayPerEvent(conformanceEvents, mapToXStateEvent, createActor, nfWorkflowMachine) {
67
+ const adjacency = {}; // { fromState: { eventType: { toState, count } } }
68
+ let totalMapped = 0;
69
+
70
+ for (const event of conformanceEvents) {
71
+ const xstateEvent = mapToXStateEvent(event);
72
+ if (!xstateEvent) continue;
73
+ totalMapped++;
74
+
75
+ const actor = createActor(nfWorkflowMachine);
76
+ actor.start();
77
+ const beforeState = stateToString(actor.getSnapshot().value);
78
+ actor.send(xstateEvent);
79
+ const afterState = stateToString(actor.getSnapshot().value);
80
+ actor.stop();
81
+
82
+ if (!adjacency[beforeState]) adjacency[beforeState] = {};
83
+ const key = xstateEvent.type;
84
+ if (!adjacency[beforeState][key]) {
85
+ adjacency[beforeState][key] = { to_state: afterState, count: 0 };
86
+ }
87
+ adjacency[beforeState][key].count++;
88
+ }
89
+
90
+ return { adjacency, totalMapped };
91
+ }
92
+
93
+ // ── Mode B: Per-session running actor ───────────────────────────────────────
94
+
95
+ function replayPerSession(conformanceEvents, traceStats, mapToXStateEvent, createActor, nfWorkflowMachine) {
96
+ const adjacency = {};
97
+ let sessionsReplayed = 0;
98
+
99
+ const sessions = (traceStats && traceStats.sessions) || [];
100
+ for (const session of sessions) {
101
+ const sessionStart = new Date(session.start).getTime();
102
+ const sessionEnd = new Date(session.end).getTime();
103
+ const sessionEvents = conformanceEvents.filter(e => {
104
+ const t = new Date(e.ts).getTime();
105
+ return t >= sessionStart && t <= sessionEnd;
106
+ });
107
+
108
+ if (sessionEvents.length === 0) continue;
109
+
110
+ const actor = createActor(nfWorkflowMachine);
111
+ actor.start();
112
+ sessionsReplayed++;
113
+
114
+ for (const event of sessionEvents) {
115
+ const xstateEvent = mapToXStateEvent(event);
116
+ if (!xstateEvent) continue;
117
+
118
+ const beforeState = stateToString(actor.getSnapshot().value);
119
+ actor.send(xstateEvent);
120
+ const afterState = stateToString(actor.getSnapshot().value);
121
+
122
+ const key = xstateEvent.type;
123
+ if (!adjacency[beforeState]) adjacency[beforeState] = {};
124
+ if (!adjacency[beforeState][key]) {
125
+ adjacency[beforeState][key] = { to_state: afterState, count: 0 };
126
+ }
127
+ adjacency[beforeState][key].count++;
128
+ }
129
+
130
+ actor.stop();
131
+ }
132
+
133
+ return { adjacency, sessionsReplayed };
134
+ }
135
+
136
+ // ── Merge ───────────────────────────────────────────────────────────────────
137
+
138
+ function mergeAdjacency(perEvent, perSession) {
139
+ const merged = {};
140
+
141
+ // Add per-event transitions
142
+ for (const [from, events] of Object.entries(perEvent)) {
143
+ if (!merged[from]) merged[from] = {};
144
+ for (const [event, data] of Object.entries(events)) {
145
+ merged[from][event] = { ...data, source: 'per_event' };
146
+ }
147
+ }
148
+
149
+ // Merge per-session transitions
150
+ for (const [from, events] of Object.entries(perSession)) {
151
+ if (!merged[from]) merged[from] = {};
152
+ for (const [event, data] of Object.entries(events)) {
153
+ if (merged[from][event]) {
154
+ // Both modes have this transition
155
+ merged[from][event].source = 'both';
156
+ // Keep per-event count as authoritative
157
+ } else {
158
+ merged[from][event] = { ...data, source: 'per_session' };
159
+ }
160
+ }
161
+ }
162
+
163
+ return merged;
164
+ }
165
+
166
+ // ── Model comparison ────────────────────────────────────────────────────────
167
+
168
+ function compareWithModel(mergedAdjacency, modelTransitions) {
169
+ const observedSet = new Set();
170
+ for (const [from, events] of Object.entries(mergedAdjacency)) {
171
+ for (const [event, data] of Object.entries(events)) {
172
+ observedSet.add(`${from}::${event}::${data.to_state}`);
173
+ }
174
+ }
175
+
176
+ const modelSet = new Set();
177
+ for (const t of modelTransitions) {
178
+ modelSet.add(`${t.from}::${t.event}::${t.to}`);
179
+ }
180
+
181
+ const matching = [];
182
+ const missingInObserved = [];
183
+ const missingInModel = [];
184
+
185
+ for (const key of modelSet) {
186
+ const [from, event, to] = key.split('::');
187
+ if (observedSet.has(key)) {
188
+ matching.push({ from, event, to });
189
+ } else {
190
+ missingInObserved.push({ from, event, to });
191
+ }
192
+ }
193
+
194
+ for (const key of observedSet) {
195
+ if (!modelSet.has(key)) {
196
+ const [from, event, to] = key.split('::');
197
+ missingInModel.push({ from, event, to });
198
+ }
199
+ }
200
+
201
+ return { matching, missing_in_observed: missingInObserved, missing_in_model: missingInModel };
202
+ }
203
+
204
+ // ── Build FSM ───────────────────────────────────────────────────────────────
205
+
206
+ function buildObservedFSM(conformanceEvents, traceStats) {
207
+ const { mapToXStateEvent } = require(path.join(__dirname, 'validate-traces.cjs'));
208
+ const machinePath = (() => {
209
+ const repoDist = path.join(__dirname, '..', 'dist', 'machines', 'nf-workflow.machine.js');
210
+ const installDist = path.join(__dirname, 'dist', 'machines', 'nf-workflow.machine.js');
211
+ return fs.existsSync(repoDist) ? repoDist : installDist;
212
+ })();
213
+ const { createActor, nfWorkflowMachine } = require(machinePath);
214
+
215
+ // Mode A: Per-event isolation
216
+ const perEventResult = replayPerEvent(conformanceEvents, mapToXStateEvent, createActor, nfWorkflowMachine);
217
+
218
+ // Mode B: Per-session running actor
219
+ const perSessionResult = replayPerSession(conformanceEvents, traceStats, mapToXStateEvent, createActor, nfWorkflowMachine);
220
+
221
+ // Merge
222
+ const mergedAdjacency = mergeAdjacency(perEventResult.adjacency, perSessionResult.adjacency);
223
+
224
+ // Extract observed states
225
+ const statesObserved = new Set();
226
+ for (const [from, events] of Object.entries(mergedAdjacency)) {
227
+ statesObserved.add(from);
228
+ for (const data of Object.values(events)) {
229
+ statesObserved.add(data.to_state);
230
+ }
231
+ }
232
+
233
+ // Model comparison
234
+ const model = extractModelTransitions();
235
+ const comparison = compareWithModel(mergedAdjacency, model.transitions);
236
+
237
+ // Coverage metrics
238
+ const totalEvents = conformanceEvents.length;
239
+ const mappedEvents = perEventResult.totalMapped;
240
+ const unmappedEvents = totalEvents - mappedEvents;
241
+ const modelTransitionCount = model.transitions.length;
242
+ const exercisedCount = comparison.matching.length;
243
+
244
+ // Count per-event and per-session unique transitions
245
+ const perEventTransitions = new Set();
246
+ for (const [from, events] of Object.entries(perEventResult.adjacency)) {
247
+ for (const [event, data] of Object.entries(events)) {
248
+ perEventTransitions.add(`${from}::${event}::${data.to_state}`);
249
+ }
250
+ }
251
+ const perSessionTransitions = new Set();
252
+ for (const [from, events] of Object.entries(perSessionResult.adjacency)) {
253
+ for (const [event, data] of Object.entries(events)) {
254
+ perSessionTransitions.add(`${from}::${event}::${data.to_state}`);
255
+ }
256
+ }
257
+
258
+ return {
259
+ schema_version: '1',
260
+ generated: new Date().toISOString(),
261
+ observed_transitions: mergedAdjacency,
262
+ states_observed: [...statesObserved].sort(),
263
+ model_comparison: comparison,
264
+ coverage: {
265
+ model_coverage: modelTransitionCount > 0 ? exercisedCount / modelTransitionCount : 0,
266
+ vocabulary_coverage: totalEvents > 0 ? mappedEvents / totalEvents : 0,
267
+ total_events: totalEvents,
268
+ mapped_events: mappedEvents,
269
+ unmapped_events: unmappedEvents
270
+ },
271
+ replay_modes: {
272
+ per_event_transitions: perEventTransitions.size,
273
+ per_session_transitions: perSessionTransitions.size,
274
+ sessions_replayed: perSessionResult.sessionsReplayed
275
+ }
276
+ };
277
+ }
278
+
279
+ // ── CLI ─────────────────────────────────────────────────────────────────────
280
+
281
+ if (require.main === module) {
282
+ // Read conformance events
283
+ const pp = require(path.join(__dirname, 'planning-paths.cjs'));
284
+ const logPath = pp.resolveWithFallback(ROOT, 'conformance-events');
285
+ let conformanceEvents = [];
286
+ if (fs.existsSync(logPath)) {
287
+ const raw = fs.readFileSync(logPath, 'utf8');
288
+ const lines = raw.split('\n').filter(l => l.trim().length > 0);
289
+ for (const line of lines) {
290
+ try { conformanceEvents.push(JSON.parse(line)); } catch (_) { /* skip */ }
291
+ }
292
+ }
293
+
294
+ // Read trace stats
295
+ const statsPath = path.join(FORMAL, 'evidence', 'trace-corpus-stats.json');
296
+ let traceStats = null;
297
+ if (fs.existsSync(statsPath)) {
298
+ traceStats = JSON.parse(fs.readFileSync(statsPath, 'utf8'));
299
+ }
300
+
301
+ const fsm = buildObservedFSM(conformanceEvents, traceStats);
302
+
303
+ fs.mkdirSync(OUT_DIR, { recursive: true });
304
+ fs.writeFileSync(OUT_FILE, JSON.stringify(fsm, null, 2) + '\n');
305
+
306
+ if (JSON_FLAG) {
307
+ process.stdout.write(JSON.stringify(fsm, null, 2) + '\n');
308
+ } else {
309
+ console.log(`Observed FSM written to ${path.relative(ROOT, OUT_FILE)}`);
310
+ console.log(` States observed: ${fsm.states_observed.length} (${fsm.states_observed.join(', ')})`);
311
+ console.log(` Model coverage: ${(fsm.coverage.model_coverage * 100).toFixed(1)}%`);
312
+ console.log(` Vocabulary coverage: ${(fsm.coverage.vocabulary_coverage * 100).toFixed(1)}%`);
313
+ console.log(` Events: ${fsm.coverage.total_events} total, ${fsm.coverage.mapped_events} mapped, ${fsm.coverage.unmapped_events} unmapped`);
314
+ console.log(` Replay modes: per_event=${fsm.replay_modes.per_event_transitions} per_session=${fsm.replay_modes.per_session_transitions} sessions=${fsm.replay_modes.sessions_replayed}`);
315
+ console.log(` Model comparison: matching=${fsm.model_comparison.matching.length} missing_in_observed=${fsm.model_comparison.missing_in_observed.length} missing_in_model=${fsm.model_comparison.missing_in_model.length}`);
316
+ }
317
+
318
+ process.exit(0);
319
+ }
320
+
321
+ module.exports = {
322
+ buildObservedFSM, extractModelTransitions, replayPerEvent, replayPerSession,
323
+ mergeAdjacency, compareWithModel, stateToString
324
+ };
@@ -84,6 +84,12 @@ const TYPES = {
84
84
  legacy: (root, p) => path.join(root, '.planning', `${p.version}-INTEGRATION-REPORT.md`),
85
85
  },
86
86
 
87
+ // Quorum cache
88
+ 'quorum-cache': {
89
+ canonical: (root) => path.join(root, '.planning', '.quorum-cache'),
90
+ legacy: (root) => path.join(root, '.planning', '.quorum-cache'),
91
+ },
92
+
87
93
  // State backups
88
94
  'state-backup': {
89
95
  canonical: (root, p) => path.join(root, '.planning', 'archive', 'state-backups', `STATE.md.bak-${p.timestamp}`),
package/bin/polyrepo.cjs CHANGED
@@ -5,7 +5,7 @@ const fs = require('fs');
5
5
  const path = require('path');
6
6
  const os = require('os');
7
7
 
8
- const TAG = '[qgsd-polyrepo]';
8
+ const TAG = '[nf-polyrepo]';
9
9
  const POLYREPOS_DIR = path.join(os.homedir(), '.claude', 'polyrepos');
10
10
  const MARKER_FILE = 'polyrepo.json';
11
11
 
@@ -27,7 +27,7 @@ const os = require('os');
27
27
  function findProviders() {
28
28
  const searchPaths = [
29
29
  path.join(__dirname, 'providers.json'),
30
- path.join(os.homedir(), '.claude', 'qgsd-bin', 'providers.json'),
30
+ path.join(os.homedir(), '.claude', 'nf-bin', 'providers.json'),
31
31
  ];
32
32
  try {
33
33
  const claudeJson = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.claude.json'), 'utf8'));
@@ -0,0 +1,274 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * promote-gate-maturity.cjs — Gate maturity level promotion CLI.
6
+ *
7
+ * Manages per-model gate enforcement levels: ADVISORY -> SOFT_GATE -> HARD_GATE.
8
+ *
9
+ * Requirements: GATE-04
10
+ *
11
+ * Usage:
12
+ * node bin/promote-gate-maturity.cjs --model "alloy/quorum-votes.als" --level SOFT_GATE
13
+ * node bin/promote-gate-maturity.cjs --check # validate all models
14
+ * node bin/promote-gate-maturity.cjs --check --fix # demote violating models
15
+ * node bin/promote-gate-maturity.cjs --json # JSON output mode
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 REGISTRY_PATH = path.join(FORMAL, 'model-registry.json');
24
+ const CHECK_RESULTS_PATH = path.join(FORMAL, 'check-results.ndjson');
25
+
26
+ const JSON_FLAG = process.argv.includes('--json');
27
+ const CHECK_FLAG = process.argv.includes('--check');
28
+ const FIX_FLAG = process.argv.includes('--fix');
29
+
30
+ const LEVELS = ['ADVISORY', 'SOFT_GATE', 'HARD_GATE'];
31
+ const LEVEL_RANK = { ADVISORY: 0, SOFT_GATE: 1, HARD_GATE: 2 };
32
+
33
+ // ── Helpers ─────────────────────────────────────────────────────────────────
34
+
35
+ function getModelKeys(registry) {
36
+ return Object.keys(registry).filter(k => k.startsWith('.'));
37
+ }
38
+
39
+ function getModelLevel(model) {
40
+ return model.gate_maturity || 'ADVISORY';
41
+ }
42
+
43
+ /**
44
+ * Infer source_layer from the model path.
45
+ */
46
+ function inferSourceLayer(modelPath) {
47
+ if (modelPath.includes('evidence/') || modelPath.includes('L1') || modelPath.includes('conformance')) return 'L1';
48
+ if (modelPath.includes('semantics/') || modelPath.includes('L2')) return 'L2';
49
+ if (modelPath.includes('reasoning/') || modelPath.includes('L3')) return 'L3';
50
+ // TLA+ and Alloy models are L2 by default (formal specs of the operational model)
51
+ if (modelPath.endsWith('.tla') || modelPath.endsWith('.als') || modelPath.endsWith('.props')) return 'L2';
52
+ return null;
53
+ }
54
+
55
+ // ── Validation criteria ─────────────────────────────────────────────────────
56
+
57
+ /**
58
+ * Validate that a model meets its current (or target) gate_maturity criteria.
59
+ * Returns { valid: boolean, reason: string }
60
+ */
61
+ function validateCriteria(modelPath, model, targetLevel, checkResults) {
62
+ const level = targetLevel || getModelLevel(model);
63
+
64
+ if (level === 'ADVISORY') {
65
+ return { valid: true, reason: 'ADVISORY: no criteria required' };
66
+ }
67
+
68
+ // SOFT_GATE: requires source_layer
69
+ const sourceLayer = model.source_layer || inferSourceLayer(modelPath);
70
+ if (level === 'SOFT_GATE' || level === 'HARD_GATE') {
71
+ if (!sourceLayer) {
72
+ return { valid: false, reason: 'SOFT_GATE requires source_layer assignment' };
73
+ }
74
+ }
75
+
76
+ // HARD_GATE: requires SOFT_GATE + passing check-result
77
+ if (level === 'HARD_GATE') {
78
+ const modelLower = modelPath.toLowerCase();
79
+ const hasPassingCheck = checkResults.some(cr => {
80
+ if (cr.result !== 'pass') return false;
81
+ const toolMatch = cr.tool && modelLower.includes(cr.tool.toLowerCase());
82
+ const checkIdPart = cr.check_id ? (cr.check_id.split(':')[1] || '') : '';
83
+ const checkIdMatch = checkIdPart && modelLower.includes(checkIdPart.toLowerCase());
84
+ return toolMatch || checkIdMatch;
85
+ });
86
+ if (!hasPassingCheck) {
87
+ return { valid: false, reason: 'HARD_GATE requires at least one passing check-result' };
88
+ }
89
+ }
90
+
91
+ return { valid: true, reason: `${level}: all criteria met` };
92
+ }
93
+
94
+ // ── Promote a single model ──────────────────────────────────────────────────
95
+
96
+ function promoteModel(registry, modelPath, targetLevel, checkResults) {
97
+ const model = registry[modelPath];
98
+ if (!model) {
99
+ return { success: false, error: `Model not found: ${modelPath}` };
100
+ }
101
+
102
+ const currentLevel = getModelLevel(model);
103
+ const currentRank = LEVEL_RANK[currentLevel] ?? 0;
104
+ const targetRank = LEVEL_RANK[targetLevel];
105
+
106
+ if (targetRank === undefined) {
107
+ return { success: false, error: `Invalid level: ${targetLevel}. Must be one of: ${LEVELS.join(', ')}` };
108
+ }
109
+
110
+ if (targetRank <= currentRank) {
111
+ return { success: false, error: `Cannot promote: ${currentLevel} -> ${targetLevel} is not a promotion` };
112
+ }
113
+
114
+ // Validate target level criteria
115
+ const validation = validateCriteria(modelPath, model, targetLevel, checkResults);
116
+ if (!validation.valid) {
117
+ return { success: false, error: `Criteria not met for ${targetLevel}: ${validation.reason}` };
118
+ }
119
+
120
+ // Apply promotion
121
+ model.gate_maturity = targetLevel;
122
+ model.last_updated = new Date().toISOString();
123
+ if (!model.source_layer) {
124
+ model.source_layer = inferSourceLayer(modelPath);
125
+ }
126
+
127
+ return { success: true, from: currentLevel, to: targetLevel };
128
+ }
129
+
130
+ // ── Check all models ────────────────────────────────────────────────────────
131
+
132
+ function checkAllModels(registry, checkResults, fix) {
133
+ const modelKeys = getModelKeys(registry);
134
+ const violations = [];
135
+ const demotions = [];
136
+
137
+ for (const modelPath of modelKeys) {
138
+ const model = registry[modelPath];
139
+ const level = getModelLevel(model);
140
+
141
+ if (level === 'ADVISORY') continue; // Always valid
142
+
143
+ const validation = validateCriteria(modelPath, model, level, checkResults);
144
+ if (!validation.valid) {
145
+ violations.push({
146
+ model: modelPath,
147
+ level,
148
+ reason: validation.reason,
149
+ });
150
+
151
+ if (fix) {
152
+ // Demote to highest valid level
153
+ if (level === 'HARD_GATE') {
154
+ const softCheck = validateCriteria(modelPath, model, 'SOFT_GATE', checkResults);
155
+ if (softCheck.valid) {
156
+ model.gate_maturity = 'SOFT_GATE';
157
+ model.last_updated = new Date().toISOString();
158
+ demotions.push({ model: modelPath, from: level, to: 'SOFT_GATE' });
159
+ } else {
160
+ model.gate_maturity = 'ADVISORY';
161
+ model.last_updated = new Date().toISOString();
162
+ demotions.push({ model: modelPath, from: level, to: 'ADVISORY' });
163
+ }
164
+ } else {
165
+ model.gate_maturity = 'ADVISORY';
166
+ model.last_updated = new Date().toISOString();
167
+ demotions.push({ model: modelPath, from: level, to: 'ADVISORY' });
168
+ }
169
+ }
170
+ }
171
+ }
172
+
173
+ return {
174
+ total: modelKeys.length,
175
+ checked: modelKeys.filter(k => getModelLevel(registry[k]) !== 'ADVISORY').length,
176
+ violations,
177
+ demotions,
178
+ by_level: {
179
+ ADVISORY: modelKeys.filter(k => getModelLevel(registry[k]) === 'ADVISORY').length,
180
+ SOFT_GATE: modelKeys.filter(k => getModelLevel(registry[k]) === 'SOFT_GATE').length,
181
+ HARD_GATE: modelKeys.filter(k => getModelLevel(registry[k]) === 'HARD_GATE').length,
182
+ },
183
+ };
184
+ }
185
+
186
+ // ── Load check results ──────────────────────────────────────────────────────
187
+
188
+ function loadCheckResults() {
189
+ if (!fs.existsSync(CHECK_RESULTS_PATH)) return [];
190
+ return fs.readFileSync(CHECK_RESULTS_PATH, 'utf8')
191
+ .trim().split('\n').filter(Boolean)
192
+ .map(line => { try { return JSON.parse(line); } catch { return null; } })
193
+ .filter(Boolean);
194
+ }
195
+
196
+ // ── Entry point ─────────────────────────────────────────────────────────────
197
+
198
+ function main() {
199
+ if (!fs.existsSync(REGISTRY_PATH)) {
200
+ console.error('ERROR: model-registry.json not found at', REGISTRY_PATH);
201
+ process.exit(1);
202
+ }
203
+
204
+ const registry = JSON.parse(fs.readFileSync(REGISTRY_PATH, 'utf8'));
205
+ const checkResults = loadCheckResults();
206
+
207
+ if (CHECK_FLAG) {
208
+ const result = checkAllModels(registry, checkResults, FIX_FLAG);
209
+
210
+ if (FIX_FLAG && result.demotions.length > 0) {
211
+ fs.writeFileSync(REGISTRY_PATH, JSON.stringify(registry, null, 2) + '\n');
212
+ }
213
+
214
+ if (JSON_FLAG) {
215
+ process.stdout.write(JSON.stringify(result));
216
+ } else {
217
+ console.log(`Gate Maturity Check`);
218
+ console.log(` Total models: ${result.total}`);
219
+ console.log(` By level: ${JSON.stringify(result.by_level)}`);
220
+ console.log(` Violations: ${result.violations.length}`);
221
+ if (result.violations.length > 0) {
222
+ for (const v of result.violations) {
223
+ console.log(` - ${v.model}: ${v.reason}`);
224
+ }
225
+ }
226
+ if (result.demotions.length > 0) {
227
+ console.log(` Demotions applied:`);
228
+ for (const d of result.demotions) {
229
+ console.log(` - ${d.model}: ${d.from} -> ${d.to}`);
230
+ }
231
+ }
232
+ }
233
+
234
+ process.exit(result.violations.length > 0 && !FIX_FLAG ? 1 : 0);
235
+ }
236
+
237
+ // Promotion mode
238
+ const modelIdx = process.argv.indexOf('--model');
239
+ const levelIdx = process.argv.indexOf('--level');
240
+
241
+ if (modelIdx === -1 || levelIdx === -1) {
242
+ console.error('Usage: --model <path> --level <ADVISORY|SOFT_GATE|HARD_GATE>');
243
+ console.error(' or: --check [--fix] [--json]');
244
+ process.exit(1);
245
+ }
246
+
247
+ const modelPath = process.argv[modelIdx + 1];
248
+ const targetLevel = process.argv[levelIdx + 1];
249
+
250
+ const result = promoteModel(registry, modelPath, targetLevel, checkResults);
251
+
252
+ if (result.success) {
253
+ fs.writeFileSync(REGISTRY_PATH, JSON.stringify(registry, null, 2) + '\n');
254
+
255
+ if (JSON_FLAG) {
256
+ process.stdout.write(JSON.stringify(result));
257
+ } else {
258
+ console.log(`Promoted: ${modelPath}`);
259
+ console.log(` ${result.from} -> ${result.to}`);
260
+ }
261
+ process.exit(0);
262
+ } else {
263
+ if (JSON_FLAG) {
264
+ process.stdout.write(JSON.stringify(result));
265
+ } else {
266
+ console.error(`Promotion failed: ${result.error}`);
267
+ }
268
+ process.exit(1);
269
+ }
270
+ }
271
+
272
+ if (require.main === module) main();
273
+
274
+ module.exports = { promoteModel, validateCriteria, checkAllModels, getModelKeys, inferSourceLayer, loadCheckResults };
@@ -20,7 +20,7 @@ const ROOT = path.join(__dirname, '..');
20
20
  // ── Find .planning/formal/ root from target path ───────────────────────────────────────
21
21
  // Walks up from a given path until finding .planning/formal/, then
22
22
  // returns the grandparent of that directory as the project root.
23
- // Falls back to ROOT (the QGSD project root) if no .planning/formal/ ancestor is found.
23
+ // Falls back to ROOT (the nForma project root) if no .planning/formal/ ancestor is found.
24
24
  function findProjectRoot(startPath) {
25
25
  let current = path.dirname(startPath);
26
26
  while (true) {
@@ -13,7 +13,7 @@ const fs = require('fs');
13
13
  const NON_INTERACTIVE = process.argv.includes('--non-interactive');
14
14
  const DEBUG_ARTIFACT_PATH = path.join(process.cwd(), '.planning', 'quick', 'quorum-debug-latest.md');
15
15
  const ACCEPT_SCRIPT = path.join(__dirname, 'accept-debug-invariant.cjs');
16
- const DEFAULT_SPEC = path.join(process.cwd(), '.planning', 'formal', 'tla', 'QGSDQuorum.tla');
16
+ const DEFAULT_SPEC = path.join(process.cwd(), '.planning', 'formal', 'tla', 'NFQuorum.tla');
17
17
 
18
18
  function sanitizeTlaName(str) {
19
19
  return str.replace(/[^a-zA-Z0-9]+/g, '_').slice(0, 40).replace(/^_+|_+$/g, '') || 'Unknown';