@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,153 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // bin/run-alloy.cjs
4
+ // Invokes Alloy 6 JAR headless for the QGSD vote-counting model.
5
+ // Requirements: ALY-02
6
+ //
7
+ // Usage:
8
+ // node bin/run-alloy.cjs
9
+ //
10
+ // Prerequisites:
11
+ // - Java >=17 (https://adoptium.net/)
12
+ // - .planning/formal/alloy/org.alloytools.alloy.dist.jar (see VERIFICATION_TOOLS.md for download)
13
+
14
+ const { spawnSync } = require('child_process');
15
+ const JAVA_HEAP_MAX = process.env.QGSD_JAVA_HEAP_MAX || '512m';
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const { writeCheckResult } = require('./write-check-result.cjs');
19
+ const { getRequirementIds } = require('./requirement-map.cjs');
20
+
21
+ // ── Resolve project root (--project-root= overrides __dirname-relative) ─────
22
+ let ROOT = path.join(__dirname, '..');
23
+ for (const arg of process.argv) {
24
+ if (arg.startsWith('--project-root=')) ROOT = path.resolve(arg.slice('--project-root='.length));
25
+ }
26
+
27
+ // ── 1. Locate Java ───────────────────────────────────────────────────────────
28
+ const JAVA_HOME = process.env.JAVA_HOME;
29
+ let javaExe;
30
+
31
+ if (JAVA_HOME) {
32
+ javaExe = path.join(JAVA_HOME, 'bin', 'java');
33
+ if (!fs.existsSync(javaExe)) {
34
+ process.stderr.write(
35
+ '[run-alloy] JAVA_HOME is set but java binary not found at: ' + javaExe + '\n' +
36
+ '[run-alloy] Unset JAVA_HOME or fix the path.\n'
37
+ );
38
+ try { writeCheckResult({ tool: 'run-alloy', formalism: 'alloy', result: 'fail', check_id: 'alloy:quorum-votes', surface: 'alloy', property: 'Vote counting correctness — no vote loss, no double counting, unanimous agreement invariants', runtime_ms: 0, summary: 'fail: alloy:quorum-votes (Java not found)', triage_tags: [], requirement_ids: getRequirementIds('alloy:quorum-votes'), metadata: {} }); } catch (e) { process.stderr.write('[run-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
39
+ process.exit(1);
40
+ }
41
+ } else {
42
+ // Fall back to PATH lookup
43
+ const probe = spawnSync('java', ['--version'], { encoding: 'utf8' });
44
+ if (probe.error || probe.status !== 0) {
45
+ process.stderr.write(
46
+ '[run-alloy] Java not found. Install Java >=17 and set JAVA_HOME.\n' +
47
+ '[run-alloy] Download: https://adoptium.net/\n'
48
+ );
49
+ try { writeCheckResult({ tool: 'run-alloy', formalism: 'alloy', result: 'fail', check_id: 'alloy:quorum-votes', surface: 'alloy', property: 'Vote counting correctness — no vote loss, no double counting, unanimous agreement invariants', runtime_ms: 0, summary: 'fail: alloy:quorum-votes (Java not found)', triage_tags: [], requirement_ids: getRequirementIds('alloy:quorum-votes'), metadata: {} }); } catch (e) { process.stderr.write('[run-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
50
+ process.exit(1);
51
+ }
52
+ javaExe = 'java';
53
+ }
54
+
55
+ // ── 2. Check Java version >=17 ───────────────────────────────────────────────
56
+ const versionResult = spawnSync(javaExe, ['--version'], { encoding: 'utf8' });
57
+ if (versionResult.error || versionResult.status !== 0) {
58
+ process.stderr.write('[run-alloy] Failed to run: ' + javaExe + ' --version\n');
59
+ try { writeCheckResult({ tool: 'run-alloy', formalism: 'alloy', result: 'fail', check_id: 'alloy:quorum-votes', surface: 'alloy', property: 'Vote counting correctness — no vote loss, no double counting, unanimous agreement invariants', runtime_ms: 0, summary: 'fail: alloy:quorum-votes (version check failed)', triage_tags: [], requirement_ids: getRequirementIds('alloy:quorum-votes'), metadata: {} }); } catch (e) { process.stderr.write('[run-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
60
+ process.exit(1);
61
+ }
62
+ const versionOutput = versionResult.stdout + versionResult.stderr;
63
+ // Java version string varies: "openjdk 17.0.1 ..." or "java version \"17.0.1\""
64
+ const versionMatch = versionOutput.match(/(?:openjdk\s+|java version\s+[""]?)(\d+)/i);
65
+ const javaMajor = versionMatch ? parseInt(versionMatch[1], 10) : 0;
66
+ if (javaMajor < 17) {
67
+ process.stderr.write(
68
+ '[run-alloy] Java >=17 required. Found: ' + versionOutput.split('\n')[0] + '\n' +
69
+ '[run-alloy] Download Java 17+: https://adoptium.net/\n'
70
+ );
71
+ try { writeCheckResult({ tool: 'run-alloy', formalism: 'alloy', result: 'fail', check_id: 'alloy:quorum-votes', surface: 'alloy', property: 'Vote counting correctness — no vote loss, no double counting, unanimous agreement invariants', runtime_ms: 0, summary: 'fail: alloy:quorum-votes (Java < 17)', triage_tags: [], requirement_ids: getRequirementIds('alloy:quorum-votes'), metadata: {} }); } catch (e) { process.stderr.write('[run-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
72
+ process.exit(1);
73
+ }
74
+
75
+ // ── 3. Locate org.alloytools.alloy.dist.jar ──────────────────────────────────
76
+ const jarPath = path.join(ROOT, '.planning', 'formal', 'alloy', 'org.alloytools.alloy.dist.jar');
77
+ if (!fs.existsSync(jarPath)) {
78
+ process.stderr.write(
79
+ '[run-alloy] org.alloytools.alloy.dist.jar not found at: ' + jarPath + '\n' +
80
+ '[run-alloy] Download Alloy 6.2.0:\n' +
81
+ ' curl -L https://github.com/AlloyTools/org.alloytools.alloy/releases/download/v6.2.0/org.alloytools.alloy.dist.jar \\\n' +
82
+ ' -o .planning/formal/alloy/org.alloytools.alloy.dist.jar\n'
83
+ );
84
+ try { writeCheckResult({ tool: 'run-alloy', formalism: 'alloy', result: 'fail', check_id: 'alloy:quorum-votes', surface: 'alloy', property: 'Vote counting correctness — no vote loss, no double counting, unanimous agreement invariants', runtime_ms: 0, summary: 'fail: alloy:quorum-votes (JAR not found)', triage_tags: [], requirement_ids: getRequirementIds('alloy:quorum-votes'), metadata: {} }); } catch (e) { process.stderr.write('[run-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
85
+ process.exit(1);
86
+ }
87
+
88
+ // ── 4. Locate .planning/formal/alloy/quorum-votes.als ──────────────────────────────────
89
+ const alsPath = path.join(ROOT, '.planning', 'formal', 'alloy', 'quorum-votes.als');
90
+ if (!fs.existsSync(alsPath)) {
91
+ process.stderr.write(
92
+ '[run-alloy] quorum-votes.als not found at: ' + alsPath + '\n' +
93
+ '[run-alloy] This file should exist in the repository. Check your git status.\n'
94
+ );
95
+ try { writeCheckResult({ tool: 'run-alloy', formalism: 'alloy', result: 'fail', check_id: 'alloy:quorum-votes', surface: 'alloy', property: 'Vote counting correctness — no vote loss, no double counting, unanimous agreement invariants', runtime_ms: 0, summary: 'fail: alloy:quorum-votes (ALS not found)', triage_tags: [], requirement_ids: getRequirementIds('alloy:quorum-votes'), metadata: {} }); } catch (e) { process.stderr.write('[run-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
96
+ process.exit(1);
97
+ }
98
+
99
+ // ── 5. Invoke Alloy 6 ────────────────────────────────────────────────────────
100
+ process.stdout.write('[run-alloy] ALS: ' + alsPath + '\n');
101
+ process.stdout.write('[run-alloy] JAR: ' + jarPath + '\n');
102
+
103
+ const _startMs = Date.now();
104
+
105
+ // Use stdio: 'pipe' so we can scan stdout for counterexamples (Alloy exits 0 even on CEX)
106
+ process.stderr.write('[heap] Xms=64m Xmx=' + JAVA_HEAP_MAX + '\n');
107
+ const alloyResult = spawnSync(javaExe, [
108
+ '-Djava.awt.headless=true',
109
+ '-Xms64m', '-Xmx' + JAVA_HEAP_MAX,
110
+ '-jar', jarPath,
111
+ 'exec',
112
+ '--output', '-',
113
+ '--type', 'text',
114
+ '--quiet',
115
+ alsPath,
116
+ ], { encoding: 'utf8', stdio: 'pipe' });
117
+
118
+ if (alloyResult.error) {
119
+ process.stderr.write('[run-alloy] Alloy invocation failed: ' + alloyResult.error.message + '\n');
120
+ const _runtimeMs = Date.now() - _startMs;
121
+ try { writeCheckResult({ tool: 'run-alloy', formalism: 'alloy', result: 'fail', check_id: 'alloy:quorum-votes', surface: 'alloy', property: 'Vote counting correctness — no vote loss, no double counting, unanimous agreement invariants', runtime_ms: _runtimeMs, summary: 'fail: alloy:quorum-votes in ' + _runtimeMs + 'ms', triage_tags: _runtimeMs > 60000 ? ['timeout-risk'] : [], requirement_ids: getRequirementIds('alloy:quorum-votes'), metadata: {} }); } catch (e) { process.stderr.write('[run-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
122
+ process.exit(1);
123
+ }
124
+
125
+ // ── 6. Scan stdout for counterexamples ───────────────────────────────────────
126
+ // Alloy 6 exits 0 even when counterexamples are found. Scan stdout to detect them.
127
+ const stdout = alloyResult.stdout || '';
128
+ const stderr = alloyResult.stderr || '';
129
+
130
+ // Write stdout to process.stdout (mirrors stdio: 'inherit' output)
131
+ if (stdout) { process.stdout.write(stdout); }
132
+ if (stderr) { process.stderr.write(stderr); }
133
+
134
+ if (/Counterexample/i.test(stdout)) {
135
+ process.stderr.write(
136
+ '[run-alloy] WARNING: Counterexample found in quorum-votes.als assertions\n' +
137
+ '[run-alloy] (ThresholdPasses / BelowThresholdFails / ZeroApprovalsFail).\n' +
138
+ '[run-alloy] This indicates a spec violation — review quorum-votes.als.\n'
139
+ );
140
+ const _runtimeMs = Date.now() - _startMs;
141
+ try { writeCheckResult({ tool: 'run-alloy', formalism: 'alloy', result: 'fail', check_id: 'alloy:quorum-votes', surface: 'alloy', property: 'Vote counting correctness — no vote loss, no double counting, unanimous agreement invariants', runtime_ms: _runtimeMs, summary: 'fail: alloy:quorum-votes in ' + _runtimeMs + 'ms', triage_tags: _runtimeMs > 60000 ? ['timeout-risk'] : [], requirement_ids: getRequirementIds('alloy:quorum-votes'), metadata: {} }); } catch (e) { process.stderr.write('[run-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
142
+ process.exit(1);
143
+ }
144
+
145
+ if (alloyResult.status !== 0) {
146
+ const _runtimeMs = Date.now() - _startMs;
147
+ try { writeCheckResult({ tool: 'run-alloy', formalism: 'alloy', result: 'fail', check_id: 'alloy:quorum-votes', surface: 'alloy', property: 'Vote counting correctness — no vote loss, no double counting, unanimous agreement invariants', runtime_ms: _runtimeMs, summary: 'fail: alloy:quorum-votes in ' + _runtimeMs + 'ms', triage_tags: _runtimeMs > 60000 ? ['timeout-risk'] : [], requirement_ids: getRequirementIds('alloy:quorum-votes'), metadata: {} }); } catch (e) { process.stderr.write('[run-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
148
+ process.exit(alloyResult.status || 1);
149
+ }
150
+
151
+ const _runtimeMs = Date.now() - _startMs;
152
+ try { writeCheckResult({ tool: 'run-alloy', formalism: 'alloy', result: 'pass', check_id: 'alloy:quorum-votes', surface: 'alloy', property: 'Vote counting correctness — no vote loss, no double counting, unanimous agreement invariants', runtime_ms: _runtimeMs, summary: 'pass: alloy:quorum-votes in ' + _runtimeMs + 'ms', triage_tags: _runtimeMs > 60000 ? ['timeout-risk'] : [], requirement_ids: getRequirementIds('alloy:quorum-votes'), metadata: {} }); } catch (e) { process.stderr.write('[run-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
153
+ process.exit(0);
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // bin/run-audit-alloy.cjs
4
+ // Invokes Alloy 6 JAR headless for QGSD audit trail specs (GAP-3, GAP-9).
5
+ // Requirements: GAP-3, GAP-9
6
+ //
7
+ // Usage:
8
+ // node bin/run-audit-alloy.cjs [--spec=<name>]
9
+ //
10
+ // Valid spec names:
11
+ // scoreboard-recompute (default) — GAP-3: recomputation idempotency invariants
12
+ // availability-parsing — GAP-9: date arithmetic invariants
13
+ //
14
+ // Prerequisites:
15
+ // - Java >=17 (https://adoptium.net/)
16
+ // - .planning/formal/alloy/org.alloytools.alloy.dist.jar (see VERIFICATION_TOOLS.md for download)
17
+
18
+ const { spawnSync } = require('child_process');
19
+ const JAVA_HEAP_MAX = process.env.QGSD_JAVA_HEAP_MAX || '512m';
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const { writeCheckResult } = require('./write-check-result.cjs');
23
+ const { getRequirementIds } = require('./requirement-map.cjs');
24
+
25
+ // ── Resolve project root (--project-root= overrides __dirname-relative) ─────
26
+ let ROOT = path.join(__dirname, '..');
27
+ for (const arg of process.argv) {
28
+ if (arg.startsWith('--project-root=')) ROOT = path.resolve(arg.slice('--project-root='.length));
29
+ }
30
+
31
+ // ── 0. Parse --spec argument ──────────────────────────────────────────────────
32
+ const VALID_SPECS = ['scoreboard-recompute', 'availability-parsing'];
33
+ const CHECK_ID_MAP = {
34
+ 'scoreboard-recompute': 'alloy:scoreboard',
35
+ 'availability-parsing': 'alloy:availability',
36
+ };
37
+ const PROPERTY_MAP = {
38
+ 'scoreboard-recompute': 'Scoreboard idempotency — no vote loss, no double counting',
39
+ 'availability-parsing': 'Availability hint date arithmetic correctness',
40
+ };
41
+
42
+ const args = process.argv.slice(2);
43
+ const specArg = args.find(a => a.startsWith('--spec=')) || null;
44
+ const specName = specArg
45
+ ? specArg.split('=')[1]
46
+ : (args.find(a => !a.startsWith('-')) || 'scoreboard-recompute');
47
+
48
+ if (!VALID_SPECS.includes(specName)) {
49
+ process.stderr.write(
50
+ '[run-audit-alloy] Unknown spec: ' + specName +
51
+ '. Valid: ' + VALID_SPECS.join(', ') + '\n'
52
+ );
53
+ const check_id = CHECK_ID_MAP[specName] || ('alloy:' + specName);
54
+ try { writeCheckResult({ tool: 'run-audit-alloy', formalism: 'alloy', result: 'fail', check_id: check_id, surface: 'alloy', property: PROPERTY_MAP[specName] || specName, runtime_ms: 0, summary: 'fail: ' + check_id + ' (invalid spec)', triage_tags: [], requirement_ids: getRequirementIds(check_id), metadata: { spec: specName } }); } catch (e) { process.stderr.write('[run-audit-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
55
+ process.exit(1);
56
+ }
57
+
58
+ // ── 1. Locate Java ───────────────────────────────────────────────────────────
59
+ const JAVA_HOME = process.env.JAVA_HOME;
60
+ let javaExe;
61
+
62
+ const check_id = CHECK_ID_MAP[specName];
63
+ const property = PROPERTY_MAP[specName];
64
+
65
+ if (JAVA_HOME) {
66
+ javaExe = path.join(JAVA_HOME, 'bin', 'java');
67
+ if (!fs.existsSync(javaExe)) {
68
+ process.stderr.write(
69
+ '[run-audit-alloy] JAVA_HOME is set but java binary not found at: ' + javaExe + '\n' +
70
+ '[run-audit-alloy] Unset JAVA_HOME or fix the path.\n'
71
+ );
72
+ try { writeCheckResult({ tool: 'run-audit-alloy', formalism: 'alloy', result: 'fail', check_id: check_id, surface: 'alloy', property: property, runtime_ms: 0, summary: 'fail: ' + check_id + ' (Java not found)', triage_tags: [], requirement_ids: getRequirementIds(check_id), metadata: { spec: specName } }); } catch (e) { process.stderr.write('[run-audit-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
73
+ process.exit(1);
74
+ }
75
+ } else {
76
+ // Fall back to PATH lookup
77
+ const probe = spawnSync('java', ['--version'], { encoding: 'utf8' });
78
+ if (probe.error || probe.status !== 0) {
79
+ process.stderr.write(
80
+ '[run-audit-alloy] Java not found. Install Java >=17 and set JAVA_HOME.\n' +
81
+ '[run-audit-alloy] Download: https://adoptium.net/\n'
82
+ );
83
+ try { writeCheckResult({ tool: 'run-audit-alloy', formalism: 'alloy', result: 'fail', check_id: check_id, surface: 'alloy', property: property, runtime_ms: 0, summary: 'fail: ' + check_id + ' (Java not found)', triage_tags: [], requirement_ids: getRequirementIds(check_id), metadata: { spec: specName } }); } catch (e) { process.stderr.write('[run-audit-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
84
+ process.exit(1);
85
+ }
86
+ javaExe = 'java';
87
+ }
88
+
89
+ // ── 2. Check Java version >=17 ───────────────────────────────────────────────
90
+ const versionResult = spawnSync(javaExe, ['--version'], { encoding: 'utf8' });
91
+ if (versionResult.error || versionResult.status !== 0) {
92
+ process.stderr.write('[run-audit-alloy] Failed to run: ' + javaExe + ' --version\n');
93
+ try { writeCheckResult({ tool: 'run-audit-alloy', formalism: 'alloy', result: 'fail', check_id: check_id, surface: 'alloy', property: property, runtime_ms: 0, summary: 'fail: ' + check_id + ' (version check failed)', triage_tags: [], requirement_ids: getRequirementIds(check_id), metadata: { spec: specName } }); } catch (e) { process.stderr.write('[run-audit-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
94
+ process.exit(1);
95
+ }
96
+ const versionOutput = versionResult.stdout + versionResult.stderr;
97
+ // Java version string varies: "openjdk 17.0.1 ..." or "java version \"17.0.1\""
98
+ const versionMatch = versionOutput.match(/(?:openjdk\s+|java version\s+[""]?)(\d+)/i);
99
+ const javaMajor = versionMatch ? parseInt(versionMatch[1], 10) : 0;
100
+ if (javaMajor < 17) {
101
+ process.stderr.write(
102
+ '[run-audit-alloy] Java >=17 required. Found: ' + versionOutput.split('\n')[0] + '\n' +
103
+ '[run-audit-alloy] Download Java 17+: https://adoptium.net/\n'
104
+ );
105
+ try { writeCheckResult({ tool: 'run-audit-alloy', formalism: 'alloy', result: 'fail', check_id: check_id, surface: 'alloy', property: property, runtime_ms: 0, summary: 'fail: ' + check_id + ' (Java < 17)', triage_tags: [], requirement_ids: getRequirementIds(check_id), metadata: { spec: specName } }); } catch (e) { process.stderr.write('[run-audit-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
106
+ process.exit(1);
107
+ }
108
+
109
+ // ── 3. Locate org.alloytools.alloy.dist.jar ──────────────────────────────────
110
+ const jarPath = path.join(ROOT, '.planning', 'formal', 'alloy', 'org.alloytools.alloy.dist.jar');
111
+ if (!fs.existsSync(jarPath)) {
112
+ process.stderr.write(
113
+ '[run-audit-alloy] org.alloytools.alloy.dist.jar not found at: ' + jarPath + '\n' +
114
+ '[run-audit-alloy] Download Alloy 6.2.0:\n' +
115
+ ' curl -L https://github.com/AlloyTools/org.alloytools.alloy/releases/download/v6.2.0/org.alloytools.alloy.dist.jar \\\n' +
116
+ ' -o .planning/formal/alloy/org.alloytools.alloy.dist.jar\n'
117
+ );
118
+ try { writeCheckResult({ tool: 'run-audit-alloy', formalism: 'alloy', result: 'fail', check_id: check_id, surface: 'alloy', property: property, runtime_ms: 0, summary: 'fail: ' + check_id + ' (JAR not found)', triage_tags: [], requirement_ids: getRequirementIds(check_id), metadata: { spec: specName } }); } catch (e) { process.stderr.write('[run-audit-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
119
+ process.exit(1);
120
+ }
121
+
122
+ // ── 4. Locate .als file ───────────────────────────────────────────────────────
123
+ const alsPath = path.join(ROOT, '.planning', 'formal', 'alloy', specName + '.als');
124
+ if (!fs.existsSync(alsPath)) {
125
+ process.stderr.write(
126
+ '[run-audit-alloy] ' + specName + '.als not found at: ' + alsPath + '\n' +
127
+ '[run-audit-alloy] This file should exist in the repository. Check your git status.\n'
128
+ );
129
+ try { writeCheckResult({ tool: 'run-audit-alloy', formalism: 'alloy', result: 'fail', check_id: check_id, surface: 'alloy', property: property, runtime_ms: 0, summary: 'fail: ' + check_id + ' (ALS not found)', triage_tags: [], requirement_ids: getRequirementIds(check_id), metadata: { spec: specName } }); } catch (e) { process.stderr.write('[run-audit-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
130
+ process.exit(1);
131
+ }
132
+
133
+ // ── 5. Invoke Alloy 6 ────────────────────────────────────────────────────────
134
+ process.stdout.write('[run-audit-alloy] spec: ' + specName + '\n');
135
+ process.stdout.write('[run-audit-alloy] ALS: ' + alsPath + '\n');
136
+ process.stdout.write('[run-audit-alloy] JAR: ' + jarPath + '\n');
137
+
138
+ const _startMs = Date.now();
139
+
140
+ // Use stdio: 'pipe' so we can scan stdout for counterexamples (Alloy exits 0 even on CEX)
141
+ process.stderr.write('[heap] Xms=64m Xmx=' + JAVA_HEAP_MAX + '\n');
142
+ const alloyResult = spawnSync(javaExe, [
143
+ '-Djava.awt.headless=true',
144
+ '-Xms64m', '-Xmx' + JAVA_HEAP_MAX,
145
+ '-jar', jarPath,
146
+ 'exec',
147
+ '--output', '-',
148
+ '--type', 'text',
149
+ '--quiet',
150
+ alsPath,
151
+ ], { encoding: 'utf8', stdio: 'pipe' });
152
+
153
+ if (alloyResult.error) {
154
+ process.stderr.write('[run-audit-alloy] Alloy invocation failed: ' + alloyResult.error.message + '\n');
155
+ const _runtimeMs = Date.now() - _startMs;
156
+ try { writeCheckResult({ tool: 'run-audit-alloy', formalism: 'alloy', result: 'fail', check_id: check_id, surface: 'alloy', property: property, runtime_ms: _runtimeMs, summary: 'fail: ' + check_id + ' in ' + _runtimeMs + 'ms', triage_tags: _runtimeMs > 60000 ? ['timeout-risk'] : [], requirement_ids: getRequirementIds(check_id), metadata: { spec: specName } }); } catch (e) { process.stderr.write('[run-audit-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
157
+ process.exit(1);
158
+ }
159
+
160
+ // ── 6. Scan stdout for counterexamples ───────────────────────────────────────
161
+ // Alloy 6 exits 0 even when counterexamples are found. Scan stdout to detect them.
162
+ const stdout = alloyResult.stdout || '';
163
+ const stderr = alloyResult.stderr || '';
164
+
165
+ // Write stdout to process.stdout (mirrors stdio: 'inherit' output)
166
+ if (stdout) { process.stdout.write(stdout); }
167
+ if (stderr) { process.stderr.write(stderr); }
168
+
169
+ if (/Counterexample/i.test(stdout)) {
170
+ process.stderr.write(
171
+ '[run-audit-alloy] WARNING: Counterexample found in ' + specName + '.als assertion.\n' +
172
+ '[run-audit-alloy] This indicates a spec violation — review .planning/formal/alloy/' + specName + '.als.\n'
173
+ );
174
+ const _runtimeMs = Date.now() - _startMs;
175
+ try { writeCheckResult({ tool: 'run-audit-alloy', formalism: 'alloy', result: 'fail', check_id: check_id, surface: 'alloy', property: property, runtime_ms: _runtimeMs, summary: 'fail: ' + check_id + ' in ' + _runtimeMs + 'ms', triage_tags: _runtimeMs > 60000 ? ['timeout-risk'] : [], requirement_ids: getRequirementIds(check_id), metadata: { spec: specName } }); } catch (e) { process.stderr.write('[run-audit-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
176
+ process.exit(1);
177
+ }
178
+
179
+ if (alloyResult.status !== 0) {
180
+ const _runtimeMs = Date.now() - _startMs;
181
+ try { writeCheckResult({ tool: 'run-audit-alloy', formalism: 'alloy', result: 'fail', check_id: check_id, surface: 'alloy', property: property, runtime_ms: _runtimeMs, summary: 'fail: ' + check_id + ' in ' + _runtimeMs + 'ms', triage_tags: _runtimeMs > 60000 ? ['timeout-risk'] : [], requirement_ids: getRequirementIds(check_id), metadata: { spec: specName } }); } catch (e) { process.stderr.write('[run-audit-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
182
+ process.exit(alloyResult.status || 1);
183
+ }
184
+
185
+ const _runtimeMs = Date.now() - _startMs;
186
+ try { writeCheckResult({ tool: 'run-audit-alloy', formalism: 'alloy', result: 'pass', check_id: check_id, surface: 'alloy', property: property, runtime_ms: _runtimeMs, summary: 'pass: ' + check_id + ' in ' + _runtimeMs + 'ms', triage_tags: _runtimeMs > 60000 ? ['timeout-risk'] : [], requirement_ids: getRequirementIds(check_id), metadata: { spec: specName } }); } catch (e) { process.stderr.write('[run-audit-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
187
+ process.exit(0);
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // bin/run-breaker-tlc.cjs
4
+ // Invokes TLC model checker for the QGSD circuit breaker TLA+ specification.
5
+ // Requirements: QT-105
6
+ //
7
+ // Usage:
8
+ // node bin/run-breaker-tlc.cjs MCbreaker # safety + liveness check (~30s)
9
+ // node bin/run-breaker-tlc.cjs --config=MCbreaker
10
+ //
11
+ // Prerequisites:
12
+ // - Java >=17 (https://adoptium.net/)
13
+ // - .planning/formal/tla/tla2tools.jar (see .planning/formal/tla/README.md for download command)
14
+
15
+ const { spawnSync } = require('child_process');
16
+ const JAVA_HEAP_MAX = process.env.QGSD_JAVA_HEAP_MAX || '512m';
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const { writeCheckResult } = require('./write-check-result.cjs');
20
+ const { detectLivenessProperties } = require('./run-tlc.cjs');
21
+ const { getRequirementIds } = require('./requirement-map.cjs');
22
+
23
+ // ── Resolve project root (--project-root= overrides __dirname-relative) ─────
24
+ let ROOT = path.join(__dirname, '..');
25
+ for (const arg of process.argv) {
26
+ if (arg.startsWith('--project-root=')) ROOT = path.resolve(arg.slice('--project-root='.length));
27
+ }
28
+
29
+ // ── Parse --config argument ──────────────────────────────────────────────────
30
+ const args = process.argv.slice(2);
31
+ const configArg = args.find(a => a.startsWith('--config=')) || null;
32
+ const configName = configArg
33
+ ? configArg.split('=')[1]
34
+ : (args.find(a => !a.startsWith('-')) || 'MCbreaker');
35
+
36
+ const VALID_CONFIGS = ['MCbreaker'];
37
+ if (!VALID_CONFIGS.includes(configName)) {
38
+ process.stderr.write(
39
+ '[run-breaker-tlc] Unknown config: ' + configName +
40
+ '. Valid: ' + VALID_CONFIGS.join(', ') + '\n'
41
+ );
42
+ const _startMs = Date.now();
43
+ const _runtimeMs = 0;
44
+ try { writeCheckResult({ tool: 'run-breaker-tlc', formalism: 'tla', result: 'fail', check_id: 'tla:breaker', surface: 'tla', property: 'Circuit breaker state persistence and oscillation detection algorithm', runtime_ms: _runtimeMs, summary: 'fail: unknown config in ' + _runtimeMs + 'ms', requirement_ids: getRequirementIds('tla:breaker'), metadata: { config: configName } }); } catch (e) { process.stderr.write('[run-breaker-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
45
+ process.exit(1);
46
+ }
47
+
48
+ // ── 1. Locate Java ───────────────────────────────────────────────────────────
49
+ const JAVA_HOME = process.env.JAVA_HOME;
50
+ let javaExe;
51
+
52
+ if (JAVA_HOME) {
53
+ javaExe = path.join(JAVA_HOME, 'bin', 'java');
54
+ if (!fs.existsSync(javaExe)) {
55
+ process.stderr.write(
56
+ '[run-breaker-tlc] JAVA_HOME is set but java binary not found at: ' + javaExe + '\n' +
57
+ '[run-breaker-tlc] Unset JAVA_HOME or fix the path.\n'
58
+ );
59
+ const _startMs = Date.now();
60
+ const _runtimeMs = 0;
61
+ try { writeCheckResult({ tool: 'run-breaker-tlc', formalism: 'tla', result: 'fail', check_id: 'tla:breaker', surface: 'tla', property: 'Circuit breaker state persistence and oscillation detection algorithm', runtime_ms: _runtimeMs, summary: 'fail: Java not found in ' + _runtimeMs + 'ms', requirement_ids: getRequirementIds('tla:breaker'), metadata: {} }); } catch (e) { process.stderr.write('[run-breaker-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
62
+ process.exit(1);
63
+ }
64
+ } else {
65
+ // Fall back to PATH lookup
66
+ const probe = spawnSync('java', ['--version'], { encoding: 'utf8' });
67
+ if (probe.error || probe.status !== 0) {
68
+ process.stderr.write(
69
+ '[run-breaker-tlc] Java not found. Install Java >=17 and set JAVA_HOME.\n' +
70
+ '[run-breaker-tlc] Download: https://adoptium.net/\n'
71
+ );
72
+ const _startMs = Date.now();
73
+ const _runtimeMs = 0;
74
+ try { writeCheckResult({ tool: 'run-breaker-tlc', formalism: 'tla', result: 'fail', check_id: 'tla:breaker', surface: 'tla', property: 'Circuit breaker state persistence and oscillation detection algorithm', runtime_ms: _runtimeMs, summary: 'fail: Java not found in ' + _runtimeMs + 'ms', requirement_ids: getRequirementIds('tla:breaker'), metadata: {} }); } catch (e) { process.stderr.write('[run-breaker-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
75
+ process.exit(1);
76
+ }
77
+ javaExe = 'java';
78
+ }
79
+
80
+ // ── 2. Check Java version >=17 ───────────────────────────────────────────────
81
+ const versionResult = spawnSync(javaExe, ['--version'], { encoding: 'utf8' });
82
+ if (versionResult.error || versionResult.status !== 0) {
83
+ process.stderr.write('[run-breaker-tlc] Failed to run: ' + javaExe + ' --version\n');
84
+ const _startMs = Date.now();
85
+ const _runtimeMs = 0;
86
+ try { writeCheckResult({ tool: 'run-breaker-tlc', formalism: 'tla', result: 'fail', check_id: 'tla:breaker', surface: 'tla', property: 'Circuit breaker state persistence and oscillation detection algorithm', runtime_ms: _runtimeMs, summary: 'fail: Java version check failed in ' + _runtimeMs + 'ms', requirement_ids: getRequirementIds('tla:breaker'), metadata: {} }); } catch (e) { process.stderr.write('[run-breaker-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
87
+ process.exit(1);
88
+ }
89
+ const versionOutput = versionResult.stdout + versionResult.stderr;
90
+ // Java version string varies: "openjdk 17.0.1 ..." or "java version \"17.0.1\""
91
+ const versionMatch = versionOutput.match(/(?:openjdk\s+|java version\s+[""]?)(\d+)/i);
92
+ const javaMajor = versionMatch ? parseInt(versionMatch[1], 10) : 0;
93
+ if (javaMajor < 17) {
94
+ process.stderr.write(
95
+ '[run-breaker-tlc] Java >=17 required. Found: ' + versionOutput.split('\n')[0] + '\n' +
96
+ '[run-breaker-tlc] Download Java 17+: https://adoptium.net/\n'
97
+ );
98
+ const _startMs = Date.now();
99
+ const _runtimeMs = 0;
100
+ try { writeCheckResult({ tool: 'run-breaker-tlc', formalism: 'tla', result: 'fail', check_id: 'tla:breaker', surface: 'tla', property: 'Circuit breaker state persistence and oscillation detection algorithm', runtime_ms: _runtimeMs, summary: 'fail: Java ' + javaMajor + ' < 17 in ' + _runtimeMs + 'ms', requirement_ids: getRequirementIds('tla:breaker'), metadata: {} }); } catch (e) { process.stderr.write('[run-breaker-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
101
+ process.exit(1);
102
+ }
103
+
104
+ // ── 3. Locate tla2tools.jar ──────────────────────────────────────────────────
105
+ const jarPath = path.join(ROOT, '.planning', 'formal', 'tla', 'tla2tools.jar');
106
+ if (!fs.existsSync(jarPath)) {
107
+ process.stderr.write(
108
+ '[run-breaker-tlc] tla2tools.jar not found at: ' + jarPath + '\n' +
109
+ '[run-breaker-tlc] Download v1.8.0:\n' +
110
+ ' curl -L https://github.com/tlaplus/tlaplus/releases/download/v1.8.0/tla2tools.jar \\\n' +
111
+ ' -o .planning/formal/tla/tla2tools.jar\n'
112
+ );
113
+ const _startMs = Date.now();
114
+ const _runtimeMs = 0;
115
+ try { writeCheckResult({ tool: 'run-breaker-tlc', formalism: 'tla', result: 'fail', check_id: 'tla:breaker', surface: 'tla', property: 'Circuit breaker state persistence and oscillation detection algorithm', runtime_ms: _runtimeMs, summary: 'fail: tla2tools.jar not found in ' + _runtimeMs + 'ms', requirement_ids: getRequirementIds('tla:breaker'), metadata: {} }); } catch (e) { process.stderr.write('[run-breaker-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
116
+ process.exit(1);
117
+ }
118
+
119
+ // ── 4. Invoke TLC ────────────────────────────────────────────────────────────
120
+ const specPath = path.join(ROOT, '.planning', 'formal', 'tla', 'QGSDCircuitBreaker.tla');
121
+ const cfgPath = path.join(ROOT, '.planning', 'formal', 'tla', configName + '.cfg');
122
+ // Always use 'auto' workers — MCbreaker has a small state space and liveness
123
+ // can safely run with multiple workers (no known multi-worker liveness bugs at this scale).
124
+ const workers = 'auto';
125
+
126
+ process.stdout.write('[run-breaker-tlc] Config: ' + configName + ' Workers: ' + workers + '\n');
127
+ process.stdout.write('[run-breaker-tlc] Spec: ' + specPath + '\n');
128
+ process.stdout.write('[run-breaker-tlc] Cfg: ' + cfgPath + '\n');
129
+
130
+ const _startMs = Date.now();
131
+ process.stderr.write('[heap] Xms=64m Xmx=' + JAVA_HEAP_MAX + '\n');
132
+ const tlcResult = spawnSync(javaExe, [
133
+ '-Xms64m', '-Xmx' + JAVA_HEAP_MAX,
134
+ '-jar', jarPath,
135
+ '-config', cfgPath,
136
+ '-workers', workers,
137
+ specPath,
138
+ ], { encoding: 'utf8', stdio: 'inherit' });
139
+ const _runtimeMs = Date.now() - _startMs;
140
+
141
+ if (tlcResult.error) {
142
+ process.stderr.write('[run-breaker-tlc] TLC invocation failed: ' + tlcResult.error.message + '\n');
143
+ try { writeCheckResult({ tool: 'run-breaker-tlc', formalism: 'tla', result: 'fail', check_id: 'tla:breaker', surface: 'tla', property: 'Circuit breaker state persistence and oscillation detection algorithm', runtime_ms: _runtimeMs, summary: 'fail: TLC invocation failed in ' + _runtimeMs + 'ms', requirement_ids: getRequirementIds('tla:breaker'), metadata: {} }); } catch (e) { process.stderr.write('[run-breaker-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
144
+ process.exit(1);
145
+ }
146
+
147
+ const passed = (tlcResult.status || 0) === 0;
148
+ const triage_tags = _runtimeMs > 120000 ? ['timeout-risk'] : [];
149
+
150
+ if (passed) {
151
+ const missingDeclarations = detectLivenessProperties(configName, cfgPath);
152
+ if (missingDeclarations.length > 0) {
153
+ try {
154
+ writeCheckResult({
155
+ tool: 'run-breaker-tlc',
156
+ formalism: 'tla',
157
+ result: 'inconclusive',
158
+ check_id: 'tla:breaker',
159
+ surface: 'tla',
160
+ property: 'Circuit breaker state persistence and oscillation detection algorithm',
161
+ runtime_ms: _runtimeMs,
162
+ summary: 'inconclusive: fairness missing in ' + _runtimeMs + 'ms',
163
+ triage_tags: ['needs-fairness'],
164
+ requirement_ids: getRequirementIds('tla:breaker'),
165
+ metadata: {
166
+ config: configName,
167
+ reason: 'Fairness declaration missing for: ' + missingDeclarations.join(', '),
168
+ }
169
+ });
170
+ } catch (e) {
171
+ process.stderr.write('[run-breaker-tlc] Warning: failed to write inconclusive result: ' + e.message + '\n');
172
+ }
173
+ process.stdout.write('[run-breaker-tlc] Result: inconclusive — fairness declaration missing for: ' + missingDeclarations.join(', ') + '\n');
174
+ process.exit(0);
175
+ }
176
+ try { writeCheckResult({ tool: 'run-breaker-tlc', formalism: 'tla', result: 'pass', check_id: 'tla:breaker', surface: 'tla', property: 'Circuit breaker state persistence and oscillation detection algorithm', runtime_ms: _runtimeMs, summary: 'pass: ' + configName + ' in ' + _runtimeMs + 'ms', triage_tags: triage_tags, requirement_ids: getRequirementIds('tla:breaker'), metadata: {} }); } catch (e) { process.stderr.write('[run-breaker-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
177
+ process.exit(0);
178
+ } else {
179
+ try { writeCheckResult({ tool: 'run-breaker-tlc', formalism: 'tla', result: 'fail', check_id: 'tla:breaker', surface: 'tla', property: 'Circuit breaker state persistence and oscillation detection algorithm', runtime_ms: _runtimeMs, summary: 'fail: ' + configName + ' in ' + _runtimeMs + 'ms', triage_tags: triage_tags, requirement_ids: getRequirementIds('tla:breaker'), metadata: {} }); } catch (e) { process.stderr.write('[run-breaker-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
180
+ process.exit(tlcResult.status || 0);
181
+ }