thumbgate 1.34.3 → 1.37.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 (200) hide show
  1. package/.agents/skills/cyberstrike-compare-not-clone/SKILL.md +36 -0
  2. package/.agents/skills/gitlab-sandbox-allowlist-not-trust/SKILL.md +77 -0
  3. package/.agents/skills/jit-harness-compare-not-clone/SKILL.md +34 -0
  4. package/.agents/skills/openui-catalog-compose-honesty/SKILL.md +64 -0
  5. package/.agents/skills/zvec-grep-compare-not-clone/SKILL.md +34 -0
  6. package/.claude-plugin/plugin.json +1 -1
  7. package/.well-known/llms.txt +1 -0
  8. package/.well-known/mcp/server-card.json +1 -1
  9. package/CONTRIBUTING.md +95 -0
  10. package/README.md +195 -632
  11. package/THIRD_PARTY_NOTICES.md +89 -0
  12. package/adapters/claude/.mcp.json +2 -2
  13. package/adapters/forge/forge.yaml +3 -3
  14. package/adapters/future-agi/.mcp.json +8 -0
  15. package/adapters/future-agi/FUTURE_AGI.md +23 -0
  16. package/adapters/future-agi/config.toml +3 -0
  17. package/adapters/future-agi/future-agi-bridge.js +9 -0
  18. package/adapters/future-agi/opencode.json +8 -0
  19. package/adapters/herdr/herdr-plugin.toml +18 -0
  20. package/adapters/mcp/server-stdio.js +238 -25
  21. package/adapters/opencode/opencode.json +1 -1
  22. package/adapters/workos/WORKOS.md +52 -0
  23. package/bin/cli.js +373 -5
  24. package/bin/futureagi-bridge +9 -0
  25. package/config/gate-templates.json +653 -4
  26. package/config/gates/actor-critic-audit.json +34 -0
  27. package/config/gates/default.json +21 -2
  28. package/config/gates/five-walls-governance.json +34 -0
  29. package/config/gates/future-agi-guardrails.json +34 -0
  30. package/config/gates/radware-threat-defense-2026.json +61 -0
  31. package/config/gates/simatree-data-governance.json +33 -0
  32. package/config/mcp-allowlists.json +4 -0
  33. package/config/merge-quality-checks.json +10 -1
  34. package/config/model-candidates.json +382 -24
  35. package/config/model-tiers.json +18 -0
  36. package/config/post-deploy-marketing-pages.json +10 -0
  37. package/config/progressive/01-wire-only.json +11 -0
  38. package/config/progressive/02-dashboard-empty-ok.json +10 -0
  39. package/config/progressive/03-one-lesson.json +10 -0
  40. package/config/progressive/04-warn-fires.json +11 -0
  41. package/config/progressive/05-strict-optional.json +11 -0
  42. package/config/progressive/README.md +15 -0
  43. package/config/schemas/broker-execution-receipt.schema.json +139 -0
  44. package/config/schemas/provider-execution-attestation-v1.schema.json +58 -0
  45. package/conformance/provider-attestation/vectors.json +320 -0
  46. package/docs/specs/provider-execution-attestation-v1.md +69 -0
  47. package/openapi/openapi.yaml +15 -0
  48. package/package.json +401 -147
  49. package/public/about.html +2 -2
  50. package/public/ai-malpractice-prevention.html +7 -7
  51. package/public/blog/a-10-dollar-vps-is-not-a-computer.html +143 -0
  52. package/public/blog/a-receipt-is-not-world-state.html +388 -0
  53. package/public/blog/git-at-agent-scale.html +374 -0
  54. package/public/blog/no-llm-in-the-gate.html +133 -0
  55. package/public/blog.html +80 -0
  56. package/public/case-studies.html +16 -1
  57. package/public/compare.html +28 -0
  58. package/public/diagnostic.html +216 -7
  59. package/public/docs/connectors.html +39 -0
  60. package/public/federal.html +2 -2
  61. package/public/founders.html +639 -0
  62. package/public/index.html +87 -9
  63. package/public/install.html +8 -8
  64. package/public/learn.html +39 -0
  65. package/public/numbers.html +2 -2
  66. package/public/peter.html +310 -0
  67. package/public/platform-partners.html +119 -0
  68. package/public/pricing.html +24 -3
  69. package/public/privacy.html +117 -0
  70. package/public/pro.html +17 -0
  71. package/public/support.html +62 -0
  72. package/public/terms.html +130 -0
  73. package/public/third-party-notices.html +95 -0
  74. package/public/yt.html +351 -0
  75. package/scripts/action-receipts.js +133 -3
  76. package/scripts/adaptive-governance-arena.js +349 -0
  77. package/scripts/admin-override.js +205 -0
  78. package/scripts/agent-action-inventory.js +869 -0
  79. package/scripts/agent-audit-trace.js +42 -2
  80. package/scripts/agent-egress-policy.js +1117 -0
  81. package/scripts/agent-memory-lifecycle.js +141 -2
  82. package/scripts/agent-operations-planner.js +441 -1
  83. package/scripts/agent-readiness.js +68 -0
  84. package/scripts/agent-security-central.js +647 -0
  85. package/scripts/allowlist-bridge-honesty.js +417 -0
  86. package/scripts/async-job-runner.js +102 -11
  87. package/scripts/audit-trail.js +212 -0
  88. package/scripts/auto-promote-gates.js +178 -27
  89. package/scripts/billing.js +1 -1
  90. package/scripts/broker-execution-receipts.js +719 -0
  91. package/scripts/budget-aware-gates-proof.js +423 -0
  92. package/scripts/claude-feedback-sync.js +29 -3
  93. package/scripts/claw-harness-production.js +237 -0
  94. package/scripts/cli-progress.js +111 -0
  95. package/scripts/cli-schema.js +163 -1
  96. package/scripts/codex-runbook-flywheel.js +318 -0
  97. package/scripts/context-footprint.js +186 -0
  98. package/scripts/contextfs.js +143 -61
  99. package/scripts/dashboard-limits.js +27 -0
  100. package/scripts/dashboard.js +279 -9
  101. package/scripts/deepseek-v4-runtime-guardrails.js +72 -6
  102. package/scripts/docker-sandbox-planner.js +18 -0
  103. package/scripts/double-blind-eval-protocol.js +252 -0
  104. package/scripts/edotenv-rl-gateway.js +259 -0
  105. package/scripts/ensure-production-search-corpus.js +162 -0
  106. package/scripts/eval-holdout.js +311 -0
  107. package/scripts/feedback-aggregate.js +21 -2
  108. package/scripts/feedback-loop.js +87 -5
  109. package/scripts/feedback-quality.js +9 -0
  110. package/scripts/file-ledger-lock.js +4 -1
  111. package/scripts/financial-control-plane.js +41 -1
  112. package/scripts/find-dormant-requires.js +118 -0
  113. package/scripts/fs-utils.js +84 -8
  114. package/scripts/gate-stats.js +2 -2
  115. package/scripts/gates-engine.js +859 -58
  116. package/scripts/generate-case-study-outreach.js +24 -15
  117. package/scripts/git-at-scale.js +628 -0
  118. package/scripts/governance-conflict-audit.js +1650 -0
  119. package/scripts/governance-difficulty-curriculum.js +328 -0
  120. package/scripts/graphrag-retrieval.js +275 -0
  121. package/scripts/gurobi-optimizer.js +324 -0
  122. package/scripts/gurobi_optimizer.py +485 -0
  123. package/scripts/harness-selector.js +82 -1
  124. package/scripts/hidden-entry-points.js +284 -0
  125. package/scripts/human-escalation.js +199 -1
  126. package/scripts/hybrid-feedback-context.js +152 -19
  127. package/scripts/intent-governed-execution.js +602 -0
  128. package/scripts/intervention-policy.js +123 -20
  129. package/scripts/jit-harness-compose.js +628 -0
  130. package/scripts/jsonl-watcher.js +10 -0
  131. package/scripts/lesson-embedding-index.js +95 -12
  132. package/scripts/lesson-retrieval.js +105 -19
  133. package/scripts/local-model-profile.js +19 -2
  134. package/scripts/mailer/resend-mailer.js +1 -1
  135. package/scripts/matryoshka-embedding.js +235 -0
  136. package/scripts/mcp-oauth.js +42 -4
  137. package/scripts/mcp-session-handles.js +1016 -0
  138. package/scripts/mcp-wiring-doctor.js +314 -0
  139. package/scripts/memory-firewall.js +115 -2
  140. package/scripts/memory-scope-readiness.js +299 -0
  141. package/scripts/memory-vs-rag-route.js +161 -0
  142. package/scripts/model-tier-router.js +148 -21
  143. package/scripts/nvidia-specdecode-al-doctor.js +536 -0
  144. package/scripts/openui-catalog-compose-honesty.js +593 -0
  145. package/scripts/operational-integrity.js +19 -1
  146. package/scripts/override-audit.js +213 -0
  147. package/scripts/package-manager-honesty-doctor.js +458 -0
  148. package/scripts/pr-manager.js +63 -1
  149. package/scripts/prove-herdr-adapter.js +52 -0
  150. package/scripts/prove-memory-pyramid-and-symbolic-canvas.js +95 -0
  151. package/scripts/prove-workos.js +73 -0
  152. package/scripts/provider-attestation-conformance.js +192 -0
  153. package/scripts/provider-receipt-contract.js +136 -0
  154. package/scripts/qwen38-max-cost-optimizer.js +401 -0
  155. package/scripts/radware-threat-defense.js +280 -0
  156. package/scripts/rag-embedding-identity.js +221 -0
  157. package/scripts/rag-precision-guardrails.js +112 -2
  158. package/scripts/remote-feedback-capture.js +159 -0
  159. package/scripts/research-agent-harness.js +256 -0
  160. package/scripts/rsi-safety-hillclimb.js +200 -0
  161. package/scripts/rule-sprawl.js +188 -0
  162. package/scripts/schedule-manager.js +147 -0
  163. package/scripts/self-heal.js +8 -0
  164. package/scripts/session-lease.js +415 -0
  165. package/scripts/simatree-data-governance.js +347 -0
  166. package/scripts/slo-alert-engine.js +172 -7
  167. package/scripts/solver-parity.js +539 -0
  168. package/scripts/stealth-memory-injection-gate.js +333 -0
  169. package/scripts/switchyard-router.js +366 -0
  170. package/scripts/telemetry-analytics.js +84 -27
  171. package/scripts/temporal-decay-weighting.js +138 -0
  172. package/scripts/test-all.js +165 -0
  173. package/scripts/token-savings.js +42 -0
  174. package/scripts/tool-kpi-tracker.js +108 -5
  175. package/scripts/tool-registry.js +193 -5
  176. package/scripts/universal-claim-evaluator.js +14 -2
  177. package/scripts/vector-store.js +279 -9
  178. package/scripts/workflow-notebook.js +391 -0
  179. package/scripts/workflow-sentinel.js +111 -12
  180. package/scripts/workos-production-guard.js +260 -0
  181. package/scripts/workspace-search-route.js +515 -0
  182. package/server.json +2 -2
  183. package/src/agent-identity-boundary.js +76 -0
  184. package/src/agent-retrieval-cache.js +155 -0
  185. package/src/alert-noise-ledger.js +502 -0
  186. package/src/api/server.js +802 -185
  187. package/src/git-fast-cache.js +220 -0
  188. package/src/git-wal-sync.js +156 -0
  189. package/src/hash-anchored-edit.js +82 -0
  190. package/src/hermes-platform-protocol.js +475 -0
  191. package/src/hermes-sync-plane.js +241 -0
  192. package/src/index.js +30 -1
  193. package/src/iso42001-compliance-guard.js +97 -0
  194. package/src/latency-budget.js +244 -0
  195. package/src/mcp-writeguard.js +316 -0
  196. package/src/miminions-adapter.js +106 -0
  197. package/src/pipeline-compass.js +104 -0
  198. package/src/ppl-alert-pipeline.js +284 -0
  199. package/src/rendezvous-router.js +90 -0
  200. package/src/security-questionnaire.js +195 -0
