@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,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debt ledger I/O module
|
|
3
|
+
* Provides atomic read/write operations for debt ledger with fail-open behavior
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('node:fs');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Read debt ledger from file
|
|
11
|
+
* Implements fail-open: returns empty ledger on missing or corrupt files
|
|
12
|
+
* @param {string} ledgerPath - Path to debt.json file
|
|
13
|
+
* @returns {object} Ledger object with schema_version, created_at, last_updated, debt_entries
|
|
14
|
+
*/
|
|
15
|
+
function readDebtLedger(ledgerPath) {
|
|
16
|
+
try {
|
|
17
|
+
const content = fs.readFileSync(ledgerPath, 'utf8');
|
|
18
|
+
const ledger = JSON.parse(content);
|
|
19
|
+
return ledger;
|
|
20
|
+
} catch (err) {
|
|
21
|
+
// Fail-open: log error but return empty ledger
|
|
22
|
+
console.error(`[debt-ledger] Failed to read ledger at ${ledgerPath}:`, err.message);
|
|
23
|
+
|
|
24
|
+
const now = new Date().toISOString();
|
|
25
|
+
return {
|
|
26
|
+
schema_version: '1',
|
|
27
|
+
created_at: now,
|
|
28
|
+
last_updated: now,
|
|
29
|
+
debt_entries: []
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Write debt ledger to file atomically
|
|
36
|
+
* Uses temp file + rename pattern to prevent corruption on crash
|
|
37
|
+
* @param {string} ledgerPath - Path to debt.json file
|
|
38
|
+
* @param {object} ledger - Ledger object to write
|
|
39
|
+
*/
|
|
40
|
+
function writeDebtLedger(ledgerPath, ledger) {
|
|
41
|
+
// Ensure parent directory exists
|
|
42
|
+
const dir = path.dirname(ledgerPath);
|
|
43
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
44
|
+
|
|
45
|
+
// Update last_updated timestamp
|
|
46
|
+
const now = new Date().toISOString();
|
|
47
|
+
const ledgerToWrite = {
|
|
48
|
+
...ledger,
|
|
49
|
+
last_updated: now
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Atomic write: write to temp file, then rename
|
|
53
|
+
const tmpPath = ledgerPath + '.tmp';
|
|
54
|
+
fs.writeFileSync(tmpPath, JSON.stringify(ledgerToWrite, null, 2), 'utf8');
|
|
55
|
+
fs.renameSync(tmpPath, ledgerPath);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = {
|
|
59
|
+
readDebtLedger,
|
|
60
|
+
writeDebtLedger
|
|
61
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debt retention policy module
|
|
3
|
+
* Handles archival of resolved entries older than max_age
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('node:fs');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Apply retention policy to debt ledger
|
|
11
|
+
* Archives resolved entries older than max_age to separate array
|
|
12
|
+
* Non-resolved entries are NEVER archived regardless of age
|
|
13
|
+
* @param {object} ledger - Debt ledger object
|
|
14
|
+
* @param {number} maxAgeDays - Maximum age in days for resolved entries (default: 90)
|
|
15
|
+
* @returns {object} { active: [], archived: [] } - Split ledger entries
|
|
16
|
+
*/
|
|
17
|
+
function applyRetentionPolicy(ledger, maxAgeDays = 90) {
|
|
18
|
+
const active = [];
|
|
19
|
+
const archived = [];
|
|
20
|
+
|
|
21
|
+
// Calculate cutoff timestamp
|
|
22
|
+
const now = new Date();
|
|
23
|
+
const cutoffMs = now.getTime() - (maxAgeDays * 24 * 60 * 60 * 1000);
|
|
24
|
+
const cutoffDate = new Date(cutoffMs);
|
|
25
|
+
|
|
26
|
+
// Process each entry
|
|
27
|
+
for (const entry of ledger.debt_entries) {
|
|
28
|
+
// Only archive resolved entries
|
|
29
|
+
if (entry.status !== 'resolved') {
|
|
30
|
+
active.push(entry);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Determine cutoff timestamp: prefer resolved_at, fall back to last_seen
|
|
35
|
+
const relevantTimestamp = entry.resolved_at || entry.last_seen;
|
|
36
|
+
if (!relevantTimestamp) {
|
|
37
|
+
// No timestamp available, keep active to avoid data loss
|
|
38
|
+
active.push(entry);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const entryDate = new Date(relevantTimestamp);
|
|
43
|
+
if (entryDate < cutoffDate) {
|
|
44
|
+
// Entry is older than max_age, archive it
|
|
45
|
+
archived.push(entry);
|
|
46
|
+
} else {
|
|
47
|
+
// Entry is newer than max_age, keep it active
|
|
48
|
+
active.push(entry);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { active, archived };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Write archived entries to JSONL file
|
|
57
|
+
* Appends entries (does not overwrite)
|
|
58
|
+
* @param {array} archivedEntries - Array of debt entries to archive
|
|
59
|
+
* @param {string} archivePath - Path to .jsonl archive file
|
|
60
|
+
*/
|
|
61
|
+
function writeArchive(archivedEntries, archivePath) {
|
|
62
|
+
// Ensure parent directory exists
|
|
63
|
+
const dir = path.dirname(archivePath);
|
|
64
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
65
|
+
|
|
66
|
+
// Write each entry as a separate JSON line
|
|
67
|
+
const lines = archivedEntries.map(entry => JSON.stringify(entry)).join('\n');
|
|
68
|
+
if (lines.length > 0) {
|
|
69
|
+
fs.appendFileSync(archivePath, lines + '\n', 'utf8');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = {
|
|
74
|
+
applyRetentionPolicy,
|
|
75
|
+
writeArchive
|
|
76
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debt status state machine
|
|
3
|
+
* Enforces valid transitions: open -> acknowledged -> resolving -> resolved
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Allowed transitions map
|
|
8
|
+
* Each state maps to an array of valid target states
|
|
9
|
+
*/
|
|
10
|
+
const ALLOWED_TRANSITIONS = {
|
|
11
|
+
'open': ['acknowledged'], // open can only go to acknowledged
|
|
12
|
+
'acknowledged': ['resolving', 'open'], // can move forward or revert to open
|
|
13
|
+
'resolving': ['resolved'], // can only go to resolved
|
|
14
|
+
'resolved': [] // terminal state — no outbound transitions
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Check if a transition is allowed
|
|
19
|
+
* @param {string} fromStatus - Current status
|
|
20
|
+
* @param {string} toStatus - Target status
|
|
21
|
+
* @returns {boolean} true if transition is allowed, false otherwise
|
|
22
|
+
*/
|
|
23
|
+
function canTransition(fromStatus, toStatus) {
|
|
24
|
+
// Reject transition to same status (no-op transitions not allowed)
|
|
25
|
+
if (fromStatus === toStatus) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Check if transition is in the allowed list
|
|
30
|
+
const allowed = ALLOWED_TRANSITIONS[fromStatus] || [];
|
|
31
|
+
return allowed.includes(toStatus);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Transition a debt entry to a new status
|
|
36
|
+
* @param {object} entry - Debt entry object
|
|
37
|
+
* @param {string} newStatus - Target status
|
|
38
|
+
* @returns {object} { success: boolean, entry?: object, error?: string }
|
|
39
|
+
*/
|
|
40
|
+
function transitionDebtEntry(entry, newStatus) {
|
|
41
|
+
const errors = [];
|
|
42
|
+
|
|
43
|
+
// Validate target status is one of the allowed values
|
|
44
|
+
const validStatuses = ['open', 'acknowledged', 'resolving', 'resolved'];
|
|
45
|
+
if (!validStatuses.includes(newStatus)) {
|
|
46
|
+
errors.push(`Invalid target status: ${newStatus}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check if transition is allowed
|
|
50
|
+
if (!canTransition(entry.status, newStatus)) {
|
|
51
|
+
errors.push(`Transition not allowed: ${entry.status} -> ${newStatus}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Return error if any validation failed
|
|
55
|
+
if (errors.length > 0) {
|
|
56
|
+
return {
|
|
57
|
+
success: false,
|
|
58
|
+
error: errors.join('; ')
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Apply transition: create updated entry with new status
|
|
63
|
+
const updated = { ...entry, status: newStatus };
|
|
64
|
+
|
|
65
|
+
// Add resolved_at timestamp when transitioning to resolved state
|
|
66
|
+
if (newStatus === 'resolved') {
|
|
67
|
+
updated.resolved_at = new Date().toISOString();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
success: true,
|
|
72
|
+
entry: updated
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
canTransition,
|
|
78
|
+
transitionDebtEntry,
|
|
79
|
+
ALLOWED_TRANSITIONS
|
|
80
|
+
};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// bin/detect-coverage-gaps.cjs
|
|
4
|
+
// TLC state-space vs conformance trace coverage gap detector.
|
|
5
|
+
// Requirements: SIG-01
|
|
6
|
+
//
|
|
7
|
+
// Usage:
|
|
8
|
+
// node bin/detect-coverage-gaps.cjs [--spec=QGSDQuorum] [--log=path]
|
|
9
|
+
//
|
|
10
|
+
// Output:
|
|
11
|
+
// .planning/formal/coverage-gaps.md — structured test backlog when gaps exist
|
|
12
|
+
//
|
|
13
|
+
// Computes: TLC-reachable states minus trace-observed states = coverage gaps.
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
// ── State mapping definitions ────────────────────────────────────────────────
|
|
19
|
+
// Maps each TLA+ spec to its state variable name and value-to-name mapping.
|
|
20
|
+
// Source of truth: state comments in the TLA+ spec files.
|
|
21
|
+
const STATE_MAPS = {
|
|
22
|
+
'QGSDQuorum': {
|
|
23
|
+
variable: 's',
|
|
24
|
+
values: { '0': 'COLLECTING_VOTES', '1': 'DECIDED', '2': 'DELIBERATING' },
|
|
25
|
+
},
|
|
26
|
+
'QGSDStopHook': {
|
|
27
|
+
variable: 'phase',
|
|
28
|
+
values: { '0': 'IDLE', '1': 'READING', '2': 'DECIDING', '3': 'BLOCKED' },
|
|
29
|
+
},
|
|
30
|
+
'QGSDCircuitBreaker': {
|
|
31
|
+
variable: 'state',
|
|
32
|
+
values: { '0': 'MONITORING', '1': 'TRIGGERED', '2': 'RECOVERING' },
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// ── Action-to-state reverse mapping ──────────────────────────────────────────
|
|
37
|
+
// Maps conformance event action names to state names for trace parsing.
|
|
38
|
+
const ACTION_TO_STATE = {
|
|
39
|
+
'quorum_start': 'COLLECTING_VOTES',
|
|
40
|
+
'quorum_complete': 'DECIDED',
|
|
41
|
+
'quorum_block': 'DECIDED',
|
|
42
|
+
'deliberation_round': 'DELIBERATING',
|
|
43
|
+
'stop_hook_read': 'READING',
|
|
44
|
+
'stop_hook_decide': 'DECIDING',
|
|
45
|
+
'stop_hook_block': 'BLOCKED',
|
|
46
|
+
'stop_hook_idle': 'IDLE',
|
|
47
|
+
'breaker_trigger': 'TRIGGERED',
|
|
48
|
+
'breaker_recover': 'RECOVERING',
|
|
49
|
+
'breaker_monitor': 'MONITORING',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* parseTlcStates(specName) — returns the full set of named states for a spec.
|
|
54
|
+
* @param {string} specName - TLA+ spec name (e.g. 'QGSDQuorum')
|
|
55
|
+
* @returns {{ specName: string, states: Set<string>, variable: string } | null}
|
|
56
|
+
*/
|
|
57
|
+
function parseTlcStates(specName) {
|
|
58
|
+
const map = STATE_MAPS[specName];
|
|
59
|
+
if (!map) return null;
|
|
60
|
+
return {
|
|
61
|
+
specName,
|
|
62
|
+
states: new Set(Object.values(map.values)),
|
|
63
|
+
variable: map.variable,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* parseTraceStates(logPath) — extracts observed states from conformance JSONL.
|
|
69
|
+
* @param {string} logPath - path to conformance-events.jsonl
|
|
70
|
+
* @returns {Set<string> | null} - set of observed state names, or null if file missing
|
|
71
|
+
*/
|
|
72
|
+
function parseTraceStates(logPath) {
|
|
73
|
+
const pp = require('./planning-paths.cjs');
|
|
74
|
+
const p = logPath || pp.resolveWithFallback(process.cwd(), 'conformance-events');
|
|
75
|
+
try {
|
|
76
|
+
if (!fs.existsSync(p)) return null;
|
|
77
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
78
|
+
const lines = raw.split('\n').filter(l => l.trim().length > 0);
|
|
79
|
+
const observed = new Set();
|
|
80
|
+
for (const line of lines) {
|
|
81
|
+
try {
|
|
82
|
+
const event = JSON.parse(line);
|
|
83
|
+
// Try state field first
|
|
84
|
+
if (event.state && typeof event.state === 'string') {
|
|
85
|
+
observed.add(event.state);
|
|
86
|
+
}
|
|
87
|
+
// Map action to state
|
|
88
|
+
if (event.action && ACTION_TO_STATE[event.action]) {
|
|
89
|
+
observed.add(ACTION_TO_STATE[event.action]);
|
|
90
|
+
}
|
|
91
|
+
} catch (_) { /* skip malformed lines */ }
|
|
92
|
+
}
|
|
93
|
+
return observed;
|
|
94
|
+
} catch (_) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* detectCoverageGaps(options) — computes TLC reachable minus trace observed.
|
|
101
|
+
* @param {{ specName?: string, logPath?: string, outputPath?: string }} options
|
|
102
|
+
* @returns {{ status: string, gaps?: string[], outputPath?: string, reason?: string }}
|
|
103
|
+
*/
|
|
104
|
+
function detectCoverageGaps(options = {}) {
|
|
105
|
+
const specName = options.specName || 'QGSDQuorum';
|
|
106
|
+
const pp2 = require('./planning-paths.cjs');
|
|
107
|
+
const logPath = options.logPath || pp2.resolveWithFallback(process.cwd(), 'conformance-events');
|
|
108
|
+
const outputPath = options.outputPath || path.join(process.cwd(), '.planning', 'formal', 'coverage-gaps.md');
|
|
109
|
+
|
|
110
|
+
const tlcResult = parseTlcStates(specName);
|
|
111
|
+
if (!tlcResult) {
|
|
112
|
+
return { status: 'unknown-spec', reason: 'Spec ' + specName + ' not found in STATE_MAPS' };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const traceStates = parseTraceStates(logPath);
|
|
116
|
+
if (traceStates === null) {
|
|
117
|
+
return { status: 'no-traces', reason: 'conformance log not found' };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Compute gap = reachable - observed
|
|
121
|
+
const gaps = [];
|
|
122
|
+
for (const state of tlcResult.states) {
|
|
123
|
+
if (!traceStates.has(state)) {
|
|
124
|
+
gaps.push(state);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (gaps.length === 0) {
|
|
129
|
+
return { status: 'full-coverage', gaps: [] };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Write coverage-gaps.md
|
|
133
|
+
const reachable = tlcResult.states.size;
|
|
134
|
+
const observed = reachable - gaps.length;
|
|
135
|
+
const pct = ((observed / reachable) * 100).toFixed(0);
|
|
136
|
+
const timestamp = new Date().toISOString();
|
|
137
|
+
|
|
138
|
+
// Build variable value lookup
|
|
139
|
+
const specMap = STATE_MAPS[specName];
|
|
140
|
+
const stateToValue = {};
|
|
141
|
+
for (const [val, name] of Object.entries(specMap.values)) {
|
|
142
|
+
stateToValue[name] = specMap.variable + '=' + val;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const md = [
|
|
146
|
+
'# TLC Coverage Gaps',
|
|
147
|
+
'',
|
|
148
|
+
'Generated: ' + timestamp,
|
|
149
|
+
'Spec: ' + specName,
|
|
150
|
+
'',
|
|
151
|
+
'## Unreached States',
|
|
152
|
+
'',
|
|
153
|
+
'| State | Variable Value | Description |',
|
|
154
|
+
'|-------|---------------|-------------|',
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
for (const gap of gaps.sort()) {
|
|
158
|
+
const varVal = stateToValue[gap] || 'unknown';
|
|
159
|
+
md.push('| ' + gap + ' | ' + varVal + ' | State reachable by TLC but never observed in conformance traces |');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
md.push('');
|
|
163
|
+
md.push('## Coverage Summary');
|
|
164
|
+
md.push('');
|
|
165
|
+
md.push('- TLC reachable: ' + reachable + ' states');
|
|
166
|
+
md.push('- Trace observed: ' + observed + ' states');
|
|
167
|
+
md.push('- Gaps: ' + gaps.length + ' states (' + pct + '% coverage)');
|
|
168
|
+
md.push('');
|
|
169
|
+
md.push('## Action Items');
|
|
170
|
+
md.push('');
|
|
171
|
+
md.push('Each gap represents a state that formal verification proves is reachable but production has never exercised.');
|
|
172
|
+
md.push('Add test cases that drive the system into these states.');
|
|
173
|
+
md.push('');
|
|
174
|
+
|
|
175
|
+
// Ensure output directory exists
|
|
176
|
+
const outDir = path.dirname(outputPath);
|
|
177
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
178
|
+
fs.writeFileSync(outputPath, md.join('\n'));
|
|
179
|
+
|
|
180
|
+
return { status: 'gaps-found', gaps, outputPath };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── CLI entrypoint ───────────────────────────────────────────────────────────
|
|
184
|
+
if (require.main === module) {
|
|
185
|
+
const args = process.argv.slice(2);
|
|
186
|
+
const specArg = args.find(a => a.startsWith('--spec='));
|
|
187
|
+
const logArg = args.find(a => a.startsWith('--log='));
|
|
188
|
+
|
|
189
|
+
const specName = specArg ? specArg.split('=')[1] : 'QGSDQuorum';
|
|
190
|
+
const logPath = logArg ? logArg.split('=')[1] : undefined;
|
|
191
|
+
|
|
192
|
+
const result = detectCoverageGaps({ specName, logPath });
|
|
193
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
194
|
+
|
|
195
|
+
if (result.status === 'gaps-found') {
|
|
196
|
+
process.stderr.write('[detect-coverage-gaps] ' + result.gaps.length + ' gap(s) found. See: ' + result.outputPath + '\n');
|
|
197
|
+
} else if (result.status === 'no-traces') {
|
|
198
|
+
process.stderr.write('[detect-coverage-gaps] No conformance log found — nothing to compare.\n');
|
|
199
|
+
} else if (result.status === 'full-coverage') {
|
|
200
|
+
process.stderr.write('[detect-coverage-gaps] Full coverage — all TLC-reachable states observed in traces.\n');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = { detectCoverageGaps, parseTlcStates, parseTraceStates };
|