thumbgate 1.35.0 → 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.
- package/.agents/skills/cyberstrike-compare-not-clone/SKILL.md +36 -0
- package/.agents/skills/gitlab-sandbox-allowlist-not-trust/SKILL.md +77 -0
- package/.agents/skills/jit-harness-compare-not-clone/SKILL.md +34 -0
- package/.agents/skills/openui-catalog-compose-honesty/SKILL.md +64 -0
- package/.agents/skills/zvec-grep-compare-not-clone/SKILL.md +34 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/llms.txt +1 -0
- package/.well-known/mcp/server-card.json +1 -1
- package/CONTRIBUTING.md +95 -0
- package/README.md +195 -632
- package/THIRD_PARTY_NOTICES.md +89 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/forge/forge.yaml +3 -3
- package/adapters/future-agi/.mcp.json +8 -0
- package/adapters/future-agi/FUTURE_AGI.md +23 -0
- package/adapters/future-agi/config.toml +3 -0
- package/adapters/future-agi/future-agi-bridge.js +9 -0
- package/adapters/future-agi/opencode.json +8 -0
- package/adapters/herdr/herdr-plugin.toml +18 -0
- package/adapters/mcp/server-stdio.js +238 -25
- package/adapters/opencode/opencode.json +1 -1
- package/adapters/workos/WORKOS.md +52 -0
- package/bin/cli.js +336 -3
- package/bin/futureagi-bridge +9 -0
- package/config/gate-templates.json +581 -4
- package/config/gates/actor-critic-audit.json +34 -0
- package/config/gates/default.json +9 -3
- package/config/gates/five-walls-governance.json +34 -0
- package/config/gates/future-agi-guardrails.json +34 -0
- package/config/gates/radware-threat-defense-2026.json +61 -0
- package/config/gates/simatree-data-governance.json +33 -0
- package/config/mcp-allowlists.json +4 -0
- package/config/merge-quality-checks.json +6 -0
- package/config/model-candidates.json +312 -29
- package/config/model-tiers.json +18 -0
- package/config/post-deploy-marketing-pages.json +10 -0
- package/config/progressive/01-wire-only.json +11 -0
- package/config/progressive/02-dashboard-empty-ok.json +10 -0
- package/config/progressive/03-one-lesson.json +10 -0
- package/config/progressive/04-warn-fires.json +11 -0
- package/config/progressive/05-strict-optional.json +11 -0
- package/config/progressive/README.md +15 -0
- package/config/schemas/broker-execution-receipt.schema.json +139 -0
- package/config/schemas/provider-execution-attestation-v1.schema.json +58 -0
- package/conformance/provider-attestation/vectors.json +320 -0
- package/docs/specs/provider-execution-attestation-v1.md +69 -0
- package/openapi/openapi.yaml +15 -0
- package/package.json +390 -148
- package/public/about.html +2 -2
- package/public/ai-malpractice-prevention.html +7 -7
- package/public/blog/a-10-dollar-vps-is-not-a-computer.html +143 -0
- package/public/blog/a-receipt-is-not-world-state.html +388 -0
- package/public/blog/git-at-agent-scale.html +374 -0
- package/public/blog/no-llm-in-the-gate.html +133 -0
- package/public/blog.html +80 -0
- package/public/case-studies.html +16 -1
- package/public/compare.html +28 -0
- package/public/diagnostic.html +216 -7
- package/public/docs/connectors.html +39 -0
- package/public/federal.html +2 -2
- package/public/founders.html +639 -0
- package/public/index.html +87 -9
- package/public/install.html +8 -8
- package/public/learn.html +39 -0
- package/public/numbers.html +2 -2
- package/public/peter.html +310 -0
- package/public/platform-partners.html +119 -0
- package/public/pricing.html +24 -3
- package/public/privacy.html +117 -0
- package/public/pro.html +17 -0
- package/public/support.html +62 -0
- package/public/terms.html +130 -0
- package/public/third-party-notices.html +95 -0
- package/public/yt.html +351 -0
- package/scripts/action-receipts.js +133 -3
- package/scripts/adaptive-governance-arena.js +349 -0
- package/scripts/admin-override.js +205 -0
- package/scripts/agent-action-inventory.js +869 -0
- package/scripts/agent-audit-trace.js +42 -2
- package/scripts/agent-egress-policy.js +1117 -0
- package/scripts/agent-memory-lifecycle.js +141 -2
- package/scripts/agent-operations-planner.js +441 -1
- package/scripts/agent-readiness.js +68 -0
- package/scripts/agent-security-central.js +647 -0
- package/scripts/allowlist-bridge-honesty.js +417 -0
- package/scripts/async-job-runner.js +102 -11
- package/scripts/audit-trail.js +212 -0
- package/scripts/billing.js +1 -1
- package/scripts/broker-execution-receipts.js +719 -0
- package/scripts/budget-aware-gates-proof.js +423 -0
- package/scripts/claude-feedback-sync.js +29 -3
- package/scripts/claw-harness-production.js +237 -0
- package/scripts/cli-schema.js +163 -1
- package/scripts/codex-runbook-flywheel.js +318 -0
- package/scripts/context-footprint.js +186 -0
- package/scripts/contextfs.js +143 -61
- package/scripts/dashboard.js +251 -32
- package/scripts/deepseek-v4-runtime-guardrails.js +72 -6
- package/scripts/docker-sandbox-planner.js +18 -0
- package/scripts/double-blind-eval-protocol.js +252 -0
- package/scripts/edotenv-rl-gateway.js +259 -0
- package/scripts/ensure-production-search-corpus.js +162 -0
- package/scripts/eval-holdout.js +311 -0
- package/scripts/feedback-aggregate.js +21 -2
- package/scripts/feedback-loop.js +87 -5
- package/scripts/feedback-quality.js +9 -0
- package/scripts/file-ledger-lock.js +4 -1
- package/scripts/financial-control-plane.js +41 -1
- package/scripts/find-dormant-requires.js +118 -0
- package/scripts/fs-utils.js +84 -8
- package/scripts/gates-engine.js +810 -63
- package/scripts/generate-case-study-outreach.js +24 -15
- package/scripts/git-at-scale.js +628 -0
- package/scripts/governance-conflict-audit.js +1650 -0
- package/scripts/governance-difficulty-curriculum.js +328 -0
- package/scripts/graphrag-retrieval.js +275 -0
- package/scripts/gurobi-optimizer.js +324 -0
- package/scripts/gurobi_optimizer.py +485 -0
- package/scripts/harness-selector.js +82 -1
- package/scripts/hidden-entry-points.js +284 -0
- package/scripts/human-escalation.js +199 -1
- package/scripts/hybrid-feedback-context.js +152 -19
- package/scripts/intent-governed-execution.js +602 -0
- package/scripts/intervention-policy.js +123 -20
- package/scripts/jit-harness-compose.js +628 -0
- package/scripts/jsonl-watcher.js +10 -0
- package/scripts/lesson-embedding-index.js +95 -12
- package/scripts/lesson-retrieval.js +105 -19
- package/scripts/local-model-profile.js +19 -2
- package/scripts/mailer/resend-mailer.js +1 -1
- package/scripts/matryoshka-embedding.js +235 -0
- package/scripts/mcp-oauth.js +42 -4
- package/scripts/mcp-session-handles.js +1016 -0
- package/scripts/mcp-wiring-doctor.js +314 -0
- package/scripts/memory-firewall.js +115 -2
- package/scripts/memory-scope-readiness.js +299 -0
- package/scripts/memory-vs-rag-route.js +161 -0
- package/scripts/model-tier-router.js +148 -21
- package/scripts/nvidia-specdecode-al-doctor.js +536 -0
- package/scripts/openui-catalog-compose-honesty.js +593 -0
- package/scripts/operational-integrity.js +19 -1
- package/scripts/override-audit.js +213 -0
- package/scripts/package-manager-honesty-doctor.js +458 -0
- package/scripts/pr-manager.js +63 -1
- package/scripts/prove-herdr-adapter.js +52 -0
- package/scripts/prove-memory-pyramid-and-symbolic-canvas.js +95 -0
- package/scripts/prove-workos.js +73 -0
- package/scripts/provider-attestation-conformance.js +192 -0
- package/scripts/provider-receipt-contract.js +136 -0
- package/scripts/qwen38-max-cost-optimizer.js +401 -0
- package/scripts/radware-threat-defense.js +280 -0
- package/scripts/rag-embedding-identity.js +221 -0
- package/scripts/rag-precision-guardrails.js +112 -2
- package/scripts/remote-feedback-capture.js +159 -0
- package/scripts/research-agent-harness.js +256 -0
- package/scripts/rsi-safety-hillclimb.js +200 -0
- package/scripts/rule-sprawl.js +188 -0
- package/scripts/schedule-manager.js +147 -0
- package/scripts/self-heal.js +8 -0
- package/scripts/session-lease.js +415 -0
- package/scripts/simatree-data-governance.js +347 -0
- package/scripts/slo-alert-engine.js +172 -7
- package/scripts/solver-parity.js +539 -0
- package/scripts/stealth-memory-injection-gate.js +333 -0
- package/scripts/switchyard-router.js +366 -0
- package/scripts/telemetry-analytics.js +84 -27
- package/scripts/temporal-decay-weighting.js +138 -0
- package/scripts/test-all.js +165 -0
- package/scripts/token-savings.js +42 -0
- package/scripts/tool-kpi-tracker.js +108 -5
- package/scripts/tool-registry.js +193 -5
- package/scripts/universal-claim-evaluator.js +14 -2
- package/scripts/vector-store.js +279 -9
- package/scripts/workflow-notebook.js +391 -0
- package/scripts/workflow-sentinel.js +111 -12
- package/scripts/workos-production-guard.js +260 -0
- package/scripts/workspace-search-route.js +515 -0
- package/server.json +2 -2
- package/src/agent-identity-boundary.js +76 -0
- package/src/agent-retrieval-cache.js +155 -0
- package/src/alert-noise-ledger.js +502 -0
- package/src/api/server.js +724 -153
- package/src/git-fast-cache.js +220 -0
- package/src/git-wal-sync.js +156 -0
- package/src/hash-anchored-edit.js +82 -0
- package/src/hermes-platform-protocol.js +475 -0
- package/src/hermes-sync-plane.js +241 -0
- package/src/index.js +30 -1
- package/src/iso42001-compliance-guard.js +97 -0
- package/src/latency-budget.js +244 -0
- package/src/mcp-writeguard.js +316 -0
- package/src/miminions-adapter.js +106 -0
- package/src/pipeline-compass.js +104 -0
- package/src/ppl-alert-pipeline.js +284 -0
- package/src/rendezvous-router.js +90 -0
- package/src/security-questionnaire.js +195 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Budget-aware gates proof — sales-safe demo of ThumbGate optimization
|
|
5
|
+
*
|
|
6
|
+
* Uses the Fabrizio Ellis free-pip Gurobi path (when available) to compare:
|
|
7
|
+
* 1) Deterministic heuristic selection
|
|
8
|
+
* 2) MILP solver selection (Gurobi via gurobi-optimizer.js, heuristic fallback in CI)
|
|
9
|
+
*
|
|
10
|
+
* What this proves for buyers:
|
|
11
|
+
* - ThumbGate does not load "every prevention rule always"
|
|
12
|
+
* - Model routing respects cost + latency budgets
|
|
13
|
+
* - Rule knapsacks maximize risk mitigation under eval-time/token budgets
|
|
14
|
+
*
|
|
15
|
+
* What this does NOT claim:
|
|
16
|
+
* - Gurobi partnership / co-sell / affiliation
|
|
17
|
+
* - Captured cash or revenue from the solve
|
|
18
|
+
* - That commercial Gurobi is required (free-pip + fail-open heuristics work)
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* node scripts/budget-aware-gates-proof.js
|
|
22
|
+
* node scripts/budget-aware-gates-proof.js --json
|
|
23
|
+
* node scripts/budget-aware-gates-proof.js --write proof/budget-aware-gates.json
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const fs = require('node:fs');
|
|
27
|
+
const path = require('node:path');
|
|
28
|
+
const {
|
|
29
|
+
optimizeModelRouting,
|
|
30
|
+
optimizeRuleSelection,
|
|
31
|
+
probeGurobi,
|
|
32
|
+
} = require('./gurobi-optimizer');
|
|
33
|
+
|
|
34
|
+
/** Fixed demo workload — deterministic for CI and demos. */
|
|
35
|
+
const DEMO_MODEL_CANDIDATES = [
|
|
36
|
+
{ id: 'local-vllm-8b', score: 7.8, cost: 0.0, latency_ms: 180, notes: 'local free' },
|
|
37
|
+
{ id: 'qwen-flash', score: 8.4, cost: 0.002, latency_ms: 220, notes: 'cheap cloud' },
|
|
38
|
+
{ id: 'claude-sonnet', score: 9.6, cost: 0.018, latency_ms: 900, notes: 'premium' },
|
|
39
|
+
{ id: 'claude-opus', score: 9.9, cost: 0.06, latency_ms: 1400, notes: 'over budget' },
|
|
40
|
+
{ id: 'gpt-mini', score: 8.1, cost: 0.004, latency_ms: 350, notes: 'mid tier' },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const DEMO_RULES = [
|
|
44
|
+
{ id: 'secret-egress', risk_mitigation: 9.5, eval_time_ms: 4, token_footprint: 80 },
|
|
45
|
+
{ id: 'deny-network-egress', risk_mitigation: 7.0, eval_time_ms: 3, token_footprint: 60 },
|
|
46
|
+
{ id: 'task-scope-lease', risk_mitigation: 8.2, eval_time_ms: 6, token_footprint: 120 },
|
|
47
|
+
{ id: 'protected-file-approval', risk_mitigation: 8.8, eval_time_ms: 5, token_footprint: 100 },
|
|
48
|
+
{ id: 'spend-guard', risk_mitigation: 6.5, eval_time_ms: 2, token_footprint: 40 },
|
|
49
|
+
{ id: 'stealth-memory-injection', risk_mitigation: 9.0, eval_time_ms: 12, token_footprint: 200 },
|
|
50
|
+
{ id: 'claim-verification', risk_mitigation: 5.5, eval_time_ms: 15, token_footprint: 250 },
|
|
51
|
+
{ id: 'outbound-email-human', risk_mitigation: 7.5, eval_time_ms: 3, token_footprint: 50 },
|
|
52
|
+
{ id: 'branch-protection', risk_mitigation: 8.0, eval_time_ms: 4, token_footprint: 70 },
|
|
53
|
+
{ id: 'low-value-noise-rule', risk_mitigation: 1.0, eval_time_ms: 20, token_footprint: 300 },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const DEFAULT_BUDGETS = {
|
|
57
|
+
maxBudgetUsd: 0.01,
|
|
58
|
+
maxLatencyMs: 500,
|
|
59
|
+
maxEvalTimeMs: 30,
|
|
60
|
+
maxTokenFootprint: 600,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
function greedyHeuristicRouting(candidates, { maxBudgetUsd, maxLatencyMs }) {
|
|
64
|
+
const list = Array.isArray(candidates) ? candidates : [];
|
|
65
|
+
if (list.length === 0) {
|
|
66
|
+
return {
|
|
67
|
+
success: false,
|
|
68
|
+
selected: null,
|
|
69
|
+
solver: 'pure-greedy-heuristic',
|
|
70
|
+
objective: 0,
|
|
71
|
+
candidate: null,
|
|
72
|
+
reason: 'no_candidates',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const valid = list.filter(
|
|
76
|
+
(c) => (c.cost || 0) <= maxBudgetUsd && (c.latency_ms || 0) <= maxLatencyMs
|
|
77
|
+
);
|
|
78
|
+
const pool = valid.length > 0 ? valid : list;
|
|
79
|
+
const best = pool.reduce(
|
|
80
|
+
(prev, curr) => ((curr.score || 0) > (prev.score || 0) ? curr : prev),
|
|
81
|
+
pool[0]
|
|
82
|
+
);
|
|
83
|
+
return {
|
|
84
|
+
success: true,
|
|
85
|
+
selected: best.id,
|
|
86
|
+
solver: 'pure-greedy-heuristic',
|
|
87
|
+
objective: best.score || 0,
|
|
88
|
+
candidate: best,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function greedyHeuristicRules(rules, { maxEvalTimeMs, maxTokenFootprint }) {
|
|
93
|
+
const list = Array.isArray(rules) ? rules : [];
|
|
94
|
+
if (list.length === 0) {
|
|
95
|
+
return {
|
|
96
|
+
success: false,
|
|
97
|
+
selected_rules: [],
|
|
98
|
+
solver: 'pure-greedy-knapsack',
|
|
99
|
+
used_time_ms: 0,
|
|
100
|
+
used_tokens: 0,
|
|
101
|
+
total_mitigation: 0,
|
|
102
|
+
reason: 'no_rules',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const sorted = [...list].sort(
|
|
106
|
+
(a, b) =>
|
|
107
|
+
(b.risk_mitigation || 0) / Math.max(b.eval_time_ms || 0.1, 0.1)
|
|
108
|
+
- (a.risk_mitigation || 0) / Math.max(a.eval_time_ms || 0.1, 0.1)
|
|
109
|
+
);
|
|
110
|
+
const selected = [];
|
|
111
|
+
let curTime = 0;
|
|
112
|
+
let curTokens = 0;
|
|
113
|
+
let mitigation = 0;
|
|
114
|
+
for (const r of sorted) {
|
|
115
|
+
const t = r.eval_time_ms || 0;
|
|
116
|
+
const tok = r.token_footprint || 0;
|
|
117
|
+
if (curTime + t <= maxEvalTimeMs && curTokens + tok <= maxTokenFootprint) {
|
|
118
|
+
selected.push(r.id);
|
|
119
|
+
curTime += t;
|
|
120
|
+
curTokens += tok;
|
|
121
|
+
mitigation += r.risk_mitigation || 0;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
success: true,
|
|
126
|
+
selected_rules: selected,
|
|
127
|
+
solver: 'pure-greedy-knapsack',
|
|
128
|
+
used_time_ms: curTime,
|
|
129
|
+
used_tokens: curTokens,
|
|
130
|
+
total_mitigation: mitigation,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function sumMitigation(rules, ids) {
|
|
135
|
+
const set = new Set(ids || []);
|
|
136
|
+
return rules
|
|
137
|
+
.filter((r) => set.has(r.id))
|
|
138
|
+
.reduce((s, r) => s + (r.risk_mitigation || 0), 0);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function findCandidate(id) {
|
|
142
|
+
return DEMO_MODEL_CANDIDATES.find((c) => c.id === id) || null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Run the full proof suite.
|
|
147
|
+
* @param {object} options
|
|
148
|
+
* @returns {object} structured proof report
|
|
149
|
+
*/
|
|
150
|
+
function runBudgetAwareGatesProof(options = {}) {
|
|
151
|
+
const budgets = { ...DEFAULT_BUDGETS, ...(options.budgets || {}) };
|
|
152
|
+
const started = Date.now();
|
|
153
|
+
|
|
154
|
+
const heuristicRouting = greedyHeuristicRouting(DEMO_MODEL_CANDIDATES, budgets);
|
|
155
|
+
const solverRouting = optimizeModelRouting(DEMO_MODEL_CANDIDATES, {
|
|
156
|
+
maxBudgetUsd: budgets.maxBudgetUsd,
|
|
157
|
+
maxLatencyMs: budgets.maxLatencyMs,
|
|
158
|
+
}, options.solverOpts || {});
|
|
159
|
+
|
|
160
|
+
const heuristicRules = greedyHeuristicRules(DEMO_RULES, budgets);
|
|
161
|
+
const solverRules = optimizeRuleSelection(DEMO_RULES, {
|
|
162
|
+
maxEvalTimeMs: budgets.maxEvalTimeMs,
|
|
163
|
+
maxTokenFootprint: budgets.maxTokenFootprint,
|
|
164
|
+
}, options.solverOpts || {});
|
|
165
|
+
|
|
166
|
+
const probe = options.skipProbe ? { ok: null, gurobi: false } : probeGurobi(options.solverOpts || {});
|
|
167
|
+
|
|
168
|
+
// When Gurobi error-falls-back to unconstrained top score, replace with
|
|
169
|
+
// budget-valid heuristic so buyer narrative never claims compliance for
|
|
170
|
+
// over-budget selections (e.g. claude-opus at $0.06 under a $0.01 cap).
|
|
171
|
+
const routingPick = coerceBudgetCompliantRouting(solverRouting, heuristicRouting, budgets);
|
|
172
|
+
const rulesPick = coerceBudgetCompliantRules(solverRules, heuristicRules, budgets);
|
|
173
|
+
|
|
174
|
+
const routingHeuristicScore = heuristicRouting.objective || 0;
|
|
175
|
+
const routingSolverScore = routingPick.objective != null
|
|
176
|
+
? routingPick.objective
|
|
177
|
+
: (findCandidate(routingPick.selected)?.score || 0);
|
|
178
|
+
const rulesHeuristicMitigation = heuristicRules.total_mitigation
|
|
179
|
+
|| sumMitigation(DEMO_RULES, heuristicRules.selected_rules);
|
|
180
|
+
const rulesSolverMitigation = rulesPick.total_mitigation != null
|
|
181
|
+
? rulesPick.total_mitigation
|
|
182
|
+
: sumMitigation(DEMO_RULES, rulesPick.selected_rules || []);
|
|
183
|
+
|
|
184
|
+
const solverIsGurobi = isSuccessfulGurobiLabel(routingPick.solver)
|
|
185
|
+
|| isSuccessfulGurobiLabel(rulesPick.solver)
|
|
186
|
+
|| (probe.gurobi === true && !isGurobiErrorFallback(solverRouting.solver)
|
|
187
|
+
&& !isGurobiErrorFallback(solverRules.solver));
|
|
188
|
+
|
|
189
|
+
const report = {
|
|
190
|
+
schema: 'thumbgate.budget_aware_gates_proof.v1',
|
|
191
|
+
mode: 'simulation',
|
|
192
|
+
autoApply: false,
|
|
193
|
+
humanOversightRequired: true,
|
|
194
|
+
capturedRevenueUsd: 0,
|
|
195
|
+
generatedAt: new Date().toISOString(),
|
|
196
|
+
latencyMs: Date.now() - started,
|
|
197
|
+
budgets,
|
|
198
|
+
probe: {
|
|
199
|
+
ok: probe.ok,
|
|
200
|
+
gurobiAvailable: Boolean(
|
|
201
|
+
(probe.gurobi === true && !isGurobiErrorFallback(solverRouting.solver)
|
|
202
|
+
&& !isGurobiErrorFallback(solverRules.solver))
|
|
203
|
+
|| isSuccessfulGurobiLabel(routingPick.solver)
|
|
204
|
+
|| isSuccessfulGurobiLabel(rulesPick.solver)
|
|
205
|
+
),
|
|
206
|
+
solverLabel: routingPick.solver || null,
|
|
207
|
+
rawSolverLabel: solverRouting.solver || null,
|
|
208
|
+
python: probe.python || null,
|
|
209
|
+
},
|
|
210
|
+
modelRouting: {
|
|
211
|
+
budgetUsd: budgets.maxBudgetUsd,
|
|
212
|
+
maxLatencyMs: budgets.maxLatencyMs,
|
|
213
|
+
heuristic: {
|
|
214
|
+
selected: heuristicRouting.selected,
|
|
215
|
+
score: routingHeuristicScore,
|
|
216
|
+
solver: heuristicRouting.solver,
|
|
217
|
+
candidate: findCandidate(heuristicRouting.selected),
|
|
218
|
+
},
|
|
219
|
+
optimized: {
|
|
220
|
+
selected: routingPick.selected,
|
|
221
|
+
score: routingSolverScore,
|
|
222
|
+
solver: routingPick.solver,
|
|
223
|
+
candidate: findCandidate(routingPick.selected),
|
|
224
|
+
budgetCompliant: isRoutingWithinBudget(findCandidate(routingPick.selected), budgets),
|
|
225
|
+
},
|
|
226
|
+
scoreDelta: routingSolverScore - routingHeuristicScore,
|
|
227
|
+
sameSelection: heuristicRouting.selected === routingPick.selected,
|
|
228
|
+
},
|
|
229
|
+
ruleKnapsack: {
|
|
230
|
+
maxEvalTimeMs: budgets.maxEvalTimeMs,
|
|
231
|
+
maxTokenFootprint: budgets.maxTokenFootprint,
|
|
232
|
+
ruleCount: DEMO_RULES.length,
|
|
233
|
+
heuristic: {
|
|
234
|
+
selected: heuristicRules.selected_rules,
|
|
235
|
+
count: (heuristicRules.selected_rules || []).length,
|
|
236
|
+
mitigation: rulesHeuristicMitigation,
|
|
237
|
+
usedTimeMs: heuristicRules.used_time_ms,
|
|
238
|
+
usedTokens: heuristicRules.used_tokens,
|
|
239
|
+
solver: heuristicRules.solver,
|
|
240
|
+
},
|
|
241
|
+
optimized: {
|
|
242
|
+
selected: rulesPick.selected_rules || [],
|
|
243
|
+
count: (rulesPick.selected_rules || []).length,
|
|
244
|
+
mitigation: rulesSolverMitigation,
|
|
245
|
+
usedTimeMs: rulesPick.used_time_ms,
|
|
246
|
+
usedTokens: rulesPick.used_tokens,
|
|
247
|
+
solver: rulesPick.solver,
|
|
248
|
+
},
|
|
249
|
+
mitigationDelta: rulesSolverMitigation - rulesHeuristicMitigation,
|
|
250
|
+
droppedLowValue: DEMO_RULES
|
|
251
|
+
.map((r) => r.id)
|
|
252
|
+
.filter((id) => !(rulesPick.selected_rules || []).includes(id)),
|
|
253
|
+
},
|
|
254
|
+
buyerNarrative: {
|
|
255
|
+
headline: 'Budget-aware enforcement simulation — fixture knapsack, not live rule loader',
|
|
256
|
+
bullets: [
|
|
257
|
+
`SIMULATION: model routing under $${budgets.maxBudgetUsd} / ${budgets.maxLatencyMs}ms budget selects ${routingPick.selected} (solver=${routingPick.solver}).`,
|
|
258
|
+
`SIMULATION: prevention-rule knapsack keeps ${ (rulesPick.selected_rules || []).length } of ${DEMO_RULES.length} fixture rules under ${budgets.maxEvalTimeMs}ms / ${budgets.maxTokenFootprint} tokens.`,
|
|
259
|
+
`Risk-mitigation score (fixtures): heuristic ${rulesHeuristicMitigation.toFixed(1)} → optimized ${rulesSolverMitigation.toFixed(1)} (Δ ${ (rulesSolverMitigation - rulesHeuristicMitigation).toFixed(1) }).`,
|
|
260
|
+
solverIsGurobi
|
|
261
|
+
? 'MILP solver path succeeded on this host (free-pip Gurobi or compatible). Heuristics remain the CI/fallback path.'
|
|
262
|
+
: 'Running on deterministic heuristic fallback — same API, fail-open when solver unavailable.',
|
|
263
|
+
],
|
|
264
|
+
disclaimers: [
|
|
265
|
+
'This proof is a sales-safe SIMULATION on fixed demo fixtures. Production PreToolUse does not yet load selections from optimizeRuleSelection/optimizeModelRouting.',
|
|
266
|
+
'ThumbGate product claim is budget-aware gates as an architecture; this script proves the selection math, not live enforcement wiring.',
|
|
267
|
+
'No Gurobi partnership, co-sell, or affiliation is claimed.',
|
|
268
|
+
'capturedRevenueUsd is not computed here — optimization ≠ cash collected.',
|
|
269
|
+
],
|
|
270
|
+
},
|
|
271
|
+
fixtures: {
|
|
272
|
+
candidates: DEMO_MODEL_CANDIDATES,
|
|
273
|
+
rules: DEMO_RULES,
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
return report;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function isGurobiErrorFallback(label) {
|
|
281
|
+
const s = String(label || '').toLowerCase();
|
|
282
|
+
return s.includes('gurobi-error')
|
|
283
|
+
|| s.includes('error-fallback')
|
|
284
|
+
|| /gurobi.*(?:error|unavailable|license)/.test(s);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function isSuccessfulGurobiLabel(label) {
|
|
288
|
+
const s = String(label || '').toLowerCase();
|
|
289
|
+
if (!s) return false;
|
|
290
|
+
if (isGurobiErrorFallback(s)) return false;
|
|
291
|
+
// Accept exact successful labels only (not every gurobi* string).
|
|
292
|
+
return s === 'gurobi' || s === 'gurobipy' || s === 'gurobi-milp' || s.startsWith('gurobi-ok');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function isRoutingWithinBudget(candidate, budgets) {
|
|
296
|
+
if (!candidate) return false;
|
|
297
|
+
return (candidate.cost || 0) <= budgets.maxBudgetUsd
|
|
298
|
+
&& (candidate.latency_ms || 0) <= budgets.maxLatencyMs;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function coerceBudgetCompliantRouting(solverRouting, heuristicRouting, budgets) {
|
|
302
|
+
const cand = findCandidate(solverRouting && solverRouting.selected);
|
|
303
|
+
if (
|
|
304
|
+
cand
|
|
305
|
+
&& isRoutingWithinBudget(cand, budgets)
|
|
306
|
+
&& !isGurobiErrorFallback(solverRouting && solverRouting.solver)
|
|
307
|
+
) {
|
|
308
|
+
return solverRouting;
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
...heuristicRouting,
|
|
312
|
+
solver: heuristicRouting.solver
|
|
313
|
+
|| (isGurobiErrorFallback(solverRouting && solverRouting.solver)
|
|
314
|
+
? 'heuristic-after-gurobi-error'
|
|
315
|
+
: 'heuristic-budget-coerce'),
|
|
316
|
+
coercedFrom: solverRouting && solverRouting.selected,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function coerceBudgetCompliantRules(solverRules, heuristicRules, budgets) {
|
|
321
|
+
const selected = (solverRules && solverRules.selected_rules) || [];
|
|
322
|
+
if (!selected.length || isGurobiErrorFallback(solverRules && solverRules.solver)) {
|
|
323
|
+
return {
|
|
324
|
+
...heuristicRules,
|
|
325
|
+
solver: heuristicRules.solver || 'heuristic-after-gurobi-error',
|
|
326
|
+
coercedFrom: selected,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
// Soft check: if used budgets exceed caps, fall back to heuristic.
|
|
330
|
+
const usedTime = solverRules.used_time_ms;
|
|
331
|
+
const usedTokens = solverRules.used_tokens;
|
|
332
|
+
if (
|
|
333
|
+
(usedTime != null && usedTime > budgets.maxEvalTimeMs)
|
|
334
|
+
|| (usedTokens != null && usedTokens > budgets.maxTokenFootprint)
|
|
335
|
+
) {
|
|
336
|
+
return {
|
|
337
|
+
...heuristicRules,
|
|
338
|
+
solver: heuristicRules.solver || 'heuristic-budget-coerce',
|
|
339
|
+
coercedFrom: selected,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
return solverRules;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function formatMarkdown(report) {
|
|
346
|
+
const lines = [
|
|
347
|
+
'# Budget-aware gates proof',
|
|
348
|
+
'',
|
|
349
|
+
`Generated: ${report.generatedAt}`,
|
|
350
|
+
'',
|
|
351
|
+
report.buyerNarrative.headline,
|
|
352
|
+
'',
|
|
353
|
+
'## What buyers should hear',
|
|
354
|
+
'',
|
|
355
|
+
...report.buyerNarrative.bullets.map((b) => `- ${b}`),
|
|
356
|
+
'',
|
|
357
|
+
'## Model routing',
|
|
358
|
+
'',
|
|
359
|
+
`| Path | Selected | Score | Solver |`,
|
|
360
|
+
`|------|----------|-------|--------|`,
|
|
361
|
+
`| Heuristic | ${report.modelRouting.heuristic.selected} | ${report.modelRouting.heuristic.score} | ${report.modelRouting.heuristic.solver} |`,
|
|
362
|
+
`| Optimized | ${report.modelRouting.optimized.selected} | ${report.modelRouting.optimized.score} | ${report.modelRouting.optimized.solver} |`,
|
|
363
|
+
'',
|
|
364
|
+
'## Rule knapsack',
|
|
365
|
+
'',
|
|
366
|
+
`| Path | Rules | Mitigation | Time ms | Tokens |`,
|
|
367
|
+
`|------|-------|------------|---------|--------|`,
|
|
368
|
+
`| Heuristic | ${report.ruleKnapsack.heuristic.count} | ${report.ruleKnapsack.heuristic.mitigation.toFixed(1)} | ${report.ruleKnapsack.heuristic.usedTimeMs} | ${report.ruleKnapsack.heuristic.usedTokens} |`,
|
|
369
|
+
`| Optimized | ${report.ruleKnapsack.optimized.count} | ${report.ruleKnapsack.optimized.mitigation.toFixed(1)} | ${report.ruleKnapsack.optimized.usedTimeMs ?? '—'} | ${report.ruleKnapsack.optimized.usedTokens ?? '—'} |`,
|
|
370
|
+
'',
|
|
371
|
+
'## Disclaimers',
|
|
372
|
+
'',
|
|
373
|
+
...report.buyerNarrative.disclaimers.map((d) => `- ${d}`),
|
|
374
|
+
'',
|
|
375
|
+
];
|
|
376
|
+
return lines.join('\n');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function mainCli(argv = process.argv.slice(2)) {
|
|
380
|
+
const json = argv.includes('--json');
|
|
381
|
+
const writeIdx = argv.indexOf('--write');
|
|
382
|
+
const writePath = writeIdx >= 0 ? argv[writeIdx + 1] : null;
|
|
383
|
+
|
|
384
|
+
const report = runBudgetAwareGatesProof();
|
|
385
|
+
|
|
386
|
+
if (writePath) {
|
|
387
|
+
const abs = path.resolve(writePath);
|
|
388
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
389
|
+
const jsonPath = /\.json$/i.test(abs) ? abs : `${abs}.json`;
|
|
390
|
+
const mdPath = jsonPath.replace(/\.json$/i, '.md');
|
|
391
|
+
fs.writeFileSync(jsonPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
|
392
|
+
if (mdPath !== jsonPath) {
|
|
393
|
+
fs.writeFileSync(mdPath, formatMarkdown(report), 'utf8');
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (json) {
|
|
398
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
399
|
+
} else {
|
|
400
|
+
process.stdout.write(formatMarkdown(report));
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return report;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Path-based main check (Sonar S3403: require.main === module is unreliable under CJS).
|
|
407
|
+
if (
|
|
408
|
+
process.argv[1]
|
|
409
|
+
&& path.resolve(process.argv[1]) === path.resolve(__filename)
|
|
410
|
+
) {
|
|
411
|
+
mainCli();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
module.exports = {
|
|
415
|
+
DEMO_MODEL_CANDIDATES,
|
|
416
|
+
DEMO_RULES,
|
|
417
|
+
DEFAULT_BUDGETS,
|
|
418
|
+
greedyHeuristicRouting,
|
|
419
|
+
greedyHeuristicRules,
|
|
420
|
+
runBudgetAwareGatesProof,
|
|
421
|
+
formatMarkdown,
|
|
422
|
+
mainCli,
|
|
423
|
+
};
|
|
@@ -17,9 +17,12 @@ const {
|
|
|
17
17
|
const { refreshStatuslineCache } = require('./hook-thumbgate-cache-updater');
|
|
18
18
|
|
|
19
19
|
const SYNC_STATE_FILE = 'claude-feedback-sync-state.json';
|
|
20
|
-
const DEFAULT_RECENT_FEEDBACK_LIMIT =
|
|
21
|
-
const DEFAULT_PROCESSED_ID_LIMIT =
|
|
20
|
+
const DEFAULT_RECENT_FEEDBACK_LIMIT = 1000;
|
|
21
|
+
const DEFAULT_PROCESSED_ID_LIMIT = 4096;
|
|
22
22
|
const DUPLICATE_WINDOW_MS = 5 * 60 * 1000;
|
|
23
|
+
// At this length, identical text + signal is a re-surfaced prompt, not a
|
|
24
|
+
// repeated sentiment; shorter bare signals stay window-bound.
|
|
25
|
+
const IDENTICAL_TEXT_DEDUP_MIN_LENGTH = 20;
|
|
23
26
|
|
|
24
27
|
function getClaudeHistoryPath(options = {}) {
|
|
25
28
|
if (options.historyPath) return options.historyPath;
|
|
@@ -40,6 +43,7 @@ function readSyncState(options = {}) {
|
|
|
40
43
|
return {
|
|
41
44
|
historyOffset: Number(parsed.historyOffset || 0),
|
|
42
45
|
historySize: Number(parsed.historySize || 0),
|
|
46
|
+
historyIno: parsed.historyIno ? String(parsed.historyIno) : null,
|
|
43
47
|
processedIds: Array.isArray(parsed.processedIds) ? parsed.processedIds : [],
|
|
44
48
|
statePath,
|
|
45
49
|
};
|
|
@@ -47,6 +51,7 @@ function readSyncState(options = {}) {
|
|
|
47
51
|
return {
|
|
48
52
|
historyOffset: 0,
|
|
49
53
|
historySize: 0,
|
|
54
|
+
historyIno: null,
|
|
50
55
|
processedIds: [],
|
|
51
56
|
statePath,
|
|
52
57
|
};
|
|
@@ -58,6 +63,7 @@ function writeSyncState(state, options = {}) {
|
|
|
58
63
|
const payload = {
|
|
59
64
|
historyOffset: Number(state.historyOffset || 0),
|
|
60
65
|
historySize: Number(state.historySize || 0),
|
|
66
|
+
historyIno: state.historyIno ? String(state.historyIno) : null,
|
|
61
67
|
processedIds: Array.isArray(state.processedIds) ? state.processedIds.slice(-DEFAULT_PROCESSED_ID_LIMIT) : [],
|
|
62
68
|
updatedAt: new Date().toISOString(),
|
|
63
69
|
};
|
|
@@ -72,11 +78,25 @@ function readHistoryEntriesSince(filePath, state) {
|
|
|
72
78
|
entries: [],
|
|
73
79
|
nextOffset: 0,
|
|
74
80
|
size: 0,
|
|
81
|
+
ino: null,
|
|
75
82
|
};
|
|
76
83
|
}
|
|
77
84
|
|
|
78
85
|
const stat = fs.statSync(filePath);
|
|
79
|
-
const
|
|
86
|
+
const currentIno = stat.ino > 0 ? String(stat.ino) : null;
|
|
87
|
+
|
|
88
|
+
// Rotation guard: a replaced file (new inode) or one smaller than the
|
|
89
|
+
// recorded size means the saved offset points into vanished content. Scan
|
|
90
|
+
// the replacement from 0 so entries it already holds are preserved; the
|
|
91
|
+
// processedIds and feedback-log dedup layers stop old signals from
|
|
92
|
+
// mass re-importing. Skipping to end here would permanently drop any
|
|
93
|
+
// signal already present in the replacement file.
|
|
94
|
+
const rotated = Boolean(state) && (
|
|
95
|
+
(state.historyIno && currentIno && state.historyIno !== currentIno)
|
|
96
|
+
|| (state.historySize > 0 && stat.size < state.historySize)
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const safeOffset = !rotated && state && state.historyOffset > 0 && state.historyOffset <= stat.size
|
|
80
100
|
? state.historyOffset
|
|
81
101
|
: 0;
|
|
82
102
|
|
|
@@ -100,6 +120,7 @@ function readHistoryEntriesSince(filePath, state) {
|
|
|
100
120
|
entries,
|
|
101
121
|
nextOffset: stat.size,
|
|
102
122
|
size: stat.size,
|
|
123
|
+
ino: currentIno,
|
|
103
124
|
};
|
|
104
125
|
}
|
|
105
126
|
|
|
@@ -204,6 +225,10 @@ function hasMatchingFeedbackEntry(candidate, feedbackEntries) {
|
|
|
204
225
|
);
|
|
205
226
|
if (feedbackText !== candidateText) return false;
|
|
206
227
|
|
|
228
|
+
if (candidateText.length >= IDENTICAL_TEXT_DEDUP_MIN_LENGTH) {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
|
|
207
232
|
const feedbackTimestamp = Date.parse(entry.timestamp || '');
|
|
208
233
|
if (!Number.isFinite(feedbackTimestamp) || !Number.isFinite(candidate.timestampMs)) {
|
|
209
234
|
return true;
|
|
@@ -296,6 +321,7 @@ function syncClaudeHistoryFeedback(options = {}) {
|
|
|
296
321
|
writeSyncState({
|
|
297
322
|
historyOffset: history.nextOffset,
|
|
298
323
|
historySize: history.size,
|
|
324
|
+
historyIno: history.ino,
|
|
299
325
|
processedIds: Array.from(processedIds),
|
|
300
326
|
}, { feedbackDir });
|
|
301
327
|
|