@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,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 };
|