thumbgate 1.27.17 → 1.27.18

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 (165) hide show
  1. package/.claude-plugin/marketplace.json +85 -0
  2. package/.claude-plugin/plugin.json +3 -4
  3. package/.well-known/llms.txt +12 -33
  4. package/.well-known/mcp/server-card.json +8 -8
  5. package/README.md +30 -221
  6. package/adapters/chatgpt/openapi.yaml +1695 -0
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/gemini/function-declarations.json +0 -1
  9. package/adapters/mcp/server-stdio.js +11 -263
  10. package/adapters/opencode/opencode.json +1 -1
  11. package/bench/thumbgate-bench.json +2 -2
  12. package/bin/cli.js +122 -1421
  13. package/bin/postinstall.js +27 -11
  14. package/config/gate-templates.json +0 -216
  15. package/config/gates/claim-verification.json +0 -12
  16. package/config/gates/default.json +2 -31
  17. package/config/github-about.json +2 -2
  18. package/config/mcp-allowlists.json +13 -23
  19. package/config/merge-quality-checks.json +1 -0
  20. package/config/model-candidates.json +6 -121
  21. package/openapi/openapi.yaml +0 -12
  22. package/package.json +99 -229
  23. package/public/blog.html +4 -4
  24. package/public/codex-plugin.html +20 -72
  25. package/public/compare.html +8 -31
  26. package/public/dashboard.html +166 -930
  27. package/public/federal.html +2 -2
  28. package/public/guide.html +9 -33
  29. package/public/index.html +116 -495
  30. package/public/learn.html +18 -191
  31. package/public/lessons.html +10 -168
  32. package/public/numbers.html +7 -7
  33. package/public/pro.html +11 -34
  34. package/scripts/agent-memory-lifecycle.js +0 -211
  35. package/scripts/agent-readiness.js +3 -167
  36. package/scripts/agent-reward-model.js +1 -53
  37. package/scripts/auto-promote-gates.js +10 -82
  38. package/scripts/auto-wire-hooks.js +0 -14
  39. package/scripts/billing.js +1 -93
  40. package/scripts/build-metadata.js +10 -50
  41. package/scripts/cli-feedback.js +6 -18
  42. package/scripts/cli-schema.js +0 -90
  43. package/scripts/cli-telemetry.js +1 -6
  44. package/scripts/commercial-offer.js +2 -82
  45. package/scripts/context-manager.js +6 -74
  46. package/scripts/dashboard.js +2 -68
  47. package/scripts/durability/step.js +3 -3
  48. package/scripts/export-databricks-bundle.js +1 -5
  49. package/scripts/export-dpo-pairs.js +2 -7
  50. package/scripts/feedback-loop.js +1 -145
  51. package/scripts/feedback-quality.js +0 -87
  52. package/scripts/filesystem-search.js +10 -35
  53. package/scripts/gate-stats.js +0 -89
  54. package/scripts/gates-engine.js +85 -1105
  55. package/scripts/gemini-embedding-policy.js +1 -2
  56. package/scripts/hook-runtime.js +14 -20
  57. package/scripts/hook-thumbgate-cache-updater.js +2 -18
  58. package/scripts/hybrid-feedback-context.js +7 -142
  59. package/scripts/lesson-inference.js +3 -8
  60. package/scripts/lesson-search.js +1 -17
  61. package/scripts/license.js +1 -0
  62. package/scripts/llm-client.js +9 -390
  63. package/scripts/local-model-profile.js +8 -15
  64. package/scripts/memory-scope-readiness.js +0 -159
  65. package/scripts/meta-agent-loop.js +0 -36
  66. package/scripts/operational-integrity.js +5 -39
  67. package/scripts/oss-pr-opportunity-scout.js +5 -35
  68. package/scripts/plausible-server-events.js +6 -9
  69. package/scripts/pro-local-dashboard.js +4 -4
  70. package/scripts/proxy-pointer-rag-guardrails.js +1 -42
  71. package/scripts/rate-limiter.js +13 -64
  72. package/scripts/secret-scanner.js +5 -44
  73. package/scripts/security-scanner.js +10 -260
  74. package/scripts/self-distill-agent.js +1 -3
  75. package/scripts/seo-gsd.js +2 -916
  76. package/scripts/statusline-cache-path.js +2 -17
  77. package/scripts/statusline-local-stats.js +1 -9
  78. package/scripts/statusline-meta.js +2 -28
  79. package/scripts/statusline.sh +4 -20
  80. package/scripts/telemetry-analytics.js +0 -357
  81. package/scripts/thompson-sampling.js +10 -31
  82. package/scripts/thumbgate-bench.js +1 -16
  83. package/scripts/thumbgate-search.js +19 -85
  84. package/scripts/tool-registry.js +1 -169
  85. package/scripts/vector-store.js +0 -45
  86. package/scripts/workflow-sentinel.js +53 -286
  87. package/scripts/workspace-evolver.js +2 -62
  88. package/src/api/server.js +290 -2578
  89. package/.claude/commands/dashboard.md +0 -15
  90. package/.claude/commands/thumbgate-blocked.md +0 -27
  91. package/.claude/commands/thumbgate-dashboard.md +0 -15
  92. package/.claude/commands/thumbgate-doctor.md +0 -30
  93. package/.claude/commands/thumbgate-guard.md +0 -36
  94. package/.claude/commands/thumbgate-protect.md +0 -30
  95. package/.claude/commands/thumbgate-rules.md +0 -30
  96. package/.well-known/agentic-verify.txt +0 -1
  97. package/adapters/gcp/dfcx-webhook-gate.js +0 -295
  98. package/adapters/letta/README.md +0 -41
  99. package/adapters/letta/thumbgate-letta-adapter.js +0 -133
  100. package/adapters/policy-engine/ethicore-guardian-client.js +0 -68
  101. package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +0 -260
  102. package/bench/observability-eval-suite.json +0 -26
  103. package/bin/dashboard-cli.js +0 -7
  104. package/commands/dashboard.md +0 -15
  105. package/commands/thumbgate-dashboard.md +0 -15
  106. package/config/builtin-lessons.json +0 -23
  107. package/config/gate-classifier-routing.json +0 -98
  108. package/config/post-deploy-marketing-pages.json +0 -61
  109. package/public/about.html +0 -162
  110. package/public/agent-manager.html +0 -179
  111. package/public/agents-cost-savings.html +0 -153
  112. package/public/ai-malpractice-prevention.html +0 -818
  113. package/public/assets/brand/github-social-preview.png +0 -0
  114. package/public/assets/brand/thumbgate-icon-512.png +0 -0
  115. package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
  116. package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
  117. package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
  118. package/public/assets/brand/thumbgate-logo-transparent.svg +0 -22
  119. package/public/assets/brand/thumbgate-mark-inline-v3.svg +0 -19
  120. package/public/assets/brand/thumbgate-mark-pro.svg +0 -23
  121. package/public/assets/brand/thumbgate-mark-team.svg +0 -26
  122. package/public/assets/brand/thumbgate-mark.svg +0 -21
  123. package/public/assets/brand/thumbgate-wordmark.svg +0 -20
  124. package/public/assets/claude-thumbgate-statusbar.svg +0 -8
  125. package/public/assets/codex-thumbgate-statusbar-test.svg +0 -9
  126. package/public/assets/legal-intake-control-flow.svg +0 -66
  127. package/public/brand/thumbgate-mark.svg +0 -19
  128. package/public/brand/thumbgate-og.svg +0 -16
  129. package/public/chatgpt-app.html +0 -330
  130. package/public/codex-enterprise.html +0 -123
  131. package/public/js/buyer-intent.js +0 -672
  132. package/public/pricing.html +0 -399
  133. package/scripts/action-receipts.js +0 -324
  134. package/scripts/activation-quickstart.js +0 -187
  135. package/scripts/agent-operations-planner.js +0 -621
  136. package/scripts/ai-component-inventory.js +0 -367
  137. package/scripts/async-eval-observability.js +0 -236
  138. package/scripts/audit.js +0 -65
  139. package/scripts/aws-blocks-guardrails.js +0 -228
  140. package/scripts/classifier-routing.js +0 -130
  141. package/scripts/dashboard-chat.js +0 -332
  142. package/scripts/feedback-aggregate.js +0 -281
  143. package/scripts/feedback-sanitizer.js +0 -105
  144. package/scripts/hob-pack.js +0 -591
  145. package/scripts/hook-stop-anti-claim.js +0 -301
  146. package/scripts/install-shim.js +0 -87
  147. package/scripts/mcp-oauth.js +0 -293
  148. package/scripts/noop-detect.js +0 -285
  149. package/scripts/omlx-smoke.js +0 -192
  150. package/scripts/parallel-workflow-orchestrator.js +0 -293
  151. package/scripts/plan-gate.js +0 -243
  152. package/scripts/plausible-domain-config.js +0 -99
  153. package/scripts/qa-scenario-planner.js +0 -136
  154. package/scripts/repeat-metric.js +0 -137
  155. package/scripts/secret-fixture-tokens.js +0 -61
  156. package/scripts/secret-redaction.js +0 -166
  157. package/scripts/self-harness-optimizer.js +0 -141
  158. package/scripts/self-healing-check.js +0 -193
  159. package/scripts/silent-failure-cluster.js +0 -531
  160. package/scripts/statusline-cache-read.js +0 -57
  161. package/scripts/sync-telemetry-from-prod.js +0 -374
  162. package/scripts/tool-contract-validator.js +0 -76
  163. package/scripts/trajectory-scorer.js +0 -63
  164. package/scripts/verify-marketing-pages-deployed.js +0 -195
  165. package/scripts/visitor-journey.js +0 -172
