@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,228 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // bin/run-phase-tlc.cjs
4
+ // PLAN-02: TLC verification runner for proposed-changes.tla with structured feedback.
5
+ //
6
+ // Runs TLC on a proposed-changes.tla generated from a PLAN.md file and returns
7
+ // structured results with pass/violations fields. Provides iterativeVerify for
8
+ // single-attempt orchestration and formatTlcFeedback for human-readable feedback.
9
+ //
10
+ // Usage:
11
+ // node bin/run-phase-tlc.cjs <path-to-PLAN.md>
12
+ //
13
+ // Requires: Java >=17, .planning/formal/tla/tla2tools.jar
14
+ //
15
+ // NOTE: Uses spawnSync (no shell) for safe subprocess invocation -- no exec().
16
+
17
+ const { spawnSync } = require('child_process');
18
+ const JAVA_HEAP_MAX = process.env.QGSD_JAVA_HEAP_MAX || '512m';
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+
22
+ const { generateProposedChanges, generateTlaCfg } = require('./generate-proposed-changes.cjs');
23
+ const { classifyTruth } = require('./generate-phase-spec.cjs');
24
+
25
+ /**
26
+ * Run TLC on a proposed-changes.tla spec file.
27
+ *
28
+ * @param {string} specPath - Path to the .tla spec file
29
+ * @param {string} cfgPath - Path to the .cfg config file
30
+ * @param {{ javaOverride?: string, jarOverride?: string }} [options]
31
+ * @returns {{ passed: boolean, violations: string[], output: string, runtimeMs: number }}
32
+ */
33
+ function runPhaseTlc(specPath, cfgPath, options) {
34
+ options = options || {};
35
+
36
+ // Check tla2tools.jar existence
37
+ const tla2toolsPath = options.jarOverride || path.join(__dirname, '..', '.planning', 'formal', 'tla', 'tla2tools.jar');
38
+ if (!fs.existsSync(tla2toolsPath)) {
39
+ return {
40
+ passed: false,
41
+ violations: ['tla2tools.jar not found at ' + tla2toolsPath + ' -- run: curl -L https://github.com/tlaplus/tlaplus/releases/download/v1.8.0/tla2tools.jar -o .planning/formal/tla/tla2tools.jar'],
42
+ output: '',
43
+ runtimeMs: 0,
44
+ };
45
+ }
46
+
47
+ // Locate Java binary
48
+ let javaExe = options.javaOverride || null;
49
+
50
+ if (!javaExe) {
51
+ const JAVA_HOME = process.env.JAVA_HOME;
52
+ if (JAVA_HOME) {
53
+ javaExe = path.join(JAVA_HOME, 'bin', 'java');
54
+ if (!fs.existsSync(javaExe)) {
55
+ javaExe = null;
56
+ }
57
+ }
58
+ if (!javaExe) {
59
+ const probe = spawnSync('java', ['--version'], { encoding: 'utf8' });
60
+ if (probe.error || probe.status !== 0) {
61
+ return {
62
+ passed: false,
63
+ violations: ['Java not found -- install JDK 17+'],
64
+ output: '',
65
+ runtimeMs: 0,
66
+ };
67
+ }
68
+ javaExe = 'java';
69
+ }
70
+ } else {
71
+ // Validate override path
72
+ if (!fs.existsSync(javaExe)) {
73
+ return {
74
+ passed: false,
75
+ violations: ['Java not found at ' + javaExe + ' -- install JDK 17+'],
76
+ output: '',
77
+ runtimeMs: 0,
78
+ };
79
+ }
80
+ }
81
+
82
+ // Invoke TLC via spawnSync (no shell -- safe subprocess)
83
+ const startMs = Date.now();
84
+ process.stderr.write('[heap] Xms=64m Xmx=' + JAVA_HEAP_MAX + '\n');
85
+ const tlcResult = spawnSync(javaExe, [
86
+ '-XX:+UseParallelGC',
87
+ '-Xms64m', '-Xmx' + JAVA_HEAP_MAX,
88
+ '-jar', tla2toolsPath,
89
+ '-workers', '1',
90
+ '-config', cfgPath,
91
+ specPath,
92
+ ], { encoding: 'utf8', timeout: 60000 });
93
+ const runtimeMs = Date.now() - startMs;
94
+
95
+ const output = (tlcResult.stdout || '') + (tlcResult.stderr || '');
96
+ const violations = [];
97
+
98
+ if (tlcResult.error) {
99
+ violations.push('TLC invocation failed: ' + tlcResult.error.message);
100
+ return { passed: false, violations, output, runtimeMs };
101
+ }
102
+
103
+ // Parse violations
104
+ const invariantRegex = /Invariant (\w+) is violated/g;
105
+ const propertyRegex = /Property (\w+) is violated/g;
106
+ const errorRegex = /^Error:\s*(.+)/gm;
107
+
108
+ let match;
109
+ while ((match = invariantRegex.exec(output)) !== null) {
110
+ violations.push('Invariant ' + match[1] + ' is violated');
111
+ }
112
+ while ((match = propertyRegex.exec(output)) !== null) {
113
+ violations.push('Property ' + match[1] + ' is violated');
114
+ }
115
+ while ((match = errorRegex.exec(output)) !== null) {
116
+ // Only add Error lines that aren't already captured as violations
117
+ const errText = match[1].trim();
118
+ if (!violations.some(v => v.includes(errText))) {
119
+ violations.push('Error: ' + errText);
120
+ }
121
+ }
122
+
123
+ // Check for successful completion
124
+ const passed = output.includes('Model checking completed. No error has been found.') && violations.length === 0;
125
+
126
+ return { passed, violations, output, runtimeMs };
127
+ }
128
+
129
+ /**
130
+ * Format TLC feedback for the planner with truth mapping.
131
+ *
132
+ * @param {number} attempt - Current attempt number (1-based)
133
+ * @param {number} maxAttempts - Maximum attempts
134
+ * @param {{ passed: boolean, violations: string[], output: string, runtimeMs: number }} tlcResult
135
+ * @param {string[]} truthsList - Original truths list from PLAN.md
136
+ * @returns {string}
137
+ */
138
+ function formatTlcFeedback(attempt, maxAttempts, tlcResult, truthsList) {
139
+ if (tlcResult.passed) {
140
+ return 'ATTEMPT ' + attempt + '/' + maxAttempts + ': TLC verification PASSED. All ' + truthsList.length + ' properties satisfied.';
141
+ }
142
+
143
+ let feedback = 'ATTEMPT ' + attempt + '/' + maxAttempts + ': TLC verification FAILED.\n';
144
+
145
+ for (const violation of tlcResult.violations) {
146
+ const reqMatch = violation.match(/Req(\d{2})/);
147
+ if (reqMatch) {
148
+ const truthIndex = parseInt(reqMatch[1], 10) - 1;
149
+ const truthText = truthIndex >= 0 && truthIndex < truthsList.length
150
+ ? truthsList[truthIndex]
151
+ : '(unknown truth)';
152
+ const kindMatch = violation.match(/^(Invariant|Property)/);
153
+ const kind = kindMatch ? kindMatch[1].toUpperCase() : 'UNKNOWN';
154
+ feedback += 'Violated: Req' + reqMatch[1] + ' (' + kind + ') -- "' + truthText + '"\n';
155
+ } else {
156
+ feedback += violation + '\n';
157
+ }
158
+ }
159
+
160
+ feedback += 'Suggestion: Revise the truth statement or adjust the plan to satisfy this constraint.';
161
+ return feedback;
162
+ }
163
+
164
+ /**
165
+ * Perform a single TLC verification attempt on a PLAN.md file.
166
+ *
167
+ * @param {string} planFilePath - Path to the PLAN.md file
168
+ * @returns {{ status: string, reason?: string, violations?: string[], feedback?: string, truthCount?: number, specPath?: string, runtimeMs?: number }}
169
+ */
170
+ function iterativeVerify(planFilePath) {
171
+ // Generate spec
172
+ const genResult = generateProposedChanges(planFilePath);
173
+
174
+ if (!genResult.generated) {
175
+ return { status: 'skipped', reason: 'no truths in plan' };
176
+ }
177
+
178
+ // Generate TLC config
179
+ const { cfgPath } = generateTlaCfg(genResult.specPath);
180
+
181
+ // Run TLC
182
+ const tlcResult = runPhaseTlc(genResult.specPath, cfgPath);
183
+
184
+ if (tlcResult.passed) {
185
+ return {
186
+ status: 'passed',
187
+ truthCount: genResult.truthCount,
188
+ specPath: genResult.specPath,
189
+ runtimeMs: tlcResult.runtimeMs,
190
+ };
191
+ }
192
+
193
+ // Build truths list for feedback
194
+ const truths = genResult.classifications.map(c => c.truth);
195
+ const feedback = formatTlcFeedback(1, 3, tlcResult, truths);
196
+
197
+ return {
198
+ status: 'failed',
199
+ violations: tlcResult.violations,
200
+ feedback,
201
+ specPath: genResult.specPath,
202
+ };
203
+ }
204
+
205
+ // ── CLI entrypoint ────────────────────────────────────────────────────────────
206
+ if (require.main === module) {
207
+ const args = process.argv.slice(2).filter(a => !a.startsWith('--'));
208
+
209
+ if (args.length === 0) {
210
+ process.stderr.write('[run-phase-tlc] Usage: node bin/run-phase-tlc.cjs <path-to-PLAN.md>\n');
211
+ process.exit(1);
212
+ }
213
+
214
+ const planFilePath = path.resolve(args[0]);
215
+ if (!fs.existsSync(planFilePath)) {
216
+ process.stderr.write('[run-phase-tlc] Error: file not found: ' + planFilePath + '\n');
217
+ process.exit(1);
218
+ }
219
+
220
+ const result = iterativeVerify(planFilePath);
221
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
222
+
223
+ if (result.status === 'failed') {
224
+ process.exit(1);
225
+ }
226
+ }
227
+
228
+ module.exports = { runPhaseTlc, iterativeVerify, formatTlcFeedback };
@@ -0,0 +1,446 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // bin/run-prism.cjs
4
+ // Invokes PRISM model checker against .planning/formal/prism/quorum.pm.
5
+ // Requirements: PRM-01
6
+ //
7
+ // Usage:
8
+ // node bin/run-prism.cjs # default: P=? [ F s=1 ]
9
+ // node bin/run-prism.cjs -pf "P=? [ F s=1 ]" # explicit property
10
+ // node bin/run-prism.cjs -const tp_rate=0.9274 -const unavail=0.0215
11
+ //
12
+ // Prerequisites:
13
+ // - PRISM 4.x installed; set PRISM_BIN to path of the prism shell script
14
+ // e.g. export PRISM_BIN="$HOME/prism/bin/prism"
15
+ // - Java >=17 (same JRE used by TLA+/Alloy CI step)
16
+ //
17
+ // CI: PRISM_BIN is set by the formal-verify workflow step that extracts the
18
+ // Linux binary tarball.
19
+
20
+ const { spawnSync } = require('child_process');
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const { writeCheckResult } = require('./write-check-result.cjs');
24
+ const { readPolicy } = require('./read-policy.cjs');
25
+ const { getRequirementIds } = require('./requirement-map.cjs');
26
+
27
+ // ── Check ID mapping for multi-model support ─────────────────────────────────
28
+ const CHECK_ID_MAP = {
29
+ 'quorum': 'prism:quorum',
30
+ 'mcp-availability': 'prism:mcp-availability',
31
+ };
32
+ const PROPERTY_MAP = {
33
+ 'quorum': 'Quorum consensus probability under agent availability rates',
34
+ 'mcp-availability': 'MCP server availability under nondeterministic failure modes',
35
+ };
36
+
37
+ // ── Locate PRISM binary ──────────────────────────────────────────────────────
38
+ const prismBin = process.env.PRISM_BIN || 'prism';
39
+
40
+ // Verify the binary exists (skip check if it's just 'prism' on PATH)
41
+ if (prismBin !== 'prism' && !fs.existsSync(prismBin)) {
42
+ process.stderr.write(
43
+ '[run-prism] PRISM binary not found at: ' + prismBin + '\n' +
44
+ '[run-prism] Install PRISM and set PRISM_BIN env var:\n' +
45
+ '[run-prism] export PRISM_BIN="$HOME/prism/bin/prism"\n' +
46
+ '[run-prism] Download: https://www.prismmodelchecker.org/download.php\n'
47
+ );
48
+ try {
49
+ writeCheckResult({
50
+ tool: 'run-prism', formalism: 'prism', result: 'fail',
51
+ check_id: 'prism:quorum', surface: 'prism', property: 'Quorum consensus probability under agent availability rates',
52
+ runtime_ms: 0, summary: 'fail: prism:quorum (binary not found)', triage_tags: [],
53
+ requirement_ids: getRequirementIds('prism:quorum'),
54
+ observation_window: { window_start: new Date().toISOString(), window_end: new Date().toISOString(), n_traces: 0, n_events: 0, window_days: 0 },
55
+ metadata: {}
56
+ });
57
+ } catch (e) { process.stderr.write('[run-prism] Warning: failed to write check result: ' + e.message + '\n'); }
58
+ process.exit(1);
59
+ }
60
+
61
+ // ── Locate model file ────────────────────────────────────────────────────────
62
+ const modelPath = path.join(__dirname, '..', '.planning', 'formal', 'prism', 'quorum.pm');
63
+ if (!fs.existsSync(modelPath)) {
64
+ process.stderr.write(
65
+ '[run-prism] Model file not found: ' + modelPath + '\n'
66
+ );
67
+ try {
68
+ writeCheckResult({
69
+ tool: 'run-prism', formalism: 'prism', result: 'fail',
70
+ check_id: 'prism:quorum', surface: 'prism', property: 'Quorum consensus probability under agent availability rates',
71
+ runtime_ms: 0, summary: 'fail: prism:quorum (model not found)', triage_tags: [],
72
+ requirement_ids: getRequirementIds('prism:quorum'),
73
+ observation_window: { window_start: new Date().toISOString(), window_end: new Date().toISOString(), n_traces: 0, n_events: 0, window_days: 0 },
74
+ metadata: {}
75
+ });
76
+ } catch (e) { process.stderr.write('[run-prism] Warning: failed to write check result: ' + e.message + '\n'); }
77
+ process.exit(1);
78
+ }
79
+
80
+ // ── readMCPAvailabilityRates (MCPENV-04) ─────────────────────────────────────
81
+ // Reads quorum-scoreboard.json and computes per-slot availability rates.
82
+ // Returns { 'slot-name': availabilityRate, ... } or null if no data.
83
+ // Rate = 1.0 - (unavail_count / total_count) per slot, excluding 'claude' (self).
84
+ // Exported for tests.
85
+ function readMCPAvailabilityRates(sbPath) {
86
+ let p = sbPath;
87
+ if (!p) {
88
+ try {
89
+ const pp = require('./planning-paths.cjs');
90
+ p = pp.resolveWithFallback(process.cwd(), 'quorum-scoreboard');
91
+ } catch (_) {
92
+ p = path.join(process.cwd(), '.planning', 'quorum-scoreboard.json');
93
+ }
94
+ }
95
+ try {
96
+ const raw = fs.readFileSync(p, 'utf8');
97
+ const sb = JSON.parse(raw);
98
+ const rounds = Array.isArray(sb.rounds) ? sb.rounds : [];
99
+ if (rounds.length === 0) return null;
100
+
101
+ const slotStats = {};
102
+ for (const round of rounds) {
103
+ const votes = round.votes || {};
104
+ for (const [slot, code] of Object.entries(votes)) {
105
+ if (slot === 'claude') continue; // exclude self
106
+ // FILTER FIRST — inside readMCPAvailabilityRates, before building the rates object.
107
+ // Composite keys (e.g. 'claude-1:deepseek-ai/DeepSeek-V3.2') contain ':' or '/'
108
+ // which are illegal PRISM identifier characters. Filter them out here so the returned
109
+ // rates object contains only base keys — making the function directly testable with
110
+ // realistic scoreboards that include composite keys.
111
+ if (slot.includes(':') || slot.includes('/')) {
112
+ process.stderr.write('[run-prism] Skipping composite key (invalid PRISM identifier): ' + slot + '\n');
113
+ continue;
114
+ }
115
+ if (!slotStats[slot]) slotStats[slot] = { total: 0, unavail: 0 };
116
+ slotStats[slot].total++;
117
+ if (code === 'UNAVAIL') slotStats[slot].unavail++;
118
+ }
119
+ }
120
+
121
+ const rates = {};
122
+ for (const [slot, stats] of Object.entries(slotStats)) {
123
+ if (stats.total === 0) continue;
124
+ rates[slot] = Math.round((1.0 - stats.unavail / stats.total) * 1e6) / 1e6;
125
+ }
126
+ return Object.keys(rates).length > 0 ? rates : null;
127
+ } catch (_) {
128
+ return null; // missing or malformed scoreboard — caller uses priors
129
+ }
130
+ }
131
+
132
+ // ── Read scoreboard for empirical tp_rate / unavail injection (PRISM-02) ────
133
+ // Uses process.cwd()/.planning/quorum-scoreboard.json so tests can point to
134
+ // a fixture by spawning with a custom cwd (same pattern as run-formal-verify).
135
+ let liveTPRate = null;
136
+ let liveUnavail = null;
137
+ let scoreboardPath;
138
+ try {
139
+ const pp = require('./planning-paths.cjs');
140
+ scoreboardPath = pp.resolveWithFallback(process.cwd(), 'quorum-scoreboard');
141
+ } catch (_) {
142
+ scoreboardPath = path.join(process.cwd(), '.planning', 'quorum-scoreboard.json');
143
+ }
144
+
145
+ // ── Load calibration policy ───────────────────────────────────────────────
146
+ const policyPath = path.join(__dirname, '..', '.planning', 'formal', 'policy.yaml');
147
+ let policy;
148
+ try {
149
+ policy = readPolicy(policyPath);
150
+ } catch (e) {
151
+ process.stderr.write('[run-prism] Failed to load policy.yaml: ' + e.message + '\n');
152
+ try {
153
+ writeCheckResult({
154
+ tool: 'run-prism', formalism: 'prism', result: 'fail',
155
+ check_id: 'prism:quorum', surface: 'prism', property: 'Quorum consensus probability under agent availability rates',
156
+ runtime_ms: 0, summary: 'fail: prism:quorum (policy load failed)', triage_tags: [],
157
+ requirement_ids: getRequirementIds('prism:quorum'),
158
+ observation_window: { window_start: new Date().toISOString(), window_end: new Date().toISOString(), n_traces: 0, n_events: 0, window_days: 0 },
159
+ metadata: {}
160
+ });
161
+ } catch (_) {}
162
+ process.exit(1);
163
+ }
164
+
165
+ // ── LOOP-01: export-prism-constants pre-step ─────────────────────────────────
166
+ // Ensures rates.const is always current from the scoreboard before PRISM runs.
167
+ // Fail-open: if export fails, run-prism continues with whatever rates.const exists.
168
+ {
169
+ const exportConstantsPath = path.join(__dirname, 'export-prism-constants.cjs');
170
+ const exportResult = spawnSync(process.execPath, [exportConstantsPath], {
171
+ encoding: 'utf8',
172
+ cwd: process.cwd(),
173
+ timeout: 10000,
174
+ });
175
+ if (exportResult.status !== 0 || exportResult.error) {
176
+ process.stderr.write(
177
+ '[run-prism] Warning: export-prism-constants pre-step failed — rates.const may be stale.\n' +
178
+ (exportResult.stderr || '') + '\n'
179
+ );
180
+ } else {
181
+ process.stdout.write('[run-prism] Pre-step: rates.const updated from scoreboard.\n');
182
+ }
183
+ }
184
+ // ─────────────────────────────────────────────────────────────────────────────
185
+
186
+ if (fs.existsSync(scoreboardPath)) {
187
+ try {
188
+ const sb = JSON.parse(fs.readFileSync(scoreboardPath, 'utf8'));
189
+ const rounds = Array.isArray(sb.rounds) ? sb.rounds : [];
190
+ // Aggregate TP and UNAVAIL counts across all slots (excluding 'claude')
191
+ let totalVotes = 0;
192
+ let tpCount = 0;
193
+ let unavailCount = 0;
194
+ for (const round of rounds) {
195
+ const votes = round.votes || {};
196
+ for (const [slot, code] of Object.entries(votes)) {
197
+ if (slot === 'claude') continue; // exclude self
198
+ totalVotes++;
199
+ if (code === 'TP' || code === 'TP+' || code === 'TN' || code === 'TN+') tpCount++;
200
+ if (code === 'UNAVAIL') unavailCount++;
201
+ }
202
+ }
203
+ if (totalVotes > 0) {
204
+ liveTPRate = Math.round((tpCount / totalVotes) * 1e6) / 1e6;
205
+ liveUnavail = Math.round((unavailCount / totalVotes) * 1e6) / 1e6;
206
+ process.stdout.write(
207
+ '[run-prism] Injected from scoreboard: tp_rate=' + liveTPRate +
208
+ ' unavail=' + liveUnavail + ' (' + rounds.length + ' rounds)\n'
209
+ );
210
+ }
211
+ } catch (_) { /* malformed scoreboard — fall through to priors */ }
212
+ }
213
+ if (liveTPRate === null) {
214
+ liveTPRate = policy.conservative_priors.tp_rate;
215
+ liveUnavail = policy.conservative_priors.unavail;
216
+ process.stderr.write(
217
+ '[run-prism] No scoreboard found — using conservative priors: ' +
218
+ 'tp_rate=' + policy.conservative_priors.tp_rate + ' unavail=' + policy.conservative_priors.unavail + '\n'
219
+ );
220
+ }
221
+
222
+ // ── Cold-start state detection (CALIB-02, CALIB-03) ─────────────────────
223
+ function computeColdStartState(pol, sbPath, crPath) {
224
+ let ciRunCount = 0;
225
+ let quorumRoundCount = 0;
226
+ let firstRunTimestamp = null;
227
+
228
+ // Count CI runs: number of lines in check-results.ndjson
229
+ if (fs.existsSync(crPath)) {
230
+ const lines = fs.readFileSync(crPath, 'utf8')
231
+ .trim().split('\n').filter(l => l.length > 0);
232
+ ciRunCount = lines.length;
233
+ }
234
+
235
+ // Read quorum rounds from scoreboard
236
+ if (fs.existsSync(sbPath)) {
237
+ try {
238
+ const sb = JSON.parse(fs.readFileSync(sbPath, 'utf8'));
239
+ const rounds = Array.isArray(sb.rounds) ? sb.rounds : [];
240
+ quorumRoundCount = rounds.length;
241
+ if (rounds.length > 0) {
242
+ const firstRound = rounds[0];
243
+ // Support timestamp (ISO) or date (MM-DD) field
244
+ const raw = firstRound.timestamp || firstRound.date;
245
+ if (raw) {
246
+ const parsed = Date.parse(raw);
247
+ if (!isNaN(parsed)) firstRunTimestamp = parsed;
248
+ }
249
+ }
250
+ } catch (e) {
251
+ process.stderr.write('[run-prism] Warning: failed to parse scoreboard for cold-start: ' + e.message + '\n');
252
+ }
253
+ }
254
+
255
+ // Compute days since first run (0 if no history)
256
+ const daysSinceFirst = firstRunTimestamp
257
+ ? (Date.now() - firstRunTimestamp) / (1000 * 60 * 60 * 24)
258
+ : 0;
259
+
260
+ // Cold-start is true if ANY threshold is unmet
261
+ const allThresholdsMet =
262
+ ciRunCount >= pol.cold_start.min_ci_runs &&
263
+ quorumRoundCount >= pol.cold_start.min_quorum_rounds &&
264
+ daysSinceFirst >= pol.cold_start.min_days;
265
+ const inColdStart = !allThresholdsMet;
266
+
267
+ return { inColdStart, ciRunCount, quorumRoundCount, daysSinceFirst, firstRunTimestamp };
268
+ }
269
+
270
+ const checkResultsPath = path.join(process.cwd(), '.planning', 'formal', 'check-results.ndjson');
271
+ const coldStartState = computeColdStartState(policy, scoreboardPath, checkResultsPath);
272
+ if (coldStartState.inColdStart) {
273
+ process.stderr.write(
274
+ '[run-prism] Cold-start mode active (thresholds not yet met):\n' +
275
+ '[run-prism] CI runs: ' + coldStartState.ciRunCount + ' / ' + policy.cold_start.min_ci_runs + '\n' +
276
+ '[run-prism] Quorum rounds: ' + coldStartState.quorumRoundCount + ' / ' + policy.cold_start.min_quorum_rounds + '\n' +
277
+ '[run-prism] Days: ' + coldStartState.daysSinceFirst.toFixed(2) + ' / ' + policy.cold_start.min_days + '\n'
278
+ );
279
+ }
280
+
281
+ // ── Build argument list ──────────────────────────────────────────────────────
282
+ // Extra args passed to this script are forwarded to PRISM after the model path.
283
+ // If .planning/formal/prism/quorum.props exists, pass it as the properties file (runs all 4 properties).
284
+ // Otherwise fall back to: -pf "P=? [ F s=1 ]"
285
+ const extraArgs = process.argv.slice(2);
286
+
287
+ // ── MCPENV-04: --model mcp-availability flag ─────────────────────────────────
288
+ // When --model mcp-availability is passed, run mcp-availability.pm instead of quorum.pm.
289
+ // Injects per-slot availability rates from scoreboard as -const flags.
290
+ const modelArgIdx = extraArgs.indexOf('--model');
291
+ const modelArgValue = modelArgIdx >= 0 ? extraArgs[modelArgIdx + 1] : null;
292
+ const useMCPAvailabilityModel = modelArgValue === 'mcp-availability';
293
+
294
+ // Strip --model <value> from extraArgs before forwarding to PRISM
295
+ const filteredExtraArgs = useMCPAvailabilityModel
296
+ ? extraArgs.filter((a, i) => i !== modelArgIdx && i !== modelArgIdx + 1)
297
+ : extraArgs;
298
+
299
+ let activeModelPath = modelPath; // default: quorum.pm
300
+ let activeMcpRates = null; // per-slot rates if mcp-availability model
301
+
302
+ if (useMCPAvailabilityModel) {
303
+ const mcpModelPath = path.join(__dirname, '..', '.planning', 'formal', 'prism', 'mcp-availability.pm');
304
+ if (!fs.existsSync(mcpModelPath)) {
305
+ process.stderr.write('[run-prism] mcp-availability.pm not found at: ' + mcpModelPath + '\n');
306
+ process.exit(1);
307
+ }
308
+ activeModelPath = mcpModelPath;
309
+ activeMcpRates = readMCPAvailabilityRates();
310
+ if (activeMcpRates) {
311
+ process.stdout.write('[run-prism] MCP rates from scoreboard: ' + JSON.stringify(activeMcpRates) + '\n');
312
+ } else {
313
+ process.stderr.write('[run-prism] No scoreboard rates for mcp-availability — using priors (0.85 per slot)\n');
314
+ }
315
+ process.stdout.write('[run-prism] Model: mcp-availability\n');
316
+ }
317
+
318
+ const hasPf = filteredExtraArgs.some(a => a === '-pf' || a === '-prop');
319
+ const propsFile = path.join(__dirname, '..', '.planning', 'formal', 'prism', useMCPAvailabilityModel ? 'mcp-availability.props' : 'quorum.props');
320
+ const hasProps = !hasPf && fs.existsSync(propsFile);
321
+
322
+ // Determine if caller already overrides tp_rate or unavail
323
+ const callerOverridesTP = filteredExtraArgs.some((a, i) => a === '-const' && (filteredExtraArgs[i + 1] || '').startsWith('tp_rate='));
324
+ const callerOverridesUnavail = filteredExtraArgs.some((a, i) => a === '-const' && (filteredExtraArgs[i + 1] || '').startsWith('unavail='));
325
+
326
+ const prismArgs = [activeModelPath];
327
+ if (hasProps) {
328
+ prismArgs.push(propsFile);
329
+ } else if (!hasPf) {
330
+ prismArgs.push('-pf', 'P=? [ F s=1 ]');
331
+ }
332
+
333
+ if (useMCPAvailabilityModel) {
334
+ // Inject per-slot rates as -const flags (slot name: 'codex-1' → 'codex_1_avail')
335
+ if (activeMcpRates) {
336
+ for (const [slot, rate] of Object.entries(activeMcpRates)) {
337
+ const constName = slot.replace(/-/g, '_') + '_avail';
338
+ prismArgs.push('-const', constName + '=' + rate);
339
+ }
340
+ }
341
+ // No tp_rate/unavail injection for mcp-availability model
342
+ } else {
343
+ // Inject empirical/prior rates unless caller overrides (quorum.pm path)
344
+ if (!callerOverridesTP) {
345
+ prismArgs.push('-const', 'tp_rate=' + liveTPRate);
346
+ }
347
+ if (!callerOverridesUnavail) {
348
+ prismArgs.push('-const', 'unavail=' + liveUnavail);
349
+ }
350
+ }
351
+ prismArgs.push(...filteredExtraArgs);
352
+
353
+ process.stdout.write('[run-prism] Binary: ' + prismBin + '\n');
354
+ process.stdout.write('[run-prism] Model: ' + activeModelPath + '\n');
355
+ process.stdout.write('[run-prism] Args: ' + prismArgs.slice(1).join(' ') + '\n');
356
+
357
+ // ── Invoke PRISM ─────────────────────────────────────────────────────────────
358
+ const _startMs = Date.now();
359
+
360
+ const result = spawnSync(prismBin, prismArgs, {
361
+ encoding: 'utf8',
362
+ stdio: 'inherit',
363
+ });
364
+
365
+ if (result.error) {
366
+ process.stderr.write('[run-prism] Failed to launch PRISM: ' + result.error.message + '\n');
367
+ const _runtimeMs = Date.now() - _startMs;
368
+ const modelName = useMCPAvailabilityModel ? 'mcp-availability' : 'quorum';
369
+ const check_id = CHECK_ID_MAP[modelName];
370
+ try {
371
+ writeCheckResult({
372
+ tool: 'run-prism', formalism: 'prism', result: 'fail',
373
+ check_id: check_id, surface: 'prism', property: PROPERTY_MAP[modelName],
374
+ runtime_ms: _runtimeMs, summary: 'fail: ' + check_id + ' in ' + _runtimeMs + 'ms', triage_tags: [],
375
+ requirement_ids: getRequirementIds(check_id),
376
+ observation_window: { window_start: new Date().toISOString(), window_end: new Date().toISOString(), n_traces: 0, n_events: 0, window_days: 0 },
377
+ metadata: {}
378
+ });
379
+ } catch (e) { process.stderr.write('[run-prism] Warning: failed to write check result: ' + e.message + '\n'); }
380
+ process.exit(1);
381
+ }
382
+
383
+ const passed = (result.status || 0) === 0;
384
+
385
+ // Apply cold-start override: never emit result=fail during cold-start (CALIB-02)
386
+ let finalResult = passed ? 'pass' : 'fail';
387
+ if (!passed && coldStartState.inColdStart) {
388
+ finalResult = 'warn';
389
+ process.stderr.write('[run-prism] Cold-start mode: suppressing fail → emitting warn\n');
390
+ }
391
+
392
+ // Build observation_window as top-level field for v2.1 (CALIB-03)
393
+ const _runtimeMs = Date.now() - _startMs;
394
+ const modelName = useMCPAvailabilityModel ? 'mcp-availability' : 'quorum';
395
+ const check_id = CHECK_ID_MAP[modelName];
396
+ const property = PROPERTY_MAP[modelName];
397
+
398
+ const observationWindow = {
399
+ window_start: coldStartState.firstRunTimestamp
400
+ ? new Date(coldStartState.firstRunTimestamp).toISOString()
401
+ : new Date().toISOString(),
402
+ window_end: new Date().toISOString(),
403
+ n_traces: coldStartState.quorumRoundCount,
404
+ n_events: coldStartState.ciRunCount,
405
+ window_days: coldStartState.firstRunTimestamp ? (Date.now() - coldStartState.firstRunTimestamp) / (1000 * 60 * 60 * 24) : 0,
406
+ };
407
+
408
+ // Build triage_tags based on PRISM thresholds
409
+ const tags = [];
410
+ if (_runtimeMs > 300000) tags.push('timeout-risk');
411
+ else if (_runtimeMs > 120000) tags.push('slow-verify');
412
+ if (observationWindow.window_days < 7 || observationWindow.n_traces < 30) tags.push('low-confidence');
413
+
414
+ // Prepare metadata for non-observation_window fields
415
+ const metadata = {};
416
+ if (useMCPAvailabilityModel) {
417
+ metadata.model = 'mcp-availability';
418
+ metadata.per_slot_rates = activeMcpRates || 'priors';
419
+ }
420
+ metadata.tp_rate = liveTPRate;
421
+ metadata.unavail = liveUnavail;
422
+
423
+ try {
424
+ writeCheckResult({
425
+ tool: 'run-prism',
426
+ formalism: 'prism',
427
+ result: finalResult,
428
+ check_id: check_id,
429
+ surface: 'prism',
430
+ property: property,
431
+ runtime_ms: _runtimeMs,
432
+ summary: finalResult + ': ' + modelName + ' in ' + _runtimeMs + 'ms',
433
+ triage_tags: tags,
434
+ requirement_ids: getRequirementIds(check_id),
435
+ observation_window: observationWindow,
436
+ metadata: metadata,
437
+ });
438
+ } catch (e) {
439
+ process.stderr.write('[run-prism] Warning: failed to write check result: ' + e.message + '\n');
440
+ }
441
+
442
+ if (require.main === module) {
443
+ process.exit(passed ? 0 : (finalResult === 'warn' ? 0 : 1));
444
+ }
445
+
446
+ module.exports = { readMCPAvailabilityRates };