@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,580 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // bin/lint-formal-models.cjs
4
+ // Formal model health linter — finds fat, unbounded, and overly complex models.
5
+ // For each violation, identifies the hottest field/variable and suggests a fix.
6
+ //
7
+ // NOTE: This script uses ONLY fs/path — no child_process, no shell commands.
8
+ //
9
+ // Data sources:
10
+ // .planning/formal/policy.yaml — model_health budgets
11
+ // .planning/formal/alloy/*.als — Alloy models
12
+ // .planning/formal/state-space-report.json — pre-computed TLA+ analysis
13
+ //
14
+ // Usage:
15
+ // node bin/lint-formal-models.cjs # lint all models
16
+ // node bin/lint-formal-models.cjs --json # JSON output
17
+ // node bin/lint-formal-models.cjs --summary # counts only
18
+ //
19
+ // Exit codes:
20
+ // 0 — no violations (or mode=warn)
21
+ // 1 — violations found and mode=fail
22
+
23
+ var fs = require('fs');
24
+ var path = require('path');
25
+
26
+ var ROOT = process.cwd();
27
+ var ALLOY_DIR = path.join(ROOT, '.planning', 'formal', 'alloy');
28
+ var POLICY = path.join(ROOT, '.planning', 'formal', 'policy.yaml');
29
+ var TLA_REPORT = path.join(ROOT, '.planning', 'formal', 'state-space-report.json');
30
+ var TAG = '[lint-formal]';
31
+
32
+ var cliArgs = process.argv.slice(2);
33
+ var jsonMode = cliArgs.indexOf('--json') !== -1;
34
+ var summaryMode = cliArgs.indexOf('--summary') !== -1;
35
+
36
+ // ── Policy loader (simple YAML subset — no dependency needed) ────────────────
37
+
38
+ function loadPolicy() {
39
+ var defaults = {
40
+ max_scenarios: 1e12,
41
+ max_sigs: 12,
42
+ max_fields: 10,
43
+ require_bounded_tla: true,
44
+ mode: 'warn',
45
+ };
46
+
47
+ if (!fs.existsSync(POLICY)) return defaults;
48
+
49
+ var content = fs.readFileSync(POLICY, 'utf8');
50
+ var section = extractYamlSection(content, 'model_health');
51
+ if (!section) return defaults;
52
+
53
+ return {
54
+ max_scenarios: parseYamlNumber(section, 'max_scenarios', defaults.max_scenarios),
55
+ max_sigs: parseYamlNumber(section, 'max_sigs', defaults.max_sigs),
56
+ max_fields: parseYamlNumber(section, 'max_fields', defaults.max_fields),
57
+ require_bounded_tla: parseYamlBool(section, 'require_bounded_tla', defaults.require_bounded_tla),
58
+ mode: parseYamlString(section, 'mode', defaults.mode),
59
+ };
60
+ }
61
+
62
+ function extractYamlSection(content, key) {
63
+ var regex = new RegExp('^' + key + ':\\s*$', 'm');
64
+ var match = content.match(regex);
65
+ if (!match) return null;
66
+ var start = match.index + match[0].length;
67
+ var rest = content.substring(start);
68
+ var lines = rest.split('\n');
69
+ var sectionLines = [];
70
+ for (var i = 0; i < lines.length; i++) {
71
+ if (i > 0 && lines[i].length > 0 && !lines[i].startsWith(' ') && !lines[i].startsWith('#')) break;
72
+ sectionLines.push(lines[i]);
73
+ }
74
+ return sectionLines.join('\n');
75
+ }
76
+
77
+ function parseYamlNumber(section, key, fallback) {
78
+ var match = section.match(new RegExp('^\\s*' + key + ':\\s*([\\d.e+]+)', 'm'));
79
+ return match ? parseFloat(match[1]) : fallback;
80
+ }
81
+
82
+ function parseYamlBool(section, key, fallback) {
83
+ var match = section.match(new RegExp('^\\s*' + key + ':\\s*(true|false)', 'm'));
84
+ return match ? match[1] === 'true' : fallback;
85
+ }
86
+
87
+ function parseYamlString(section, key, fallback) {
88
+ var match = section.match(new RegExp('^\\s*' + key + ':\\s*"?([\\w]+)"?', 'm'));
89
+ return match ? match[1] : fallback;
90
+ }
91
+
92
+ // ── Alloy parser ────────────────────────────────────────────────────────────
93
+
94
+ function parseAlloySigs(content) {
95
+ var sigs = [];
96
+ var sigRegex = /\b(abstract\s+)?(one\s+|lone\s+)?sig\s+(\w+(?:\s*,\s*\w+)*)\s*(?:extends\s+(\w+)\s*)?\{([^}]*)\}/g;
97
+ var match;
98
+ while ((match = sigRegex.exec(content)) !== null) {
99
+ var isAbstract = !!match[1];
100
+ var mult = (match[2] || '').trim();
101
+ var names = match[3].split(',').map(function(n) { return n.trim(); }).filter(Boolean);
102
+ var parent = match[4] || null;
103
+ var body = match[5];
104
+ for (var i = 0; i < names.length; i++) {
105
+ sigs.push({ name: names[i], isAbstract: isAbstract, mult: mult, parent: parent, fields: parseFields(body) });
106
+ }
107
+ }
108
+ return sigs;
109
+ }
110
+
111
+ function parseFields(body) {
112
+ var fields = [];
113
+ var lines = body.split(',');
114
+ for (var i = 0; i < lines.length; i++) {
115
+ var trimmed = lines[i].replace(/--.*$/, '').trim();
116
+ if (!trimmed) continue;
117
+ var m = trimmed.match(/^(\w+)\s*:\s*(one\s+|set\s+|lone\s+|seq\s+)?(.+)$/);
118
+ if (m) {
119
+ fields.push({ name: m[1], mult: (m[2] || 'one').trim(), type: m[3].trim() });
120
+ }
121
+ }
122
+ return fields;
123
+ }
124
+
125
+ function parseAlloyCommands(content) {
126
+ var commands = [];
127
+ var cmdRegex = /\b(run|check)\s+(?:(\w+)\s*)?(?:\{[^}]*\}\s*)?for\s+(.+)/g;
128
+ var match;
129
+ while ((match = cmdRegex.exec(content)) !== null) {
130
+ commands.push({
131
+ type: match[1],
132
+ name: match[2] || '(anon)',
133
+ scopeStr: match[3].trim(),
134
+ scope: parseScopeStr(match[3].trim()),
135
+ });
136
+ }
137
+ return commands;
138
+ }
139
+
140
+ function parseScopeStr(scopeStr) {
141
+ var scope = {};
142
+ var defaultScope = null;
143
+ var clean = scopeStr.replace(/--.*$/, '').trim();
144
+ var butMatch = clean.match(/^(\d+)\s+but\s+(.+)$/);
145
+ var entries = butMatch ? butMatch[2] : clean;
146
+ if (butMatch) defaultScope = parseInt(butMatch[1], 10);
147
+
148
+ var parts = entries.split(',');
149
+ for (var i = 0; i < parts.length; i++) {
150
+ var trimmed = parts[i].trim();
151
+ var m = trimmed.match(/^(\d+)\s+(\w+)$/);
152
+ if (m) {
153
+ scope[m[2]] = parseInt(m[1], 10);
154
+ } else if (defaultScope === null) {
155
+ var bare = trimmed.match(/^(\d+)$/);
156
+ if (bare) defaultScope = parseInt(bare[1], 10);
157
+ }
158
+ }
159
+ if (defaultScope !== null) scope._default = defaultScope;
160
+ return scope;
161
+ }
162
+
163
+ function getSigScope(sigName, scope, sigs) {
164
+ if (scope[sigName] !== undefined) return scope[sigName];
165
+ var sig = sigs.find(function(s) { return s.name === sigName; });
166
+ if (sig && (sig.mult === 'one' || sig.mult === 'lone')) return 1;
167
+ if (scope._default !== undefined) return scope._default;
168
+ return 3;
169
+ }
170
+
171
+ // ── Heat analysis — finds which field contributes most to blowup ────────────
172
+
173
+ function analyzeAlloyHeat(sigs, scope) {
174
+ var intBits = scope['int'] || scope['Int'] || 4;
175
+ var intRange = Math.pow(2, intBits);
176
+
177
+ var sigAtoms = {};
178
+ var i, j, sig;
179
+ for (i = 0; i < sigs.length; i++) {
180
+ sig = sigs[i];
181
+ if (sig.isAbstract && !sig.mult) {
182
+ sigAtoms[sig.name] = 0;
183
+ } else {
184
+ sigAtoms[sig.name] = getSigScope(sig.name, scope, sigs);
185
+ }
186
+ }
187
+ for (i = 0; i < sigs.length; i++) {
188
+ sig = sigs[i];
189
+ if (sig.isAbstract && !sig.mult) {
190
+ var children = sigs.filter(function(s) { return s.parent === sig.name; });
191
+ var sum = 0;
192
+ for (j = 0; j < children.length; j++) sum += (sigAtoms[children[j].name] || 0);
193
+ sigAtoms[sig.name] = sum;
194
+ }
195
+ }
196
+
197
+ var heatMap = [];
198
+ var totalScenarios = 1n;
199
+
200
+ for (i = 0; i < sigs.length; i++) {
201
+ sig = sigs[i];
202
+ var atomCount = sigAtoms[sig.name] || 0;
203
+ if (atomCount === 0) continue;
204
+
205
+ for (var k = 0; k < sig.fields.length; k++) {
206
+ var field = sig.fields[k];
207
+ var targetSize;
208
+ if (field.type === 'Int' || field.type === 'int') {
209
+ targetSize = intRange;
210
+ } else if (field.type === 'Bool' || field.type === 'BOOLEAN') {
211
+ targetSize = 2;
212
+ } else if (sigAtoms[field.type] !== undefined) {
213
+ targetSize = sigAtoms[field.type];
214
+ } else {
215
+ targetSize = scope._default || 3;
216
+ }
217
+ if (targetSize <= 0) targetSize = 1;
218
+
219
+ var fieldScenarios;
220
+ if (field.mult === 'one') {
221
+ fieldScenarios = BigInt(targetSize) ** BigInt(atomCount);
222
+ } else if (field.mult === 'lone') {
223
+ fieldScenarios = BigInt(targetSize + 1) ** BigInt(atomCount);
224
+ } else if (field.mult === 'set') {
225
+ fieldScenarios = (2n ** BigInt(targetSize)) ** BigInt(atomCount);
226
+ } else if (field.mult === 'seq') {
227
+ fieldScenarios = BigInt(targetSize) ** BigInt(atomCount * 3);
228
+ } else {
229
+ fieldScenarios = BigInt(targetSize) ** BigInt(atomCount);
230
+ }
231
+
232
+ totalScenarios *= fieldScenarios;
233
+ heatMap.push({
234
+ sig: sig.name, field: field.name, mult: field.mult, type: field.type,
235
+ targetSize: targetSize, atomCount: atomCount, contribution: fieldScenarios,
236
+ });
237
+ }
238
+ }
239
+
240
+ heatMap.sort(function(a, b) {
241
+ if (a.contribution > b.contribution) return -1;
242
+ if (a.contribution < b.contribution) return 1;
243
+ return 0;
244
+ });
245
+
246
+ return { totalScenarios: totalScenarios, heatMap: heatMap, sigAtoms: sigAtoms };
247
+ }
248
+
249
+ // ── Fix suggestions ─────────────────────────────────────────────────────────
250
+
251
+ function suggestAlloyFix(heatMap, totalScenarios, budget, sigAtoms) {
252
+ var suggestions = [];
253
+ if (heatMap.length === 0) return suggestions;
254
+
255
+ var hottest = heatMap[0];
256
+
257
+ // 1. Constrain set fields (biggest blowup source)
258
+ if (hottest.mult === 'set') {
259
+ suggestions.push({
260
+ priority: 'high',
261
+ type: 'constrain-set',
262
+ message: 'Hottest: ' + hottest.sig + '.' + hottest.field +
263
+ ' (set ' + hottest.type + ', ' + hottest.atomCount + ' atoms) -> ' + formatBig(hottest.contribution) +
264
+ ' scenarios. Add: fact { all x: ' + hottest.sig + ' | #x.' + hottest.field + ' <= 2 }',
265
+ });
266
+ }
267
+
268
+ // 2. Reduce scope of dominant sig
269
+ if (hottest.atomCount > 3) {
270
+ var reduced = hottest.atomCount;
271
+ var budgetBig = BigInt(Math.floor(budget));
272
+ while (reduced > 1) {
273
+ reduced--;
274
+ var ratio = Math.pow(hottest.targetSize, hottest.atomCount - reduced);
275
+ var est = totalScenarios / BigInt(Math.max(1, Math.floor(ratio)));
276
+ if (est <= budgetBig) break;
277
+ }
278
+ suggestions.push({
279
+ priority: 'medium',
280
+ type: 'reduce-scope',
281
+ message: 'Reduce ' + hottest.sig + ' scope from ' + hottest.atomCount + ' to ' + reduced +
282
+ ' in run/check commands.',
283
+ });
284
+ }
285
+
286
+ // 3. Split if too many sigs
287
+ var activeSigs = Object.keys(sigAtoms).filter(function(k) { return sigAtoms[k] > 0; });
288
+ if (activeSigs.length > 8) {
289
+ suggestions.push({
290
+ priority: 'medium',
291
+ type: 'split-model',
292
+ message: activeSigs.length + ' active sigs. Split into smaller models, each verifying a property subset.' +
293
+ ' Overlapping sigs at seams is fine — it tests coupling.',
294
+ });
295
+ }
296
+
297
+ // 4. Reduce Int bitwidth
298
+ for (var i = 0; i < heatMap.length; i++) {
299
+ if ((heatMap[i].type === 'Int' || heatMap[i].type === 'int') && heatMap[i].targetSize > 16) {
300
+ suggestions.push({
301
+ priority: 'low',
302
+ type: 'reduce-int-bits',
303
+ message: 'Int bitwidth gives ' + heatMap[i].targetSize + ' values. Use "4 int" (16 values) unless wider range needed.',
304
+ });
305
+ break;
306
+ }
307
+ }
308
+
309
+ return suggestions;
310
+ }
311
+
312
+ function suggestTLAFix(data) {
313
+ var suggestions = [];
314
+ if (data.has_unbounded) {
315
+ var domains = data.unbounded_domains || [];
316
+ for (var i = 0; i < domains.length; i++) {
317
+ suggestions.push({
318
+ priority: 'high',
319
+ type: 'add-bound',
320
+ message: 'Unbounded: ' + domains[i] + '. Add .cfg with CONSTANTS bounding this domain.',
321
+ });
322
+ }
323
+ }
324
+ if (!data.cfg_file) {
325
+ suggestions.push({
326
+ priority: 'high',
327
+ type: 'add-cfg',
328
+ message: 'No .cfg file. Create one with SPECIFICATION, CONSTANTS, INVARIANT to enable TLC checking.',
329
+ });
330
+ }
331
+ return suggestions;
332
+ }
333
+
334
+ // ── Lint engine ─────────────────────────────────────────────────────────────
335
+
336
+ function lintAlloyModels(policy) {
337
+ var findings = [];
338
+ if (!fs.existsSync(ALLOY_DIR)) return findings;
339
+
340
+ var files = fs.readdirSync(ALLOY_DIR).filter(function(f) { return f.endsWith('.als'); }).sort();
341
+
342
+ for (var i = 0; i < files.length; i++) {
343
+ var filePath = path.join(ALLOY_DIR, files[i]);
344
+ var modelName = files[i].replace('.als', '');
345
+
346
+ try {
347
+ var content = fs.readFileSync(filePath, 'utf8');
348
+ var sigs = parseAlloySigs(content);
349
+ var commands = parseAlloyCommands(content);
350
+ var violations = [];
351
+ var suggestions = [];
352
+
353
+ if (sigs.length > policy.max_sigs) {
354
+ violations.push({ rule: 'max-sigs', message: sigs.length + ' sigs (max ' + policy.max_sigs + ')' });
355
+ }
356
+
357
+ var totalFields = sigs.reduce(function(s, sig) { return s + sig.fields.length; }, 0);
358
+ if (totalFields > policy.max_fields) {
359
+ violations.push({ rule: 'max-fields', message: totalFields + ' fields (max ' + policy.max_fields + ')' });
360
+ }
361
+
362
+ var worstScenarios = 0n;
363
+ var worstScope = null;
364
+ var worstHeat = null;
365
+ var worstSigAtoms = null;
366
+
367
+ for (var c = 0; c < commands.length; c++) {
368
+ var analysis = analyzeAlloyHeat(sigs, commands[c].scope);
369
+ if (analysis.totalScenarios > worstScenarios) {
370
+ worstScenarios = analysis.totalScenarios;
371
+ worstScope = commands[c].scopeStr;
372
+ worstHeat = analysis.heatMap;
373
+ worstSigAtoms = analysis.sigAtoms;
374
+ }
375
+ }
376
+
377
+ if (worstScenarios > BigInt(Math.floor(policy.max_scenarios))) {
378
+ violations.push({
379
+ rule: 'max-scenarios',
380
+ message: formatBig(worstScenarios) + ' scenarios (max ' + formatBig(BigInt(Math.floor(policy.max_scenarios))) + ')',
381
+ scope: worstScope,
382
+ });
383
+ if (worstHeat && worstSigAtoms) {
384
+ suggestions = suggestAlloyFix(worstHeat, worstScenarios, policy.max_scenarios, worstSigAtoms);
385
+ }
386
+ }
387
+
388
+ var heatSummary = [];
389
+ if (worstHeat) {
390
+ for (var h = 0; h < Math.min(3, worstHeat.length); h++) {
391
+ var entry = worstHeat[h];
392
+ heatSummary.push({
393
+ sig: entry.sig, field: entry.field, mult: entry.mult,
394
+ type: entry.type, contribution: formatBig(entry.contribution),
395
+ });
396
+ }
397
+ }
398
+
399
+ findings.push({
400
+ framework: 'Alloy', model: modelName, file: files[i],
401
+ scenarios: worstScenarios, scenariosStr: formatBig(worstScenarios),
402
+ sigCount: sigs.length, fieldCount: totalFields, commandCount: commands.length,
403
+ violations: violations, suggestions: suggestions, heatMap: heatSummary,
404
+ pass: violations.length === 0,
405
+ });
406
+ } catch (err) {
407
+ findings.push({
408
+ framework: 'Alloy', model: modelName, file: files[i],
409
+ scenarios: null, scenariosStr: '?', sigCount: 0, fieldCount: 0, commandCount: 0,
410
+ violations: [{ rule: 'parse-error', message: err.message }],
411
+ suggestions: [], heatMap: [], pass: false,
412
+ });
413
+ }
414
+ }
415
+ return findings;
416
+ }
417
+
418
+ function lintTLAModels(policy) {
419
+ var findings = [];
420
+ if (!fs.existsSync(TLA_REPORT)) return findings;
421
+
422
+ var report;
423
+ try { report = JSON.parse(fs.readFileSync(TLA_REPORT, 'utf8')); } catch (_) { return findings; }
424
+
425
+ var entries = Object.entries(report.models || {});
426
+ for (var i = 0; i < entries.length; i++) {
427
+ var modelPath = entries[i][0];
428
+ var data = entries[i][1];
429
+ var name = data.module_name || path.basename(modelPath, '.tla');
430
+ if (name.indexOf('_TTrace_') !== -1) continue;
431
+
432
+ var violations = [];
433
+ var suggestions = [];
434
+ var states = data.estimated_states;
435
+
436
+ if (policy.require_bounded_tla && data.has_unbounded) {
437
+ violations.push({ rule: 'unbounded-tla', message: 'Unbounded: ' + (data.unbounded_domains || []).join(', ') });
438
+ suggestions = suggestTLAFix(data);
439
+ }
440
+ if (states !== null && states > policy.max_scenarios) {
441
+ violations.push({ rule: 'max-scenarios', message: states + ' states (max ' + policy.max_scenarios + ')' });
442
+ }
443
+ if (!data.cfg_file && policy.require_bounded_tla) {
444
+ violations.push({ rule: 'missing-cfg', message: 'No .cfg file — cannot be checked by TLC' });
445
+ if (suggestions.length === 0) suggestions = suggestTLAFix(data);
446
+ }
447
+
448
+ findings.push({
449
+ framework: 'TLA+', model: name, file: path.basename(modelPath),
450
+ scenarios: states !== null ? BigInt(states) : null,
451
+ scenariosStr: states !== null ? String(states) : (data.has_unbounded ? 'UNBOUNDED' : '?'),
452
+ sigCount: (data.variables || []).length, fieldCount: 0,
453
+ commandCount: (data.invariant_count || 0) + (data.property_count || 0),
454
+ violations: violations, suggestions: suggestions, heatMap: [],
455
+ pass: violations.length === 0,
456
+ });
457
+ }
458
+ return findings;
459
+ }
460
+
461
+ // ── Output ──────────────────────────────────────────────────────────────────
462
+
463
+ function formatBig(n) {
464
+ if (n === null || n === undefined) return '?';
465
+ var s = n.toString();
466
+ if (s.length <= 6) return s;
467
+ return s[0] + '.' + s.substring(1, 3) + 'e' + (s.length - 1);
468
+ }
469
+
470
+ function printFindings(findings, policy) {
471
+ var bad = findings.filter(function(f) { return !f.pass; });
472
+ var good = findings.filter(function(f) { return f.pass; });
473
+
474
+ if (summaryMode) {
475
+ console.log(TAG + ' ' + findings.length + ' models scanned');
476
+ console.log(TAG + ' ' + good.length + ' pass, ' + bad.length + ' violations');
477
+ if (bad.length > 0) {
478
+ for (var i = 0; i < bad.length; i++) {
479
+ var rules = bad[i].violations.map(function(v) { return v.rule; }).join(', ');
480
+ console.log(TAG + ' ' + bad[i].framework + '/' + bad[i].model + ': ' + rules);
481
+ }
482
+ }
483
+ return;
484
+ }
485
+
486
+ console.log('');
487
+ console.log('Formal Model Health Report');
488
+ console.log('='.repeat(70));
489
+ console.log('Policy: max_scenarios=' + policy.max_scenarios +
490
+ ' max_sigs=' + policy.max_sigs + ' max_fields=' + policy.max_fields +
491
+ ' mode=' + policy.mode);
492
+ console.log('');
493
+
494
+ if (bad.length > 0) {
495
+ console.log('VIOLATIONS (' + bad.length + ')');
496
+ console.log('-'.repeat(70));
497
+
498
+ for (var i = 0; i < bad.length; i++) {
499
+ var f = bad[i];
500
+ console.log('');
501
+ console.log(' ' + f.framework + '/' + f.model + ' (' + f.file + ')');
502
+ console.log(' Scenarios: ' + f.scenariosStr + ' | Sigs: ' + f.sigCount + ' | Fields: ' + f.fieldCount);
503
+
504
+ for (var v = 0; v < f.violations.length; v++) {
505
+ console.log(' VIOLATION: ' + f.violations[v].message);
506
+ }
507
+
508
+ if (f.heatMap.length > 0) {
509
+ console.log(' Hottest fields:');
510
+ for (var h = 0; h < f.heatMap.length; h++) {
511
+ var e = f.heatMap[h];
512
+ console.log(' ' + (h + 1) + '. ' + e.sig + '.' + e.field +
513
+ ' (' + e.mult + ' ' + e.type + ') -> ' + e.contribution);
514
+ }
515
+ }
516
+
517
+ if (f.suggestions.length > 0) {
518
+ console.log(' Fix:');
519
+ for (var s = 0; s < f.suggestions.length; s++) {
520
+ console.log(' [' + f.suggestions[s].priority + '] ' + f.suggestions[s].message);
521
+ }
522
+ }
523
+ }
524
+ }
525
+
526
+ if (good.length > 0) {
527
+ console.log('');
528
+ console.log('PASSING (' + good.length + ')');
529
+ console.log('-'.repeat(70));
530
+ for (var i = 0; i < good.length; i++) {
531
+ console.log(' ' + good[i].framework + '/' + good[i].model + ': ' + good[i].scenariosStr);
532
+ }
533
+ }
534
+
535
+ console.log('');
536
+ console.log('='.repeat(70));
537
+ console.log('Total: ' + findings.length + ' | Pass: ' + good.length + ' | Violations: ' + bad.length +
538
+ ' | Mode: ' + policy.mode);
539
+ if (bad.length > 0 && policy.mode === 'warn') {
540
+ console.log('(mode=warn: exit 0 despite violations)');
541
+ }
542
+ }
543
+
544
+ // ── Main ─────────────────────────────────────────────────────────────────────
545
+
546
+ function main() {
547
+ var policy = loadPolicy();
548
+ var findings = lintAlloyModels(policy).concat(lintTLAModels(policy));
549
+
550
+ findings.sort(function(a, b) {
551
+ if (a.pass !== b.pass) return a.pass ? 1 : -1;
552
+ var sa = a.scenarios || 0n;
553
+ var sb = b.scenarios || 0n;
554
+ if (sa > sb) return -1;
555
+ if (sa < sb) return 1;
556
+ return 0;
557
+ });
558
+
559
+ if (jsonMode) {
560
+ var jsonFindings = findings.map(function(f) {
561
+ return Object.assign({}, f, { scenarios: f.scenarios !== null ? f.scenarios.toString() : null });
562
+ });
563
+ process.stdout.write(JSON.stringify({
564
+ policy: policy, findings: jsonFindings,
565
+ summary: { total: findings.length, pass: good(findings), violations: bad(findings) },
566
+ }, null, 2) + '\n');
567
+ return;
568
+ }
569
+
570
+ printFindings(findings, policy);
571
+
572
+ if (findings.some(function(f) { return !f.pass; }) && policy.mode === 'fail') {
573
+ process.exit(1);
574
+ }
575
+ }
576
+
577
+ function good(f) { return f.filter(function(x) { return x.pass; }).length; }
578
+ function bad(f) { return f.filter(function(x) { return !x.pass; }).length; }
579
+
580
+ main();