@@ -0,0 +1,539 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Frozen solver acceptance-set + independent-engine parity.
6
+ *
7
+ * Process stolen from Gurobi Phase One Champion Kit evaluation/migration
8
+ * playbooks — not their product, branding, PDFs, or sales theater:
9
+ * 1. Freeze the acceptance set before engines run
10
+ * 2. Compare like-for-like (same fixtures, same budgets, same host)
11
+ * 3. Do not grade your own homework (status-quo heuristic vs solver)
12
+ *
13
+ * ECI: existing-surface maintenance. No enterprise SKU, no $499 pitch,
14
+ * no captured cash, no Gurobi affiliation.
15
+ *
16
+ * Usage:
17
+ * node scripts/solver-parity.js
18
+ * node scripts/solver-parity.js --json
19
+ * node scripts/solver-parity.js --acceptance tests/fixtures/solver-acceptance-set.json
20
+ * npm run solver:parity
21
+ */
22
+
23
+ const fs = require('node:fs');
24
+ const path = require('node:path');
25
+ const {
26
+ optimizeModelRouting,
27
+ optimizeRuleSelection,
28
+ probeGurobi,
29
+ } = require('./gurobi-optimizer');
30
+
31
+ const DEFAULT_ACCEPTANCE_PATH = path.join(
32
+ __dirname,
33
+ '..',
34
+ 'tests',
35
+ 'fixtures',
36
+ 'solver-acceptance-set.json'
37
+ );
38
+
39
+ const SCHEMA = 'thumbgate.solver_acceptance_set.v1';
40
+ const REPORT_SCHEMA = 'thumbgate.solver_parity_report.v1';
41
+
42
+ function loadAcceptanceSet(filePath = DEFAULT_ACCEPTANCE_PATH) {
43
+ const abs = path.resolve(filePath);
44
+ if (!fs.existsSync(abs)) {
45
+ throw new Error(`acceptance set not found: ${abs}`);
46
+ }
47
+ const parsed = JSON.parse(fs.readFileSync(abs, 'utf8'));
48
+ if (!parsed || parsed.schema !== SCHEMA) {
49
+ throw new Error(`acceptance set schema must be ${SCHEMA}`);
50
+ }
51
+ if (!Array.isArray(parsed.cases) || parsed.cases.length === 0) {
52
+ throw new Error('acceptance set must include a non-empty cases array');
53
+ }
54
+ return parsed;
55
+ }
56
+
57
+ function isGurobiErrorFallback(label) {
58
+ const s = String(label || '').toLowerCase();
59
+ return s.includes('gurobi-error')
60
+ || s.includes('error-fallback')
61
+ || /gurobi.*(?:error|unavailable|license)/.test(s);
62
+ }
63
+
64
+ function isSuccessfulGurobiLabel(label) {
65
+ const s = String(label || '').toLowerCase();
66
+ if (!s) return false;
67
+ if (isGurobiErrorFallback(s)) return false;
68
+ return s === 'gurobi'
69
+ || s === 'gurobipy'
70
+ || s === 'gurobi-milp'
71
+ || s.startsWith('gurobi-ok');
72
+ }
73
+
74
+ function isHeuristicSolver(label) {
75
+ const s = String(label || '').toLowerCase();
76
+ return s.includes('heuristic') || s.includes('knapsack') || s.includes('fallback');
77
+ }
78
+
79
+ function feasibleRoutingIds(candidates, budgets) {
80
+ const list = Array.isArray(candidates) ? candidates : [];
81
+ const { maxBudgetUsd, maxLatencyMs } = budgets || {};
82
+ return list
83
+ .filter((c) => (c.cost || 0) <= maxBudgetUsd && (c.latency_ms || 0) <= maxLatencyMs)
84
+ .map((c) => c.id);
85
+ }
86
+
87
+ function independentHeuristicRouting(candidates, budgets) {
88
+ const list = Array.isArray(candidates) ? candidates : [];
89
+ if (list.length === 0) {
90
+ return {
91
+ success: false,
92
+ selected: null,
93
+ solver: 'independent-greedy-heuristic',
94
+ objective: 0,
95
+ reason: 'no_candidates',
96
+ };
97
+ }
98
+ const valid = list.filter(
99
+ (c) => (c.cost || 0) <= budgets.maxBudgetUsd && (c.latency_ms || 0) <= budgets.maxLatencyMs
100
+ );
101
+ if (valid.length === 0) {
102
+ return {
103
+ success: false,
104
+ selected: null,
105
+ solver: 'independent-greedy-heuristic',
106
+ objective: 0,
107
+ reason: 'no_feasible_candidates',
108
+ violations: list.map((c) => c.id),
109
+ };
110
+ }
111
+ const best = valid.reduce(
112
+ (prev, curr) => ((curr.score || 0) > (prev.score || 0) ? curr : prev),
113
+ valid[0]
114
+ );
115
+ return {
116
+ success: true,
117
+ selected: best.id,
118
+ solver: 'independent-greedy-heuristic',
119
+ objective: best.score || 0,
120
+ };
121
+ }
122
+
123
+ function independentHeuristicRules(rules, budgets) {
124
+ const list = Array.isArray(rules) ? rules : [];
125
+ if (list.length === 0) {
126
+ return {
127
+ success: false,
128
+ selected_rules: [],
129
+ solver: 'independent-greedy-knapsack',
130
+ used_time_ms: 0,
131
+ used_tokens: 0,
132
+ total_mitigation: 0,
133
+ reason: 'no_rules',
134
+ };
135
+ }
136
+ const sorted = [...list].sort(
137
+ (a, b) =>
138
+ (b.risk_mitigation || 0) / Math.max(b.eval_time_ms || 0.1, 0.1)
139
+ - (a.risk_mitigation || 0) / Math.max(a.eval_time_ms || 0.1, 0.1)
140
+ );
141
+ const selected = [];
142
+ let curTime = 0;
143
+ let curTokens = 0;
144
+ let mitigation = 0;
145
+ for (const r of sorted) {
146
+ const t = r.eval_time_ms || 0;
147
+ const tok = r.token_footprint || 0;
148
+ if (curTime + t <= budgets.maxEvalTimeMs && curTokens + tok <= budgets.maxTokenFootprint) {
149
+ selected.push(r.id);
150
+ curTime += t;
151
+ curTokens += tok;
152
+ mitigation += r.risk_mitigation || 0;
153
+ }
154
+ }
155
+ return {
156
+ success: true,
157
+ selected_rules: selected,
158
+ solver: 'independent-greedy-knapsack',
159
+ used_time_ms: curTime,
160
+ used_tokens: curTokens,
161
+ total_mitigation: mitigation,
162
+ };
163
+ }
164
+
165
+ function sumMitigation(rules, ids) {
166
+ const set = new Set(ids || []);
167
+ return (rules || [])
168
+ .filter((r) => set.has(r.id))
169
+ .reduce((s, r) => s + (r.risk_mitigation || 0), 0);
170
+ }
171
+
172
+ function rulesWithinBudget(rules, ids, budgets) {
173
+ const set = new Set(ids || []);
174
+ const used = (rules || []).filter((r) => set.has(r.id));
175
+ const time = used.reduce((s, r) => s + (r.eval_time_ms || 0), 0);
176
+ const tokens = used.reduce((s, r) => s + (r.token_footprint || 0), 0);
177
+ return {
178
+ time,
179
+ tokens,
180
+ ok: time <= budgets.maxEvalTimeMs && tokens <= budgets.maxTokenFootprint,
181
+ };
182
+ }
183
+
184
+ function sameIdList(a, b) {
185
+ const left = [...(a || [])].map(String).sort();
186
+ const right = [...(b || [])].map(String).sort();
187
+ if (left.length !== right.length) return false;
188
+ return left.every((id, i) => id === right[i]);
189
+ }
190
+
191
+ function nowMs() {
192
+ return Date.now();
193
+ }
194
+
195
+ function evaluateRoutingCase(testCase, solverOpts) {
196
+ const budgets = testCase.budgets || {};
197
+ const candidates = testCase.candidates || [];
198
+ const expect = testCase.expect || {};
199
+ const failures = [];
200
+
201
+ const hStart = nowMs();
202
+ const heuristic = independentHeuristicRouting(candidates, budgets);
203
+ const heuristicMs = nowMs() - hStart;
204
+ const heuristic2 = independentHeuristicRouting(candidates, budgets);
205
+ const stable = heuristic.selected === heuristic2.selected
206
+ && heuristic.success === heuristic2.success;
207
+
208
+ const sStart = nowMs();
209
+ const solver = optimizeModelRouting(candidates, {
210
+ maxBudgetUsd: budgets.maxBudgetUsd,
211
+ maxLatencyMs: budgets.maxLatencyMs,
212
+ }, solverOpts || {});
213
+ const solverMs = nowMs() - sStart;
214
+
215
+ const feasible = feasibleRoutingIds(candidates, budgets);
216
+ const gurobi = isSuccessfulGurobiLabel(solver.solver);
217
+ const heuristicEngine = isHeuristicSolver(solver.solver) && !gurobi;
218
+
219
+ if (typeof expect.success === 'boolean' && heuristic.success !== expect.success) {
220
+ failures.push(`heuristic.success=${heuristic.success} expected ${expect.success}`);
221
+ }
222
+ if (Object.prototype.hasOwnProperty.call(expect, 'selected')) {
223
+ if (heuristic.selected !== expect.selected) {
224
+ failures.push(`heuristic.selected=${heuristic.selected} expected ${expect.selected}`);
225
+ }
226
+ }
227
+ if (expect.heuristicSelected && heuristic.selected !== expect.heuristicSelected) {
228
+ failures.push(
229
+ `heuristic.selected=${heuristic.selected} expected ${expect.heuristicSelected}`
230
+ );
231
+ }
232
+ if (expect.success === false) {
233
+ if (solver.success && solver.selected) {
234
+ failures.push(`solver.success=${solver.success} with selected=${solver.selected} on empty/infeasible fixture`);
235
+ }
236
+ }
237
+ if (expect.success === true && candidates.length > 0 && !solver.success) {
238
+ failures.push(`solver.success=false expected true (${solver.error || solver.reason || 'no reason'})`);
239
+ }
240
+ if (expect.feasibleMustHold && heuristic.success && heuristic.selected) {
241
+ if (!feasible.includes(heuristic.selected)) {
242
+ failures.push(`heuristic selected infeasible ${heuristic.selected}`);
243
+ }
244
+ }
245
+ if (expect.feasibleMustHold && solver.success && solver.selected && !heuristicEngine) {
246
+ if (!feasible.includes(solver.selected)) {
247
+ failures.push(`solver selected infeasible ${solver.selected}`);
248
+ }
249
+ }
250
+ if (expect.feasibleMustHold && solver.success && solver.selected && heuristicEngine) {
251
+ if (!feasible.includes(solver.selected)) {
252
+ failures.push(`heuristic-fallback solver selected infeasible ${solver.selected}`);
253
+ }
254
+ }
255
+ for (const banned of expect.neverSelected || []) {
256
+ if (heuristic.selected === banned) {
257
+ failures.push(`heuristic selected banned ${banned}`);
258
+ }
259
+ if (solver.selected === banned) {
260
+ failures.push(`solver selected banned ${banned}`);
261
+ }
262
+ }
263
+ if (!stable) {
264
+ failures.push('heuristic was not stable across two runs');
265
+ }
266
+
267
+ return {
268
+ id: testCase.id,
269
+ stage: testCase.stage || 'benchmarking',
270
+ kind: 'routing',
271
+ ok: failures.length === 0,
272
+ failures,
273
+ independentEngines: gurobi,
274
+ homeworkGrade: gurobi ? 'independent-solver' : 'solver-fell-back-to-heuristic',
275
+ metrics: {
276
+ heuristicWallMs: heuristicMs,
277
+ solverWallMs: solverMs,
278
+ heuristicObjective: heuristic.objective || 0,
279
+ solverObjective: solver.objective || 0,
280
+ gap: (solver.objective || 0) - (heuristic.objective || 0),
281
+ sameSelection: heuristic.selected === solver.selected,
282
+ stable,
283
+ },
284
+ heuristic,
285
+ solver: {
286
+ success: solver.success,
287
+ selected: solver.selected || null,
288
+ solver: solver.solver || null,
289
+ status: solver.status || null,
290
+ objective: solver.objective,
291
+ },
292
+ capturedRevenueUsd: 0,
293
+ };
294
+ }
295
+
296
+ function evaluateRulesCase(testCase, solverOpts) {
297
+ const budgets = testCase.budgets || {};
298
+ const rules = testCase.rules || [];
299
+ const expect = testCase.expect || {};
300
+ const failures = [];
301
+
302
+ const hStart = nowMs();
303
+ const heuristic = independentHeuristicRules(rules, budgets);
304
+ const heuristicMs = nowMs() - hStart;
305
+ const heuristic2 = independentHeuristicRules(rules, budgets);
306
+ const stable = sameIdList(heuristic.selected_rules, heuristic2.selected_rules);
307
+
308
+ const sStart = nowMs();
309
+ const solver = optimizeRuleSelection(rules, {
310
+ maxEvalTimeMs: budgets.maxEvalTimeMs,
311
+ maxTokenFootprint: budgets.maxTokenFootprint,
312
+ }, solverOpts || {});
313
+ const solverMs = nowMs() - sStart;
314
+
315
+ const gurobi = isSuccessfulGurobiLabel(solver.solver);
316
+ const heuristicEngine = isHeuristicSolver(solver.solver) && !gurobi;
317
+ const solverIds = solver.selected_rules || [];
318
+ const heuristicBudget = rulesWithinBudget(rules, heuristic.selected_rules, budgets);
319
+ const solverBudget = rulesWithinBudget(rules, solverIds, budgets);
320
+
321
+ if (expect.heuristicSelected
322
+ && !sameIdList(heuristic.selected_rules, expect.heuristicSelected)) {
323
+ failures.push(
324
+ `heuristic selected [${(heuristic.selected_rules || []).join(',')}] `
325
+ + `expected [${expect.heuristicSelected.join(',')}]`
326
+ );
327
+ }
328
+ if (expect.withinBudget && !heuristicBudget.ok) {
329
+ failures.push(
330
+ `heuristic exceeded budget time=${heuristicBudget.time} tokens=${heuristicBudget.tokens}`
331
+ );
332
+ }
333
+ if (expect.withinBudget && solver.success && !solverBudget.ok) {
334
+ failures.push(
335
+ `solver exceeded budget time=${solverBudget.time} tokens=${solverBudget.tokens}`
336
+ );
337
+ }
338
+ for (const banned of expect.neverSelected || []) {
339
+ if ((heuristic.selected_rules || []).includes(banned)) {
340
+ failures.push(`heuristic selected banned ${banned}`);
341
+ }
342
+ if (solverIds.includes(banned)) {
343
+ failures.push(`solver selected banned ${banned}`);
344
+ }
345
+ }
346
+ if (gurobi && expect.solverWhenGurobi
347
+ && !sameIdList(solverIds, expect.solverWhenGurobi)) {
348
+ failures.push(
349
+ `gurobi selected [${solverIds.join(',')}] expected [${expect.solverWhenGurobi.join(',')}]`
350
+ );
351
+ }
352
+ if (heuristicEngine && expect.solverWhenHeuristic
353
+ && !sameIdList(solverIds, expect.solverWhenHeuristic)) {
354
+ failures.push(
355
+ `heuristic-fallback selected [${solverIds.join(',')}] `
356
+ + `expected [${expect.solverWhenHeuristic.join(',')}]`
357
+ );
358
+ }
359
+ if (!stable) {
360
+ failures.push('heuristic was not stable across two runs');
361
+ }
362
+
363
+ const solverMitigation = sumMitigation(rules, solverIds);
364
+ return {
365
+ id: testCase.id,
366
+ stage: testCase.stage || 'benchmarking',
367
+ kind: 'rules',
368
+ ok: failures.length === 0,
369
+ failures,
370
+ independentEngines: gurobi,
371
+ homeworkGrade: gurobi ? 'independent-solver' : 'solver-fell-back-to-heuristic',
372
+ metrics: {
373
+ heuristicWallMs: heuristicMs,
374
+ solverWallMs: solverMs,
375
+ heuristicObjective: heuristic.total_mitigation || 0,
376
+ solverObjective: solverMitigation,
377
+ gap: solverMitigation - (heuristic.total_mitigation || 0),
378
+ sameSelection: sameIdList(heuristic.selected_rules, solverIds),
379
+ stable,
380
+ },
381
+ heuristic,
382
+ solver: {
383
+ success: solver.success,
384
+ selected_rules: solverIds,
385
+ solver: solver.solver || null,
386
+ status: solver.status || null,
387
+ objective: solver.objective,
388
+ used_time_ms: solver.used_time_ms,
389
+ used_tokens: solver.used_tokens,
390
+ },
391
+ capturedRevenueUsd: 0,
392
+ };
393
+ }
394
+
395
+ function evaluateCase(testCase, solverOpts) {
396
+ if (testCase.kind === 'rules') {
397
+ return evaluateRulesCase(testCase, solverOpts);
398
+ }
399
+ return evaluateRoutingCase(testCase, solverOpts);
400
+ }
401
+
402
+ function runParity(options = {}) {
403
+ const started = Date.now();
404
+ const acceptancePath = options.acceptancePath || DEFAULT_ACCEPTANCE_PATH;
405
+ const acceptance = options.acceptance || loadAcceptanceSet(acceptancePath);
406
+ const solverOpts = options.solverOpts || {};
407
+ const probe = options.skipProbe ? { ok: null, gurobi: false, python: null } : probeGurobi(solverOpts);
408
+
409
+ const cases = (acceptance.cases || []).map((c) => evaluateCase(c, solverOpts));
410
+ const failed = cases.filter((c) => !c.ok);
411
+ const gurobiUsed = cases.some((c) => c.independentEngines) || probe.gurobi === true;
412
+
413
+ return {
414
+ schema: REPORT_SCHEMA,
415
+ mode: 'parity',
416
+ generatedAt: new Date().toISOString(),
417
+ latencyMs: Date.now() - started,
418
+ acceptancePath: path.resolve(acceptancePath),
419
+ frozenAt: acceptance.frozenAt || null,
420
+ capturedRevenueUsd: 0,
421
+ probe: {
422
+ ok: probe.ok,
423
+ gurobiAvailable: Boolean(gurobiUsed),
424
+ python: probe.python || null,
425
+ },
426
+ process: {
427
+ freezeAcceptanceSet: true,
428
+ likeForLike: true,
429
+ doNotGradeOwnHomework: true,
430
+ stages: ['discovery', 'benchmarking', 'decision'],
431
+ affiliation: 'none',
432
+ source: 'Phase One evaluation process — not Gurobi product copy',
433
+ },
434
+ summary: {
435
+ total: cases.length,
436
+ passed: cases.length - failed.length,
437
+ failed: failed.length,
438
+ ok: failed.length === 0,
439
+ },
440
+ cases,
441
+ disclaimers: [
442
+ 'Independent heuristic is the status-quo baseline. Solver is a separate engine.',
443
+ 'When Gurobi is unavailable the Node/Python path falls back to a heuristic — that is not an independent check.',
444
+ 'No Gurobi partnership, co-sell, or affiliation is claimed.',
445
+ 'capturedRevenueUsd is always 0 — a solve is not cash collected.',
446
+ 'This is existing-surface maintenance (routing + knapsack). Not an enterprise SKU.',
447
+ ],
448
+ };
449
+ }
450
+
451
+ function formatReport(report) {
452
+ const lines = [
453
+ '# Solver parity — frozen acceptance set',
454
+ '',
455
+ `Generated: ${report.generatedAt}`,
456
+ `Frozen at: ${report.frozenAt || 'unknown'}`,
457
+ `Result: ${report.summary.ok ? 'PASS' : 'FAIL'} `
458
+ + `${report.summary.passed}/${report.summary.total}`,
459
+ '',
460
+ 'Process: freeze acceptance set → like-for-like engines → do not grade your own homework.',
461
+ 'Not Gurobi product copy. No affiliation. capturedRevenueUsd=0.',
462
+ '',
463
+ '| Case | Stage | Kind | Independent engines | Heuristic | Solver | Gap | Wall ms (h/s) |',
464
+ '|------|-------|------|---------------------|-----------|--------|-----|---------------|',
465
+ ];
466
+ for (const c of report.cases) {
467
+ const hPick = c.kind === 'rules'
468
+ ? (c.heuristic.selected_rules || []).join('+') || '—'
469
+ : (c.heuristic.selected || '—');
470
+ const sPick = c.kind === 'rules'
471
+ ? (c.solver.selected_rules || []).join('+') || '—'
472
+ : (c.solver.selected || '—');
473
+ lines.push(
474
+ `| ${c.ok ? 'ok' : 'FAIL'} ${c.id} | ${c.stage} | ${c.kind} | ${c.homeworkGrade} | `
475
+ + `${hPick} | ${sPick} | ${Number(c.metrics.gap).toFixed(1)} | `
476
+ + `${c.metrics.heuristicWallMs}/${c.metrics.solverWallMs} |`
477
+ );
478
+ if (!c.ok) {
479
+ for (const f of c.failures) {
480
+ lines.push(`| └ ${f} | | | | | | | |`);
481
+ }
482
+ }
483
+ }
484
+ lines.push(
485
+ '',
486
+ '## Disclaimers',
487
+ '',
488
+ ...report.disclaimers.map((d) => `- ${d}`),
489
+ ''
490
+ );
491
+ return lines.join('\n');
492
+ }
493
+
494
+ function mainCli(argv = process.argv.slice(2)) {
495
+ const json = argv.includes('--json');
496
+ const accIdx = argv.indexOf('--acceptance');
497
+ const acceptancePath = accIdx >= 0 ? argv[accIdx + 1] : DEFAULT_ACCEPTANCE_PATH;
498
+ const writeIdx = argv.indexOf('--write');
499
+ const writePath = writeIdx >= 0 ? argv[writeIdx + 1] : null;
500
+
501
+ const report = runParity({ acceptancePath });
502
+
503
+ if (writePath) {
504
+ const abs = path.resolve(writePath);
505
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
506
+ const jsonPath = /\.json$/i.test(abs) ? abs : `${abs}.json`;
507
+ const mdPath = jsonPath.replace(/\.json$/i, '.md');
508
+ fs.writeFileSync(jsonPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
509
+ if (mdPath !== jsonPath) {
510
+ fs.writeFileSync(mdPath, formatReport(report), 'utf8');
511
+ }
512
+ }
513
+
514
+ if (json) {
515
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
516
+ } else {
517
+ process.stdout.write(formatReport(report));
518
+ }
519
+
520
+ return report.summary.ok ? 0 : 1;
521
+ }
522
+
523
+ module.exports = {
524
+ SCHEMA,
525
+ REPORT_SCHEMA,
526
+ DEFAULT_ACCEPTANCE_PATH,
527
+ loadAcceptanceSet,
528
+ independentHeuristicRouting,
529
+ independentHeuristicRules,
530
+ evaluateCase,
531
+ runParity,
532
+ formatReport,
533
+ mainCli,
534
+ isSuccessfulGurobiLabel,
535
+ };
536
+
537
+ if (path.resolve(process.argv[1] || '') === path.resolve(__filename)) {
538
+ process.exitCode = mainCli();
539
+ }