@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,360 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// bin/check-spec-sync.cjs
|
|
4
|
+
// Verifies that formal specs stay in sync with the XState machine.
|
|
5
|
+
//
|
|
6
|
+
// The XState machine (src/machines/qgsd-workflow.machine.ts) is the SOURCE OF TRUTH.
|
|
7
|
+
// Formal specs must mirror it — not the other way around.
|
|
8
|
+
//
|
|
9
|
+
// Checks:
|
|
10
|
+
// 1. State names in QGSDQuorum.tla TypeOK match the XState machine states
|
|
11
|
+
// 2. MaxDeliberation in MCsafety.cfg and MCliveness.cfg matches the XState context default
|
|
12
|
+
// 3. Initial state in QGSDQuorum.tla Init matches the XState initial state
|
|
13
|
+
// 4. Alloy .als files do not reference state names or guard names not in XState machine
|
|
14
|
+
// 5. Guard names in XState machine match keys in .planning/formal/tla/guards/qgsd-workflow.json (bidirectional)
|
|
15
|
+
//
|
|
16
|
+
// Exit 0 = in sync; Exit 1 = drift detected.
|
|
17
|
+
// Usage: node bin/check-spec-sync.cjs [--tla-path=<path>] [--guards-path=<path>]
|
|
18
|
+
//
|
|
19
|
+
// CLI overrides (for fixture-based tests):
|
|
20
|
+
// --tla-path=<abs-path> Override path to QGSDQuorum.tla (default: .planning/formal/tla/QGSDQuorum.tla)
|
|
21
|
+
// --guards-path=<abs-path> Override path to guards JSON (default: .planning/formal/tla/guards/qgsd-workflow.json)
|
|
22
|
+
|
|
23
|
+
const { buildSync } = require('esbuild');
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const os = require('os');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
|
|
28
|
+
const ROOT = path.join(__dirname, '..');
|
|
29
|
+
|
|
30
|
+
// ── CLI overrides (for fixture-based drift injection tests) ───────────────────
|
|
31
|
+
// These allow tests to substitute temp fixture files without modifying the repo.
|
|
32
|
+
const tlaPathOverride = process.argv.find(a => a.startsWith('--tla-path='));
|
|
33
|
+
const guardsPathOverride = process.argv.find(a => a.startsWith('--guards-path='));
|
|
34
|
+
|
|
35
|
+
// Resolved paths (absolute)
|
|
36
|
+
const TLA_ABS_PATH = tlaPathOverride
|
|
37
|
+
? tlaPathOverride.slice('--tla-path='.length)
|
|
38
|
+
: path.join(ROOT, '.planning', 'formal', 'tla', 'QGSDQuorum.tla');
|
|
39
|
+
|
|
40
|
+
const GUARDS_ABS_PATH = guardsPathOverride
|
|
41
|
+
? guardsPathOverride.slice('--guards-path='.length)
|
|
42
|
+
: path.join(ROOT, '.planning', 'formal', 'tla', 'guards', 'qgsd-workflow.json');
|
|
43
|
+
|
|
44
|
+
// ── Load files ───────────────────────────────────────────────────────────────
|
|
45
|
+
function load(rel) {
|
|
46
|
+
const abs = path.join(ROOT, rel);
|
|
47
|
+
if (!fs.existsSync(abs)) {
|
|
48
|
+
process.stderr.write('[check-spec-sync] File not found: ' + rel + '\n');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
return fs.readFileSync(abs, 'utf8');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function loadAbs(abs) {
|
|
55
|
+
if (!fs.existsSync(abs)) {
|
|
56
|
+
process.stderr.write('[check-spec-sync] File not found: ' + abs + '\n');
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
return fs.readFileSync(abs, 'utf8');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const tlaSrc = loadAbs(TLA_ABS_PATH);
|
|
63
|
+
const safetyCfg = load('.planning/formal/tla/MCsafety.cfg');
|
|
64
|
+
const livenessCfg = load('.planning/formal/tla/MCliveness.cfg');
|
|
65
|
+
|
|
66
|
+
// ── 1. Extract XState facts using esbuild+require() ────────────────────────────
|
|
67
|
+
// Compile the TypeScript machine to a temporary CJS bundle, then require() it.
|
|
68
|
+
// This gives us the live machine object — no regex parsing of raw source.
|
|
69
|
+
|
|
70
|
+
const MACHINE_FILE = path.join(ROOT, 'src/machines/qgsd-workflow.machine.ts');
|
|
71
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'check-spec-sync-'));
|
|
72
|
+
const tmpBundle = path.join(tmpDir, 'machine.cjs');
|
|
73
|
+
|
|
74
|
+
let machineObj = null;
|
|
75
|
+
let xstateExtractError = null;
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
buildSync({
|
|
79
|
+
entryPoints: [MACHINE_FILE],
|
|
80
|
+
bundle: true,
|
|
81
|
+
platform: 'node',
|
|
82
|
+
format: 'cjs',
|
|
83
|
+
outfile: tmpBundle,
|
|
84
|
+
logLevel: 'silent',
|
|
85
|
+
});
|
|
86
|
+
const m = require(tmpBundle);
|
|
87
|
+
// Find the exported machine — look for .config property (XState v5 machine)
|
|
88
|
+
machineObj = Object.values(m).find(v => v && typeof v === 'object' && v.config);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
xstateExtractError = err.message;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Extract structural facts from the machine object
|
|
94
|
+
let xstateStateNames = [];
|
|
95
|
+
let xstateGuardNames = [];
|
|
96
|
+
let xstateEventNames = [];
|
|
97
|
+
let xstateMaxDelib = null;
|
|
98
|
+
let xstateInitial = null;
|
|
99
|
+
|
|
100
|
+
if (machineObj && machineObj.config) {
|
|
101
|
+
const cfg = machineObj.config;
|
|
102
|
+
|
|
103
|
+
// State names (all top-level states)
|
|
104
|
+
xstateStateNames = Object.keys(cfg.states || {});
|
|
105
|
+
|
|
106
|
+
// Initial state
|
|
107
|
+
xstateInitial = cfg.initial || null;
|
|
108
|
+
|
|
109
|
+
// Guard names from implementations.guards
|
|
110
|
+
const impls = machineObj.implementations || {};
|
|
111
|
+
xstateGuardNames = Object.keys(impls.guards || {});
|
|
112
|
+
|
|
113
|
+
// Event names from all transitions across all states
|
|
114
|
+
const eventSet = new Set();
|
|
115
|
+
for (const stateCfg of Object.values(cfg.states || {})) {
|
|
116
|
+
for (const evtName of Object.keys(stateCfg.on || {})) {
|
|
117
|
+
eventSet.add(evtName);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
xstateEventNames = [...eventSet];
|
|
121
|
+
|
|
122
|
+
// maxDeliberation from context defaults
|
|
123
|
+
const ctxDefaults = cfg.context || {};
|
|
124
|
+
if (typeof ctxDefaults.maxDeliberation === 'number') {
|
|
125
|
+
xstateMaxDelib = ctxDefaults.maxDeliberation;
|
|
126
|
+
}
|
|
127
|
+
} else if (xstateExtractError) {
|
|
128
|
+
// Fallback: use regex if esbuild fails (e.g., in environments without esbuild)
|
|
129
|
+
process.stderr.write('[check-spec-sync] esbuild extraction failed: ' + xstateExtractError + '\n');
|
|
130
|
+
process.stderr.write('[check-spec-sync] Falling back to regex extraction (limited)\n');
|
|
131
|
+
const machineSrc = load('src/machines/qgsd-workflow.machine.ts');
|
|
132
|
+
xstateStateNames = (machineSrc.match(/^ ([A-Z_]+):\s*\{/gm) || []).map(l => l.trim().split(':')[0]);
|
|
133
|
+
const md = machineSrc.match(/maxDeliberation:\s*(\d+)/);
|
|
134
|
+
xstateMaxDelib = md ? parseInt(md[1], 10) : null;
|
|
135
|
+
const im = machineSrc.match(/initial:\s*'([A-Z_]+)'/);
|
|
136
|
+
xstateInitial = im ? im[1] : null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── 2. Extract TLA+ facts ────────────────────────────────────────────────────
|
|
140
|
+
// Phase values in TypeOK: phase \in {"IDLE", "COLLECTING_VOTES", ...}
|
|
141
|
+
const typeOkMatch = tlaSrc.match(/phase\s*\\in\s*\{([^}]+)\}/);
|
|
142
|
+
const tlaPhaseValues = typeOkMatch
|
|
143
|
+
? (typeOkMatch[1].match(/"([A-Z_]+)"/g) || []).map(s => s.replace(/"/g, ''))
|
|
144
|
+
: [];
|
|
145
|
+
|
|
146
|
+
// Initial phase in Init block: find Init ==, then first phase = "..."
|
|
147
|
+
const initBlockMatch = tlaSrc.match(/Init\s*==[\s\S]*?phase\s*=\s*"([A-Z_]+)"/);
|
|
148
|
+
const tlaInitPhase = initBlockMatch ? initBlockMatch[1] : null;
|
|
149
|
+
|
|
150
|
+
// ── 3. Extract MaxDeliberation from cfg files ─────────────────────────────────
|
|
151
|
+
const safetyMaxDelibMatch = safetyCfg.match(/MaxDeliberation\s*=\s*(\d+)/);
|
|
152
|
+
const livenessMaxDelibMatch = livenessCfg.match(/MaxDeliberation\s*=\s*(\d+)/);
|
|
153
|
+
const safetyMaxDelib = safetyMaxDelibMatch ? parseInt(safetyMaxDelibMatch[1], 10) : null;
|
|
154
|
+
const livenessMaxDelib = livenessMaxDelibMatch ? parseInt(livenessMaxDelibMatch[1], 10) : null;
|
|
155
|
+
|
|
156
|
+
// ── 4. Run checks ────────────────────────────────────────────────────────────
|
|
157
|
+
const errors = [];
|
|
158
|
+
const warnings = [];
|
|
159
|
+
|
|
160
|
+
function fail(msg) { errors.push(' FAIL ' + msg); }
|
|
161
|
+
function warn(msg) { warnings.push(' WARN ' + msg); }
|
|
162
|
+
function ok(msg) { process.stdout.write(' OK ' + msg + '\n'); }
|
|
163
|
+
|
|
164
|
+
process.stdout.write('\n[check-spec-sync] Source of truth: src/machines/qgsd-workflow.machine.ts\n\n');
|
|
165
|
+
|
|
166
|
+
// Check 1: State names
|
|
167
|
+
if (xstateStateNames.length === 0) {
|
|
168
|
+
fail('Could not extract state names from XState machine');
|
|
169
|
+
} else {
|
|
170
|
+
ok('XState states: ' + xstateStateNames.join(', '));
|
|
171
|
+
|
|
172
|
+
if (tlaPhaseValues.length === 0) {
|
|
173
|
+
fail('Could not parse phase values from QGSDQuorum.tla TypeOK');
|
|
174
|
+
} else {
|
|
175
|
+
ok('TLA+ phases: ' + tlaPhaseValues.join(', '));
|
|
176
|
+
|
|
177
|
+
const missing = xstateStateNames.filter(s => !tlaPhaseValues.includes(s));
|
|
178
|
+
const extra = tlaPhaseValues.filter(s => !xstateStateNames.includes(s));
|
|
179
|
+
|
|
180
|
+
if (missing.length > 0) {
|
|
181
|
+
fail('XState states missing from TLA+ TypeOK: ' + missing.join(', '));
|
|
182
|
+
}
|
|
183
|
+
if (extra.length > 0) {
|
|
184
|
+
fail('TLA+ TypeOK has orphaned phases not in XState machine: ' + extra.join(', ') +
|
|
185
|
+
'\n (These TLA+ phases have no corresponding XState state — update QGSDQuorum.tla)');
|
|
186
|
+
}
|
|
187
|
+
if (missing.length === 0 && extra.length === 0) {
|
|
188
|
+
ok('State names match exactly');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Check 2: MaxDeliberation
|
|
194
|
+
if (xstateMaxDelib === null) {
|
|
195
|
+
fail('Could not extract maxDeliberation from XState context defaults');
|
|
196
|
+
} else {
|
|
197
|
+
ok('XState maxDeliberation: ' + xstateMaxDelib);
|
|
198
|
+
|
|
199
|
+
if (safetyMaxDelib === null) {
|
|
200
|
+
fail('Could not parse MaxDeliberation from MCsafety.cfg');
|
|
201
|
+
} else if (safetyMaxDelib !== xstateMaxDelib) {
|
|
202
|
+
fail(
|
|
203
|
+
'MCsafety.cfg MaxDeliberation=' + safetyMaxDelib +
|
|
204
|
+
' does not match XState maxDeliberation=' + xstateMaxDelib
|
|
205
|
+
);
|
|
206
|
+
} else {
|
|
207
|
+
ok('MCsafety.cfg MaxDeliberation=' + safetyMaxDelib + ' matches');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (livenessMaxDelib === null) {
|
|
211
|
+
fail('Could not parse MaxDeliberation from MCliveness.cfg');
|
|
212
|
+
} else if (livenessMaxDelib !== xstateMaxDelib) {
|
|
213
|
+
fail(
|
|
214
|
+
'MCliveness.cfg MaxDeliberation=' + livenessMaxDelib +
|
|
215
|
+
' does not match XState maxDeliberation=' + xstateMaxDelib
|
|
216
|
+
);
|
|
217
|
+
} else {
|
|
218
|
+
ok('MCliveness.cfg MaxDeliberation=' + livenessMaxDelib + ' matches');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Check 3: Initial state
|
|
223
|
+
if (xstateInitial === null) {
|
|
224
|
+
fail('Could not extract initial state from XState machine');
|
|
225
|
+
} else {
|
|
226
|
+
ok('XState initial state: ' + xstateInitial);
|
|
227
|
+
|
|
228
|
+
if (tlaInitPhase === null) {
|
|
229
|
+
fail('Could not parse Init phase from QGSDQuorum.tla');
|
|
230
|
+
} else if (tlaInitPhase !== xstateInitial) {
|
|
231
|
+
fail(
|
|
232
|
+
'TLA+ Init sets phase="' + tlaInitPhase +
|
|
233
|
+
'" but XState initial is "' + xstateInitial + '"'
|
|
234
|
+
);
|
|
235
|
+
} else {
|
|
236
|
+
ok('Initial state matches: "' + xstateInitial + '"');
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Check 4: Alloy orphan detection
|
|
241
|
+
// Scan Alloy .als files for:
|
|
242
|
+
// (a) ALL_CAPS quoted string literals → cross-reference against xstateStateNames
|
|
243
|
+
// (b) camelCase guard name occurrences (verbatim word-boundary matches) → advisory cross-reference
|
|
244
|
+
//
|
|
245
|
+
// Note: Alloy orphan detection uses warn() not fail() because Alloy models are intentional
|
|
246
|
+
// abstractions — they may use different predicate names (e.g., MajorityReached instead of
|
|
247
|
+
// minQuorumMet). The authoritative guard mapping check is in Check 5 (guards/qgsd-workflow.json).
|
|
248
|
+
const alloyDir = path.join(ROOT, '.planning', 'formal', 'alloy');
|
|
249
|
+
if (fs.existsSync(alloyDir)) {
|
|
250
|
+
const alsFiles = fs.readdirSync(alloyDir).filter(f => f.endsWith('.als'));
|
|
251
|
+
for (const alsFile of alsFiles) {
|
|
252
|
+
const alsSrc = fs.readFileSync(path.join(alloyDir, alsFile), 'utf8');
|
|
253
|
+
|
|
254
|
+
// (a) State orphan scan: quoted ALL_CAPS string literals
|
|
255
|
+
const alloyStateRefs = (alsSrc.match(/"([A-Z_]{3,})"/g) || [])
|
|
256
|
+
.map(s => s.replace(/"/g, ''));
|
|
257
|
+
const orphanedStates = alloyStateRefs.filter(s => xstateStateNames.length > 0 && !xstateStateNames.includes(s));
|
|
258
|
+
if (orphanedStates.length > 0) {
|
|
259
|
+
warn('Alloy ' + alsFile + ' references states not in XState machine: ' + [...new Set(orphanedStates)].join(', '));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// (b) Guard name cross-reference: scan for verbatim camelCase XState guard names.
|
|
263
|
+
// Reports which XState guard names appear by exact name in the Alloy source (informational).
|
|
264
|
+
// Only flags Alloy files that reference XState guard names verbatim — PascalCase TLA+
|
|
265
|
+
// predicates (MinQuorumMet, DeliberationBounded) are NOT XState guard names and are not checked here.
|
|
266
|
+
if (xstateGuardNames.length > 0) {
|
|
267
|
+
const referencedGuards = xstateGuardNames.filter(g => {
|
|
268
|
+
const re = new RegExp('\\b' + g + '\\b');
|
|
269
|
+
return re.test(alsSrc);
|
|
270
|
+
});
|
|
271
|
+
if (referencedGuards.length > 0) {
|
|
272
|
+
ok('Alloy ' + alsFile + ' references XState guard names: ' + referencedGuards.join(', '));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
ok('Alloy orphan scan complete');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Check 5: Guard drift enforcement
|
|
280
|
+
// Verify each XState guard name is documented in the TLA+ guard mapping file, and vice versa.
|
|
281
|
+
//
|
|
282
|
+
// Mapping context:
|
|
283
|
+
// XState uses camelCase guard names (minQuorumMet, noInfiniteDeliberation, phaseMonotonicallyAdvances).
|
|
284
|
+
// TLA+ uses PascalCase predicates (MinQuorumMet, DeliberationBounded) — different names, same semantics.
|
|
285
|
+
// The bridge is .planning/formal/tla/guards/qgsd-workflow.json, which maps XState guard names to TLA+ expressions.
|
|
286
|
+
//
|
|
287
|
+
// If a guard is renamed in the XState machine, it must also be updated in guards/qgsd-workflow.json.
|
|
288
|
+
// If a mapping entry is removed from guards/qgsd-workflow.json without removing the guard from
|
|
289
|
+
// the machine (or vice versa), this check will detect the inconsistency.
|
|
290
|
+
//
|
|
291
|
+
// This check also corroborates by scanning QGSDQuorum.tla source text for camelCase guard name
|
|
292
|
+
// occurrences (they appear in the header comment block as documentation of guard translations).
|
|
293
|
+
if (xstateGuardNames.length > 0) {
|
|
294
|
+
if (!fs.existsSync(GUARDS_ABS_PATH)) {
|
|
295
|
+
fail('.planning/formal/tla/guards/qgsd-workflow.json not found — cannot verify guard name sync');
|
|
296
|
+
} else {
|
|
297
|
+
let guardsJson = null;
|
|
298
|
+
try {
|
|
299
|
+
guardsJson = JSON.parse(fs.readFileSync(GUARDS_ABS_PATH, 'utf8'));
|
|
300
|
+
} catch (e) {
|
|
301
|
+
fail('Could not parse guards JSON at ' + GUARDS_ABS_PATH + ': ' + e.message);
|
|
302
|
+
}
|
|
303
|
+
if (guardsJson) {
|
|
304
|
+
const mappedGuardNames = Object.keys(guardsJson.guards || {});
|
|
305
|
+
ok('XState guard names: ' + xstateGuardNames.join(', '));
|
|
306
|
+
ok('Guards JSON mapped names: ' + mappedGuardNames.join(', '));
|
|
307
|
+
|
|
308
|
+
// Forward check: XState guard → guards JSON (drift detection)
|
|
309
|
+
// If a guard is renamed in XState but guards/qgsd-workflow.json still has the old name, this fires.
|
|
310
|
+
const missingFromMapping = xstateGuardNames.filter(g => !mappedGuardNames.includes(g));
|
|
311
|
+
if (missingFromMapping.length > 0) {
|
|
312
|
+
fail(
|
|
313
|
+
'XState guards not found in guards/qgsd-workflow.json: ' + missingFromMapping.join(', ') +
|
|
314
|
+
'\n (Update .planning/formal/tla/guards/qgsd-workflow.json to map these guard names to their TLA+ predicates)'
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Reverse check: guards JSON → XState (orphan detection)
|
|
319
|
+
// If a guard mapping entry exists in guards/qgsd-workflow.json but the guard was removed from
|
|
320
|
+
// the XState machine, this is an orphaned mapping.
|
|
321
|
+
const orphanedGuards = mappedGuardNames.filter(g => !xstateGuardNames.includes(g));
|
|
322
|
+
if (orphanedGuards.length > 0) {
|
|
323
|
+
fail(
|
|
324
|
+
'guards/qgsd-workflow.json references guards not in XState machine: ' + orphanedGuards.join(', ') +
|
|
325
|
+
'\n (These guard mappings are orphaned — remove them or restore the XState guard)'
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (missingFromMapping.length === 0 && orphanedGuards.length === 0) {
|
|
330
|
+
ok('Guard names match exactly between XState machine and guards/qgsd-workflow.json');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Corroboration: check TLA+ source mentions guard names in comment references
|
|
334
|
+
const tlaGuardRefs = xstateGuardNames.filter(g => tlaSrc.includes(g));
|
|
335
|
+
if (tlaGuardRefs.length > 0) {
|
|
336
|
+
ok('TLA+ source references guard names (comment docs): ' + tlaGuardRefs.join(', '));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
} else {
|
|
341
|
+
warn('No guard names extracted from XState machine — skipping guard drift check');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ── 5. Report ────────────────────────────────────────────────────────────────
|
|
345
|
+
if (warnings.length > 0) {
|
|
346
|
+
process.stdout.write('\nWarnings:\n');
|
|
347
|
+
warnings.forEach(w => process.stdout.write(w + '\n'));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (errors.length > 0) {
|
|
351
|
+
process.stdout.write('\nSpec drift detected:\n');
|
|
352
|
+
errors.forEach(e => process.stderr.write(e + '\n'));
|
|
353
|
+
process.stdout.write(
|
|
354
|
+
'\nThe XState machine is the source of truth.\n' +
|
|
355
|
+
'Update the formal specs to match the code, then re-run this check.\n\n'
|
|
356
|
+
);
|
|
357
|
+
process.exit(1);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
process.stdout.write('\nAll checks passed — formal specs are in sync with the XState machine.\n\n');
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// bin/check-trace-redaction.cjs
|
|
4
|
+
// Validates trace event files against .planning/formal/trace/redaction.yaml policy.
|
|
5
|
+
// Prevents secrets and PII from persisting in trace event artifacts.
|
|
6
|
+
//
|
|
7
|
+
// Exit code 0: no violations (or no trace directory/files)
|
|
8
|
+
// Exit code 1: one or more violations found
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { writeCheckResult } = require('./write-check-result.cjs');
|
|
13
|
+
const { getRequirementIds } = require('./requirement-map.cjs');
|
|
14
|
+
|
|
15
|
+
const DEFAULT_POLICY_PATH = path.join(__dirname, '..', '.planning', 'formal', 'trace', 'redaction.yaml');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse redaction.yaml using line-by-line regex (no external YAML parser).
|
|
19
|
+
* Handles two sections:
|
|
20
|
+
* forbidden_keys: - "key_name"
|
|
21
|
+
* forbidden_patterns: - name: "n"\n regex: "r"
|
|
22
|
+
*
|
|
23
|
+
* @param {string} policyPath Absolute path to redaction.yaml
|
|
24
|
+
* @returns {{ forbidden_keys: string[], forbidden_patterns: Array<{name: string, regex: string, compiled: RegExp}> }}
|
|
25
|
+
*/
|
|
26
|
+
function parseRedactionPolicy(policyPath) {
|
|
27
|
+
const yaml = fs.readFileSync(policyPath, 'utf8');
|
|
28
|
+
const lines = yaml.split('\n');
|
|
29
|
+
|
|
30
|
+
const forbidden_keys = [];
|
|
31
|
+
const forbidden_patterns = [];
|
|
32
|
+
|
|
33
|
+
let inKeys = false;
|
|
34
|
+
let inPatterns = false;
|
|
35
|
+
let currentPattern = null;
|
|
36
|
+
|
|
37
|
+
for (const line of lines) {
|
|
38
|
+
const trimmed = line.trim();
|
|
39
|
+
|
|
40
|
+
// Section headers
|
|
41
|
+
if (trimmed === 'forbidden_keys:') {
|
|
42
|
+
inKeys = true;
|
|
43
|
+
inPatterns = false;
|
|
44
|
+
if (currentPattern) {
|
|
45
|
+
if (currentPattern.name && currentPattern.regex) {
|
|
46
|
+
forbidden_patterns.push({
|
|
47
|
+
name: currentPattern.name,
|
|
48
|
+
regex: currentPattern.regex,
|
|
49
|
+
compiled: new RegExp(currentPattern.regex),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
currentPattern = null;
|
|
53
|
+
}
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (trimmed === 'forbidden_patterns:') {
|
|
57
|
+
inKeys = false;
|
|
58
|
+
inPatterns = true;
|
|
59
|
+
if (currentPattern) {
|
|
60
|
+
if (currentPattern.name && currentPattern.regex) {
|
|
61
|
+
forbidden_patterns.push({
|
|
62
|
+
name: currentPattern.name,
|
|
63
|
+
regex: currentPattern.regex,
|
|
64
|
+
compiled: new RegExp(currentPattern.regex),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
currentPattern = null;
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Skip blank lines and top-level comments
|
|
73
|
+
if (trimmed === '' || (trimmed.startsWith('#') && !line.startsWith(' '))) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (inKeys) {
|
|
78
|
+
// Lines like: - "api_key" or - api_key
|
|
79
|
+
const keyMatch = trimmed.match(/^-\s+"?([^"]+)"?$/);
|
|
80
|
+
if (keyMatch) {
|
|
81
|
+
forbidden_keys.push(keyMatch[1].trim());
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (inPatterns) {
|
|
86
|
+
// New pattern entry starts with "- name:"
|
|
87
|
+
const newEntryMatch = trimmed.match(/^-\s+name:\s+"?([^"]+)"?$/);
|
|
88
|
+
if (newEntryMatch) {
|
|
89
|
+
// Flush previous pattern
|
|
90
|
+
if (currentPattern && currentPattern.name && currentPattern.regex) {
|
|
91
|
+
forbidden_patterns.push({
|
|
92
|
+
name: currentPattern.name,
|
|
93
|
+
regex: currentPattern.regex,
|
|
94
|
+
compiled: new RegExp(currentPattern.regex),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
currentPattern = { name: newEntryMatch[1].trim(), regex: null };
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
// Regex line within a pattern entry
|
|
101
|
+
const regexMatch = trimmed.match(/^regex:\s+"?(.+?)"?$/);
|
|
102
|
+
if (regexMatch && currentPattern) {
|
|
103
|
+
currentPattern.regex = regexMatch[1].trim();
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Flush last pattern
|
|
110
|
+
if (currentPattern && currentPattern.name && currentPattern.regex) {
|
|
111
|
+
forbidden_patterns.push({
|
|
112
|
+
name: currentPattern.name,
|
|
113
|
+
regex: currentPattern.regex,
|
|
114
|
+
compiled: new RegExp(currentPattern.regex),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { forbidden_keys, forbidden_patterns };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Validate a single trace event object against the redaction policy.
|
|
123
|
+
* Returns an array of violation objects (empty if clean).
|
|
124
|
+
*
|
|
125
|
+
* @param {Object} event Parsed trace event object
|
|
126
|
+
* @param {{ forbidden_keys: string[], forbidden_patterns: Array<{name: string, compiled: RegExp}> }} policy
|
|
127
|
+
* @returns {Array<{ key: string, value?: string, pattern_name?: string, violation_type: 'forbidden_key'|'forbidden_pattern' }>}
|
|
128
|
+
*/
|
|
129
|
+
function validateTraceEvent(event, policy) {
|
|
130
|
+
const violations = [];
|
|
131
|
+
if (!event || typeof event !== 'object') return violations;
|
|
132
|
+
|
|
133
|
+
for (const [key, value] of Object.entries(event)) {
|
|
134
|
+
// Check forbidden keys
|
|
135
|
+
if (policy.forbidden_keys.includes(key)) {
|
|
136
|
+
violations.push({ key, violation_type: 'forbidden_key' });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check forbidden patterns against string values
|
|
140
|
+
if (typeof value === 'string') {
|
|
141
|
+
for (const pattern of policy.forbidden_patterns) {
|
|
142
|
+
if (pattern.compiled.test(value)) {
|
|
143
|
+
violations.push({ key, value, pattern_name: pattern.name, violation_type: 'forbidden_pattern' });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return violations;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (require.main === module) {
|
|
153
|
+
// Parse CLI args: optional --trace-dir <path>
|
|
154
|
+
let traceDir = path.join(process.cwd(), '.planning', 'formal', 'trace');
|
|
155
|
+
const args = process.argv.slice(2);
|
|
156
|
+
const traceDirIdx = args.indexOf('--trace-dir');
|
|
157
|
+
if (traceDirIdx !== -1 && args[traceDirIdx + 1]) {
|
|
158
|
+
traceDir = args[traceDirIdx + 1];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Load policy
|
|
162
|
+
const policy = parseRedactionPolicy(DEFAULT_POLICY_PATH);
|
|
163
|
+
|
|
164
|
+
// Graceful: no trace directory
|
|
165
|
+
const _startMs = Date.now();
|
|
166
|
+
if (!fs.existsSync(traceDir)) {
|
|
167
|
+
try {
|
|
168
|
+
writeCheckResult({
|
|
169
|
+
tool: 'check-trace-redaction',
|
|
170
|
+
formalism: 'redaction',
|
|
171
|
+
result: 'pass',
|
|
172
|
+
check_id: 'ci:trace-redaction', surface: 'ci', property: 'Trace redaction — no forbidden keys or patterns in conformance event logs',
|
|
173
|
+
runtime_ms: Date.now() - _startMs, summary: 'pass: ci:trace-redaction in ' + (Date.now() - _startMs) + 'ms', triage_tags: [],
|
|
174
|
+
requirement_ids: getRequirementIds('ci:trace-redaction'),
|
|
175
|
+
metadata: { reason: 'no-trace-directory', directory: traceDir },
|
|
176
|
+
});
|
|
177
|
+
} catch (e) {
|
|
178
|
+
process.stderr.write('[check-trace-redaction] Warning: failed to write check result: ' + e.message + '\n');
|
|
179
|
+
}
|
|
180
|
+
process.exit(0);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Find .json and .jsonl files (non-recursive)
|
|
184
|
+
const allFiles = fs.readdirSync(traceDir);
|
|
185
|
+
const traceFiles = allFiles.filter(f => f.endsWith('.json') || f.endsWith('.jsonl'));
|
|
186
|
+
|
|
187
|
+
if (traceFiles.length === 0) {
|
|
188
|
+
try {
|
|
189
|
+
writeCheckResult({
|
|
190
|
+
tool: 'check-trace-redaction',
|
|
191
|
+
formalism: 'redaction',
|
|
192
|
+
result: 'pass',
|
|
193
|
+
check_id: 'ci:trace-redaction', surface: 'ci', property: 'Trace redaction — no forbidden keys or patterns in conformance event logs',
|
|
194
|
+
runtime_ms: Date.now() - _startMs, summary: 'pass: ci:trace-redaction in ' + (Date.now() - _startMs) + 'ms', triage_tags: [],
|
|
195
|
+
requirement_ids: getRequirementIds('ci:trace-redaction'),
|
|
196
|
+
metadata: { reason: 'no-trace-events', directory: traceDir },
|
|
197
|
+
});
|
|
198
|
+
} catch (e) {
|
|
199
|
+
process.stderr.write('[check-trace-redaction] Warning: failed to write check result: ' + e.message + '\n');
|
|
200
|
+
}
|
|
201
|
+
process.exit(0);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Scan all files — collect all violations (no fail-fast)
|
|
205
|
+
const allViolations = [];
|
|
206
|
+
let fileCount = 0;
|
|
207
|
+
let eventCount = 0;
|
|
208
|
+
|
|
209
|
+
for (const filename of traceFiles) {
|
|
210
|
+
const filePath = path.join(traceDir, filename);
|
|
211
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
212
|
+
const fileLines = raw.split('\n').filter(l => l.trim().length > 0);
|
|
213
|
+
fileCount++;
|
|
214
|
+
|
|
215
|
+
for (const line of fileLines) {
|
|
216
|
+
let event;
|
|
217
|
+
try {
|
|
218
|
+
event = JSON.parse(line);
|
|
219
|
+
} catch (_) {
|
|
220
|
+
continue; // skip unparseable lines
|
|
221
|
+
}
|
|
222
|
+
eventCount++;
|
|
223
|
+
const violations = validateTraceEvent(event, policy);
|
|
224
|
+
for (const v of violations) {
|
|
225
|
+
allViolations.push({ file: filename, ...v });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const _runtimeMs = Date.now() - _startMs;
|
|
231
|
+
if (allViolations.length > 0) {
|
|
232
|
+
process.stdout.write('[check-trace-redaction] ' + allViolations.length + ' violation(s) found:\n');
|
|
233
|
+
for (const v of allViolations.slice(0, 10)) {
|
|
234
|
+
process.stdout.write(' ' + JSON.stringify(v) + '\n');
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
writeCheckResult({
|
|
238
|
+
tool: 'check-trace-redaction',
|
|
239
|
+
formalism: 'redaction',
|
|
240
|
+
result: 'fail',
|
|
241
|
+
check_id: 'ci:trace-redaction', surface: 'ci', property: 'Trace redaction — no forbidden keys or patterns in conformance event logs',
|
|
242
|
+
runtime_ms: _runtimeMs, summary: 'fail: ci:trace-redaction in ' + _runtimeMs + 'ms', triage_tags: [],
|
|
243
|
+
requirement_ids: getRequirementIds('ci:trace-redaction'),
|
|
244
|
+
metadata: {
|
|
245
|
+
violations: allViolations.slice(0, 10),
|
|
246
|
+
total_violations: allViolations.length,
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
} catch (e) {
|
|
250
|
+
process.stderr.write('[check-trace-redaction] Warning: failed to write check result: ' + e.message + '\n');
|
|
251
|
+
}
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
writeCheckResult({
|
|
257
|
+
tool: 'check-trace-redaction',
|
|
258
|
+
formalism: 'redaction',
|
|
259
|
+
result: 'pass',
|
|
260
|
+
check_id: 'ci:trace-redaction', surface: 'ci', property: 'Trace redaction — no forbidden keys or patterns in conformance event logs',
|
|
261
|
+
runtime_ms: _runtimeMs, summary: 'pass: ci:trace-redaction in ' + _runtimeMs + 'ms', triage_tags: [],
|
|
262
|
+
requirement_ids: getRequirementIds('ci:trace-redaction'),
|
|
263
|
+
metadata: { files_checked: fileCount, events_checked: eventCount },
|
|
264
|
+
});
|
|
265
|
+
} catch (e) {
|
|
266
|
+
process.stderr.write('[check-trace-redaction] Warning: failed to write check result: ' + e.message + '\n');
|
|
267
|
+
}
|
|
268
|
+
process.exit(0);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
module.exports = { parseRedactionPolicy, validateTraceEvent };
|