@@ -1,281 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const fs = require('node:fs');
5
- const os = require('node:os');
6
- const path = require('node:path');
7
- const crypto = require('node:crypto');
8
- const {
9
- getFallbackFeedbackDir,
10
- getGlobalFeedbackDir,
11
- getHomeDir,
12
- getLegacyFeedbackDir,
13
- getThumbgateFeedbackDir,
14
- resolveFeedbackDir,
15
- resolveProjectDir,
16
- } = require('./feedback-paths');
17
- const { readJsonl } = require('./fs-utils');
18
-
19
- const FEEDBACK_LOG = 'feedback-log.jsonl';
20
- const MEMORY_LOG = 'memory-log.jsonl';
21
- const STATUSLINE_CACHE = 'statusline_cache.json';
22
-
23
- function truthyDisabled(value) {
24
- return value === '0' || value === 'false' || value === 'local';
25
- }
26
-
27
- function shouldAggregateFeedback(options = {}) {
28
- const env = options.env || process.env;
29
- return !truthyDisabled(String(env.THUMBGATE_STATUSLINE_AGGREGATE || env.THUMBGATE_AGGREGATE_FEEDBACK || '1').toLowerCase());
30
- }
31
-
32
- function normalizePath(candidate) {
33
- if (!candidate) return null;
34
- try {
35
- return path.resolve(String(candidate));
36
- } catch {
37
- return null;
38
- }
39
- }
40
-
41
- function uniquePaths(values = []) {
42
- const seen = new Set();
43
- const out = [];
44
- for (const value of values) {
45
- const resolved = normalizePath(value);
46
- if (!resolved || seen.has(resolved)) continue;
47
- seen.add(resolved);
48
- out.push(resolved);
49
- }
50
- return out;
51
- }
52
-
53
- function safeExists(candidate) {
54
- try {
55
- return Boolean(candidate && fs.existsSync(candidate));
56
- } catch {
57
- return false;
58
- }
59
- }
60
-
61
- function immediateChildDirs(parentDir) {
62
- try {
63
- return fs.readdirSync(parentDir, { withFileTypes: true })
64
- .filter((entry) => entry.isDirectory())
65
- .map((entry) => path.join(parentDir, entry.name));
66
- } catch {
67
- return [];
68
- }
69
- }
70
-
71
- function ancestorProjectFeedbackDirs(projectDir, options = {}) {
72
- const home = normalizePath(getHomeDir(options));
73
- const start = normalizePath(projectDir);
74
- if (!start) return [];
75
-
76
- const dirs = [];
77
- let cursor = start;
78
- while (cursor && cursor !== path.dirname(cursor)) {
79
- dirs.push(
80
- path.join(cursor, '.thumbgate'),
81
- path.join(cursor, '.thumbgate-compat'),
82
- path.join(cursor, '.claude', 'memory', 'feedback')
83
- );
84
- if (home && cursor === home) break;
85
- const next = path.dirname(cursor);
86
- if (next === cursor) break;
87
- cursor = next;
88
- }
89
- return dirs;
90
- }
91
-
92
- function listFeedbackStoreDirs(options = {}) {
93
- const env = options.env || process.env;
94
- const projectDir = resolveProjectDir({ cwd: options.cwd, env, projectDir: options.projectDir });
95
- const home = getHomeDir({ env });
96
- const homeThumbgate = path.join(home, '.thumbgate');
97
- const projectsDir = path.join(homeThumbgate, 'projects');
98
- const explicitRoots = String(env.THUMBGATE_AGGREGATE_ROOTS || '')
99
- .split(path.delimiter)
100
- .map((value) => value.trim())
101
- .filter(Boolean);
102
- const explicitFeedbackDir = options.feedbackDir || env.THUMBGATE_FEEDBACK_DIR;
103
- const normalizedExplicitFeedbackDir = normalizePath(explicitFeedbackDir);
104
-
105
- if (
106
- normalizedExplicitFeedbackDir &&
107
- normalizedExplicitFeedbackDir.startsWith(normalizePath(os.tmpdir()) + path.sep) &&
108
- explicitRoots.length === 0
109
- ) {
110
- return uniquePaths([explicitFeedbackDir])
111
- .filter((dir) => safeExists(path.join(dir, FEEDBACK_LOG)) || safeExists(path.join(dir, MEMORY_LOG)) || safeExists(path.join(dir, STATUSLINE_CACHE)));
112
- }
113
-
114
- return uniquePaths([
115
- explicitFeedbackDir,
116
- resolveFeedbackDir({ projectDir, env }),
117
- getThumbgateFeedbackDir({ projectDir, env }),
118
- getFallbackFeedbackDir({ projectDir, env }),
119
- getLegacyFeedbackDir({ projectDir, env }),
120
- getGlobalFeedbackDir({ projectDir, env }),
121
- homeThumbgate,
122
- ...immediateChildDirs(projectsDir),
123
- ...ancestorProjectFeedbackDirs(projectDir, { env }),
124
- ...explicitRoots,
125
- ]).filter((dir) => safeExists(path.join(dir, FEEDBACK_LOG)) || safeExists(path.join(dir, MEMORY_LOG)) || safeExists(path.join(dir, STATUSLINE_CACHE)));
126
- }
127
-
128
- function normalizeSignal(signal) {
129
- const raw = String(signal || '').toLowerCase();
130
- if (raw === 'positive' || raw === 'up' || raw === 'thumbs_up') return 'positive';
131
- if (raw === 'negative' || raw === 'down' || raw === 'thumbs_down') return 'negative';
132
- return raw || null;
133
- }
134
-
135
- function stableEntryKey(entry = {}, source = {}) {
136
- const id = entry.id || entry.feedbackId || entry.sourceFeedbackId;
137
- if (id) return `id:${id}`;
138
- const material = JSON.stringify({
139
- sourcePath: source.logPath || '',
140
- sourceIndex: Number.isFinite(source.index) ? source.index : -1,
141
- entry,
142
- });
143
- return `sha:${crypto.createHash('sha256').update(material).digest('hex')}`;
144
- }
145
-
146
- function sortByTimestamp(entries) {
147
- return entries.sort((a, b) => {
148
- const at = a.timestamp ? Date.parse(a.timestamp) : 0;
149
- const bt = b.timestamp ? Date.parse(b.timestamp) : 0;
150
- return (Number.isFinite(at) ? at : 0) - (Number.isFinite(bt) ? bt : 0);
151
- });
152
- }
153
-
154
- function collectAggregateLogEntries(fileName, options = {}) {
155
- const stores = listFeedbackStoreDirs(options);
156
- const seen = new Set();
157
- const entries = [];
158
-
159
- for (const dir of stores) {
160
- const logPath = path.join(dir, fileName);
161
- if (!safeExists(logPath)) continue;
162
- const rows = readJsonl(logPath, { maxLines: 0 }) || [];
163
- for (let index = 0; index < rows.length; index += 1) {
164
- const rawEntry = rows[index];
165
- const entry = { ...rawEntry };
166
- if (entry.signal || entry.feedback) entry.signal = normalizeSignal(entry.signal || entry.feedback);
167
- const key = stableEntryKey(entry, { logPath, index });
168
- if (seen.has(key)) continue;
169
- seen.add(key);
170
- entries.push({ ...entry, sourceFeedbackDir: dir });
171
- }
172
- }
173
-
174
- return {
175
- entries: sortByTimestamp(entries),
176
- stores,
177
- };
178
- }
179
-
180
- function summarizeFeedbackEntries(entries) {
181
- let totalPositive = 0;
182
- let totalNegative = 0;
183
- let rubricSamples = 0;
184
-
185
- for (const entry of entries) {
186
- if (entry.signal === 'positive') totalPositive += 1;
187
- if (entry.signal === 'negative') totalNegative += 1;
188
- if (entry.rubric && entry.rubric.weightedScore != null) rubricSamples += 1;
189
- }
190
-
191
- return { totalPositive, totalNegative, rubricSamples };
192
- }
193
-
194
- function createRateWindows(total, totalPositive, approvalRate) {
195
- return {
196
- '7d': { total: 0, positive: 0, rate: 0 },
197
- '30d': { total: 0, positive: 0, rate: 0 },
198
- lifetime: { total, positive: totalPositive, rate: approvalRate },
199
- };
200
- }
201
-
202
- function applyEntryToRateWindow(window, entry) {
203
- window.total += 1;
204
- if (entry.signal === 'positive') window.positive += 1;
205
- }
206
-
207
- function updateRateWindows(windows, entries, now = Date.now()) {
208
- const sevenDays = 7 * 24 * 60 * 60 * 1000;
209
- const thirtyDays = 30 * 24 * 60 * 60 * 1000;
210
-
211
- for (const entry of entries) {
212
- const ts = entry.timestamp ? Date.parse(entry.timestamp) : Number.NaN;
213
- if (!Number.isFinite(ts)) continue;
214
- const age = now - ts;
215
- if (age <= sevenDays) applyEntryToRateWindow(windows['7d'], entry);
216
- if (age <= thirtyDays) applyEntryToRateWindow(windows['30d'], entry);
217
- }
218
- }
219
-
220
- function finalizeRateWindows(windows) {
221
- for (const key of ['7d', '30d']) {
222
- const window = windows[key];
223
- window.rate = 0;
224
- if (window.total > 0) {
225
- window.rate = Math.round((window.positive / window.total) * 1000) / 1000;
226
- }
227
- }
228
- }
229
-
230
- function trendFromRateWindows(windows) {
231
- const hasTrendData = windows['7d'].total > 0 && windows['30d'].total > 0;
232
- if (hasTrendData) {
233
- if (windows['7d'].rate > windows['30d'].rate + 0.05) return 'improving';
234
- if (windows['7d'].rate < windows['30d'].rate - 0.05) return 'degrading';
235
- }
236
- return 'stable';
237
- }
238
-
239
- function computeAggregateFeedbackStats(options = {}) {
240
- const { entries, stores } = collectAggregateLogEntries(FEEDBACK_LOG, options);
241
- const memory = collectAggregateLogEntries(MEMORY_LOG, options);
242
- const { totalPositive, totalNegative, rubricSamples } = summarizeFeedbackEntries(entries);
243
- const total = totalPositive + totalNegative;
244
- const approvalRate = total > 0 ? Math.round((totalPositive / total) * 1000) / 1000 : 0;
245
- const windows = createRateWindows(total, totalPositive, approvalRate);
246
- updateRateWindows(windows, entries);
247
- finalizeRateWindows(windows);
248
- const trend = trendFromRateWindows(windows);
249
-
250
- return {
251
- total,
252
- totalPositive,
253
- totalNegative,
254
- approvalRate,
255
- recentRate: windows['7d'].rate || approvalRate,
256
- trend,
257
- windows,
258
- rubric: { samples: rubricSamples || memory.entries.length, blockedPromotions: 0, failingCriteria: {} },
259
- aggregate: {
260
- enabled: true,
261
- stores: stores.length,
262
- feedbackLogPaths: stores.map((dir) => path.join(dir, FEEDBACK_LOG)),
263
- memoryStores: memory.stores.length,
264
- },
265
- };
266
- }
267
-
268
- function getAggregateStatuslineCachePath(options = {}) {
269
- const env = options.env || process.env;
270
- if (env.THUMBGATE_STATUSLINE_CACHE) return path.resolve(env.THUMBGATE_STATUSLINE_CACHE);
271
- return path.join(getHomeDir({ env }), '.thumbgate', STATUSLINE_CACHE);
272
- }
273
-
274
- module.exports = {
275
- collectAggregateLogEntries,
276
- computeAggregateFeedbackStats,
277
- getAggregateStatuslineCachePath,
278
- listFeedbackStoreDirs,
279
- normalizeSignal,
280
- shouldAggregateFeedback,
281
- };
@@ -1,105 +0,0 @@
1
- 'use strict';
2
-
3
- const crypto = require('crypto');
4
-
5
- const TRANSPORT_KEYS = new Set([
6
- 'hookeventname',
7
- 'hook_event_name',
8
- 'sessionid',
9
- 'session_id',
10
- 'transcriptpath',
11
- 'transcript_path',
12
- 'timestamp',
13
- 'createdat',
14
- 'created_at',
15
- 'updatedat',
16
- 'updated_at',
17
- 'cwd',
18
- 'pid',
19
- 'processid',
20
- 'process_id',
21
- 'promptid',
22
- 'prompt_id',
23
- 'traceid',
24
- 'trace_id',
25
- 'requestid',
26
- 'request_id',
27
- 'installid',
28
- 'install_id',
29
- 'visitorsessionid',
30
- 'visitor_session_id',
31
- 'toolinput',
32
- 'tool_input',
33
- ]);
34
-
35
- const TRANSPORT_WORDS = new Set([
36
- ...TRANSPORT_KEYS,
37
- 'hook',
38
- 'event',
39
- 'userpromptsubmit',
40
- 'user_prompt_submit',
41
- 'pretooluse',
42
- 'pre_tool_use',
43
- 'posttooluse',
44
- 'post_tool_use',
45
- 'claude',
46
- 'codex',
47
- 'projects',
48
- 'redacted',
49
- 'tmp',
50
- 'private',
51
- 'folders',
52
- 'json',
53
- ]);
54
-
55
- function stripEphemeralText(text) {
56
- if (!text || typeof text !== 'string') return '';
57
- return String(text)
58
- .replace(/["']?(?:hook_?event_?name|session_?id|transcript_?path|timestamp|created_?at|updated_?at|cwd|pid|process_?id|prompt_?id|trace_?id|request_?id|install_?id|visitor_?session_?id)["']?\s*[:=]\s*["']?[^"',}\]\s]+["']?/gi, ' ')
59
- .replace(/\/(?:private\/)?tmp\/[^\s"',}\]]+/gi, ' ')
60
- .replace(/\/var\/folders\/[^\s"',}\]]+/gi, ' ')
61
- .replace(/\/Users\/[^/\s]+\/\.(?:claude|codex|thumbgate)\/[^\s"',}\]]+/gi, ' ')
62
- .replace(/\/Users\/[^/\s]+\/\.config\/thumbgate\/[^\s"',}\]]+/gi, ' ')
63
- .replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, ' ')
64
- .replace(/\b[0-9a-f]{24,}\b/gi, ' ')
65
- .replace(/\b\d{4}-\d{2}-\d{2}t\d{2}:\d{2}:\d{2}(?:\.\d+)?z?\b/gi, ' ')
66
- .replace(/\b\d{10,13}\b/g, ' ')
67
- .replace(/:\d{4,5}\b/g, ':PORT');
68
- }
69
-
70
- function transportWordsOnly(text) {
71
- const tokens = String(text || '')
72
- .toLowerCase()
73
- .replace(/[^a-z0-9_ -]/g, ' ')
74
- .split(/\s+/)
75
- .filter((token) => token.length >= 3);
76
- if (tokens.length === 0) return true;
77
- return tokens.every((token) => TRANSPORT_WORDS.has(token));
78
- }
79
-
80
- function sanitizeFeedbackText(text) {
81
- const stripped = stripEphemeralText(text)
82
- .replace(/\/Users\/[^\s/]+/g, '/Users/redacted')
83
- .replace(/\s+/g, ' ')
84
- .trim();
85
- if (transportWordsOnly(stripped)) return '';
86
- return stripped;
87
- }
88
-
89
- function actionFingerprint(parts) {
90
- const raw = Array.isArray(parts) ? parts.join(' ') : String(parts || '');
91
- const stable = sanitizeFeedbackText(raw)
92
- .toLowerCase()
93
- .replace(/[^a-z0-9._:/ -]/g, ' ')
94
- .replace(/\s+/g, ' ')
95
- .trim();
96
- if (!stable || transportWordsOnly(stable)) return null;
97
- return crypto.createHash('sha256').update(stable).digest('hex').slice(0, 16);
98
- }
99
-
100
- module.exports = {
101
- TRANSPORT_WORDS,
102
- sanitizeFeedbackText,
103
- actionFingerprint,
104
- transportWordsOnly,
105
- };