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
@@ -56,90 +56,6 @@ const {
56
56
 
57
57
  const AUDIT_TRAIL_TAG = 'audit-trail';
58
58
 
59
- /**
60
- * Anonymous fire-and-forget CLI feedback telemetry.
61
- *
62
- * Pings the hosted /v1/telemetry/ping endpoint exactly once per successful
63
- * local feedback capture so the dashboard can measure CLI-side lesson volume.
64
- *
65
- * Hard contract (do NOT widen without explicit approval):
66
- * - ONE event type: `feedback_captured`
67
- * - Payload: { installId, signal: 'up'|'down', tier, ts } only.
68
- * No context strings, tags, file paths, or content of any kind.
69
- * - Opt-out: THUMBGATE_DISABLE_TELEMETRY=1 (or 'true') short-circuits
70
- * immediately. Legacy THUMBGATE_NO_TELEMETRY=1 / DO_NOT_TRACK=1 are
71
- * also honored for parity with cli-telemetry.js.
72
- * - Fire-and-forget: NEVER await this call. Errors are swallowed.
73
- * - 2-second timeout via AbortSignal.timeout.
74
- */
75
- function emitAnonymousFeedbackPing(signal) {
76
- try {
77
- const env = process.env || {};
78
- if (
79
- env.THUMBGATE_DISABLE_TELEMETRY === '1' ||
80
- env.THUMBGATE_DISABLE_TELEMETRY === 'true' ||
81
- env.THUMBGATE_NO_TELEMETRY === '1' ||
82
- env.DO_NOT_TRACK === '1'
83
- ) {
84
- return;
85
- }
86
-
87
- const normalizedSignal = signal === 'positive' ? 'up' : signal === 'negative' ? 'down' : null;
88
- if (!normalizedSignal) return;
89
-
90
- // Reuse the canonical installId from cli-telemetry.js (persisted at
91
- // ~/.thumbgate/install-id). Falls back to a fresh UUID if that module
92
- // is unavailable — better to ship an event we can dedup on the server
93
- // than to drop the ping entirely.
94
- let installId = null;
95
- try {
96
- const { getInstallId } = require('./cli-telemetry');
97
- installId = getInstallId();
98
- } catch (_) { /* fall through */ }
99
- if (!installId) {
100
- try {
101
- installId = require('crypto').randomUUID();
102
- } catch (_) {
103
- return; // no crypto, no install id → drop silently
104
- }
105
- }
106
-
107
- let tier = 'free';
108
- try {
109
- const { getStatuslineMeta } = require('./statusline-meta');
110
- const meta = getStatuslineMeta({ env });
111
- const rawTier = String(meta && meta.tier ? meta.tier : 'free').toLowerCase();
112
- if (rawTier === 'pro' || rawTier === 'enterprise' || rawTier === 'free') {
113
- tier = rawTier;
114
- }
115
- } catch (_) { /* default to 'free' */ }
116
-
117
- const base = env.THUMBGATE_PUBLIC_APP_ORIGIN
118
- || env.THUMBGATE_API_URL
119
- || 'https://thumbgate-production.up.railway.app';
120
-
121
- const body = JSON.stringify({
122
- eventType: 'feedback_captured',
123
- clientType: 'cli',
124
- installId,
125
- signal: normalizedSignal,
126
- tier,
127
- ts: new Date().toISOString(),
128
- });
129
-
130
- // Fire-and-forget. No await. AbortSignal.timeout enforces the 2s cap.
131
- if (typeof fetch !== 'function' || typeof AbortSignal === 'undefined' || typeof AbortSignal.timeout !== 'function') {
132
- return;
133
- }
134
- fetch(`${base.replace(/\/+$/, '')}/v1/telemetry/ping`, {
135
- method: 'POST',
136
- headers: { 'Content-Type': 'application/json' },
137
- body,
138
- signal: AbortSignal.timeout(2000),
139
- }).catch(() => { /* fire-and-forget */ });
140
- } catch (_) { /* telemetry must never disrupt CLI */ }
141
- }
142
-
143
59
  function isAuditTrailEntry(entry = {}) {
144
60
  return Array.isArray(entry.tags) && entry.tags.includes(AUDIT_TRAIL_TAG);
145
61
  }
@@ -315,34 +231,6 @@ function getMemoryFirewallModule() {
315
231
  }
316
232
  }
