@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,382 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * install-formal-tools.cjs
6
+ *
7
+ * Cross-platform installer for QGSD formal verification tools:
8
+ * TLA+ — downloads tla2tools.jar into .planning/formal/tla/
9
+ * Alloy — downloads org.alloytools.alloy.dist.jar into .planning/formal/alloy/
10
+ * PRISM — downloads and installs platform-specific binary
11
+ * UPPAAL — downloads verifyta binary into .planning/formal/uppaal/bin/
12
+ * Petri — no install needed (bundled via npm)
13
+ *
14
+ * Usage:
15
+ * node bin/install-formal-tools.cjs
16
+ * node bin/install.js --formal
17
+ *
18
+ * Idempotent — safe to run multiple times.
19
+ * PRISM install is non-blocking (failure = warning, not exit 1).
20
+ * Always exits 0 — failures are non-blocking warnings.
21
+ */
22
+
23
+ const https = require('https');
24
+ const fs = require('fs');
25
+ const path = require('path');
26
+ const os = require('os');
27
+ const { spawnSync } = require('child_process');
28
+ const zlib = require('zlib');
29
+
30
+ // ─── Output helpers ────────────────────────────────────────────────────────
31
+
32
+ function ok(msg) {
33
+ process.stdout.write(`\x1b[32m✓\x1b[0m ${msg}\n`);
34
+ }
35
+
36
+ function skip(msg) {
37
+ process.stdout.write(`\x1b[33m→\x1b[0m ${msg}\n`);
38
+ }
39
+
40
+ function fail(msg) {
41
+ process.stderr.write(`\x1b[31m✗\x1b[0m ${msg}\n`);
42
+ }
43
+
44
+ function info(msg) {
45
+ process.stdout.write(` ${msg}\n`);
46
+ }
47
+
48
+ // ─── HTTPS download with redirect following ────────────────────────────────
49
+
50
+ /**
51
+ * Download a file from url to dest, following 301/302 redirects.
52
+ * Returns a Promise that resolves when the file is fully written.
53
+ */
54
+ function downloadFile(url, dest) {
55
+ return new Promise((resolve, reject) => {
56
+ const doGet = (currentUrl) => {
57
+ https.get(currentUrl, (res) => {
58
+ if (res.statusCode === 301 || res.statusCode === 302) {
59
+ const location = res.headers.location;
60
+ if (!location) {
61
+ reject(new Error(`Redirect from ${currentUrl} missing Location header`));
62
+ return;
63
+ }
64
+ res.resume(); // drain response
65
+ doGet(location);
66
+ return;
67
+ }
68
+ if (res.statusCode !== 200) {
69
+ res.resume();
70
+ reject(new Error(`HTTP ${res.statusCode} for ${currentUrl}`));
71
+ return;
72
+ }
73
+ const out = fs.createWriteStream(dest);
74
+ res.pipe(out);
75
+ out.on('finish', () => out.close(resolve));
76
+ out.on('error', (err) => {
77
+ fs.unlink(dest, () => {}); // clean up partial file
78
+ reject(err);
79
+ });
80
+ res.on('error', (err) => {
81
+ fs.unlink(dest, () => {});
82
+ reject(err);
83
+ });
84
+ }).on('error', reject);
85
+ };
86
+ doGet(url);
87
+ });
88
+ }
89
+
90
+ // ─── Java check (soft warning only) ───────────────────────────────────────
91
+
92
+ function checkJava() {
93
+ const result = spawnSync('java', ['-version'], { encoding: 'utf8', stdio: 'pipe' });
94
+ if (result.status !== 0 || result.error) {
95
+ process.stdout.write(
96
+ `\x1b[33m⚠\x1b[0m Java not found — TLA+, Alloy, and PRISM require Java 17+\n` +
97
+ ` Download: https://adoptium.net/\n`
98
+ );
99
+ return;
100
+ }
101
+ // java -version prints to stderr; output format: openjdk version "17.0.x" ...
102
+ // or legacy: java version "1.8.0_xxx"
103
+ const output = result.stderr || result.stdout || '';
104
+ const match = output.match(/"([^"]+)"/);
105
+ if (match) {
106
+ const versionStr = match[1]; // e.g. "17.0.2" or "1.8.0_362"
107
+ const parts = versionStr.split('.');
108
+ let major = parseInt(parts[0], 10);
109
+ // Pre-Java 9: version format was "1.x.y" — treat as x
110
+ if (major === 1 && parts.length >= 2) {
111
+ major = parseInt(parts[1], 10);
112
+ }
113
+ if (isNaN(major) || major < 17) {
114
+ process.stdout.write(
115
+ `\x1b[33m⚠\x1b[0m Java ${versionStr} detected — Java 17+ required for TLA+, Alloy, and PRISM\n` +
116
+ ` Upgrade: https://adoptium.net/\n`
117
+ );
118
+ }
119
+ }
120
+ }
121
+
122
+ // ─── Main ──────────────────────────────────────────────────────────────────
123
+
124
+ (async () => {
125
+ const results = [];
126
+
127
+ // Java check (soft warning, never blocks)
128
+ checkJava();
129
+ process.stdout.write('\n');
130
+
131
+ // ── TLA+ ──────────────────────────────────────────────────────────────
132
+
133
+ const tlaDest = path.join(process.cwd(), '.planning', 'formal', 'tla', 'tla2tools.jar');
134
+ const tlaUrl = 'https://github.com/tlaplus/tlaplus/releases/latest/download/tla2tools.jar';
135
+
136
+ if (fs.existsSync(tlaDest)) {
137
+ skip('TLA+ tla2tools.jar already present — skipping');
138
+ results.push({ name: 'TLA+', status: 'skip' });
139
+ } else {
140
+ process.stdout.write(' Downloading TLA+ tla2tools.jar…\n');
141
+ try {
142
+ fs.mkdirSync(path.dirname(tlaDest), { recursive: true });
143
+ await downloadFile(tlaUrl, tlaDest);
144
+ ok('TLA+ tla2tools.jar downloaded');
145
+ results.push({ name: 'TLA+', status: 'ok' });
146
+ } catch (err) {
147
+ fail(`TLA+ download failed: ${err.message}`);
148
+ results.push({ name: 'TLA+', status: 'fail' });
149
+ }
150
+ }
151
+
152
+ // ── Alloy ─────────────────────────────────────────────────────────────
153
+
154
+ const alloyDest = path.join(process.cwd(), '.planning', 'formal', 'alloy', 'org.alloytools.alloy.dist.jar');
155
+ const alloyUrl = 'https://github.com/AlloyTools/org.alloytools.alloy/releases/latest/download/org.alloytools.alloy.dist.jar';
156
+
157
+ if (fs.existsSync(alloyDest)) {
158
+ skip('Alloy org.alloytools.alloy.dist.jar already present — skipping');
159
+ results.push({ name: 'Alloy', status: 'skip' });
160
+ } else {
161
+ process.stdout.write(' Downloading Alloy org.alloytools.alloy.dist.jar…\n');
162
+ try {
163
+ fs.mkdirSync(path.dirname(alloyDest), { recursive: true });
164
+ await downloadFile(alloyUrl, alloyDest);
165
+ ok('Alloy org.alloytools.alloy.dist.jar downloaded');
166
+ results.push({ name: 'Alloy', status: 'ok' });
167
+ } catch (err) {
168
+ fail(`Alloy download failed: ${err.message}`);
169
+ results.push({ name: 'Alloy', status: 'fail' });
170
+ }
171
+ }
172
+
173
+ // ── PRISM ─────────────────────────────────────────────────────────────
174
+
175
+ const prismBin = process.env.PRISM_BIN;
176
+ if (prismBin && fs.existsSync(prismBin)) {
177
+ skip('PRISM already configured — skipping');
178
+ results.push({ name: 'PRISM', status: 'skip' });
179
+ } else {
180
+ const platform = process.platform;
181
+ const tmpDir = os.tmpdir();
182
+
183
+ try {
184
+ if (platform === 'darwin') {
185
+ const tarUrl = 'https://www.prismmodelchecker.org/dl/prism-4.8.1-mac64.tar.gz';
186
+ const tarPath = path.join(tmpDir, 'prism-mac64.tar.gz');
187
+ process.stdout.write(' Downloading PRISM for macOS…\n');
188
+ await downloadFile(tarUrl, tarPath);
189
+ process.stdout.write(' Extracting PRISM…\n');
190
+ const extract = spawnSync('tar', ['-xzf', tarPath, '-C', tmpDir], { stdio: 'inherit' });
191
+ if (extract.status !== 0) throw new Error('tar extraction failed');
192
+ const extractedName = fs.readdirSync(tmpDir).find(d => d.startsWith('prism-') && !d.endsWith('.tar.gz'));
193
+ if (!extractedName) throw new Error('Could not find extracted PRISM directory');
194
+ const extractedDir = path.join(tmpDir, extractedName);
195
+ process.stdout.write(' Running PRISM install.sh…\n');
196
+ const install = spawnSync('bash', ['./install.sh'], { cwd: extractedDir, stdio: 'inherit' });
197
+ if (install.status !== 0) throw new Error('install.sh failed');
198
+ // Remove quarantine attribute (macOS Gatekeeper) — ignore errors
199
+ spawnSync('xattr', ['-dr', 'com.apple.quarantine', extractedDir], { stdio: 'pipe' });
200
+ ok('PRISM installed');
201
+ info(`export PRISM_BIN="${extractedDir}/bin/prism"`);
202
+ info('Add the above line to your ~/.zshrc or ~/.bash_profile');
203
+ results.push({ name: 'PRISM', status: 'ok' });
204
+ } else if (platform === 'linux') {
205
+ const tarUrl = 'https://www.prismmodelchecker.org/dl/prism-4.8.1-linux64.tar.gz';
206
+ const tarPath = path.join(tmpDir, 'prism-linux64.tar.gz');
207
+ process.stdout.write(' Downloading PRISM for Linux…\n');
208
+ await downloadFile(tarUrl, tarPath);
209
+ process.stdout.write(' Extracting PRISM…\n');
210
+ const extract = spawnSync('tar', ['-xzf', tarPath, '-C', tmpDir], { stdio: 'inherit' });
211
+ if (extract.status !== 0) throw new Error('tar extraction failed');
212
+ const extractedName = fs.readdirSync(tmpDir).find(d => d.startsWith('prism-') && !d.endsWith('.tar.gz'));
213
+ if (!extractedName) throw new Error('Could not find extracted PRISM directory');
214
+ const extractedDir = path.join(tmpDir, extractedName);
215
+ process.stdout.write(' Running PRISM install.sh…\n');
216
+ const install = spawnSync('bash', ['./install.sh'], { cwd: extractedDir, stdio: 'inherit' });
217
+ if (install.status !== 0) throw new Error('install.sh failed');
218
+ ok('PRISM installed');
219
+ info(` sudo ln -s "${extractedDir}/bin/prism" /usr/local/bin/prism`);
220
+ info(' OR:');
221
+ info(` export PRISM_BIN="${extractedDir}/bin/prism"`);
222
+ results.push({ name: 'PRISM', status: 'ok' });
223
+ } else if (platform === 'win32') {
224
+ const exeUrl = 'https://www.prismmodelchecker.org/dl/prism-4.8.1-win-installer.exe';
225
+ const installerPath = path.join(tmpDir, 'prism-installer.exe');
226
+ process.stdout.write(' Downloading PRISM installer for Windows…\n');
227
+ await downloadFile(exeUrl, installerPath);
228
+ process.stdout.write(' Running PRISM silent install…\n');
229
+ const install = spawnSync(installerPath, ['/S'], { stdio: 'inherit' });
230
+ if (install.status !== 0) throw new Error('PRISM silent installer failed');
231
+ ok('PRISM installed');
232
+ info('Add C:\\Program Files\\PRISM\\bin to your PATH');
233
+ results.push({ name: 'PRISM', status: 'ok' });
234
+ } else {
235
+ fail(`PRISM install — unsupported platform: ${platform}`);
236
+ info('Download manually from https://prismmodelchecker.org/download.php');
237
+ results.push({ name: 'PRISM', status: 'fail' });
238
+ }
239
+ } catch (err) {
240
+ fail(`PRISM install failed — see https://prismmodelchecker.org/download.php`);
241
+ info(`Error: ${err.message}`);
242
+ results.push({ name: 'PRISM', status: 'fail' });
243
+ }
244
+ }
245
+
246
+ // ── UPPAAL ──────────────────────────────────────────────────────────
247
+
248
+ const uppaalDest = path.join(process.cwd(), '.planning', 'formal', 'uppaal', 'bin', 'verifyta');
249
+
250
+ if (fs.existsSync(uppaalDest)) {
251
+ skip('UPPAAL verifyta already present — skipping');
252
+ results.push({ name: 'UPPAAL', status: 'skip' });
253
+ } else {
254
+ const platform = process.platform;
255
+ const tmpDir = os.tmpdir();
256
+
257
+ try {
258
+ let zipUrl;
259
+ if (platform === 'darwin') {
260
+ zipUrl = 'https://download.uppaal.org/uppaal-5.0/uppaal-5.0.0/UPPAAL-5.0.0-app.zip';
261
+ } else if (platform === 'linux') {
262
+ zipUrl = 'https://download.uppaal.org/uppaal-5.0/uppaal-5.0.0/uppaal-5.0.0-linux64.zip';
263
+ } else if (platform === 'win32') {
264
+ zipUrl = 'https://download.uppaal.org/uppaal-5.0/uppaal-5.0.0/uppaal-5.0.0-win64.zip';
265
+ } else {
266
+ fail(`UPPAAL install — unsupported platform: ${platform}`);
267
+ info('Download manually from https://uppaal.org/downloads/');
268
+ results.push({ name: 'UPPAAL', status: 'fail' });
269
+ // skip to Petri nets
270
+ }
271
+
272
+ if (zipUrl) {
273
+ const zipPath = path.join(tmpDir, `uppaal-${platform}.zip`);
274
+ process.stdout.write(` Downloading UPPAAL verifyta for ${platform}…\n`);
275
+ await downloadFile(zipUrl, zipPath);
276
+
277
+ process.stdout.write(' Extracting UPPAAL…\n');
278
+ const uppaalTmpDir = path.join(tmpDir, 'uppaal-extract');
279
+ fs.mkdirSync(uppaalTmpDir, { recursive: true });
280
+ const extract = spawnSync('unzip', ['-o', zipPath, '-d', uppaalTmpDir], { stdio: 'pipe' });
281
+ if (extract.status !== 0) throw new Error('unzip extraction failed');
282
+
283
+ // Locate verifyta binary inside extracted directory
284
+ let verifytaSrc = null;
285
+ if (platform === 'linux') {
286
+ // Linux: look in bin/ or bin-Linux/
287
+ const candidates = [
288
+ path.join(uppaalTmpDir, 'uppaal-5.0.0', 'bin', 'verifyta'),
289
+ path.join(uppaalTmpDir, 'uppaal-5.0.0', 'bin-Linux', 'verifyta'),
290
+ ];
291
+ verifytaSrc = candidates.find(c => fs.existsSync(c)) || null;
292
+ } else if (platform === 'win32') {
293
+ verifytaSrc = path.join(uppaalTmpDir, 'uppaal-5.0.0', 'bin-Windows', 'verifyta.exe');
294
+ if (!fs.existsSync(verifytaSrc)) verifytaSrc = null;
295
+ }
296
+
297
+ // Fallback: use find to locate verifyta recursively (works for macOS .app bundle and any layout)
298
+ if (!verifytaSrc) {
299
+ const findResult = spawnSync('find', [uppaalTmpDir, '-name', 'verifyta', '-type', 'f'], { encoding: 'utf8' });
300
+ if (findResult.status === 0 && findResult.stdout.trim()) {
301
+ verifytaSrc = findResult.stdout.trim().split('\n')[0];
302
+ }
303
+ }
304
+
305
+ if (!verifytaSrc) throw new Error('Could not locate verifyta binary in extracted archive');
306
+
307
+ // Determine the bin directory containing verifyta (to copy sibling libs)
308
+ const srcBinDir = path.dirname(verifytaSrc);
309
+ const destBinDir = path.join(process.cwd(), '.planning', 'formal', 'uppaal', 'bin');
310
+ fs.mkdirSync(destBinDir, { recursive: true });
311
+
312
+ // Copy verifyta binary
313
+ fs.copyFileSync(verifytaSrc, uppaalDest);
314
+
315
+ // Copy sibling shared libraries (.so, .dylib)
316
+ try {
317
+ const siblings = fs.readdirSync(srcBinDir);
318
+ for (const sib of siblings) {
319
+ if (sib === path.basename(verifytaSrc)) continue;
320
+ if (sib.endsWith('.so') || sib.endsWith('.dylib') || sib.includes('.so.')) {
321
+ fs.copyFileSync(path.join(srcBinDir, sib), path.join(destBinDir, sib));
322
+ }
323
+ }
324
+ } catch (_) { /* non-critical */ }
325
+
326
+ // chmod +x (non-Windows)
327
+ if (platform !== 'win32') {
328
+ fs.chmodSync(uppaalDest, 0o755);
329
+ }
330
+
331
+ // macOS Gatekeeper handling
332
+ if (platform === 'darwin') {
333
+ // Step a: remove quarantine attribute
334
+ spawnSync('xattr', ['-dr', 'com.apple.quarantine', destBinDir], { stdio: 'pipe' });
335
+
336
+ // Step b: verify binary runs; if blocked, try codesign removal
337
+ const verifyRun = spawnSync(uppaalDest, ['--version'], { stdio: 'pipe', timeout: 5000 });
338
+ if (verifyRun.status !== 0 || verifyRun.signal) {
339
+ spawnSync('codesign', ['--remove-signature', uppaalDest], { stdio: 'pipe' });
340
+ // Step c: advisory warning if still blocked
341
+ const retryRun = spawnSync(uppaalDest, ['--version'], { stdio: 'pipe', timeout: 5000 });
342
+ if (retryRun.status !== 0 || retryRun.signal) {
343
+ info('If macOS Gatekeeper blocks verifyta, run: sudo spctl --master-disable (re-enable after with --master-enable)');
344
+ }
345
+ }
346
+ }
347
+
348
+ ok('UPPAAL verifyta installed');
349
+ info(`Path: ${uppaalDest}`);
350
+ results.push({ name: 'UPPAAL', status: 'ok' });
351
+ }
352
+ } catch (err) {
353
+ fail(`UPPAAL install failed — see https://uppaal.org/downloads/`);
354
+ info(`Error: ${err.message}`);
355
+ results.push({ name: 'UPPAAL', status: 'fail' });
356
+ }
357
+ }
358
+
359
+ // ── Petri nets ────────────────────────────────────────────────────────
360
+
361
+ skip('Petri nets — no install needed, bundled via @hpcc-js/wasm-graphviz npm');
362
+ results.push({ name: 'Petri', status: 'skip' });
363
+
364
+ // ── Summary table ─────────────────────────────────────────────────────
365
+
366
+ process.stdout.write('\n Results:\n');
367
+ const statusLabel = { ok: '\x1b[32m✓ installed\x1b[0m ', skip: '\x1b[33m→ skipped \x1b[0m ', fail: '\x1b[31m✗ failed \x1b[0m ' };
368
+ const nameWidth = 8;
369
+ for (const r of results) {
370
+ const padded = r.name.padEnd(nameWidth);
371
+ process.stdout.write(` ${padded} ${statusLabel[r.status] || r.status}\n`);
372
+ }
373
+ process.stdout.write('\n');
374
+
375
+ // ── Exit code ─────────────────────────────────────────────────────────
376
+
377
+ // Best-effort — all failures are non-blocking warnings
378
+ process.exit(0);
379
+ })().catch(err => {
380
+ fail(err.message);
381
+ process.exit(0);
382
+ });