@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,228 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// bin/run-phase-tlc.cjs
|
|
4
|
+
// PLAN-02: TLC verification runner for proposed-changes.tla with structured feedback.
|
|
5
|
+
//
|
|
6
|
+
// Runs TLC on a proposed-changes.tla generated from a PLAN.md file and returns
|
|
7
|
+
// structured results with pass/violations fields. Provides iterativeVerify for
|
|
8
|
+
// single-attempt orchestration and formatTlcFeedback for human-readable feedback.
|
|
9
|
+
//
|
|
10
|
+
// Usage:
|
|
11
|
+
// node bin/run-phase-tlc.cjs <path-to-PLAN.md>
|
|
12
|
+
//
|
|
13
|
+
// Requires: Java >=17, .planning/formal/tla/tla2tools.jar
|
|
14
|
+
//
|
|
15
|
+
// NOTE: Uses spawnSync (no shell) for safe subprocess invocation -- no exec().
|
|
16
|
+
|
|
17
|
+
const { spawnSync } = require('child_process');
|
|
18
|
+
const JAVA_HEAP_MAX = process.env.QGSD_JAVA_HEAP_MAX || '512m';
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
const { generateProposedChanges, generateTlaCfg } = require('./generate-proposed-changes.cjs');
|
|
23
|
+
const { classifyTruth } = require('./generate-phase-spec.cjs');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Run TLC on a proposed-changes.tla spec file.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} specPath - Path to the .tla spec file
|
|
29
|
+
* @param {string} cfgPath - Path to the .cfg config file
|
|
30
|
+
* @param {{ javaOverride?: string, jarOverride?: string }} [options]
|
|
31
|
+
* @returns {{ passed: boolean, violations: string[], output: string, runtimeMs: number }}
|
|
32
|
+
*/
|
|
33
|
+
function runPhaseTlc(specPath, cfgPath, options) {
|
|
34
|
+
options = options || {};
|
|
35
|
+
|
|
36
|
+
// Check tla2tools.jar existence
|
|
37
|
+
const tla2toolsPath = options.jarOverride || path.join(__dirname, '..', '.planning', 'formal', 'tla', 'tla2tools.jar');
|
|
38
|
+
if (!fs.existsSync(tla2toolsPath)) {
|
|
39
|
+
return {
|
|
40
|
+
passed: false,
|
|
41
|
+
violations: ['tla2tools.jar not found at ' + tla2toolsPath + ' -- run: curl -L https://github.com/tlaplus/tlaplus/releases/download/v1.8.0/tla2tools.jar -o .planning/formal/tla/tla2tools.jar'],
|
|
42
|
+
output: '',
|
|
43
|
+
runtimeMs: 0,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Locate Java binary
|
|
48
|
+
let javaExe = options.javaOverride || null;
|
|
49
|
+
|
|
50
|
+
if (!javaExe) {
|
|
51
|
+
const JAVA_HOME = process.env.JAVA_HOME;
|
|
52
|
+
if (JAVA_HOME) {
|
|
53
|
+
javaExe = path.join(JAVA_HOME, 'bin', 'java');
|
|
54
|
+
if (!fs.existsSync(javaExe)) {
|
|
55
|
+
javaExe = null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (!javaExe) {
|
|
59
|
+
const probe = spawnSync('java', ['--version'], { encoding: 'utf8' });
|
|
60
|
+
if (probe.error || probe.status !== 0) {
|
|
61
|
+
return {
|
|
62
|
+
passed: false,
|
|
63
|
+
violations: ['Java not found -- install JDK 17+'],
|
|
64
|
+
output: '',
|
|
65
|
+
runtimeMs: 0,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
javaExe = 'java';
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
// Validate override path
|
|
72
|
+
if (!fs.existsSync(javaExe)) {
|
|
73
|
+
return {
|
|
74
|
+
passed: false,
|
|
75
|
+
violations: ['Java not found at ' + javaExe + ' -- install JDK 17+'],
|
|
76
|
+
output: '',
|
|
77
|
+
runtimeMs: 0,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Invoke TLC via spawnSync (no shell -- safe subprocess)
|
|
83
|
+
const startMs = Date.now();
|
|
84
|
+
process.stderr.write('[heap] Xms=64m Xmx=' + JAVA_HEAP_MAX + '\n');
|
|
85
|
+
const tlcResult = spawnSync(javaExe, [
|
|
86
|
+
'-XX:+UseParallelGC',
|
|
87
|
+
'-Xms64m', '-Xmx' + JAVA_HEAP_MAX,
|
|
88
|
+
'-jar', tla2toolsPath,
|
|
89
|
+
'-workers', '1',
|
|
90
|
+
'-config', cfgPath,
|
|
91
|
+
specPath,
|
|
92
|
+
], { encoding: 'utf8', timeout: 60000 });
|
|
93
|
+
const runtimeMs = Date.now() - startMs;
|
|
94
|
+
|
|
95
|
+
const output = (tlcResult.stdout || '') + (tlcResult.stderr || '');
|
|
96
|
+
const violations = [];
|
|
97
|
+
|
|
98
|
+
if (tlcResult.error) {
|
|
99
|
+
violations.push('TLC invocation failed: ' + tlcResult.error.message);
|
|
100
|
+
return { passed: false, violations, output, runtimeMs };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Parse violations
|
|
104
|
+
const invariantRegex = /Invariant (\w+) is violated/g;
|
|
105
|
+
const propertyRegex = /Property (\w+) is violated/g;
|
|
106
|
+
const errorRegex = /^Error:\s*(.+)/gm;
|
|
107
|
+
|
|
108
|
+
let match;
|
|
109
|
+
while ((match = invariantRegex.exec(output)) !== null) {
|
|
110
|
+
violations.push('Invariant ' + match[1] + ' is violated');
|
|
111
|
+
}
|
|
112
|
+
while ((match = propertyRegex.exec(output)) !== null) {
|
|
113
|
+
violations.push('Property ' + match[1] + ' is violated');
|
|
114
|
+
}
|
|
115
|
+
while ((match = errorRegex.exec(output)) !== null) {
|
|
116
|
+
// Only add Error lines that aren't already captured as violations
|
|
117
|
+
const errText = match[1].trim();
|
|
118
|
+
if (!violations.some(v => v.includes(errText))) {
|
|
119
|
+
violations.push('Error: ' + errText);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Check for successful completion
|
|
124
|
+
const passed = output.includes('Model checking completed. No error has been found.') && violations.length === 0;
|
|
125
|
+
|
|
126
|
+
return { passed, violations, output, runtimeMs };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Format TLC feedback for the planner with truth mapping.
|
|
131
|
+
*
|
|
132
|
+
* @param {number} attempt - Current attempt number (1-based)
|
|
133
|
+
* @param {number} maxAttempts - Maximum attempts
|
|
134
|
+
* @param {{ passed: boolean, violations: string[], output: string, runtimeMs: number }} tlcResult
|
|
135
|
+
* @param {string[]} truthsList - Original truths list from PLAN.md
|
|
136
|
+
* @returns {string}
|
|
137
|
+
*/
|
|
138
|
+
function formatTlcFeedback(attempt, maxAttempts, tlcResult, truthsList) {
|
|
139
|
+
if (tlcResult.passed) {
|
|
140
|
+
return 'ATTEMPT ' + attempt + '/' + maxAttempts + ': TLC verification PASSED. All ' + truthsList.length + ' properties satisfied.';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let feedback = 'ATTEMPT ' + attempt + '/' + maxAttempts + ': TLC verification FAILED.\n';
|
|
144
|
+
|
|
145
|
+
for (const violation of tlcResult.violations) {
|
|
146
|
+
const reqMatch = violation.match(/Req(\d{2})/);
|
|
147
|
+
if (reqMatch) {
|
|
148
|
+
const truthIndex = parseInt(reqMatch[1], 10) - 1;
|
|
149
|
+
const truthText = truthIndex >= 0 && truthIndex < truthsList.length
|
|
150
|
+
? truthsList[truthIndex]
|
|
151
|
+
: '(unknown truth)';
|
|
152
|
+
const kindMatch = violation.match(/^(Invariant|Property)/);
|
|
153
|
+
const kind = kindMatch ? kindMatch[1].toUpperCase() : 'UNKNOWN';
|
|
154
|
+
feedback += 'Violated: Req' + reqMatch[1] + ' (' + kind + ') -- "' + truthText + '"\n';
|
|
155
|
+
} else {
|
|
156
|
+
feedback += violation + '\n';
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
feedback += 'Suggestion: Revise the truth statement or adjust the plan to satisfy this constraint.';
|
|
161
|
+
return feedback;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Perform a single TLC verification attempt on a PLAN.md file.
|
|
166
|
+
*
|
|
167
|
+
* @param {string} planFilePath - Path to the PLAN.md file
|
|
168
|
+
* @returns {{ status: string, reason?: string, violations?: string[], feedback?: string, truthCount?: number, specPath?: string, runtimeMs?: number }}
|
|
169
|
+
*/
|
|
170
|
+
function iterativeVerify(planFilePath) {
|
|
171
|
+
// Generate spec
|
|
172
|
+
const genResult = generateProposedChanges(planFilePath);
|
|
173
|
+
|
|
174
|
+
if (!genResult.generated) {
|
|
175
|
+
return { status: 'skipped', reason: 'no truths in plan' };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Generate TLC config
|
|
179
|
+
const { cfgPath } = generateTlaCfg(genResult.specPath);
|
|
180
|
+
|
|
181
|
+
// Run TLC
|
|
182
|
+
const tlcResult = runPhaseTlc(genResult.specPath, cfgPath);
|
|
183
|
+
|
|
184
|
+
if (tlcResult.passed) {
|
|
185
|
+
return {
|
|
186
|
+
status: 'passed',
|
|
187
|
+
truthCount: genResult.truthCount,
|
|
188
|
+
specPath: genResult.specPath,
|
|
189
|
+
runtimeMs: tlcResult.runtimeMs,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Build truths list for feedback
|
|
194
|
+
const truths = genResult.classifications.map(c => c.truth);
|
|
195
|
+
const feedback = formatTlcFeedback(1, 3, tlcResult, truths);
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
status: 'failed',
|
|
199
|
+
violations: tlcResult.violations,
|
|
200
|
+
feedback,
|
|
201
|
+
specPath: genResult.specPath,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── CLI entrypoint ────────────────────────────────────────────────────────────
|
|
206
|
+
if (require.main === module) {
|
|
207
|
+
const args = process.argv.slice(2).filter(a => !a.startsWith('--'));
|
|
208
|
+
|
|
209
|
+
if (args.length === 0) {
|
|
210
|
+
process.stderr.write('[run-phase-tlc] Usage: node bin/run-phase-tlc.cjs <path-to-PLAN.md>\n');
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const planFilePath = path.resolve(args[0]);
|
|
215
|
+
if (!fs.existsSync(planFilePath)) {
|
|
216
|
+
process.stderr.write('[run-phase-tlc] Error: file not found: ' + planFilePath + '\n');
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const result = iterativeVerify(planFilePath);
|
|
221
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
222
|
+
|
|
223
|
+
if (result.status === 'failed') {
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
module.exports = { runPhaseTlc, iterativeVerify, formatTlcFeedback };
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// bin/run-prism.cjs
|
|
4
|
+
// Invokes PRISM model checker against .planning/formal/prism/quorum.pm.
|
|
5
|
+
// Requirements: PRM-01
|
|
6
|
+
//
|
|
7
|
+
// Usage:
|
|
8
|
+
// node bin/run-prism.cjs # default: P=? [ F s=1 ]
|
|
9
|
+
// node bin/run-prism.cjs -pf "P=? [ F s=1 ]" # explicit property
|
|
10
|
+
// node bin/run-prism.cjs -const tp_rate=0.9274 -const unavail=0.0215
|
|
11
|
+
//
|
|
12
|
+
// Prerequisites:
|
|
13
|
+
// - PRISM 4.x installed; set PRISM_BIN to path of the prism shell script
|
|
14
|
+
// e.g. export PRISM_BIN="$HOME/prism/bin/prism"
|
|
15
|
+
// - Java >=17 (same JRE used by TLA+/Alloy CI step)
|
|
16
|
+
//
|
|
17
|
+
// CI: PRISM_BIN is set by the formal-verify workflow step that extracts the
|
|
18
|
+
// Linux binary tarball.
|
|
19
|
+
|
|
20
|
+
const { spawnSync } = require('child_process');
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const { writeCheckResult } = require('./write-check-result.cjs');
|
|
24
|
+
const { readPolicy } = require('./read-policy.cjs');
|
|
25
|
+
const { getRequirementIds } = require('./requirement-map.cjs');
|
|
26
|
+
|
|
27
|
+
// ── Check ID mapping for multi-model support ─────────────────────────────────
|
|
28
|
+
const CHECK_ID_MAP = {
|
|
29
|
+
'quorum': 'prism:quorum',
|
|
30
|
+
'mcp-availability': 'prism:mcp-availability',
|
|
31
|
+
};
|
|
32
|
+
const PROPERTY_MAP = {
|
|
33
|
+
'quorum': 'Quorum consensus probability under agent availability rates',
|
|
34
|
+
'mcp-availability': 'MCP server availability under nondeterministic failure modes',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// ── Locate PRISM binary ──────────────────────────────────────────────────────
|
|
38
|
+
const prismBin = process.env.PRISM_BIN || 'prism';
|
|
39
|
+
|
|
40
|
+
// Verify the binary exists (skip check if it's just 'prism' on PATH)
|
|
41
|
+
if (prismBin !== 'prism' && !fs.existsSync(prismBin)) {
|
|
42
|
+
process.stderr.write(
|
|
43
|
+
'[run-prism] PRISM binary not found at: ' + prismBin + '\n' +
|
|
44
|
+
'[run-prism] Install PRISM and set PRISM_BIN env var:\n' +
|
|
45
|
+
'[run-prism] export PRISM_BIN="$HOME/prism/bin/prism"\n' +
|
|
46
|
+
'[run-prism] Download: https://www.prismmodelchecker.org/download.php\n'
|
|
47
|
+
);
|
|
48
|
+
try {
|
|
49
|
+
writeCheckResult({
|
|
50
|
+
tool: 'run-prism', formalism: 'prism', result: 'fail',
|
|
51
|
+
check_id: 'prism:quorum', surface: 'prism', property: 'Quorum consensus probability under agent availability rates',
|
|
52
|
+
runtime_ms: 0, summary: 'fail: prism:quorum (binary not found)', triage_tags: [],
|
|
53
|
+
requirement_ids: getRequirementIds('prism:quorum'),
|
|
54
|
+
observation_window: { window_start: new Date().toISOString(), window_end: new Date().toISOString(), n_traces: 0, n_events: 0, window_days: 0 },
|
|
55
|
+
metadata: {}
|
|
56
|
+
});
|
|
57
|
+
} catch (e) { process.stderr.write('[run-prism] Warning: failed to write check result: ' + e.message + '\n'); }
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Locate model file ────────────────────────────────────────────────────────
|
|
62
|
+
const modelPath = path.join(__dirname, '..', '.planning', 'formal', 'prism', 'quorum.pm');
|
|
63
|
+
if (!fs.existsSync(modelPath)) {
|
|
64
|
+
process.stderr.write(
|
|
65
|
+
'[run-prism] Model file not found: ' + modelPath + '\n'
|
|
66
|
+
);
|
|
67
|
+
try {
|
|
68
|
+
writeCheckResult({
|
|
69
|
+
tool: 'run-prism', formalism: 'prism', result: 'fail',
|
|
70
|
+
check_id: 'prism:quorum', surface: 'prism', property: 'Quorum consensus probability under agent availability rates',
|
|
71
|
+
runtime_ms: 0, summary: 'fail: prism:quorum (model not found)', triage_tags: [],
|
|
72
|
+
requirement_ids: getRequirementIds('prism:quorum'),
|
|
73
|
+
observation_window: { window_start: new Date().toISOString(), window_end: new Date().toISOString(), n_traces: 0, n_events: 0, window_days: 0 },
|
|
74
|
+
metadata: {}
|
|
75
|
+
});
|
|
76
|
+
} catch (e) { process.stderr.write('[run-prism] Warning: failed to write check result: ' + e.message + '\n'); }
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── readMCPAvailabilityRates (MCPENV-04) ─────────────────────────────────────
|
|
81
|
+
// Reads quorum-scoreboard.json and computes per-slot availability rates.
|
|
82
|
+
// Returns { 'slot-name': availabilityRate, ... } or null if no data.
|
|
83
|
+
// Rate = 1.0 - (unavail_count / total_count) per slot, excluding 'claude' (self).
|
|
84
|
+
// Exported for tests.
|
|
85
|
+
function readMCPAvailabilityRates(sbPath) {
|
|
86
|
+
let p = sbPath;
|
|
87
|
+
if (!p) {
|
|
88
|
+
try {
|
|
89
|
+
const pp = require('./planning-paths.cjs');
|
|
90
|
+
p = pp.resolveWithFallback(process.cwd(), 'quorum-scoreboard');
|
|
91
|
+
} catch (_) {
|
|
92
|
+
p = path.join(process.cwd(), '.planning', 'quorum-scoreboard.json');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
97
|
+
const sb = JSON.parse(raw);
|
|
98
|
+
const rounds = Array.isArray(sb.rounds) ? sb.rounds : [];
|
|
99
|
+
if (rounds.length === 0) return null;
|
|
100
|
+
|
|
101
|
+
const slotStats = {};
|
|
102
|
+
for (const round of rounds) {
|
|
103
|
+
const votes = round.votes || {};
|
|
104
|
+
for (const [slot, code] of Object.entries(votes)) {
|
|
105
|
+
if (slot === 'claude') continue; // exclude self
|
|
106
|
+
// FILTER FIRST — inside readMCPAvailabilityRates, before building the rates object.
|
|
107
|
+
// Composite keys (e.g. 'claude-1:deepseek-ai/DeepSeek-V3.2') contain ':' or '/'
|
|
108
|
+
// which are illegal PRISM identifier characters. Filter them out here so the returned
|
|
109
|
+
// rates object contains only base keys — making the function directly testable with
|
|
110
|
+
// realistic scoreboards that include composite keys.
|
|
111
|
+
if (slot.includes(':') || slot.includes('/')) {
|
|
112
|
+
process.stderr.write('[run-prism] Skipping composite key (invalid PRISM identifier): ' + slot + '\n');
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (!slotStats[slot]) slotStats[slot] = { total: 0, unavail: 0 };
|
|
116
|
+
slotStats[slot].total++;
|
|
117
|
+
if (code === 'UNAVAIL') slotStats[slot].unavail++;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const rates = {};
|
|
122
|
+
for (const [slot, stats] of Object.entries(slotStats)) {
|
|
123
|
+
if (stats.total === 0) continue;
|
|
124
|
+
rates[slot] = Math.round((1.0 - stats.unavail / stats.total) * 1e6) / 1e6;
|
|
125
|
+
}
|
|
126
|
+
return Object.keys(rates).length > 0 ? rates : null;
|
|
127
|
+
} catch (_) {
|
|
128
|
+
return null; // missing or malformed scoreboard — caller uses priors
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Read scoreboard for empirical tp_rate / unavail injection (PRISM-02) ────
|
|
133
|
+
// Uses process.cwd()/.planning/quorum-scoreboard.json so tests can point to
|
|
134
|
+
// a fixture by spawning with a custom cwd (same pattern as run-formal-verify).
|
|
135
|
+
let liveTPRate = null;
|
|
136
|
+
let liveUnavail = null;
|
|
137
|
+
let scoreboardPath;
|
|
138
|
+
try {
|
|
139
|
+
const pp = require('./planning-paths.cjs');
|
|
140
|
+
scoreboardPath = pp.resolveWithFallback(process.cwd(), 'quorum-scoreboard');
|
|
141
|
+
} catch (_) {
|
|
142
|
+
scoreboardPath = path.join(process.cwd(), '.planning', 'quorum-scoreboard.json');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── Load calibration policy ───────────────────────────────────────────────
|
|
146
|
+
const policyPath = path.join(__dirname, '..', '.planning', 'formal', 'policy.yaml');
|
|
147
|
+
let policy;
|
|
148
|
+
try {
|
|
149
|
+
policy = readPolicy(policyPath);
|
|
150
|
+
} catch (e) {
|
|
151
|
+
process.stderr.write('[run-prism] Failed to load policy.yaml: ' + e.message + '\n');
|
|
152
|
+
try {
|
|
153
|
+
writeCheckResult({
|
|
154
|
+
tool: 'run-prism', formalism: 'prism', result: 'fail',
|
|
155
|
+
check_id: 'prism:quorum', surface: 'prism', property: 'Quorum consensus probability under agent availability rates',
|
|
156
|
+
runtime_ms: 0, summary: 'fail: prism:quorum (policy load failed)', triage_tags: [],
|
|
157
|
+
requirement_ids: getRequirementIds('prism:quorum'),
|
|
158
|
+
observation_window: { window_start: new Date().toISOString(), window_end: new Date().toISOString(), n_traces: 0, n_events: 0, window_days: 0 },
|
|
159
|
+
metadata: {}
|
|
160
|
+
});
|
|
161
|
+
} catch (_) {}
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── LOOP-01: export-prism-constants pre-step ─────────────────────────────────
|
|
166
|
+
// Ensures rates.const is always current from the scoreboard before PRISM runs.
|
|
167
|
+
// Fail-open: if export fails, run-prism continues with whatever rates.const exists.
|
|
168
|
+
{
|
|
169
|
+
const exportConstantsPath = path.join(__dirname, 'export-prism-constants.cjs');
|
|
170
|
+
const exportResult = spawnSync(process.execPath, [exportConstantsPath], {
|
|
171
|
+
encoding: 'utf8',
|
|
172
|
+
cwd: process.cwd(),
|
|
173
|
+
timeout: 10000,
|
|
174
|
+
});
|
|
175
|
+
if (exportResult.status !== 0 || exportResult.error) {
|
|
176
|
+
process.stderr.write(
|
|
177
|
+
'[run-prism] Warning: export-prism-constants pre-step failed — rates.const may be stale.\n' +
|
|
178
|
+
(exportResult.stderr || '') + '\n'
|
|
179
|
+
);
|
|
180
|
+
} else {
|
|
181
|
+
process.stdout.write('[run-prism] Pre-step: rates.const updated from scoreboard.\n');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
if (fs.existsSync(scoreboardPath)) {
|
|
187
|
+
try {
|
|
188
|
+
const sb = JSON.parse(fs.readFileSync(scoreboardPath, 'utf8'));
|
|
189
|
+
const rounds = Array.isArray(sb.rounds) ? sb.rounds : [];
|
|
190
|
+
// Aggregate TP and UNAVAIL counts across all slots (excluding 'claude')
|
|
191
|
+
let totalVotes = 0;
|
|
192
|
+
let tpCount = 0;
|
|
193
|
+
let unavailCount = 0;
|
|
194
|
+
for (const round of rounds) {
|
|
195
|
+
const votes = round.votes || {};
|
|
196
|
+
for (const [slot, code] of Object.entries(votes)) {
|
|
197
|
+
if (slot === 'claude') continue; // exclude self
|
|
198
|
+
totalVotes++;
|
|
199
|
+
if (code === 'TP' || code === 'TP+' || code === 'TN' || code === 'TN+') tpCount++;
|
|
200
|
+
if (code === 'UNAVAIL') unavailCount++;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (totalVotes > 0) {
|
|
204
|
+
liveTPRate = Math.round((tpCount / totalVotes) * 1e6) / 1e6;
|
|
205
|
+
liveUnavail = Math.round((unavailCount / totalVotes) * 1e6) / 1e6;
|
|
206
|
+
process.stdout.write(
|
|
207
|
+
'[run-prism] Injected from scoreboard: tp_rate=' + liveTPRate +
|
|
208
|
+
' unavail=' + liveUnavail + ' (' + rounds.length + ' rounds)\n'
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
} catch (_) { /* malformed scoreboard — fall through to priors */ }
|
|
212
|
+
}
|
|
213
|
+
if (liveTPRate === null) {
|
|
214
|
+
liveTPRate = policy.conservative_priors.tp_rate;
|
|
215
|
+
liveUnavail = policy.conservative_priors.unavail;
|
|
216
|
+
process.stderr.write(
|
|
217
|
+
'[run-prism] No scoreboard found — using conservative priors: ' +
|
|
218
|
+
'tp_rate=' + policy.conservative_priors.tp_rate + ' unavail=' + policy.conservative_priors.unavail + '\n'
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── Cold-start state detection (CALIB-02, CALIB-03) ─────────────────────
|
|
223
|
+
function computeColdStartState(pol, sbPath, crPath) {
|
|
224
|
+
let ciRunCount = 0;
|
|
225
|
+
let quorumRoundCount = 0;
|
|
226
|
+
let firstRunTimestamp = null;
|
|
227
|
+
|
|
228
|
+
// Count CI runs: number of lines in check-results.ndjson
|
|
229
|
+
if (fs.existsSync(crPath)) {
|
|
230
|
+
const lines = fs.readFileSync(crPath, 'utf8')
|
|
231
|
+
.trim().split('\n').filter(l => l.length > 0);
|
|
232
|
+
ciRunCount = lines.length;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Read quorum rounds from scoreboard
|
|
236
|
+
if (fs.existsSync(sbPath)) {
|
|
237
|
+
try {
|
|
238
|
+
const sb = JSON.parse(fs.readFileSync(sbPath, 'utf8'));
|
|
239
|
+
const rounds = Array.isArray(sb.rounds) ? sb.rounds : [];
|
|
240
|
+
quorumRoundCount = rounds.length;
|
|
241
|
+
if (rounds.length > 0) {
|
|
242
|
+
const firstRound = rounds[0];
|
|
243
|
+
// Support timestamp (ISO) or date (MM-DD) field
|
|
244
|
+
const raw = firstRound.timestamp || firstRound.date;
|
|
245
|
+
if (raw) {
|
|
246
|
+
const parsed = Date.parse(raw);
|
|
247
|
+
if (!isNaN(parsed)) firstRunTimestamp = parsed;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
} catch (e) {
|
|
251
|
+
process.stderr.write('[run-prism] Warning: failed to parse scoreboard for cold-start: ' + e.message + '\n');
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Compute days since first run (0 if no history)
|
|
256
|
+
const daysSinceFirst = firstRunTimestamp
|
|
257
|
+
? (Date.now() - firstRunTimestamp) / (1000 * 60 * 60 * 24)
|
|
258
|
+
: 0;
|
|
259
|
+
|
|
260
|
+
// Cold-start is true if ANY threshold is unmet
|
|
261
|
+
const allThresholdsMet =
|
|
262
|
+
ciRunCount >= pol.cold_start.min_ci_runs &&
|
|
263
|
+
quorumRoundCount >= pol.cold_start.min_quorum_rounds &&
|
|
264
|
+
daysSinceFirst >= pol.cold_start.min_days;
|
|
265
|
+
const inColdStart = !allThresholdsMet;
|
|
266
|
+
|
|
267
|
+
return { inColdStart, ciRunCount, quorumRoundCount, daysSinceFirst, firstRunTimestamp };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const checkResultsPath = path.join(process.cwd(), '.planning', 'formal', 'check-results.ndjson');
|
|
271
|
+
const coldStartState = computeColdStartState(policy, scoreboardPath, checkResultsPath);
|
|
272
|
+
if (coldStartState.inColdStart) {
|
|
273
|
+
process.stderr.write(
|
|
274
|
+
'[run-prism] Cold-start mode active (thresholds not yet met):\n' +
|
|
275
|
+
'[run-prism] CI runs: ' + coldStartState.ciRunCount + ' / ' + policy.cold_start.min_ci_runs + '\n' +
|
|
276
|
+
'[run-prism] Quorum rounds: ' + coldStartState.quorumRoundCount + ' / ' + policy.cold_start.min_quorum_rounds + '\n' +
|
|
277
|
+
'[run-prism] Days: ' + coldStartState.daysSinceFirst.toFixed(2) + ' / ' + policy.cold_start.min_days + '\n'
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ── Build argument list ──────────────────────────────────────────────────────
|
|
282
|
+
// Extra args passed to this script are forwarded to PRISM after the model path.
|
|
283
|
+
// If .planning/formal/prism/quorum.props exists, pass it as the properties file (runs all 4 properties).
|
|
284
|
+
// Otherwise fall back to: -pf "P=? [ F s=1 ]"
|
|
285
|
+
const extraArgs = process.argv.slice(2);
|
|
286
|
+
|
|
287
|
+
// ── MCPENV-04: --model mcp-availability flag ─────────────────────────────────
|
|
288
|
+
// When --model mcp-availability is passed, run mcp-availability.pm instead of quorum.pm.
|
|
289
|
+
// Injects per-slot availability rates from scoreboard as -const flags.
|
|
290
|
+
const modelArgIdx = extraArgs.indexOf('--model');
|
|
291
|
+
const modelArgValue = modelArgIdx >= 0 ? extraArgs[modelArgIdx + 1] : null;
|
|
292
|
+
const useMCPAvailabilityModel = modelArgValue === 'mcp-availability';
|
|
293
|
+
|
|
294
|
+
// Strip --model <value> from extraArgs before forwarding to PRISM
|
|
295
|
+
const filteredExtraArgs = useMCPAvailabilityModel
|
|
296
|
+
? extraArgs.filter((a, i) => i !== modelArgIdx && i !== modelArgIdx + 1)
|
|
297
|
+
: extraArgs;
|
|
298
|
+
|
|
299
|
+
let activeModelPath = modelPath; // default: quorum.pm
|
|
300
|
+
let activeMcpRates = null; // per-slot rates if mcp-availability model
|
|
301
|
+
|
|
302
|
+
if (useMCPAvailabilityModel) {
|
|
303
|
+
const mcpModelPath = path.join(__dirname, '..', '.planning', 'formal', 'prism', 'mcp-availability.pm');
|
|
304
|
+
if (!fs.existsSync(mcpModelPath)) {
|
|
305
|
+
process.stderr.write('[run-prism] mcp-availability.pm not found at: ' + mcpModelPath + '\n');
|
|
306
|
+
process.exit(1);
|
|
307
|
+
}
|
|
308
|
+
activeModelPath = mcpModelPath;
|
|
309
|
+
activeMcpRates = readMCPAvailabilityRates();
|
|
310
|
+
if (activeMcpRates) {
|
|
311
|
+
process.stdout.write('[run-prism] MCP rates from scoreboard: ' + JSON.stringify(activeMcpRates) + '\n');
|
|
312
|
+
} else {
|
|
313
|
+
process.stderr.write('[run-prism] No scoreboard rates for mcp-availability — using priors (0.85 per slot)\n');
|
|
314
|
+
}
|
|
315
|
+
process.stdout.write('[run-prism] Model: mcp-availability\n');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const hasPf = filteredExtraArgs.some(a => a === '-pf' || a === '-prop');
|
|
319
|
+
const propsFile = path.join(__dirname, '..', '.planning', 'formal', 'prism', useMCPAvailabilityModel ? 'mcp-availability.props' : 'quorum.props');
|
|
320
|
+
const hasProps = !hasPf && fs.existsSync(propsFile);
|
|
321
|
+
|
|
322
|
+
// Determine if caller already overrides tp_rate or unavail
|
|
323
|
+
const callerOverridesTP = filteredExtraArgs.some((a, i) => a === '-const' && (filteredExtraArgs[i + 1] || '').startsWith('tp_rate='));
|
|
324
|
+
const callerOverridesUnavail = filteredExtraArgs.some((a, i) => a === '-const' && (filteredExtraArgs[i + 1] || '').startsWith('unavail='));
|
|
325
|
+
|
|
326
|
+
const prismArgs = [activeModelPath];
|
|
327
|
+
if (hasProps) {
|
|
328
|
+
prismArgs.push(propsFile);
|
|
329
|
+
} else if (!hasPf) {
|
|
330
|
+
prismArgs.push('-pf', 'P=? [ F s=1 ]');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (useMCPAvailabilityModel) {
|
|
334
|
+
// Inject per-slot rates as -const flags (slot name: 'codex-1' → 'codex_1_avail')
|
|
335
|
+
if (activeMcpRates) {
|
|
336
|
+
for (const [slot, rate] of Object.entries(activeMcpRates)) {
|
|
337
|
+
const constName = slot.replace(/-/g, '_') + '_avail';
|
|
338
|
+
prismArgs.push('-const', constName + '=' + rate);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// No tp_rate/unavail injection for mcp-availability model
|
|
342
|
+
} else {
|
|
343
|
+
// Inject empirical/prior rates unless caller overrides (quorum.pm path)
|
|
344
|
+
if (!callerOverridesTP) {
|
|
345
|
+
prismArgs.push('-const', 'tp_rate=' + liveTPRate);
|
|
346
|
+
}
|
|
347
|
+
if (!callerOverridesUnavail) {
|
|
348
|
+
prismArgs.push('-const', 'unavail=' + liveUnavail);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
prismArgs.push(...filteredExtraArgs);
|
|
352
|
+
|
|
353
|
+
process.stdout.write('[run-prism] Binary: ' + prismBin + '\n');
|
|
354
|
+
process.stdout.write('[run-prism] Model: ' + activeModelPath + '\n');
|
|
355
|
+
process.stdout.write('[run-prism] Args: ' + prismArgs.slice(1).join(' ') + '\n');
|
|
356
|
+
|
|
357
|
+
// ── Invoke PRISM ─────────────────────────────────────────────────────────────
|
|
358
|
+
const _startMs = Date.now();
|
|
359
|
+
|
|
360
|
+
const result = spawnSync(prismBin, prismArgs, {
|
|
361
|
+
encoding: 'utf8',
|
|
362
|
+
stdio: 'inherit',
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
if (result.error) {
|
|
366
|
+
process.stderr.write('[run-prism] Failed to launch PRISM: ' + result.error.message + '\n');
|
|
367
|
+
const _runtimeMs = Date.now() - _startMs;
|
|
368
|
+
const modelName = useMCPAvailabilityModel ? 'mcp-availability' : 'quorum';
|
|
369
|
+
const check_id = CHECK_ID_MAP[modelName];
|
|
370
|
+
try {
|
|
371
|
+
writeCheckResult({
|
|
372
|
+
tool: 'run-prism', formalism: 'prism', result: 'fail',
|
|
373
|
+
check_id: check_id, surface: 'prism', property: PROPERTY_MAP[modelName],
|
|
374
|
+
runtime_ms: _runtimeMs, summary: 'fail: ' + check_id + ' in ' + _runtimeMs + 'ms', triage_tags: [],
|
|
375
|
+
requirement_ids: getRequirementIds(check_id),
|
|
376
|
+
observation_window: { window_start: new Date().toISOString(), window_end: new Date().toISOString(), n_traces: 0, n_events: 0, window_days: 0 },
|
|
377
|
+
metadata: {}
|
|
378
|
+
});
|
|
379
|
+
} catch (e) { process.stderr.write('[run-prism] Warning: failed to write check result: ' + e.message + '\n'); }
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const passed = (result.status || 0) === 0;
|
|
384
|
+
|
|
385
|
+
// Apply cold-start override: never emit result=fail during cold-start (CALIB-02)
|
|
386
|
+
let finalResult = passed ? 'pass' : 'fail';
|
|
387
|
+
if (!passed && coldStartState.inColdStart) {
|
|
388
|
+
finalResult = 'warn';
|
|
389
|
+
process.stderr.write('[run-prism] Cold-start mode: suppressing fail → emitting warn\n');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Build observation_window as top-level field for v2.1 (CALIB-03)
|
|
393
|
+
const _runtimeMs = Date.now() - _startMs;
|
|
394
|
+
const modelName = useMCPAvailabilityModel ? 'mcp-availability' : 'quorum';
|
|
395
|
+
const check_id = CHECK_ID_MAP[modelName];
|
|
396
|
+
const property = PROPERTY_MAP[modelName];
|
|
397
|
+
|
|
398
|
+
const observationWindow = {
|
|
399
|
+
window_start: coldStartState.firstRunTimestamp
|
|
400
|
+
? new Date(coldStartState.firstRunTimestamp).toISOString()
|
|
401
|
+
: new Date().toISOString(),
|
|
402
|
+
window_end: new Date().toISOString(),
|
|
403
|
+
n_traces: coldStartState.quorumRoundCount,
|
|
404
|
+
n_events: coldStartState.ciRunCount,
|
|
405
|
+
window_days: coldStartState.firstRunTimestamp ? (Date.now() - coldStartState.firstRunTimestamp) / (1000 * 60 * 60 * 24) : 0,
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// Build triage_tags based on PRISM thresholds
|
|
409
|
+
const tags = [];
|
|
410
|
+
if (_runtimeMs > 300000) tags.push('timeout-risk');
|
|
411
|
+
else if (_runtimeMs > 120000) tags.push('slow-verify');
|
|
412
|
+
if (observationWindow.window_days < 7 || observationWindow.n_traces < 30) tags.push('low-confidence');
|
|
413
|
+
|
|
414
|
+
// Prepare metadata for non-observation_window fields
|
|
415
|
+
const metadata = {};
|
|
416
|
+
if (useMCPAvailabilityModel) {
|
|
417
|
+
metadata.model = 'mcp-availability';
|
|
418
|
+
metadata.per_slot_rates = activeMcpRates || 'priors';
|
|
419
|
+
}
|
|
420
|
+
metadata.tp_rate = liveTPRate;
|
|
421
|
+
metadata.unavail = liveUnavail;
|
|
422
|
+
|
|
423
|
+
try {
|
|
424
|
+
writeCheckResult({
|
|
425
|
+
tool: 'run-prism',
|
|
426
|
+
formalism: 'prism',
|
|
427
|
+
result: finalResult,
|
|
428
|
+
check_id: check_id,
|
|
429
|
+
surface: 'prism',
|
|
430
|
+
property: property,
|
|
431
|
+
runtime_ms: _runtimeMs,
|
|
432
|
+
summary: finalResult + ': ' + modelName + ' in ' + _runtimeMs + 'ms',
|
|
433
|
+
triage_tags: tags,
|
|
434
|
+
requirement_ids: getRequirementIds(check_id),
|
|
435
|
+
observation_window: observationWindow,
|
|
436
|
+
metadata: metadata,
|
|
437
|
+
});
|
|
438
|
+
} catch (e) {
|
|
439
|
+
process.stderr.write('[run-prism] Warning: failed to write check result: ' + e.message + '\n');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (require.main === module) {
|
|
443
|
+
process.exit(passed ? 0 : (finalResult === 'warn' ? 0 : 1));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
module.exports = { readMCPAvailabilityRates };
|