317
233
 
318
- function launchSelfHarnessOptimizer() {
319
- if (process.env.THUMBGATE_DISABLE_SELF_HARNESS_OPTIMIZER === '1') {
320
- return { launched: false, reason: 'disabled' };
321
- }
322
-
323
- const explicitOptimizerPath = process.env.THUMBGATE_SELF_HARNESS_OPTIMIZER_PATH
324
- ? path.resolve(process.env.THUMBGATE_SELF_HARNESS_OPTIMIZER_PATH)
325
- : null;
326
- const localOptimizerPath = path.join(process.cwd(), 'scripts', 'self-harness-optimizer.js');
327
- const packageOptimizerPath = path.join(__dirname, 'self-harness-optimizer.js');
328
- const optimizerPath = explicitOptimizerPath
329
- || (fs.existsSync(localOptimizerPath) ? localOptimizerPath : null)
330
- || (fs.existsSync(packageOptimizerPath) ? packageOptimizerPath : null);
331
-
332
- if (!optimizerPath || !fs.existsSync(optimizerPath)) {
333
- return { launched: false, reason: 'missing' };
334
- }
335
-
336
- const { spawn } = require('child_process');
337
- const child = spawn(process.execPath, [optimizerPath], {
338
- detached: true,
339
- stdio: 'ignore',
340
- env: process.env,
341
- });
342
- child.unref();
343
- return { launched: true, path: optimizerPath };
344
- }
345
-
346
234
 
347
235
  function appendJSONL(filePath, record) {
348
236
  ensureDir(path.dirname(filePath));
@@ -1171,7 +1059,6 @@ function captureFeedback(params) {
1171
1059
  : null),
1172
1060
  structuredRule: structuredRule || null,
1173
1061
  ...(reflection && { reflection }),
1174
- gateAction: params.gateAction || null,
1175
1062
  timestamp: now,
1176
1063
  };
1177
1064
 
@@ -1225,7 +1112,6 @@ function captureFeedback(params) {
1225
1112
  summary.lastUpdated = now;
1226
1113
  saveSummary(summary);
1227
1114
  appendJSONL(FEEDBACK_LOG_PATH, feedbackEvent);
1228
- emitAnonymousFeedbackPing(signal);
1229
1115
  try { appendRejectionLedger(feedbackEvent, action.reason); } catch { /* non-critical */ }
1230
1116
  try {
1231
1117
  appendSequence(historyEntries, feedbackEvent, getFeedbackPaths(), { accepted: false });
@@ -1267,7 +1153,6 @@ function captureFeedback(params) {
1267
1153
  ...feedbackEvent,
1268
1154
  validationIssues: prepared.issues,
1269
1155
  });
1270
- emitAnonymousFeedbackPing(signal);
1271
1156
  try { appendRejectionLedger(feedbackEvent, `Schema validation failed: ${prepared.issues.join('; ')}`); } catch { /* non-critical */ }
1272
1157
  try {
1273
1158
  appendSequence(historyEntries, feedbackEvent, getFeedbackPaths(), { accepted: false });
@@ -1342,7 +1227,6 @@ function captureFeedback(params) {
1342
1227
  }
1343
1228
 
1344
1229
  appendJSONL(FEEDBACK_LOG_PATH, feedbackEvent);
1345
- emitAnonymousFeedbackPing(signal);
1346
1230
 
1347
1231
  // Synthesis: merge similar lessons instead of creating duplicates
1348
1232
  let synthesisResult = null;
@@ -1514,7 +1398,7 @@ function captureFeedback(params) {
1514
1398
  if (feedbackEvent.signal === 'negative') {
1515
1399
  try {
1516
1400
  const autoPromote = require('./auto-promote-gates');
1517
- const promoteResult = autoPromote.promote(FEEDBACK_LOG_PATH, { gateAction: feedbackEvent.gateAction });
1401
+ const promoteResult = autoPromote.promote(FEEDBACK_LOG_PATH);
1518
1402
  // First-rule activation telemetry: anonymous ping the first time
1519
1403
  // a prevention rule auto-promotes for this install. Idempotent —
1520
1404
  // see scripts/activation-tracker.js. Critical for activation funnel
@@ -1527,38 +1411,10 @@ function captureFeedback(params) {
1527
1411
  totalGates: promoteResult.totalGates,
1528
1412
  });
1529
1413
  } catch { /* activation telemetry is non-critical */ }
1530
-
1531
- // Trigger Self-Harness Optimizer to propagate the new rules to prompt
1532
- // files & validate. Use spawn instead of fork: fork creates an IPC
1533
- // pipe that keeps CLI feedback capture alive after the durable write.
1534
- try {
1535
- launchSelfHarnessOptimizer();
1536
- } catch (err) {
1537
- console.error('Failed to trigger self-harness optimizer:', err);
1538
- }
1539
1414
  }
1540
1415
  } catch { /* Gate promotion is non-critical */ }
1541
1416
  }
