@nforma.ai/nforma 0.2.1

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 (215) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +1024 -0
  3. package/agents/qgsd-codebase-mapper.md +764 -0
  4. package/agents/qgsd-debugger.md +1201 -0
  5. package/agents/qgsd-executor.md +472 -0
  6. package/agents/qgsd-integration-checker.md +443 -0
  7. package/agents/qgsd-phase-researcher.md +502 -0
  8. package/agents/qgsd-plan-checker.md +643 -0
  9. package/agents/qgsd-planner.md +1182 -0
  10. package/agents/qgsd-project-researcher.md +621 -0
  11. package/agents/qgsd-quorum-orchestrator.md +628 -0
  12. package/agents/qgsd-quorum-slot-worker.md +41 -0
  13. package/agents/qgsd-quorum-synthesizer.md +133 -0
  14. package/agents/qgsd-quorum-test-worker.md +37 -0
  15. package/agents/qgsd-quorum-worker.md +161 -0
  16. package/agents/qgsd-research-synthesizer.md +239 -0
  17. package/agents/qgsd-roadmapper.md +660 -0
  18. package/agents/qgsd-verifier.md +628 -0
  19. package/bin/accept-debug-invariant.cjs +165 -0
  20. package/bin/account-manager.cjs +719 -0
  21. package/bin/aggregate-requirements.cjs +466 -0
  22. package/bin/analyze-assumptions.cjs +757 -0
  23. package/bin/analyze-state-space.cjs +921 -0
  24. package/bin/attribute-trace-divergence.cjs +150 -0
  25. package/bin/auth-drivers/gh-cli.cjs +93 -0
  26. package/bin/auth-drivers/index.cjs +46 -0
  27. package/bin/auth-drivers/pool.cjs +67 -0
  28. package/bin/auth-drivers/simple.cjs +95 -0
  29. package/bin/autoClosePtoF.cjs +110 -0
  30. package/bin/blessed-terminal.cjs +350 -0
  31. package/bin/build-phase-index.cjs +472 -0
  32. package/bin/call-quorum-slot.cjs +541 -0
  33. package/bin/ccr-secure-config.cjs +99 -0
  34. package/bin/ccr-secure-start.cjs +83 -0
  35. package/bin/check-bundled-sdks.cjs +177 -0
  36. package/bin/check-coverage-guard.cjs +112 -0
  37. package/bin/check-liveness-fairness.cjs +95 -0
  38. package/bin/check-mcp-health.cjs +123 -0
  39. package/bin/check-provider-health.cjs +395 -0
  40. package/bin/check-results-exit.cjs +24 -0
  41. package/bin/check-spec-sync.cjs +360 -0
  42. package/bin/check-trace-redaction.cjs +271 -0
  43. package/bin/check-trace-schema-drift.cjs +99 -0
  44. package/bin/compareDrift.cjs +21 -0
  45. package/bin/conformance-schema.cjs +12 -0
  46. package/bin/count-scenarios.cjs +420 -0
  47. package/bin/debt-dedup.cjs +144 -0
  48. package/bin/debt-ledger.cjs +61 -0
  49. package/bin/debt-retention.cjs +76 -0
  50. package/bin/debt-state-machine.cjs +80 -0
  51. package/bin/detect-coverage-gaps.cjs +204 -0
  52. package/bin/detect-project-intent.cjs +362 -0
  53. package/bin/export-prism-constants.cjs +164 -0
  54. package/bin/extract-annotations.cjs +633 -0
  55. package/bin/extractFormalExpected.cjs +104 -0
  56. package/bin/fingerprint-drift.cjs +24 -0
  57. package/bin/fingerprint-issue.cjs +46 -0
  58. package/bin/formal-core.cjs +519 -0
  59. package/bin/formal-ref-linker.cjs +141 -0
  60. package/bin/formal-test-sync.cjs +788 -0
  61. package/bin/generate-formal-specs.cjs +588 -0
  62. package/bin/generate-petri-net.cjs +397 -0
  63. package/bin/generate-phase-spec.cjs +249 -0
  64. package/bin/generate-proposed-changes.cjs +194 -0
  65. package/bin/generate-tla-cfg.cjs +122 -0
  66. package/bin/generate-traceability-matrix.cjs +701 -0
  67. package/bin/generate-triage-bundle.cjs +300 -0
  68. package/bin/gh-account-rotate.cjs +34 -0
  69. package/bin/initialize-model-registry.cjs +105 -0
  70. package/bin/install-formal-tools.cjs +382 -0
  71. package/bin/install.js +2424 -0
  72. package/bin/isNumericThreshold.cjs +34 -0
  73. package/bin/issue-classifier.cjs +151 -0
  74. package/bin/levenshtein.cjs +74 -0
  75. package/bin/lint-formal-models.cjs +580 -0
  76. package/bin/load-baseline-requirements.cjs +275 -0
  77. package/bin/manage-agents-core.cjs +815 -0
  78. package/bin/migrate-formal-dir.cjs +172 -0
  79. package/bin/migrate-planning.cjs +206 -0
  80. package/bin/migrate-to-slots.cjs +255 -0
  81. package/bin/nForma.cjs +2726 -0
  82. package/bin/observe-config.cjs +353 -0
  83. package/bin/observe-debt-writer.cjs +140 -0
  84. package/bin/observe-handler-grafana.cjs +128 -0
  85. package/bin/observe-handler-internal.cjs +301 -0
  86. package/bin/observe-handler-logstash.cjs +153 -0
  87. package/bin/observe-handler-prometheus.cjs +185 -0
  88. package/bin/observe-handlers.cjs +436 -0
  89. package/bin/observe-registry.cjs +131 -0
  90. package/bin/observe-render.cjs +168 -0
  91. package/bin/planning-paths.cjs +167 -0
  92. package/bin/polyrepo.cjs +560 -0
  93. package/bin/prism-priority.cjs +153 -0
  94. package/bin/probe-quorum-slots.cjs +167 -0
  95. package/bin/promote-model.cjs +225 -0
  96. package/bin/propose-debug-invariants.cjs +165 -0
  97. package/bin/providers.json +392 -0
  98. package/bin/pty-proxy.py +129 -0
  99. package/bin/qgsd-solve.cjs +2477 -0
  100. package/bin/quorum-consensus-gate.cjs +238 -0
  101. package/bin/quorum-formal-context.cjs +183 -0
  102. package/bin/quorum-slot-dispatch.cjs +934 -0
  103. package/bin/read-policy.cjs +60 -0
  104. package/bin/requirement-map.cjs +63 -0
  105. package/bin/requirements-core.cjs +247 -0
  106. package/bin/resolve-cli.cjs +101 -0
  107. package/bin/review-mcp-logs.cjs +294 -0
  108. package/bin/run-account-manager-tlc.cjs +188 -0
  109. package/bin/run-account-pool-alloy.cjs +158 -0
  110. package/bin/run-alloy.cjs +153 -0
  111. package/bin/run-audit-alloy.cjs +187 -0
  112. package/bin/run-breaker-tlc.cjs +181 -0
  113. package/bin/run-formal-check.cjs +395 -0
  114. package/bin/run-formal-verify.cjs +701 -0
  115. package/bin/run-installer-alloy.cjs +188 -0
  116. package/bin/run-oauth-rotation-prism.cjs +132 -0
  117. package/bin/run-oscillation-tlc.cjs +202 -0
  118. package/bin/run-phase-tlc.cjs +228 -0
  119. package/bin/run-prism.cjs +446 -0
  120. package/bin/run-protocol-tlc.cjs +201 -0
  121. package/bin/run-quorum-composition-alloy.cjs +155 -0
  122. package/bin/run-sensitivity-sweep.cjs +231 -0
  123. package/bin/run-stop-hook-tlc.cjs +188 -0
  124. package/bin/run-tlc.cjs +467 -0
  125. package/bin/run-transcript-alloy.cjs +173 -0
  126. package/bin/run-uppaal.cjs +264 -0
  127. package/bin/secrets.cjs +134 -0
  128. package/bin/sensitivity-report.cjs +219 -0
  129. package/bin/sensitivity-sweep-feedback.cjs +194 -0
  130. package/bin/set-secret.cjs +29 -0
  131. package/bin/setup-telemetry-cron.sh +36 -0
  132. package/bin/sweepPtoF.cjs +63 -0
  133. package/bin/sync-baseline-requirements.cjs +290 -0
  134. package/bin/task-envelope.cjs +360 -0
  135. package/bin/telemetry-collector.cjs +229 -0
  136. package/bin/unified-mcp-server.mjs +735 -0
  137. package/bin/update-agents.cjs +369 -0
  138. package/bin/update-scoreboard.cjs +1134 -0
  139. package/bin/validate-debt-entry.cjs +207 -0
  140. package/bin/validate-invariant.cjs +419 -0
  141. package/bin/validate-memory.cjs +389 -0
  142. package/bin/validate-requirements-haiku.cjs +435 -0
  143. package/bin/validate-traces.cjs +438 -0
  144. package/bin/verify-formal-results.cjs +124 -0
  145. package/bin/verify-quorum-health.cjs +273 -0
  146. package/bin/write-check-result.cjs +106 -0
  147. package/bin/xstate-to-tla.cjs +483 -0
  148. package/bin/xstate-trace-walker.cjs +205 -0
  149. package/commands/qgsd/add-phase.md +43 -0
  150. package/commands/qgsd/add-requirement.md +24 -0
  151. package/commands/qgsd/add-todo.md +47 -0
  152. package/commands/qgsd/audit-milestone.md +37 -0
  153. package/commands/qgsd/check-todos.md +45 -0
  154. package/commands/qgsd/cleanup.md +18 -0
  155. package/commands/qgsd/close-formal-gaps.md +33 -0
  156. package/commands/qgsd/complete-milestone.md +136 -0
  157. package/commands/qgsd/debug.md +166 -0
  158. package/commands/qgsd/discuss-phase.md +83 -0
  159. package/commands/qgsd/execute-phase.md +117 -0
  160. package/commands/qgsd/fix-tests.md +27 -0
  161. package/commands/qgsd/formal-test-sync.md +32 -0
  162. package/commands/qgsd/health.md +22 -0
  163. package/commands/qgsd/help.md +22 -0
  164. package/commands/qgsd/insert-phase.md +32 -0
  165. package/commands/qgsd/join-discord.md +18 -0
  166. package/commands/qgsd/list-phase-assumptions.md +46 -0
  167. package/commands/qgsd/map-codebase.md +71 -0
  168. package/commands/qgsd/map-requirements.md +20 -0
  169. package/commands/qgsd/mcp-restart.md +176 -0
  170. package/commands/qgsd/mcp-set-model.md +134 -0
  171. package/commands/qgsd/mcp-setup.md +1371 -0
  172. package/commands/qgsd/mcp-status.md +274 -0
  173. package/commands/qgsd/mcp-update.md +238 -0
  174. package/commands/qgsd/new-milestone.md +44 -0
  175. package/commands/qgsd/new-project.md +42 -0
  176. package/commands/qgsd/observe.md +260 -0
  177. package/commands/qgsd/pause-work.md +38 -0
  178. package/commands/qgsd/plan-milestone-gaps.md +34 -0
  179. package/commands/qgsd/plan-phase.md +44 -0
  180. package/commands/qgsd/polyrepo.md +50 -0
  181. package/commands/qgsd/progress.md +24 -0
  182. package/commands/qgsd/queue.md +54 -0
  183. package/commands/qgsd/quick.md +133 -0
  184. package/commands/qgsd/quorum-test.md +275 -0
  185. package/commands/qgsd/quorum.md +707 -0
  186. package/commands/qgsd/reapply-patches.md +110 -0
  187. package/commands/qgsd/remove-phase.md +31 -0
  188. package/commands/qgsd/research-phase.md +189 -0
  189. package/commands/qgsd/resume-work.md +40 -0
  190. package/commands/qgsd/set-profile.md +34 -0
  191. package/commands/qgsd/settings.md +39 -0
  192. package/commands/qgsd/solve.md +565 -0
  193. package/commands/qgsd/sync-baselines.md +119 -0
  194. package/commands/qgsd/triage.md +233 -0
  195. package/commands/qgsd/update.md +37 -0
  196. package/commands/qgsd/verify-work.md +38 -0
  197. package/hooks/dist/config-loader.js +297 -0
  198. package/hooks/dist/conformance-schema.cjs +12 -0
  199. package/hooks/dist/gsd-context-monitor.js +64 -0
  200. package/hooks/dist/qgsd-check-update.js +62 -0
  201. package/hooks/dist/qgsd-circuit-breaker.js +682 -0
  202. package/hooks/dist/qgsd-precompact.js +156 -0
  203. package/hooks/dist/qgsd-prompt.js +653 -0
  204. package/hooks/dist/qgsd-session-start.js +122 -0
  205. package/hooks/dist/qgsd-slot-correlator.js +58 -0
  206. package/hooks/dist/qgsd-spec-regen.js +86 -0
  207. package/hooks/dist/qgsd-statusline.js +91 -0
  208. package/hooks/dist/qgsd-stop.js +553 -0
  209. package/hooks/dist/qgsd-token-collector.js +133 -0
  210. package/hooks/dist/unified-mcp-server.mjs +669 -0
  211. package/package.json +95 -0
  212. package/scripts/build-hooks.js +46 -0
  213. package/scripts/postinstall.js +48 -0
  214. package/scripts/secret-audit.sh +45 -0
  215. package/templates/qgsd.json +49 -0
