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,293 +0,0 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const { getFeedbackPaths } = require('./feedback-loop');
6
- const { ensureDir } = require('./fs-utils');
7
- const { loadOptionalModule } = require('./private-core-boundary');
8
-
9
- const launcher = loadOptionalModule(path.join(__dirname, 'hosted-job-launcher'), () => ({
10
- launchManagedJob: () => {
11
- throw new Error('Managed jobs require ThumbGate-Core.');
12
- },
13
- resumeHostedJob: () => {
14
- throw new Error('Resuming hosted jobs requires ThumbGate-Core.');
15
- },
16
- }));
17
-
18
- const runner = loadOptionalModule(path.join(__dirname, 'async-job-runner'), () => ({
19
- readJobState: () => null,
20
- listJobStates: () => [],
21
- }));
22
-
23
- const { launchManagedJob, resumeHostedJob } = launcher;
24
- const { readJobState, listJobStates } = runner;
25
-
26
- const DEFAULT_CONCURRENCY = 3;
27
- const POLL_INTERVAL_MS = 200;
28
-
29
- function nowIso() {
30
- return new Date().toISOString();
31
- }
32
-
33
- /**
34
- * Dynamically decompose a high-level objective into parallel, specialized subtasks.
35
- * Supports rule-based fallback and can be extended to use LLM planning.
36
- */
37
- function planWorkflow(objective) {
38
- const obj = (objective || '').toLowerCase().trim();
39
- const subtasks = [];
40
-
41
- if (obj.includes('security') || obj.includes('audit') || obj.includes('leak') || obj.includes('secret')) {
42
- subtasks.push({
43
- name: 'scan_secrets',
44
- tags: ['security', 'secret-scanner'],
45
- stages: [
46
- {
47
- name: 'secret_scan',
48
- command: 'node scripts/secret-scanner.js --json || true',
49
- }
50
- ]
51
- });
52
- subtasks.push({
53
- name: 'audit_dependencies',
54
- tags: ['security', 'dependencies'],
55
- stages: [
56
- {
57
- name: 'npm_audit',
58
- command: 'npm audit --json || true',
59
- }
60
- ]
61
- });
62
- subtasks.push({
63
- name: 'check_permissions',
64
- tags: ['security', 'credentials'],
65
- stages: [
66
- {
67
- name: 'credential_gate_check',
68
- command: 'node scripts/single-use-credential-gate.js plan || true',
69
- }
70
- ]
71
- });
72
- } else if (obj.includes('performance') || obj.includes('benchmark') || obj.includes('bench')) {
73
- subtasks.push({
74
- name: 'benchmark_candidates',
75
- tags: ['performance', 'bench'],
76
- stages: [
77
- {
78
- name: 'run_bench',
79
- command: 'npx thumbgate bench --json --min-score=90 || true',
80
- }
81
- ]
82
- });
83
- subtasks.push({
84
- name: 'check_budget',
85
- tags: ['performance', 'budget'],
86
- stages: [
87
- {
88
- name: 'budget_status',
89
- command: 'node scripts/budget-guard.js --status || true',
90
- }
91
- ]
92
- });
93
- } else {
94
- // Default general-purpose fallback workflow: code search and check integrity
95
- subtasks.push({
96
- name: 'code_search',
97
- tags: ['exploration'],
98
- stages: [
99
- {
100
- name: 'search_fs',
101
- command: 'node scripts/filesystem-search.js --query="pretool" --limit=5 || true',
102
- }
103
- ]
104
- });
105
- subtasks.push({
106
- name: 'check_integrity',
107
- tags: ['integrity'],
108
- stages: [
109
- {
110
- name: 'ops_integrity',
111
- command: 'node scripts/operational-integrity.js --ci || true',
112
- }
113
- ]
114
- });
115
- }
116
-
117
- return {
118
- objective,
119
- plannedAt: nowIso(),
120
- subtasks: subtasks.map((task, idx) => ({
121
- ...task,
122
- id: `subtask_${Date.now()}_${idx}_${Math.random().toString(36).slice(2, 6)}`,
123
- autoImprove: false,
124
- verificationMode: 'none',
125
- recordFeedback: false,
126
- })),
127
- };
128
- }
129
-
130
- /**
131
- * Execute a list of planned subtasks in parallel, respecting a concurrency limit.
132
- * Polls active jobs until all complete, then consolidates the results.
133
- */
134
- async function executeWorkflow(objective, options = {}) {
135
- const plan = planWorkflow(objective);
136
- const concurrency = Number(options.concurrency) || DEFAULT_CONCURRENCY;
137
- const timeoutMs = Number(options.timeoutMs) || 60000; // 60s timeout safety
138
-
139
- const { FEEDBACK_DIR } = getFeedbackPaths();
140
- const workflowId = `wf_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
141
- const workflowDir = path.join(FEEDBACK_DIR, 'workflows', workflowId);
142
- ensureDir(workflowDir);
143
-
144
- const activeJobs = new Map();
145
- const queue = [...plan.subtasks];
146
- const results = [];
147
- const start = Date.now();
148
-
149
- const runNext = () => {
150
- while (activeJobs.size < concurrency && queue.length > 0) {
151
- const task = queue.shift();
152
- const launched = launchManagedJob(task, { cwd: options.cwd });
153
- activeJobs.set(task.id, {
154
- jobId: launched.jobId,
155
- taskName: task.name,
156
- launchedAt: Date.now(),
157
- });
158
- }
159
- };
160
-
161
- runNext();
162
-
163
- // Polling loop
164
- await new Promise((resolve) => {
165
- const interval = setInterval(() => {
166
- let allDone = true;
167
-
168
- for (const [taskId, info] of activeJobs.entries()) {
169
- const jobState = readJobState(info.jobId);
170
- if (!jobState) {
171
- allDone = false;
172
- continue;
173
- }
174
-
175
- const isTerminal = ['completed', 'failed', 'cancelled'].includes(jobState.status);
176
- if (isTerminal) {
177
- results.push({
178
- taskId,
179
- taskName: info.taskName,
180
- jobId: info.jobId,
181
- status: jobState.status,
182
- context: jobState.currentContext,
183
- stageHistory: jobState.stageHistory,
184
- lastError: jobState.lastError,
185
- });
186
- activeJobs.delete(taskId);
187
- runNext();
188
- } else {
189
- allDone = false;
190
- }
191
- }
192
-
193
- const elapsed = Date.now() - start;
194
- if (allDone && queue.length === 0) {
195
- clearInterval(interval);
196
- resolve();
197
- } else if (elapsed >= timeoutMs) {
198
- clearInterval(interval);
199
- // Timeout remaining active tasks
200
- for (const [taskId, info] of activeJobs.entries()) {
201
- results.push({
202
- taskId,
203
- taskName: info.taskName,
204
- jobId: info.jobId,
205
- status: 'timeout',
206
- lastError: { message: `Subtask timed out after ${timeoutMs}ms`, code: 'TIMEOUT' },
207
- });
208
- }
209
- resolve();
210
- }
211
- }, POLL_INTERVAL_MS);
212
- });
213
-
214
- const durationMs = Date.now() - start;
215
-
216
- // Compile final markdown report
217
- const reportPath = path.join(workflowDir, 'report.md');
218
- const reportContent = compileWorkflowReport(plan, results, durationMs, workflowId);
219
- fs.writeFileSync(reportPath, reportContent, 'utf8');
220
-
221
- // Also save the raw execution results JSON
222
- const resultsJsonPath = path.join(workflowDir, 'results.json');
223
- fs.writeFileSync(resultsJsonPath, JSON.stringify({
224
- workflowId,
225
- objective,
226
- durationMs,
227
- plan,
228
- results,
229
- }, null, 2) + '\n', 'utf8');
230
-
231
- return {
232
- workflowId,
233
- objective,
234
- durationMs,
235
- reportPath,
236
- results,
237
- };
238
- }
239
-
240
- function compileWorkflowReport(plan, results, durationMs, workflowId) {
241
- const timestamp = nowIso();
242
- const totalSubtasks = plan.subtasks.length;
243
- const completed = results.filter((r) => r.status === 'completed').length;
244
- const failed = results.filter((r) => r.status === 'failed' || r.status === 'timeout').length;
245
-
246
- let report = `# Dynamic Workflow Execution Report: ${workflowId}\n\n`;
247
- report += `**Objective:** ${plan.objective}\n`;
248
- report += `**Executed At:** ${timestamp}\n`;
249
- report += `**Duration:** ${(durationMs / 1000).toFixed(2)}s\n`;
250
- report += `**Status:** ${completed === totalSubtasks ? '✅ SUCCESS' : '⚠️ COMPLETED WITH FAILURES'}\n\n`;
251
-
252
- report += `## Summary\n`;
253
- report += `- Total planned subtasks: ${totalSubtasks}\n`;
254
- report += `- Completed successfully: ${completed}\n`;
255
- report += `- Failed/Timed out: ${failed}\n\n`;
256
-
257
- report += `## Subtask Breakdown\n\n`;
258
-
259
- for (const res of results) {
260
- const taskPlan = plan.subtasks.find((t) => t.id === res.taskId) || {};
261
- const commandUsed = taskPlan.stages && taskPlan.stages[0] ? taskPlan.stages[0].command : 'N/A';
262
-
263
- report += `### ✦ Subtask: \`${res.taskName}\`\n`;
264
- report += `- **Job ID:** \`${res.jobId}\`\n`;
265
- report += `- **Status:** ${res.status === 'completed' ? '✅ COMPLETED' : '❌ ' + res.status.toUpperCase()}\n`;
266
- report += `- **Command Run:** \`${commandUsed}\`\n`;
267
-
268
- if (res.lastError) {
269
- report += `- **Error:** \`${res.lastError.message}\` (Code: \`${res.lastError.code}\`)\n`;
270
- }
271
-
272
- if (res.context) {
273
- report += `\n**Output Context Preview:**\n\`\`\`json\n`;
274
- try {
275
- // Try parsing output context as JSON for clean formatting
276
- const parsed = JSON.parse(res.context);
277
- report += JSON.stringify(parsed, null, 2);
278
- } catch {
279
- report += res.context.slice(0, 1000) + (res.context.length > 1000 ? '\n... (truncated)' : '');
280
- }
281
- report += `\n\`\`\`\n`;
282
- }
283
- report += `\n---\n\n`;
284
- }
285
-
286
- return report;
287
- }
288
-
289
- module.exports = {
290
- planWorkflow,
291
- executeWorkflow,
292
- compileWorkflowReport,
293
- };
@@ -1,243 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * Plan Gate — implementing the CodeRabbit "Planning-First" pattern.
6
- *
7
- * 1. (Static) Validates structured 'PLAN.md' / 'PRD' content (used in loop-closure).
8
- * 2. (Dynamic) Intercepts high-risk tool calls during agent execution.
9
- */
10
-
11
- const fs = require('fs');
12
- const path = require('path');
13
-
14
- const RISK_TOOLS = ['Bash', 'Write', 'Edit', 'Deploy'];
15
-
16
- // ---------------------------------------------------------------------------
17
- // Gate validators (Legacy / Loop Closure)
18
- // ---------------------------------------------------------------------------
19
-
20
- function countTableRows(content, sectionHeading) {
21
- const sectionRegex = new RegExp(
22
- `#+\\s*${sectionHeading}[^\\n]*\\n([\\s\\S]*?)(?=\\n#+\\s|$)`,
23
- );
24
- const match = content.match(sectionRegex);
25
- if (!match) return 0;
26
-
27
- const lines = match[1].split('\n').filter((l) => l.trim().startsWith('|'));
28
- // Subtract header row and separator row
29
- const dataRows = lines.filter(
30
- (l) => !/^\|\s*-+/.test(l.trim()) && !/^\|\s*:?-+/.test(l.trim()),
31
- );
32
- // First row is the header
33
- return Math.max(0, dataRows.length - 1);
34
- }
35
-
36
- function countContracts(content) {
37
- const sectionRegex = /#+\s*Contracts[^\n]*\n([\s\S]*?)(?=\n#+\s|$)/;
38
- const match = content.match(sectionRegex);
39
- if (!match) return 0;
40
-
41
- const section = match[1];
42
- // Find code blocks and look for interface/type keywords inside them
43
- const codeBlockRegex = /```[\s\S]*?```/g;
44
- let count = 0;
45
- let blockMatch;
46
- while ((blockMatch = codeBlockRegex.exec(section)) !== null) {
47
- const block = blockMatch[0];
48
- const interfaceMatches = block.match(/\b(interface|type)\s+\w+/g);
49
- if (interfaceMatches) count += interfaceMatches.length;
50
- }
51
- return count;
52
- }
53
-
54
- function countValidationScenarios(content) {
55
- const sectionRegex =
56
- /#+\s*Validation\s+Checklist[^\n]*\n([\s\S]*?)(?=\n#+\s|$)/;
57
- const match = content.match(sectionRegex);
58
- if (!match) return 0;
59
-
60
- const lines = match[1].split('\n');
61
- return lines.filter((l) => /^\s*-\s*\[\s*\]/.test(l)).length;
62
- }
63
-
64
- function getStatus(content) {
65
- const match = content.match(/#+\s*Status[^\n]*\n\s*(\S+)/);
66
- return match ? match[1].trim() : null;
67
- }
68
-
69
- function validatePlan(content) {
70
- const questionCount = countTableRows(content, 'Clarifying Questions Resolved');
71
- const contractCount = countContracts(content);
72
- const scenarioCount = countValidationScenarios(content);
73
- const status = getStatus(content);
74
-
75
- const gates = [
76
- {
77
- name: 'Clarifying Questions',
78
- pass: questionCount >= 3,
79
- detail: `${questionCount} questions resolved`,
80
- },
81
- {
82
- name: 'Contracts Defined',
83
- pass: contractCount >= 1,
84
- detail: `${contractCount} interface${contractCount !== 1 ? 's' : ''} found`,
85
- },
86
- {
87
- name: 'Validation Checklist',
88
- pass: scenarioCount >= 2,
89
- detail: `${scenarioCount} scenarios defined`,
90
- },
91
- {
92
- name: 'Status',
93
- pass: status !== 'COMPLETE',
94
- detail:
95
- status === 'COMPLETE'
96
- ? 'COMPLETE (already finished — cannot re-approve)'
97
- : `${status || 'UNKNOWN'} (not COMPLETE)`,
98
- },
99
- ];
100
-
101
- const allPass = gates.every((g) => g.pass);
102
- return { gates, allPass };
103
- }
104
-
105
- function formatReport(result) {
106
- const lines = result.gates.map(
107
- (g) => `${g.pass ? '✅' : '❌'} ${g.name}: ${g.detail}`,
108
- );
109
- lines.push('');
110
- lines.push(
111
- result.allPass
112
- ? 'RESULT: PASS — all gates satisfied'
113
- : 'RESULT: BLOCKED — resolve issues above before spawning agents',
114
- );
115
- return lines.join('\n');
116
- }
117
-
118
- // ---------------------------------------------------------------------------
119
- // Dynamic Gating (CodeRabbit Orchestration Pattern)
120
- // ---------------------------------------------------------------------------
121
-
122
- /**
123
- * Evaluates the planning state for the current tool call.
124
- */
125
- function evaluatePlanGate(toolName, toolInput, options = {}) {
126
- if (!RISK_TOOLS.includes(toolName)) return null;
127
-
128
- const projectRoot = options.projectRoot || process.cwd();
129
- const planPath = path.join(projectRoot, 'PLAN.md');
130
-
131
- // Tier 1: Existence Check
132
- if (!fs.existsSync(planPath)) {
133
- return {
134
- decision: 'warn',
135
- gate: 'plan-gate-missing',
136
- message: '⚠️ THUMBGATE: High-risk tool call without a PLAN.md. Please create a plan documenting your intent and assumptions.',
137
- severity: 'high'
138
- };
139
- }
140
-
141
- // Tier 2: Alignment Check (Simple)
142
- const planContent = fs.readFileSync(planPath, 'utf8');
143
- const action = toolName === 'Bash' ? toolInput.command : toolInput.filePath;
144
-
145
- if (action && !planContent.toLowerCase().includes(path.basename(action).toLowerCase())) {
146
- return {
147
- decision: 'warn',
148
- gate: 'plan-gate-drift',
149
- message: `⚠️ THUMBGATE: Strategic Drift detected. The action "${action}" is not mentioned in your PLAN.md.`,
150
- severity: 'medium'
151
- };
152
- }
153
-
154
- // Tier 3: Implicit Assumption Extraction
155
- const assumptions = extractAssumptions(planContent);
156
- if (assumptions.length > 0) {
157
- return {
158
- decision: 'warn',
159
- gate: 'plan-gate-assumptions',
160
- message: '🔍 THUMBGATE: Explicitly verify these implicit assumptions before proceeding:\n- ' + assumptions.join('\n- '),
161
- severity: 'medium'
162
- };
163
- }
164
-
165
- // Tier 4: Self-Critique / Risk Mitigation Check (Tip #8)
166
- const hasCritique = /(?:critique|self-critique|risk|mitigation|alternative|flaw|weakness|pitfall)/i.test(planContent);
167
- if (!hasCritique) {
168
- return {
169
- decision: 'warn',
170
- gate: 'plan-gate-critique-missing',
171
- message: '🧐 THUMBGATE: No Self-Critique/Risk Analysis detected in your PLAN.md. Please add a "Critique", "Risks", or "Mitigations" section to evaluate potential flaws in this plan before executing high-risk tools.',
172
- severity: 'medium'
173
- };
174
- }
175
-
176
- return null;
177
- }
178
-
179
- /**
180
- * Scans plan content for "Assumes" or "Implicit" keywords.
181
- */
182
- function extractAssumptions(content) {
183
- const lines = content.split('\n');
184
- const assumptions = [];
185
- const regex = /(?:assume|assumption|implicit|pre-requisite|depends on)s?[:\-]?\s*(.*)/i;
186
-
187
- for (const line of lines) {
188
- const match = line.match(regex);
189
- if (match && match[1].trim()) {
190
- assumptions.push(match[1].trim());
191
- }
192
- }
193
- return assumptions.slice(0, 5);
194
- }
195
-
196
- // ---------------------------------------------------------------------------
197
- // Main
198
- // ---------------------------------------------------------------------------
199
-
200
- function run() {
201
- const args = process.argv.slice(2);
202
- const jsonFlag = args.includes('--json');
203
- const filePath = args.find((a) => a !== '--json');
204
-
205
- if (!filePath) {
206
- console.error('Usage: node scripts/plan-gate.js <plan-file.md> [--json]');
207
- process.exit(1);
208
- }
209
-
210
- const resolved = path.resolve(filePath);
211
- if (!fs.existsSync(resolved)) {
212
- console.error(`File not found: ${resolved}`);
213
- process.exit(1);
214
- }
215
-
216
- const content = fs.readFileSync(resolved, 'utf-8');
217
- const result = validatePlan(content);
218
-
219
- if (jsonFlag) {
220
- console.log(JSON.stringify(result, null, 2));
221
- } else {
222
- console.log(formatReport(result));
223
- }
224
-
225
- process.exit(result.allPass ? 0 : 1);
226
- }
227
-
228
- // Export for testing
229
- module.exports = {
230
- validatePlan,
231
- formatReport,
232
- countTableRows,
233
- countContracts,
234
- countValidationScenarios,
235
- getStatus,
236
- evaluatePlanGate,
237
- extractAssumptions,
238
- };
239
-
240
- // Run only when executed directly
241
- if (require.main === module) {
242
- run();
243
- }
@@ -1,99 +0,0 @@
1
- 'use strict';
2
-
3
- const PRIMARY_PLAUSIBLE_DOMAIN = 'thumbgate.ai';
4
- const FALLBACK_REGISTERED_PLAUSIBLE_DOMAIN = 'thumbgate-production.up.railway.app';
5
-
6
- function splitDomains(value) {
7
- return String(value || '')
8
- .split(/[\s,]+/)
9
- .map((domain) => domain.trim().toLowerCase())
10
- .filter(Boolean);
11
- }
12
-
13
- function normalizeDomain(value) {
14
- const input = String(value || '').trim();
15
- if (!input) return '';
16
- try {
17
- return new URL(input.includes('://') ? input : `https://${input}`).hostname.toLowerCase();
18
- } catch {
19
- const withoutProtocol = input.replace(/^https?:\/\//i, '');
20
- const hostnameAndPort = withoutProtocol.split('/')[0];
21
- return hostnameAndPort.toLowerCase().split(':')[0];
22
- }
23
- }
24
-
25
- function getConfiguredRegisteredDomains(env = process.env) {
26
- const configured = [
27
- ...splitDomains(env.PLAUSIBLE_SITE_ID),
28
- ...splitDomains(env.PLAUSIBLE_SITE_IDS),
29
- ...splitDomains(env.THUMBGATE_PLAUSIBLE_REGISTERED_DOMAINS),
30
- ...splitDomains(env.PLAUSIBLE_REGISTERED_DOMAINS),
31
- ].map(normalizeDomain).filter(Boolean);
32
-
33
- return [...new Set([
34
- FALLBACK_REGISTERED_PLAUSIBLE_DOMAIN,
35
- ...configured,
36
- ])];
37
- }
38
-
39
- function isPlausibleDomainRegistered(domain, env = process.env) {
40
- const normalized = normalizeDomain(domain);
41
- if (!normalized) return false;
42
- return getConfiguredRegisteredDomains(env).includes(normalized);
43
- }
44
-
45
- function resolvePlausibleDataDomain({ host = '', env = process.env } = {}) {
46
- const explicit = normalizeDomain(env.THUMBGATE_PLAUSIBLE_DOMAIN);
47
- if (explicit) return explicit;
48
-
49
- const normalizedHost = normalizeDomain(host);
50
- if (isPlausibleDomainRegistered(normalizedHost, env)) {
51
- return normalizedHost;
52
- }
53
-
54
- // Prevent local/loopback traffic from mapping to the production Plausible site domain
55
- const isLocal = normalizedHost === 'localhost' ||
56
- normalizedHost === '127.0.0.1' ||
57
- normalizedHost === '::1' ||
58
- normalizedHost.endsWith('.local') ||
59
- normalizedHost.startsWith('192.168.') ||
60
- normalizedHost.startsWith('10.');
61
- if (isLocal) {
62
- return 'localhost';
63
- }
64
-
65
- return FALLBACK_REGISTERED_PLAUSIBLE_DOMAIN;
66
- }
67
-
68
- function analyzePlausibleDomainCoverage({
69
- emittedDomains = [],
70
- registeredDomains = [],
71
- primaryDomain = PRIMARY_PLAUSIBLE_DOMAIN,
72
- } = {}) {
73
- const emitted = [...new Set(emittedDomains.map(normalizeDomain).filter(Boolean))];
74
- const registered = [...new Set(registeredDomains.map(normalizeDomain).filter(Boolean))];
75
- const registeredSet = new Set(registered);
76
- const missingEmittedDomains = emitted.filter((domain) => !registeredSet.has(domain));
77
- const primaryRegistered = registeredSet.has(normalizeDomain(primaryDomain));
78
-
79
- return {
80
- ok: missingEmittedDomains.length === 0 && primaryRegistered,
81
- emittedDomains: emitted,
82
- registeredDomains: registered,
83
- missingEmittedDomains,
84
- primaryDomain: normalizeDomain(primaryDomain),
85
- primaryRegistered,
86
- severity: missingEmittedDomains.length > 0 || !primaryRegistered ? 'critical' : 'ok',
87
- };
88
- }
89
-
90
- module.exports = {
91
- PRIMARY_PLAUSIBLE_DOMAIN,
92
- FALLBACK_REGISTERED_PLAUSIBLE_DOMAIN,
93
- splitDomains,
94
- normalizeDomain,
95
- getConfiguredRegisteredDomains,
96
- isPlausibleDomainRegistered,
97
- resolvePlausibleDataDomain,
98
- analyzePlausibleDomainCoverage,
99
- };