@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,294 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* review-mcp-logs.cjs
|
|
6
|
+
*
|
|
7
|
+
* Scans ~/.claude/debug/*.txt for MCP tool call timing, failures, and hang
|
|
8
|
+
* patterns. Outputs a structured report surfacing automation opportunities.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* node bin/review-mcp-logs.cjs [--files N] [--days N] [--json] [--tool <name>]
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
|
|
18
|
+
// ─── CLI args ─────────────────────────────────────────────────────────────────
|
|
19
|
+
const args = process.argv.slice(2);
|
|
20
|
+
const getArg = (flag) => {
|
|
21
|
+
const i = args.indexOf(flag);
|
|
22
|
+
return i !== -1 && args[i + 1] ? args[i + 1] : null;
|
|
23
|
+
};
|
|
24
|
+
const hasFlag = (flag) => args.includes(flag);
|
|
25
|
+
|
|
26
|
+
const MAX_FILES = parseInt(getArg('--files') ?? '50', 10);
|
|
27
|
+
const MAX_DAYS = parseInt(getArg('--days') ?? '7', 10);
|
|
28
|
+
const JSON_OUTPUT = hasFlag('--json');
|
|
29
|
+
const TOOL_FILTER = getArg('--tool');
|
|
30
|
+
|
|
31
|
+
// ─── Paths ────────────────────────────────────────────────────────────────────
|
|
32
|
+
const DEBUG_DIR = path.join(os.homedir(), '.claude', 'debug');
|
|
33
|
+
|
|
34
|
+
// ─── Regex patterns ───────────────────────────────────────────────────────────
|
|
35
|
+
// MCP tool timing lines emitted by Claude Code
|
|
36
|
+
const RE_RUNNING = /MCP server "([^"]+)": Tool '([^']+)' still running \((\d+)s elapsed\)/;
|
|
37
|
+
const RE_COMPLETE = /MCP server "([^"]+)": Tool '([^']+)' completed successfully in (\d+)ms/;
|
|
38
|
+
const RE_FAILED = /MCP server "([^"]+)": Tool '([^']+)' failed after (\d+)s: (.+)/;
|
|
39
|
+
const RE_CALLING = /MCP server "([^"]+)": Calling MCP tool: (.+)/;
|
|
40
|
+
const RE_STDERR = /MCP server "([^"]+)" Server stderr: (.+)/;
|
|
41
|
+
const RE_CONNECT = /MCP server "([^"]+)": Successfully connected .+ in (\d+)ms/;
|
|
42
|
+
const RE_TOOL_ERR = /mcp__([^_]+)__(\S+) tool error \((\d+)ms\): (.+)/;
|
|
43
|
+
const RE_TIMESTAMP = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z)/;
|
|
44
|
+
|
|
45
|
+
// ─── Data structures ──────────────────────────────────────────────────────────
|
|
46
|
+
const servers = {}; // serverName -> { calls: [], failures: [], hangs: [] }
|
|
47
|
+
const timeline = []; // [{ts, server, tool, durationMs, status}]
|
|
48
|
+
const stderrLog = {}; // serverName -> [messages]
|
|
49
|
+
|
|
50
|
+
function ensureServer(name) {
|
|
51
|
+
if (!servers[name]) {
|
|
52
|
+
servers[name] = { calls: [], failures: [], hangs: [], connectMs: null };
|
|
53
|
+
}
|
|
54
|
+
return servers[name];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── Parse files ──────────────────────────────────────────────────────────────
|
|
58
|
+
const cutoff = Date.now() - MAX_DAYS * 86400 * 1000;
|
|
59
|
+
|
|
60
|
+
let files;
|
|
61
|
+
try {
|
|
62
|
+
files = fs.readdirSync(DEBUG_DIR)
|
|
63
|
+
.filter(f => f.endsWith('.txt'))
|
|
64
|
+
.map(f => ({ name: f, mtime: fs.statSync(path.join(DEBUG_DIR, f)).mtimeMs }))
|
|
65
|
+
.filter(f => f.mtime >= cutoff)
|
|
66
|
+
.sort((a, b) => b.mtime - a.mtime)
|
|
67
|
+
.slice(0, MAX_FILES)
|
|
68
|
+
.map(f => path.join(DEBUG_DIR, f.name));
|
|
69
|
+
} catch {
|
|
70
|
+
console.error(`Cannot read debug dir: ${DEBUG_DIR}`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (files.length === 0) {
|
|
75
|
+
console.log(`No debug files found in last ${MAX_DAYS} days at ${DEBUG_DIR}`);
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (const file of files) {
|
|
80
|
+
let content;
|
|
81
|
+
try { content = fs.readFileSync(file, 'utf8'); } catch { continue; }
|
|
82
|
+
|
|
83
|
+
for (const line of content.split('\n')) {
|
|
84
|
+
const tsMatch = line.match(RE_TIMESTAMP);
|
|
85
|
+
const ts = tsMatch ? new Date(tsMatch[1]).getTime() : 0;
|
|
86
|
+
|
|
87
|
+
let m;
|
|
88
|
+
|
|
89
|
+
if ((m = line.match(RE_COMPLETE))) {
|
|
90
|
+
const [, server, tool, ms] = m;
|
|
91
|
+
if (TOOL_FILTER && !server.includes(TOOL_FILTER) && !tool.includes(TOOL_FILTER)) continue;
|
|
92
|
+
const s = ensureServer(server);
|
|
93
|
+
s.calls.push({ tool, durationMs: parseInt(ms, 10), status: 'ok', ts });
|
|
94
|
+
timeline.push({ ts, server, tool, durationMs: parseInt(ms, 10), status: 'ok' });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
else if ((m = line.match(RE_FAILED))) {
|
|
98
|
+
const [, server, tool, sec, reason] = m;
|
|
99
|
+
if (TOOL_FILTER && !server.includes(TOOL_FILTER) && !tool.includes(TOOL_FILTER)) continue;
|
|
100
|
+
const s = ensureServer(server);
|
|
101
|
+
const durationMs = parseInt(sec, 10) * 1000;
|
|
102
|
+
s.failures.push({ tool, durationMs, reason: reason.trim(), ts });
|
|
103
|
+
timeline.push({ ts, server, tool, durationMs, status: 'fail', reason: reason.trim() });
|
|
104
|
+
if (durationMs > 60000) s.hangs.push({ tool, durationMs, reason: reason.trim(), ts });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
else if ((m = line.match(RE_TOOL_ERR))) {
|
|
108
|
+
const [, serverPart, tool, ms, reason] = m;
|
|
109
|
+
const server = `claude-${serverPart}`;
|
|
110
|
+
if (TOOL_FILTER && !server.includes(TOOL_FILTER) && !tool.includes(TOOL_FILTER)) continue;
|
|
111
|
+
const s = ensureServer(server);
|
|
112
|
+
const durationMs = parseInt(ms, 10);
|
|
113
|
+
s.failures.push({ tool, durationMs, reason: reason.trim(), ts, fromToolError: true });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
else if ((m = line.match(RE_CONNECT))) {
|
|
117
|
+
const [, server, ms] = m;
|
|
118
|
+
ensureServer(server).connectMs = parseInt(ms, 10);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
else if ((m = line.match(RE_STDERR))) {
|
|
122
|
+
const [, server, msg] = m;
|
|
123
|
+
if (!stderrLog[server]) stderrLog[server] = [];
|
|
124
|
+
stderrLog[server].push(msg.trim());
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─── Compute per-server stats ─────────────────────────────────────────────────
|
|
130
|
+
const serverStats = {};
|
|
131
|
+
for (const [name, data] of Object.entries(servers)) {
|
|
132
|
+
const ok = data.calls;
|
|
133
|
+
const failed = data.failures;
|
|
134
|
+
const durations = ok.map(c => c.durationMs);
|
|
135
|
+
|
|
136
|
+
serverStats[name] = {
|
|
137
|
+
totalCalls: ok.length + failed.length,
|
|
138
|
+
successCount: ok.length,
|
|
139
|
+
failureCount: failed.length,
|
|
140
|
+
hangCount: data.hangs.length,
|
|
141
|
+
connectMs: data.connectMs,
|
|
142
|
+
p50Ms: percentile(durations, 50),
|
|
143
|
+
p95Ms: percentile(durations, 95),
|
|
144
|
+
maxMs: durations.length ? Math.max(...durations) : 0,
|
|
145
|
+
failures: failed,
|
|
146
|
+
hangs: data.hangs,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ─── Automation patterns ──────────────────────────────────────────────────────
|
|
151
|
+
// Find tools that always fail — automation target: add to UNAVAIL skip list
|
|
152
|
+
const alwaysFailing = Object.entries(serverStats)
|
|
153
|
+
.filter(([, s]) => s.totalCalls > 0 && s.successCount === 0)
|
|
154
|
+
.map(([name]) => name);
|
|
155
|
+
|
|
156
|
+
// Find tools with consistent long latency — automation target: increase timeout or route differently
|
|
157
|
+
const slowServers = Object.entries(serverStats)
|
|
158
|
+
.filter(([, s]) => s.p95Ms > 30000 && s.successCount > 0)
|
|
159
|
+
.sort(([, a], [, b]) => b.p95Ms - a.p95Ms);
|
|
160
|
+
|
|
161
|
+
// Find recurring error messages — may hint at config fixes
|
|
162
|
+
const errorFreq = {};
|
|
163
|
+
for (const [server, data] of Object.entries(servers)) {
|
|
164
|
+
for (const f of data.failures) {
|
|
165
|
+
const key = f.reason.slice(0, 80);
|
|
166
|
+
errorFreq[key] = (errorFreq[key] ?? 0) + 1;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const topErrors = Object.entries(errorFreq)
|
|
170
|
+
.sort(([, a], [, b]) => b - a)
|
|
171
|
+
.slice(0, 8);
|
|
172
|
+
|
|
173
|
+
// ─── Output ───────────────────────────────────────────────────────────────────
|
|
174
|
+
if (JSON_OUTPUT) {
|
|
175
|
+
console.log(JSON.stringify({ serverStats, alwaysFailing, slowServers: slowServers.map(([n, s]) => ({ name: n, p95Ms: s.p95Ms, maxMs: s.maxMs })), topErrors }, null, 2));
|
|
176
|
+
process.exit(0);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const hr = '─'.repeat(60);
|
|
180
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
181
|
+
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
182
|
+
const yellow= (s) => `\x1b[33m${s}\x1b[0m`;
|
|
183
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
184
|
+
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
185
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
186
|
+
|
|
187
|
+
const fmtMs = (ms) => {
|
|
188
|
+
if (!ms) return dim('—');
|
|
189
|
+
if (ms >= 60000) return red(`${(ms/1000).toFixed(0)}s`);
|
|
190
|
+
if (ms >= 10000) return yellow(`${(ms/1000).toFixed(1)}s`);
|
|
191
|
+
return green(`${ms}ms`);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
console.log(`\n${bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')}`);
|
|
195
|
+
console.log(bold(' QGSD ► MCP LOG REVIEW'));
|
|
196
|
+
console.log(bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
197
|
+
console.log(dim(` Scanned: ${files.length} debug files | Last ${MAX_DAYS} days | ${DEBUG_DIR}`));
|
|
198
|
+
console.log();
|
|
199
|
+
|
|
200
|
+
// ── Per-server table ──────────────────────────────────────────────────────────
|
|
201
|
+
console.log(bold('MCP SERVER HEALTH'));
|
|
202
|
+
console.log(hr);
|
|
203
|
+
|
|
204
|
+
const colW = [22, 6, 6, 6, 6, 10, 10, 10];
|
|
205
|
+
const header = [
|
|
206
|
+
'Server', 'Calls', 'OK', 'Fail', 'Hang', 'p50', 'p95', 'max'
|
|
207
|
+
].map((h, i) => h.padEnd(colW[i])).join(' ');
|
|
208
|
+
console.log(dim(header));
|
|
209
|
+
|
|
210
|
+
for (const [name, s] of Object.entries(serverStats).sort(([a],[b]) => a.localeCompare(b))) {
|
|
211
|
+
const failColor = s.failureCount > 0 ? red : green;
|
|
212
|
+
const hangColor = s.hangCount > 0 ? red : dim;
|
|
213
|
+
const row = [
|
|
214
|
+
name.slice(0, 21).padEnd(colW[0]),
|
|
215
|
+
String(s.totalCalls).padEnd(colW[1]),
|
|
216
|
+
green(String(s.successCount)).padEnd(colW[2] + 9),
|
|
217
|
+
failColor(String(s.failureCount)).padEnd(colW[3] + 9),
|
|
218
|
+
hangColor(String(s.hangCount)).padEnd(colW[4] + 9),
|
|
219
|
+
fmtMs(s.p50Ms).padEnd(colW[5] + 9),
|
|
220
|
+
fmtMs(s.p95Ms).padEnd(colW[6] + 9),
|
|
221
|
+
fmtMs(s.maxMs),
|
|
222
|
+
].join(' ');
|
|
223
|
+
console.log(row);
|
|
224
|
+
}
|
|
225
|
+
console.log();
|
|
226
|
+
|
|
227
|
+
// ── Failures detail ───────────────────────────────────────────────────────────
|
|
228
|
+
const allFailures = Object.entries(serverStats)
|
|
229
|
+
.flatMap(([server, s]) => s.failures.map(f => ({ server, ...f })))
|
|
230
|
+
.sort((a, b) => b.ts - a.ts)
|
|
231
|
+
.slice(0, 15);
|
|
232
|
+
|
|
233
|
+
if (allFailures.length > 0) {
|
|
234
|
+
console.log(bold('RECENT FAILURES (last 15)'));
|
|
235
|
+
console.log(hr);
|
|
236
|
+
for (const f of allFailures) {
|
|
237
|
+
const dt = f.ts ? new Date(f.ts).toISOString().slice(0,19).replace('T',' ') : '?';
|
|
238
|
+
console.log(` ${dim(dt)} ${yellow(f.server)} ${cyan(f.tool)} ${fmtMs(f.durationMs)}`);
|
|
239
|
+
console.log(` ${dim(f.reason.slice(0, 100))}`);
|
|
240
|
+
}
|
|
241
|
+
console.log();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ── Automation insights ───────────────────────────────────────────────────────
|
|
245
|
+
console.log(bold('AUTOMATION INSIGHTS'));
|
|
246
|
+
console.log(hr);
|
|
247
|
+
|
|
248
|
+
if (alwaysFailing.length > 0) {
|
|
249
|
+
console.log(red(' ✗ Always-failing servers (add to UNAVAIL skip list):'));
|
|
250
|
+
alwaysFailing.forEach(s => console.log(` • ${s}`));
|
|
251
|
+
console.log();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (slowServers.length > 0) {
|
|
255
|
+
console.log(yellow(' ⚠ Chronically slow servers (p95 > 30s):'));
|
|
256
|
+
slowServers.forEach(([name, s]) => {
|
|
257
|
+
console.log(` • ${name} p95=${fmtMs(s.p95Ms)} max=${fmtMs(s.maxMs)}`);
|
|
258
|
+
console.log(` → consider raising CLAUDE_MCP_TIMEOUT_MS or routing via faster provider`);
|
|
259
|
+
});
|
|
260
|
+
console.log();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (topErrors.length > 0) {
|
|
264
|
+
console.log(cyan(' ↻ Most common error patterns:'));
|
|
265
|
+
topErrors.forEach(([msg, count]) => {
|
|
266
|
+
console.log(` [${count}x] ${dim(msg)}`);
|
|
267
|
+
});
|
|
268
|
+
console.log();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ── Suggested CLAUDE_MCP_TIMEOUT_MS ──────────────────────────────────────────
|
|
272
|
+
const allP95 = Object.values(serverStats).map(s => s.p95Ms).filter(Boolean);
|
|
273
|
+
if (allP95.length > 0) {
|
|
274
|
+
const suggested = Math.max(60000, Math.min(300000, Math.ceil(Math.max(...allP95) * 1.5 / 10000) * 10000));
|
|
275
|
+
console.log(bold('SUGGESTED CONFIG'));
|
|
276
|
+
console.log(hr);
|
|
277
|
+
console.log(` CLAUDE_MCP_TIMEOUT_MS=${suggested} ${dim('(1.5× observed p95 max, capped 60s–300s)')}`);
|
|
278
|
+
console.log(` ${dim('Set this in ~/.claude.json env for claude-deepseek/minimax/etc.')}`);
|
|
279
|
+
console.log();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
console.log(bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
283
|
+
console.log(dim(` Run with --json for machine-readable output`));
|
|
284
|
+
console.log(dim(` Run with --tool <name> to filter a specific server`));
|
|
285
|
+
console.log(dim(` Run with --days N to change lookback window`));
|
|
286
|
+
console.log();
|
|
287
|
+
|
|
288
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
289
|
+
function percentile(arr, p) {
|
|
290
|
+
if (!arr.length) return 0;
|
|
291
|
+
const sorted = [...arr].sort((a, b) => a - b);
|
|
292
|
+
const idx = Math.ceil((p / 100) * sorted.length) - 1;
|
|
293
|
+
return sorted[Math.max(0, idx)];
|
|
294
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// bin/run-account-manager-tlc.cjs
|
|
4
|
+
// Invokes TLC model checker for the QGSD account manager TLA+ specification.
|
|
5
|
+
// Source spec: .planning/formal/tla/QGSDAccountManager.tla
|
|
6
|
+
// Source impl: bin/account-manager.cjs
|
|
7
|
+
//
|
|
8
|
+
// Checks:
|
|
9
|
+
// TypeOK — all variables conform to declared types
|
|
10
|
+
// ActiveIsPoolMember — active account (when set) must be in the pool
|
|
11
|
+
// NoActiveWhenEmpty — empty pool implies no active account
|
|
12
|
+
// IdleNoPending — in IDLE state, no pending operation
|
|
13
|
+
// OpMatchesState — pending_op.type matches current FSM state
|
|
14
|
+
// IdleReachable — IDLE is eventually reachable from any state (liveness)
|
|
15
|
+
//
|
|
16
|
+
// Usage:
|
|
17
|
+
// node bin/run-account-manager-tlc.cjs # default: MCaccount-manager
|
|
18
|
+
// node bin/run-account-manager-tlc.cjs MCaccount-manager
|
|
19
|
+
// node bin/run-account-manager-tlc.cjs --config=MCaccount-manager
|
|
20
|
+
//
|
|
21
|
+
// Prerequisites:
|
|
22
|
+
// - Java >=17 (https://adoptium.net/)
|
|
23
|
+
// - .planning/formal/tla/tla2tools.jar (see .planning/formal/tla/README.md for download command)
|
|
24
|
+
|
|
25
|
+
const { spawnSync } = require('child_process');
|
|
26
|
+
const JAVA_HEAP_MAX = process.env.QGSD_JAVA_HEAP_MAX || '512m';
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
const { writeCheckResult } = require('./write-check-result.cjs');
|
|
30
|
+
const { detectLivenessProperties } = require('./run-tlc.cjs');
|
|
31
|
+
const { getRequirementIds } = require('./requirement-map.cjs');
|
|
32
|
+
|
|
33
|
+
// ── Resolve project root (--project-root= overrides __dirname-relative) ─────
|
|
34
|
+
let ROOT = path.join(__dirname, '..');
|
|
35
|
+
for (const arg of process.argv) {
|
|
36
|
+
if (arg.startsWith('--project-root=')) ROOT = path.resolve(arg.slice('--project-root='.length));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Parse --config argument ──────────────────────────────────────────────────
|
|
40
|
+
const args = process.argv.slice(2);
|
|
41
|
+
const configArg = args.find(a => a.startsWith('--config=')) || null;
|
|
42
|
+
const configName = configArg
|
|
43
|
+
? configArg.split('=')[1]
|
|
44
|
+
: (args.find(a => !a.startsWith('-')) || 'MCaccount-manager');
|
|
45
|
+
|
|
46
|
+
const VALID_CONFIGS = ['MCaccount-manager'];
|
|
47
|
+
if (!VALID_CONFIGS.includes(configName)) {
|
|
48
|
+
process.stderr.write(
|
|
49
|
+
'[run-account-manager-tlc] Unknown config: ' + configName +
|
|
50
|
+
'. Valid: ' + VALID_CONFIGS.join(', ') + '\n'
|
|
51
|
+
);
|
|
52
|
+
const _startMs = Date.now();
|
|
53
|
+
const _runtimeMs = 0;
|
|
54
|
+
try { writeCheckResult({ tool: 'run-account-manager-tlc', formalism: 'tla', result: 'fail', check_id: 'tla:account-manager', surface: 'tla', property: 'Account manager quorum state machine — MCAM correctness', runtime_ms: _runtimeMs, summary: 'fail: unknown config in ' + _runtimeMs + 'ms', requirement_ids: getRequirementIds('tla:account-manager'), metadata: {} }); } catch (e) { process.stderr.write('[run-account-manager-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── 1. Locate Java ───────────────────────────────────────────────────────────
|
|
59
|
+
const JAVA_HOME = process.env.JAVA_HOME;
|
|
60
|
+
let javaExe;
|
|
61
|
+
|
|
62
|
+
if (JAVA_HOME) {
|
|
63
|
+
javaExe = path.join(JAVA_HOME, 'bin', 'java');
|
|
64
|
+
if (!fs.existsSync(javaExe)) {
|
|
65
|
+
process.stderr.write(
|
|
66
|
+
'[run-account-manager-tlc] JAVA_HOME is set but java binary not found at: ' + javaExe + '\n' +
|
|
67
|
+
'[run-account-manager-tlc] Unset JAVA_HOME or fix the path.\n'
|
|
68
|
+
);
|
|
69
|
+
const _startMs = Date.now();
|
|
70
|
+
const _runtimeMs = 0;
|
|
71
|
+
try { writeCheckResult({ tool: 'run-account-manager-tlc', formalism: 'tla', result: 'fail', check_id: 'tla:account-manager', surface: 'tla', property: 'Account manager quorum state machine — MCAM correctness', runtime_ms: _runtimeMs, summary: 'fail: Java not found in ' + _runtimeMs + 'ms', requirement_ids: getRequirementIds('tla:account-manager'), metadata: {} }); } catch (e) { process.stderr.write('[run-account-manager-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
const probe = spawnSync('java', ['--version'], { encoding: 'utf8' });
|
|
76
|
+
if (probe.error || probe.status !== 0) {
|
|
77
|
+
process.stderr.write(
|
|
78
|
+
'[run-account-manager-tlc] Java not found. Install Java >=17 and set JAVA_HOME.\n' +
|
|
79
|
+
'[run-account-manager-tlc] Download: https://adoptium.net/\n'
|
|
80
|
+
);
|
|
81
|
+
const _startMs = Date.now();
|
|
82
|
+
const _runtimeMs = 0;
|
|
83
|
+
try { writeCheckResult({ tool: 'run-account-manager-tlc', formalism: 'tla', result: 'fail', check_id: 'tla:account-manager', surface: 'tla', property: 'Account manager quorum state machine — MCAM correctness', runtime_ms: _runtimeMs, summary: 'fail: Java not found in ' + _runtimeMs + 'ms', requirement_ids: getRequirementIds('tla:account-manager'), metadata: {} }); } catch (e) { process.stderr.write('[run-account-manager-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
javaExe = 'java';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── 2. Check Java version >=17 ───────────────────────────────────────────────
|
|
90
|
+
const versionResult = spawnSync(javaExe, ['--version'], { encoding: 'utf8' });
|
|
91
|
+
if (versionResult.error || versionResult.status !== 0) {
|
|
92
|
+
process.stderr.write('[run-account-manager-tlc] Failed to run: ' + javaExe + ' --version\n');
|
|
93
|
+
const _startMs = Date.now();
|
|
94
|
+
const _runtimeMs = 0;
|
|
95
|
+
try { writeCheckResult({ tool: 'run-account-manager-tlc', formalism: 'tla', result: 'fail', check_id: 'tla:account-manager', surface: 'tla', property: 'Account manager quorum state machine — MCAM correctness', runtime_ms: _runtimeMs, summary: 'fail: Java version check failed in ' + _runtimeMs + 'ms', requirement_ids: getRequirementIds('tla:account-manager'), metadata: {} }); } catch (e) { process.stderr.write('[run-account-manager-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
const versionOutput = versionResult.stdout + versionResult.stderr;
|
|
99
|
+
const versionMatch = versionOutput.match(/(?:openjdk\s+|java version\s+[""]?)(\d+)/i);
|
|
100
|
+
const javaMajor = versionMatch ? parseInt(versionMatch[1], 10) : 0;
|
|
101
|
+
if (javaMajor < 17) {
|
|
102
|
+
process.stderr.write(
|
|
103
|
+
'[run-account-manager-tlc] Java >=17 required. Found: ' + versionOutput.split('\n')[0] + '\n' +
|
|
104
|
+
'[run-account-manager-tlc] Download Java 17+: https://adoptium.net/\n'
|
|
105
|
+
);
|
|
106
|
+
const _startMs = Date.now();
|
|
107
|
+
const _runtimeMs = 0;
|
|
108
|
+
try { writeCheckResult({ tool: 'run-account-manager-tlc', formalism: 'tla', result: 'fail', check_id: 'tla:account-manager', surface: 'tla', property: 'Account manager quorum state machine — MCAM correctness', runtime_ms: _runtimeMs, summary: 'fail: Java ' + javaMajor + ' < 17 in ' + _runtimeMs + 'ms', requirement_ids: getRequirementIds('tla:account-manager'), metadata: {} }); } catch (e) { process.stderr.write('[run-account-manager-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── 3. Locate tla2tools.jar ──────────────────────────────────────────────────
|
|
113
|
+
const jarPath = path.join(ROOT, '.planning', 'formal', 'tla', 'tla2tools.jar');
|
|
114
|
+
if (!fs.existsSync(jarPath)) {
|
|
115
|
+
process.stderr.write(
|
|
116
|
+
'[run-account-manager-tlc] tla2tools.jar not found at: ' + jarPath + '\n' +
|
|
117
|
+
'[run-account-manager-tlc] Download v1.8.0:\n' +
|
|
118
|
+
' curl -L https://github.com/tlaplus/tlaplus/releases/download/v1.8.0/tla2tools.jar \\\n' +
|
|
119
|
+
' -o .planning/formal/tla/tla2tools.jar\n'
|
|
120
|
+
);
|
|
121
|
+
const _startMs = Date.now();
|
|
122
|
+
const _runtimeMs = 0;
|
|
123
|
+
try { writeCheckResult({ tool: 'run-account-manager-tlc', formalism: 'tla', result: 'fail', check_id: 'tla:account-manager', surface: 'tla', property: 'Account manager quorum state machine — MCAM correctness', runtime_ms: _runtimeMs, summary: 'fail: tla2tools.jar not found in ' + _runtimeMs + 'ms', requirement_ids: getRequirementIds('tla:account-manager'), metadata: {} }); } catch (e) { process.stderr.write('[run-account-manager-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── 4. Invoke TLC ────────────────────────────────────────────────────────────
|
|
128
|
+
const specPath = path.join(ROOT, '.planning', 'formal', 'tla', 'QGSDAccountManager.tla');
|
|
129
|
+
const cfgPath = path.join(ROOT, '.planning', 'formal', 'tla', configName + '.cfg');
|
|
130
|
+
// Use workers=1 for liveness (IdleReachable) — avoids multi-worker liveness bugs in TLC
|
|
131
|
+
const workers = '1';
|
|
132
|
+
|
|
133
|
+
process.stdout.write('[run-account-manager-tlc] Config: ' + configName + ' Workers: ' + workers + '\n');
|
|
134
|
+
process.stdout.write('[run-account-manager-tlc] Spec: ' + specPath + '\n');
|
|
135
|
+
process.stdout.write('[run-account-manager-tlc] Cfg: ' + cfgPath + '\n');
|
|
136
|
+
|
|
137
|
+
const _startMs = Date.now();
|
|
138
|
+
process.stderr.write('[heap] Xms=64m Xmx=' + JAVA_HEAP_MAX + '\n');
|
|
139
|
+
const tlcResult = spawnSync(javaExe, [
|
|
140
|
+
'-Xms64m', '-Xmx' + JAVA_HEAP_MAX,
|
|
141
|
+
'-jar', jarPath,
|
|
142
|
+
'-config', cfgPath,
|
|
143
|
+
'-workers', workers,
|
|
144
|
+
specPath,
|
|
145
|
+
], { encoding: 'utf8', stdio: 'inherit' });
|
|
146
|
+
const _runtimeMs = Date.now() - _startMs;
|
|
147
|
+
|
|
148
|
+
if (tlcResult.error) {
|
|
149
|
+
process.stderr.write('[run-account-manager-tlc] TLC invocation failed: ' + tlcResult.error.message + '\n');
|
|
150
|
+
try { writeCheckResult({ tool: 'run-account-manager-tlc', formalism: 'tla', result: 'fail', check_id: 'tla:account-manager', surface: 'tla', property: 'Account manager quorum state machine — MCAM correctness', runtime_ms: _runtimeMs, summary: 'fail: TLC invocation failed in ' + _runtimeMs + 'ms', requirement_ids: getRequirementIds('tla:account-manager'), metadata: {} }); } catch (e) { process.stderr.write('[run-account-manager-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const passed = (tlcResult.status || 0) === 0;
|
|
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-account-manager-tlc',
|
|
163
|
+
formalism: 'tla',
|
|
164
|
+
result: 'inconclusive',
|
|
165
|
+
check_id: 'tla:account-manager',
|
|
166
|
+
surface: 'tla',
|
|
167
|
+
property: 'Account manager quorum state machine — MCAM correctness',
|
|
168
|
+
runtime_ms: _runtimeMs,
|
|
169
|
+
summary: 'inconclusive: fairness missing in ' + _runtimeMs + 'ms',
|
|
170
|
+
triage_tags: ['needs-fairness'],
|
|
171
|
+
requirement_ids: getRequirementIds('tla:account-manager'),
|
|
172
|
+
metadata: {
|
|
173
|
+
config: configName,
|
|
174
|
+
reason: 'Fairness declaration missing for: ' + missingDeclarations.join(', '),
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
} catch (e) {
|
|
178
|
+
process.stderr.write('[run-account-manager-tlc] Warning: failed to write inconclusive result: ' + e.message + '\n');
|
|
179
|
+
}
|
|
180
|
+
process.stdout.write('[run-account-manager-tlc] Result: inconclusive — fairness declaration missing for: ' + missingDeclarations.join(', ') + '\n');
|
|
181
|
+
process.exit(0);
|
|
182
|
+
}
|
|
183
|
+
try { writeCheckResult({ tool: 'run-account-manager-tlc', formalism: 'tla', result: 'pass', check_id: 'tla:account-manager', surface: 'tla', property: 'Account manager quorum state machine — MCAM correctness', runtime_ms: _runtimeMs, summary: 'pass: ' + configName + ' in ' + _runtimeMs + 'ms', triage_tags: triage_tags, requirement_ids: getRequirementIds('tla:account-manager'), metadata: {} }); } catch (e) { process.stderr.write('[run-account-manager-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
|
|
184
|
+
process.exit(0);
|
|
185
|
+
} else {
|
|
186
|
+
try { writeCheckResult({ tool: 'run-account-manager-tlc', formalism: 'tla', result: 'fail', check_id: 'tla:account-manager', surface: 'tla', property: 'Account manager quorum state machine — MCAM correctness', runtime_ms: _runtimeMs, summary: 'fail: ' + configName + ' in ' + _runtimeMs + 'ms', triage_tags: triage_tags, requirement_ids: getRequirementIds('tla:account-manager'), metadata: {} }); } catch (e) { process.stderr.write('[run-account-manager-tlc] Warning: failed to write check result: ' + e.message + '\n'); }
|
|
187
|
+
process.exit(tlcResult.status || 0);
|
|
188
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// bin/run-account-pool-alloy.cjs
|
|
4
|
+
// Invokes Alloy 6 JAR headless for the QGSD account pool structure spec.
|
|
5
|
+
// Requirements: ALY-AM-01
|
|
6
|
+
//
|
|
7
|
+
// Usage:
|
|
8
|
+
// node bin/run-account-pool-alloy.cjs
|
|
9
|
+
//
|
|
10
|
+
// Checks (defined in .planning/formal/alloy/account-pool-structure.als):
|
|
11
|
+
// AddPreservesValidity — adding to a valid state yields a valid state
|
|
12
|
+
// SwitchPreservesValidity — switching in a valid state yields a valid state
|
|
13
|
+
// RemovePreservesValidity — removing from a valid state yields a valid state
|
|
14
|
+
// SwitchPreservesPool — switch never modifies pool membership
|
|
15
|
+
// RemoveShrinksPool — remove reduces pool size by exactly one
|
|
16
|
+
//
|
|
17
|
+
// Prerequisites:
|
|
18
|
+
// - Java >=17 (https://adoptium.net/)
|
|
19
|
+
// - .planning/formal/alloy/org.alloytools.alloy.dist.jar (see VERIFICATION_TOOLS.md for download)
|
|
20
|
+
|
|
21
|
+
const { spawnSync } = require('child_process');
|
|
22
|
+
const JAVA_HEAP_MAX = process.env.QGSD_JAVA_HEAP_MAX || '512m';
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
const { writeCheckResult } = require('./write-check-result.cjs');
|
|
26
|
+
const { getRequirementIds } = require('./requirement-map.cjs');
|
|
27
|
+
|
|
28
|
+
// ── Resolve project root (--project-root= overrides __dirname-relative) ─────
|
|
29
|
+
let ROOT = path.join(__dirname, '..');
|
|
30
|
+
for (const arg of process.argv) {
|
|
31
|
+
if (arg.startsWith('--project-root=')) ROOT = path.resolve(arg.slice('--project-root='.length));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── 1. Locate Java ───────────────────────────────────────────────────────────
|
|
35
|
+
const JAVA_HOME = process.env.JAVA_HOME;
|
|
36
|
+
let javaExe;
|
|
37
|
+
|
|
38
|
+
if (JAVA_HOME) {
|
|
39
|
+
javaExe = path.join(JAVA_HOME, 'bin', 'java');
|
|
40
|
+
if (!fs.existsSync(javaExe)) {
|
|
41
|
+
process.stderr.write(
|
|
42
|
+
'[run-account-pool-alloy] JAVA_HOME is set but java binary not found at: ' + javaExe + '\n' +
|
|
43
|
+
'[run-account-pool-alloy] Unset JAVA_HOME or fix the path.\n'
|
|
44
|
+
);
|
|
45
|
+
try { writeCheckResult({ tool: 'run-account-pool-alloy', formalism: 'alloy', result: 'fail', check_id: 'alloy:account-pool', surface: 'alloy', property: 'Account pool state machine — slot assignment and release invariants', runtime_ms: 0, summary: 'fail: alloy:account-pool (Java not found)', triage_tags: [], requirement_ids: getRequirementIds('alloy:account-pool'), metadata: {} }); } catch (e) { process.stderr.write('[run-account-pool-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
const probe = spawnSync('java', ['--version'], { encoding: 'utf8' });
|
|
50
|
+
if (probe.error || probe.status !== 0) {
|
|
51
|
+
process.stderr.write(
|
|
52
|
+
'[run-account-pool-alloy] Java not found. Install Java >=17 and set JAVA_HOME.\n' +
|
|
53
|
+
'[run-account-pool-alloy] Download: https://adoptium.net/\n'
|
|
54
|
+
);
|
|
55
|
+
try { writeCheckResult({ tool: 'run-account-pool-alloy', formalism: 'alloy', result: 'fail', check_id: 'alloy:account-pool', surface: 'alloy', property: 'Account pool state machine — slot assignment and release invariants', runtime_ms: 0, summary: 'fail: alloy:account-pool (Java not found)', triage_tags: [], requirement_ids: getRequirementIds('alloy:account-pool'), metadata: {} }); } catch (e) { process.stderr.write('[run-account-pool-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
javaExe = 'java';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── 2. Check Java version >=17 ───────────────────────────────────────────────
|
|
62
|
+
const versionResult = spawnSync(javaExe, ['--version'], { encoding: 'utf8' });
|
|
63
|
+
if (versionResult.error || versionResult.status !== 0) {
|
|
64
|
+
process.stderr.write('[run-account-pool-alloy] Failed to run: ' + javaExe + ' --version\n');
|
|
65
|
+
try { writeCheckResult({ tool: 'run-account-pool-alloy', formalism: 'alloy', result: 'fail', check_id: 'alloy:account-pool', surface: 'alloy', property: 'Account pool state machine — slot assignment and release invariants', runtime_ms: 0, summary: 'fail: alloy:account-pool (version check failed)', triage_tags: [], requirement_ids: getRequirementIds('alloy:account-pool'), metadata: {} }); } catch (e) { process.stderr.write('[run-account-pool-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
const versionOutput = versionResult.stdout + versionResult.stderr;
|
|
69
|
+
const versionMatch = versionOutput.match(/(?:openjdk\s+|java version\s+[""]?)(\d+)/i);
|
|
70
|
+
const javaMajor = versionMatch ? parseInt(versionMatch[1], 10) : 0;
|
|
71
|
+
if (javaMajor < 17) {
|
|
72
|
+
process.stderr.write(
|
|
73
|
+
'[run-account-pool-alloy] Java >=17 required. Found: ' + versionOutput.split('\n')[0] + '\n' +
|
|
74
|
+
'[run-account-pool-alloy] Download Java 17+: https://adoptium.net/\n'
|
|
75
|
+
);
|
|
76
|
+
try { writeCheckResult({ tool: 'run-account-pool-alloy', formalism: 'alloy', result: 'fail', check_id: 'alloy:account-pool', surface: 'alloy', property: 'Account pool state machine — slot assignment and release invariants', runtime_ms: 0, summary: 'fail: alloy:account-pool (Java < 17)', triage_tags: [], requirement_ids: getRequirementIds('alloy:account-pool'), metadata: {} }); } catch (e) { process.stderr.write('[run-account-pool-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── 3. Locate org.alloytools.alloy.dist.jar ──────────────────────────────────
|
|
81
|
+
const jarPath = path.join(ROOT, '.planning', 'formal', 'alloy', 'org.alloytools.alloy.dist.jar');
|
|
82
|
+
if (!fs.existsSync(jarPath)) {
|
|
83
|
+
process.stderr.write(
|
|
84
|
+
'[run-account-pool-alloy] org.alloytools.alloy.dist.jar not found at: ' + jarPath + '\n' +
|
|
85
|
+
'[run-account-pool-alloy] Download Alloy 6.2.0:\n' +
|
|
86
|
+
' curl -L https://github.com/AlloyTools/org.alloytools.alloy/releases/download/v6.2.0/org.alloytools.alloy.dist.jar \\\n' +
|
|
87
|
+
' -o .planning/formal/alloy/org.alloytools.alloy.dist.jar\n'
|
|
88
|
+
);
|
|
89
|
+
try { writeCheckResult({ tool: 'run-account-pool-alloy', formalism: 'alloy', result: 'fail', check_id: 'alloy:account-pool', surface: 'alloy', property: 'Account pool state machine — slot assignment and release invariants', runtime_ms: 0, summary: 'fail: alloy:account-pool (JAR not found)', triage_tags: [], requirement_ids: getRequirementIds('alloy:account-pool'), metadata: {} }); } catch (e) { process.stderr.write('[run-account-pool-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── 4. Locate .planning/formal/alloy/account-pool-structure.als ────────────────────────
|
|
94
|
+
const alsPath = path.join(ROOT, '.planning', 'formal', 'alloy', 'account-pool-structure.als');
|
|
95
|
+
if (!fs.existsSync(alsPath)) {
|
|
96
|
+
process.stderr.write(
|
|
97
|
+
'[run-account-pool-alloy] account-pool-structure.als not found at: ' + alsPath + '\n' +
|
|
98
|
+
'[run-account-pool-alloy] This file should exist in the repository. Check your git status.\n'
|
|
99
|
+
);
|
|
100
|
+
try { writeCheckResult({ tool: 'run-account-pool-alloy', formalism: 'alloy', result: 'fail', check_id: 'alloy:account-pool', surface: 'alloy', property: 'Account pool state machine — slot assignment and release invariants', runtime_ms: 0, summary: 'fail: alloy:account-pool (ALS not found)', triage_tags: [], requirement_ids: getRequirementIds('alloy:account-pool'), metadata: {} }); } catch (e) { process.stderr.write('[run-account-pool-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── 5. Invoke Alloy 6 ────────────────────────────────────────────────────────
|
|
105
|
+
process.stdout.write('[run-account-pool-alloy] ALS: ' + alsPath + '\n');
|
|
106
|
+
process.stdout.write('[run-account-pool-alloy] JAR: ' + jarPath + '\n');
|
|
107
|
+
|
|
108
|
+
const _startMs = Date.now();
|
|
109
|
+
|
|
110
|
+
// Use stdio: 'pipe' so we can scan stdout for counterexamples (Alloy exits 0 even on CEX)
|
|
111
|
+
process.stderr.write('[heap] Xms=64m Xmx=' + JAVA_HEAP_MAX + '\n');
|
|
112
|
+
const alloyResult = spawnSync(javaExe, [
|
|
113
|
+
'-Djava.awt.headless=true',
|
|
114
|
+
'-Xms64m', '-Xmx' + JAVA_HEAP_MAX,
|
|
115
|
+
'-jar', jarPath,
|
|
116
|
+
'exec',
|
|
117
|
+
'--output', '-',
|
|
118
|
+
'--type', 'text',
|
|
119
|
+
'--quiet',
|
|
120
|
+
alsPath,
|
|
121
|
+
], { encoding: 'utf8', stdio: 'pipe' });
|
|
122
|
+
|
|
123
|
+
if (alloyResult.error) {
|
|
124
|
+
process.stderr.write('[run-account-pool-alloy] Alloy invocation failed: ' + alloyResult.error.message + '\n');
|
|
125
|
+
const _runtimeMs = Date.now() - _startMs;
|
|
126
|
+
try { writeCheckResult({ tool: 'run-account-pool-alloy', formalism: 'alloy', result: 'fail', check_id: 'alloy:account-pool', surface: 'alloy', property: 'Account pool state machine — slot assignment and release invariants', runtime_ms: _runtimeMs, summary: 'fail: alloy:account-pool in ' + _runtimeMs + 'ms', triage_tags: _runtimeMs > 60000 ? ['timeout-risk'] : [], requirement_ids: getRequirementIds('alloy:account-pool'), metadata: {} }); } catch (e) { process.stderr.write('[run-account-pool-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── 6. Scan stdout for counterexamples ───────────────────────────────────────
|
|
131
|
+
// Alloy 6 exits 0 even when counterexamples are found. Scan stdout to detect them.
|
|
132
|
+
const stdout = alloyResult.stdout || '';
|
|
133
|
+
const stderr = alloyResult.stderr || '';
|
|
134
|
+
|
|
135
|
+
if (stdout) { process.stdout.write(stdout); }
|
|
136
|
+
if (stderr) { process.stderr.write(stderr); }
|
|
137
|
+
|
|
138
|
+
if (/Counterexample/i.test(stdout)) {
|
|
139
|
+
process.stderr.write(
|
|
140
|
+
'[run-account-pool-alloy] WARNING: Counterexample found in account-pool-structure.als assertion.\n' +
|
|
141
|
+
'[run-account-pool-alloy] This indicates a structural invariant violation — review .planning/formal/alloy/account-pool-structure.als.\n' +
|
|
142
|
+
'[run-account-pool-alloy] Assertions checked: AddPreservesValidity, SwitchPreservesValidity,\n' +
|
|
143
|
+
'[run-account-pool-alloy] RemovePreservesValidity, SwitchPreservesPool, RemoveShrinksPool\n'
|
|
144
|
+
);
|
|
145
|
+
const _runtimeMs = Date.now() - _startMs;
|
|
146
|
+
try { writeCheckResult({ tool: 'run-account-pool-alloy', formalism: 'alloy', result: 'fail', check_id: 'alloy:account-pool', surface: 'alloy', property: 'Account pool state machine — slot assignment and release invariants', runtime_ms: _runtimeMs, summary: 'fail: alloy:account-pool in ' + _runtimeMs + 'ms', triage_tags: _runtimeMs > 60000 ? ['timeout-risk'] : [], requirement_ids: getRequirementIds('alloy:account-pool'), metadata: {} }); } catch (e) { process.stderr.write('[run-account-pool-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (alloyResult.status !== 0) {
|
|
151
|
+
const _runtimeMs = Date.now() - _startMs;
|
|
152
|
+
try { writeCheckResult({ tool: 'run-account-pool-alloy', formalism: 'alloy', result: 'fail', check_id: 'alloy:account-pool', surface: 'alloy', property: 'Account pool state machine — slot assignment and release invariants', runtime_ms: _runtimeMs, summary: 'fail: alloy:account-pool in ' + _runtimeMs + 'ms', triage_tags: _runtimeMs > 60000 ? ['timeout-risk'] : [], requirement_ids: getRequirementIds('alloy:account-pool'), metadata: {} }); } catch (e) { process.stderr.write('[run-account-pool-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
|
|
153
|
+
process.exit(alloyResult.status || 1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const _runtimeMs = Date.now() - _startMs;
|
|
157
|
+
try { writeCheckResult({ tool: 'run-account-pool-alloy', formalism: 'alloy', result: 'pass', check_id: 'alloy:account-pool', surface: 'alloy', property: 'Account pool state machine — slot assignment and release invariants', runtime_ms: _runtimeMs, summary: 'pass: alloy:account-pool in ' + _runtimeMs + 'ms', triage_tags: _runtimeMs > 60000 ? ['timeout-risk'] : [], requirement_ids: getRequirementIds('alloy:account-pool'), metadata: {} }); } catch (e) { process.stderr.write('[run-account-pool-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
|
|
158
|
+
process.exit(0);
|