@@ -0,0 +1,483 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // bin/xstate-to-tla.cjs
4
+ // Transpiles an XState v5 machine definition (.ts) to TLA+ spec + TLC model config.
5
+ //
6
+ // Strategy:
7
+ // 1. Compile the TypeScript machine file to a temp CJS bundle via esbuild.
8
+ // 2. require() the bundle and find the exported XState machine object.
9
+ // 3. Walk machine.config to extract states, transitions, guard names, and
10
+ // which context variables each transition assigns.
11
+ // 4. Emit .planning/formal/tla/<ModuleName>_xstate.tla and .planning/formal/tla/MC<modulename>.cfg.
12
+ //
13
+ // Usage:
14
+ // node bin/xstate-to-tla.cjs <machine-file.ts>
15
+ // node bin/xstate-to-tla.cjs src/machines/qgsd-workflow.machine.ts \
16
+ // --module=QGSDQuorum \
17
+ // --config=.planning/formal/tla/guards/qgsd-workflow.json
18
+ // node bin/xstate-to-tla.cjs src/machines/account-manager.machine.ts \
19
+ // --module=QGSDAccountManager \
20
+ // --config=.planning/formal/tla/guards/account-manager.json \
21
+ // --dry
22
+ //
23
+ // Config file format (JSON):
24
+ // {
25
+ // "guards": {
26
+ // "minQuorumMet": "successCount * 2 >= N",
27
+ // "noInfiniteDeliberation": "deliberationRounds < MaxDeliberation"
28
+ // },
29
+ // "vars": {
30
+ // "currentPhase": "skip", // omit — redundant with 'state'
31
+ // "maxDeliberation": "const", // never changes, always UNCHANGED
32
+ // "successCount": "event", // value comes from event — add as action param
33
+ // "slotsAvailable": "event",
34
+ // "deliberationRounds": "deliberationRounds + 1" // literal TLA+ expression
35
+ // }
36
+ // }
37
+ //
38
+ // Var annotation meanings:
39
+ // "skip" — omit from VARIABLES, UNCHANGED, and assignments
40
+ // "const" — never changes in any transition; put in UNCHANGED
41
+ // "event" — value provided by the triggering event; becomes an action parameter
42
+ // <tla-expr> — literal TLA+ expression to use on the RHS of var' = <expr>
43
+ // (absent) — generate: var' = var \* FIXME: provide TLA+ expression
44
+ //
45
+ // Prerequisites: esbuild (devDependency)
46
+
47
+ const { buildSync } = require('esbuild');
48
+ const fs = require('fs');
49
+ const os = require('os');
50
+ const path = require('path');
51
+
52
+ const TAG = '[xstate-to-tla]';
53
+
54
+ // ── CLI ───────────────────────────────────────────────────────────────────────
55
+ const argv = process.argv.slice(2);
56
+ const inputFile = argv.find(a => !a.startsWith('-'));
57
+ const moduleArg = (argv.find(a => a.startsWith('--module=')) || '').slice('--module='.length);
58
+ const configArg = (argv.find(a => a.startsWith('--config=')) || '').slice('--config='.length);
59
+ const outDirArg = (argv.find(a => a.startsWith('--out-dir=')) || '').slice('--out-dir='.length);
60
+ const dry = argv.includes('--dry');
61
+
62
+ if (!inputFile) {
63
+ process.stderr.write(
64
+ 'Usage: node bin/xstate-to-tla.cjs <machine-file.ts>\n' +
65
+ ' [--module=ModuleName]\n' +
66
+ ' [--config=guards-and-vars.json]\n' +
67
+ ' [--out-dir=.planning/formal/tla]\n' +
68
+ ' [--dry]\n'
69
+ );
70
+ process.exit(1);
71
+ }
72
+
73
+ const absInput = path.resolve(inputFile);
74
+ if (!fs.existsSync(absInput)) {
75
+ process.stderr.write(TAG + ' File not found: ' + absInput + '\n');
76
+ process.exit(1);
77
+ }
78
+
79
+ // ── Load user config ──────────────────────────────────────────────────────────
80
+ let userGuards = {}; // guardName → TLA+ expression
81
+ let userVars = {}; // varName → 'skip' | 'const' | 'event' | tla-expr
82
+
83
+ if (configArg) {
84
+ try {
85
+ const raw = JSON.parse(fs.readFileSync(path.resolve(configArg), 'utf8'));
86
+ userGuards = raw.guards || {};
87
+ userVars = raw.vars || {};
88
+ process.stdout.write(TAG + ' Config: ' + configArg + '\n');
89
+ process.stdout.write(TAG + ' guards: ' + Object.keys(userGuards).join(', ') + '\n');
90
+ process.stdout.write(TAG + ' vars: ' + Object.keys(userVars).join(', ') + '\n');
91
+ } catch (e) {
92
+ process.stderr.write(TAG + ' Failed to load config: ' + e.message + '\n');
93
+ process.exit(1);
94
+ }
95
+ }
96
+
97
+ // ── Compile TypeScript → temp CJS ────────────────────────────────────────────
98
+ const tmpBundle = path.join(os.tmpdir(), 'xstate-to-tla-' + Date.now() + '.cjs');
99
+ try {
100
+ buildSync({
101
+ entryPoints: [absInput],
102
+ bundle: true,
103
+ format: 'cjs',
104
+ outfile: tmpBundle,
105
+ platform: 'node',
106
+ logLevel: 'silent',
107
+ });
108
+ } catch (e) {
109
+ process.stderr.write(TAG + ' esbuild compilation failed: ' + e.message + '\n');
110
+ process.exit(1);
111
+ }
112
+
113
+ // ── Load compiled module ──────────────────────────────────────────────────────
114
+ let mod;
115
+ try {
116
+ mod = require(tmpBundle);
117
+ } catch (e) {
118
+ process.stderr.write(TAG + ' Failed to load compiled module: ' + e.message + '\n');
119
+ fs.unlinkSync(tmpBundle);
120
+ process.exit(1);
121
+ } finally {
122
+ try { fs.unlinkSync(tmpBundle); } catch (_) {}
123
+ }
124
+
125
+ // Find the XState machine export: an object with .config.states
126
+ const machine = Object.values(mod).find(v =>
127
+ v && typeof v === 'object' && v.config && v.config.states
128
+ );
129
+ if (!machine) {
130
+ process.stderr.write(TAG + ' No XState machine export found in: ' + inputFile + '\n');
131
+ process.stderr.write(TAG + ' Exports: ' + Object.keys(mod).join(', ') + '\n');
132
+ process.exit(1);
133
+ }
134
+
135
+ const cfg = machine.config;
136
+
137
+ // ── Extract machine structure ─────────────────────────────────────────────────
138
+ const machineId = cfg.id || path.basename(inputFile, '.ts').replace('.machine', '');
139
+ const initial = String(cfg.initial);
140
+ const ctxDefaults = cfg.context || {};
141
+
142
+ // Derive module name
143
+ const moduleName = moduleArg || machineId
144
+ .split(/[-_\s]+/)
145
+ .map(w => w.charAt(0).toUpperCase() + w.slice(1))
146
+ .join('');
147
+
148
+ // Context variables (excluding 'skip' ones)
149
+ const allCtxKeys = Object.keys(ctxDefaults);
150
+ const ctxVars = allCtxKeys.filter(k => userVars[k] !== 'skip');
151
+
152
+ // State list
153
+ const stateNames = Object.keys(cfg.states);
154
+ const finalStates = stateNames.filter(s => cfg.states[s].type === 'final');
155
+
156
+ // ── Parse transitions ─────────────────────────────────────────────────────────
157
+ // Returns array of { fromState, event, guard, target, assignedKeys }
158
+ function parseTransitions() {
159
+ const result = [];
160
+ for (const stateName of stateNames) {
161
+ const stateDef = cfg.states[stateName];
162
+ if (!stateDef.on) continue;
163
+
164
+ for (const [eventName, transVal] of Object.entries(stateDef.on)) {
165
+ const branches = Array.isArray(transVal) ? transVal : [transVal];
166
+
167
+ for (const branch of branches) {
168
+ if (!branch) continue;
169
+ const guard = typeof branch.guard === 'string' ? branch.guard : null;
170
+ const target = branch.target ? String(branch.target) : null;
171
+
172
+ // Collect assign keys from actions
173
+ const assignedKeys = [];
174
+ const actions = branch.actions
175
+ ? (Array.isArray(branch.actions) ? branch.actions : [branch.actions])
176
+ : [];
177
+ for (const act of actions) {
178
+ if (act && act.type === 'xstate.assign' && act.assignment) {
179
+ for (const k of Object.keys(act.assignment)) {
180
+ if (!assignedKeys.includes(k)) assignedKeys.push(k);
181
+ }
182
+ }
183
+ }
184
+
185
+ result.push({ fromState: stateName, event: eventName, guard, target, assignedKeys });
186
+ }
187
+ }
188
+ }
189
+ return result;
190
+ }
191
+
192
+ const allTransitions = parseTransitions();
193
+
194
+ // ── Action name derivation ────────────────────────────────────────────────────
195
+ function toCamel(s) {
196
+ return s.toLowerCase()
197
+ .split(/[_\s]+/)
198
+ .map(w => w.charAt(0).toUpperCase() + w.slice(1))
199
+ .join('');
200
+ }
201
+
202
+ // Count branches per (fromState, event) pair — for multi-branch disambiguation
203
+ const branchCount = {};
204
+ for (const t of allTransitions) {
205
+ const k = t.fromState + '::' + t.event;
206
+ branchCount[k] = (branchCount[k] || 0) + 1;
207
+ }
208
+
209
+ // Which events appear in more than one state? → need state prefix to stay unique
210
+ const eventStateSet = {};
211
+ for (const t of allTransitions) {
212
+ if (!eventStateSet[t.event]) eventStateSet[t.event] = new Set();
213
+ eventStateSet[t.event].add(t.fromState);
214
+ }
215
+
216
+ for (const t of allTransitions) {
217
+ const cc = toCamel(t.event);
218
+ const multiState = eventStateSet[t.event].size > 1;
219
+ const statePrefix = multiState ? toCamel(t.fromState) : '';
220
+ const k = t.fromState + '::' + t.event;
221
+ const multiBranch = branchCount[k] > 1;
222
+
223
+ if (multiBranch) {
224
+ t.actionName = statePrefix + cc + 'To' + (t.target || 'Unknown');
225
+ } else {
226
+ t.actionName = statePrefix + cc;
227
+ }
228
+ }
229
+
230
+ // ── TLA+ generation helpers ───────────────────────────────────────────────────
231
+ const ts_date = new Date().toISOString().split('T')[0];
232
+ const outDir = outDirArg
233
+ ? path.resolve(outDirArg)
234
+ : path.join(__dirname, '..', '.planning', 'formal', 'tla');
235
+
236
+ // Variables that appear in UNCHANGED (excludes state, skip-vars, and vars whose annotation is const/event/expr)
237
+ // We need UNCHANGED for: ctxVars that are NOT in assignedKeys for this transition
238
+ function genUnchanged(assignedInThisTrans) {
239
+ // 'const' vars + ctxVars not assigned (and not 'event' either — event vars that aren't assigned stay unchanged)
240
+ const unchanged = ctxVars.filter(v => !assignedInThisTrans.includes(v));
241
+ if (unchanged.length === 0) return null;
242
+ if (unchanged.length === 1) return unchanged[0];
243
+ return '<<' + unchanged.join(', ') + '>>';
244
+ }
245
+
246
+ // Generate the TLA+ assignment line for one variable in a transition
247
+ function genAssignLine(varName, isParam) {
248
+ if (isParam) return " /\\ " + varName + "' = " + varName + ' \\* param from event';
249
+ const ann = userVars[varName];
250
+ if (ann && ann !== 'const' && ann !== 'event' && ann !== 'skip') {
251
+ return " /\\ " + varName + "' = " + ann;
252
+ }
253
+ return " /\\ " + varName + "' = " + varName + ' \\* FIXME: XState assign for ' + varName;
254
+ }
255
+
256
+ // Generate one TLA+ action block
257
+ function genAction(t) {
258
+ const lines = [];
259
+ const cc = t.actionName;
260
+
261
+ // Which assigned vars are "event" type → become parameters
262
+ const params = t.assignedKeys.filter(k => userVars[k] === 'event');
263
+ const nonParamAssigned = t.assignedKeys.filter(k => userVars[k] !== 'event' && userVars[k] !== 'skip');
264
+
265
+ const paramStr = params.length > 0 ? '(' + params.join(', ') + ')' : '';
266
+
267
+ lines.push('\\* ' + t.fromState + ' -[' + t.event + (t.guard ? ' / ' + t.guard : '') + ']-> ' + (t.target || '?'));
268
+ lines.push(cc + paramStr + ' ==');
269
+ lines.push(' /\\ state = "' + t.fromState + '"');
270
+
271
+ // Guard
272
+ if (t.guard) {
273
+ const tlaGuard = userGuards[t.guard];
274
+ if (tlaGuard) {
275
+ lines.push(' /\\ ' + tlaGuard);
276
+ } else {
277
+ lines.push(' /\\ TRUE \\* FIXME: guard ' + t.guard + ' — add to config guards');
278
+ }
279
+ }
280
+
281
+ // State transition
282
+ if (t.target) {
283
+ lines.push(" /\\ state' = \"" + t.target + '"');
284
+ } else {
285
+ lines.push(" /\\ state' = state \\* FIXME: unknown target");
286
+ }
287
+
288
+ // Variable assignments
289
+ for (const v of params) {
290
+ lines.push(genAssignLine(v, true));
291
+ }
292
+ for (const v of nonParamAssigned) {
293
+ lines.push(genAssignLine(v, false));
294
+ }
295
+
296
+ // UNCHANGED
297
+ const unch = genUnchanged(t.assignedKeys);
298
+ if (unch) lines.push(' /\\ UNCHANGED ' + unch);
299
+
300
+ return lines.join('\n');
301
+ }
302
+
303
+ // Self-loop for final (absorbing) states
304
+ function genFinalSelfLoop(stateName) {
305
+ const lines = [
306
+ '\\* ' + stateName + ' is a final (absorbing) state',
307
+ 'Stay' + stateName + ' ==',
308
+ ' /\\ state = "' + stateName + '"',
309
+ " /\\ state' = \"" + stateName + '"',
310
+ ];
311
+ if (ctxVars.length > 0) {
312
+ lines.push(' /\\ UNCHANGED <<' + ctxVars.join(', ') + '>>');
313
+ }
314
+ return lines.join('\n');
315
+ }
316
+
317
+ // All unique action names (for Next and WF)
318
+ const actionNames = [];
319
+ for (const t of allTransitions) {
320
+ if (!actionNames.includes(t.actionName)) actionNames.push(t.actionName);
321
+ }
322
+ for (const s of finalStates) {
323
+ const aName = 'Stay' + s;
324
+ if (!actionNames.includes(aName)) actionNames.push(aName);
325
+ }
326
+
327
+ // ── Assemble TLA+ file ────────────────────────────────────────────────────────
328
+ const varsTuple = ['state', ...ctxVars].length === 1
329
+ ? 'state'
330
+ : '<<state, ' + ctxVars.join(', ') + '>>';
331
+
332
+ const lines = [
333
+ '---- MODULE ' + moduleName + '_xstate ----',
334
+ '(*',
335
+ ' * .planning/formal/tla/' + moduleName + '_xstate.tla',
336
+ ' * GENERATED by bin/xstate-to-tla.cjs',
337
+ ' * Source: ' + path.relative(path.join(__dirname, '..'), absInput),
338
+ ' * Regenerate: node bin/xstate-to-tla.cjs ' + path.relative(path.join(__dirname, '..'), absInput) +
339
+ (configArg ? ' --config=' + configArg : '') +
340
+ ' --module=' + moduleName,
341
+ ' * Generated: ' + ts_date,
342
+ ' *',
343
+ ' * XState machine id: ' + machineId,
344
+ ' * Initial state: ' + initial,
345
+ ' * States (' + stateNames.length + '): ' + stateNames.join(', '),
346
+ ' * Final states: ' + (finalStates.length ? finalStates.join(', ') : '(none)'),
347
+ '*)',
348
+ 'EXTENDS Naturals, FiniteSets, TLC',
349
+ '',
350
+ '\\* ── Variables ────────────────────────────────────────────────────────────────',
351
+ 'VARIABLES',
352
+ ' state' + (ctxVars.length > 0 ? ',' : '') + ' \\* FSM state',
353
+ ...ctxVars.map((v, i) => {
354
+ const ann = userVars[v] || '(no annotation)';
355
+ const dflt = ctxDefaults[v];
356
+ return ' ' + v + (i < ctxVars.length - 1 ? ',' : '') +
357
+ ' \\* default: ' + JSON.stringify(dflt) + ' annotation: ' + ann;
358
+ }),
359
+ '',
360
+ 'vars == ' + varsTuple,
361
+ '',
362
+ '\\* ── Type invariant ────────────────────────────────────────────────────────────',
363
+ '\\* @requirement QUORUM-01',
364
+ 'TypeOK ==',
365
+ ' /\\ state \\in {' + stateNames.map(s => '"' + s + '"').join(', ') + '}',
366
+ ...ctxVars.map(v => {
367
+ const dflt = ctxDefaults[v];
368
+ if (typeof dflt === 'number') return ' /\\ ' + v + ' \\in Nat \\* FIXME: tighten bound if needed';
369
+ if (typeof dflt === 'string') return ' /\\ ' + v + ' \\in STRING';
370
+ if (typeof dflt === 'boolean') return ' /\\ ' + v + ' \\in BOOLEAN';
371
+ return ' /\\ TRUE \\* FIXME: type for ' + v;
372
+ }),
373
+ '',
374
+ '\\* ── Initial state ─────────────────────────────────────────────────────────────',
375
+ 'Init ==',
376
+ ' /\\ state = "' + initial + '"',
377
+ ...ctxVars.map(v => {
378
+ const dflt = ctxDefaults[v];
379
+ if (typeof dflt === 'string') return ' /\\ ' + v + " = \"" + dflt + '"';
380
+ if (typeof dflt === 'number') return ' /\\ ' + v + ' = ' + dflt;
381
+ if (typeof dflt === 'boolean') return ' /\\ ' + v + ' = ' + (dflt ? 'TRUE' : 'FALSE');
382
+ return ' /\\ ' + v + ' = 0 \\* FIXME: initial value';
383
+ }),
384
+ '',
385
+ '\\* ── Actions ────────────────────────────────────────────────────────────────────',
386
+ ];
387
+
388
+ // Non-final state transitions
389
+ for (const stateName of stateNames) {
390
+ if (!finalStates.includes(stateName)) {
391
+ const stateTrans = allTransitions.filter(t => t.fromState === stateName);
392
+ for (const t of stateTrans) {
393
+ lines.push('');
394
+ lines.push(genAction(t));
395
+ }
396
+ }
397
+ }
398
+
399
+ // Final state self-loops
400
+ for (const stateName of finalStates) {
401
+ lines.push('');
402
+ lines.push(genFinalSelfLoop(stateName));
403
+ }
404
+
405
+ // Next
406
+ lines.push('');
407
+ lines.push('\\* ── Next ──────────────────────────────────────────────────────────────────────');
408
+ lines.push('Next ==');
409
+ for (const t of allTransitions) {
410
+ const params = t.assignedKeys.filter(k => userVars[k] === 'event');
411
+ const paramStr = params.length > 0 ? '(\\E ' + params.map(p => p + ' \\in Nat').join(', ') + ' : ' : '';
412
+ const closeParen = params.length > 0 ? ')' : '';
413
+ if (params.length > 0) {
414
+ lines.push(' \\/ \\E ' + params.map(p => p + ' \\in Nat').join(', ') + ' : ' + t.actionName + '(' + params.join(', ') + ')');
415
+ } else {
416
+ lines.push(' \\/ ' + t.actionName);
417
+ }
418
+ }
419
+ for (const s of finalStates) {
420
+ lines.push(' \\/ Stay' + s);
421
+ }
422
+
423
+ // Spec
424
+ lines.push('');
425
+ lines.push('\\* ── Specification ─────────────────────────────────────────────────────────────');
426
+ lines.push('Spec == Init /\\ [][Next]_vars');
427
+ for (const a of actionNames) {
428
+ lines.push(' /\\ WF_vars(' + a + ')');
429
+ }
430
+
431
+ // Invariant/liveness placeholders
432
+ lines.push('');
433
+ lines.push('\\* ── Invariants (add domain-specific properties here) ──────────────────────────');
434
+ lines.push('\\* TypeOK is the structural baseline. Add semantic invariants below.');
435
+ lines.push('');
436
+ lines.push('====');
437
+ lines.push('');
438
+
439
+ const tlaContent = lines.join('\n');
440
+
441
+ // ── Generate .cfg ─────────────────────────────────────────────────────────────
442
+ const cfgName = 'MC' + moduleName;
443
+ const cfgContent = [
444
+ '\\* .planning/formal/tla/' + cfgName + '.cfg',
445
+ '\\* GENERATED by bin/xstate-to-tla.cjs',
446
+ '\\* Regenerate: node bin/xstate-to-tla.cjs ' + path.relative(path.join(__dirname, '..'), absInput) +
447
+ (configArg ? ' --config=' + configArg : '') + ' --module=' + moduleName,
448
+ '\\* Generated: ' + ts_date,
449
+ '',
450
+ 'SPECIFICATION Spec',
451
+ 'INVARIANT TypeOK',
452
+ 'CHECK_DEADLOCK FALSE',
453
+ '',
454
+ ].join('\n');
455
+
456
+ // ── Write or dry-run ──────────────────────────────────────────────────────────
457
+ const tlaOutPath = path.join(outDir, moduleName + '_xstate.tla');
458
+ const cfgOutPath = path.join(outDir, cfgName + '.cfg');
459
+
460
+ if (dry) {
461
+ process.stdout.write('\n--- ' + path.relative(process.cwd(), tlaOutPath) + ' ---\n');
462
+ process.stdout.write(tlaContent);
463
+ process.stdout.write('\n--- ' + path.relative(process.cwd(), cfgOutPath) + ' ---\n');
464
+ process.stdout.write(cfgContent + '\n');
465
+ } else {
466
+ fs.mkdirSync(outDir, { recursive: true });
467
+ fs.writeFileSync(tlaOutPath, tlaContent, 'utf8');
468
+ fs.writeFileSync(cfgOutPath, cfgContent, 'utf8');
469
+ process.stdout.write(TAG + ' TLA+: ' + path.relative(process.cwd(), tlaOutPath) + '\n');
470
+ process.stdout.write(TAG + ' CFG: ' + path.relative(process.cwd(), cfgOutPath) + '\n');
471
+ process.stdout.write(TAG + ' States: ' + stateNames.join(', ') + '\n');
472
+ process.stdout.write(TAG + ' Actions: ' + actionNames.join(', ') + '\n');
473
+
474
+ // Report unresolved guards
475
+ const allGuardNames = [...new Set(allTransitions.filter(t => t.guard).map(t => t.guard))];
476
+ const unresolved = allGuardNames.filter(g => !userGuards[g]);
477
+ if (unresolved.length > 0) {
478
+ process.stdout.write(TAG + ' WARN: guards without TLA+ mapping — search for FIXME:\n');
479
+ for (const g of unresolved) {
480
+ process.stdout.write(TAG + ' ' + g + '\n');
481
+ }
482
+ }
483
+ }
@@ -0,0 +1,205 @@
1
+ 'use strict';
2
+ // bin/xstate-trace-walker.cjs
3
+ // Reusable XState trace replay library used by validate-traces.cjs and attribute-trace-divergence.cjs.
4
+ // Evaluates XState transitions and guard conditions given a conformance event and current snapshot.
5
+ //
6
+ // Key design principle: replayTrace creates ONE actor for the full sequence (not fresh per event).
7
+ // This enables multi-event interaction bug detection — guards in event N see context accumulated
8
+ // from events 1..N-1, rather than seeing the uninitialized defaults from a fresh IDLE snapshot.
9
+ // (Pitfall 1: "fresh-actor validation blindspot" documented in v0.21-02-RESEARCH.md)
10
+ //
11
+ // Exports: evaluateTransitions, replayTrace, evaluateGuard
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+
16
+ // ── Machine loader (cached) ────────────────────────────────────────────────────
17
+
18
+ let _cachedMachineModule = null;
19
+
20
+ function loadMachineModule() {
21
+ if (_cachedMachineModule) return _cachedMachineModule;
22
+ const repoDist = path.join(__dirname, '..', 'dist', 'machines', 'qgsd-workflow.machine.cjs');
23
+ const installDist = path.join(__dirname, 'dist', 'machines', 'qgsd-workflow.machine.cjs');
24
+ const machinePath = fs.existsSync(repoDist) ? repoDist : installDist;
25
+ if (!fs.existsSync(machinePath)) {
26
+ throw new Error('Cannot find qgsd-workflow.machine.cjs at ' + repoDist + ' or ' + installDist);
27
+ }
28
+ _cachedMachineModule = require(machinePath);
29
+ return _cachedMachineModule;
30
+ }
31
+
32
+ // ── Guard evaluation ───────────────────────────────────────────────────────────
33
+
34
+ /**
35
+ * evaluateGuard: evaluate a named guard function against snapshot context + event.
36
+ *
37
+ * @param {string|null} guardName - The guard name string (from transition config)
38
+ * @param {object} context - The XState snapshot context at evaluation time
39
+ * @param {object} event - The event being evaluated
40
+ * @param {object} machine - The XState machine (used to resolve guard fn)
41
+ * @returns {{ guardName: string|null, guardPassed: boolean, guardContext: object }}
42
+ */
43
+ function evaluateGuard(guardName, context, event, machine) {
44
+ if (!guardName) {
45
+ // No guard → transition is always taken (unconditional)
46
+ return { guardName: null, guardPassed: true, guardContext: Object.assign({}, context) };
47
+ }
48
+
49
+ // Resolve guard function from machine implementations (XState v5) or options (XState v4)
50
+ const guards = (machine && (machine.implementations?.guards || machine.options?.guards || machine.config?.guards)) || {};
51
+ const guardFn = guards[guardName];
52
+
53
+ if (!guardFn || typeof guardFn !== 'function') {
54
+ // Guard function not found → fail-open (return true to avoid false negatives)
55
+ return { guardName, guardPassed: true, guardContext: Object.assign({}, context) };
56
+ }
57
+
58
+ let guardPassed = true;
59
+ try {
60
+ // XState v5 guard signature: ({ context, event }) => boolean
61
+ // XState v4 guard signature: (context, event) => boolean
62
+ // Try v5 first (object destructuring), fall back to v4
63
+ guardPassed = !!guardFn({ context, event }, event);
64
+ } catch (_) {
65
+ // Guard threw — fail-open
66
+ guardPassed = true;
67
+ }
68
+
69
+ return { guardName, guardPassed, guardContext: Object.assign({}, context) };
70
+ }
71
+
72
+ // ── Core transition evaluator ──────────────────────────────────────────────────
73
+
74
+ /**
75
+ * evaluateTransitions: given a snapshot and event, determine which transitions would be
76
+ * taken and evaluate all applicable guards.
77
+ *
78
+ * Uses the actor approach: clones the snapshot via a fresh actor seeded to that state,
79
+ * sends the event, and captures before/after. Guard results are computed by evaluating
80
+ * the guard functions against the pre-send context.
81
+ *
82
+ * @param {object} snapshot - XState snapshot (from actor.getSnapshot())
83
+ * @param {object} event - Event object (must have `type` field)
84
+ * @param {object} machine - The XState machine definition
85
+ * @returns {{
86
+ * currentState: string,
87
+ * expectedNextState: string|null,
88
+ * emptyTransitions: boolean,
89
+ * possibleTransitions: Array<{ guardName: string|null, guardPassed: boolean, guardContext: object }>
90
+ * }}
91
+ */
92
+ function evaluateTransitions(snapshot, event, machine) {
93
+ // Extract current state name (handles string or object shape from XState v4/v5)
94
+ const currentState = typeof snapshot.value === 'string'
95
+ ? snapshot.value
96
+ : Object.keys(snapshot.value)[0];
97
+
98
+ const context = snapshot.context || {};
99
+
100
+ // Look up transitions for this state + event from machine config
101
+ const machineConfig = machine.config || {};
102
+ const states = machineConfig.states || {};
103
+ const stateConfig = states[currentState] || {};
104
+ const eventTransitions = stateConfig.on ? (stateConfig.on[event.type] || stateConfig.on[event.type]) : null;
105
+
106
+ if (!eventTransitions) {
107
+ // No transitions defined for this event in this state
108
+ return {
109
+ currentState,
110
+ expectedNextState: null,
111
+ emptyTransitions: true,
112
+ possibleTransitions: [],
113
+ };
114
+ }
115
+
116
+ const transArray = Array.isArray(eventTransitions) ? eventTransitions : [eventTransitions];
117
+
118
+ if (transArray.length === 0) {
119
+ return {
120
+ currentState,
121
+ expectedNextState: null,
122
+ emptyTransitions: true,
123
+ possibleTransitions: [],
124
+ };
125
+ }
126
+
127
+ // Evaluate each transition's guard
128
+ const possibleTransitions = transArray.map(trans => {
129
+ const guardName = trans.guard || trans.cond || null;
130
+ return evaluateGuard(guardName, context, event, machine);
131
+ });
132
+
133
+ // Determine expected next state: first transition whose guard passes
134
+ let expectedNextState = null;
135
+ for (let i = 0; i < transArray.length; i++) {
136
+ if (possibleTransitions[i].guardPassed) {
137
+ expectedNextState = transArray[i].target || currentState;
138
+ break;
139
+ }
140
+ }
141
+
142
+ return {
143
+ currentState,
144
+ expectedNextState,
145
+ emptyTransitions: false,
146
+ possibleTransitions,
147
+ };
148
+ }
149
+
150
+ // ── Trace replayer ─────────────────────────────────────────────────────────────
151
+
152
+ /**
153
+ * replayTrace: replay a sequence of events through a SINGLE XState actor.
154
+ *
155
+ * This is the key difference from fresh-actor-per-event validation:
156
+ * - Creates ONE actor for the full event sequence
157
+ * - Guards evaluated for event N see context accumulated from events 1..N-1
158
+ * - Enables detection of cross-event interaction bugs (Pitfall 1 prevention)
159
+ *
160
+ * @param {Array<object>} events - Array of event objects (each must have `type` field)
161
+ * @param {object} machine - The XState machine definition
162
+ * @returns {Array<{
163
+ * event: object,
164
+ * snapshotBefore: object,
165
+ * snapshotAfter: object,
166
+ * walkerResult: object (result from evaluateTransitions)
167
+ * }>}
168
+ */
169
+ function replayTrace(events, machine) {
170
+ const { createActor } = loadMachineModule();
171
+ const actor = createActor(machine);
172
+ actor.start();
173
+
174
+ const results = [];
175
+
176
+ for (const event of events) {
177
+ const snapshotBefore = actor.getSnapshot();
178
+
179
+ // Evaluate transitions BEFORE sending (captures pre-send context for guard evaluation)
180
+ const walkerResult = evaluateTransitions(snapshotBefore, event, machine);
181
+
182
+ // Send the event to advance the actor state
183
+ try {
184
+ actor.send(event);
185
+ } catch (_) {
186
+ // Fail-open: if actor errors on this event, continue with remaining events
187
+ }
188
+
189
+ const snapshotAfter = actor.getSnapshot();
190
+
191
+ results.push({
192
+ event,
193
+ snapshotBefore,
194
+ snapshotAfter,
195
+ walkerResult,
196
+ });
197
+ }
198
+
199
+ actor.stop();
200
+ return results;
201
+ }
202
+
203
+ // ── Exports ───────────────────────────────────────────────────────────────────
204
+
205
+ module.exports = { evaluateTransitions, replayTrace, evaluateGuard };