@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,201 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // bin/run-protocol-tlc.cjs
4
+ // Invokes TLC model checker for QGSD protocol termination TLA+ specifications.
5
+ // Requirements: GAP-2, GAP-6
6
+ //
7
+ // Usage:
8
+ // node bin/run-protocol-tlc.cjs MCdeliberation # R3/R3.6 deliberation protocol (liveness, -workers 1)
9
+ // node bin/run-protocol-tlc.cjs MCprefilter # R4 pre-filter protocol (liveness, -workers 1)
10
+ // node bin/run-protocol-tlc.cjs --config=MCdeliberation
11
+ //
12
+ // Prerequisites:
13
+ // - Java >=17 (https://adoptium.net/)
14
+ // - .planning/formal/tla/tla2tools.jar (see .planning/formal/tla/README.md for download command)
15
+
16
+ const { spawnSync } = require('child_process');
17
+ const JAVA_HEAP_MAX = process.env.QGSD_JAVA_HEAP_MAX || '512m';
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const { writeCheckResult } = require('./write-check-result.cjs');
21
+ const { detectLivenessProperties } = require('./run-tlc.cjs');
22
+ const { getRequirementIds } = require('./requirement-map.cjs');
23
+
24
+ // ── Resolve project root (--project-root= overrides __dirname-relative) ─────
25
+ let ROOT = path.join(__dirname, '..');
26
+ for (const arg of process.argv) {
27
+ if (arg.startsWith('--project-root=')) ROOT = path.resolve(arg.slice('--project-root='.length));
28
+ }
29
+
30
+ const CHECK_ID_MAP = {
31
+ 'MCdeliberation': 'tla:deliberation',
32
+ 'MCprefilter': 'tla:prefilter',
33
+ };
34
+
35
+ const PROPERTY_MAP = {
36
+ 'MCdeliberation': 'R3 deliberation loop — max 10 rounds + 10 improvement iterations',
37
+ 'MCprefilter': 'R4 pre-filter protocol — max 3 rounds bounded termination',
38
+ };
39
+
40
+ // ── Parse --config argument ──────────────────────────────────────────────────
41
+ const args = process.argv.slice(2);
42
+ const configArg = args.find(a => a.startsWith('--config=')) || null;
43
+ const configName = configArg
44
+ ? configArg.split('=')[1]
45
+ : (args.find(a => !a.startsWith('-')) || 'MCdeliberation');
46
+
47
+ const VALID_CONFIGS = ['MCdeliberation', 'MCprefilter'];
48
+ if (!VALID_CONFIGS.includes(configName)) {
49
+ process.stderr.write(
50
+ '[run-protocol-tlc] Unknown config: ' + configName +
51
+ '. Valid: ' + VALID_CONFIGS.join(', ') + '\n'
52
+ );
53
+ const _startMs = Date.now();
54
+ const _runtimeMs = 0;
55
+ try { writeCheckResult({ tool: 'run-protocol-tlc', formalism: 'tla', result: 'fail', check_id: CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase()), surface: 'tla', property: PROPERTY_MAP[configName] || configName, runtime_ms: _runtimeMs, summary: 'fail: unknown config in ' + _runtimeMs + 'ms', requirement_ids: getRequirementIds(CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase())), metadata: { config: configName } }); } catch (e) { process.stderr.write('[run-protocol-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
56
+ process.exit(1);
57
+ }
58
+
59
+ // ── 1. Locate Java ───────────────────────────────────────────────────────────
60
+ const JAVA_HOME = process.env.JAVA_HOME;
61
+ let javaExe;
62
+
63
+ if (JAVA_HOME) {
64
+ javaExe = path.join(JAVA_HOME, 'bin', 'java');
65
+ if (!fs.existsSync(javaExe)) {
66
+ process.stderr.write(
67
+ '[run-protocol-tlc] JAVA_HOME is set but java binary not found at: ' + javaExe + '\n' +
68
+ '[run-protocol-tlc] Unset JAVA_HOME or fix the path.\n'
69
+ );
70
+ const _startMs = Date.now();
71
+ const _runtimeMs = 0;
72
+ try { writeCheckResult({ tool: 'run-protocol-tlc', formalism: 'tla', result: 'fail', check_id: CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase()), surface: 'tla', property: PROPERTY_MAP[configName] || configName, runtime_ms: _runtimeMs, summary: 'fail: Java not found in ' + _runtimeMs + 'ms', requirement_ids: getRequirementIds(CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase())), metadata: { config: configName } }); } catch (e) { process.stderr.write('[run-protocol-tlc] 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-protocol-tlc] Java not found. Install Java >=17 and set JAVA_HOME.\n' +
81
+ '[run-protocol-tlc] Download: https://adoptium.net/\n'
82
+ );
83
+ const _startMs = Date.now();
84
+ const _runtimeMs = 0;
85
+ try { writeCheckResult({ tool: 'run-protocol-tlc', formalism: 'tla', result: 'fail', check_id: CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase()), surface: 'tla', property: PROPERTY_MAP[configName] || configName, runtime_ms: _runtimeMs, summary: 'fail: Java not found in ' + _runtimeMs + 'ms', requirement_ids: getRequirementIds(CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase())), metadata: { config: configName } }); } catch (e) { process.stderr.write('[run-protocol-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
86
+ process.exit(1);
87
+ }
88
+ javaExe = 'java';
89
+ }
90
+
91
+ // ── 2. Check Java version >=17 ───────────────────────────────────────────────
92
+ const versionResult = spawnSync(javaExe, ['--version'], { encoding: 'utf8' });
93
+ if (versionResult.error || versionResult.status !== 0) {
94
+ process.stderr.write('[run-protocol-tlc] Failed to run: ' + javaExe + ' --version\n');
95
+ const _startMs = Date.now();
96
+ const _runtimeMs = 0;
97
+ try { writeCheckResult({ tool: 'run-protocol-tlc', formalism: 'tla', result: 'fail', check_id: CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase()), surface: 'tla', property: PROPERTY_MAP[configName] || configName, runtime_ms: _runtimeMs, summary: 'fail: Java version check failed in ' + _runtimeMs + 'ms', requirement_ids: getRequirementIds(CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase())), metadata: { config: configName } }); } catch (e) { process.stderr.write('[run-protocol-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
98
+ process.exit(1);
99
+ }
100
+ const versionOutput = versionResult.stdout + versionResult.stderr;
101
+ // Java version string varies: "openjdk 17.0.1 ..." or "java version \"17.0.1\""
102
+ const versionMatch = versionOutput.match(/(?:openjdk\s+|java version\s+[""]?)(\d+)/i);
103
+ const javaMajor = versionMatch ? parseInt(versionMatch[1], 10) : 0;
104
+ if (javaMajor < 17) {
105
+ process.stderr.write(
106
+ '[run-protocol-tlc] Java >=17 required. Found: ' + versionOutput.split('\n')[0] + '\n' +
107
+ '[run-protocol-tlc] Download Java 17+: https://adoptium.net/\n'
108
+ );
109
+ const _startMs = Date.now();
110
+ const _runtimeMs = 0;
111
+ try { writeCheckResult({ tool: 'run-protocol-tlc', formalism: 'tla', result: 'fail', check_id: CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase()), surface: 'tla', property: PROPERTY_MAP[configName] || configName, runtime_ms: _runtimeMs, summary: 'fail: Java ' + javaMajor + ' < 17 in ' + _runtimeMs + 'ms', requirement_ids: getRequirementIds(CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase())), metadata: { config: configName } }); } catch (e) { process.stderr.write('[run-protocol-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
112
+ process.exit(1);
113
+ }
114
+
115
+ // ── 3. Locate tla2tools.jar ──────────────────────────────────────────────────
116
+ const jarPath = path.join(ROOT, '.planning', 'formal', 'tla', 'tla2tools.jar');
117
+ if (!fs.existsSync(jarPath)) {
118
+ process.stderr.write(
119
+ '[run-protocol-tlc] tla2tools.jar not found at: ' + jarPath + '\n' +
120
+ '[run-protocol-tlc] Download v1.8.0:\n' +
121
+ ' curl -L https://github.com/tlaplus/tlaplus/releases/download/v1.8.0/tla2tools.jar \\\n' +
122
+ ' -o .planning/formal/tla/tla2tools.jar\n'
123
+ );
124
+ const _startMs = Date.now();
125
+ const _runtimeMs = 0;
126
+ try { writeCheckResult({ tool: 'run-protocol-tlc', formalism: 'tla', result: 'fail', check_id: CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase()), surface: 'tla', property: PROPERTY_MAP[configName] || configName, runtime_ms: _runtimeMs, summary: 'fail: tla2tools.jar not found in ' + _runtimeMs + 'ms', requirement_ids: getRequirementIds(CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase())), metadata: { config: configName } }); } catch (e) { process.stderr.write('[run-protocol-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
127
+ process.exit(1);
128
+ }
129
+
130
+ // ── 4. Resolve spec and config paths ─────────────────────────────────────────
131
+ const specFileName = configName === 'MCdeliberation'
132
+ ? 'QGSDDeliberation.tla'
133
+ : 'QGSDPreFilter.tla';
134
+ const specPath = path.join(ROOT, '.planning', 'formal', 'tla', specFileName);
135
+ const cfgPath = path.join(ROOT, '.planning', 'formal', 'tla', configName + '.cfg');
136
+
137
+ // Both MCdeliberation and MCprefilter have PROPERTY (liveness) — always use -workers 1.
138
+ // This avoids the TLC multi-worker liveness checking bug for both specs.
139
+ const workers = '1';
140
+
141
+ // ── 5. Invoke TLC ────────────────────────────────────────────────────────────
142
+ process.stdout.write('[run-protocol-tlc] Config: ' + configName + ' Workers: ' + workers + '\n');
143
+ process.stdout.write('[run-protocol-tlc] Spec: ' + specPath + '\n');
144
+ process.stdout.write('[run-protocol-tlc] Cfg: ' + cfgPath + '\n');
145
+
146
+ const _startMs = Date.now();
147
+ process.stderr.write('[heap] Xms=64m Xmx=' + JAVA_HEAP_MAX + '\n');
148
+ const tlcResult = spawnSync(javaExe, [
149
+ '-Xms64m', '-Xmx' + JAVA_HEAP_MAX,
150
+ '-jar', jarPath,
151
+ '-config', cfgPath,
152
+ '-workers', workers,
153
+ specPath,
154
+ ], { encoding: 'utf8', stdio: 'inherit' });
155
+ const _runtimeMs = Date.now() - _startMs;
156
+
157
+ if (tlcResult.error) {
158
+ process.stderr.write('[run-protocol-tlc] TLC invocation failed: ' + tlcResult.error.message + '\n');
159
+ const check_id = CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase());
160
+ const property = PROPERTY_MAP[configName] || configName;
161
+ try { writeCheckResult({ tool: 'run-protocol-tlc', formalism: 'tla', result: 'fail', check_id: check_id, surface: 'tla', property: property, runtime_ms: _runtimeMs, summary: 'fail: TLC invocation failed in ' + _runtimeMs + 'ms', requirement_ids: getRequirementIds(check_id), metadata: { config: configName } }); } catch (e) { process.stderr.write('[run-protocol-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
162
+ process.exit(1);
163
+ }
164
+
165
+ const passed = (tlcResult.status || 0) === 0;
166
+ const check_id = CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase());
167
+ const property = PROPERTY_MAP[configName] || configName;
168
+ const triage_tags = _runtimeMs > 120000 ? ['timeout-risk'] : [];
169
+
170
+ if (passed) {
171
+ const missingDeclarations = detectLivenessProperties(configName, cfgPath);
172
+ if (missingDeclarations.length > 0) {
173
+ try {
174
+ writeCheckResult({
175
+ tool: 'run-protocol-tlc',
176
+ formalism: 'tla',
177
+ result: 'inconclusive',
178
+ check_id: check_id,
179
+ surface: 'tla',
180
+ property: property,
181
+ runtime_ms: _runtimeMs,
182
+ summary: 'inconclusive: fairness missing in ' + _runtimeMs + 'ms',
183
+ triage_tags: ['needs-fairness'],
184
+ requirement_ids: getRequirementIds(check_id),
185
+ metadata: {
186
+ config: configName,
187
+ reason: 'Fairness declaration missing for: ' + missingDeclarations.join(', '),
188
+ }
189
+ });
190
+ } catch (e) {
191
+ process.stderr.write('[run-protocol-tlc] Warning: failed to write inconclusive result: ' + e.message + '\n');
192
+ }
193
+ process.stdout.write('[run-protocol-tlc] Result: inconclusive — fairness declaration missing for: ' + missingDeclarations.join(', ') + '\n');
194
+ process.exit(0);
195
+ }
196
+ try { writeCheckResult({ tool: 'run-protocol-tlc', formalism: 'tla', result: 'pass', check_id: check_id, surface: 'tla', property: property, runtime_ms: _runtimeMs, summary: 'pass: ' + configName + ' in ' + _runtimeMs + 'ms', triage_tags: triage_tags, requirement_ids: getRequirementIds(check_id), metadata: { config: configName } }); } catch (e) { process.stderr.write('[run-protocol-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
197
+ process.exit(0);
198
+ } else {
199
+ try { writeCheckResult({ tool: 'run-protocol-tlc', formalism: 'tla', result: 'fail', check_id: check_id, surface: 'tla', property: property, runtime_ms: _runtimeMs, summary: 'fail: ' + configName + ' in ' + _runtimeMs + 'ms', triage_tags: triage_tags, requirement_ids: getRequirementIds(check_id), metadata: { config: configName } }); } catch (e) { process.stderr.write('[run-protocol-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
200
+ process.exit(tlcResult.status || 0);
201
+ }
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // bin/run-quorum-composition-alloy.cjs
4
+ // Invokes Alloy 6 JAR headless for the QGSD quorum composition model.
5
+ // Requirements: SPEC-03
6
+ //
7
+ // Usage:
8
+ // node bin/run-quorum-composition-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
+ const CHECK_ID = 'alloy:quorum-composition';
28
+ const PROPERTY = 'Quorum composition rules — no empty selection, high-risk full fan-out, solo mode single slot';
29
+
30
+ // ── 1. Locate Java ───────────────────────────────────────────────────────────
31
+ const JAVA_HOME = process.env.JAVA_HOME;
32
+ let javaExe;
33
+
34
+ if (JAVA_HOME) {
35
+ javaExe = path.join(JAVA_HOME, 'bin', 'java');
36
+ if (!fs.existsSync(javaExe)) {
37
+ process.stderr.write(
38
+ '[run-quorum-composition-alloy] JAVA_HOME is set but java binary not found at: ' + javaExe + '\n' +
39
+ '[run-quorum-composition-alloy] Unset JAVA_HOME or fix the path.\n'
40
+ );
41
+ try { writeCheckResult({ tool: 'run-quorum-composition-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: {} }); } catch (e) { process.stderr.write('[run-quorum-composition-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
42
+ process.exit(1);
43
+ }
44
+ } else {
45
+ // Fall back to PATH lookup
46
+ const probe = spawnSync('java', ['--version'], { encoding: 'utf8' });
47
+ if (probe.error || probe.status !== 0) {
48
+ process.stderr.write(
49
+ '[run-quorum-composition-alloy] Java not found. Install Java >=17 and set JAVA_HOME.\n' +
50
+ '[run-quorum-composition-alloy] Download: https://adoptium.net/\n'
51
+ );
52
+ try { writeCheckResult({ tool: 'run-quorum-composition-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: {} }); } catch (e) { process.stderr.write('[run-quorum-composition-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
53
+ process.exit(1);
54
+ }
55
+ javaExe = 'java';
56
+ }
57
+
58
+ // ── 2. Check Java version >=17 ───────────────────────────────────────────────
59
+ const versionResult = spawnSync(javaExe, ['--version'], { encoding: 'utf8' });
60
+ if (versionResult.error || versionResult.status !== 0) {
61
+ process.stderr.write('[run-quorum-composition-alloy] Failed to run: ' + javaExe + ' --version\n');
62
+ try { writeCheckResult({ tool: 'run-quorum-composition-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: {} }); } catch (e) { process.stderr.write('[run-quorum-composition-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
63
+ process.exit(1);
64
+ }
65
+ const versionOutput = versionResult.stdout + versionResult.stderr;
66
+ const versionMatch = versionOutput.match(/(?:openjdk\s+|java version\s+[""]?)(\d+)/i);
67
+ const javaMajor = versionMatch ? parseInt(versionMatch[1], 10) : 0;
68
+ if (javaMajor < 17) {
69
+ process.stderr.write(
70
+ '[run-quorum-composition-alloy] Java >=17 required. Found: ' + versionOutput.split('\n')[0] + '\n' +
71
+ '[run-quorum-composition-alloy] Download Java 17+: https://adoptium.net/\n'
72
+ );
73
+ try { writeCheckResult({ tool: 'run-quorum-composition-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: {} }); } catch (e) { process.stderr.write('[run-quorum-composition-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
74
+ process.exit(1);
75
+ }
76
+
77
+ // ── 3. Locate org.alloytools.alloy.dist.jar ──────────────────────────────────
78
+ const jarPath = path.join(ROOT, '.planning', 'formal', 'alloy', 'org.alloytools.alloy.dist.jar');
79
+ if (!fs.existsSync(jarPath)) {
80
+ process.stderr.write(
81
+ '[run-quorum-composition-alloy] org.alloytools.alloy.dist.jar not found at: ' + jarPath + '\n' +
82
+ '[run-quorum-composition-alloy] Download Alloy 6.2.0:\n' +
83
+ ' curl -L https://github.com/AlloyTools/org.alloytools.alloy/releases/download/v6.2.0/org.alloytools.alloy.dist.jar \\\n' +
84
+ ' -o .planning/formal/alloy/org.alloytools.alloy.dist.jar\n'
85
+ );
86
+ try { writeCheckResult({ tool: 'run-quorum-composition-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: {} }); } catch (e) { process.stderr.write('[run-quorum-composition-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
87
+ process.exit(1);
88
+ }
89
+
90
+ // ── 4. Locate .planning/formal/alloy/quorum-composition.als ────────────────────────────
91
+ const alsPath = path.join(ROOT, '.planning', 'formal', 'alloy', 'quorum-composition.als');
92
+ if (!fs.existsSync(alsPath)) {
93
+ process.stderr.write(
94
+ '[run-quorum-composition-alloy] quorum-composition.als not found at: ' + alsPath + '\n' +
95
+ '[run-quorum-composition-alloy] This file should exist in the repository. Check your git status.\n'
96
+ );
97
+ try { writeCheckResult({ tool: 'run-quorum-composition-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: {} }); } catch (e) { process.stderr.write('[run-quorum-composition-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
98
+ process.exit(1);
99
+ }
100
+
101
+ // ── 5. Invoke Alloy 6 ────────────────────────────────────────────────────────
102
+ process.stdout.write('[run-quorum-composition-alloy] ALS: ' + alsPath + '\n');
103
+ process.stdout.write('[run-quorum-composition-alloy] JAR: ' + jarPath + '\n');
104
+
105
+ const _startMs = Date.now();
106
+
107
+ // Use stdio: 'pipe' so we can scan stdout for counterexamples (Alloy exits 0 even on CEX)
108
+ process.stderr.write('[heap] Xms=64m Xmx=' + JAVA_HEAP_MAX + '\n');
109
+ const alloyResult = spawnSync(javaExe, [
110
+ '-Djava.awt.headless=true',
111
+ '-Xms64m', '-Xmx' + JAVA_HEAP_MAX,
112
+ '-jar', jarPath,
113
+ 'exec',
114
+ '--output', '-',
115
+ '--type', 'text',
116
+ '--quiet',
117
+ alsPath,
118
+ ], { encoding: 'utf8', stdio: 'pipe' });
119
+
120
+ if (alloyResult.error) {
121
+ process.stderr.write('[run-quorum-composition-alloy] Alloy invocation failed: ' + alloyResult.error.message + '\n');
122
+ const _runtimeMs = Date.now() - _startMs;
123
+ try { writeCheckResult({ tool: 'run-quorum-composition-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: {} }); } catch (e) { process.stderr.write('[run-quorum-composition-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
124
+ process.exit(1);
125
+ }
126
+
127
+ // ── 6. Scan stdout for counterexamples ───────────────────────────────────────
128
+ // Alloy 6 exits 0 even when counterexamples are found. Scan stdout to detect them.
129
+ const stdout = alloyResult.stdout || '';
130
+ const stderr = alloyResult.stderr || '';
131
+
132
+ // Write stdout to process.stdout (mirrors stdio: 'inherit' output)
133
+ if (stdout) { process.stdout.write(stdout); }
134
+ if (stderr) { process.stderr.write(stderr); }
135
+
136
+ if (/Counterexample/i.test(stdout)) {
137
+ process.stderr.write(
138
+ '[run-quorum-composition-alloy] WARNING: Counterexample found in quorum-composition.als assertions\n' +
139
+ '[run-quorum-composition-alloy] (NoEmptySelection / HighRiskFullFanOut / SoloModeSingleSlot / AllRulesHold).\n' +
140
+ '[run-quorum-composition-alloy] This indicates a spec violation — review quorum-composition.als.\n'
141
+ );
142
+ const _runtimeMs = Date.now() - _startMs;
143
+ try { writeCheckResult({ tool: 'run-quorum-composition-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: {} }); } catch (e) { process.stderr.write('[run-quorum-composition-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
144
+ process.exit(1);
145
+ }
146
+
147
+ if (alloyResult.status !== 0) {
148
+ const _runtimeMs = Date.now() - _startMs;
149
+ try { writeCheckResult({ tool: 'run-quorum-composition-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: {} }); } catch (e) { process.stderr.write('[run-quorum-composition-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
150
+ process.exit(alloyResult.status || 1);
151
+ }
152
+
153
+ const _runtimeMs = Date.now() - _startMs;
154
+ try { writeCheckResult({ tool: 'run-quorum-composition-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: {} }); } catch (e) { process.stderr.write('[run-quorum-composition-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
155
+ process.exit(0);
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // bin/run-sensitivity-sweep.cjs
4
+ // Sensitivity sweep: varies key model parameters and records outcome deltas.
5
+ // Requirements: SENS-01
6
+ //
7
+ // Usage:
8
+ // node bin/run-sensitivity-sweep.cjs
9
+ // SENSITIVITY_REPORT_PATH=/path/to/file.ndjson node bin/run-sensitivity-sweep.cjs
10
+ //
11
+ // Outputs to .planning/formal/sensitivity-report.ndjson (separate from check-results.ndjson).
12
+ // Graceful degradation: if TLC/PRISM not found, records inconclusive results.
13
+ // Always exits 0.
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 os = require('os');
20
+
21
+ const TAG = '[run-sensitivity-sweep]';
22
+
23
+ const REPORT_PATH = process.env.SENSITIVITY_REPORT_PATH ||
24
+ path.join(__dirname, '..', '.planning', 'formal', 'sensitivity-report.ndjson');
25
+
26
+ // ── Tool location helpers ────────────────────────────────────────────────────
27
+
28
+ function locateTLC() {
29
+ const jar = path.join(__dirname, '..', '.planning', 'formal', 'tla', 'tla2tools.jar');
30
+ return fs.existsSync(jar) ? jar : null;
31
+ }
32
+
33
+ function locatePRISM() {
34
+ const envBin = process.env.PRISM_BIN;
35
+ if (envBin) return fs.existsSync(envBin) ? envBin : null;
36
+ const which = spawnSync('which', ['prism'], { encoding: 'utf8' });
37
+ return (which.status === 0 && which.stdout.trim()) ? which.stdout.trim() : null;
38
+ }
39
+
40
+ // ── Sweep runners ────────────────────────────────────────────────────────────
41
+
42
+ function runTLCSweep(maxSize) {
43
+ const startMs = Date.now();
44
+ const tlcJar = locateTLC();
45
+ if (!tlcJar) {
46
+ return {
47
+ result: 'inconclusive',
48
+ runtime_ms: Date.now() - startMs,
49
+ summary: 'inconclusive: tla2tools.jar not found — install TLC to run sweep',
50
+ triage_tags: ['no-tlc'],
51
+ };
52
+ }
53
+
54
+ // Write temp cfg with MaxSize override
55
+ const baseCfgPath = path.join(__dirname, '..', '.planning', 'formal', 'tla', 'MCsafety.cfg');
56
+ if (!fs.existsSync(baseCfgPath)) {
57
+ return {
58
+ result: 'inconclusive',
59
+ runtime_ms: Date.now() - startMs,
60
+ summary: 'inconclusive: MCsafety.cfg not found',
61
+ triage_tags: ['missing-cfg'],
62
+ };
63
+ }
64
+
65
+ const tmpCfg = path.join(os.tmpdir(), 'sens-mcsafety-' + maxSize + '-' + Date.now() + '.cfg');
66
+ const baseCfg = fs.readFileSync(baseCfgPath, 'utf8');
67
+ const overrideCfg = baseCfg.replace(/MaxSize\s*=\s*\d+/, 'MaxSize = ' + maxSize);
68
+ fs.writeFileSync(tmpCfg, overrideCfg, 'utf8');
69
+
70
+ const tlaFile = path.join(__dirname, '..', '.planning', 'formal', 'tla', 'QGSDQuorum.tla');
71
+ process.stderr.write('[heap] Xms=64m Xmx=' + JAVA_HEAP_MAX + '\n');
72
+ const javaResult = spawnSync('java', [
73
+ '-Xms64m', '-Xmx' + JAVA_HEAP_MAX,
74
+ '-jar', tlcJar,
75
+ '-config', tmpCfg,
76
+ tlaFile,
77
+ ], { encoding: 'utf8', timeout: 120000 });
78
+
79
+ try { fs.unlinkSync(tmpCfg); } catch (_) {}
80
+
81
+ const runtimeMs = Date.now() - startMs;
82
+
83
+ if (javaResult.error) {
84
+ return {
85
+ result: 'inconclusive',
86
+ runtime_ms: runtimeMs,
87
+ summary: 'inconclusive: TLC launch error — ' + javaResult.error.message,
88
+ triage_tags: ['tlc-error'],
89
+ };
90
+ }
91
+
92
+ const passed = javaResult.status === 0;
93
+ return {
94
+ result: passed ? 'pass' : 'fail',
95
+ runtime_ms: runtimeMs,
96
+ summary: (passed ? 'pass' : 'fail') + ': TLA+ MCsafety MaxSize=' + maxSize + ' in ' + runtimeMs + 'ms',
97
+ triage_tags: [],
98
+ };
99
+ }
100
+
101
+ function runPRISMSweep(tpRate) {
102
+ const startMs = Date.now();
103
+ const prismBin = locatePRISM();
104
+ if (!prismBin) {
105
+ return {
106
+ result: 'inconclusive',
107
+ runtime_ms: Date.now() - startMs,
108
+ summary: 'inconclusive: prism binary not found — install PRISM to run sweep',
109
+ triage_tags: ['no-prism'],
110
+ };
111
+ }
112
+
113
+ const modelPath = path.join(__dirname, '..', '.planning', 'formal', 'prism', 'quorum.pm');
114
+ if (!fs.existsSync(modelPath)) {
115
+ return {
116
+ result: 'inconclusive',
117
+ runtime_ms: Date.now() - startMs,
118
+ summary: 'inconclusive: .planning/formal/prism/quorum.pm not found',
119
+ triage_tags: ['missing-model'],
120
+ };
121
+ }
122
+
123
+ const prismResult = spawnSync(prismBin, [
124
+ modelPath, '-pf', 'P=? [ F s=1 ]',
125
+ '-const', 'tp_rate=' + tpRate,
126
+ ], { encoding: 'utf8', timeout: 60000 });
127
+
128
+ const runtimeMs = Date.now() - startMs;
129
+
130
+ if (prismResult.error) {
131
+ return {
132
+ result: 'inconclusive',
133
+ runtime_ms: runtimeMs,
134
+ summary: 'inconclusive: PRISM launch error — ' + prismResult.error.message,
135
+ triage_tags: ['prism-error'],
136
+ };
137
+ }
138
+
139
+ const passed = prismResult.status === 0;
140
+ return {
141
+ result: passed ? 'pass' : 'fail',
142
+ runtime_ms: runtimeMs,
143
+ summary: (passed ? 'pass' : 'fail') + ': PRISM quorum tp_rate=' + tpRate + ' in ' + runtimeMs + 'ms',
144
+ triage_tags: [],
145
+ };
146
+ }
147
+
148
+ // ── Parameter sweep definitions (SENS-01: ≥2 parameters, ≥3 values each) ────
149
+
150
+ const SWEEP_PARAMS = [
151
+ {
152
+ name: 'MaxSize',
153
+ description: 'TLA+ quorum size (N slots required for consensus)',
154
+ model: 'tla',
155
+ values: [1, 2, 3],
156
+ baseline: 3,
157
+ run: runTLCSweep,
158
+ },
159
+ {
160
+ name: 'tp_rate',
161
+ description: 'PRISM slot approval probability (P(slot votes APPROVE | available))',
162
+ model: 'prism',
163
+ values: [0.5, 0.75, 0.95],
164
+ baseline: 0.85,
165
+ run: runPRISMSweep,
166
+ },
167
+ ];
168
+
169
+ // ── Record writer ─────────────────────────────────────────────────────────────
170
+
171
+ function writeRecord(record) {
172
+ fs.appendFileSync(REPORT_PATH, JSON.stringify(record) + '\n', 'utf8');
173
+ }
174
+
175
+ // ── Main ──────────────────────────────────────────────────────────────────────
176
+
177
+ function main() {
178
+ // Ensure output directory exists
179
+ fs.mkdirSync(path.dirname(REPORT_PATH), { recursive: true });
180
+
181
+ for (const param of SWEEP_PARAMS) {
182
+ process.stderr.write(TAG + ' Sweeping ' + param.name + ': ' + param.values.join(', ') + '\n');
183
+
184
+ // Run baseline first (if it's in the values list) to set baseline_result
185
+ let baselineResult = null;
186
+
187
+ for (const value of param.values) {
188
+ const outcome = param.run(value);
189
+
190
+ // Set baseline_result when we hit the baseline value
191
+ if (value === param.baseline) {
192
+ baselineResult = outcome.result;
193
+ }
194
+
195
+ const delta = baselineResult === null
196
+ ? 'unknown'
197
+ : outcome.result === baselineResult
198
+ ? 'stable'
199
+ : 'flip-to-' + outcome.result;
200
+
201
+ const record = {
202
+ tool: 'run-sensitivity-sweep',
203
+ formalism: param.model,
204
+ result: outcome.result,
205
+ timestamp: new Date().toISOString(),
206
+ check_id: 'sens:' + param.model + '-' + param.name.toLowerCase().replace(/_/g, '-'),
207
+ surface: 'sensitivity',
208
+ property: param.description + ' sweep',
209
+ runtime_ms: outcome.runtime_ms,
210
+ summary: outcome.summary,
211
+ triage_tags: outcome.triage_tags || [],
212
+ metadata: {
213
+ parameter: param.name,
214
+ value: value,
215
+ baseline: param.baseline,
216
+ baseline_result: baselineResult,
217
+ delta: delta,
218
+ },
219
+ };
220
+
221
+ writeRecord(record);
222
+ process.stderr.write(
223
+ TAG + ' ' + param.name + '=' + value + ' → ' + outcome.result + ' (' + delta + ')\n'
224
+ );
225
+ }
226
+ }
227
+
228
+ process.stderr.write(TAG + ' Sweep complete. Written to: ' + REPORT_PATH + '\n');
229
+ }
230
+
231
+ main();