@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,188 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// bin/run-stop-hook-tlc.cjs
|
|
4
|
+
// Invokes TLC model checker for the QGSD Stop hook TLA+ specification.
|
|
5
|
+
// Requirements: SPEC-01
|
|
6
|
+
//
|
|
7
|
+
// Usage:
|
|
8
|
+
// node bin/run-stop-hook-tlc.cjs MCStopHook # Stop hook decision logic (liveness, -workers 1)
|
|
9
|
+
// node bin/run-stop-hook-tlc.cjs --config=MCStopHook
|
|
10
|
+
//
|
|
11
|
+
// Prerequisites:
|
|
12
|
+
// - Java >=17 (https://adoptium.net/)
|
|
13
|
+
// - .planning/formal/tla/tla2tools.jar (see .planning/formal/tla/README.md for download command)
|
|
14
|
+
|
|
15
|
+
const { spawnSync } = require('child_process');
|
|
16
|
+
const JAVA_HEAP_MAX = process.env.QGSD_JAVA_HEAP_MAX || '512m';
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const { writeCheckResult } = require('./write-check-result.cjs');
|
|
20
|
+
const { detectLivenessProperties } = require('./run-tlc.cjs');
|
|
21
|
+
const { getRequirementIds } = require('./requirement-map.cjs');
|
|
22
|
+
|
|
23
|
+
// ── Resolve project root (--project-root= overrides __dirname-relative) ─────
|
|
24
|
+
let ROOT = path.join(__dirname, '..');
|
|
25
|
+
for (const arg of process.argv) {
|
|
26
|
+
if (arg.startsWith('--project-root=')) ROOT = path.resolve(arg.slice('--project-root='.length));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const CHECK_ID_MAP = {
|
|
30
|
+
'MCStopHook': 'tla:stop-hook',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const PROPERTY_MAP = {
|
|
34
|
+
'MCStopHook': 'Stop hook safety (BLOCK => hasCommand) + liveness (hasQuorumEvidence => <>PASS)',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// ── Parse --config argument ──────────────────────────────────────────────────
|
|
38
|
+
const args = process.argv.slice(2);
|
|
39
|
+
const configArg = args.find(a => a.startsWith('--config=')) || null;
|
|
40
|
+
const configName = configArg
|
|
41
|
+
? configArg.split('=')[1]
|
|
42
|
+
: (args.find(a => !a.startsWith('-')) || 'MCStopHook');
|
|
43
|
+
|
|
44
|
+
const VALID_CONFIGS = ['MCStopHook'];
|
|
45
|
+
if (!VALID_CONFIGS.includes(configName)) {
|
|
46
|
+
process.stderr.write(
|
|
47
|
+
'[run-stop-hook-tlc] Unknown config: ' + configName +
|
|
48
|
+
'. Valid: ' + VALID_CONFIGS.join(', ') + '\n'
|
|
49
|
+
);
|
|
50
|
+
const _runtimeMs = 0;
|
|
51
|
+
try { writeCheckResult({ tool: 'run-stop-hook-tlc', formalism: 'tla', result: 'fail', check_id: CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase()), surface: 'tla', property: PROPERTY_MAP[configName] || configName, runtime_ms: _runtimeMs, summary: 'fail: unknown config in ' + _runtimeMs + 'ms', triage_tags: [], requirement_ids: getRequirementIds(CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase())), metadata: { config: configName } }); } catch (e) { process.stderr.write('[run-stop-hook-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── 1. Locate Java ───────────────────────────────────────────────────────────
|
|
56
|
+
const JAVA_HOME = process.env.JAVA_HOME;
|
|
57
|
+
let javaExe;
|
|
58
|
+
|
|
59
|
+
if (JAVA_HOME) {
|
|
60
|
+
javaExe = path.join(JAVA_HOME, 'bin', 'java');
|
|
61
|
+
if (!fs.existsSync(javaExe)) {
|
|
62
|
+
process.stderr.write(
|
|
63
|
+
'[run-stop-hook-tlc] JAVA_HOME is set but java binary not found at: ' + javaExe + '\n' +
|
|
64
|
+
'[run-stop-hook-tlc] Unset JAVA_HOME or fix the path.\n'
|
|
65
|
+
);
|
|
66
|
+
const _runtimeMs = 0;
|
|
67
|
+
try { writeCheckResult({ tool: 'run-stop-hook-tlc', formalism: 'tla', result: 'fail', check_id: CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase()), surface: 'tla', property: PROPERTY_MAP[configName] || configName, runtime_ms: _runtimeMs, summary: 'fail: Java not found in ' + _runtimeMs + 'ms', triage_tags: [], requirement_ids: getRequirementIds(CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase())), metadata: { config: configName } }); } catch (e) { process.stderr.write('[run-stop-hook-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
// Fall back to PATH lookup
|
|
72
|
+
const probe = spawnSync('java', ['--version'], { encoding: 'utf8' });
|
|
73
|
+
if (probe.error || probe.status !== 0) {
|
|
74
|
+
process.stderr.write(
|
|
75
|
+
'[run-stop-hook-tlc] Java not found. Install Java >=17 and set JAVA_HOME.\n' +
|
|
76
|
+
'[run-stop-hook-tlc] Download: https://adoptium.net/\n'
|
|
77
|
+
);
|
|
78
|
+
const _runtimeMs = 0;
|
|
79
|
+
try { writeCheckResult({ tool: 'run-stop-hook-tlc', formalism: 'tla', result: 'fail', check_id: CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase()), surface: 'tla', property: PROPERTY_MAP[configName] || configName, runtime_ms: _runtimeMs, summary: 'fail: Java not found in ' + _runtimeMs + 'ms', triage_tags: [], requirement_ids: getRequirementIds(CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase())), metadata: { config: configName } }); } catch (e) { process.stderr.write('[run-stop-hook-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
javaExe = 'java';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── 2. Check Java version >=17 ───────────────────────────────────────────────
|
|
86
|
+
const versionResult = spawnSync(javaExe, ['--version'], { encoding: 'utf8' });
|
|
87
|
+
if (versionResult.error || versionResult.status !== 0) {
|
|
88
|
+
process.stderr.write('[run-stop-hook-tlc] Failed to run: ' + javaExe + ' --version\n');
|
|
89
|
+
const _runtimeMs = 0;
|
|
90
|
+
try { writeCheckResult({ tool: 'run-stop-hook-tlc', formalism: 'tla', result: 'fail', check_id: CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase()), surface: 'tla', property: PROPERTY_MAP[configName] || configName, runtime_ms: _runtimeMs, summary: 'fail: Java version check failed in ' + _runtimeMs + 'ms', triage_tags: [], requirement_ids: getRequirementIds(CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase())), metadata: { config: configName } }); } catch (e) { process.stderr.write('[run-stop-hook-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
const versionOutput = versionResult.stdout + versionResult.stderr;
|
|
94
|
+
const versionMatch = versionOutput.match(/(?:openjdk\s+|java version\s+[""]?)(\d+)/i);
|
|
95
|
+
const javaMajor = versionMatch ? parseInt(versionMatch[1], 10) : 0;
|
|
96
|
+
if (javaMajor < 17) {
|
|
97
|
+
process.stderr.write(
|
|
98
|
+
'[run-stop-hook-tlc] Java >=17 required. Found: ' + versionOutput.split('\n')[0] + '\n' +
|
|
99
|
+
'[run-stop-hook-tlc] Download Java 17+: https://adoptium.net/\n'
|
|
100
|
+
);
|
|
101
|
+
const _runtimeMs = 0;
|
|
102
|
+
try { writeCheckResult({ tool: 'run-stop-hook-tlc', formalism: 'tla', result: 'fail', check_id: CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase()), surface: 'tla', property: PROPERTY_MAP[configName] || configName, runtime_ms: _runtimeMs, summary: 'fail: Java ' + javaMajor + ' < 17 in ' + _runtimeMs + 'ms', triage_tags: [], requirement_ids: getRequirementIds(CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase())), metadata: { config: configName } }); } catch (e) { process.stderr.write('[run-stop-hook-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── 3. Locate tla2tools.jar ──────────────────────────────────────────────────
|
|
107
|
+
const jarPath = path.join(ROOT, '.planning', 'formal', 'tla', 'tla2tools.jar');
|
|
108
|
+
if (!fs.existsSync(jarPath)) {
|
|
109
|
+
process.stderr.write(
|
|
110
|
+
'[run-stop-hook-tlc] tla2tools.jar not found at: ' + jarPath + '\n' +
|
|
111
|
+
'[run-stop-hook-tlc] Download v1.8.0:\n' +
|
|
112
|
+
' curl -L https://github.com/tlaplus/tlaplus/releases/download/v1.8.0/tla2tools.jar \\\n' +
|
|
113
|
+
' -o .planning/formal/tla/tla2tools.jar\n'
|
|
114
|
+
);
|
|
115
|
+
const _runtimeMs = 0;
|
|
116
|
+
try { writeCheckResult({ tool: 'run-stop-hook-tlc', formalism: 'tla', result: 'fail', check_id: CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase()), surface: 'tla', property: PROPERTY_MAP[configName] || configName, runtime_ms: _runtimeMs, summary: 'fail: tla2tools.jar not found in ' + _runtimeMs + 'ms', triage_tags: [], requirement_ids: getRequirementIds(CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase())), metadata: { config: configName } }); } catch (e) { process.stderr.write('[run-stop-hook-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── 4. Resolve spec and config paths ─────────────────────────────────────────
|
|
121
|
+
const specPath = path.join(ROOT, '.planning', 'formal', 'tla', 'QGSDStopHook.tla');
|
|
122
|
+
const cfgPath = path.join(ROOT, '.planning', 'formal', 'tla', configName + '.cfg');
|
|
123
|
+
|
|
124
|
+
// LivenessProperty1/2/3 requires -workers 1 (TLC multi-worker liveness bug in v1.8.0)
|
|
125
|
+
const workers = '1';
|
|
126
|
+
|
|
127
|
+
// ── 5. Invoke TLC ────────────────────────────────────────────────────────────
|
|
128
|
+
process.stdout.write('[run-stop-hook-tlc] Config: ' + configName + ' Workers: ' + workers + '\n');
|
|
129
|
+
process.stdout.write('[run-stop-hook-tlc] Spec: ' + specPath + '\n');
|
|
130
|
+
process.stdout.write('[run-stop-hook-tlc] Cfg: ' + cfgPath + '\n');
|
|
131
|
+
|
|
132
|
+
const _startMs = Date.now();
|
|
133
|
+
process.stderr.write('[heap] Xms=64m Xmx=' + JAVA_HEAP_MAX + '\n');
|
|
134
|
+
const tlcResult = spawnSync(javaExe, [
|
|
135
|
+
'-XX:+UseParallelGC',
|
|
136
|
+
'-Xms64m', '-Xmx' + JAVA_HEAP_MAX,
|
|
137
|
+
'-jar', jarPath,
|
|
138
|
+
'-workers', workers,
|
|
139
|
+
'-config', cfgPath,
|
|
140
|
+
specPath,
|
|
141
|
+
], { encoding: 'utf8', stdio: 'inherit' });
|
|
142
|
+
const _runtimeMs = Date.now() - _startMs;
|
|
143
|
+
|
|
144
|
+
if (tlcResult.error) {
|
|
145
|
+
process.stderr.write('[run-stop-hook-tlc] TLC invocation failed: ' + tlcResult.error.message + '\n');
|
|
146
|
+
const check_id = CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase());
|
|
147
|
+
const property = PROPERTY_MAP[configName] || configName;
|
|
148
|
+
try { writeCheckResult({ tool: 'run-stop-hook-tlc', formalism: 'tla', result: 'fail', check_id: check_id, surface: 'tla', property: property, runtime_ms: _runtimeMs, summary: 'fail: TLC invocation failed in ' + _runtimeMs + 'ms', triage_tags: [], requirement_ids: getRequirementIds(check_id), metadata: { config: configName } }); } catch (e) { process.stderr.write('[run-stop-hook-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const passed = (tlcResult.status || 0) === 0;
|
|
153
|
+
const check_id = CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase());
|
|
154
|
+
const property = PROPERTY_MAP[configName] || configName;
|
|
155
|
+
const triage_tags = _runtimeMs > 120000 ? ['timeout-risk'] : [];
|
|
156
|
+
|
|
157
|
+
if (passed) {
|
|
158
|
+
const missingDeclarations = detectLivenessProperties(configName, cfgPath);
|
|
159
|
+
if (missingDeclarations.length > 0) {
|
|
160
|
+
try {
|
|
161
|
+
writeCheckResult({
|
|
162
|
+
tool: 'run-stop-hook-tlc',
|
|
163
|
+
formalism: 'tla',
|
|
164
|
+
result: 'inconclusive',
|
|
165
|
+
check_id: check_id,
|
|
166
|
+
surface: 'tla',
|
|
167
|
+
property: property,
|
|
168
|
+
runtime_ms: _runtimeMs,
|
|
169
|
+
summary: 'inconclusive: fairness missing in ' + _runtimeMs + 'ms',
|
|
170
|
+
triage_tags: ['needs-fairness'],
|
|
171
|
+
requirement_ids: getRequirementIds(check_id),
|
|
172
|
+
metadata: {
|
|
173
|
+
config: configName,
|
|
174
|
+
reason: 'Fairness declaration missing for: ' + missingDeclarations.join(', '),
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
} catch (e) {
|
|
178
|
+
process.stderr.write('[run-stop-hook-tlc] Warning: failed to write inconclusive result: ' + e.message + '\n');
|
|
179
|
+
}
|
|
180
|
+
process.stdout.write('[run-stop-hook-tlc] Result: inconclusive — fairness declaration missing for: ' + missingDeclarations.join(', ') + '\n');
|
|
181
|
+
process.exit(0);
|
|
182
|
+
}
|
|
183
|
+
try { writeCheckResult({ tool: 'run-stop-hook-tlc', formalism: 'tla', result: 'pass', check_id: check_id, surface: 'tla', property: property, runtime_ms: _runtimeMs, summary: 'pass: ' + configName + ' in ' + _runtimeMs + 'ms', triage_tags: triage_tags, requirement_ids: getRequirementIds(check_id), metadata: { config: configName } }); } catch (e) { process.stderr.write('[run-stop-hook-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
|
|
184
|
+
process.exit(0);
|
|
185
|
+
} else {
|
|
186
|
+
try { writeCheckResult({ tool: 'run-stop-hook-tlc', formalism: 'tla', result: 'fail', check_id: check_id, surface: 'tla', property: property, runtime_ms: _runtimeMs, summary: 'fail: ' + configName + ' in ' + _runtimeMs + 'ms', triage_tags: triage_tags, requirement_ids: getRequirementIds(check_id), metadata: { config: configName } }); } catch (e) { process.stderr.write('[run-stop-hook-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
|
|
187
|
+
process.exit(tlcResult.status || 1);
|
|
188
|
+
}
|
package/bin/run-tlc.cjs
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// bin/run-tlc.cjs
|
|
4
|
+
// Invokes TLC model checker for the QGSD formal TLA+ specification.
|
|
5
|
+
// Requirements: TLA-04
|
|
6
|
+
//
|
|
7
|
+
// Usage:
|
|
8
|
+
// node bin/run-tlc.cjs MCsafety # safety check (N=5, symmetry, ~30s)
|
|
9
|
+
// node bin/run-tlc.cjs MCliveness # liveness check (N=3, no symmetry, ~60s)
|
|
10
|
+
// node bin/run-tlc.cjs --config=MCsafety
|
|
11
|
+
//
|
|
12
|
+
// Prerequisites:
|
|
13
|
+
// - Java >=17 (https://adoptium.net/)
|
|
14
|
+
// - .planning/formal/tla/tla2tools.jar (see .planning/formal/tla/README.md for download command)
|
|
15
|
+
|
|
16
|
+
const { spawnSync } = require('child_process');
|
|
17
|
+
const JAVA_HEAP_MAX = process.env.QGSD_JAVA_HEAP_MAX || '512m';
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const { writeCheckResult } = require('./write-check-result.cjs');
|
|
21
|
+
const { getRequirementIds } = require('./requirement-map.cjs');
|
|
22
|
+
|
|
23
|
+
// ── Resolve project root (--project-root= overrides __dirname-relative) ─────
|
|
24
|
+
let ROOT = path.join(__dirname, '..');
|
|
25
|
+
for (const arg of process.argv) {
|
|
26
|
+
if (arg.startsWith('--project-root=')) ROOT = path.resolve(arg.slice('--project-root='.length));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const CHECK_ID_MAP = {
|
|
30
|
+
'MCsafety': 'tla:quorum-safety',
|
|
31
|
+
'MCliveness': 'tla:quorum-liveness',
|
|
32
|
+
'MCMCPEnv': 'tla:mcp-environment',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const PROPERTY_MAP = {
|
|
36
|
+
'MCsafety': 'Safety invariants — TypeInvariant, SafetyInvariant, no deadlock',
|
|
37
|
+
'MCliveness': 'Liveness — EventuallyDecided, EventuallyTerminates',
|
|
38
|
+
'MCMCPEnv': 'MCP environment — MCPEnvSafety, MCPEnvLiveness',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// ── Surface map for liveness detection ──────────────────────────────────────
|
|
42
|
+
const SURFACE_MAP = {
|
|
43
|
+
'MCliveness': 'quorum',
|
|
44
|
+
'MCsafety': 'safety',
|
|
45
|
+
'MCdeliberation': 'deliberation',
|
|
46
|
+
'MCprefilter': 'prefilter',
|
|
47
|
+
'MCrecruiting-liveness': 'recruiting',
|
|
48
|
+
'MCbreaker': 'breaker',
|
|
49
|
+
'MCconvergence': 'convergence',
|
|
50
|
+
'MCoscillation': 'oscillation',
|
|
51
|
+
'MCaccount-manager': 'account-manager',
|
|
52
|
+
'MCMCPEnv': 'mcp-calls', // MCPENV-02
|
|
53
|
+
'MCStopHook': 'stop-hook',
|
|
54
|
+
'MCAgentLoop': 'agent-loop',
|
|
55
|
+
'MCTUINavigation': 'tui-nav',
|
|
56
|
+
'MCinstaller': 'installer',
|
|
57
|
+
'MCDeliberationRevision': 'deliberation-revision',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Detect liveness properties in a TLC config that lack fairness declarations.
|
|
62
|
+
* Returns array of property names missing a matching ## header in invariants.md.
|
|
63
|
+
* Returns [] if no PROPERTY/PROPERTIES lines found (not a liveness config).
|
|
64
|
+
* @param {string} configName - Config name (e.g., 'MCliveness')
|
|
65
|
+
* @param {string} cfgPath - Absolute path to the .cfg file
|
|
66
|
+
* @param {string} [specDir] - Override for .planning/formal/spec directory (test injection)
|
|
67
|
+
* @returns {string[]} Property names with no fairness declaration
|
|
68
|
+
*/
|
|
69
|
+
function detectLivenessProperties(configName, cfgPath, specDir) {
|
|
70
|
+
const defaultSpecDir = path.join(ROOT, '.planning', 'formal', 'spec');
|
|
71
|
+
const resolvedSpecDir = specDir || defaultSpecDir;
|
|
72
|
+
|
|
73
|
+
let cfgContent;
|
|
74
|
+
try {
|
|
75
|
+
cfgContent = fs.readFileSync(cfgPath, 'utf8');
|
|
76
|
+
} catch (e) {
|
|
77
|
+
return []; // Can't read config — not a detection failure
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Extract all property names from PROPERTY/PROPERTIES lines (handles multi-name lines)
|
|
81
|
+
const propertyNames = [];
|
|
82
|
+
for (const line of cfgContent.split('\n')) {
|
|
83
|
+
const m = line.trim().match(/^PROPERT(?:Y|IES)\s+(.*)/);
|
|
84
|
+
if (m) {
|
|
85
|
+
propertyNames.push(...m[1].trim().split(/\s+/).filter(n => /^\w+$/.test(n)));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (propertyNames.length === 0) {
|
|
89
|
+
return []; // No liveness properties declared
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let surface = SURFACE_MAP[configName];
|
|
93
|
+
|
|
94
|
+
// Fallback: if config not in SURFACE_MAP, try stripping MC prefix
|
|
95
|
+
if (!surface && configName.startsWith('MC')) {
|
|
96
|
+
surface = configName.substring(2).toLowerCase();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!surface) {
|
|
100
|
+
return []; // Unknown surface — can't validate; do not block unknown configs
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const invariantsPath = path.join(resolvedSpecDir, surface, 'invariants.md');
|
|
104
|
+
let invariantsContent;
|
|
105
|
+
try {
|
|
106
|
+
invariantsContent = fs.readFileSync(invariantsPath, 'utf8');
|
|
107
|
+
} catch (e) {
|
|
108
|
+
// invariants.md missing entirely — all properties lack declarations
|
|
109
|
+
return propertyNames;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return propertyNames.filter(propName => !invariantsContent.includes('## ' + propName));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── Main execution (only when run directly, not when require()'d by tests) ──
|
|
116
|
+
if (require.main === module) {
|
|
117
|
+
// ── Parse --config argument ────────────────────────────────────────────────
|
|
118
|
+
const args = process.argv.slice(2);
|
|
119
|
+
const configArg = args.find(a => a.startsWith('--config=')) || null;
|
|
120
|
+
const configName = configArg
|
|
121
|
+
? configArg.split('=')[1]
|
|
122
|
+
: (args.find(a => !a.startsWith('-')) || 'MCsafety');
|
|
123
|
+
|
|
124
|
+
// Validate config: accept any configName as long as .cfg file exists
|
|
125
|
+
const _cfgCheckPath = path.join(ROOT, '.planning', 'formal', 'tla', configName + '.cfg');
|
|
126
|
+
if (!fs.existsSync(_cfgCheckPath)) {
|
|
127
|
+
process.stderr.write(
|
|
128
|
+
'[run-tlc] Config file not found: ' + _cfgCheckPath + '\n'
|
|
129
|
+
);
|
|
130
|
+
const _startMs = Date.now();
|
|
131
|
+
const _runtimeMs = 0;
|
|
132
|
+
try {
|
|
133
|
+
writeCheckResult({
|
|
134
|
+
tool: 'run-tlc',
|
|
135
|
+
formalism: 'tla',
|
|
136
|
+
result: 'fail',
|
|
137
|
+
check_id: 'tla:' + configName.toLowerCase(),
|
|
138
|
+
surface: 'tla',
|
|
139
|
+
property: 'Config not found: ' + configName,
|
|
140
|
+
runtime_ms: _runtimeMs,
|
|
141
|
+
summary: 'fail: config file not found in ' + _runtimeMs + 'ms',
|
|
142
|
+
requirement_ids: getRequirementIds('tla:' + configName.toLowerCase()),
|
|
143
|
+
metadata: { config: configName }
|
|
144
|
+
});
|
|
145
|
+
} catch (e) {
|
|
146
|
+
process.stderr.write('[run-tlc] Warning: failed to write check result: ' + e.message + '\n');
|
|
147
|
+
}
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── 1. Locate Java ─────────────────────────────────────────────────────────
|
|
152
|
+
const JAVA_HOME = process.env.JAVA_HOME;
|
|
153
|
+
let javaExe;
|
|
154
|
+
|
|
155
|
+
if (JAVA_HOME) {
|
|
156
|
+
javaExe = path.join(JAVA_HOME, 'bin', 'java');
|
|
157
|
+
if (!fs.existsSync(javaExe)) {
|
|
158
|
+
process.stderr.write(
|
|
159
|
+
'[run-tlc] JAVA_HOME is set but java binary not found at: ' + javaExe + '\n' +
|
|
160
|
+
'[run-tlc] Unset JAVA_HOME or fix the path.\n'
|
|
161
|
+
);
|
|
162
|
+
const _startMs = Date.now();
|
|
163
|
+
const _runtimeMs = 0;
|
|
164
|
+
try {
|
|
165
|
+
writeCheckResult({
|
|
166
|
+
tool: 'run-tlc',
|
|
167
|
+
formalism: 'tla',
|
|
168
|
+
result: 'fail',
|
|
169
|
+
check_id: CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase()),
|
|
170
|
+
surface: 'tla',
|
|
171
|
+
property: PROPERTY_MAP[configName] || configName,
|
|
172
|
+
runtime_ms: _runtimeMs,
|
|
173
|
+
summary: 'fail: Java not found in ' + _runtimeMs + 'ms',
|
|
174
|
+
requirement_ids: getRequirementIds(CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase())),
|
|
175
|
+
metadata: { config: configName }
|
|
176
|
+
});
|
|
177
|
+
} catch (e) {
|
|
178
|
+
process.stderr.write('[run-tlc] Warning: failed to write check result: ' + e.message + '\n');
|
|
179
|
+
}
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
// Fall back to PATH lookup
|
|
184
|
+
const probe = spawnSync('java', ['--version'], { encoding: 'utf8' });
|
|
185
|
+
if (probe.error || probe.status !== 0) {
|
|
186
|
+
process.stderr.write(
|
|
187
|
+
'[run-tlc] Java not found. Install Java >=17 and set JAVA_HOME.\n' +
|
|
188
|
+
'[run-tlc] Download: https://adoptium.net/\n'
|
|
189
|
+
);
|
|
190
|
+
const _startMs = Date.now();
|
|
191
|
+
const _runtimeMs = 0;
|
|
192
|
+
try {
|
|
193
|
+
writeCheckResult({
|
|
194
|
+
tool: 'run-tlc',
|
|
195
|
+
formalism: 'tla',
|
|
196
|
+
result: 'fail',
|
|
197
|
+
check_id: CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase()),
|
|
198
|
+
surface: 'tla',
|
|
199
|
+
property: PROPERTY_MAP[configName] || configName,
|
|
200
|
+
runtime_ms: _runtimeMs,
|
|
201
|
+
summary: 'fail: Java not found in ' + _runtimeMs + 'ms',
|
|
202
|
+
requirement_ids: getRequirementIds(CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase())),
|
|
203
|
+
metadata: { config: configName }
|
|
204
|
+
});
|
|
205
|
+
} catch (e) {
|
|
206
|
+
process.stderr.write('[run-tlc] Warning: failed to write check result: ' + e.message + '\n');
|
|
207
|
+
}
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
javaExe = 'java';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ── 2. Check Java version >=17 ─────────────────────────────────────────────
|
|
214
|
+
const versionResult = spawnSync(javaExe, ['--version'], { encoding: 'utf8' });
|
|
215
|
+
if (versionResult.error || versionResult.status !== 0) {
|
|
216
|
+
process.stderr.write('[run-tlc] Failed to run: ' + javaExe + ' --version\n');
|
|
217
|
+
const _startMs = Date.now();
|
|
218
|
+
const _runtimeMs = 0;
|
|
219
|
+
try {
|
|
220
|
+
writeCheckResult({
|
|
221
|
+
tool: 'run-tlc',
|
|
222
|
+
formalism: 'tla',
|
|
223
|
+
result: 'fail',
|
|
224
|
+
check_id: CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase()),
|
|
225
|
+
surface: 'tla',
|
|
226
|
+
property: PROPERTY_MAP[configName] || configName,
|
|
227
|
+
runtime_ms: _runtimeMs,
|
|
228
|
+
summary: 'fail: Java version check failed in ' + _runtimeMs + 'ms',
|
|
229
|
+
requirement_ids: getRequirementIds(CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase())),
|
|
230
|
+
metadata: { config: configName }
|
|
231
|
+
});
|
|
232
|
+
} catch (e) {
|
|
233
|
+
process.stderr.write('[run-tlc] Warning: failed to write check result: ' + e.message + '\n');
|
|
234
|
+
}
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
const versionOutput = versionResult.stdout + versionResult.stderr;
|
|
238
|
+
// Java version string varies: "openjdk 17.0.1 ..." or "java version \"17.0.1\""
|
|
239
|
+
const versionMatch = versionOutput.match(/(?:openjdk\s+|java version\s+[""]?)(\d+)/i);
|
|
240
|
+
const javaMajor = versionMatch ? parseInt(versionMatch[1], 10) : 0;
|
|
241
|
+
if (javaMajor < 17) {
|
|
242
|
+
process.stderr.write(
|
|
243
|
+
'[run-tlc] Java >=17 required. Found: ' + versionOutput.split('\n')[0] + '\n' +
|
|
244
|
+
'[run-tlc] Download Java 17+: https://adoptium.net/\n'
|
|
245
|
+
);
|
|
246
|
+
const _startMs = Date.now();
|
|
247
|
+
const _runtimeMs = 0;
|
|
248
|
+
try {
|
|
249
|
+
writeCheckResult({
|
|
250
|
+
tool: 'run-tlc',
|
|
251
|
+
formalism: 'tla',
|
|
252
|
+
result: 'fail',
|
|
253
|
+
check_id: CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase()),
|
|
254
|
+
surface: 'tla',
|
|
255
|
+
property: PROPERTY_MAP[configName] || configName,
|
|
256
|
+
runtime_ms: _runtimeMs,
|
|
257
|
+
summary: 'fail: Java ' + javaMajor + ' < 17 in ' + _runtimeMs + 'ms',
|
|
258
|
+
requirement_ids: getRequirementIds(CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase())),
|
|
259
|
+
metadata: { config: configName }
|
|
260
|
+
});
|
|
261
|
+
} catch (e) {
|
|
262
|
+
process.stderr.write('[run-tlc] Warning: failed to write check result: ' + e.message + '\n');
|
|
263
|
+
}
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── 3. Locate tla2tools.jar ────────────────────────────────────────────────
|
|
268
|
+
const jarPath = path.join(ROOT, '.planning', 'formal', 'tla', 'tla2tools.jar');
|
|
269
|
+
if (!fs.existsSync(jarPath)) {
|
|
270
|
+
process.stderr.write(
|
|
271
|
+
'[run-tlc] tla2tools.jar not found at: ' + jarPath + '\n' +
|
|
272
|
+
'[run-tlc] Download v1.8.0:\n' +
|
|
273
|
+
' curl -L https://github.com/tlaplus/tlaplus/releases/download/v1.8.0/tla2tools.jar \\\n' +
|
|
274
|
+
' -o .planning/formal/tla/tla2tools.jar\n'
|
|
275
|
+
);
|
|
276
|
+
const _startMs = Date.now();
|
|
277
|
+
const _runtimeMs = 0;
|
|
278
|
+
try {
|
|
279
|
+
writeCheckResult({
|
|
280
|
+
tool: 'run-tlc',
|
|
281
|
+
formalism: 'tla',
|
|
282
|
+
result: 'fail',
|
|
283
|
+
check_id: CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase()),
|
|
284
|
+
surface: 'tla',
|
|
285
|
+
property: PROPERTY_MAP[configName] || configName,
|
|
286
|
+
runtime_ms: _runtimeMs,
|
|
287
|
+
summary: 'fail: tla2tools.jar not found in ' + _runtimeMs + 'ms',
|
|
288
|
+
requirement_ids: getRequirementIds(CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase())),
|
|
289
|
+
metadata: { config: configName }
|
|
290
|
+
});
|
|
291
|
+
} catch (e) {
|
|
292
|
+
process.stderr.write('[run-tlc] Warning: failed to write check result: ' + e.message + '\n');
|
|
293
|
+
}
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ── 4. Invoke TLC ──────────────────────────────────────────────────────────
|
|
298
|
+
// Map config names to their corresponding spec files.
|
|
299
|
+
// Static map for known exceptions; auto-discovery handles the rest.
|
|
300
|
+
const SPEC_MAP = {
|
|
301
|
+
'MCMCPEnv': 'QGSDMCPEnv.tla',
|
|
302
|
+
'MCsafety': 'QGSDQuorum.tla',
|
|
303
|
+
'MCliveness': 'QGSDQuorum.tla',
|
|
304
|
+
'MCQGSDQuorum': 'QGSDQuorum_xstate.tla',
|
|
305
|
+
'MCrecruiting-liveness': 'QGSDRecruiting.tla',
|
|
306
|
+
'MCrecruiting-safety': 'QGSDRecruiting.tla',
|
|
307
|
+
'MCTUINavigation': 'TUINavigation.tla',
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// Auto-discover spec file: (1) check SPEC_MAP, (2) scan cfg header for QGSD*.tla ref,
|
|
311
|
+
// (3) try naming conventions, (4) fall back to QGSDQuorum.tla
|
|
312
|
+
function resolveSpecFile(cfgName) {
|
|
313
|
+
if (SPEC_MAP[cfgName]) return SPEC_MAP[cfgName];
|
|
314
|
+
|
|
315
|
+
const tlaDir = path.join(ROOT, '.planning', 'formal', 'tla');
|
|
316
|
+
|
|
317
|
+
// Strategy 1: read cfg header for QGSD*.tla reference (with or without .tla suffix)
|
|
318
|
+
try {
|
|
319
|
+
const cfgContent = fs.readFileSync(path.join(tlaDir, cfgName + '.cfg'), 'utf8');
|
|
320
|
+
const headerLines = cfgContent.split('\n').slice(0, 5).join('\n');
|
|
321
|
+
// Match QGSD*.tla or "for QGSD*." (without .tla extension)
|
|
322
|
+
const refMatch = headerLines.match(/QGSD\w+\.tla/) || headerLines.match(/QGSD\w+/);
|
|
323
|
+
if (refMatch) {
|
|
324
|
+
const candidate = refMatch[0].endsWith('.tla') ? refMatch[0] : refMatch[0] + '.tla';
|
|
325
|
+
if (fs.existsSync(path.join(tlaDir, candidate))) return candidate;
|
|
326
|
+
}
|
|
327
|
+
} catch (_) { /* fall through */ }
|
|
328
|
+
|
|
329
|
+
// Strategy 2: naming convention — strip MC prefix, normalize hyphens, find matching QGSD*.tla
|
|
330
|
+
const stripped = cfgName.replace(/^MC/, '').toLowerCase().replace(/-/g, '');
|
|
331
|
+
try {
|
|
332
|
+
const allTla = fs.readdirSync(tlaDir).filter(f => f.endsWith('.tla') && !f.includes('TTrace'));
|
|
333
|
+
const qgsdFiles = allTla.filter(f => f.startsWith('QGSD'));
|
|
334
|
+
const normalize = (s) => s.toLowerCase().replace(/-/g, '');
|
|
335
|
+
// Exact match against QGSD-prefixed files
|
|
336
|
+
const match = qgsdFiles.find(f => normalize(f.replace('QGSD', '').replace('.tla', '')) === stripped);
|
|
337
|
+
if (match) return match;
|
|
338
|
+
// Fuzzy substring match against QGSD-prefixed files
|
|
339
|
+
const fuzzy = qgsdFiles.find(f => normalize(f).includes(stripped));
|
|
340
|
+
if (fuzzy) return fuzzy;
|
|
341
|
+
// Fallback: check non-QGSD-prefixed files (exact match on stripped name)
|
|
342
|
+
const nonPrefixed = allTla.find(f => normalize(f.replace('.tla', '')) === stripped);
|
|
343
|
+
if (nonPrefixed) return nonPrefixed;
|
|
344
|
+
} catch (_) { /* fall through */ }
|
|
345
|
+
|
|
346
|
+
return 'QGSDQuorum.tla';
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const specFile = resolveSpecFile(configName);
|
|
350
|
+
const specPath = path.join(ROOT, '.planning', 'formal', 'tla', specFile);
|
|
351
|
+
const cfgPath = path.join(ROOT, '.planning', 'formal', 'tla', configName + '.cfg');
|
|
352
|
+
// Use -workers 1 for liveness (defensive — avoids known multi-worker liveness bugs in older TLC)
|
|
353
|
+
const workers = configName === 'MCliveness' ? '1' : 'auto';
|
|
354
|
+
|
|
355
|
+
process.stdout.write('[run-tlc] Config: ' + configName + ' Workers: ' + workers + '\n');
|
|
356
|
+
process.stdout.write('[run-tlc] Spec: ' + specPath + '\n');
|
|
357
|
+
process.stdout.write('[run-tlc] Cfg: ' + cfgPath + '\n');
|
|
358
|
+
|
|
359
|
+
const _startMs = Date.now();
|
|
360
|
+
process.stderr.write('[heap] Xms=64m Xmx=' + JAVA_HEAP_MAX + '\n');
|
|
361
|
+
const tlcResult = spawnSync(javaExe, [
|
|
362
|
+
'-Xms64m', '-Xmx' + JAVA_HEAP_MAX,
|
|
363
|
+
'-jar', jarPath,
|
|
364
|
+
'-config', cfgPath,
|
|
365
|
+
'-workers', workers,
|
|
366
|
+
specPath,
|
|
367
|
+
], { encoding: 'utf8', stdio: 'inherit' });
|
|
368
|
+
const _runtimeMs = Date.now() - _startMs;
|
|
369
|
+
|
|
370
|
+
if (tlcResult.error) {
|
|
371
|
+
process.stderr.write('[run-tlc] TLC invocation failed: ' + tlcResult.error.message + '\n');
|
|
372
|
+
const check_id = CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase());
|
|
373
|
+
const property = PROPERTY_MAP[configName] || configName;
|
|
374
|
+
try {
|
|
375
|
+
writeCheckResult({
|
|
376
|
+
tool: 'run-tlc',
|
|
377
|
+
formalism: 'tla',
|
|
378
|
+
result: 'fail',
|
|
379
|
+
check_id: check_id,
|
|
380
|
+
surface: 'tla',
|
|
381
|
+
property: property,
|
|
382
|
+
runtime_ms: _runtimeMs,
|
|
383
|
+
summary: 'fail: TLC invocation failed in ' + _runtimeMs + 'ms',
|
|
384
|
+
requirement_ids: getRequirementIds(check_id),
|
|
385
|
+
metadata: { config: configName }
|
|
386
|
+
});
|
|
387
|
+
} catch (e) {
|
|
388
|
+
process.stderr.write('[run-tlc] Warning: failed to write check result: ' + e.message + '\n');
|
|
389
|
+
}
|
|
390
|
+
process.exit(1);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const passed = (tlcResult.status || 0) === 0;
|
|
394
|
+
|
|
395
|
+
const check_id = CHECK_ID_MAP[configName] || ('tla:' + configName.toLowerCase());
|
|
396
|
+
const property = PROPERTY_MAP[configName] || configName;
|
|
397
|
+
const triage_tags = _runtimeMs > 120000 ? ['timeout-risk'] : [];
|
|
398
|
+
|
|
399
|
+
if (passed) {
|
|
400
|
+
const missingDeclarations = detectLivenessProperties(configName, cfgPath);
|
|
401
|
+
if (missingDeclarations.length > 0) {
|
|
402
|
+
try {
|
|
403
|
+
writeCheckResult({
|
|
404
|
+
tool: 'run-tlc',
|
|
405
|
+
formalism: 'tla',
|
|
406
|
+
result: 'inconclusive',
|
|
407
|
+
check_id: check_id,
|
|
408
|
+
surface: 'tla',
|
|
409
|
+
property: property,
|
|
410
|
+
runtime_ms: _runtimeMs,
|
|
411
|
+
summary: 'inconclusive: fairness missing in ' + _runtimeMs + 'ms',
|
|
412
|
+
triage_tags: ['needs-fairness'],
|
|
413
|
+
requirement_ids: getRequirementIds(check_id),
|
|
414
|
+
metadata: {
|
|
415
|
+
config: configName,
|
|
416
|
+
reason: 'Fairness declaration missing for: ' + missingDeclarations.join(', '),
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
} catch (e) {
|
|
420
|
+
process.stderr.write('[run-tlc] Warning: failed to write inconclusive result: ' + e.message + '\n');
|
|
421
|
+
}
|
|
422
|
+
process.stdout.write('[run-tlc] Result: inconclusive — fairness declaration missing for: ' + missingDeclarations.join(', ') + '\n');
|
|
423
|
+
process.exit(0); // inconclusive is not a failure
|
|
424
|
+
} else {
|
|
425
|
+
try {
|
|
426
|
+
writeCheckResult({
|
|
427
|
+
tool: 'run-tlc',
|
|
428
|
+
formalism: 'tla',
|
|
429
|
+
result: 'pass',
|
|
430
|
+
check_id: check_id,
|
|
431
|
+
surface: 'tla',
|
|
432
|
+
property: property,
|
|
433
|
+
runtime_ms: _runtimeMs,
|
|
434
|
+
summary: 'pass: ' + configName + ' in ' + _runtimeMs + 'ms',
|
|
435
|
+
triage_tags: triage_tags,
|
|
436
|
+
requirement_ids: getRequirementIds(check_id),
|
|
437
|
+
metadata: { config: configName }
|
|
438
|
+
});
|
|
439
|
+
} catch (e) {
|
|
440
|
+
process.stderr.write('[run-tlc] Warning: failed to write check result: ' + e.message + '\n');
|
|
441
|
+
}
|
|
442
|
+
process.exit(0);
|
|
443
|
+
}
|
|
444
|
+
} else {
|
|
445
|
+
try {
|
|
446
|
+
writeCheckResult({
|
|
447
|
+
tool: 'run-tlc',
|
|
448
|
+
formalism: 'tla',
|
|
449
|
+
result: 'fail',
|
|
450
|
+
check_id: check_id,
|
|
451
|
+
surface: 'tla',
|
|
452
|
+
property: property,
|
|
453
|
+
runtime_ms: _runtimeMs,
|
|
454
|
+
summary: 'fail: ' + configName + ' in ' + _runtimeMs + 'ms',
|
|
455
|
+
triage_tags: triage_tags,
|
|
456
|
+
requirement_ids: getRequirementIds(check_id),
|
|
457
|
+
metadata: { config: configName }
|
|
458
|
+
});
|
|
459
|
+
} catch (e) {
|
|
460
|
+
process.stderr.write('[run-tlc] Warning: failed to write check result: ' + e.message + '\n');
|
|
461
|
+
}
|
|
462
|
+
process.exit(tlcResult.status || 0);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Export for testing — safe because main execution is guarded by require.main === module
|
|
467
|
+
module.exports = { detectLivenessProperties };
|