@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,104 @@
1
+ /**
2
+ * Formal expected value extraction for P->F residual layer
3
+ * Parses formal_ref strings and loads parameter values from spec files
4
+ * Fail-open: returns null on any error (missing file, bad parse, etc.)
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const fs = require('node:fs');
10
+ const path = require('node:path');
11
+
12
+ /**
13
+ * Parse a formal_ref string into its components
14
+ * Formats:
15
+ * "spec:{module}/{file}:{param}" -> { type: 'spec', module, file, param }
16
+ * "requirement:{id}" -> { type: 'requirement', id }
17
+ * null/invalid -> null
18
+ * @param {string} formalRef
19
+ * @returns {object|null}
20
+ */
21
+ function parseFormalRef(formalRef) {
22
+ if (!formalRef || typeof formalRef !== 'string') return null;
23
+
24
+ if (formalRef.startsWith('requirement:')) {
25
+ return { type: 'requirement', id: formalRef.slice('requirement:'.length) };
26
+ }
27
+
28
+ if (formalRef.startsWith('spec:')) {
29
+ const rest = formalRef.slice('spec:'.length);
30
+ // Format: module/file:param
31
+ const colonIdx = rest.lastIndexOf(':');
32
+ if (colonIdx === -1) {
33
+ // No param key — invariant reference (e.g., "spec:safety/invariant-consistency")
34
+ return { type: 'spec', path: rest, param: null };
35
+ }
36
+ const filePath = rest.slice(0, colonIdx);
37
+ const param = rest.slice(colonIdx + 1);
38
+ return { type: 'spec', path: filePath, param: param || null };
39
+ }
40
+
41
+ return null;
42
+ }
43
+
44
+ /**
45
+ * Extract formal expected value from a spec file
46
+ * Supports .cfg files (line-based key=value) and .json files
47
+ * @param {string} formalRef - Formal reference string
48
+ * @param {object} [options]
49
+ * @param {string} [options.specDir] - Override default spec directory (for testing)
50
+ * @returns {*} The parameter value, or null if not found
51
+ */
52
+ function extractFormalExpected(formalRef, options = {}) {
53
+ const parsed = parseFormalRef(formalRef);
54
+ if (!parsed) return null;
55
+
56
+ // Requirements are text, not numeric — return null
57
+ if (parsed.type === 'requirement') return null;
58
+
59
+ // Spec without param key — invariant reference, no extractable value
60
+ if (parsed.type === 'spec' && !parsed.param) return null;
61
+
62
+ const specDir = options.specDir || path.resolve(process.cwd(), '.planning/formal/spec');
63
+ const filePath = path.join(specDir, parsed.path);
64
+
65
+ try {
66
+ const content = fs.readFileSync(filePath, 'utf8');
67
+
68
+ // JSON files
69
+ if (filePath.endsWith('.json')) {
70
+ const data = JSON.parse(content);
71
+ const val = data[parsed.param];
72
+ return val !== undefined ? val : null;
73
+ }
74
+
75
+ // CFG files (TLA+ config format: key = value)
76
+ for (const line of content.split('\n')) {
77
+ const trimmed = line.trim();
78
+ // Skip comments and empty lines
79
+ if (!trimmed || trimmed.startsWith('\\*') || trimmed.startsWith('SPECIFICATION') ||
80
+ trimmed.startsWith('CONSTANTS') || trimmed.startsWith('SYMMETRY') ||
81
+ trimmed.startsWith('INVARIANT') || trimmed.startsWith('PROPERTY') ||
82
+ trimmed.startsWith('CHECK_DEADLOCK')) {
83
+ continue;
84
+ }
85
+ // Match key = value patterns
86
+ const match = trimmed.match(/^(\w+)\s*=\s*(.+)$/);
87
+ if (match && match[1] === parsed.param) {
88
+ const rawVal = match[2].trim();
89
+ // Try to parse as number
90
+ const num = Number(rawVal);
91
+ if (!isNaN(num) && rawVal !== '') return num;
92
+ // Return as string
93
+ return rawVal;
94
+ }
95
+ }
96
+
97
+ return null; // Param not found in file
98
+ } catch {
99
+ // Fail-open: file not found, parse error, etc.
100
+ return null;
101
+ }
102
+ }
103
+
104
+ module.exports = { extractFormalExpected, parseFormalRef };
@@ -0,0 +1,24 @@
1
+ const crypto = require('crypto');
2
+
3
+ /**
4
+ * Generate deterministic fingerprint for a drift using formal parameter key
5
+ * Fingerprint depends only on the parameter key, not measured values or timestamps
6
+ *
7
+ * @param {Object} drift - Drift object with formal_parameter_key and other optional fields
8
+ * @returns {string} - 16-char hex fingerprint
9
+ * @throws {Error} - If formal_parameter_key is missing or empty
10
+ */
11
+ function fingerprintDrift(drift) {
12
+ // Validate that formal_parameter_key exists and is non-empty
13
+ if (!drift.formal_parameter_key || typeof drift.formal_parameter_key !== 'string' || drift.formal_parameter_key.trim() === '') {
14
+ throw new Error('formal_parameter_key required (non-empty string)');
15
+ }
16
+
17
+ // Deterministic hash of the formal parameter key
18
+ return crypto.createHash('sha256')
19
+ .update(drift.formal_parameter_key)
20
+ .digest('hex')
21
+ .slice(0, 16);
22
+ }
23
+
24
+ module.exports = { fingerprintDrift };
@@ -0,0 +1,46 @@
1
+ const crypto = require('crypto');
2
+
3
+ /**
4
+ * Normalize and hash an error message
5
+ * Strips timestamps, line numbers, and lowercases for stable hashing
6
+ *
7
+ * @param {string} msg - Error message text
8
+ * @returns {string} - 16-char hex hash
9
+ */
10
+ function hashMessage(msg) {
11
+ const normalized = (msg || '')
12
+ .replace(/\d{4}-\d{2}-\d{2}T[\d:.Z]+/g, 'TIMESTAMP') // ISO8601 timestamps
13
+ .replace(/:\d+/g, ':LINE') // Line numbers
14
+ .toLowerCase()
15
+ .trim();
16
+
17
+ return crypto.createHash('sha256').update(normalized).digest('hex').slice(0, 16);
18
+ }
19
+
20
+ /**
21
+ * Generate deterministic fingerprint for an issue using hierarchical strategy:
22
+ * exception_type -> function_name -> message_hash
23
+ *
24
+ * @param {Object} issue - Issue object with optional exception_type, function_name, message
25
+ * @returns {string} - 16-char hex fingerprint
26
+ */
27
+ function fingerprintIssue(issue) {
28
+ // Normalize exception type
29
+ const exceptionType = (issue.exception_type || 'unknown').toLowerCase();
30
+
31
+ // Normalize function name (replace non-alphanumeric with underscore)
32
+ const functionName = (issue.function_name || 'unknown')
33
+ .toLowerCase()
34
+ .replace(/[^a-z0-9_]/g, '_');
35
+
36
+ // Hash the message
37
+ const msgHash = hashMessage(issue.message);
38
+
39
+ // Combine components with colon separator
40
+ const combined = `${exceptionType}:${functionName}:${msgHash}`;
41
+
42
+ // Final deterministic hash
43
+ return crypto.createHash('sha256').update(combined).digest('hex').slice(0, 16);
44
+ }
45
+
46
+ module.exports = { fingerprintIssue, hashMessage };
@@ -0,0 +1,519 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Pure data functions for formal verification browsing.
5
+ * No blessed dependency — all functions are testable in isolation.
6
+ * Consumers: bin/nForma.cjs (blessed TUI)
7
+ *
8
+ * Data sources (project-relative paths via basePath):
9
+ * .planning/formal/model-registry.json — formal model inventory
10
+ * .planning/formal/traceability-matrix.json — requirement-property links
11
+ * .planning/formal/unit-test-coverage.json — test coverage sidecar
12
+ * .planning/formal/state-space-report.json — variable domains & risk
13
+ * .planning/formal/check-results.ndjson — verification results
14
+ * .planning/formal/requirements.json — requirement definitions
15
+ * .planning/formal/tla/*.tla — TLA+ source files
16
+ * .planning/formal/alloy/*.als — Alloy source files
17
+ */
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Data loaders (fail-open: return empty on missing/corrupt files)
24
+ // ---------------------------------------------------------------------------
25
+
26
+ function loadJSON(basePath, relPath) {
27
+ const p = path.join(basePath || process.cwd(), relPath);
28
+ if (!fs.existsSync(p)) return null;
29
+ try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_) { return null; }
30
+ }
31
+
32
+ function loadModelRegistry(basePath) {
33
+ return loadJSON(basePath, '.planning/formal/model-registry.json') || { models: {} };
34
+ }
35
+
36
+ function loadTraceabilityMatrix(basePath) {
37
+ return loadJSON(basePath, '.planning/formal/traceability-matrix.json') || { requirements: {}, properties: {} };
38
+ }
39
+
40
+ function loadUnitTestCoverage(basePath) {
41
+ return loadJSON(basePath, '.planning/formal/unit-test-coverage.json') || { requirements: {} };
42
+ }
43
+
44
+ function loadStateSpaceReport(basePath) {
45
+ return loadJSON(basePath, '.planning/formal/state-space-report.json') || { models: {} };
46
+ }
47
+
48
+ function loadRequirements(basePath) {
49
+ return loadJSON(basePath, '.planning/formal/requirements.json') || { requirements: [] };
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // TLA+ / Alloy source parsers (lightweight — extract variables & properties)
54
+ // ---------------------------------------------------------------------------
55
+
56
+ function parseTLAVariables(content) {
57
+ const vars = [];
58
+ const lines = content.split('\n');
59
+ let inVarBlock = false;
60
+
61
+ for (let i = 0; i < lines.length; i++) {
62
+ const line = lines[i];
63
+ if (/^VARIABLES?\b/.test(line.trim())) {
64
+ inVarBlock = true;
65
+ // Variables may be on same line: VARIABLES a, b, c
66
+ const inline = line.replace(/^VARIABLES?\s*/, '').trim();
67
+ if (inline) {
68
+ for (const v of inline.split(',')) {
69
+ const name = v.trim().replace(/\\.*$/, '').trim();
70
+ if (name) vars.push({ name, comment: extractInlineComment(line) });
71
+ }
72
+ }
73
+ continue;
74
+ }
75
+ if (inVarBlock) {
76
+ if (line.trim() === '' || /^[A-Z]/.test(line.trim()) && !/^\s/.test(line)) {
77
+ // End of variable block (blank line or new definition)
78
+ if (!/^\s/.test(line) && line.trim() !== '') inVarBlock = false;
79
+ continue;
80
+ }
81
+ for (const v of line.split(',')) {
82
+ const name = v.trim().replace(/\\.*$/, '').trim();
83
+ if (name) vars.push({ name, comment: extractInlineComment(line) });
84
+ }
85
+ }
86
+ }
87
+ return vars;
88
+ }
89
+
90
+ function extractInlineComment(line) {
91
+ const match = line.match(/\\[*]\s*(.+)$/);
92
+ return match ? match[1].trim() : null;
93
+ }
94
+
95
+ function parseTLAProperties(content) {
96
+ const props = [];
97
+ const lines = content.split('\n');
98
+ let pendingReqs = [];
99
+
100
+ for (let i = 0; i < lines.length; i++) {
101
+ const line = lines[i].trim();
102
+ // Collect @requirement annotations
103
+ const reqMatch = line.match(/@requirement\s+(.+)/);
104
+ if (reqMatch) {
105
+ pendingReqs.push(...reqMatch[1].split(/[,\s]+/).filter(Boolean));
106
+ continue;
107
+ }
108
+ // Property definition: Name ==
109
+ const propMatch = line.match(/^(\w+)\s*==/);
110
+ if (propMatch) {
111
+ const name = propMatch[1];
112
+ // Skip infrastructure definitions
113
+ if (!['vars', 'Init', 'Next', 'Spec', 'Fairness', 'Symmetry'].includes(name)) {
114
+ props.push({
115
+ name,
116
+ requirements: pendingReqs.length > 0 ? [...pendingReqs] : [],
117
+ line: i + 1,
118
+ });
119
+ }
120
+ pendingReqs = [];
121
+ } else if (!line.startsWith('\\*') && !line.startsWith('(*') && line !== '') {
122
+ // Non-comment, non-empty line clears pending annotations
123
+ if (!reqMatch) pendingReqs = [];
124
+ }
125
+ }
126
+ return props;
127
+ }
128
+
129
+ function parseAlloyConstructs(content) {
130
+ const constructs = [];
131
+ const lines = content.split('\n');
132
+ let pendingReqs = [];
133
+
134
+ for (let i = 0; i < lines.length; i++) {
135
+ const line = lines[i].trim();
136
+ const reqMatch = line.match(/@requirement\s+(.+)/);
137
+ if (reqMatch) {
138
+ pendingReqs.push(...reqMatch[1].split(/[,\s]+/).filter(Boolean));
139
+ continue;
140
+ }
141
+ // Alloy construct: sig, fact, assert, pred, fun, check, run
142
+ const cMatch = line.match(/^(sig|fact|assert|pred|fun|check|run)\s+(\w+)/);
143
+ if (cMatch) {
144
+ constructs.push({
145
+ kind: cMatch[1],
146
+ name: cMatch[2],
147
+ requirements: pendingReqs.length > 0 ? [...pendingReqs] : [],
148
+ line: i + 1,
149
+ });
150
+ pendingReqs = [];
151
+ }
152
+ }
153
+ return constructs;
154
+ }
155
+
156
+ function parseAlloySigs(content) {
157
+ const sigs = [];
158
+ const lines = content.split('\n');
159
+ for (let i = 0; i < lines.length; i++) {
160
+ const line = lines[i].trim();
161
+ const sigMatch = line.match(/^(?:abstract\s+|one\s+|lone\s+)?sig\s+(\w+)(?:\s+extends\s+(\w+))?/);
162
+ if (sigMatch) {
163
+ const fields = [];
164
+ // Parse fields in the sig body
165
+ for (let j = i + 1; j < lines.length; j++) {
166
+ const fl = lines[j].trim();
167
+ if (fl.startsWith('}')) break;
168
+ const fieldMatch = fl.match(/^(\w+)\s*:\s*(.+?)(?:,|$)/);
169
+ if (fieldMatch) {
170
+ fields.push({ name: fieldMatch[1], type: fieldMatch[2].trim().replace(/,\s*$/, '') });
171
+ }
172
+ }
173
+ sigs.push({
174
+ name: sigMatch[1],
175
+ extends: sigMatch[2] || null,
176
+ fields,
177
+ line: i + 1,
178
+ });
179
+ }
180
+ }
181
+ return sigs;
182
+ }
183
+
184
+ // ---------------------------------------------------------------------------
185
+ // Aggregation functions
186
+ // ---------------------------------------------------------------------------
187
+
188
+ /**
189
+ * Build a comprehensive model summary from all data sources.
190
+ * Returns array of model objects with parsed details.
191
+ */
192
+ function buildModelIndex(basePath) {
193
+ const registry = loadModelRegistry(basePath);
194
+ const stateSpace = loadStateSpaceReport(basePath);
195
+ const matrix = loadTraceabilityMatrix(basePath);
196
+ const base = basePath || process.cwd();
197
+
198
+ const models = [];
199
+
200
+ for (const [modelPath, entry] of Object.entries(registry.models || {})) {
201
+ const absPath = path.join(base, modelPath);
202
+ const exists = fs.existsSync(absPath);
203
+ const ext = path.extname(modelPath).toLowerCase();
204
+ const formalism = ext === '.tla' ? 'TLA+' : ext === '.als' ? 'Alloy' : ext === '.pm' ? 'PRISM' : 'Unknown';
205
+
206
+ let variables = [];
207
+ let properties = [];
208
+ let constructs = [];
209
+ let sigs = [];
210
+
211
+ if (exists) {
212
+ const content = fs.readFileSync(absPath, 'utf8');
213
+ if (formalism === 'TLA+') {
214
+ variables = parseTLAVariables(content);
215
+ properties = parseTLAProperties(content);
216
+ } else if (formalism === 'Alloy') {
217
+ constructs = parseAlloyConstructs(content);
218
+ sigs = parseAlloySigs(content);
219
+ }
220
+ }
221
+
222
+ // State-space info
223
+ const ssEntry = (stateSpace.models || {})[modelPath] || null;
224
+
225
+ // Properties from traceability matrix
226
+ const matrixProps = [];
227
+ for (const [key, prop] of Object.entries(matrix.properties || {})) {
228
+ if (prop.model_file === modelPath) {
229
+ matrixProps.push(prop);
230
+ }
231
+ }
232
+
233
+ models.push({
234
+ path: modelPath,
235
+ formalism,
236
+ exists,
237
+ description: entry.description || '',
238
+ version: entry.version || null,
239
+ requirements: entry.requirements || [],
240
+ variables,
241
+ properties, // TLA+ properties
242
+ constructs, // Alloy constructs
243
+ sigs, // Alloy sigs
244
+ stateSpace: ssEntry,
245
+ matrixProperties: matrixProps,
246
+ });
247
+ }
248
+
249
+ return models;
250
+ }
251
+
252
+ /**
253
+ * Build a test coverage index: requirement → test cases.
254
+ */
255
+ function buildTestIndex(basePath) {
256
+ const utc = loadUnitTestCoverage(basePath);
257
+ const reqs = loadRequirements(basePath);
258
+ const reqMap = {};
259
+
260
+ for (const r of (reqs.requirements || [])) {
261
+ reqMap[r.id] = {
262
+ id: r.id,
263
+ text: r.text || '',
264
+ category: r.category || 'Uncategorized',
265
+ status: r.status || 'Unknown',
266
+ covered: false,
267
+ testCases: [],
268
+ };
269
+ }
270
+
271
+ for (const [reqId, entry] of Object.entries(utc.requirements || {})) {
272
+ if (!reqMap[reqId]) {
273
+ reqMap[reqId] = { id: reqId, text: '', category: 'Unknown', status: 'Unknown', covered: false, testCases: [] };
274
+ }
275
+ reqMap[reqId].covered = entry.covered || false;
276
+ reqMap[reqId].testCases = (entry.test_cases || []).map(tc => ({
277
+ file: tc.test_file || '',
278
+ name: tc.test_name || '',
279
+ }));
280
+ }
281
+
282
+ return reqMap;
283
+ }
284
+
285
+ /**
286
+ * Build a variable index across all models.
287
+ * Returns array of { variable, model, domain, cardinality, bounded, risk }.
288
+ */
289
+ function buildVariableIndex(basePath) {
290
+ const models = buildModelIndex(basePath);
291
+ const stateSpace = loadStateSpaceReport(basePath);
292
+ const variables = [];
293
+
294
+ for (const model of models) {
295
+ if (model.formalism === 'TLA+') {
296
+ const ssVars = ((stateSpace.models || {})[model.path] || {}).variables || [];
297
+ const ssMap = {};
298
+ for (const sv of ssVars) { ssMap[sv.name] = sv; }
299
+
300
+ for (const v of model.variables) {
301
+ const sv = ssMap[v.name] || {};
302
+ variables.push({
303
+ name: v.name,
304
+ model: model.path,
305
+ formalism: 'TLA+',
306
+ comment: v.comment,
307
+ domain: sv.domain || null,
308
+ cardinality: sv.cardinality || null,
309
+ bounded: sv.bounded != null ? sv.bounded : null,
310
+ });
311
+ }
312
+ } else if (model.formalism === 'Alloy') {
313
+ for (const sig of model.sigs) {
314
+ for (const field of sig.fields) {
315
+ variables.push({
316
+ name: `${sig.name}.${field.name}`,
317
+ model: model.path,
318
+ formalism: 'Alloy',
319
+ comment: null,
320
+ domain: field.type,
321
+ cardinality: null,
322
+ bounded: true, // Alloy is always bounded
323
+ });
324
+ }
325
+ }
326
+ }
327
+ }
328
+
329
+ return variables;
330
+ }
331
+
332
+ /**
333
+ * Build an interconnection graph: nodes (reqs, models, tests, properties)
334
+ * and edges showing how they link together.
335
+ */
336
+ function buildInterconnectionGraph(basePath) {
337
+ const models = buildModelIndex(basePath);
338
+ const testIndex = buildTestIndex(basePath);
339
+ const matrix = loadTraceabilityMatrix(basePath);
340
+
341
+ const nodes = { requirements: {}, models: {}, properties: {}, tests: {} };
342
+ const edges = [];
343
+
344
+ // Requirements
345
+ for (const [reqId, entry] of Object.entries(testIndex)) {
346
+ nodes.requirements[reqId] = {
347
+ type: 'requirement',
348
+ id: reqId,
349
+ text: entry.text,
350
+ category: entry.category,
351
+ status: entry.status,
352
+ };
353
+ }
354
+
355
+ // Models + edges to requirements
356
+ for (const model of models) {
357
+ const shortPath = path.basename(model.path);
358
+ nodes.models[model.path] = {
359
+ type: 'model',
360
+ id: model.path,
361
+ shortName: shortPath,
362
+ formalism: model.formalism,
363
+ varCount: model.variables.length + model.sigs.reduce((n, s) => n + s.fields.length, 0),
364
+ propCount: model.properties.length + model.constructs.length,
365
+ riskLevel: model.stateSpace ? model.stateSpace.risk_level : null,
366
+ };
367
+
368
+ for (const reqId of model.requirements) {
369
+ edges.push({ from: reqId, to: model.path, type: 'req-model' });
370
+ }
371
+
372
+ // Properties → requirements
373
+ for (const prop of model.properties) {
374
+ const propKey = `${model.path}::${prop.name}`;
375
+ nodes.properties[propKey] = {
376
+ type: 'property',
377
+ id: propKey,
378
+ name: prop.name,
379
+ model: model.path,
380
+ };
381
+ for (const reqId of prop.requirements) {
382
+ edges.push({ from: reqId, to: propKey, type: 'req-property' });
383
+ }
384
+ }
385
+ for (const c of model.constructs) {
386
+ const cKey = `${model.path}::${c.name}`;
387
+ nodes.properties[cKey] = {
388
+ type: 'property',
389
+ id: cKey,
390
+ name: c.name,
391
+ kind: c.kind,
392
+ model: model.path,
393
+ };
394
+ for (const reqId of c.requirements) {
395
+ edges.push({ from: reqId, to: cKey, type: 'req-property' });
396
+ }
397
+ }
398
+ }
399
+
400
+ // Tests → requirements
401
+ for (const [reqId, entry] of Object.entries(testIndex)) {
402
+ for (const tc of entry.testCases) {
403
+ const testKey = `${tc.file}::${tc.name}`;
404
+ if (!nodes.tests[testKey]) {
405
+ nodes.tests[testKey] = { type: 'test', id: testKey, file: tc.file, name: tc.name };
406
+ }
407
+ edges.push({ from: reqId, to: testKey, type: 'req-test' });
408
+ }
409
+ }
410
+
411
+ return { nodes, edges };
412
+ }
413
+
414
+ /**
415
+ * Build a summary dashboard of the formal verification ecosystem.
416
+ */
417
+ function buildFormalSummary(basePath) {
418
+ const models = buildModelIndex(basePath);
419
+ const testIndex = buildTestIndex(basePath);
420
+ const stateSpace = loadStateSpaceReport(basePath);
421
+
422
+ const tlaModels = models.filter(m => m.formalism === 'TLA+');
423
+ const alloyModels = models.filter(m => m.formalism === 'Alloy');
424
+
425
+ // Count variables
426
+ let totalVars = 0;
427
+ let unboundedVars = 0;
428
+ for (const m of models) {
429
+ if (m.formalism === 'TLA+') totalVars += m.variables.length;
430
+ if (m.formalism === 'Alloy') totalVars += m.sigs.reduce((n, s) => n + s.fields.length, 0);
431
+ }
432
+ const ssModels = Object.values(stateSpace.models || {});
433
+ for (const ss of ssModels) {
434
+ for (const v of (ss.variables || [])) {
435
+ if (!v.bounded) unboundedVars++;
436
+ }
437
+ }
438
+
439
+ // Count properties
440
+ let totalProps = 0;
441
+ let annotatedProps = 0;
442
+ for (const m of models) {
443
+ for (const p of m.properties) {
444
+ totalProps++;
445
+ if (p.requirements.length > 0) annotatedProps++;
446
+ }
447
+ for (const c of m.constructs) {
448
+ totalProps++;
449
+ if (c.requirements.length > 0) annotatedProps++;
450
+ }
451
+ }
452
+
453
+ // Risk distribution
454
+ const byRisk = { MINIMAL: 0, LOW: 0, MODERATE: 0, HIGH: 0 };
455
+ for (const m of models) {
456
+ if (m.stateSpace && m.stateSpace.risk_level) {
457
+ byRisk[m.stateSpace.risk_level] = (byRisk[m.stateSpace.risk_level] || 0) + 1;
458
+ }
459
+ }
460
+
461
+ // Test coverage
462
+ const totalReqs = Object.keys(testIndex).length;
463
+ const coveredReqs = Object.values(testIndex).filter(t => t.covered).length;
464
+ const totalTests = Object.values(testIndex).reduce((n, t) => n + t.testCases.length, 0);
465
+
466
+ // All unique requirements across models
467
+ const allModelReqs = new Set();
468
+ for (const m of models) {
469
+ for (const r of m.requirements) allModelReqs.add(r);
470
+ }
471
+
472
+ return {
473
+ models: {
474
+ total: models.length,
475
+ tla: tlaModels.length,
476
+ alloy: alloyModels.length,
477
+ other: models.length - tlaModels.length - alloyModels.length,
478
+ },
479
+ variables: {
480
+ total: totalVars,
481
+ unbounded: unboundedVars,
482
+ },
483
+ properties: {
484
+ total: totalProps,
485
+ annotated: annotatedProps,
486
+ unannotated: totalProps - annotatedProps,
487
+ },
488
+ risk: byRisk,
489
+ coverage: {
490
+ totalReqs,
491
+ coveredReqs,
492
+ totalTests,
493
+ modelLinkedReqs: allModelReqs.size,
494
+ },
495
+ };
496
+ }
497
+
498
+ // ---------------------------------------------------------------------------
499
+ // Exports
500
+ // ---------------------------------------------------------------------------
501
+
502
+ module.exports = {
503
+ loadModelRegistry,
504
+ loadTraceabilityMatrix,
505
+ loadUnitTestCoverage,
506
+ loadStateSpaceReport,
507
+ loadRequirements,
508
+ parseTLAVariables,
509
+ parseTLAProperties,
510
+ parseAlloyConstructs,
511
+ parseAlloySigs,
512
+ buildModelIndex,
513
+ buildTestIndex,
514
+ buildVariableIndex,
515
+ buildInterconnectionGraph,
516
+ buildFormalSummary,
517
+ };
518
+
519
+ module.exports._pure = module.exports;