@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.
Files changed (215) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +1024 -0
  3. package/agents/qgsd-codebase-mapper.md +764 -0
  4. package/agents/qgsd-debugger.md +1201 -0
  5. package/agents/qgsd-executor.md +472 -0
  6. package/agents/qgsd-integration-checker.md +443 -0
  7. package/agents/qgsd-phase-researcher.md +502 -0
  8. package/agents/qgsd-plan-checker.md +643 -0
  9. package/agents/qgsd-planner.md +1182 -0
  10. package/agents/qgsd-project-researcher.md +621 -0
  11. package/agents/qgsd-quorum-orchestrator.md +628 -0
  12. package/agents/qgsd-quorum-slot-worker.md +41 -0
  13. package/agents/qgsd-quorum-synthesizer.md +133 -0
  14. package/agents/qgsd-quorum-test-worker.md +37 -0
  15. package/agents/qgsd-quorum-worker.md +161 -0
  16. package/agents/qgsd-research-synthesizer.md +239 -0
  17. package/agents/qgsd-roadmapper.md +660 -0
  18. package/agents/qgsd-verifier.md +628 -0
  19. package/bin/accept-debug-invariant.cjs +165 -0
  20. package/bin/account-manager.cjs +719 -0
  21. package/bin/aggregate-requirements.cjs +466 -0
  22. package/bin/analyze-assumptions.cjs +757 -0
  23. package/bin/analyze-state-space.cjs +921 -0
  24. package/bin/attribute-trace-divergence.cjs +150 -0
  25. package/bin/auth-drivers/gh-cli.cjs +93 -0
  26. package/bin/auth-drivers/index.cjs +46 -0
  27. package/bin/auth-drivers/pool.cjs +67 -0
  28. package/bin/auth-drivers/simple.cjs +95 -0
  29. package/bin/autoClosePtoF.cjs +110 -0
  30. package/bin/blessed-terminal.cjs +350 -0
  31. package/bin/build-phase-index.cjs +472 -0
  32. package/bin/call-quorum-slot.cjs +541 -0
  33. package/bin/ccr-secure-config.cjs +99 -0
  34. package/bin/ccr-secure-start.cjs +83 -0
  35. package/bin/check-bundled-sdks.cjs +177 -0
  36. package/bin/check-coverage-guard.cjs +112 -0
  37. package/bin/check-liveness-fairness.cjs +95 -0
  38. package/bin/check-mcp-health.cjs +123 -0
  39. package/bin/check-provider-health.cjs +395 -0
  40. package/bin/check-results-exit.cjs +24 -0
  41. package/bin/check-spec-sync.cjs +360 -0
  42. package/bin/check-trace-redaction.cjs +271 -0
  43. package/bin/check-trace-schema-drift.cjs +99 -0
  44. package/bin/compareDrift.cjs +21 -0
  45. package/bin/conformance-schema.cjs +12 -0
  46. package/bin/count-scenarios.cjs +420 -0
  47. package/bin/debt-dedup.cjs +144 -0
  48. package/bin/debt-ledger.cjs +61 -0
  49. package/bin/debt-retention.cjs +76 -0
  50. package/bin/debt-state-machine.cjs +80 -0
  51. package/bin/detect-coverage-gaps.cjs +204 -0
  52. package/bin/detect-project-intent.cjs +362 -0
  53. package/bin/export-prism-constants.cjs +164 -0
  54. package/bin/extract-annotations.cjs +633 -0
  55. package/bin/extractFormalExpected.cjs +104 -0
  56. package/bin/fingerprint-drift.cjs +24 -0
  57. package/bin/fingerprint-issue.cjs +46 -0
  58. package/bin/formal-core.cjs +519 -0
  59. package/bin/formal-ref-linker.cjs +141 -0
  60. package/bin/formal-test-sync.cjs +788 -0
  61. package/bin/generate-formal-specs.cjs +588 -0
  62. package/bin/generate-petri-net.cjs +397 -0
  63. package/bin/generate-phase-spec.cjs +249 -0
  64. package/bin/generate-proposed-changes.cjs +194 -0
  65. package/bin/generate-tla-cfg.cjs +122 -0
  66. package/bin/generate-traceability-matrix.cjs +701 -0
  67. package/bin/generate-triage-bundle.cjs +300 -0
  68. package/bin/gh-account-rotate.cjs +34 -0
  69. package/bin/initialize-model-registry.cjs +105 -0
  70. package/bin/install-formal-tools.cjs +382 -0
  71. package/bin/install.js +2424 -0
  72. package/bin/isNumericThreshold.cjs +34 -0
  73. package/bin/issue-classifier.cjs +151 -0
  74. package/bin/levenshtein.cjs +74 -0
  75. package/bin/lint-formal-models.cjs +580 -0
  76. package/bin/load-baseline-requirements.cjs +275 -0
  77. package/bin/manage-agents-core.cjs +815 -0
  78. package/bin/migrate-formal-dir.cjs +172 -0
  79. package/bin/migrate-planning.cjs +206 -0
  80. package/bin/migrate-to-slots.cjs +255 -0
  81. package/bin/nForma.cjs +2726 -0
  82. package/bin/observe-config.cjs +353 -0
  83. package/bin/observe-debt-writer.cjs +140 -0
  84. package/bin/observe-handler-grafana.cjs +128 -0
  85. package/bin/observe-handler-internal.cjs +301 -0
  86. package/bin/observe-handler-logstash.cjs +153 -0
  87. package/bin/observe-handler-prometheus.cjs +185 -0
  88. package/bin/observe-handlers.cjs +436 -0
  89. package/bin/observe-registry.cjs +131 -0
  90. package/bin/observe-render.cjs +168 -0
  91. package/bin/planning-paths.cjs +167 -0
  92. package/bin/polyrepo.cjs +560 -0
  93. package/bin/prism-priority.cjs +153 -0
  94. package/bin/probe-quorum-slots.cjs +167 -0
  95. package/bin/promote-model.cjs +225 -0
  96. package/bin/propose-debug-invariants.cjs +165 -0
  97. package/bin/providers.json +392 -0
  98. package/bin/pty-proxy.py +129 -0
  99. package/bin/qgsd-solve.cjs +2477 -0
  100. package/bin/quorum-consensus-gate.cjs +238 -0
  101. package/bin/quorum-formal-context.cjs +183 -0
  102. package/bin/quorum-slot-dispatch.cjs +934 -0
  103. package/bin/read-policy.cjs +60 -0
  104. package/bin/requirement-map.cjs +63 -0
  105. package/bin/requirements-core.cjs +247 -0
  106. package/bin/resolve-cli.cjs +101 -0
  107. package/bin/review-mcp-logs.cjs +294 -0
  108. package/bin/run-account-manager-tlc.cjs +188 -0
  109. package/bin/run-account-pool-alloy.cjs +158 -0
  110. package/bin/run-alloy.cjs +153 -0
  111. package/bin/run-audit-alloy.cjs +187 -0
  112. package/bin/run-breaker-tlc.cjs +181 -0
  113. package/bin/run-formal-check.cjs +395 -0
  114. package/bin/run-formal-verify.cjs +701 -0
  115. package/bin/run-installer-alloy.cjs +188 -0
  116. package/bin/run-oauth-rotation-prism.cjs +132 -0
  117. package/bin/run-oscillation-tlc.cjs +202 -0
  118. package/bin/run-phase-tlc.cjs +228 -0
  119. package/bin/run-prism.cjs +446 -0
  120. package/bin/run-protocol-tlc.cjs +201 -0
  121. package/bin/run-quorum-composition-alloy.cjs +155 -0
  122. package/bin/run-sensitivity-sweep.cjs +231 -0
  123. package/bin/run-stop-hook-tlc.cjs +188 -0
  124. package/bin/run-tlc.cjs +467 -0
  125. package/bin/run-transcript-alloy.cjs +173 -0
  126. package/bin/run-uppaal.cjs +264 -0
  127. package/bin/secrets.cjs +134 -0
  128. package/bin/sensitivity-report.cjs +219 -0
  129. package/bin/sensitivity-sweep-feedback.cjs +194 -0
  130. package/bin/set-secret.cjs +29 -0
  131. package/bin/setup-telemetry-cron.sh +36 -0
  132. package/bin/sweepPtoF.cjs +63 -0
  133. package/bin/sync-baseline-requirements.cjs +290 -0
  134. package/bin/task-envelope.cjs +360 -0
  135. package/bin/telemetry-collector.cjs +229 -0
  136. package/bin/unified-mcp-server.mjs +735 -0
  137. package/bin/update-agents.cjs +369 -0
  138. package/bin/update-scoreboard.cjs +1134 -0
  139. package/bin/validate-debt-entry.cjs +207 -0
  140. package/bin/validate-invariant.cjs +419 -0
  141. package/bin/validate-memory.cjs +389 -0
  142. package/bin/validate-requirements-haiku.cjs +435 -0
  143. package/bin/validate-traces.cjs +438 -0
  144. package/bin/verify-formal-results.cjs +124 -0
  145. package/bin/verify-quorum-health.cjs +273 -0
  146. package/bin/write-check-result.cjs +106 -0
  147. package/bin/xstate-to-tla.cjs +483 -0
  148. package/bin/xstate-trace-walker.cjs +205 -0
  149. package/commands/qgsd/add-phase.md +43 -0
  150. package/commands/qgsd/add-requirement.md +24 -0
  151. package/commands/qgsd/add-todo.md +47 -0
  152. package/commands/qgsd/audit-milestone.md +37 -0
  153. package/commands/qgsd/check-todos.md +45 -0
  154. package/commands/qgsd/cleanup.md +18 -0
  155. package/commands/qgsd/close-formal-gaps.md +33 -0
  156. package/commands/qgsd/complete-milestone.md +136 -0
  157. package/commands/qgsd/debug.md +166 -0
  158. package/commands/qgsd/discuss-phase.md +83 -0
  159. package/commands/qgsd/execute-phase.md +117 -0
  160. package/commands/qgsd/fix-tests.md +27 -0
  161. package/commands/qgsd/formal-test-sync.md +32 -0
  162. package/commands/qgsd/health.md +22 -0
  163. package/commands/qgsd/help.md +22 -0
  164. package/commands/qgsd/insert-phase.md +32 -0
  165. package/commands/qgsd/join-discord.md +18 -0
  166. package/commands/qgsd/list-phase-assumptions.md +46 -0
  167. package/commands/qgsd/map-codebase.md +71 -0
  168. package/commands/qgsd/map-requirements.md +20 -0
  169. package/commands/qgsd/mcp-restart.md +176 -0
  170. package/commands/qgsd/mcp-set-model.md +134 -0
  171. package/commands/qgsd/mcp-setup.md +1371 -0
  172. package/commands/qgsd/mcp-status.md +274 -0
  173. package/commands/qgsd/mcp-update.md +238 -0
  174. package/commands/qgsd/new-milestone.md +44 -0
  175. package/commands/qgsd/new-project.md +42 -0
  176. package/commands/qgsd/observe.md +260 -0
  177. package/commands/qgsd/pause-work.md +38 -0
  178. package/commands/qgsd/plan-milestone-gaps.md +34 -0
  179. package/commands/qgsd/plan-phase.md +44 -0
  180. package/commands/qgsd/polyrepo.md +50 -0
  181. package/commands/qgsd/progress.md +24 -0
  182. package/commands/qgsd/queue.md +54 -0
  183. package/commands/qgsd/quick.md +133 -0
  184. package/commands/qgsd/quorum-test.md +275 -0
  185. package/commands/qgsd/quorum.md +707 -0
  186. package/commands/qgsd/reapply-patches.md +110 -0
  187. package/commands/qgsd/remove-phase.md +31 -0
  188. package/commands/qgsd/research-phase.md +189 -0
  189. package/commands/qgsd/resume-work.md +40 -0
  190. package/commands/qgsd/set-profile.md +34 -0
  191. package/commands/qgsd/settings.md +39 -0
  192. package/commands/qgsd/solve.md +565 -0
  193. package/commands/qgsd/sync-baselines.md +119 -0
  194. package/commands/qgsd/triage.md +233 -0
  195. package/commands/qgsd/update.md +37 -0
  196. package/commands/qgsd/verify-work.md +38 -0
  197. package/hooks/dist/config-loader.js +297 -0
  198. package/hooks/dist/conformance-schema.cjs +12 -0
  199. package/hooks/dist/gsd-context-monitor.js +64 -0
  200. package/hooks/dist/qgsd-check-update.js +62 -0
  201. package/hooks/dist/qgsd-circuit-breaker.js +682 -0
  202. package/hooks/dist/qgsd-precompact.js +156 -0
  203. package/hooks/dist/qgsd-prompt.js +653 -0
  204. package/hooks/dist/qgsd-session-start.js +122 -0
  205. package/hooks/dist/qgsd-slot-correlator.js +58 -0
  206. package/hooks/dist/qgsd-spec-regen.js +86 -0
  207. package/hooks/dist/qgsd-statusline.js +91 -0
  208. package/hooks/dist/qgsd-stop.js +553 -0
  209. package/hooks/dist/qgsd-token-collector.js +133 -0
  210. package/hooks/dist/unified-mcp-server.mjs +669 -0
  211. package/package.json +95 -0
  212. package/scripts/build-hooks.js +46 -0
  213. package/scripts/postinstall.js +48 -0
  214. package/scripts/secret-audit.sh +45 -0
  215. 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 };