@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,238 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // bin/quorum-consensus-gate.cjs
4
+ // PRISM consensus probability gate for quorum rounds.
5
+ // Requirements: SIG-04
6
+ //
7
+ // Usage:
8
+ // node bin/quorum-consensus-gate.cjs [--min-quorum=2]
9
+ //
10
+ // Computes P(consensus_reached) from current scoreboard availability rates
11
+ // using the Poisson binomial distribution (closed-form, no PRISM dependency).
12
+ // Gates quorum rounds when probability is below threshold.
13
+ //
14
+ // Exit 0 = proceed, Exit 1 = defer.
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ /**
20
+ * poissonBinomialCDF(probabilities, k) — computes P(X >= k) for heterogeneous trials.
21
+ *
22
+ * Uses recursive DP approach for the Poisson binomial distribution:
23
+ * dp[0] = 1 (base case: 0 successes with 0 trials)
24
+ * For each probability p_i: dp[j] = dp[j] * (1-p_i) + dp[j-1] * p_i for j from n down to 1
25
+ * Returns P(X >= k) = sum of dp[k..n]
26
+ *
27
+ * Equivalent to the PRISM mcp-availability.pm model but computed in O(n^2) without Java.
28
+ *
29
+ * @param {number[]} probabilities - per-slot availability rates
30
+ * @param {number} k - minimum number of successes needed
31
+ * @returns {number} P(X >= k)
32
+ */
33
+ function poissonBinomialCDF(probabilities, k) {
34
+ const n = probabilities.length;
35
+ if (k > n) return 0;
36
+ if (k <= 0) return 1.0;
37
+
38
+ // dp[j] = probability of exactly j successes after processing i trials
39
+ const dp = new Array(n + 1).fill(0);
40
+ dp[0] = 1; // base: P(0 successes with 0 trials) = 1
41
+
42
+ for (let i = 0; i < n; i++) {
43
+ const p = probabilities[i];
44
+ // Process backwards to avoid overwriting dp[j-1] before it's used
45
+ for (let j = i + 1; j >= 1; j--) {
46
+ dp[j] = dp[j] * (1 - p) + dp[j - 1] * p;
47
+ }
48
+ dp[0] = dp[0] * (1 - p);
49
+ }
50
+
51
+ // P(X >= k) = sum of dp[k..n]
52
+ let result = 0;
53
+ for (let j = k; j <= n; j++) {
54
+ result += dp[j];
55
+ }
56
+
57
+ return result;
58
+ }
59
+
60
+ /**
61
+ * computeConsensusProbability(slotRates, minQuorum) — computes P(consensus_reached).
62
+ * @param {Object} slotRates - { slot_name: availability_rate }
63
+ * @param {number} minQuorum - minimum number of slots needed for consensus
64
+ * @returns {{ probability: number, slotCount: number, minQuorum: number, rates: Object }}
65
+ */
66
+ function computeConsensusProbability(slotRates, minQuorum) {
67
+ const probabilities = Object.values(slotRates);
68
+ const probability = poissonBinomialCDF(probabilities, minQuorum);
69
+
70
+ return {
71
+ probability: Math.round(probability * 1e6) / 1e6,
72
+ slotCount: probabilities.length,
73
+ minQuorum,
74
+ rates: slotRates,
75
+ };
76
+ }
77
+
78
+ /**
79
+ * checkConsensusGate(options) — reads scoreboard, computes probability, returns gate decision.
80
+ * @param {{ scoreboardPath?: string, configPath?: string, minQuorum?: number }} options
81
+ * @returns {{ action: string, probability: number, threshold: number, message: string }}
82
+ */
83
+ function checkConsensusGate(options = {}) {
84
+ const pp = require('./planning-paths.cjs');
85
+ const scoreboardPath = options.scoreboardPath || pp.resolveWithFallback(process.cwd(), 'quorum-scoreboard');
86
+ const configPath = options.configPath || path.join(process.cwd(), '.planning', 'config.json');
87
+ const minQuorum = options.minQuorum || 2;
88
+
89
+ // Read scoreboard availability rates
90
+ let slotRates = null;
91
+ try {
92
+ const { readMCPAvailabilityRates } = require('./run-prism.cjs');
93
+ slotRates = readMCPAvailabilityRates(scoreboardPath);
94
+ } catch (_) {
95
+ // run-prism.cjs not available — fall through to priors
96
+ }
97
+
98
+ // If no rates (empty/missing scoreboard): use conservative prior rates
99
+ if (!slotRates || Object.keys(slotRates).length === 0) {
100
+ slotRates = {
101
+ 'slot-1': 0.85,
102
+ 'slot-2': 0.85,
103
+ 'slot-3': 0.85,
104
+ 'slot-4': 0.85,
105
+ };
106
+ }
107
+
108
+ // Read threshold from config.json
109
+ let threshold = 0.70;
110
+ try {
111
+ if (fs.existsSync(configPath)) {
112
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
113
+ if (config.workflow && typeof config.workflow.consensus_probability_threshold === 'number') {
114
+ threshold = config.workflow.consensus_probability_threshold;
115
+ }
116
+ }
117
+ } catch (_) {
118
+ // Use default threshold
119
+ }
120
+
121
+ // Compute P(consensus_reached)
122
+ const result = computeConsensusProbability(slotRates, minQuorum);
123
+ const probability = result.probability;
124
+
125
+ if (probability >= threshold) {
126
+ return {
127
+ action: 'proceed',
128
+ probability,
129
+ threshold,
130
+ message: 'Consensus probability ' + probability.toFixed(4) + ' >= threshold ' + threshold.toFixed(2) + ' -- proceeding',
131
+ };
132
+ } else {
133
+ return {
134
+ action: 'defer',
135
+ probability,
136
+ threshold,
137
+ message: 'WARNING: Consensus probability ' + probability.toFixed(4) + ' < threshold ' + threshold.toFixed(2) + ' -- deferring quorum round. Slot availability too low for reliable consensus.',
138
+ };
139
+ }
140
+ }
141
+
142
+ /**
143
+ * readEarlyEscalationThreshold(configPaths) — reads workflow.early_escalation_threshold from config.
144
+ *
145
+ * Tries each config path in order. Returns 0.10 (default) if:
146
+ * - No paths provided or none exist
147
+ * - JSON parse fails (fail-open)
148
+ * - Value is missing, non-numeric, or outside [0, 1.0] range
149
+ *
150
+ * @param {string[]} configPaths - ordered list of config file paths to try
151
+ * @returns {number} threshold (0.0 to 1.0), or 0.10 default
152
+ */
153
+ function readEarlyEscalationThreshold(configPaths) {
154
+ const DEFAULT = 0.10;
155
+ const paths = configPaths || [
156
+ path.join(process.cwd(), '.planning', 'config.json'),
157
+ path.join(process.cwd(), '.planning', 'qgsd.json'),
158
+ ];
159
+ for (const cfgPath of paths) {
160
+ try {
161
+ if (fs.existsSync(cfgPath)) {
162
+ const config = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
163
+ const val = config.workflow?.early_escalation_threshold;
164
+ if (typeof val === 'number' && val > 0 && val <= 1.0) return val;
165
+ }
166
+ } catch (_) {
167
+ // Fail-open: JSON parse error, continue to next path
168
+ }
169
+ }
170
+ return DEFAULT;
171
+ }
172
+
173
+ /**
174
+ * computeEarlyEscalation(slotRates, minQuorum, remainingRounds, threshold)
175
+ *
176
+ * Computes P(consensus within remainingRounds) and checks early escalation:
177
+ * - P(at least minQuorum succeed in 1 round) using poissonBinomialCDF
178
+ * - P(consensus within k rounds) = 1 - (1 - pPerRound)^k
179
+ * - If P(consensus | remaining) < threshold, shouldEscalate = true
180
+ *
181
+ * @param {Object} slotRates - { slot_name: availability_rate }
182
+ * @param {number} minQuorum - minimum successful slots needed
183
+ * @param {number} remainingRounds - rounds left in deliberation loop
184
+ * @param {number} threshold - escalation threshold (optional, defaults to 0.10)
185
+ * @returns {{ shouldEscalate: boolean, probability: number, threshold: number, remainingRounds: number, pPerRound: number }}
186
+ */
187
+ function computeEarlyEscalation(slotRates, minQuorum, remainingRounds, threshold) {
188
+ if (typeof threshold !== 'number') threshold = readEarlyEscalationThreshold();
189
+ if (remainingRounds <= 0) {
190
+ return { shouldEscalate: true, probability: 0, threshold, remainingRounds, pPerRound: 0 };
191
+ }
192
+ const rates = Object.values(slotRates);
193
+ const pPerRound = poissonBinomialCDF(rates, minQuorum);
194
+ const probability = 1 - Math.pow(1 - pPerRound, remainingRounds);
195
+ const rounded = Math.round(probability * 1e6) / 1e6;
196
+ return {
197
+ shouldEscalate: rounded < threshold,
198
+ probability: rounded,
199
+ threshold,
200
+ remainingRounds,
201
+ pPerRound: Math.round(pPerRound * 1e6) / 1e6,
202
+ };
203
+ }
204
+
205
+ // ── CLI entrypoint ───────────────────────────────────────────────────────────
206
+ if (require.main === module) {
207
+ const args = process.argv.slice(2);
208
+ const minQuorumArg = args.find(a => a.startsWith('--min-quorum='));
209
+ const minQuorum = minQuorumArg ? parseInt(minQuorumArg.split('=')[1], 10) : undefined;
210
+
211
+ const remainingRoundsArg = args.find(a => a.startsWith('--remaining-rounds='));
212
+
213
+ if (remainingRoundsArg) {
214
+ // HEAL-01: Early escalation mode -- compute P(consensus | remaining rounds)
215
+ const remainingRounds = parseInt(remainingRoundsArg.split('=')[1], 10);
216
+ const pp2 = require('./planning-paths.cjs');
217
+ const scoreboardPath = pp2.resolveWithFallback(process.cwd(), 'quorum-scoreboard');
218
+ let slotRates = null;
219
+ try {
220
+ const { readMCPAvailabilityRates } = require('./run-prism.cjs');
221
+ slotRates = readMCPAvailabilityRates(scoreboardPath);
222
+ } catch (_) {}
223
+ if (!slotRates || Object.keys(slotRates).length === 0) {
224
+ slotRates = { 'slot-1': 0.85, 'slot-2': 0.85, 'slot-3': 0.85, 'slot-4': 0.85 };
225
+ }
226
+ const result = computeEarlyEscalation(slotRates, minQuorum || 2, remainingRounds);
227
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
228
+ // Exit 1 = should escalate (stop deliberating), Exit 0 = continue deliberating
229
+ process.exit(result.shouldEscalate ? 1 : 0);
230
+ } else {
231
+ // Original behavior: unconditional consensus gate check
232
+ const result = checkConsensusGate({ minQuorum });
233
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
234
+ process.exit(result.action === 'proceed' ? 0 : 1);
235
+ }
236
+ }
237
+
238
+ module.exports = { checkConsensusGate, computeConsensusProbability, poissonBinomialCDF, computeEarlyEscalation, readEarlyEscalationThreshold };
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // bin/quorum-formal-context.cjs
4
+ // PLAN-03: Generates formal_spec_summary and verification_result fields for
5
+ // injection into the quorum slot-worker prompt.
6
+ //
7
+ // Translates PLAN.md truths into plain-English descriptions with INVARIANT/PROPERTY
8
+ // classifications, maps TLC results to PASS/FAIL/INCONCLUSIVE, and builds a
9
+ // formatted evidence block for prompt injection.
10
+ //
11
+ // Usage:
12
+ // node bin/quorum-formal-context.cjs <path-to-PLAN.md> [--tlc-result=<json>]
13
+ //
14
+ // Output: Formatted evidence block to stdout
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ const { parsePlanFrontmatter, classifyTruth } = require('./generate-phase-spec.cjs');
20
+
21
+ /**
22
+ * Sanitize a truth string for safe embedding in quorum prompts.
23
+ * - Replaces newlines with spaces
24
+ * - Replaces angle brackets with square brackets (prevents XML/tag injection)
25
+ * - Preserves backslashes (valid in TLA+ operators)
26
+ * - Wraps in backticks instead of double quotes (avoids nested quote confusion)
27
+ *
28
+ * @param {string} truth
29
+ * @returns {string}
30
+ */
31
+ function sanitizeTruth(truth) {
32
+ return truth
33
+ .replace(/\n/g, ' ')
34
+ .replace(/\r/g, '')
35
+ .replace(/</g, '[')
36
+ .replace(/>/g, ']');
37
+ }
38
+
39
+ /**
40
+ * Generate a plain-English formal spec summary from a PLAN.md file.
41
+ *
42
+ * @param {string} planFilePath - Path to the PLAN.md file
43
+ * @returns {{ summary: string, truthCount: number } | null}
44
+ */
45
+ function generateFormalSpecSummary(planFilePath) {
46
+ const content = fs.readFileSync(planFilePath, 'utf8');
47
+ const fm = parsePlanFrontmatter(content);
48
+ const truths = fm.truths || [];
49
+
50
+ if (truths.length === 0) {
51
+ return null;
52
+ }
53
+
54
+ let summary = "Proposed formal properties (from plan's must_haves: truths:):\n";
55
+
56
+ truths.forEach((truth, idx) => {
57
+ const kind = classifyTruth(truth);
58
+ const sanitized = sanitizeTruth(truth);
59
+ const kindLabel = kind === 'INVARIANT'
60
+ ? 'This is a safety property (must always hold)'
61
+ : 'This is a liveness property (must eventually hold)';
62
+ summary += (idx + 1) + '. [' + kind + '] `' + sanitized + '` -- ' + kindLabel + '\n';
63
+ });
64
+
65
+ return { summary, truthCount: truths.length };
66
+ }
67
+
68
+ /**
69
+ * Generate a verification result string from a TLC result object.
70
+ *
71
+ * @param {{ status?: string, truthCount?: number, runtimeMs?: number, violations?: string[], reason?: string } | null | undefined} tlcResult
72
+ * @returns {string}
73
+ */
74
+ function generateVerificationResult(tlcResult) {
75
+ if (tlcResult === null || tlcResult === undefined) {
76
+ return 'INCONCLUSIVE: No verification was run';
77
+ }
78
+
79
+ if (tlcResult.status === 'skipped') {
80
+ return 'INCONCLUSIVE: No truths in plan to verify';
81
+ }
82
+
83
+ if (tlcResult.status === 'passed') {
84
+ const count = tlcResult.truthCount || 0;
85
+ const ms = tlcResult.runtimeMs || 0;
86
+ return 'PASS: All ' + count + ' properties verified by TLC in ' + ms + 'ms';
87
+ }
88
+
89
+ if (tlcResult.status === 'failed') {
90
+ const violations = tlcResult.violations || [];
91
+ // Check for Java not found
92
+ if (violations.some(v => v.includes('Java not found'))) {
93
+ return 'INCONCLUSIVE: Java/TLC not available for verification';
94
+ }
95
+ return 'FAIL: ' + violations.length + ' properties violated -- ' + violations.join(', ');
96
+ }
97
+
98
+ return 'INCONCLUSIVE: Unknown verification state';
99
+ }
100
+
101
+ /**
102
+ * Build a formatted formal evidence block for quorum prompt injection.
103
+ *
104
+ * @param {string | null} formalSpecSummary - Output of generateFormalSpecSummary().summary
105
+ * @param {string | null} verificationResult - Output of generateVerificationResult()
106
+ * @returns {string | null}
107
+ */
108
+ function buildFormalEvidenceBlock(formalSpecSummary, verificationResult) {
109
+ if (!formalSpecSummary && !verificationResult) {
110
+ return null;
111
+ }
112
+
113
+ let block = '=== Formal Evidence ===\n';
114
+ if (formalSpecSummary) {
115
+ block += "Proposed TLA+ properties (from plan's must_haves: truths:):\n";
116
+ block += formalSpecSummary + '\n';
117
+ }
118
+ if (verificationResult) {
119
+ block += 'Verification result: ' + verificationResult + '\n';
120
+ }
121
+ block += '======================';
122
+
123
+ return block;
124
+ }
125
+
126
+ /**
127
+ * Convenience function: get full formal context for a plan file.
128
+ *
129
+ * @param {string} planFilePath - Path to the PLAN.md file
130
+ * @param {{ status?: string, truthCount?: number, runtimeMs?: number, violations?: string[] } | null} tlcResult
131
+ * @returns {{ formalSpecSummary: string | null, verificationResult: string | null, evidenceBlock: string | null }}
132
+ */
133
+ function getFormalContext(planFilePath, tlcResult) {
134
+ const summaryResult = generateFormalSpecSummary(planFilePath);
135
+
136
+ if (!summaryResult) {
137
+ return { formalSpecSummary: null, verificationResult: null, evidenceBlock: null };
138
+ }
139
+
140
+ const formalSpecSummary = summaryResult.summary;
141
+ const verificationResult = generateVerificationResult(tlcResult);
142
+ const evidenceBlock = buildFormalEvidenceBlock(formalSpecSummary, verificationResult);
143
+
144
+ return { formalSpecSummary, verificationResult, evidenceBlock };
145
+ }
146
+
147
+ // ── CLI entrypoint ────────────────────────────────────────────────────────────
148
+ if (require.main === module) {
149
+ const args = process.argv.slice(2).filter(a => !a.startsWith('--'));
150
+ const flags = process.argv.slice(2).filter(a => a.startsWith('--'));
151
+
152
+ if (args.length === 0) {
153
+ process.stderr.write('[quorum-formal-context] Usage: node bin/quorum-formal-context.cjs <path-to-PLAN.md> [--tlc-result=<json>]\n');
154
+ process.exit(1);
155
+ }
156
+
157
+ const planFilePath = path.resolve(args[0]);
158
+ if (!fs.existsSync(planFilePath)) {
159
+ process.stderr.write('[quorum-formal-context] Error: file not found: ' + planFilePath + '\n');
160
+ process.exit(1);
161
+ }
162
+
163
+ // Parse optional --tlc-result flag
164
+ let tlcResult = null;
165
+ const tlcFlag = flags.find(f => f.startsWith('--tlc-result='));
166
+ if (tlcFlag) {
167
+ try {
168
+ tlcResult = JSON.parse(tlcFlag.split('=').slice(1).join('='));
169
+ } catch (e) {
170
+ process.stderr.write('[quorum-formal-context] Warning: could not parse --tlc-result JSON: ' + e.message + '\n');
171
+ }
172
+ }
173
+
174
+ const ctx = getFormalContext(planFilePath, tlcResult);
175
+
176
+ if (ctx.evidenceBlock) {
177
+ process.stdout.write(ctx.evidenceBlock + '\n');
178
+ } else {
179
+ process.stderr.write('[quorum-formal-context] No formal evidence found in ' + planFilePath + '\n');
180
+ }
181
+ }
182
+
183
+ module.exports = { generateFormalSpecSummary, generateVerificationResult, buildFormalEvidenceBlock, getFormalContext };