@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.
- package/LICENSE +22 -0
- package/README.md +1024 -0
- package/agents/qgsd-codebase-mapper.md +764 -0
- package/agents/qgsd-debugger.md +1201 -0
- package/agents/qgsd-executor.md +472 -0
- package/agents/qgsd-integration-checker.md +443 -0
- package/agents/qgsd-phase-researcher.md +502 -0
- package/agents/qgsd-plan-checker.md +643 -0
- package/agents/qgsd-planner.md +1182 -0
- package/agents/qgsd-project-researcher.md +621 -0
- package/agents/qgsd-quorum-orchestrator.md +628 -0
- package/agents/qgsd-quorum-slot-worker.md +41 -0
- package/agents/qgsd-quorum-synthesizer.md +133 -0
- package/agents/qgsd-quorum-test-worker.md +37 -0
- package/agents/qgsd-quorum-worker.md +161 -0
- package/agents/qgsd-research-synthesizer.md +239 -0
- package/agents/qgsd-roadmapper.md +660 -0
- package/agents/qgsd-verifier.md +628 -0
- package/bin/accept-debug-invariant.cjs +165 -0
- package/bin/account-manager.cjs +719 -0
- package/bin/aggregate-requirements.cjs +466 -0
- package/bin/analyze-assumptions.cjs +757 -0
- package/bin/analyze-state-space.cjs +921 -0
- package/bin/attribute-trace-divergence.cjs +150 -0
- package/bin/auth-drivers/gh-cli.cjs +93 -0
- package/bin/auth-drivers/index.cjs +46 -0
- package/bin/auth-drivers/pool.cjs +67 -0
- package/bin/auth-drivers/simple.cjs +95 -0
- package/bin/autoClosePtoF.cjs +110 -0
- package/bin/blessed-terminal.cjs +350 -0
- package/bin/build-phase-index.cjs +472 -0
- package/bin/call-quorum-slot.cjs +541 -0
- package/bin/ccr-secure-config.cjs +99 -0
- package/bin/ccr-secure-start.cjs +83 -0
- package/bin/check-bundled-sdks.cjs +177 -0
- package/bin/check-coverage-guard.cjs +112 -0
- package/bin/check-liveness-fairness.cjs +95 -0
- package/bin/check-mcp-health.cjs +123 -0
- package/bin/check-provider-health.cjs +395 -0
- package/bin/check-results-exit.cjs +24 -0
- package/bin/check-spec-sync.cjs +360 -0
- package/bin/check-trace-redaction.cjs +271 -0
- package/bin/check-trace-schema-drift.cjs +99 -0
- package/bin/compareDrift.cjs +21 -0
- package/bin/conformance-schema.cjs +12 -0
- package/bin/count-scenarios.cjs +420 -0
- package/bin/debt-dedup.cjs +144 -0
- package/bin/debt-ledger.cjs +61 -0
- package/bin/debt-retention.cjs +76 -0
- package/bin/debt-state-machine.cjs +80 -0
- package/bin/detect-coverage-gaps.cjs +204 -0
- package/bin/detect-project-intent.cjs +362 -0
- package/bin/export-prism-constants.cjs +164 -0
- package/bin/extract-annotations.cjs +633 -0
- package/bin/extractFormalExpected.cjs +104 -0
- package/bin/fingerprint-drift.cjs +24 -0
- package/bin/fingerprint-issue.cjs +46 -0
- package/bin/formal-core.cjs +519 -0
- package/bin/formal-ref-linker.cjs +141 -0
- package/bin/formal-test-sync.cjs +788 -0
- package/bin/generate-formal-specs.cjs +588 -0
- package/bin/generate-petri-net.cjs +397 -0
- package/bin/generate-phase-spec.cjs +249 -0
- package/bin/generate-proposed-changes.cjs +194 -0
- package/bin/generate-tla-cfg.cjs +122 -0
- package/bin/generate-traceability-matrix.cjs +701 -0
- package/bin/generate-triage-bundle.cjs +300 -0
- package/bin/gh-account-rotate.cjs +34 -0
- package/bin/initialize-model-registry.cjs +105 -0
- package/bin/install-formal-tools.cjs +382 -0
- package/bin/install.js +2424 -0
- package/bin/isNumericThreshold.cjs +34 -0
- package/bin/issue-classifier.cjs +151 -0
- package/bin/levenshtein.cjs +74 -0
- package/bin/lint-formal-models.cjs +580 -0
- package/bin/load-baseline-requirements.cjs +275 -0
- package/bin/manage-agents-core.cjs +815 -0
- package/bin/migrate-formal-dir.cjs +172 -0
- package/bin/migrate-planning.cjs +206 -0
- package/bin/migrate-to-slots.cjs +255 -0
- package/bin/nForma.cjs +2726 -0
- package/bin/observe-config.cjs +353 -0
- package/bin/observe-debt-writer.cjs +140 -0
- package/bin/observe-handler-grafana.cjs +128 -0
- package/bin/observe-handler-internal.cjs +301 -0
- package/bin/observe-handler-logstash.cjs +153 -0
- package/bin/observe-handler-prometheus.cjs +185 -0
- package/bin/observe-handlers.cjs +436 -0
- package/bin/observe-registry.cjs +131 -0
- package/bin/observe-render.cjs +168 -0
- package/bin/planning-paths.cjs +167 -0
- package/bin/polyrepo.cjs +560 -0
- package/bin/prism-priority.cjs +153 -0
- package/bin/probe-quorum-slots.cjs +167 -0
- package/bin/promote-model.cjs +225 -0
- package/bin/propose-debug-invariants.cjs +165 -0
- package/bin/providers.json +392 -0
- package/bin/pty-proxy.py +129 -0
- package/bin/qgsd-solve.cjs +2477 -0
- package/bin/quorum-consensus-gate.cjs +238 -0
- package/bin/quorum-formal-context.cjs +183 -0
- package/bin/quorum-slot-dispatch.cjs +934 -0
- package/bin/read-policy.cjs +60 -0
- package/bin/requirement-map.cjs +63 -0
- package/bin/requirements-core.cjs +247 -0
- package/bin/resolve-cli.cjs +101 -0
- package/bin/review-mcp-logs.cjs +294 -0
- package/bin/run-account-manager-tlc.cjs +188 -0
- package/bin/run-account-pool-alloy.cjs +158 -0
- package/bin/run-alloy.cjs +153 -0
- package/bin/run-audit-alloy.cjs +187 -0
- package/bin/run-breaker-tlc.cjs +181 -0
- package/bin/run-formal-check.cjs +395 -0
- package/bin/run-formal-verify.cjs +701 -0
- package/bin/run-installer-alloy.cjs +188 -0
- package/bin/run-oauth-rotation-prism.cjs +132 -0
- package/bin/run-oscillation-tlc.cjs +202 -0
- package/bin/run-phase-tlc.cjs +228 -0
- package/bin/run-prism.cjs +446 -0
- package/bin/run-protocol-tlc.cjs +201 -0
- package/bin/run-quorum-composition-alloy.cjs +155 -0
- package/bin/run-sensitivity-sweep.cjs +231 -0
- package/bin/run-stop-hook-tlc.cjs +188 -0
- package/bin/run-tlc.cjs +467 -0
- package/bin/run-transcript-alloy.cjs +173 -0
- package/bin/run-uppaal.cjs +264 -0
- package/bin/secrets.cjs +134 -0
- package/bin/sensitivity-report.cjs +219 -0
- package/bin/sensitivity-sweep-feedback.cjs +194 -0
- package/bin/set-secret.cjs +29 -0
- package/bin/setup-telemetry-cron.sh +36 -0
- package/bin/sweepPtoF.cjs +63 -0
- package/bin/sync-baseline-requirements.cjs +290 -0
- package/bin/task-envelope.cjs +360 -0
- package/bin/telemetry-collector.cjs +229 -0
- package/bin/unified-mcp-server.mjs +735 -0
- package/bin/update-agents.cjs +369 -0
- package/bin/update-scoreboard.cjs +1134 -0
- package/bin/validate-debt-entry.cjs +207 -0
- package/bin/validate-invariant.cjs +419 -0
- package/bin/validate-memory.cjs +389 -0
- package/bin/validate-requirements-haiku.cjs +435 -0
- package/bin/validate-traces.cjs +438 -0
- package/bin/verify-formal-results.cjs +124 -0
- package/bin/verify-quorum-health.cjs +273 -0
- package/bin/write-check-result.cjs +106 -0
- package/bin/xstate-to-tla.cjs +483 -0
- package/bin/xstate-trace-walker.cjs +205 -0
- package/commands/qgsd/add-phase.md +43 -0
- package/commands/qgsd/add-requirement.md +24 -0
- package/commands/qgsd/add-todo.md +47 -0
- package/commands/qgsd/audit-milestone.md +37 -0
- package/commands/qgsd/check-todos.md +45 -0
- package/commands/qgsd/cleanup.md +18 -0
- package/commands/qgsd/close-formal-gaps.md +33 -0
- package/commands/qgsd/complete-milestone.md +136 -0
- package/commands/qgsd/debug.md +166 -0
- package/commands/qgsd/discuss-phase.md +83 -0
- package/commands/qgsd/execute-phase.md +117 -0
- package/commands/qgsd/fix-tests.md +27 -0
- package/commands/qgsd/formal-test-sync.md +32 -0
- package/commands/qgsd/health.md +22 -0
- package/commands/qgsd/help.md +22 -0
- package/commands/qgsd/insert-phase.md +32 -0
- package/commands/qgsd/join-discord.md +18 -0
- package/commands/qgsd/list-phase-assumptions.md +46 -0
- package/commands/qgsd/map-codebase.md +71 -0
- package/commands/qgsd/map-requirements.md +20 -0
- package/commands/qgsd/mcp-restart.md +176 -0
- package/commands/qgsd/mcp-set-model.md +134 -0
- package/commands/qgsd/mcp-setup.md +1371 -0
- package/commands/qgsd/mcp-status.md +274 -0
- package/commands/qgsd/mcp-update.md +238 -0
- package/commands/qgsd/new-milestone.md +44 -0
- package/commands/qgsd/new-project.md +42 -0
- package/commands/qgsd/observe.md +260 -0
- package/commands/qgsd/pause-work.md +38 -0
- package/commands/qgsd/plan-milestone-gaps.md +34 -0
- package/commands/qgsd/plan-phase.md +44 -0
- package/commands/qgsd/polyrepo.md +50 -0
- package/commands/qgsd/progress.md +24 -0
- package/commands/qgsd/queue.md +54 -0
- package/commands/qgsd/quick.md +133 -0
- package/commands/qgsd/quorum-test.md +275 -0
- package/commands/qgsd/quorum.md +707 -0
- package/commands/qgsd/reapply-patches.md +110 -0
- package/commands/qgsd/remove-phase.md +31 -0
- package/commands/qgsd/research-phase.md +189 -0
- package/commands/qgsd/resume-work.md +40 -0
- package/commands/qgsd/set-profile.md +34 -0
- package/commands/qgsd/settings.md +39 -0
- package/commands/qgsd/solve.md +565 -0
- package/commands/qgsd/sync-baselines.md +119 -0
- package/commands/qgsd/triage.md +233 -0
- package/commands/qgsd/update.md +37 -0
- package/commands/qgsd/verify-work.md +38 -0
- package/hooks/dist/config-loader.js +297 -0
- package/hooks/dist/conformance-schema.cjs +12 -0
- package/hooks/dist/gsd-context-monitor.js +64 -0
- package/hooks/dist/qgsd-check-update.js +62 -0
- package/hooks/dist/qgsd-circuit-breaker.js +682 -0
- package/hooks/dist/qgsd-precompact.js +156 -0
- package/hooks/dist/qgsd-prompt.js +653 -0
- package/hooks/dist/qgsd-session-start.js +122 -0
- package/hooks/dist/qgsd-slot-correlator.js +58 -0
- package/hooks/dist/qgsd-spec-regen.js +86 -0
- package/hooks/dist/qgsd-statusline.js +91 -0
- package/hooks/dist/qgsd-stop.js +553 -0
- package/hooks/dist/qgsd-token-collector.js +133 -0
- package/hooks/dist/unified-mcp-server.mjs +669 -0
- package/package.json +95 -0
- package/scripts/build-hooks.js +46 -0
- package/scripts/postinstall.js +48 -0
- package/scripts/secret-audit.sh +45 -0
- 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 };
|