@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,588 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// bin/generate-formal-specs.cjs
|
|
4
|
+
// Generates ALL formal verification artifacts from the XState machine.
|
|
5
|
+
//
|
|
6
|
+
// The XState machine (src/machines/qgsd-workflow.machine.ts) is the SINGLE SOURCE OF TRUTH.
|
|
7
|
+
// All formal specs are generated artifacts — do not edit them by hand.
|
|
8
|
+
//
|
|
9
|
+
// Generates:
|
|
10
|
+
// .planning/formal/tla/QGSDQuorum.tla — TLA+ spec (states, transitions, invariants)
|
|
11
|
+
// .planning/formal/tla/MCsafety.cfg — TLC safety model config (N=5, symmetry)
|
|
12
|
+
// .planning/formal/tla/MCliveness.cfg — TLC liveness model config (N=3)
|
|
13
|
+
// .planning/formal/alloy/quorum-votes.als — Alloy vote-counting model
|
|
14
|
+
// .planning/formal/prism/quorum.pm — PRISM DTMC convergence model
|
|
15
|
+
// .planning/formal/prism/quorum.props — PRISM property file
|
|
16
|
+
//
|
|
17
|
+
// Usage:
|
|
18
|
+
// node bin/generate-formal-specs.cjs # write all specs
|
|
19
|
+
// node bin/generate-formal-specs.cjs --dry # print without writing
|
|
20
|
+
//
|
|
21
|
+
// Guard translations are driven by GUARD_REGISTRY below.
|
|
22
|
+
// When guard formulas change in the machine, update GUARD_REGISTRY — never hardcode
|
|
23
|
+
// guard logic in template strings.
|
|
24
|
+
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
|
|
28
|
+
let ROOT = path.join(__dirname, '..');
|
|
29
|
+
for (const arg of process.argv) {
|
|
30
|
+
if (arg.startsWith('--project-root=')) ROOT = path.resolve(arg.slice('--project-root='.length));
|
|
31
|
+
}
|
|
32
|
+
const DRY = process.argv.includes('--dry');
|
|
33
|
+
|
|
34
|
+
// ── Model registry update helper ──────────────────────────────────────────────
|
|
35
|
+
// Updates .planning/formal/model-registry.json after each spec write (ARCH-01 wiring).
|
|
36
|
+
// Fail-open: if registry does not exist (not yet initialized), warns and skips.
|
|
37
|
+
function updateModelRegistry(absPath) {
|
|
38
|
+
if (DRY) return; // dry-run: skip registry update
|
|
39
|
+
const registryPath = path.join(ROOT, '.planning', 'formal', 'model-registry.json');
|
|
40
|
+
if (!fs.existsSync(registryPath)) {
|
|
41
|
+
process.stderr.write('[update-model-registry] Skipping registry update: .planning/formal/model-registry.json not yet initialized\n');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
let registry;
|
|
45
|
+
try {
|
|
46
|
+
registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
47
|
+
} catch (err) {
|
|
48
|
+
process.stderr.write('[update-model-registry] Cannot parse registry: ' + err.message + '\n');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (!registry.models) registry.models = {}; // guard: handles corrupted registry
|
|
52
|
+
const key = path.relative(ROOT, absPath).replace(/\\/g, '/');
|
|
53
|
+
const now = new Date().toISOString();
|
|
54
|
+
const existing = registry.models[key] || {};
|
|
55
|
+
registry.models[key] = {
|
|
56
|
+
version: (existing.version || 0) + 1,
|
|
57
|
+
last_updated: now,
|
|
58
|
+
update_source: 'generate',
|
|
59
|
+
source_id: 'generate:formal-specs',
|
|
60
|
+
session_id: null,
|
|
61
|
+
description: existing.description || ''
|
|
62
|
+
};
|
|
63
|
+
registry.last_sync = now;
|
|
64
|
+
// Atomic write: tmp file + rename
|
|
65
|
+
const tmpPath = registryPath + '.tmp.' + Date.now() + '.' + Math.random().toString(36).slice(2);
|
|
66
|
+
fs.writeFileSync(tmpPath, JSON.stringify(registry, null, 2), 'utf8');
|
|
67
|
+
fs.renameSync(tmpPath, registryPath);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Parse XState machine ──────────────────────────────────────────────────────
|
|
71
|
+
const machineFile = path.join(ROOT, 'src', 'machines', 'qgsd-workflow.machine.ts');
|
|
72
|
+
if (!fs.existsSync(machineFile)) {
|
|
73
|
+
process.stderr.write('[generate-formal-specs] XState machine not found at ' + machineFile + ' — skipping (not required for external projects)\n');
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
const src = fs.readFileSync(machineFile, 'utf8');
|
|
77
|
+
|
|
78
|
+
// State names (4-space indent top-level state keys inside `states:`)
|
|
79
|
+
const stateNames = (src.match(/^ ([A-Z_]+):\s*\{/gm) || [])
|
|
80
|
+
.map(l => l.trim().split(':')[0]);
|
|
81
|
+
|
|
82
|
+
// maxDeliberation default
|
|
83
|
+
const maxDelibMatch = src.match(/maxDeliberation:\s*(\d+)/);
|
|
84
|
+
const maxDelib = maxDelibMatch ? parseInt(maxDelibMatch[1], 10) : null;
|
|
85
|
+
|
|
86
|
+
// maxSize default — cap on voters polled per round
|
|
87
|
+
const maxSizeMatch = src.match(/maxSize:\s*(\d+)/);
|
|
88
|
+
const maxSize = maxSizeMatch ? parseInt(maxSizeMatch[1], 10) : 3;
|
|
89
|
+
|
|
90
|
+
// polledCount initial value
|
|
91
|
+
const polledCountMatch = src.match(/polledCount:\s*(\d+)/);
|
|
92
|
+
const polledCountInit = polledCountMatch ? parseInt(polledCountMatch[1], 10) : 0;
|
|
93
|
+
|
|
94
|
+
// Initial state
|
|
95
|
+
const initialMatch = src.match(/initial:\s*'([A-Z_]+)'/);
|
|
96
|
+
const initialState = initialMatch ? initialMatch[1] : null;
|
|
97
|
+
|
|
98
|
+
// Final state (type: 'final') — match state block with no nested braces before type:'final'
|
|
99
|
+
// [^{]* prevents the regex from crossing into adjacent state blocks
|
|
100
|
+
const finalMatch = src.match(/^ ([A-Z_]+):\s*\{[^{]*?type:\s*'final'/m);
|
|
101
|
+
const finalState = finalMatch ? finalMatch[1] : null;
|
|
102
|
+
|
|
103
|
+
if (!stateNames.length || maxDelib === null || !initialState || !finalState || maxSize === null) {
|
|
104
|
+
process.stderr.write('[generate-formal-specs] Could not extract all facts from XState machine.\n');
|
|
105
|
+
process.stderr.write(' states: ' + stateNames.join(', ') + '\n');
|
|
106
|
+
process.stderr.write(' maxDeliberation: ' + maxDelib + '\n');
|
|
107
|
+
process.stderr.write(' maxSize: ' + maxSize + '\n');
|
|
108
|
+
process.stderr.write(' initial: ' + initialState + '\n');
|
|
109
|
+
process.stderr.write(' final: ' + finalState + '\n');
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const ts = new Date().toISOString().split('T')[0];
|
|
114
|
+
const GENERATED_HEADER = (comment, file) =>
|
|
115
|
+
comment + ' ' + file + '\n' +
|
|
116
|
+
comment + ' GENERATED — do not edit by hand.\n' +
|
|
117
|
+
comment + ' Source of truth: src/machines/qgsd-workflow.machine.ts\n' +
|
|
118
|
+
comment + ' Regenerate: node bin/generate-formal-specs.cjs\n' +
|
|
119
|
+
comment + ' Generated: ' + ts + '\n';
|
|
120
|
+
|
|
121
|
+
process.stdout.write('[generate-formal-specs] XState machine → ' + stateNames.join(', ') +
|
|
122
|
+
' maxDeliberation=' + maxDelib + ' maxSize=' + maxSize + ' initial=' + initialState + ' final=' + finalState + '\n');
|
|
123
|
+
|
|
124
|
+
// ── Guard-to-formal translation registry ─────────────────────────────────────
|
|
125
|
+
// Each guard maps its TypeScript predicate to its TLA+, Alloy, and PRISM translations.
|
|
126
|
+
// Update this registry when guard formulas change — never hardcode guard logic in templates.
|
|
127
|
+
const GUARD_REGISTRY = {
|
|
128
|
+
unanimityMet: {
|
|
129
|
+
ts: 'successCount >= polledCount',
|
|
130
|
+
tla: 'n = p', // CollectVotes(n,p): all polled approved
|
|
131
|
+
alloy: '#r.approvals = #r.polled', // VoteRound: approvals equals polled set
|
|
132
|
+
prism: 'tp_rate', // P(unanimous | available) = tp_rate
|
|
133
|
+
desc: 'All polled agents approved (unanimity within the polled set)',
|
|
134
|
+
},
|
|
135
|
+
minQuorumMet: {
|
|
136
|
+
ts: 'successCount >= Math.ceil(slotsAvailable / 2)',
|
|
137
|
+
tla: 'n * 2 >= N', // majority of total roster
|
|
138
|
+
alloy: 'mul[#r.approvals, 2] >= r.total',
|
|
139
|
+
prism: 'tp_rate * (1 - unavail)',
|
|
140
|
+
desc: 'Majority of available agents approved (legacy — superseded by unanimityMet)',
|
|
141
|
+
},
|
|
142
|
+
noInfiniteDeliberation: {
|
|
143
|
+
ts: 'deliberationRounds < maxDeliberation',
|
|
144
|
+
tla: 'deliberationRounds < MaxDeliberation',
|
|
145
|
+
alloy: 'r.rounds < MaxDeliberation',
|
|
146
|
+
prism: 'deliberationRounds < maxDelib',
|
|
147
|
+
desc: 'Deliberation has not reached the maximum round cap',
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// ── 1. QGSDQuorum.tla ─────────────────────────────────────────────────────────
|
|
152
|
+
// Intermediate states = all states except initial and final
|
|
153
|
+
const collectingState = 'COLLECTING_VOTES';
|
|
154
|
+
const deliberatingState = 'DELIBERATING';
|
|
155
|
+
const phaseSet = stateNames.map(s => '"' + s + '"').join(', ');
|
|
156
|
+
|
|
157
|
+
const tlaSpec = [
|
|
158
|
+
'---- MODULE QGSDQuorum ----',
|
|
159
|
+
'(*',
|
|
160
|
+
GENERATED_HEADER(' *', '.planning/formal/tla/QGSDQuorum.tla'),
|
|
161
|
+
' * Models the quorum workflow defined in src/machines/qgsd-workflow.machine.ts.',
|
|
162
|
+
' * Guard translations (from GUARD_REGISTRY in bin/generate-formal-specs.cjs):',
|
|
163
|
+
' * unanimityMet (' + GUARD_REGISTRY.unanimityMet.ts + '): ' + GUARD_REGISTRY.unanimityMet.tla,
|
|
164
|
+
' * noInfiniteDeliberation (' + GUARD_REGISTRY.noInfiniteDeliberation.ts + '): ' + GUARD_REGISTRY.noInfiniteDeliberation.tla,
|
|
165
|
+
'*)',
|
|
166
|
+
'EXTENDS Naturals, FiniteSets, TLC',
|
|
167
|
+
'',
|
|
168
|
+
'CONSTANTS',
|
|
169
|
+
' Agents, \\* Set of quorum model slots (e.g., {"a1","a2","a3","a4","a5"})',
|
|
170
|
+
' MaxDeliberation, \\* Maximum deliberation rounds before forced ' + finalState + ' (default: ' + maxDelib + ')',
|
|
171
|
+
' MaxSize \\* Cap on voters polled per round (default: ' + maxSize + ')',
|
|
172
|
+
'',
|
|
173
|
+
'ASSUME MaxDeliberation \\in Nat /\\ MaxDeliberation > 0',
|
|
174
|
+
'',
|
|
175
|
+
'\\* N = total number of agents; used for cardinality checks',
|
|
176
|
+
'N == Cardinality(Agents)',
|
|
177
|
+
'',
|
|
178
|
+
'ASSUME MaxSize \\in 1..N',
|
|
179
|
+
'',
|
|
180
|
+
'\\* AgentSymmetry: referenced by MCsafety.cfg as SYMMETRY AgentSymmetry.',
|
|
181
|
+
'AgentSymmetry == Permutations(Agents)',
|
|
182
|
+
'',
|
|
183
|
+
'VARIABLES',
|
|
184
|
+
' phase, \\* One of: ' + phaseSet,
|
|
185
|
+
' successCount, \\* Number of APPROVE votes collected in current round',
|
|
186
|
+
' polledCount, \\* Number of agents actually recruited this round (≤ MaxSize; may be less if roster runs dry)',
|
|
187
|
+
' deliberationRounds \\* Number of deliberation rounds completed',
|
|
188
|
+
'',
|
|
189
|
+
'vars == <<phase, successCount, polledCount, deliberationRounds>>',
|
|
190
|
+
'',
|
|
191
|
+
'\\* ── Type invariant ───────────────────────────────────────────────────────────',
|
|
192
|
+
'\\* @requirement QUORUM-01',
|
|
193
|
+
'TypeOK ==',
|
|
194
|
+
' /\\ phase \\in {' + phaseSet + '}',
|
|
195
|
+
' /\\ successCount \\in 0..MaxSize',
|
|
196
|
+
' /\\ polledCount \\in 0..MaxSize',
|
|
197
|
+
' /\\ deliberationRounds \\in 0..MaxDeliberation',
|
|
198
|
+
'',
|
|
199
|
+
'\\* ── Initial state ────────────────────────────────────────────────────────────',
|
|
200
|
+
'Init ==',
|
|
201
|
+
' /\\ phase = "' + initialState + '"',
|
|
202
|
+
' /\\ successCount = 0',
|
|
203
|
+
' /\\ polledCount = ' + polledCountInit,
|
|
204
|
+
' /\\ deliberationRounds = 0',
|
|
205
|
+
'',
|
|
206
|
+
'\\* ── Actions ──────────────────────────────────────────────────────────────────',
|
|
207
|
+
'',
|
|
208
|
+
'\\* StartQuorum: workflow leaves ' + initialState + ' → ' + collectingState,
|
|
209
|
+
'StartQuorum ==',
|
|
210
|
+
' /\\ phase = "' + initialState + '"',
|
|
211
|
+
' /\\ phase\' = "' + collectingState + '"',
|
|
212
|
+
' /\\ UNCHANGED <<successCount, polledCount, deliberationRounds>>',
|
|
213
|
+
'',
|
|
214
|
+
'\\* CollectVotes(n, p): n APPROVE votes from p polled agents (p ≤ MaxSize).',
|
|
215
|
+
'\\* unanimityMet (' + GUARD_REGISTRY.unanimityMet.ts + '): ' + GUARD_REGISTRY.unanimityMet.desc + '.',
|
|
216
|
+
'\\* ' + GUARD_REGISTRY.unanimityMet.tla + ' → ' + finalState + '; otherwise → ' + deliberatingState + '.',
|
|
217
|
+
'CollectVotes(n, p) ==',
|
|
218
|
+
' /\\ phase = "' + collectingState + '"',
|
|
219
|
+
' /\\ p \\in 1..MaxSize',
|
|
220
|
+
' /\\ n \\in 0..p',
|
|
221
|
+
' /\\ successCount\' = n',
|
|
222
|
+
' /\\ polledCount\' = p',
|
|
223
|
+
' /\\ IF ' + GUARD_REGISTRY.unanimityMet.tla,
|
|
224
|
+
' THEN /\\ phase\' = "' + finalState + '"',
|
|
225
|
+
' /\\ UNCHANGED deliberationRounds',
|
|
226
|
+
' ELSE /\\ phase\' = "' + deliberatingState + '"',
|
|
227
|
+
' /\\ deliberationRounds\' = deliberationRounds + 1',
|
|
228
|
+
'',
|
|
229
|
+
'\\* Deliberate(n): n APPROVE votes after a deliberation round.',
|
|
230
|
+
'\\* Unanimity or exhaustion (deliberationRounds >= MaxDeliberation) → ' + finalState + '.',
|
|
231
|
+
'Deliberate(n) ==',
|
|
232
|
+
' /\\ phase = "' + deliberatingState + '"',
|
|
233
|
+
' /\\ n \\in 0..MaxSize',
|
|
234
|
+
' /\\ successCount\' = n',
|
|
235
|
+
' /\\ IF n = polledCount \\/ deliberationRounds >= MaxDeliberation',
|
|
236
|
+
' THEN /\\ phase\' = "' + finalState + '"',
|
|
237
|
+
' /\\ UNCHANGED deliberationRounds',
|
|
238
|
+
' ELSE /\\ phase\' = "' + deliberatingState + '"',
|
|
239
|
+
' /\\ deliberationRounds\' = deliberationRounds + 1',
|
|
240
|
+
' /\\ UNCHANGED polledCount',
|
|
241
|
+
'',
|
|
242
|
+
'\\* Decide: forced termination when deliberation limit is exhausted.',
|
|
243
|
+
'Decide ==',
|
|
244
|
+
' /\\ phase = "' + deliberatingState + '"',
|
|
245
|
+
' /\\ deliberationRounds >= MaxDeliberation',
|
|
246
|
+
' /\\ phase\' = "' + finalState + '"',
|
|
247
|
+
' /\\ UNCHANGED <<successCount, polledCount, deliberationRounds>>',
|
|
248
|
+
'',
|
|
249
|
+
'Next ==',
|
|
250
|
+
' \\/ StartQuorum',
|
|
251
|
+
' \\/ \\E p \\in 1..MaxSize : \\E n \\in 0..p : CollectVotes(n, p)',
|
|
252
|
+
' \\/ \\E n \\in 0..MaxSize : Deliberate(n)',
|
|
253
|
+
' \\/ Decide',
|
|
254
|
+
'',
|
|
255
|
+
'\\* ── Safety invariants ────────────────────────────────────────────────────────',
|
|
256
|
+
'',
|
|
257
|
+
'\\* UnanimityMet: if ' + finalState + ' via approval, unanimity was achieved or deliberation was exhausted.',
|
|
258
|
+
'\\* @requirement QUORUM-02',
|
|
259
|
+
'\\* @requirement SAFE-01',
|
|
260
|
+
'UnanimityMet ==',
|
|
261
|
+
' phase = "' + finalState + '" =>',
|
|
262
|
+
' (successCount = polledCount \\/ deliberationRounds >= MaxDeliberation)',
|
|
263
|
+
'',
|
|
264
|
+
'\\* QuorumCeilingMet: when ' + finalState + ', polledCount did not exceed MaxSize.',
|
|
265
|
+
'\\* @requirement QUORUM-03',
|
|
266
|
+
'\\* @requirement SLOT-01',
|
|
267
|
+
'QuorumCeilingMet ==',
|
|
268
|
+
' phase = "' + finalState + '" =>',
|
|
269
|
+
' /\\ polledCount <= MaxSize',
|
|
270
|
+
' /\\ (successCount = polledCount \\/ deliberationRounds >= MaxDeliberation)',
|
|
271
|
+
'',
|
|
272
|
+
'\\* NoInvalidTransition: ' + initialState + ' can only advance to ' + collectingState + '.',
|
|
273
|
+
'\\* Kept for backwards compatibility; AllTransitionsValid covers this and all other states.',
|
|
274
|
+
'NoInvalidTransition ==',
|
|
275
|
+
' [][phase = "' + initialState + '" => phase\' \\in {"' + initialState + '", "' + collectingState + '"}]_vars',
|
|
276
|
+
'',
|
|
277
|
+
'\\* AllTransitionsValid: every state can only reach its defined successors.',
|
|
278
|
+
'\\* Covers all four states — a superset of NoInvalidTransition.',
|
|
279
|
+
'\\* @requirement SAFE-02',
|
|
280
|
+
'AllTransitionsValid ==',
|
|
281
|
+
' /\\ [][phase = "' + initialState + '" => phase\' \\in {"' + initialState + '", "' + collectingState + '"}]_vars',
|
|
282
|
+
' /\\ [][phase = "' + collectingState + '" => phase\' \\in {"' + collectingState + '", "' + deliberatingState + '", "' + finalState + '"}]_vars',
|
|
283
|
+
' /\\ [][phase = "' + deliberatingState + '" => phase\' \\in {"' + deliberatingState + '", "' + finalState + '"}]_vars',
|
|
284
|
+
' /\\ [][phase = "' + finalState + '" => phase\' = "' + finalState + '"]_vars',
|
|
285
|
+
'',
|
|
286
|
+
'\\* DeliberationBounded: deliberationRounds never exceeds MaxDeliberation.',
|
|
287
|
+
'\\* Follows from the guard noInfiniteDeliberation on the DELIBERATING→DELIBERATING branch.',
|
|
288
|
+
'\\* @requirement LOOP-01',
|
|
289
|
+
'DeliberationBounded ==',
|
|
290
|
+
' deliberationRounds <= MaxDeliberation',
|
|
291
|
+
'',
|
|
292
|
+
'\\* DeliberationMonotone: deliberationRounds only ever increases.',
|
|
293
|
+
'\\* Ensures rounds cannot be rolled back — a key soundness property.',
|
|
294
|
+
'\\* @requirement SAFE-03',
|
|
295
|
+
'DeliberationMonotone ==',
|
|
296
|
+
' [][deliberationRounds\' >= deliberationRounds]_vars',
|
|
297
|
+
'',
|
|
298
|
+
'\\* ── Liveness ─────────────────────────────────────────────────────────────────',
|
|
299
|
+
'',
|
|
300
|
+
'\\* EventualConsensus: every behavior eventually reaches ' + finalState + '.',
|
|
301
|
+
'\\* @requirement QUORUM-04',
|
|
302
|
+
'\\* @requirement RECV-01',
|
|
303
|
+
'EventualConsensus == <>(phase = "' + finalState + '")',
|
|
304
|
+
'',
|
|
305
|
+
'\\* ── Composite actions for fairness ──────────────────────────────────────────',
|
|
306
|
+
'AnyCollectVotes == \\E p \\in 1..MaxSize : \\E n \\in 0..p : CollectVotes(n, p)',
|
|
307
|
+
'AnyDeliberate == \\E n \\in 0..MaxSize : Deliberate(n)',
|
|
308
|
+
'',
|
|
309
|
+
'\\* ── Full specification with fairness ────────────────────────────────────────',
|
|
310
|
+
'Spec == Init /\\ [][Next]_vars',
|
|
311
|
+
' /\\ WF_vars(Decide)',
|
|
312
|
+
' /\\ WF_vars(StartQuorum)',
|
|
313
|
+
' /\\ WF_vars(AnyCollectVotes)',
|
|
314
|
+
' /\\ WF_vars(AnyDeliberate)',
|
|
315
|
+
'',
|
|
316
|
+
'====',
|
|
317
|
+
'',
|
|
318
|
+
].join('\n');
|
|
319
|
+
|
|
320
|
+
// ── 2. MCsafety.cfg + MCliveness.cfg ─────────────────────────────────────────
|
|
321
|
+
const SAFETY_AGENTS = 5;
|
|
322
|
+
const LIVENESS_AGENTS = 3;
|
|
323
|
+
|
|
324
|
+
function agentDecls(n) {
|
|
325
|
+
const lines = [];
|
|
326
|
+
for (let i = 1; i <= n; i++) lines.push(' a' + i + ' = a' + i);
|
|
327
|
+
return lines.join('\n');
|
|
328
|
+
}
|
|
329
|
+
function agentsSet(n) {
|
|
330
|
+
const names = [];
|
|
331
|
+
for (let i = 1; i <= n; i++) names.push('a' + i);
|
|
332
|
+
return '{' + names.join(', ') + '}';
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const tlaCfgHeader = (file, desc) => [
|
|
336
|
+
'\\* ' + file,
|
|
337
|
+
'\\* GENERATED — do not edit by hand.',
|
|
338
|
+
'\\* Source of truth: src/machines/qgsd-workflow.machine.ts',
|
|
339
|
+
'\\* Regenerate: node bin/generate-formal-specs.cjs',
|
|
340
|
+
'\\* Generated: ' + ts,
|
|
341
|
+
'\\*',
|
|
342
|
+
'\\* ' + desc,
|
|
343
|
+
'\\* Run: node bin/run-tlc.cjs ' + file.replace('.planning/formal/tla/', '').replace('.cfg', ''),
|
|
344
|
+
].join('\n');
|
|
345
|
+
|
|
346
|
+
const safetyCfg = tlaCfgHeader('.planning/formal/tla/MCsafety.cfg',
|
|
347
|
+
'TLC safety model: N=' + SAFETY_AGENTS + ' agents, symmetry reduction, no liveness.') + '\n' + [
|
|
348
|
+
'SPECIFICATION Spec',
|
|
349
|
+
'CONSTANTS',
|
|
350
|
+
agentDecls(SAFETY_AGENTS),
|
|
351
|
+
' Agents = ' + agentsSet(SAFETY_AGENTS),
|
|
352
|
+
' MaxDeliberation = ' + maxDelib,
|
|
353
|
+
' MaxSize = ' + maxSize,
|
|
354
|
+
'SYMMETRY AgentSymmetry',
|
|
355
|
+
'INVARIANT TypeOK',
|
|
356
|
+
'INVARIANT UnanimityMet',
|
|
357
|
+
'INVARIANT QuorumCeilingMet',
|
|
358
|
+
'INVARIANT DeliberationBounded',
|
|
359
|
+
'PROPERTY AllTransitionsValid',
|
|
360
|
+
'PROPERTY DeliberationMonotone',
|
|
361
|
+
'CHECK_DEADLOCK FALSE',
|
|
362
|
+
'',
|
|
363
|
+
].join('\n');
|
|
364
|
+
|
|
365
|
+
const livenessCfg = tlaCfgHeader('.planning/formal/tla/MCliveness.cfg',
|
|
366
|
+
'TLC liveness model: N=' + LIVENESS_AGENTS + ' agents, no symmetry (incompatible with liveness).') + '\n' + [
|
|
367
|
+
'SPECIFICATION Spec',
|
|
368
|
+
'CONSTANTS',
|
|
369
|
+
agentDecls(LIVENESS_AGENTS),
|
|
370
|
+
' Agents = ' + agentsSet(LIVENESS_AGENTS),
|
|
371
|
+
' MaxDeliberation = ' + maxDelib,
|
|
372
|
+
' MaxSize = ' + maxSize,
|
|
373
|
+
'PROPERTY EventualConsensus',
|
|
374
|
+
'CHECK_DEADLOCK FALSE',
|
|
375
|
+
'',
|
|
376
|
+
].join('\n');
|
|
377
|
+
|
|
378
|
+
// ── 3. quorum-votes.als ───────────────────────────────────────────────────────
|
|
379
|
+
// Alloy vote-counting model — derived from unanimityMet guard in XState machine
|
|
380
|
+
const alloySpec = [
|
|
381
|
+
GENERATED_HEADER('--', '.planning/formal/alloy/quorum-votes.als'),
|
|
382
|
+
'-- QGSD Quorum Vote-Counting Model (Alloy 6)',
|
|
383
|
+
'-- Requirements: ALY-01',
|
|
384
|
+
'--',
|
|
385
|
+
'-- Models the unanimityMet guard from src/machines/qgsd-workflow.machine.ts:',
|
|
386
|
+
'-- ' + GUARD_REGISTRY.unanimityMet.ts,
|
|
387
|
+
'-- ≡ ' + GUARD_REGISTRY.unanimityMet.alloy + ' (all polled agents approved)',
|
|
388
|
+
'--',
|
|
389
|
+
'-- Guard registry translation: GUARD_REGISTRY.unanimityMet.alloy',
|
|
390
|
+
'-- ' + GUARD_REGISTRY.unanimityMet.desc,
|
|
391
|
+
'--',
|
|
392
|
+
'-- Checks that no round reaches ' + finalState + ' without satisfying the unanimity predicate.',
|
|
393
|
+
'-- Scope: ' + SAFETY_AGENTS + ' agents (QGSD quorum slot count), 5 vote rounds.',
|
|
394
|
+
'',
|
|
395
|
+
'module quorum_votes',
|
|
396
|
+
'',
|
|
397
|
+
'-- Fix agent count to ' + SAFETY_AGENTS + ' (QGSD quorum slot count).',
|
|
398
|
+
'-- This makes the numeric threshold assertions below concrete and verifiable.',
|
|
399
|
+
'fact AgentCount { #Agent = ' + SAFETY_AGENTS + ' }',
|
|
400
|
+
'',
|
|
401
|
+
'sig Agent {}',
|
|
402
|
+
'',
|
|
403
|
+
'sig VoteRound {',
|
|
404
|
+
' approvals : set Agent,',
|
|
405
|
+
' polled : one Int,',
|
|
406
|
+
' total : one Int',
|
|
407
|
+
'}',
|
|
408
|
+
'',
|
|
409
|
+
'-- UnanimityReached: mirrors unanimityMet guard from XState machine.',
|
|
410
|
+
'-- ' + GUARD_REGISTRY.unanimityMet.desc + '.',
|
|
411
|
+
'-- Equivalent to successCount >= polledCount in TypeScript.',
|
|
412
|
+
'pred UnanimityReached [r : VoteRound] {',
|
|
413
|
+
' #r.approvals = r.polled',
|
|
414
|
+
'}',
|
|
415
|
+
'',
|
|
416
|
+
'pred ValidRound [r : VoteRound] {',
|
|
417
|
+
' r.total = #Agent -- total must equal actual agent count',
|
|
418
|
+
' r.polled <= r.total -- can\'t poll more than exist',
|
|
419
|
+
' r.polled >= 1 -- must poll at least one agent',
|
|
420
|
+
' #r.approvals <= r.polled -- can\'t have more approvals than polled',
|
|
421
|
+
'}',
|
|
422
|
+
'',
|
|
423
|
+
'-- ASSERTION 1: Full unanimity — all polled agents approve.',
|
|
424
|
+
'-- Non-trivial: Alloy must verify #approvals = polled for unanimity.',
|
|
425
|
+
'-- @requirement QUORUM-02',
|
|
426
|
+
'-- @requirement SAFE-01',
|
|
427
|
+
'assert ThresholdPasses {',
|
|
428
|
+
' all r : VoteRound |',
|
|
429
|
+
' (ValidRound[r] and #r.approvals = r.polled) implies UnanimityReached[r]',
|
|
430
|
+
'}',
|
|
431
|
+
'',
|
|
432
|
+
'-- ASSERTION 2: One missing approval fails unanimity.',
|
|
433
|
+
'-- Non-trivial: any polled agent not approving must block consensus.',
|
|
434
|
+
'-- @requirement QUORUM-02',
|
|
435
|
+
'-- @requirement SAFE-01',
|
|
436
|
+
'assert BelowThresholdFails {',
|
|
437
|
+
' all r : VoteRound |',
|
|
438
|
+
' (ValidRound[r] and r.polled > 1 and #r.approvals = minus[r.polled, 1]) implies not UnanimityReached[r]',
|
|
439
|
+
'}',
|
|
440
|
+
'',
|
|
441
|
+
'-- ASSERTION 3: Zero approvals always fails — safety baseline regardless of N.',
|
|
442
|
+
'-- @requirement SAFE-04',
|
|
443
|
+
'assert ZeroApprovalsFail {',
|
|
444
|
+
' all r : VoteRound | ValidRound[r] implies (not (#r.approvals = 0 and UnanimityReached[r]))',
|
|
445
|
+
'}',
|
|
446
|
+
'',
|
|
447
|
+
'check ThresholdPasses for ' + SAFETY_AGENTS + ' Agent, 5 VoteRound',
|
|
448
|
+
'check BelowThresholdFails for ' + SAFETY_AGENTS + ' Agent, 5 VoteRound',
|
|
449
|
+
'check ZeroApprovalsFail for ' + SAFETY_AGENTS + ' Agent, 5 VoteRound',
|
|
450
|
+
'',
|
|
451
|
+
'-- Show an example valid unanimity round',
|
|
452
|
+
'run UnanimityReached for ' + SAFETY_AGENTS + ' Agent, 1 VoteRound',
|
|
453
|
+
'',
|
|
454
|
+
].join('\n');
|
|
455
|
+
|
|
456
|
+
// ── 4. quorum.pm ─────────────────────────────────────────────────────────────
|
|
457
|
+
// PRISM DTMC — 3-state model derived from machine states (collecting, deliberating, decided)
|
|
458
|
+
// State numbering: 0=collecting, 1=decided, 2=deliberating (absorbing at 1)
|
|
459
|
+
const prismSpec = [
|
|
460
|
+
GENERATED_HEADER('//', '.planning/formal/prism/quorum.pm'),
|
|
461
|
+
'// QGSD Quorum Convergence — DTMC Model',
|
|
462
|
+
'// Requirements: PRM-01',
|
|
463
|
+
'//',
|
|
464
|
+
'// Discrete-Time Markov Chain modeling quorum state transitions.',
|
|
465
|
+
'// States:',
|
|
466
|
+
'// s=0 : ' + collectingState + ' (initial)',
|
|
467
|
+
'// s=1 : ' + finalState + ' (absorbing)',
|
|
468
|
+
'// s=2 : ' + deliberatingState + ' (retry)',
|
|
469
|
+
'//',
|
|
470
|
+
'// Derived from src/machines/qgsd-workflow.machine.ts:',
|
|
471
|
+
'// ' + stateNames.join(', '),
|
|
472
|
+
'//',
|
|
473
|
+
'// Guard translations (from GUARD_REGISTRY):',
|
|
474
|
+
'// unanimityMet (' + GUARD_REGISTRY.unanimityMet.ts + '): ' + GUARD_REGISTRY.unanimityMet.desc,
|
|
475
|
+
'// PRISM translation: ' + GUARD_REGISTRY.unanimityMet.prism + ' = P(all polled agents approve)',
|
|
476
|
+
'//',
|
|
477
|
+
'// Default rates are conservative priors. Override with empirical values:',
|
|
478
|
+
'// node bin/export-prism-constants.cjs',
|
|
479
|
+
'//',
|
|
480
|
+
'// To run (requires PRISM_BIN env var):',
|
|
481
|
+
'// $PRISM_BIN .planning/formal/prism/quorum.pm -pf "P=? [ F s=1 ]"',
|
|
482
|
+
'//',
|
|
483
|
+
'// To override rates from empirical scoreboard data (no file-include in PRISM):',
|
|
484
|
+
'// $PRISM_BIN .planning/formal/prism/quorum.pm -pf "P=? [ F s=1 ]" -const tp_rate=0.72 -const unavail=0.28',
|
|
485
|
+
'',
|
|
486
|
+
'dtmc',
|
|
487
|
+
'',
|
|
488
|
+
'// Slot aggregate rates (conservative priors — override with empirical data)',
|
|
489
|
+
'// tp_rate = P(a slot votes APPROVE | it is AVAILABLE) — unanimityMet criterion',
|
|
490
|
+
'// unavail = P(slot is UNAVAILABLE in a given round)',
|
|
491
|
+
'const double tp_rate; // injected by run-prism.cjs from scoreboard (see bin/export-prism-constants.cjs)',
|
|
492
|
+
'const double unavail; // injected by run-prism.cjs from scoreboard (see bin/export-prism-constants.cjs)',
|
|
493
|
+
'',
|
|
494
|
+
'module quorum_convergence',
|
|
495
|
+
' s : [0..2] init 0;',
|
|
496
|
+
'',
|
|
497
|
+
' // From ' + collectingState + ':',
|
|
498
|
+
' // unanimityMet → ' + finalState + '; otherwise → ' + deliberatingState,
|
|
499
|
+
' [] s=0 -> (tp_rate * (1 - unavail)) : (s\'=1)',
|
|
500
|
+
' + (1 - tp_rate * (1 - unavail)) : (s\'=2);',
|
|
501
|
+
'',
|
|
502
|
+
' // From ' + deliberatingState + ': same transition probabilities (memoryless DTMC)',
|
|
503
|
+
' // Note: MaxDeliberation (' + maxDelib + ') is enforced by XState guard, not modeled here.',
|
|
504
|
+
' // The DTMC captures convergence probability per round, not the capped count.',
|
|
505
|
+
' [] s=2 -> (tp_rate * (1 - unavail)) : (s\'=1)',
|
|
506
|
+
' + (1 - tp_rate * (1 - unavail)) : (s\'=2);',
|
|
507
|
+
'',
|
|
508
|
+
' // ' + finalState + ' is an absorbing state',
|
|
509
|
+
' [] s=1 -> 1.0 : (s\'=1);',
|
|
510
|
+
'',
|
|
511
|
+
'endmodule',
|
|
512
|
+
'',
|
|
513
|
+
'// Reward structure: count deliberation rounds (steps spent outside ' + finalState + ')',
|
|
514
|
+
'rewards "rounds"',
|
|
515
|
+
' s=0 : 1; // cost of one ' + collectingState + ' step',
|
|
516
|
+
' s=2 : 1; // cost of one ' + deliberatingState + ' step',
|
|
517
|
+
'endrewards',
|
|
518
|
+
'',
|
|
519
|
+
'// Properties checked in .planning/formal/prism/quorum.props (run with quorum.props file):',
|
|
520
|
+
'// P1: Eventual convergence — P=? [ F s=1 ] (should be 1.0)',
|
|
521
|
+
'// P2: Expected rounds — R{"rounds"}=? [ F s=1 ] (should be ~1/' + 'p where p=tp_rate*(1-unavail))',
|
|
522
|
+
'// P3: Decide within ' + maxDelib + ' rounds — P=? [ F<=' + maxDelib + ' s=1 ]',
|
|
523
|
+
'// P4: Decide within 10 — P=? [ F<=10 s=1 ]',
|
|
524
|
+
'',
|
|
525
|
+
].join('\n');
|
|
526
|
+
|
|
527
|
+
// ── 4b. quorum.props ─────────────────────────────────────────────────────────
|
|
528
|
+
const prismProps = [
|
|
529
|
+
'// .planning/formal/prism/quorum.props',
|
|
530
|
+
'// GENERATED — do not edit by hand.',
|
|
531
|
+
'// Source of truth: src/machines/qgsd-workflow.machine.ts',
|
|
532
|
+
'// Regenerate: node bin/generate-formal-specs.cjs',
|
|
533
|
+
'// Generated: ' + ts,
|
|
534
|
+
'//',
|
|
535
|
+
'// Run all properties:',
|
|
536
|
+
'// $PRISM_BIN .planning/formal/prism/quorum.pm .planning/formal/prism/quorum.props',
|
|
537
|
+
'',
|
|
538
|
+
'// P1: Eventual convergence — must be exactly 1.0 (certain termination)',
|
|
539
|
+
'// @requirement PRM-01',
|
|
540
|
+
'// @requirement QUORUM-04',
|
|
541
|
+
'P=? [ F s=1 ]',
|
|
542
|
+
'',
|
|
543
|
+
'// P2: Expected deliberation rounds until ' + finalState,
|
|
544
|
+
'// Conservative priors (tp_rate=0.85, unavail=0.15) give p=0.7225, E=~1.38 rounds.',
|
|
545
|
+
'// @requirement PRM-01',
|
|
546
|
+
'R{"rounds"}=? [ F s=1 ]',
|
|
547
|
+
'',
|
|
548
|
+
'// P3: Probability of deciding within MaxDeliberation=' + maxDelib + ' rounds',
|
|
549
|
+
'// @requirement PRM-01',
|
|
550
|
+
'// @requirement LOOP-01',
|
|
551
|
+
'P=? [ F<=' + maxDelib + ' s=1 ]',
|
|
552
|
+
'',
|
|
553
|
+
'// P4: Probability of deciding within 10 rounds (high-confidence bound)',
|
|
554
|
+
'// @requirement PRM-01',
|
|
555
|
+
'P=? [ F<=10 s=1 ]',
|
|
556
|
+
'',
|
|
557
|
+
].join('\n');
|
|
558
|
+
|
|
559
|
+
// ── Write or print ────────────────────────────────────────────────────────────
|
|
560
|
+
const outputs = [
|
|
561
|
+
{ rel: '.planning/formal/tla/QGSDQuorum.tla', content: tlaSpec },
|
|
562
|
+
{ rel: '.planning/formal/tla/MCsafety.cfg', content: safetyCfg },
|
|
563
|
+
{ rel: '.planning/formal/tla/MCliveness.cfg', content: livenessCfg },
|
|
564
|
+
{ rel: '.planning/formal/alloy/quorum-votes.als', content: alloySpec },
|
|
565
|
+
{ rel: '.planning/formal/prism/quorum.pm', content: prismSpec },
|
|
566
|
+
{ rel: '.planning/formal/prism/quorum.props', content: prismProps },
|
|
567
|
+
];
|
|
568
|
+
|
|
569
|
+
const REGISTRY_EXTS = new Set(['.tla', '.als', '.pm']);
|
|
570
|
+
|
|
571
|
+
for (const { rel, content } of outputs) {
|
|
572
|
+
if (DRY) {
|
|
573
|
+
process.stdout.write('\n--- ' + rel + ' ---\n' + content + '\n');
|
|
574
|
+
} else {
|
|
575
|
+
const absOut = path.join(ROOT, rel);
|
|
576
|
+
fs.mkdirSync(path.dirname(absOut), { recursive: true });
|
|
577
|
+
fs.writeFileSync(absOut, content, 'utf8');
|
|
578
|
+
process.stdout.write('[generate-formal-specs] Written: ' + rel + '\n');
|
|
579
|
+
// Update model registry for canonical spec files only (not .cfg or .props)
|
|
580
|
+
if (REGISTRY_EXTS.has(path.extname(rel))) {
|
|
581
|
+
updateModelRegistry(absOut);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (!DRY) {
|
|
587
|
+
process.stdout.write('[generate-formal-specs] All formal specs generated from XState machine.\n');
|
|
588
|
+
}
|