@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,273 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // bin/verify-quorum-health.cjs
4
+ // Verifies that the XState machine's maxDeliberation is calibrated for the
5
+ // actual empirical reliability of the quorum agents.
6
+ //
7
+ // The formal PRISM model uses conservative priors (tp=0.85, unavail=0.15).
8
+ // This tool substitutes real scoreboard rates and recomputes:
9
+ // - P(majority per round) — joint probability across all agents
10
+ // - Expected rounds — average rounds to decide
11
+ // - P(within MaxDeliberation rounds) — actual confidence for the current setting
12
+ // - Recommended MaxDeliberation for target confidence
13
+ //
14
+ // Exits 1 if actual P(within MaxDeliberation) < TARGET_CONFIDENCE.
15
+ // This makes it a CI gate: if agents degrade, the build breaks and flags it.
16
+ //
17
+ // Usage:
18
+ // node bin/verify-quorum-health.cjs # target: 95%
19
+ // node bin/verify-quorum-health.cjs --target=0.99
20
+ // node bin/verify-quorum-health.cjs --auto-apply # auto-applies maxDeliberation if below target
21
+
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+
25
+ const ROOT = path.join(__dirname, '..');
26
+
27
+ // ── Pure Functions ────────────────────────────────────────────────────────────
28
+
29
+ // suggestMaxDeliberation(pPerRound, targetConfidence)
30
+ // Computes the recommended maxDeliberation value using the geometric distribution formula:
31
+ // k = ceil(log(1 - targetConfidence) / log(1 - pPerRound))
32
+ function suggestMaxDeliberation(pPerRound, targetConfidence) {
33
+ if (pPerRound <= 0) return Infinity;
34
+ if (pPerRound >= 1) return 1;
35
+ return Math.ceil(Math.log(1 - targetConfidence) / Math.log(1 - pPerRound));
36
+ }
37
+
38
+ // computeRates(slot, rounds)
39
+ // Computes per-agent rates from a list of rounds.
40
+ function computeRates(slot, rounds) {
41
+ const SLOTS = ['gemini', 'opencode', 'copilot', 'codex'];
42
+ const MIN_ROUNDS = 30;
43
+ const TP_PRIOR = 0.85;
44
+ const UNAVAIL_PRIOR = 0.15;
45
+
46
+ // Exclude Mode A rounds (empty-string result) and UNAVAILABLE typo variant —
47
+ // neither carries a valid binary signal for rate calculation.
48
+ const relevant = rounds.filter(r => {
49
+ const v = r.votes && r.votes[slot];
50
+ return v !== undefined && v !== '' && v !== 'UNAVAILABLE';
51
+ });
52
+ const n = relevant.length;
53
+ if (n < MIN_ROUNDS) {
54
+ return { n, tpRate: TP_PRIOR, unavailRate: UNAVAIL_PRIOR, usedPrior: true };
55
+ }
56
+ const approvals = relevant.filter(r => r.votes[slot] === 'TP' || r.votes[slot] === 'TP+').length;
57
+ const unavails = relevant.filter(r => r.votes[slot] === 'UNAVAIL').length;
58
+ return {
59
+ n,
60
+ tpRate: approvals / n,
61
+ unavailRate: unavails / n,
62
+ usedPrior: false,
63
+ };
64
+ }
65
+
66
+ // pMajorityExternal(agents)
67
+ // Computes P(at least 2 of the 4 external agents approve) using inclusion-exclusion.
68
+ function pMajorityExternal(agents) {
69
+ const names = Object.keys(agents);
70
+ const n = names.length;
71
+ let pAtLeast2 = 0;
72
+
73
+ for (let mask = 0; mask < (1 << n); mask++) {
74
+ const selected = names.filter((_, i) => mask & (1 << i));
75
+ if (selected.length < 2) continue;
76
+ const prob = names.reduce((acc, name, i) =>
77
+ acc * ((mask & (1 << i)) ? agents[name].pApprove : (1 - agents[name].pApprove))
78
+ , 1);
79
+ pAtLeast2 += prob;
80
+ }
81
+ return pAtLeast2;
82
+ }
83
+
84
+ // applyMaxDeliberationUpdate(newValue, options)
85
+ // Atomically updates maxDeliberation in the XState machine and config.json,
86
+ // then regenerates formal specs. Returns { success, newValue, machineUpdated, configUpdated, specsRegenerated, error, rolledBack }
87
+ function applyMaxDeliberationUpdate(newValue, options = {}) {
88
+ const machineFile = options.machineFile || path.join(ROOT, 'src', 'machines', 'qgsd-workflow.machine.ts');
89
+ const configFile = options.configFile || path.join(ROOT, '.planning', 'config.json');
90
+ const skipSpecGen = options.skipSpecGen || false; // For testing without generate-formal-specs.cjs
91
+
92
+ // Backup originals for rollback
93
+ const backups = {};
94
+ const results = { machineUpdated: false, configUpdated: false, specsRegenerated: false };
95
+
96
+ try {
97
+ // 1. Update machine.ts
98
+ const machineSrc = fs.readFileSync(machineFile, 'utf8');
99
+ backups.machine = { path: machineFile, content: machineSrc };
100
+ const updated = machineSrc.replace(/maxDeliberation:\s*\d+/, 'maxDeliberation: ' + newValue);
101
+ fs.writeFileSync(machineFile, updated, 'utf8');
102
+ results.machineUpdated = true;
103
+
104
+ // 2. Update config.json if it exists
105
+ if (fs.existsSync(configFile)) {
106
+ const configSrc = fs.readFileSync(configFile, 'utf8');
107
+ backups.config = { path: configFile, content: configSrc };
108
+ const config = JSON.parse(configSrc);
109
+ if (!config.workflow) config.workflow = {};
110
+ config.workflow.maxDeliberation = newValue;
111
+ fs.writeFileSync(configFile, JSON.stringify(config, null, 2) + '\n', 'utf8');
112
+ results.configUpdated = true;
113
+ }
114
+
115
+ // 3. Regenerate formal specs (unless skipped for testing)
116
+ if (!skipSpecGen) {
117
+ const { spawnSync } = require('child_process');
118
+ const genResult = spawnSync('node', ['bin/generate-formal-specs.cjs'], { cwd: ROOT, timeout: 30000 });
119
+ if (genResult.error || genResult.status !== 0) throw new Error('Formal spec generation failed');
120
+ results.specsRegenerated = true;
121
+
122
+ // 4. Verify spec sync
123
+ const syncResult = spawnSync('node', ['bin/check-spec-sync.cjs'], { cwd: ROOT, timeout: 10000 });
124
+ if (syncResult.error || syncResult.status !== 0) throw new Error('Spec sync verification failed');
125
+ }
126
+
127
+ return { success: true, newValue, ...results };
128
+
129
+ } catch (err) {
130
+ // Rollback all backups
131
+ for (const backup of Object.values(backups)) {
132
+ try { fs.writeFileSync(backup.path, backup.content, 'utf8'); } catch (_) {}
133
+ }
134
+ return { success: false, error: err.message, rolledBack: true };
135
+ }
136
+ }
137
+
138
+ // ── CLI Handler ───────────────────────────────────────────────────────────────
139
+
140
+ function main() {
141
+ // ── Config ────────────────────────────────────────────────────────────────────
142
+ const DEFAULT_TARGET = 0.95;
143
+ const targetArg = process.argv.find(a => a.startsWith('--target='));
144
+ const TARGET_CONFIDENCE = targetArg ? parseFloat(targetArg.split('=')[1]) : DEFAULT_TARGET;
145
+
146
+ // ── Load XState machine (source of truth for MaxDeliberation) ─────────────────
147
+ const machineSrc = fs.readFileSync(
148
+ path.join(ROOT, 'src', 'machines', 'qgsd-workflow.machine.ts'), 'utf8'
149
+ );
150
+ const maxDelibMatch = machineSrc.match(/maxDeliberation:\s*(\d+)/);
151
+ const maxDelib = maxDelibMatch ? parseInt(maxDelibMatch[1], 10) : null;
152
+ if (!maxDelib) {
153
+ process.stderr.write('[verify-quorum-health] Cannot read maxDeliberation from XState machine.\n');
154
+ process.exit(1);
155
+ }
156
+
157
+ // ── Load scoreboard ────────────────────────────────────────────────────────────
158
+ let scoreboardPath;
159
+ try {
160
+ const pp = require('./planning-paths.cjs');
161
+ scoreboardPath = pp.resolveWithFallback(ROOT, 'quorum-scoreboard');
162
+ } catch (_) {
163
+ scoreboardPath = path.join(ROOT, '.planning', 'quorum-scoreboard.json');
164
+ }
165
+ if (!fs.existsSync(scoreboardPath)) {
166
+ process.stderr.write('[verify-quorum-health] No scoreboard found — cannot compute empirical rates.\n');
167
+ process.stderr.write('[verify-quorum-health] Run some quorum rounds first.\n');
168
+ process.exit(1);
169
+ }
170
+ const scoreboard = JSON.parse(fs.readFileSync(scoreboardPath, 'utf8'));
171
+ const rounds = scoreboard.rounds || [];
172
+
173
+ // ── Compute per-agent rates ───────────────────────────────────────────────────
174
+ const SLOTS = ['gemini', 'opencode', 'copilot', 'codex'];
175
+
176
+ const agentRates = {};
177
+ for (const slot of SLOTS) {
178
+ const r = computeRates(slot, rounds);
179
+ agentRates[slot] = {
180
+ ...r,
181
+ pApprove: r.tpRate * (1 - r.unavailRate), // P(agent votes APPROVE in one round)
182
+ };
183
+ }
184
+
185
+ const pPerRound = pMajorityExternal(agentRates);
186
+ const expected = 1 / pPerRound;
187
+ const pWithinK = k => 1 - Math.pow(1 - pPerRound, k);
188
+ const kForTarget = suggestMaxDeliberation(pPerRound, TARGET_CONFIDENCE);
189
+ const pActual = pWithinK(maxDelib);
190
+
191
+ // ── Conservative-prior comparison ────────────────────────────────────────────
192
+ const P_PRIOR_PER_ROUND = 0.85 * (1 - 0.15); // 0.7225 — what quorum.props assumed
193
+ const pPriorWithinK = k => 1 - Math.pow(1 - P_PRIOR_PER_ROUND, k);
194
+
195
+ // ── Report ────────────────────────────────────────────────────────────────────
196
+ process.stdout.write('\n[verify-quorum-health] QGSD Quorum Reliability Report\n');
197
+ process.stdout.write('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n');
198
+
199
+ process.stdout.write('Per-agent effective approval probability (tp_rate × availability):\n');
200
+ for (const [slot, r] of Object.entries(agentRates)) {
201
+ const flag = r.usedPrior ? ' [prior]' : ' [empirical, n=' + r.n + ']';
202
+ process.stdout.write(
203
+ ' ' + slot.padEnd(10) + 'tp=' + r.tpRate.toFixed(4) +
204
+ ' unavail=' + r.unavailRate.toFixed(4) +
205
+ ' p_approve=' + r.pApprove.toFixed(4) + flag + '\n'
206
+ );
207
+ }
208
+
209
+ process.stdout.write('\nJoint majority probability (P(≥2 of 4 external approve | Claude always approves)):\n');
210
+ process.stdout.write(' Conservative priors: p_round = ' + P_PRIOR_PER_ROUND.toFixed(4) + '\n');
211
+ process.stdout.write(' Empirical rates: p_round = ' + pPerRound.toFixed(4) + '\n');
212
+
213
+ process.stdout.write('\nConvergence analysis with maxDeliberation=' + maxDelib + ' (from XState machine):\n');
214
+ process.stdout.write(' Expected rounds (empirical): ' + expected.toFixed(2) + '\n');
215
+ process.stdout.write(' P(decide within ' + maxDelib + ' rounds):\n');
216
+ process.stdout.write(' Prior assumption: ' + (pPriorWithinK(maxDelib) * 100).toFixed(1) + '% (spec was designed for this)\n');
217
+ process.stdout.write(' Empirical reality: ' + (pActual * 100).toFixed(1) + '% ← actual system\n');
218
+
219
+ const gap = pPriorWithinK(maxDelib) - pActual;
220
+ if (gap > 0.01) {
221
+ process.stdout.write(' Gap: -' + (gap * 100).toFixed(1) + ' percentage points from designed confidence\n');
222
+ }
223
+
224
+ process.stdout.write('\nRecommended maxDeliberation for target confidence levels:\n');
225
+ for (const [label, t] of [['90%', 0.90], ['95%', 0.95], ['99%', 0.99]]) {
226
+ const k = suggestMaxDeliberation(pPerRound, t);
227
+ const mark = t === TARGET_CONFIDENCE ? ' ← target' : '';
228
+ process.stdout.write(' ' + label + ' confidence: maxDeliberation = ' + k + mark + '\n');
229
+ }
230
+
231
+ process.stdout.write('\n');
232
+
233
+ // ── Gate ──────────────────────────────────────────────────────────────────────
234
+ const pass = pActual >= TARGET_CONFIDENCE;
235
+
236
+ if (pass) {
237
+ process.stdout.write('✓ PASS P(within ' + maxDelib + ' rounds) = ' + (pActual * 100).toFixed(1) + '% ≥ ' + (TARGET_CONFIDENCE * 100).toFixed(0) + '% target\n\n');
238
+ } else {
239
+ process.stderr.write(
240
+ '✗ FAIL P(within ' + maxDelib + ' rounds) = ' + (pActual * 100).toFixed(1) + '% < ' + (TARGET_CONFIDENCE * 100).toFixed(0) + '% target\n' +
241
+ ' Recommended: update maxDeliberation to ' + kForTarget + '\n'
242
+ );
243
+
244
+ // ── Handle --auto-apply flag ───────────────────────────────────────────
245
+ const autoApply = process.argv.includes('--auto-apply');
246
+ if (autoApply) {
247
+ process.stderr.write('Applying maxDeliberation = ' + kForTarget + ' (--auto-apply)...\n');
248
+ const result = applyMaxDeliberationUpdate(kForTarget);
249
+ if (result.success) {
250
+ process.stderr.write('Applied successfully. Machine: ' + result.machineUpdated +
251
+ ', Config: ' + result.configUpdated + ', Specs: ' + result.specsRegenerated + '\n');
252
+ process.exit(0); // Success after auto-apply
253
+ } else {
254
+ process.stderr.write('Auto-apply failed: ' + result.error + ' (rolled back)\n');
255
+ process.exit(1);
256
+ }
257
+ } else {
258
+ process.stderr.write(' Run with --auto-apply to apply automatically, or manually update:\n' +
259
+ ' src/machines/qgsd-workflow.machine.ts and re-run: node bin/generate-formal-specs.cjs && node bin/check-spec-sync.cjs\n\n');
260
+ process.exit(1);
261
+ }
262
+ }
263
+ }
264
+
265
+ // ── Exports ───────────────────────────────────────────────────────────────────
266
+
267
+ module.exports = { suggestMaxDeliberation, applyMaxDeliberationUpdate, computeRates, pMajorityExternal };
268
+
269
+ // ── CLI Execution ─────────────────────────────────────────────────────────────
270
+
271
+ if (require.main === module) {
272
+ main();
273
+ }
@@ -0,0 +1,106 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const VALID_RESULTS = ['pass', 'fail', 'warn', 'inconclusive'];
7
+ const VALID_FORMALISMS = ['tla', 'alloy', 'prism', 'trace', 'redaction', 'uppaal'];
8
+
9
+ /**
10
+ * Path to the NDJSON output file.
11
+ * Priority: CHECK_RESULTS_PATH (exact path) > CHECK_RESULTS_ROOT (root-relative) > __dirname fallback.
12
+ * Use CHECK_RESULTS_PATH env var to redirect in tests (avoids polluting real output).
13
+ * Use CHECK_RESULTS_ROOT env var when --project-root is set (writes to ROOT/.planning/formal/).
14
+ */
15
+ const NDJSON_PATH = process.env.CHECK_RESULTS_PATH ||
16
+ (process.env.CHECK_RESULTS_ROOT
17
+ ? path.join(process.env.CHECK_RESULTS_ROOT, '.planning', 'formal', 'check-results.ndjson')
18
+ : path.join(__dirname, '..', '.planning', 'formal', 'check-results.ndjson'));
19
+
20
+ /**
21
+ * Append one normalized check result line to .planning/formal/check-results.ndjson.
22
+ *
23
+ * @param {Object} entry
24
+ * @param {string} entry.tool - Name of the tool/runner (e.g. 'run-tlc')
25
+ * @param {string} entry.formalism - One of VALID_FORMALISMS
26
+ * @param {string} entry.result - One of VALID_RESULTS
27
+ * @param {string} entry.check_id - Unique step identifier (e.g. 'tla:quorum-safety'), required
28
+ * @param {string} entry.surface - STEPS tool group (e.g. 'tla', 'alloy', 'prism', 'ci'), required
29
+ * @param {string} entry.property - Human-readable check description, required
30
+ * @param {number} entry.runtime_ms - Wall-clock elapsed milliseconds from tool start to this call, required. Stored as Math.round(runtime_ms) integer.
31
+ * @param {string} entry.summary - One-line outcome (e.g. 'pass: MCsafety in 1823ms'), required
32
+ * @param {string[]} [entry.triage_tags] - Optional anomaly tags. Defaults to [].
33
+ * @param {object} [entry.observation_window] - Optional stochastic check window metadata (PRISM-critical). Contains { window_start, window_end, n_traces, n_events, window_days }.
34
+ * @param {string[]} [entry.requirement_ids] - Optional array of requirement IDs this check covers. Defaults to [].
35
+ * @param {Object} [entry.metadata] - Optional extra fields (spec, config, etc.)
36
+ * @throws {Error} On validation failure
37
+ */
38
+ function writeCheckResult(entry) {
39
+ if (!entry || typeof entry.tool !== 'string' || entry.tool.length === 0) {
40
+ throw new Error('[write-check-result] tool is required and must be a non-empty string');
41
+ }
42
+ if (!VALID_FORMALISMS.includes(entry.formalism)) {
43
+ throw new Error(
44
+ '[write-check-result] formalism must be one of: ' + VALID_FORMALISMS.join(', ') +
45
+ ' (got: ' + entry.formalism + ')'
46
+ );
47
+ }
48
+ if (!VALID_RESULTS.includes(entry.result)) {
49
+ throw new Error(
50
+ '[write-check-result] result must be one of: ' + VALID_RESULTS.join(', ') +
51
+ ' (got: ' + entry.result + ')'
52
+ );
53
+ }
54
+
55
+ // v2.1 required field validations
56
+ if (typeof entry.check_id !== 'string' || entry.check_id.length === 0) {
57
+ throw new Error('[write-check-result] check_id is required and must be a non-empty string');
58
+ }
59
+ if (typeof entry.surface !== 'string' || entry.surface.length === 0) {
60
+ throw new Error('[write-check-result] surface is required and must be a non-empty string');
61
+ }
62
+ if (typeof entry.property !== 'string' || entry.property.length === 0) {
63
+ throw new Error('[write-check-result] property is required and must be a non-empty string');
64
+ }
65
+ if (typeof entry.runtime_ms !== 'number') {
66
+ throw new Error('[write-check-result] runtime_ms is required and must be a number');
67
+ }
68
+ if (typeof entry.summary !== 'string' || entry.summary.length === 0) {
69
+ throw new Error('[write-check-result] summary is required and must be a non-empty string');
70
+ }
71
+
72
+ // requirement_ids: optional array of requirement IDs this check covers
73
+ if (entry.requirement_ids !== undefined) {
74
+ if (!Array.isArray(entry.requirement_ids)) {
75
+ throw new Error('[write-check-result] requirement_ids must be an array');
76
+ }
77
+ for (const id of entry.requirement_ids) {
78
+ if (typeof id !== 'string') {
79
+ throw new Error('[write-check-result] requirement_ids must contain only strings (got: ' + typeof id + ')');
80
+ }
81
+ }
82
+ }
83
+
84
+ const record = {
85
+ tool: entry.tool,
86
+ formalism: entry.formalism,
87
+ result: entry.result,
88
+ timestamp: new Date().toISOString(),
89
+ check_id: entry.check_id,
90
+ surface: entry.surface,
91
+ property: entry.property,
92
+ runtime_ms: Math.round(entry.runtime_ms),
93
+ summary: entry.summary,
94
+ triage_tags: entry.triage_tags || [],
95
+ requirement_ids: Array.isArray(entry.requirement_ids) ? entry.requirement_ids : [],
96
+ metadata: entry.metadata || {},
97
+ };
98
+
99
+ if (entry.observation_window !== undefined) {
100
+ record.observation_window = entry.observation_window;
101
+ }
102
+
103
+ fs.appendFileSync(NDJSON_PATH, JSON.stringify(record) + '\n', 'utf8');
104
+ }
105
+
106
+ module.exports = { writeCheckResult, NDJSON_PATH, VALID_RESULTS, VALID_FORMALISMS };