1542
1417
 
1543
- // Auto-export to Obsidian if configured (deferred but tracked)
1544
- if (process.env.THUMBGATE_OBSIDIAN_VAULT_PATH) {
1545
- const exportPromise = new Promise((resolve) => {
1546
- setImmediate(() => {
1547
- try {
1548
- const { exportAll } = require('./obsidian-export');
1549
- exportAll({
1550
- feedbackDir: FEEDBACK_DIR,
1551
- outputDir: process.env.THUMBGATE_OBSIDIAN_VAULT_PATH,
1552
- });
1553
- } catch (_err) {
1554
- // Non-critical, do not crash feedback loop
1555
- }
1556
- resolve();
1557
- });
1558
- });
1559
- trackBackgroundSideEffect(exportPromise);
1560
- }
1561
-
1562
1418
  // --- Deferred side-effects (contextFs, RLAIF — non-critical, potentially slow) ---
1563
1419
  setImmediate(() => {
1564
1420
  try {
@@ -62,92 +62,6 @@ function normalizeFeedbackText(value) {
62
62
  .trim();
63
63
  }
64
64
 
65
- function editDistance(a, b) {
66
- const left = String(a || '');
67
- const right = String(b || '');
68
- const dp = Array.from({ length: left.length + 1 }, () => Array(right.length + 1).fill(0));
69
- for (let i = 0; i <= left.length; i++) dp[i][0] = i;
70
- for (let j = 0; j <= right.length; j++) dp[0][j] = j;
71
- for (let i = 1; i <= left.length; i++) {
72
- for (let j = 1; j <= right.length; j++) {
73
- const cost = left[i - 1] === right[j - 1] ? 0 : 1;
74
- dp[i][j] = Math.min(
75
- dp[i - 1][j] + 1,
76
- dp[i][j - 1] + 1,
77
- dp[i - 1][j - 1] + cost,
78
- );
79
- }
80
- }
81
- return dp[left.length][right.length];
82
- }
83
-
84
- function isNearThumbToken(token) {
85
- const value = String(token || '');
86
- if (value.length < 4) return false;
87
- return editDistance(value, 'thumb') <= 1 || editDistance(value, 'thumbs') <= 2;
88
- }
89
-
90
- function isNearUpToken(token) {
91
- const value = String(token || '');
92
- return value === 'up' || editDistance(value, 'up') <= 1;
93
- }
94
-
95
- function isNearDownToken(token) {
96
- const value = String(token || '');
97
- if (value.length < 2) return false;
98
- return editDistance(value, 'down') <= 1;
99
- }
100
-
101
- function detectFeedbackSignal(value) {
102
- const raw = String(value || '');
103
- if (/[👎👎🏻👎🏼👎🏽👎🏾👎🏿]/u.test(raw)) return { signal: 'down', confidence: 'emoji', match: '👎' };
104
- if (/[👍👍🏻👍🏼👍🏽👍🏾👍🏿]/u.test(raw)) return { signal: 'up', confidence: 'emoji', match: '👍' };
105
-
106
- const normalized = normalizeFeedbackText(raw);
107
- if (!normalized) return null;
108
-
109
- const exactDown = [
110
- /\bthumbs?\s*down\b/,
111
- /\bthumbs?down\b/,
112
- /\bthat failed\b/,
113
- /\bit failed\b/,
114
- /\bthat was wrong\b/,
115
- /\bfix this\b/,
116
- ];
117
- if (exactDown.some((pattern) => pattern.test(normalized))) {
118
- return { signal: 'down', confidence: 'exact', match: normalized };
119
- }
120
-
121
- const exactUp = [
122
- /\bthumbs?\s*up\b/,
123
- /\bthumbs?up\b/,
124
- /\bthat worked\b/,
125
- /\bit worked\b/,
126
- /\blooks good\b/,
127
- /\bgood job\b/,
128
- /\bgood work\b/,
129
- /\bnice work\b/,
130
- /\bperfect\b/,
131
- /\blgtm\b/,
132
- ];
133
- if (exactUp.some((pattern) => pattern.test(normalized))) {
134
- return { signal: 'up', confidence: 'exact', match: normalized };
135
- }
136
-
137
- const words = normalized.split(/\s+/).filter(Boolean);
138
- for (let i = 0; i < words.length - 1; i++) {
139
- if (!isNearThumbToken(words[i])) continue;
140
- if (isNearDownToken(words[i + 1])) {
141
- return { signal: 'down', confidence: 'fuzzy', match: `${words[i]} ${words[i + 1]}` };
142
- }
143
- if (isNearUpToken(words[i + 1])) {
144
- return { signal: 'up', confidence: 'fuzzy', match: `${words[i]} ${words[i + 1]}` };
145
- }
146
- }
147
-
148
- return null;
149
- }
150
-
151
65
  function isGenericFeedbackText(value, signal) {
152
66
  const normalized = normalizeFeedbackText(value);
153
67
  if (!normalized) return false;
@@ -217,7 +131,6 @@ function buildClarificationMessage(params = {}) {
217
131
 
218
132
  module.exports = {
219
133
  GENERIC_PHRASE_RULES,
220
- detectFeedbackSignal,
221
134
  normalizeFeedbackSignal,
222
135
  normalizeFeedbackText,
223
136
  isGenericFeedbackText,
@@ -53,31 +53,10 @@ function listJsonFiles(dirPath) {
53
53
  return results;
54
54
  }
55
55
 
56
- const STOPWORDS = new Set([
57
- 'a', 'about', 'above', 'after', 'again', 'against', 'all', 'am', 'an', 'and', 'any', 'are', 'arent', 'as', 'at',
58
- 'be', 'because', 'been', 'before', 'being', 'below', 'between', 'both', 'but', 'by',
59
- 'cant', 'cannot', 'could', 'couldnt', 'did', 'didnt', 'do', 'does', 'doesnt', 'doing', 'dont', 'down', 'during',
60
- 'each', 'few', 'for', 'from', 'further', 'had', 'hadnt', 'has', 'hasnt', 'have', 'havent', 'having',
61
- 'he', 'hed', 'hell', 'hes', 'her', 'here', 'heres', 'hers', 'herself', 'him', 'himself', 'his',
62
- 'how', 'hows', 'i', 'id', 'ill', 'im', 'ive', 'if', 'in', 'into', 'is', 'isnt', 'it', 'its', 'itself',
63
- 'lets', 'me', 'more', 'most', 'mustnt', 'my', 'myself',
64
- 'no', 'nor', 'not', 'of', 'off', 'on', 'once', 'only', 'or', 'other', 'ought', 'our', 'ours', 'ourselves', 'out', 'over', 'own',
65
- 'same', 'shant', 'she', 'shed', 'shell', 'shes', 'should', 'shouldnt', 'so', 'some', 'such',
66
- 'than', 'that', 'thats', 'the', 'their', 'theirs', 'them', 'themselves', 'then', 'there', 'theres', 'these', 'they', 'theyd', 'theyll', 'theyre', 'theyve', 'this', 'those', 'through', 'to', 'too', 'under', 'until', 'up', 'very',
67
- 'was', 'wasnt', 'we', 'wed', 'well', 'were', 'weve', 'werent', 'what', 'whats', 'when', 'whens', 'where', 'wheres', 'which', 'while', 'who', 'whos', 'whom', 'why', 'whys',
68
- 'with', 'wont', 'would', 'wouldnt', 'you', 'youd', 'youll', 'youre', 'youve', 'your', 'yours', 'yourself', 'yourselves'
69
- ]);
70
-
71
56
  function tokenize(text) {
72
57
  return String(text || '').toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
73
58
  }
74
59
 
75
- function getSearchTokens(queryText) {
76
- const allTokens = tokenize(queryText);
77
- const contentTokens = allTokens.filter(t => !STOPWORDS.has(t));
78
- return contentTokens.length > 0 ? contentTokens : allTokens;
79
- }
80
-
81
60
  function unique(arr) {
82
61
  return [...new Set(arr)];
83
62
  }
@@ -103,7 +82,7 @@ function substringBoost(query, text) {
103
82
  const q = query.toLowerCase();
104
83
  const t = text.toLowerCase();
105
84
  if (t.includes(q)) return 0.3;
106
- const words = q.split(/\s+/).filter((w) => w.length > 2 && !STOPWORDS.has(w));
85
+ const words = q.split(/\s+/).filter((w) => w.length > 2);
107
86
  const matched = words.filter((w) => t.includes(w)).length;
108
87
  return words.length > 0 ? (matched / words.length) * 0.2 : 0;
109
88
  }
@@ -138,12 +117,8 @@ function scoreRecord(queryTokens, queryText, record) {
138
117
  const recency = recencyScore(record.timestamp);
139
118
  const signalBoost = record.signal === 'down' ? 0.05 : 0;
140
119
 
141
- const matchScore = jaccard + substr;
142
- const isWildcard = !queryText || queryText === '*';
143
- const score = isWildcard ? (recency + signalBoost || 0.01) : (matchScore > 0 ? matchScore + recency + signalBoost : 0);
144
-
145
120
  return {
146
- score: Number(score.toFixed(4)),
121
+ score: jaccard + substr + recency + signalBoost,
147
122
  record,
148
123
  matchedTokens: unique(queryTokens).filter((t) => new Set(recordTokens).has(t)),
149
124
  };
@@ -154,7 +129,7 @@ function scoreRecord(queryTokens, queryText, record) {
154
129
  // ---------------------------------------------------------------------------
155
130
 
156
131
  function searchFeedbackLog(queryText, limit = 5, options = {}) {
157
- const logPath = path.join(options.feedbackDir || getFeedbackDir(), 'feedback-log.jsonl');
132
+ const logPath = path.join(getFeedbackDir(), 'feedback-log.jsonl');
158
133
  let records = readJsonl(logPath);
159
134
 
160
135
  // SQLite fallback: if JSONL is empty/tiny, pull records from the lesson DB
@@ -181,7 +156,7 @@ function searchFeedbackLog(queryText, limit = 5, options = {}) {
181
156
 
182
157
  // Wildcard query: return all records sorted by recency
183
158
  const isWildcard = queryText === '*' || queryText === '';
184
- const queryTokens = isWildcard ? [] : getSearchTokens(queryText);
159
+ const queryTokens = isWildcard ? [] : tokenize(queryText);
185
160
 
186
161
  let scored = isWildcard
187
162
  ? records.map((r) => ({ score: recencyScore(r.timestamp) || 0.01, record: r, matchedTokens: [] }))
@@ -211,9 +186,9 @@ function searchFeedbackLog(queryText, limit = 5, options = {}) {
211
186
  }
212
187
 
213
188
  function searchContextFs(queryText, limit = 5, options = {}) {
214
- const contextDir = options.contextDir || (options.feedbackDir ? path.join(options.feedbackDir, 'contextfs') : getContextFsDir());
189
+ const contextDir = getContextFsDir();
215
190
  const namespaces = options.namespaces || ['memory/error', 'memory/learning', 'rules', 'raw_history'];
216
- const queryTokens = getSearchTokens(queryText);
191
+ const queryTokens = tokenize(queryText);
217
192
  const scored = [];
218
193
 
219
194
  for (const ns of namespaces) {
@@ -246,12 +221,12 @@ function searchContextFs(queryText, limit = 5, options = {}) {
246
221
  }));
247
222
  }
248
223
 
249
- function searchPreventionRules(queryText, limit = 5, options = {}) {
250
- const rulesPath = path.join(options.feedbackDir || getFeedbackDir(), 'prevention-rules.md');
224
+ function searchPreventionRules(queryText, limit = 5) {
225
+ const rulesPath = path.join(getFeedbackDir(), 'prevention-rules.md');
251
226
  if (!fs.existsSync(rulesPath)) return [];
252
227
 
253
228
  const content = fs.readFileSync(rulesPath, 'utf-8');
254
- const queryTokens = getSearchTokens(queryText);
229
+ const queryTokens = tokenize(queryText);
255
230
  const blocks = content.split(/^#{1,3}\s+/m).filter(Boolean);
256
231
 
257
232
  return blocks
@@ -277,7 +252,7 @@ function searchPreventionRules(queryText, limit = 5, options = {}) {
277
252
  function searchAll(queryText, limit = 10, options = {}) {
278
253
  const feedbackResults = searchFeedbackLog(queryText, limit, options);
279
254
  const contextResults = searchContextFs(queryText, limit, options);
280
- const ruleResults = searchPreventionRules(queryText, limit, options);
255
+ const ruleResults = searchPreventionRules(queryText, limit);
281
256
 
282
257
  const merged = [
283
258
  ...feedbackResults.map((r) => ({ ...r, _source_type: 'feedback' })),
@@ -9,7 +9,6 @@ const { sequencePathFor } = require('./risk-scorer');
9
9
 
10
10
  const PROJECT_ROOT = path.join(__dirname, '..');
11
11
  const MANUAL_GATES_PATH = path.join(PROJECT_ROOT, 'config', 'gates', 'default.json');
12
- const STATS_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'gate-stats.json');
13
12
 
14
13
  function loadGatesFile(filePath) {
15
14
  if (!fs.existsSync(filePath)) return [];
@@ -66,15 +65,6 @@ function calculateStats() {
66
65
  // sample can produce a misleading 0.0% floor.
67
66
  const bayesErrorRate = tryComputeBayesErrorRate();
68
67
 
69
- // Calibration: per-gate assessment of whether block actions are well-supported
70
- // by negative feedback, or potentially over/under-blocking without confirmation.
71
- const calibration = computeCalibration(allGates);
72
-
73
- // First-time fix rate: 1 - (recurringBlocks / totalBlocksAndWarns)
74
- // Measures how often a single gate fire resolves the issue vs the agent retrying.
75
- // Returns null when there is no recorded block/warn data yet.
76
- const firstTimeFixRate = computeFirstTimeFixRate();
77
-
78
68
  return {
79
69
  totalGates: allGates.length,
80
70
  manualGates: manualGates.length,
@@ -87,70 +77,10 @@ function calculateStats() {
87
77
  lastPromotion,
88
78
  estimatedHoursSaved,
89
79
  bayesErrorRate,
90
- calibration,
91
- firstTimeFixRate,
92
80
  gates: allGates,
93
81
  };
94
82
  }
95
83
 
96
- /**
97
- * Assess each gate's calibration by comparing block occurrences to confirmed
98
- * negative feedback counts. A gate with many blocks but no confirming negative
99
- * feedback may be over-blocking; one with matching feedback is well-calibrated.
100
- *
101
- * @param {Array} gates - Combined array of manual + auto-promoted gate objects
102
- * @returns {Array<{gateId: string, occurrences: number, action: string, calibrationNote: string}>}
103
- */
104
- function computeCalibration(gates) {
105
- const calibration = [];
106
- for (const gate of gates || []) {
107
- if (!gate || !gate.id) continue;
108
- const occurrences = Number(gate.occurrences || 0);
109
- const action = gate.action || 'unknown';
110
- // Only annotate gates with recorded occurrence data
111
- if (occurrences === 0) continue;
112
-
113
- if (action === 'block') {
114
- const confirmedNegative = Number(gate.confirmedNegative || gate.negativeCount || 0);
115
- let calibrationNote;
116
- if (occurrences > 10 && confirmedNegative === 0) {
117
- calibrationNote = `over-blocking (${occurrences} blocks, 0 confirmed)`;
118
- } else if (confirmedNegative > 0) {
119
- calibrationNote = `well-calibrated (${occurrences} blocks, ${confirmedNegative} confirmed)`;
120
- } else {
121
- // Low occurrence count with no feedback — not enough data yet
122
- calibrationNote = `insufficient data (${occurrences} blocks, 0 confirmed)`;
123
- }
124
- calibration.push({ gateId: gate.id, occurrences, action, calibrationNote });
125
- }
126
- }
127
- return calibration;
128
- }
129
-
130
- /**
131
- * Compute the first-time fix rate from the persisted gate-stats.json file.
132
- *
133
- * firstTimeFixRate = 1 - (recurringBlocks / totalBlocksAndWarns)
134
- *
135
- * Returns null when there are no recorded block/warn events yet.
136
- * Returns a number in [0, 1] otherwise, where 1.0 means every gate fire
137
- * was a first-time occurrence and 0.0 means every gate fired at least twice.
138
- */
139
- function computeFirstTimeFixRate() {
140
- try {
141
- if (!fs.existsSync(STATS_PATH)) return null;
142
- const raw = fs.readFileSync(STATS_PATH, 'utf8');
143
- const data = JSON.parse(raw);
144
- const totalBlocksAndWarns = (data.blocked || 0) + (data.warned || 0);
145
- if (totalBlocksAndWarns === 0) return null;
146
- const recurring = data.recurringBlocks || 0;
147
- const rate = 1 - (recurring / totalBlocksAndWarns);
148
- return Math.max(0, Math.min(1, rate));
149
- } catch {
150
- return null;
151
- }
152
- }
153
-
154
84
  function tryComputeBayesErrorRate() {
155
85
  try {
156
86
  const seqPath = sequencePathFor();
@@ -212,13 +142,6 @@ function formatStats(stats) {
212
142
  lines.push(` Last promotion: ${formatLastPromotion(stats.lastPromotion)}`);
213
143
  lines.push(` Estimated time saved: ~${stats.estimatedHoursSaved} hours`);
214
144
  lines.push(` Bayes error rate: ${formatBayesErrorRate(stats.bayesErrorRate)}`);
215
- lines.push(` First-time fix rate: ${formatFirstTimeFixRate(stats.firstTimeFixRate)}`);
216
- if (Array.isArray(stats.calibration) && stats.calibration.length > 0) {
217
- lines.push('Calibration:');
218
- for (const entry of stats.calibration) {
219
- lines.push(` - ${entry.gateId}: ${entry.calibrationNote}`);
220
- }
221
- }
222
145
  return lines.join('\n');
223
146
  }
224
147
 
@@ -230,14 +153,6 @@ function formatBayesErrorRate(rate) {
230
153
  return `${pct}% — high irreducible error; the feature set can't discriminate`;
231
154
  }
232
155
 
233
- function formatFirstTimeFixRate(rate) {
234
- if (rate === null || rate === undefined) return 'n/a (no blocks or warns recorded yet)';
235
- const pct = (rate * 100).toFixed(1);
236
- if (rate >= 0.95) return `${pct}% — agents fix issues on first block (excellent)`;
237
- if (rate >= 0.80) return `${pct}% — most blocks resolved first time (good)`;
238
- return `${pct}% — recurring blocks detected; agents may be ignoring gate feedback`;
239
- }
240
-
241
156
  if (require.main === module) {
242
157
  try {
243
158
  const stats = calculateStats();
@@ -253,11 +168,7 @@ module.exports = {
253
168
  formatStats,
254
169
  formatLastPromotion,
255
170
  formatBayesErrorRate,
256
- formatFirstTimeFixRate,
257
- computeFirstTimeFixRate,
258
171
  loadGatesFile,
259
172
  tryComputeBayesErrorRate,
260
- computeCalibration,
261
173
  MANUAL_GATES_PATH,
262
- STATS_PATH,
263
174
  };