thumbgate 1.27.17 → 1.27.19

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 (107) hide show
  1. package/.claude-plugin/marketplace.json +85 -0
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.well-known/mcp/server-card.json +1 -1
  4. package/README.md +33 -5
  5. package/adapters/chatgpt/openapi.yaml +1707 -0
  6. package/adapters/claude/.mcp.json +2 -2
  7. package/adapters/codex/config.toml +2 -2
  8. package/adapters/mcp/server-stdio.js +1 -1
  9. package/adapters/opencode/opencode.json +1 -1
  10. package/bin/cli.js +63 -54
  11. package/bin/postinstall.js +23 -14
  12. package/config/post-deploy-marketing-pages.json +19 -0
  13. package/config/tessl-tiles.json +1 -3
  14. package/package.json +99 -229
  15. package/public/guide.html +0 -4
  16. package/public/index.html +28 -49
  17. package/public/learn.html +0 -8
  18. package/public/numbers.html +2 -2
  19. package/scripts/agent-readiness.js +0 -147
  20. package/scripts/bot-detection.js +61 -3
  21. package/scripts/cli-feedback.js +4 -14
  22. package/scripts/cli-schema.js +7 -0
  23. package/scripts/durability/step.js +3 -3
  24. package/scripts/feedback-loop.js +10 -32
  25. package/scripts/gates-engine.js +72 -1
  26. package/scripts/hook-runtime.js +2 -2
  27. package/scripts/license.js +10 -9
  28. package/scripts/llm-client.js +44 -260
  29. package/scripts/mcp-config.js +7 -1
  30. package/scripts/published-cli.js +0 -8
  31. package/scripts/seo-gsd.js +0 -5
  32. package/src/api/server.js +111 -35
  33. package/.claude/commands/dashboard.md +0 -15
  34. package/.claude/commands/thumbgate-blocked.md +0 -27
  35. package/.claude/commands/thumbgate-dashboard.md +0 -15
  36. package/.claude/commands/thumbgate-doctor.md +0 -30
  37. package/.claude/commands/thumbgate-guard.md +0 -36
  38. package/.claude/commands/thumbgate-protect.md +0 -30
  39. package/.claude/commands/thumbgate-rules.md +0 -30
  40. package/adapters/gcp/dfcx-webhook-gate.js +0 -295
  41. package/adapters/letta/README.md +0 -41
  42. package/adapters/letta/thumbgate-letta-adapter.js +0 -133
  43. package/adapters/policy-engine/ethicore-guardian-client.js +0 -68
  44. package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +0 -260
  45. package/bench/observability-eval-suite.json +0 -26
  46. package/bin/dashboard-cli.js +0 -7
  47. package/commands/dashboard.md +0 -15
  48. package/commands/thumbgate-dashboard.md +0 -15
  49. package/config/builtin-lessons.json +0 -23
  50. package/public/about.html +0 -162
  51. package/public/agent-manager.html +0 -179
  52. package/public/agents-cost-savings.html +0 -153
  53. package/public/ai-malpractice-prevention.html +0 -818
  54. package/public/assets/brand/github-social-preview.png +0 -0
  55. package/public/assets/brand/thumbgate-icon-512.png +0 -0
  56. package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
  57. package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
  58. package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
  59. package/public/assets/brand/thumbgate-logo-transparent.svg +0 -22
  60. package/public/assets/brand/thumbgate-mark-inline-v3.svg +0 -19
  61. package/public/assets/brand/thumbgate-mark-pro.svg +0 -23
  62. package/public/assets/brand/thumbgate-mark-team.svg +0 -26
  63. package/public/assets/brand/thumbgate-mark.svg +0 -21
  64. package/public/assets/brand/thumbgate-wordmark.svg +0 -20
  65. package/public/assets/claude-thumbgate-statusbar.svg +0 -8
  66. package/public/assets/codex-thumbgate-statusbar-test.svg +0 -9
  67. package/public/assets/legal-intake-control-flow.svg +0 -66
  68. package/public/brand/thumbgate-mark.svg +0 -19
  69. package/public/brand/thumbgate-og.svg +0 -16
  70. package/public/chatgpt-app.html +0 -330
  71. package/public/codex-enterprise.html +0 -123
  72. package/public/js/buyer-intent.js +0 -672
  73. package/public/pricing.html +0 -399
  74. package/scripts/action-receipts.js +0 -324
  75. package/scripts/activation-quickstart.js +0 -187
  76. package/scripts/agent-operations-planner.js +0 -621
  77. package/scripts/ai-component-inventory.js +0 -367
  78. package/scripts/async-eval-observability.js +0 -236
  79. package/scripts/audit.js +0 -65
  80. package/scripts/aws-blocks-guardrails.js +0 -228
  81. package/scripts/bot-detector.js +0 -50
  82. package/scripts/classifier-routing.js +0 -130
  83. package/scripts/dashboard-chat.js +0 -332
  84. package/scripts/feedback-aggregate.js +0 -281
  85. package/scripts/feedback-sanitizer.js +0 -105
  86. package/scripts/hob-pack.js +0 -591
  87. package/scripts/hook-stop-anti-claim.js +0 -301
  88. package/scripts/install-shim.js +0 -87
  89. package/scripts/mcp-oauth.js +0 -293
  90. package/scripts/noop-detect.js +0 -285
  91. package/scripts/omlx-smoke.js +0 -192
  92. package/scripts/parallel-workflow-orchestrator.js +0 -293
  93. package/scripts/plan-gate.js +0 -243
  94. package/scripts/plausible-domain-config.js +0 -99
  95. package/scripts/qa-scenario-planner.js +0 -136
  96. package/scripts/repeat-metric.js +0 -137
  97. package/scripts/secret-fixture-tokens.js +0 -61
  98. package/scripts/secret-redaction.js +0 -166
  99. package/scripts/self-harness-optimizer.js +0 -141
  100. package/scripts/self-healing-check.js +0 -193
  101. package/scripts/silent-failure-cluster.js +0 -531
  102. package/scripts/statusline-cache-read.js +0 -57
  103. package/scripts/sync-telemetry-from-prod.js +0 -374
  104. package/scripts/tool-contract-validator.js +0 -76
  105. package/scripts/trajectory-scorer.js +0 -63
  106. package/scripts/verify-marketing-pages-deployed.js +0 -195
  107. package/scripts/visitor-journey.js +0 -172
