@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,701 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// bin/run-formal-verify.cjs
|
|
4
|
+
// Master runner: executes ALL formal verification tools and generates ALL formal artifacts.
|
|
5
|
+
//
|
|
6
|
+
// Coverage:
|
|
7
|
+
// Generate (2) — xstate-to-tla.cjs (XState → TLA+, cfg)
|
|
8
|
+
// generate-formal-specs.cjs (XState → Alloy, PRISM)
|
|
9
|
+
// Petri net (2) — generate-petri-net.cjs + render account-manager DOT → SVG
|
|
10
|
+
// TLA+ (10) — MCsafety, MCliveness, MCoscillation, MCconvergence,
|
|
11
|
+
// MCbreaker, MCdeliberation, MCprefilter, MCaccount-manager, MCMCPEnv,
|
|
12
|
+
// MCStopHook
|
|
13
|
+
// Alloy (8) — quorum-votes, scoreboard-recompute, availability-parsing,
|
|
14
|
+
// transcript-scan, install-scope, taxonomy-safety, account-pool-structure,
|
|
15
|
+
// quorum-composition
|
|
16
|
+
// PRISM (3) — quorum, oauth-rotation, mcp-availability
|
|
17
|
+
// CI enforce (4) — check-trace-redaction.cjs, check-trace-schema-drift.cjs, check-liveness-fairness.cjs,
|
|
18
|
+
// validate-traces.cjs
|
|
19
|
+
// UPPAAL (1) — run-uppaal.cjs (quorum-races.xml, empirical timing bounds)
|
|
20
|
+
// Triage (1) — generate-triage-bundle.cjs (diff-report.md + suspects.md)
|
|
21
|
+
// Traceability (3) — generate-traceability-matrix.cjs (requirements <-> properties matrix)
|
|
22
|
+
// check-coverage-guard.cjs (coverage regression guard vs baseline)
|
|
23
|
+
// analyze-state-space.cjs (state-space risk classification per TLA+ model)
|
|
24
|
+
// Registry (N) — custom check commands from model-registry.json
|
|
25
|
+
// ─────────────────────────────────────────────────────────────
|
|
26
|
+
// Total: 34+ steps (dynamic — registry can add more)
|
|
27
|
+
//
|
|
28
|
+
// Usage:
|
|
29
|
+
// node bin/run-formal-verify.cjs # all 28 steps
|
|
30
|
+
// node bin/run-formal-verify.cjs --concurrent # run tool groups in parallel (old behavior)
|
|
31
|
+
// QGSD_FORMAL_CONCURRENT=1 node bin/run-formal-verify.cjs # same via env var
|
|
32
|
+
// node bin/run-formal-verify.cjs --only=generate # source extraction only (2 steps)
|
|
33
|
+
// node bin/run-formal-verify.cjs --only=tla # TLA+ only (10 steps)
|
|
34
|
+
// node bin/run-formal-verify.cjs --only=alloy # Alloy only (8 steps)
|
|
35
|
+
// node bin/run-formal-verify.cjs --only=prism # PRISM only (3 steps)
|
|
36
|
+
// node bin/run-formal-verify.cjs --only=petri # Petri only (2 steps)
|
|
37
|
+
// node bin/run-formal-verify.cjs --only=ci # CI enforcement only (4 steps)
|
|
38
|
+
// node bin/run-formal-verify.cjs --only=uppaal # UPPAAL only (1 step)
|
|
39
|
+
//
|
|
40
|
+
// Behaviour:
|
|
41
|
+
// - Runs steps sequentially; streams child output to stdout/stderr.
|
|
42
|
+
// - Continues on failure; collects pass/fail for every step.
|
|
43
|
+
// - Prints a summary table at the end.
|
|
44
|
+
// - Exits 0 only when every step passes.
|
|
45
|
+
//
|
|
46
|
+
// Prerequisites: see individual runner scripts in bin/.
|
|
47
|
+
|
|
48
|
+
const { spawnSync } = require('child_process');
|
|
49
|
+
const fs = require('fs');
|
|
50
|
+
const path = require('path');
|
|
51
|
+
|
|
52
|
+
const TAG = '[run-formal-verify]';
|
|
53
|
+
const HR = '═'.repeat(64);
|
|
54
|
+
const SEP = '─'.repeat(64);
|
|
55
|
+
|
|
56
|
+
let ROOT = process.cwd();
|
|
57
|
+
|
|
58
|
+
// Parse --project-root (overrides CWD-based ROOT for cross-repo usage)
|
|
59
|
+
for (const arg of process.argv.slice(2)) {
|
|
60
|
+
if (arg.startsWith('--project-root=')) {
|
|
61
|
+
ROOT = path.resolve(arg.slice('--project-root='.length));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Runner picker maps ─────────────────────────────────────────────────────────
|
|
66
|
+
// Maps known QGSD model names to their specialized runners. Unknown models
|
|
67
|
+
// fall back to generic runners (run-tlc.cjs, run-alloy.cjs, run-prism.cjs).
|
|
68
|
+
|
|
69
|
+
const TLA_RUNNER_MAP = {
|
|
70
|
+
'MCoscillation': { script: 'run-oscillation-tlc.cjs', args: (c) => [c] },
|
|
71
|
+
'MCconvergence': { script: 'run-oscillation-tlc.cjs', args: (c) => [c] },
|
|
72
|
+
'MCbreaker': { script: 'run-breaker-tlc.cjs', args: (c) => [c] },
|
|
73
|
+
'MCdeliberation': { script: 'run-protocol-tlc.cjs', args: (c) => [c] },
|
|
74
|
+
'MCprefilter': { script: 'run-protocol-tlc.cjs', args: (c) => [c] },
|
|
75
|
+
'MCaccount-manager': { script: 'run-account-manager-tlc.cjs', args: () => [] },
|
|
76
|
+
'MCStopHook': { script: 'run-stop-hook-tlc.cjs', args: (c) => [c] },
|
|
77
|
+
};
|
|
78
|
+
function pickTLARunner(configName) {
|
|
79
|
+
return TLA_RUNNER_MAP[configName] || { script: 'run-tlc.cjs', args: (c) => [c] };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const ALLOY_RUNNER_MAP = {
|
|
83
|
+
'quorum-votes': { script: 'run-alloy.cjs', args: [] },
|
|
84
|
+
'scoreboard-recompute': { script: 'run-audit-alloy.cjs', args: ['--spec=scoreboard-recompute'] },
|
|
85
|
+
'availability-parsing': { script: 'run-audit-alloy.cjs', args: ['--spec=availability-parsing'] },
|
|
86
|
+
'transcript-scan': { script: 'run-transcript-alloy.cjs', args: ['--spec=transcript-scan'] },
|
|
87
|
+
'install-scope': { script: 'run-installer-alloy.cjs', args: ['--spec=install-scope'] },
|
|
88
|
+
'taxonomy-safety': { script: 'run-installer-alloy.cjs', args: ['--spec=taxonomy-safety'] },
|
|
89
|
+
'account-pool-structure': { script: 'run-account-pool-alloy.cjs', args: [] },
|
|
90
|
+
'quorum-composition': { script: 'run-quorum-composition-alloy.cjs', args: [] },
|
|
91
|
+
};
|
|
92
|
+
function pickAlloyRunner(specName) {
|
|
93
|
+
return (ALLOY_RUNNER_MAP[specName] || { script: 'run-alloy.cjs', args: ['--spec=' + specName] }).script;
|
|
94
|
+
}
|
|
95
|
+
function pickAlloyArgs(specName) {
|
|
96
|
+
return (ALLOY_RUNNER_MAP[specName] || { script: 'run-alloy.cjs', args: ['--spec=' + specName] }).args;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const PRISM_RUNNER_MAP = {
|
|
100
|
+
'quorum': { script: 'run-prism.cjs', args: [] },
|
|
101
|
+
'oauth-rotation': { script: 'run-oauth-rotation-prism.cjs', args: [] },
|
|
102
|
+
'mcp-availability': { script: 'run-prism.cjs', args: ['--model=mcp-availability'] },
|
|
103
|
+
};
|
|
104
|
+
function pickPrismRunner(modelName) {
|
|
105
|
+
return (PRISM_RUNNER_MAP[modelName] || { script: 'run-prism.cjs', args: ['--model=' + modelName] }).script;
|
|
106
|
+
}
|
|
107
|
+
function pickPrismArgs(modelName) {
|
|
108
|
+
return (PRISM_RUNNER_MAP[modelName] || { script: 'run-prism.cjs', args: ['--model=' + modelName] }).args;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── Dynamic model discovery ───────────────────────────────────────────────────
|
|
112
|
+
// Scans ROOT/.planning/formal/{tla,alloy,prism,petri,uppaal}/ and builds step entries.
|
|
113
|
+
// Also reads ROOT/.planning/formal/model-registry.json for:
|
|
114
|
+
// - search_dirs: additional directories to scan for formal model files
|
|
115
|
+
// - models[].check.command: custom shell commands producing type:shell steps
|
|
116
|
+
function discoverModels(root) {
|
|
117
|
+
const discovered = [];
|
|
118
|
+
const formalDir = path.join(root, '.planning', 'formal');
|
|
119
|
+
|
|
120
|
+
// TLA+: scan for *.cfg files in .planning/formal/tla/
|
|
121
|
+
const tlaDir = path.join(formalDir, 'tla');
|
|
122
|
+
if (fs.existsSync(tlaDir)) {
|
|
123
|
+
const cfgFiles = fs.readdirSync(tlaDir).filter(f => f.endsWith('.cfg'));
|
|
124
|
+
for (const cfg of cfgFiles) {
|
|
125
|
+
const configName = cfg.replace('.cfg', '');
|
|
126
|
+
const runner = pickTLARunner(configName);
|
|
127
|
+
discovered.push({
|
|
128
|
+
tool: 'tla',
|
|
129
|
+
id: 'tla:' + configName.toLowerCase(),
|
|
130
|
+
label: 'TLA+ — ' + configName,
|
|
131
|
+
type: 'node',
|
|
132
|
+
script: runner.script,
|
|
133
|
+
args: runner.args(configName),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Alloy: scan for *.als files in .planning/formal/alloy/ (exclude subdirectories, JARs)
|
|
139
|
+
const alloyDir = path.join(formalDir, 'alloy');
|
|
140
|
+
if (fs.existsSync(alloyDir)) {
|
|
141
|
+
const alsFiles = fs.readdirSync(alloyDir).filter(f => f.endsWith('.als'));
|
|
142
|
+
for (const als of alsFiles) {
|
|
143
|
+
const specName = als.replace('.als', '');
|
|
144
|
+
discovered.push({
|
|
145
|
+
tool: 'alloy',
|
|
146
|
+
id: 'alloy:' + specName,
|
|
147
|
+
label: 'Alloy ' + specName,
|
|
148
|
+
type: 'node',
|
|
149
|
+
script: pickAlloyRunner(specName),
|
|
150
|
+
args: pickAlloyArgs(specName),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// PRISM: scan for *.pm files in .planning/formal/prism/
|
|
156
|
+
const prismDir = path.join(formalDir, 'prism');
|
|
157
|
+
if (fs.existsSync(prismDir)) {
|
|
158
|
+
const pmFiles = fs.readdirSync(prismDir).filter(f => f.endsWith('.pm'));
|
|
159
|
+
for (const pm of pmFiles) {
|
|
160
|
+
const modelName = pm.replace('.pm', '');
|
|
161
|
+
discovered.push({
|
|
162
|
+
tool: 'prism',
|
|
163
|
+
id: 'prism:' + modelName,
|
|
164
|
+
label: 'PRISM ' + modelName,
|
|
165
|
+
type: 'node',
|
|
166
|
+
script: pickPrismRunner(modelName),
|
|
167
|
+
args: pickPrismArgs(modelName),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Petri: scan for *.dot files in .planning/formal/petri/
|
|
173
|
+
const petriDir = path.join(formalDir, 'petri');
|
|
174
|
+
if (fs.existsSync(petriDir)) {
|
|
175
|
+
const dotFiles = fs.readdirSync(petriDir).filter(f => f.endsWith('.dot'));
|
|
176
|
+
for (const dot of dotFiles) {
|
|
177
|
+
const name = dot.replace('.dot', '');
|
|
178
|
+
discovered.push({
|
|
179
|
+
tool: 'petri',
|
|
180
|
+
id: 'petri:' + name,
|
|
181
|
+
label: 'Petri ' + name + ' — render DOT -> SVG',
|
|
182
|
+
type: 'wasm-dot',
|
|
183
|
+
dot: dot,
|
|
184
|
+
svg: dot.replace('.dot', '.svg'),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// UPPAAL: scan for *.xml files in .planning/formal/uppaal/
|
|
190
|
+
const uppaalDir = path.join(formalDir, 'uppaal');
|
|
191
|
+
if (fs.existsSync(uppaalDir)) {
|
|
192
|
+
const xmlFiles = fs.readdirSync(uppaalDir).filter(f => f.endsWith('.xml'));
|
|
193
|
+
for (const xml of xmlFiles) {
|
|
194
|
+
discovered.push({
|
|
195
|
+
tool: 'uppaal',
|
|
196
|
+
id: 'uppaal:' + xml.replace('.xml', ''),
|
|
197
|
+
label: 'UPPAAL ' + xml.replace('.xml', ''),
|
|
198
|
+
type: 'node',
|
|
199
|
+
script: 'run-uppaal.cjs',
|
|
200
|
+
args: [],
|
|
201
|
+
nonCritical: true,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── Registry-driven discovery ────────────────────────────────────────────
|
|
207
|
+
// Read model-registry.json for search_dirs and check.command entries.
|
|
208
|
+
// Fail-open: if missing or malformed, log warning and continue.
|
|
209
|
+
let registry = null;
|
|
210
|
+
const registryPath = path.join(root, '.planning', 'formal', 'model-registry.json');
|
|
211
|
+
try {
|
|
212
|
+
const raw = fs.readFileSync(registryPath, 'utf8');
|
|
213
|
+
registry = JSON.parse(raw);
|
|
214
|
+
} catch (err) {
|
|
215
|
+
process.stderr.write(TAG + ' Warning: could not read model-registry.json: ' + err.message + '\n');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (registry && Array.isArray(registry.search_dirs)) {
|
|
219
|
+
for (const dir of registry.search_dirs) {
|
|
220
|
+
const resolvedDir = path.resolve(root, dir);
|
|
221
|
+
if (!fs.existsSync(resolvedDir)) continue;
|
|
222
|
+
|
|
223
|
+
const files = fs.readdirSync(resolvedDir);
|
|
224
|
+
|
|
225
|
+
// TLA+: *.cfg
|
|
226
|
+
for (const f of files.filter(f => f.endsWith('.cfg'))) {
|
|
227
|
+
const configName = f.replace('.cfg', '');
|
|
228
|
+
const runner = pickTLARunner(configName);
|
|
229
|
+
const stepId = ('tla:' + path.join(dir, configName).replace(/\\/g, '/')).toLowerCase();
|
|
230
|
+
discovered.push({
|
|
231
|
+
tool: 'tla',
|
|
232
|
+
id: stepId,
|
|
233
|
+
label: 'TLA+ — ' + dir + '/' + configName,
|
|
234
|
+
type: 'node',
|
|
235
|
+
script: runner.script,
|
|
236
|
+
args: runner.args(configName),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Alloy: *.als
|
|
241
|
+
for (const f of files.filter(f => f.endsWith('.als'))) {
|
|
242
|
+
const specName = f.replace('.als', '');
|
|
243
|
+
const stepId = ('alloy:' + path.join(dir, specName).replace(/\\/g, '/')).toLowerCase();
|
|
244
|
+
discovered.push({
|
|
245
|
+
tool: 'alloy',
|
|
246
|
+
id: stepId,
|
|
247
|
+
label: 'Alloy ' + dir + '/' + specName,
|
|
248
|
+
type: 'node',
|
|
249
|
+
script: pickAlloyRunner(specName),
|
|
250
|
+
args: pickAlloyArgs(specName),
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// PRISM: *.pm
|
|
255
|
+
for (const f of files.filter(f => f.endsWith('.pm'))) {
|
|
256
|
+
const modelName = f.replace('.pm', '');
|
|
257
|
+
const stepId = ('prism:' + path.join(dir, modelName).replace(/\\/g, '/')).toLowerCase();
|
|
258
|
+
discovered.push({
|
|
259
|
+
tool: 'prism',
|
|
260
|
+
id: stepId,
|
|
261
|
+
label: 'PRISM ' + dir + '/' + modelName,
|
|
262
|
+
type: 'node',
|
|
263
|
+
script: pickPrismRunner(modelName),
|
|
264
|
+
args: pickPrismArgs(modelName),
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Petri: *.dot
|
|
269
|
+
for (const f of files.filter(f => f.endsWith('.dot'))) {
|
|
270
|
+
const name = f.replace('.dot', '');
|
|
271
|
+
const stepId = ('petri:' + path.join(dir, name).replace(/\\/g, '/')).toLowerCase();
|
|
272
|
+
discovered.push({
|
|
273
|
+
tool: 'petri',
|
|
274
|
+
id: stepId,
|
|
275
|
+
label: 'Petri ' + dir + '/' + name + ' — render DOT -> SVG',
|
|
276
|
+
type: 'wasm-dot',
|
|
277
|
+
dot: f,
|
|
278
|
+
svg: f.replace('.dot', '.svg'),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// UPPAAL: *.xml
|
|
283
|
+
for (const f of files.filter(f => f.endsWith('.xml'))) {
|
|
284
|
+
const stepId = ('uppaal:' + path.join(dir, f.replace('.xml', '')).replace(/\\/g, '/')).toLowerCase();
|
|
285
|
+
discovered.push({
|
|
286
|
+
tool: 'uppaal',
|
|
287
|
+
id: stepId,
|
|
288
|
+
label: 'UPPAAL ' + dir + '/' + f.replace('.xml', ''),
|
|
289
|
+
type: 'node',
|
|
290
|
+
script: 'run-uppaal.cjs',
|
|
291
|
+
args: [],
|
|
292
|
+
nonCritical: true,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Registry check.command entries → type:shell steps
|
|
299
|
+
if (registry && registry.models && typeof registry.models === 'object') {
|
|
300
|
+
for (const [modelPath, entry] of Object.entries(registry.models)) {
|
|
301
|
+
if (entry && entry.check && typeof entry.check.command === 'string') {
|
|
302
|
+
discovered.push({
|
|
303
|
+
tool: 'registry',
|
|
304
|
+
id: 'registry:' + modelPath,
|
|
305
|
+
label: 'Registry check — ' + modelPath,
|
|
306
|
+
type: 'shell',
|
|
307
|
+
command: entry.check.command,
|
|
308
|
+
config: entry.check.config || null,
|
|
309
|
+
cwd: root,
|
|
310
|
+
nonCritical: entry.check.nonCritical || false,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return discovered;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ── Step registry ─────────────────────────────────────────────────────────────
|
|
320
|
+
//
|
|
321
|
+
// type: 'node' — run node bin/<script> <args...>
|
|
322
|
+
// type: 'wasm-dot' — render .planning/formal/petri/<dot> → .planning/formal/petri/<svg>
|
|
323
|
+
// via @hpcc-js/wasm-graphviz (async)
|
|
324
|
+
//
|
|
325
|
+
// STATIC_STEPS: always run (generate, CI enforcement, triage, traceability).
|
|
326
|
+
// Dynamic steps are discovered from ROOT/.planning/formal/{tla,alloy,prism,petri,uppaal}/.
|
|
327
|
+
const STATIC_STEPS = [
|
|
328
|
+
// ─ Source extraction — must run first so generated specs are fresh ──────────
|
|
329
|
+
{
|
|
330
|
+
tool: 'generate', id: 'generate:tla-from-xstate',
|
|
331
|
+
label: 'Generate TLA+ spec (QGSDQuorum_xstate.tla) + TLC model config from XState machine (xstate-to-tla)',
|
|
332
|
+
type: 'node', script: 'xstate-to-tla.cjs',
|
|
333
|
+
args: ['src/machines/qgsd-workflow.machine.ts', '--module=QGSDQuorum', '--config=.planning/formal/tla/guards/qgsd-workflow.json'],
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
tool: 'generate', id: 'generate:alloy-prism-specs',
|
|
337
|
+
label: 'Generate Alloy + PRISM models from XState machine (generate-formal-specs)',
|
|
338
|
+
type: 'node', script: 'generate-formal-specs.cjs', args: [],
|
|
339
|
+
},
|
|
340
|
+
|
|
341
|
+
// ─ Petri net generator (produces DOT files — discovery handles rendering) ──
|
|
342
|
+
{
|
|
343
|
+
tool: 'petri', id: 'petri:quorum',
|
|
344
|
+
label: 'Petri quorum — generate DOT + render SVG',
|
|
345
|
+
type: 'node', script: 'generate-petri-net.cjs', args: [],
|
|
346
|
+
},
|
|
347
|
+
|
|
348
|
+
// ─ CI enforcement — redaction + schema drift ──────────────────────────────
|
|
349
|
+
{
|
|
350
|
+
tool: 'ci', id: 'ci:trace-redaction',
|
|
351
|
+
label: 'Trace redaction enforcement (check-trace-redaction.cjs)',
|
|
352
|
+
type: 'node', script: 'check-trace-redaction.cjs', args: [],
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
tool: 'ci', id: 'ci:trace-schema-drift',
|
|
356
|
+
label: 'Trace schema drift guard (check-trace-schema-drift.cjs)',
|
|
357
|
+
type: 'node', script: 'check-trace-schema-drift.cjs', args: [],
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
tool: 'ci', id: 'ci:liveness-fairness-lint',
|
|
361
|
+
label: 'Liveness-fairness lint — detect liveness properties without fairness declarations (LIVE-01, LIVE-02)',
|
|
362
|
+
type: 'node', script: 'check-liveness-fairness.cjs', args: [],
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
tool: 'ci', id: 'ci:conformance-traces',
|
|
366
|
+
label: 'Conformance trace validation — XState machine replay with evidence confidence (EVID-01, EVID-02)',
|
|
367
|
+
type: 'node', script: 'validate-traces.cjs', args: [],
|
|
368
|
+
},
|
|
369
|
+
|
|
370
|
+
// ─ Triage bundle ─────────────────────────────────────────────────────────
|
|
371
|
+
{
|
|
372
|
+
tool: 'ci', id: 'ci:triage-bundle',
|
|
373
|
+
label: 'Generate triage bundle — diff-report.md + suspects.md (generate-triage-bundle)',
|
|
374
|
+
type: 'node', script: 'generate-triage-bundle.cjs', args: [],
|
|
375
|
+
nonCritical: true,
|
|
376
|
+
},
|
|
377
|
+
|
|
378
|
+
// ─ Traceability matrix ─────────────────────────────────────────────────────
|
|
379
|
+
{
|
|
380
|
+
tool: 'traceability', id: 'traceability:matrix',
|
|
381
|
+
label: 'Generate traceability matrix (requirements <-> formal properties)',
|
|
382
|
+
type: 'node', script: 'generate-traceability-matrix.cjs', args: ['--quiet'],
|
|
383
|
+
nonCritical: true,
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
tool: 'traceability', id: 'traceability:coverage-guard',
|
|
387
|
+
label: 'Check formal coverage regression against baseline',
|
|
388
|
+
type: 'node', script: 'check-coverage-guard.cjs', args: ['--quiet'],
|
|
389
|
+
nonCritical: true,
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
tool: 'traceability', id: 'traceability:state-space',
|
|
393
|
+
label: 'State-space analysis (risk classification per TLA+ model)',
|
|
394
|
+
type: 'node', script: 'analyze-state-space.cjs', args: [],
|
|
395
|
+
nonCritical: true,
|
|
396
|
+
},
|
|
397
|
+
];
|
|
398
|
+
|
|
399
|
+
// Discover dynamic model steps from ROOT/.planning/formal/
|
|
400
|
+
const dynamicSteps = discoverModels(ROOT);
|
|
401
|
+
|
|
402
|
+
// Deduplicate: if a dynamic step has the same id as a static step, skip it
|
|
403
|
+
const staticIds = new Set(STATIC_STEPS.map(s => s.id));
|
|
404
|
+
const uniqueDynamicSteps = dynamicSteps.filter(s => !staticIds.has(s.id));
|
|
405
|
+
|
|
406
|
+
const STEPS = [...STATIC_STEPS, ...uniqueDynamicSteps];
|
|
407
|
+
|
|
408
|
+
process.stdout.write(TAG + ' Static steps: ' + STATIC_STEPS.length + '\n');
|
|
409
|
+
process.stdout.write(TAG + ' Discovered models: ' + uniqueDynamicSteps.length + '\n');
|
|
410
|
+
|
|
411
|
+
// ── CLI filter ────────────────────────────────────────────────────────────────
|
|
412
|
+
const argv = process.argv.slice(2);
|
|
413
|
+
const onlyArg = argv.find(a => a.startsWith('--only='));
|
|
414
|
+
const only = onlyArg ? onlyArg.split('=')[1] : null;
|
|
415
|
+
const concurrent = argv.includes('--concurrent') || process.env.QGSD_FORMAL_CONCURRENT === '1';
|
|
416
|
+
|
|
417
|
+
const steps = only
|
|
418
|
+
? STEPS.filter(s => s.tool === only || s.id === only)
|
|
419
|
+
: STEPS;
|
|
420
|
+
|
|
421
|
+
if (only && steps.length === 0) {
|
|
422
|
+
process.stderr.write(
|
|
423
|
+
TAG + ' Unknown --only value: ' + only + '\n' +
|
|
424
|
+
TAG + ' Valid values: tla, alloy, prism, petri, generate, ci, uppaal, registry, or a step id\n'
|
|
425
|
+
);
|
|
426
|
+
process.exit(1);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ── Result tracker ────────────────────────────────────────────────────────────
|
|
430
|
+
const results = []; // { id, label, passed, note }
|
|
431
|
+
|
|
432
|
+
function record(id, label, passed, note, nonCritical) {
|
|
433
|
+
results.push({ id, label, passed, note: note || '', nonCritical: !!nonCritical });
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ── Step execution ────────────────────────────────────────────────────────────
|
|
437
|
+
function runNodeStep(step) {
|
|
438
|
+
const scriptPath = path.join(__dirname, step.script);
|
|
439
|
+
if (!fs.existsSync(scriptPath)) {
|
|
440
|
+
process.stderr.write(TAG + ' Script not found: ' + scriptPath + '\n');
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
// Auto-forward --project-root to child scripts
|
|
444
|
+
const childArgs = [...step.args];
|
|
445
|
+
if (!childArgs.some(a => a.startsWith('--project-root='))) {
|
|
446
|
+
childArgs.push('--project-root=' + ROOT);
|
|
447
|
+
}
|
|
448
|
+
const result = spawnSync(process.execPath, [scriptPath, ...childArgs], {
|
|
449
|
+
stdio: 'inherit',
|
|
450
|
+
encoding: 'utf8',
|
|
451
|
+
cwd: ROOT,
|
|
452
|
+
env: { ...process.env, CHECK_RESULTS_ROOT: ROOT },
|
|
453
|
+
});
|
|
454
|
+
if (result.error) {
|
|
455
|
+
process.stderr.write(TAG + ' Launch error: ' + result.error.message + '\n');
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
return result.status === 0;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function runShellStep(step) {
|
|
462
|
+
// NOTE: command.split(/\s+/) is a known limitation — quoted arguments
|
|
463
|
+
// with spaces (e.g., 'echo "hello world"') will be split incorrectly.
|
|
464
|
+
// Future enhancement: accept command as an array format for complex args.
|
|
465
|
+
const parts = step.command.split(/\s+/);
|
|
466
|
+
const cmd = parts[0];
|
|
467
|
+
const args = parts.slice(1);
|
|
468
|
+
// Substitute {{config}} placeholder if config is set
|
|
469
|
+
const resolvedArgs = step.config
|
|
470
|
+
? args.map(a => a.replace('{{config}}', step.config))
|
|
471
|
+
: args;
|
|
472
|
+
const result = spawnSync(cmd, resolvedArgs, {
|
|
473
|
+
stdio: 'inherit',
|
|
474
|
+
encoding: 'utf8',
|
|
475
|
+
cwd: step.cwd || ROOT,
|
|
476
|
+
env: { ...process.env, CHECK_RESULTS_ROOT: ROOT },
|
|
477
|
+
});
|
|
478
|
+
if (result.error) {
|
|
479
|
+
process.stderr.write(TAG + ' Shell launch error: ' + result.error.message + '\n');
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
return result.status === 0;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async function runWasmDotStep(step) {
|
|
486
|
+
const petriDir = path.join(ROOT, '.planning', 'formal', 'petri');
|
|
487
|
+
const dotPath = path.join(petriDir, step.dot);
|
|
488
|
+
const svgPath = path.join(petriDir, step.svg);
|
|
489
|
+
|
|
490
|
+
if (!fs.existsSync(dotPath)) {
|
|
491
|
+
process.stderr.write(TAG + ' DOT source not found: ' + dotPath + '\n');
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const dotContent = fs.readFileSync(dotPath, 'utf8');
|
|
496
|
+
|
|
497
|
+
let Graphviz;
|
|
498
|
+
try {
|
|
499
|
+
({ Graphviz } = await import('@hpcc-js/wasm-graphviz'));
|
|
500
|
+
} catch (_) {
|
|
501
|
+
process.stderr.write(
|
|
502
|
+
TAG + ' @hpcc-js/wasm-graphviz not installed.\n' +
|
|
503
|
+
TAG + ' Run: npm install --save-dev @hpcc-js/wasm-graphviz\n'
|
|
504
|
+
);
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
try {
|
|
509
|
+
const graphviz = await Graphviz.load();
|
|
510
|
+
const svg = graphviz.dot(dotContent);
|
|
511
|
+
fs.writeFileSync(svgPath, svg);
|
|
512
|
+
process.stdout.write(TAG + ' SVG written: ' + svgPath + '\n');
|
|
513
|
+
return true;
|
|
514
|
+
} catch (err) {
|
|
515
|
+
process.stderr.write(TAG + ' SVG render failed: ' + err.message + '\n');
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// ── Group runner — executes steps sequentially within a group ─────────────────
|
|
521
|
+
async function runGroup(groupSteps) {
|
|
522
|
+
for (const step of groupSteps) {
|
|
523
|
+
process.stdout.write(TAG + ' ' + SEP + '\n');
|
|
524
|
+
process.stdout.write(TAG + ' [' + step.id + '] ' + step.label + '\n');
|
|
525
|
+
process.stdout.write(TAG + ' ' + SEP + '\n');
|
|
526
|
+
|
|
527
|
+
let passed = false;
|
|
528
|
+
if (step.type === 'node') {
|
|
529
|
+
passed = runNodeStep(step);
|
|
530
|
+
} else if (step.type === 'wasm-dot') {
|
|
531
|
+
passed = await runWasmDotStep(step);
|
|
532
|
+
} else if (step.type === 'shell') {
|
|
533
|
+
passed = runShellStep(step);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const mark = passed ? '✓' : '✗';
|
|
537
|
+
process.stdout.write('\n' + TAG + ' ' + mark + ' ' + step.id + '\n\n');
|
|
538
|
+
record(step.id, step.label, passed, undefined, step.nonCritical);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// ── runOnce() — executes the full pipeline once ───────────────────────────────
|
|
543
|
+
// Returns the count of failed steps (0 = all passed).
|
|
544
|
+
// Does NOT call process.exit() — the caller decides (non-watch exits, watch loops).
|
|
545
|
+
async function runOnce() {
|
|
546
|
+
// Reset results array so watch-mode re-runs start clean
|
|
547
|
+
results.length = 0;
|
|
548
|
+
// Truncate NDJSON file — fresh run (UNIF-02)
|
|
549
|
+
const ndjsonPath = path.join(ROOT, '.planning', 'formal', 'check-results.ndjson');
|
|
550
|
+
fs.writeFileSync(ndjsonPath, '', 'utf8');
|
|
551
|
+
|
|
552
|
+
process.stdout.write(TAG + ' ' + HR + '\n');
|
|
553
|
+
process.stdout.write(TAG + ' QGSD Formal Verification Suite\n');
|
|
554
|
+
if (only) {
|
|
555
|
+
process.stdout.write(TAG + ' Filter: --only=' + only + '\n');
|
|
556
|
+
}
|
|
557
|
+
process.stdout.write(TAG + ' Steps: ' + steps.length + '\n');
|
|
558
|
+
process.stdout.write(TAG + ' ' + HR + '\n\n');
|
|
559
|
+
|
|
560
|
+
const startMs = Date.now();
|
|
561
|
+
|
|
562
|
+
// ── Phase 1: Generate (sequential prerequisite) ────────────────────────────
|
|
563
|
+
const generateSteps = steps.filter(s => s.tool === 'generate');
|
|
564
|
+
const toolSteps = steps.filter(s => s.tool !== 'generate' && s.tool !== 'traceability');
|
|
565
|
+
const postSteps = steps.filter(s => s.tool === 'traceability');
|
|
566
|
+
|
|
567
|
+
if (generateSteps.length > 0) {
|
|
568
|
+
process.stdout.write(TAG + ' Phase 1: Running generate steps sequentially...\n\n');
|
|
569
|
+
await runGroup(generateSteps);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// ── Phase 2: Tool groups (sequential by default, --concurrent for parallel) ─
|
|
573
|
+
if (toolSteps.length > 0) {
|
|
574
|
+
const toolGroupNames = [...new Set(toolSteps.map(s => s.tool))];
|
|
575
|
+
if (concurrent) {
|
|
576
|
+
process.stdout.write(TAG + ' Phase 2: Running tool groups concurrently: ' + toolGroupNames.join(', ') + '\n\n');
|
|
577
|
+
await Promise.all(
|
|
578
|
+
toolGroupNames.map(tool => runGroup(toolSteps.filter(s => s.tool === tool)))
|
|
579
|
+
);
|
|
580
|
+
} else {
|
|
581
|
+
process.stdout.write(TAG + ' Phase 2: Running tool groups sequentially: ' + toolGroupNames.join(', ') + '\n\n');
|
|
582
|
+
for (const tool of toolGroupNames) {
|
|
583
|
+
await runGroup(toolSteps.filter(s => s.tool === tool));
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// ── Phase 3: Post-processing (needs fully populated check-results.ndjson) ──
|
|
589
|
+
if (postSteps.length > 0) {
|
|
590
|
+
process.stdout.write(TAG + ' Phase 3: Post-processing (traceability matrix)...\n\n');
|
|
591
|
+
await runGroup(postSteps);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// ── Summary ─────────────────────────────────────────────────────────────────
|
|
595
|
+
const passed = results.filter(r => r.passed).length;
|
|
596
|
+
const failed = results.filter(r => !r.passed && !r.nonCritical).length;
|
|
597
|
+
|
|
598
|
+
process.stdout.write(TAG + ' ' + HR + '\n');
|
|
599
|
+
process.stdout.write(TAG + ' SUMMARY — ' + passed + '/' + results.length + ' passed\n');
|
|
600
|
+
process.stdout.write(TAG + ' ' + HR + '\n');
|
|
601
|
+
|
|
602
|
+
for (const r of results) {
|
|
603
|
+
const mark = r.passed ? '✓' : '✗';
|
|
604
|
+
const extra = r.note ? ' (' + r.note + ')' : '';
|
|
605
|
+
process.stdout.write(TAG + ' ' + mark + ' ' + r.id.padEnd(30) + r.label + extra + '\n');
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
process.stdout.write(TAG + ' ' + HR + '\n');
|
|
609
|
+
|
|
610
|
+
const elapsedMs = Date.now() - startMs;
|
|
611
|
+
const elapsedSec = (elapsedMs / 1000).toFixed(1);
|
|
612
|
+
process.stdout.write(TAG + ' Wall-clock: ' + elapsedSec + 's (' + elapsedMs + 'ms)\n');
|
|
613
|
+
process.stdout.write(TAG + ' ' + HR + '\n');
|
|
614
|
+
|
|
615
|
+
// NDJSON-based summary (UNIF-03)
|
|
616
|
+
try {
|
|
617
|
+
const ndjsonLines = fs.readFileSync(ndjsonPath, 'utf8')
|
|
618
|
+
.split('\n').filter(l => l.trim().length > 0);
|
|
619
|
+
const checkResults = ndjsonLines.map(l => JSON.parse(l));
|
|
620
|
+
const ndjsonFailed = checkResults.filter(r => r.result === 'fail').length;
|
|
621
|
+
const ndjsonPassed = checkResults.filter(r => r.result === 'pass').length;
|
|
622
|
+
const ndjsonOther = checkResults.length - ndjsonFailed - ndjsonPassed;
|
|
623
|
+
process.stdout.write(
|
|
624
|
+
TAG + ' check-results.ndjson: ' + ndjsonPassed + ' pass, ' +
|
|
625
|
+
ndjsonFailed + ' fail' +
|
|
626
|
+
(ndjsonOther > 0 ? ', ' + ndjsonOther + ' warn/inconclusive' : '') + '\n'
|
|
627
|
+
);
|
|
628
|
+
} catch (err) {
|
|
629
|
+
process.stderr.write(TAG + ' Warning: could not read check-results.ndjson: ' + err.message + '\n');
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return failed;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// ── Entry point ───────────────────────────────────────────────────────────────
|
|
636
|
+
const watchArg = argv.includes('--watch');
|
|
637
|
+
|
|
638
|
+
if (watchArg) {
|
|
639
|
+
// ── Watch mode ─────────────────────────────────────────────────────────────
|
|
640
|
+
// machineDir uses process.cwd() so tests can point the watcher at a tmpDir
|
|
641
|
+
// by spawning with a custom cwd. __dirname-relative paths would always point
|
|
642
|
+
// to the real repo's src/machines/ regardless of spawn cwd, breaking isolation.
|
|
643
|
+
const machineDir = path.join(process.cwd(), 'src', 'machines');
|
|
644
|
+
const machineName = 'qgsd-workflow.machine.ts';
|
|
645
|
+
let debounceTimer = null;
|
|
646
|
+
let running = false; // concurrent-run guard
|
|
647
|
+
let watcher = null;
|
|
648
|
+
|
|
649
|
+
process.stdout.write(TAG + ' ' + HR + '\n');
|
|
650
|
+
process.stdout.write(TAG + ' Watch mode enabled\n');
|
|
651
|
+
process.stdout.write(TAG + ' Watching: ' + path.join(machineDir, machineName) + '\n');
|
|
652
|
+
process.stdout.write(TAG + ' Press Ctrl+C to stop.\n');
|
|
653
|
+
process.stdout.write(TAG + ' Tip: use --only=generate for faster feedback, --concurrent for parallel tool groups.\n');
|
|
654
|
+
process.stdout.write(TAG + ' ' + HR + '\n\n');
|
|
655
|
+
|
|
656
|
+
// Existence check — fail fast if invoked from wrong directory
|
|
657
|
+
if (!fs.existsSync(machineDir)) {
|
|
658
|
+
process.stderr.write(TAG + ' Error: machine directory not found: ' + machineDir + '\n');
|
|
659
|
+
process.stderr.write(TAG + ' Run --watch from the project root (where src/machines/ exists).\n');
|
|
660
|
+
process.exit(1);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Initial run
|
|
664
|
+
runOnce().catch(err => process.stderr.write(TAG + ' Error: ' + err.message + '\n'));
|
|
665
|
+
|
|
666
|
+
// Watch parent directory — NOT the file directly.
|
|
667
|
+
// On macOS, watching a file dies after first rename (editor atomic-write pattern).
|
|
668
|
+
watcher = fs.watch(machineDir, (eventType, filename) => {
|
|
669
|
+
if (filename === machineName) {
|
|
670
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
671
|
+
debounceTimer = setTimeout(() => {
|
|
672
|
+
if (running) return;
|
|
673
|
+
running = true;
|
|
674
|
+
process.stdout.write('\n' + TAG + ' ' + HR + '\n');
|
|
675
|
+
process.stdout.write(TAG + ' Change detected — re-running verification\n');
|
|
676
|
+
process.stdout.write(TAG + ' ' + new Date().toISOString() + '\n');
|
|
677
|
+
process.stdout.write(TAG + ' ' + HR + '\n\n');
|
|
678
|
+
runOnce()
|
|
679
|
+
.catch(err => process.stderr.write(TAG + ' Error: ' + err.message + '\n'))
|
|
680
|
+
.finally(() => { running = false; });
|
|
681
|
+
}, 300);
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
process.on('SIGINT', () => {
|
|
686
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
687
|
+
if (watcher) watcher.close();
|
|
688
|
+
process.stdout.write('\n' + TAG + ' Exiting watch mode.\n');
|
|
689
|
+
process.exit(0);
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
} else {
|
|
693
|
+
// ── Non-watch mode: original behavior ──────────────────────────────────────
|
|
694
|
+
runOnce().then(failed => {
|
|
695
|
+
if (failed > 0) {
|
|
696
|
+
process.stderr.write(TAG + ' ' + failed + ' step(s) failed.\n');
|
|
697
|
+
process.exit(1);
|
|
698
|
+
}
|
|
699
|
+
process.exit(0);
|
|
700
|
+
});
|
|
701
|
+
}
|