@@ -1,228 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const fs = require('node:fs');
5
- const path = require('node:path');
6
-
7
- const AWS_BLOCKS_DEPENDENCY_PATTERN = /(^|\/)@aws-blocks\/(?:blocks|[^/\s"']+)/;
8
-
9
- function toList(value) {
10
- if (Array.isArray(value)) return value.map(String).map((entry) => entry.trim()).filter(Boolean);
11
- if (typeof value === 'string') return value.split(',').map((entry) => entry.trim()).filter(Boolean);
12
- return [];
13
- }
14
-
15
- function normalizeText(value) {
16
- if (value == null) return '';
17
- if (typeof value === 'string') return value;
18
- try {
19
- return JSON.stringify(value);
20
- } catch {
21
- return String(value);
22
- }
23
- }
24
-
25
- function readPackageJson(projectDir) {
26
- try {
27
- return JSON.parse(fs.readFileSync(path.join(projectDir, 'package.json'), 'utf8'));
28
- } catch {
29
- return null;
30
- }
31
- }
32
-
33
- function packageUsesAwsBlocks(pkg) {
34
- if (!pkg || typeof pkg !== 'object') return false;
35
- const dependencyText = JSON.stringify({
36
- dependencies: pkg.dependencies || {},
37
- devDependencies: pkg.devDependencies || {},
38
- peerDependencies: pkg.peerDependencies || {},
39
- optionalDependencies: pkg.optionalDependencies || {},
40
- });
41
- const scriptsText = JSON.stringify(pkg.scripts || {});
42
- return AWS_BLOCKS_DEPENDENCY_PATTERN.test(dependencyText) || /aws-blocks|blocks-app|cdk|sandbox/i.test(scriptsText);
43
- }
44
-
45
- function detectAwsBlocksProject(projectDir = process.cwd()) {
46
- const pkg = readPackageJson(projectDir);
47
- if (packageUsesAwsBlocks(pkg)) return true;
48
- return fs.existsSync(path.join(projectDir, 'aws-blocks', 'index.ts'))
49
- || fs.existsSync(path.join(projectDir, 'aws-blocks', 'index.js'))
50
- || fs.existsSync(path.join(projectDir, 'blocks.spec.json'));
51
- }
52
-
53
- function detectAction(input = {}) {
54
- const command = normalizeText(input.command || input.args || input.shell || input.toolInput);
55
- const toolName = normalizeText(input.toolName || input.tool || input.name);
56
- const code = normalizeText(input.code || input.file || input.diff || input.body);
57
- const combined = `${toolName}\n${command}\n${code}`;
58
- const lower = combined.toLowerCase();
59
-
60
- const signals = [];
61
- const add = (id, label, severity = 'medium') => signals.push({ id, label, severity });
62
-
63
- if (/\b(cdk\s+deploy|npm\s+run\s+deploy|pnpm\s+deploy|yarn\s+deploy|sst\s+deploy|blocks?\s+deploy)\b/i.test(combined)) {
64
- add('aws-blocks-production-deploy', 'production deploy from an AWS Blocks workflow', 'high');
65
- }
66
-
67
- if (/\b(cdk\s+destroy|npm\s+run\s+destroy|pnpm\s+destroy|yarn\s+destroy|sandbox\b.*--destroy|--destroy\b|aws\s+cloudformation\s+delete-stack)\b/i.test(combined)) {
68
- add('aws-blocks-destroy', 'destroy command can remove AWS resources created from local Blocks code', 'critical');
69
- }
70
-
71
- if (/\b(drop\s+(table|database|schema|index|column)|truncate\s+table|delete\s+from\s+[\w".-]+(?:\s*;|\s*$)|update\s+[\w".-]+\s+set\b(?![\s\S]{0,160}\bwhere\b))/i.test(combined)) {
72
- add('destructive-sql-or-ddl', 'destructive or unscoped SQL mutation', 'critical');
73
- }
74
-
75
- if (/\b(aws\s+dynamodb\s+delete-table|aws\s+rds\s+delete-db-instance|aws\s+s3\s+rm\b[\s\S]*--recursive|aws\s+lambda\s+delete-function|aws\s+bedrock-agent\s+delete-|aws\s+bedrock-agent-runtime\b)/i.test(combined)) {
76
- add('destructive-aws-cli', 'destructive AWS CLI or Bedrock agent action', 'critical');
77
- }
78
-
79
- if (/\b(gcloud|aws)\b[\s\S]{0,220}\b(add-iam-policy-binding|put-role-policy|attach-role-policy|create-policy-version)\b/i.test(combined)
80
- || /\b(AdministratorAccess|roles\/owner|iam:PassRole|serviceAccountTokenCreator)\b/i.test(combined)) {
81
- add('iam-escalation', 'agent session attempts to grant broad IAM authority', 'critical');
82
- }
83
-
84
- if (/\b(new\s+Agent\s*\(|@aws-blocks\/(?:agent|blocks)|\bAgent\b[\s\S]{0,120}\btools?\b)/.test(combined)) {
85
- add('blocks-agent-tool-call', 'AWS Blocks Agent or Bedrock-style tool action needs a tool boundary', 'medium');
86
- }
87
-
88
- if (/\b(npm\s+run\s+dev|npm\s+start|pnpm\s+dev|yarn\s+dev|create-blocks-app)\b/i.test(combined)) {
89
- add('local-dev', 'local AWS Blocks development loop', 'low');
90
- }
91
-
92
- return {
93
- command,
94
- toolName,
95
- code,
96
- signals,
97
- highRisk: signals.some((signal) => ['high', 'critical'].includes(signal.severity)),
98
- localOnly: signals.some((signal) => signal.id === 'local-dev') && signals.every((signal) => signal.severity === 'low'),
99
- lower,
100
- };
101
- }
102
-
103
- function evaluateAwsBlocksAction(input = {}) {
104
- const projectDir = input.projectDir || input.cwd || process.cwd();
105
- const projectUsesAwsBlocks = Boolean(
106
- input.projectUsesAwsBlocks
107
- || input.awsBlocks
108
- || input.blocksProject
109
- || detectAwsBlocksProject(projectDir)
110
- );
111
- const evidence = new Set(toList(input.evidence || input.proof || input.receipts));
112
- const action = detectAction(input);
113
- const requiredEvidence = [];
114
-
115
- const requireEvidence = (id, label) => {
116
- if (!evidence.has(id)) requiredEvidence.push({ id, label });
117
- };
118
-
119
- if (projectUsesAwsBlocks && action.signals.some((signal) => signal.id === 'aws-blocks-production-deploy')) {
120
- requireEvidence('local-tests-pass', 'local AWS Blocks tests pass against local implementations');
121
- requireEvidence('cdk-diff-reviewed', 'CDK diff or synthesized CloudFormation change set reviewed');
122
- requireEvidence('cost-blast-radius-reviewed', 'AWS cost and resource blast radius reviewed');
123
- }
124
-
125
- if (action.signals.some((signal) => signal.id === 'aws-blocks-destroy')) {
126
- requireEvidence('resource-inventory-exported', 'resource inventory exported before destroy');
127
- requireEvidence('human-destroy-approval', 'named human approval for destroy');
128
- }
129
-
130
- if (action.signals.some((signal) => signal.id === 'destructive-sql-or-ddl')) {
131
- requireEvidence('backup-or-rollback-ready', 'backup, rollback, or restore point exists');
132
- requireEvidence('bounded-row-count-reviewed', 'row/table impact was previewed before mutation');
133
- }
134
-
135
- if (action.signals.some((signal) => signal.id === 'destructive-aws-cli')) {
136
- requireEvidence('aws-account-and-region-confirmed', 'target AWS account and region confirmed');
137
- requireEvidence('rollback-plan-attached', 'rollback or recovery plan attached');
138
- }
139
-
140
- if (action.signals.some((signal) => signal.id === 'iam-escalation')) {
141
- requireEvidence('least-privilege-review', 'least-privilege review completed');
142
- requireEvidence('security-owner-approval', 'security owner approval captured');
143
- }
144
-
145
- if (action.signals.some((signal) => signal.id === 'blocks-agent-tool-call')) {
146
- requireEvidence('agent-tool-allowlist', 'agent tool allowlist and data boundary declared');
147
- }
148
-
149
- const shouldBlock = projectUsesAwsBlocks && requiredEvidence.length > 0;
150
- const status = shouldBlock ? 'blocked' : (action.highRisk ? 'needs-review' : 'allowed');
151
-
152
- return {
153
- name: 'thumbgate-aws-blocks-guardrails',
154
- status,
155
- projectUsesAwsBlocks,
156
- signals: action.signals,
157
- requiredEvidence,
158
- enforcementBoundary: 'local AWS Blocks confidence must not automatically become production AWS authority',
159
- gates: [
160
- 'allow local AWS Blocks dev loops without AWS credentials',
161
- 'require local test proof plus CDK diff before production deploy',
162
- 'block destroy and destructive data mutations until backup, blast-radius, and human approval evidence exists',
163
- 'block IAM escalation and Bedrock/Agent tool expansion until an owner approves the tool boundary',
164
- 'write a ThumbGate receipt for every allowed high-risk cloud action',
165
- ],
166
- nextActions: shouldBlock
167
- ? requiredEvidence.map((item) => `Provide ${item.id}: ${item.label}`)
168
- : ['Record an allow receipt when this action touches real AWS resources'],
169
- };
170
- }
171
-
172
- function buildAwsBlocksHardeningOffer(input = {}) {
173
- const workflow = String(input.workflow || input.name || 'AWS Blocks backend').trim();
174
- const buyer = String(input.buyer || input.owner || 'platform owner').trim();
175
- return {
176
- name: 'thumbgate-aws-blocks-hardening-offer',
177
- status: 'ready-for-positioning',
178
- buyer,
179
- workflow,
180
- headline: 'AWS Blocks helps agents build the backend. ThumbGate stops them before unsafe cloud actions run.',
181
- offer: 'AWS Blocks Agent Safety Review',
182
- diagnosticPrice: '$499',
183
- proofPlan: [
184
- 'map one AWS Blocks local-to-cloud workflow',
185
- 'install ThumbGate against the agent running that workflow',
186
- 'add gates for deploy, destroy, data mutation, IAM, Bedrock Agent, and cost-blast-radius actions',
187
- 'produce a receipt showing the first blocked repeat and the evidence required to allow it',
188
- ],
189
- cta: 'Send one AWS Blocks workflow that is about to deploy, mutate data, or call Bedrock tools.',
190
- };
191
- }
192
-
193
- function parseArgs(argv = process.argv.slice(2)) {
194
- const args = {};
195
- for (const arg of argv) {
196
- if (arg === '--json') args.json = true;
197
- else if (arg === '--aws-blocks' || arg === '--project-uses-aws-blocks') args.projectUsesAwsBlocks = true;
198
- else if (arg.startsWith('--command=')) args.command = arg.slice('--command='.length);
199
- else if (arg.startsWith('--tool=')) args.toolName = arg.slice('--tool='.length);
200
- else if (arg.startsWith('--code=')) args.code = arg.slice('--code='.length);
201
- else if (arg.startsWith('--cwd=')) args.projectDir = arg.slice('--cwd='.length);
202
- else if (arg.startsWith('--evidence=')) args.evidence = arg.slice('--evidence='.length);
203
- else if (arg.startsWith('--workflow=')) args.workflow = arg.slice('--workflow='.length);
204
- else if (arg.startsWith('--buyer=')) args.buyer = arg.slice('--buyer='.length);
205
- else if (arg === 'offer') args.commandName = 'offer';
206
- }
207
- return args;
208
- }
209
-
210
- function runCli(args = parseArgs()) {
211
- const report = args.commandName === 'offer'
212
- ? buildAwsBlocksHardeningOffer(args)
213
- : evaluateAwsBlocksAction(args);
214
- if (args.json) console.log(JSON.stringify(report, null, 2));
215
- else {
216
- console.log(`${report.name}: ${report.status}`);
217
- for (const action of report.nextActions || report.proofPlan || []) console.log(`- ${action}`);
218
- }
219
- }
220
-
221
- if (require.main === module) runCli();
222
-
223
- module.exports = {
224
- detectAwsBlocksProject,
225
- detectAction,
226
- evaluateAwsBlocksAction,
227
- buildAwsBlocksHardeningOffer,
228
- };
@@ -1,50 +0,0 @@
1
- 'use strict';
2
-
3
- const BOT_PATTERNS = [
4
- /bot/i, /crawl/i, /spider/i, /slurp/i, /mediapartners/i,
5
- /Googlebot/i, /Bingbot/i, /DuckDuckBot/i, /Baiduspider/i,
6
- /YandexBot/i, /facebookexternalhit/i, /Twitterbot/i,
7
- /LinkedInBot/i, /WhatsApp/i, /Discordbot/i, /TelegramBot/i,
8
- /Applebot/i, /PetalBot/i, /SemrushBot/i, /AhrefsBot/i,
9
- /MJ12bot/i, /DotBot/i, /Bytespider/i,
10
- /GPTBot/i, /ChatGPT/i, /Claude-SearchBot/i, /Anthropic/i, /Perplexity/i,
11
- /Google-Extended/i, /CCBot/i, /cohere-ai/i,
12
- /HeadlessChrome/i, /PhantomJS/i, /Puppeteer/i, /Playwright/i,
13
- /python-requests/i, /node-fetch/i, /wget/i,
14
- /Scrapy/i, /HttpClient/i, /Go-http-client/i,
15
- /UptimeRobot/i, /Pingdom/i, /StatusCake/i,
16
- ];
17
-
18
- const OWNER_EMAILS = ['iganapolsky@gmail.com', 'ig5973700@gmail.com'];
19
-
20
- function classifyVisitor(req) {
21
- const ua = (req.headers && req.headers['user-agent']) || '';
22
- const email = req.email || (req.query && req.query.email) || '';
23
-
24
- for (const pattern of BOT_PATTERNS) {
25
- if (pattern.test(ua)) {
26
- return { type: 'bot', reason: `UA matches: ${pattern}`, userAgent: ua };
27
- }
28
- }
29
- if (!ua || ua.length < 10) {
30
- return { type: 'bot', reason: 'Empty or short user-agent', userAgent: ua };
31
- }
32
- for (const ownerEmail of OWNER_EMAILS) {
33
- if (email && email.toLowerCase().includes(ownerEmail.toLowerCase())) {
34
- return { type: 'owner', reason: `Email matches: ${ownerEmail}`, userAgent: ua };
35
- }
36
- }
37
- return { type: 'real_user', reason: 'No bot pattern matched', userAgent: ua };
38
- }
39
-
40
- function shouldExcludeFromAnalytics(req) {
41
- const classification = req.visitorClass || classifyVisitor(req);
42
- return classification.type === 'bot';
43
- }
44
-
45
- function botFilterMiddleware(req, res, next) {
46
- req.visitorClass = classifyVisitor(req);
47
- next();
48
- }
49
-
50
- module.exports = { classifyVisitor, botFilterMiddleware, shouldExcludeFromAnalytics, BOT_PATTERNS, OWNER_EMAILS };
@@ -1,130 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const fs = require('fs');
5
- const path = require('path');
6
-
7
- const DEFAULT_CONFIG_PATH = path.join(__dirname, '..', 'config', 'gate-classifier-routing.json');
8
-
9
- function clamp01(value, fallback = 0) {
10
- const number = Number(value);
11
- if (!Number.isFinite(number)) return fallback;
12
- return Math.min(1, Math.max(0, number));
13
- }
14
-
15
- function riskRank(risk) {
16
- const normalized = String(risk || 'medium').toLowerCase();
17
- if (['critical', 'block', 'regulated'].includes(normalized)) return 4;
18
- if (['high', 'dangerous'].includes(normalized)) return 3;
19
- if (['medium', 'warn'].includes(normalized)) return 2;
20
- return 1;
21
- }
22
-
23
- function loadClassifierRoutingConfig(configPath = DEFAULT_CONFIG_PATH) {
24
- return JSON.parse(fs.readFileSync(configPath, 'utf8'));
25
- }
26
-
27
- function lane(config, laneName, reason, input, overrides = {}) {
28
- const laneConfig = (config.lanes && config.lanes[laneName]) || {};
29
- return {
30
- lane: laneName,
31
- reason,
32
- requiresEvidence: Boolean(laneConfig.requiresEvidence || overrides.requiresEvidence),
33
- cloudAllowed: Boolean(laneConfig.cloudAllowed),
34
- maxLatencyMs: laneConfig.maxLatencyMs,
35
- risk: String(input.risk || 'medium').toLowerCase(),
36
- ambiguity: clamp01(input.ambiguity),
37
- };
38
- }
39
-
40
- function routeClassifier(input = {}, config = loadClassifierRoutingConfig()) {
41
- const thresholds = config.thresholds || {};
42
- const labelCount = Number(input.labelCount || input.examples || 0);
43
- const latencyBudgetMs = Number(input.latencyBudgetMs || 0);
44
- const ambiguity = clamp01(input.ambiguity);
45
- const risk = riskRank(input.risk);
46
- const largeBatch = Number(input.batchRows || 0) >= Number(thresholds.largeBatchRows || 50);
47
- const privacySensitive = Boolean(input.privacySensitive || input.containsSecrets || input.customerData);
48
- const allowCloud = Boolean(input.allowCloud);
49
-
50
- if (input.hasHardRule || input.exactPolicyMatch) {
51
- return lane(config, 'deterministic', 'exact hard rule or policy match; do not spend model tokens', input);
52
- }
53
-
54
- if (input.semanticCacheHit || input.equivalentRepeat) {
55
- return lane(config, 'semantic_cache', 'semantically equivalent repeat; reuse the proven prior decision without a model call', input);
56
- }
57
-
58
- if (input.rubricFailed || input.missingEvidence || input.completionClaimWithoutProof) {
59
- return lane(config, 'rubric_gate', 'rubric or completion evidence failed; block done claims until proof exists', input);
60
- }
61
-
62
- if (input.structuredDataset && (input.missingProvenance || input.missingSources)) {
63
- return lane(config, 'rubric_gate', 'structured data claim is missing source provenance', input);
64
- }
65
-
66
- if (privacySensitive && !allowCloud && risk >= 3 && ambiguity >= Number(thresholds.mediumAmbiguity || 0.35)) {
67
- return lane(config, 'human_review', 'private high-risk ambiguous action; keep data local and require approval', input);
68
- }
69
-
70
- if (labelCount >= Number(thresholds.classicalMinExamples || 40) && (largeBatch || latencyBudgetMs <= Number(thresholds.lowLatencyBudgetMs || 300)) && ambiguity < Number(thresholds.mediumAmbiguity || 0.35)) {
71
- return lane(config, 'local_classical', 'enough examples and low ambiguity; use cheap local classification', input);
72
- }
73
-
74
- if (risk >= 3 && ambiguity >= Number(thresholds.highRiskAmbiguity || 0.65)) {
75
- if (allowCloud && latencyBudgetMs >= Number(thresholds.llmMinLatencyBudgetMs || 2000)) {
76
- return lane(config, 'llm_judge', 'high-risk semantic ambiguity; use a budget-capped LLM judge with evidence', input);
77
- }
78
- return lane(config, 'human_review', 'high-risk ambiguity without approved cloud/budget route', input);
79
- }
80
-
81
- if (labelCount < Number(thresholds.classicalMinExamples || 40) || ambiguity >= Number(thresholds.mediumAmbiguity || 0.35)) {
82
- return lane(config, 'local_semantic', 'sparse labels or fuzzy intent; use local semantic recall before any LLM', input);
83
- }
84
-
85
- return lane(config, config.defaultLane || 'local_classical', 'default local route for routine gate classification', input);
86
- }
87
-
88
- function explainClassifierRoute(input = {}, config = loadClassifierRoutingConfig()) {
89
- const decision = routeClassifier(input, config);
90
- const laneConfig = (config.lanes && config.lanes[decision.lane]) || {};
91
- return {
92
- ...decision,
93
- description: laneConfig.description || '',
94
- useFor: laneConfig.useFor || [],
95
- };
96
- }
97
-
98
- function parseArgs(argv) {
99
- const input = {};
100
- for (const arg of argv) {
101
- if (arg === '--hard-rule') input.hasHardRule = true;
102
- else if (arg === '--privacy-sensitive') input.privacySensitive = true;
103
- else if (arg === '--allow-cloud') input.allowCloud = true;
104
- else if (arg === '--customer-data') input.customerData = true;
105
- else if (arg === '--semantic-cache-hit') input.semanticCacheHit = true;
106
- else if (arg === '--equivalent-repeat') input.equivalentRepeat = true;
107
- else if (arg === '--rubric-failed') input.rubricFailed = true;
108
- else if (arg === '--missing-evidence') input.missingEvidence = true;
109
- else if (arg === '--structured-dataset') input.structuredDataset = true;
110
- else if (arg === '--missing-provenance') input.missingProvenance = true;
111
- else if (arg.startsWith('--risk=')) input.risk = arg.slice('--risk='.length);
112
- else if (arg.startsWith('--ambiguity=')) input.ambiguity = Number(arg.slice('--ambiguity='.length));
113
- else if (arg.startsWith('--labels=')) input.labelCount = Number(arg.slice('--labels='.length));
114
- else if (arg.startsWith('--latency-ms=')) input.latencyBudgetMs = Number(arg.slice('--latency-ms='.length));
115
- else if (arg.startsWith('--batch-rows=')) input.batchRows = Number(arg.slice('--batch-rows='.length));
116
- }
117
- return input;
118
- }
119
-
120
- module.exports = {
121
- DEFAULT_CONFIG_PATH,
122
- explainClassifierRoute,
123
- loadClassifierRoutingConfig,
124
- routeClassifier,
125
- };
126
-
127
- if (require.main === module) {
128
- const decision = explainClassifierRoute(parseArgs(process.argv.slice(2)));
129
- process.stdout.write(`${JSON.stringify(decision, null, 2)}\n`);
130
- }