thumbgate 1.35.0 → 1.37.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/.agents/skills/cyberstrike-compare-not-clone/SKILL.md +36 -0
  2. package/.agents/skills/gitlab-sandbox-allowlist-not-trust/SKILL.md +77 -0
  3. package/.agents/skills/jit-harness-compare-not-clone/SKILL.md +34 -0
  4. package/.agents/skills/openui-catalog-compose-honesty/SKILL.md +64 -0
  5. package/.agents/skills/zvec-grep-compare-not-clone/SKILL.md +34 -0
  6. package/.claude-plugin/plugin.json +1 -1
  7. package/.well-known/llms.txt +1 -0
  8. package/.well-known/mcp/server-card.json +1 -1
  9. package/CONTRIBUTING.md +95 -0
  10. package/README.md +195 -632
  11. package/THIRD_PARTY_NOTICES.md +89 -0
  12. package/adapters/claude/.mcp.json +2 -2
  13. package/adapters/forge/forge.yaml +3 -3
  14. package/adapters/future-agi/.mcp.json +8 -0
  15. package/adapters/future-agi/FUTURE_AGI.md +23 -0
  16. package/adapters/future-agi/config.toml +3 -0
  17. package/adapters/future-agi/future-agi-bridge.js +9 -0
  18. package/adapters/future-agi/opencode.json +8 -0
  19. package/adapters/herdr/herdr-plugin.toml +18 -0
  20. package/adapters/mcp/server-stdio.js +238 -25
  21. package/adapters/opencode/opencode.json +1 -1
  22. package/adapters/workos/WORKOS.md +52 -0
  23. package/bin/cli.js +336 -3
  24. package/bin/futureagi-bridge +9 -0
  25. package/config/gate-templates.json +581 -4
  26. package/config/gates/actor-critic-audit.json +34 -0
  27. package/config/gates/default.json +9 -3
  28. package/config/gates/five-walls-governance.json +34 -0
  29. package/config/gates/future-agi-guardrails.json +34 -0
  30. package/config/gates/radware-threat-defense-2026.json +61 -0
  31. package/config/gates/simatree-data-governance.json +33 -0
  32. package/config/mcp-allowlists.json +4 -0
  33. package/config/merge-quality-checks.json +6 -0
  34. package/config/model-candidates.json +312 -29
  35. package/config/model-tiers.json +18 -0
  36. package/config/post-deploy-marketing-pages.json +10 -0
  37. package/config/progressive/01-wire-only.json +11 -0
  38. package/config/progressive/02-dashboard-empty-ok.json +10 -0
  39. package/config/progressive/03-one-lesson.json +10 -0
  40. package/config/progressive/04-warn-fires.json +11 -0
  41. package/config/progressive/05-strict-optional.json +11 -0
  42. package/config/progressive/README.md +15 -0
  43. package/config/schemas/broker-execution-receipt.schema.json +139 -0
  44. package/config/schemas/provider-execution-attestation-v1.schema.json +58 -0
  45. package/conformance/provider-attestation/vectors.json +320 -0
  46. package/docs/specs/provider-execution-attestation-v1.md +69 -0
  47. package/openapi/openapi.yaml +15 -0
  48. package/package.json +390 -148
  49. package/public/about.html +2 -2
  50. package/public/ai-malpractice-prevention.html +7 -7
  51. package/public/blog/a-10-dollar-vps-is-not-a-computer.html +143 -0
  52. package/public/blog/a-receipt-is-not-world-state.html +388 -0
  53. package/public/blog/git-at-agent-scale.html +374 -0
  54. package/public/blog/no-llm-in-the-gate.html +133 -0
  55. package/public/blog.html +80 -0
  56. package/public/case-studies.html +16 -1
  57. package/public/compare.html +28 -0
  58. package/public/diagnostic.html +216 -7
  59. package/public/docs/connectors.html +39 -0
  60. package/public/federal.html +2 -2
  61. package/public/founders.html +639 -0
  62. package/public/index.html +87 -9
  63. package/public/install.html +8 -8
  64. package/public/learn.html +39 -0
  65. package/public/numbers.html +2 -2
  66. package/public/peter.html +310 -0
  67. package/public/platform-partners.html +119 -0
  68. package/public/pricing.html +24 -3
  69. package/public/privacy.html +117 -0
  70. package/public/pro.html +17 -0
  71. package/public/support.html +62 -0
  72. package/public/terms.html +130 -0
  73. package/public/third-party-notices.html +95 -0
  74. package/public/yt.html +351 -0
  75. package/scripts/action-receipts.js +133 -3
  76. package/scripts/adaptive-governance-arena.js +349 -0
  77. package/scripts/admin-override.js +205 -0
  78. package/scripts/agent-action-inventory.js +869 -0
  79. package/scripts/agent-audit-trace.js +42 -2
  80. package/scripts/agent-egress-policy.js +1117 -0
  81. package/scripts/agent-memory-lifecycle.js +141 -2
  82. package/scripts/agent-operations-planner.js +441 -1
  83. package/scripts/agent-readiness.js +68 -0
  84. package/scripts/agent-security-central.js +647 -0
  85. package/scripts/allowlist-bridge-honesty.js +417 -0
  86. package/scripts/async-job-runner.js +102 -11
  87. package/scripts/audit-trail.js +212 -0
  88. package/scripts/billing.js +1 -1
  89. package/scripts/broker-execution-receipts.js +719 -0
  90. package/scripts/budget-aware-gates-proof.js +423 -0
  91. package/scripts/claude-feedback-sync.js +29 -3
  92. package/scripts/claw-harness-production.js +237 -0
  93. package/scripts/cli-schema.js +163 -1
  94. package/scripts/codex-runbook-flywheel.js +318 -0
  95. package/scripts/context-footprint.js +186 -0
  96. package/scripts/contextfs.js +143 -61
  97. package/scripts/dashboard.js +251 -32
  98. package/scripts/deepseek-v4-runtime-guardrails.js +72 -6
  99. package/scripts/docker-sandbox-planner.js +18 -0
  100. package/scripts/double-blind-eval-protocol.js +252 -0
  101. package/scripts/edotenv-rl-gateway.js +259 -0
  102. package/scripts/ensure-production-search-corpus.js +162 -0
  103. package/scripts/eval-holdout.js +311 -0
  104. package/scripts/feedback-aggregate.js +21 -2
  105. package/scripts/feedback-loop.js +87 -5
  106. package/scripts/feedback-quality.js +9 -0
  107. package/scripts/file-ledger-lock.js +4 -1
  108. package/scripts/financial-control-plane.js +41 -1
  109. package/scripts/find-dormant-requires.js +118 -0
  110. package/scripts/fs-utils.js +84 -8
  111. package/scripts/gates-engine.js +810 -63
  112. package/scripts/generate-case-study-outreach.js +24 -15
  113. package/scripts/git-at-scale.js +628 -0
  114. package/scripts/governance-conflict-audit.js +1650 -0
  115. package/scripts/governance-difficulty-curriculum.js +328 -0
  116. package/scripts/graphrag-retrieval.js +275 -0
  117. package/scripts/gurobi-optimizer.js +324 -0
  118. package/scripts/gurobi_optimizer.py +485 -0
  119. package/scripts/harness-selector.js +82 -1
  120. package/scripts/hidden-entry-points.js +284 -0
  121. package/scripts/human-escalation.js +199 -1
  122. package/scripts/hybrid-feedback-context.js +152 -19
  123. package/scripts/intent-governed-execution.js +602 -0
  124. package/scripts/intervention-policy.js +123 -20
  125. package/scripts/jit-harness-compose.js +628 -0
  126. package/scripts/jsonl-watcher.js +10 -0
  127. package/scripts/lesson-embedding-index.js +95 -12
  128. package/scripts/lesson-retrieval.js +105 -19
  129. package/scripts/local-model-profile.js +19 -2
  130. package/scripts/mailer/resend-mailer.js +1 -1
  131. package/scripts/matryoshka-embedding.js +235 -0
  132. package/scripts/mcp-oauth.js +42 -4
  133. package/scripts/mcp-session-handles.js +1016 -0
  134. package/scripts/mcp-wiring-doctor.js +314 -0
  135. package/scripts/memory-firewall.js +115 -2
  136. package/scripts/memory-scope-readiness.js +299 -0
  137. package/scripts/memory-vs-rag-route.js +161 -0
  138. package/scripts/model-tier-router.js +148 -21
  139. package/scripts/nvidia-specdecode-al-doctor.js +536 -0
  140. package/scripts/openui-catalog-compose-honesty.js +593 -0
  141. package/scripts/operational-integrity.js +19 -1
  142. package/scripts/override-audit.js +213 -0
  143. package/scripts/package-manager-honesty-doctor.js +458 -0
  144. package/scripts/pr-manager.js +63 -1
  145. package/scripts/prove-herdr-adapter.js +52 -0
  146. package/scripts/prove-memory-pyramid-and-symbolic-canvas.js +95 -0
  147. package/scripts/prove-workos.js +73 -0
  148. package/scripts/provider-attestation-conformance.js +192 -0
  149. package/scripts/provider-receipt-contract.js +136 -0
  150. package/scripts/qwen38-max-cost-optimizer.js +401 -0
  151. package/scripts/radware-threat-defense.js +280 -0
  152. package/scripts/rag-embedding-identity.js +221 -0
  153. package/scripts/rag-precision-guardrails.js +112 -2
  154. package/scripts/remote-feedback-capture.js +159 -0
  155. package/scripts/research-agent-harness.js +256 -0
  156. package/scripts/rsi-safety-hillclimb.js +200 -0
  157. package/scripts/rule-sprawl.js +188 -0
  158. package/scripts/schedule-manager.js +147 -0
  159. package/scripts/self-heal.js +8 -0
  160. package/scripts/session-lease.js +415 -0
  161. package/scripts/simatree-data-governance.js +347 -0
  162. package/scripts/slo-alert-engine.js +172 -7
  163. package/scripts/solver-parity.js +539 -0
  164. package/scripts/stealth-memory-injection-gate.js +333 -0
  165. package/scripts/switchyard-router.js +366 -0
  166. package/scripts/telemetry-analytics.js +84 -27
  167. package/scripts/temporal-decay-weighting.js +138 -0
  168. package/scripts/test-all.js +165 -0
  169. package/scripts/token-savings.js +42 -0
  170. package/scripts/tool-kpi-tracker.js +108 -5
  171. package/scripts/tool-registry.js +193 -5
  172. package/scripts/universal-claim-evaluator.js +14 -2
  173. package/scripts/vector-store.js +279 -9
  174. package/scripts/workflow-notebook.js +391 -0
  175. package/scripts/workflow-sentinel.js +111 -12
  176. package/scripts/workos-production-guard.js +260 -0
  177. package/scripts/workspace-search-route.js +515 -0
  178. package/server.json +2 -2
  179. package/src/agent-identity-boundary.js +76 -0
  180. package/src/agent-retrieval-cache.js +155 -0
  181. package/src/alert-noise-ledger.js +502 -0
  182. package/src/api/server.js +724 -153
  183. package/src/git-fast-cache.js +220 -0
  184. package/src/git-wal-sync.js +156 -0
  185. package/src/hash-anchored-edit.js +82 -0
  186. package/src/hermes-platform-protocol.js +475 -0
  187. package/src/hermes-sync-plane.js +241 -0
  188. package/src/index.js +30 -1
  189. package/src/iso42001-compliance-guard.js +97 -0
  190. package/src/latency-budget.js +244 -0
  191. package/src/mcp-writeguard.js +316 -0
  192. package/src/miminions-adapter.js +106 -0
  193. package/src/pipeline-compass.js +104 -0
  194. package/src/ppl-alert-pipeline.js +284 -0
  195. package/src/rendezvous-router.js +90 -0
  196. package/src/security-questionnaire.js +195 -0
@@ -13,6 +13,7 @@ const {
13
13
  getFallbackFeedbackDir,
14
14
  resolveFallbackArtifactPath,
15
15
  } = require('./feedback-paths');
16
+ const { readTextTail } = require('./fs-utils');
16
17
 
17
18
  const TELEMETRY_FILE_NAME = 'telemetry-pings.jsonl';
18
19
  const MARKETING_CLICK_EVENT_TYPES = new Set([
@@ -531,7 +532,18 @@ function sanitizeTelemetryPayload(payload = {}, headers = {}) {
531
532
  return entry;
532
533
  }
533
534
 
535
+ function headerHasPrivacyOptOut(headers = {}) {
536
+ const normalized = {};
537
+ for (const [key, value] of Object.entries(headers || {})) {
538
+ normalized[String(key).toLowerCase()] = String(value == null ? '' : value).trim();
539
+ }
540
+ return normalized['sec-gpc'] === '1' || normalized.dnt === '1';
541
+ }
542
+
534
543
  function appendTelemetryEvent(feedbackDir, payload = {}, headers = {}) {
544
+ if (headerHasPrivacyOptOut(headers)) {
545
+ return null;
546
+ }
535
547
  const entry = sanitizeTelemetryPayload(payload, headers);
536
548
  const telemetryPath = getTelemetryPath(feedbackDir);
537
549
  fs.mkdirSync(path.dirname(telemetryPath), { recursive: true });
@@ -541,31 +553,36 @@ function appendTelemetryEvent(feedbackDir, payload = {}, headers = {}) {
541
553
 
542
554
  const DEFAULT_BOUNDED_TELEMETRY_TAIL_BYTES = 8 * 1024 * 1024;
543
555
 
544
- function readTelemetryText(filePath, options = {}) {
545
- if (!fs.existsSync(filePath)) return '';
546
- const maxBytes = Number(options.maxBytes || 0);
547
- if (maxBytes > 0) {
548
- const stats = fs.statSync(filePath);
549
- if (stats.size > maxBytes) {
550
- const fd = fs.openSync(filePath, 'r');
551
- try {
552
- const buffer = Buffer.alloc(maxBytes);
553
- fs.readSync(fd, buffer, 0, maxBytes, stats.size - maxBytes);
554
- const text = buffer.toString('utf-8');
555
- const firstNewline = text.indexOf('\n');
556
- return firstNewline >= 0 ? text.slice(firstNewline + 1) : text;
557
- } finally {
558
- fs.closeSync(fd);
559
- }
560
- }
556
+ // Delegates to readTextTail(), which applies an UNCONDITIONAL full-read ceiling
557
+ // and drops the partial first line after a mid-file seek.
558
+ //
559
+ // WHY: this function used to fall through to a full fs.readFileSync() whenever
560
+ // the caller passed no maxBytes. getTelemetrySummary() only supplies maxBytes for
561
+ // a `bounded` analytics window, so the default /v1/dashboard request read the
562
+ // whole telemetry log. Once production telemetry passed V8's max string length
563
+ // that threw
564
+ // "Cannot create a string longer than 0x1fffffe8 characters"
565
+ // which escaped generateDashboard() and 503'd /v1/dashboard ("Dashboard data too
566
+ // large") on every deploy-verification run. A size ceiling must not be opt-in.
567
+ function readTelemetryTextWithMeta(filePath, options = {}) {
568
+ if (!fs.existsSync(filePath)) return { text: '', truncated: false, size: 0 };
569
+ try {
570
+ const tail = readTextTail(filePath, Number(options.maxBytes || 0));
571
+ return { text: tail.text, truncated: Boolean(tail.truncated), size: tail.size || 0 };
572
+ } catch {
573
+ // A single unreadable/oversized telemetry log must never take down the
574
+ // whole dashboard. Degrade to "no telemetry", not to a 503 — but say so:
575
+ // an unreadable source is a hole in the data, not an empty data set.
576
+ return { text: '', truncated: true, size: 0, unreadable: true };
561
577
  }
562
- return fs.readFileSync(filePath, 'utf-8');
563
578
  }
564
579
 
565
- function loadTelemetryEventsFromPath(filePath, options = {}) {
566
- const raw = readTelemetryText(filePath, options).trim();
567
- if (!raw) return [];
568
- return raw
580
+ function readTelemetryText(filePath, options = {}) {
581
+ return readTelemetryTextWithMeta(filePath, options).text;
582
+ }
583
+
584
+ function parseTelemetryLines(raw) {
585
+ return String(raw || '')
569
586
  .split('\n')
570
587
  .map((line) => {
571
588
  try {
@@ -582,13 +599,26 @@ function loadTelemetryEventsFromPath(filePath, options = {}) {
582
599
  .filter(Boolean);
583
600
  }
584
601
 
585
- function loadTelemetryEvents(feedbackDir, options = {}) {
602
+ function loadTelemetryEventsFromPath(filePath, options = {}) {
603
+ const raw = readTelemetryText(filePath, options).trim();
604
+ if (!raw) return [];
605
+ return parseTelemetryLines(raw);
606
+ }
607
+
608
+ // Same merge as loadTelemetryEvents(), but reports whether any source was read
609
+ // only as a tail. Callers that label a result "lifetime" MUST consult this:
610
+ // a tail-read source means the counts below it are partial, not lifetime.
611
+ function loadTelemetryEventsWithMeta(feedbackDir, options = {}) {
586
612
  const diagnostics = getTelemetrySourceDiagnostics(feedbackDir);
587
613
  const merged = [];
588
614
  const seen = new Set();
615
+ const truncatedPaths = [];
589
616
 
590
617
  for (const filePath of diagnostics.activePaths) {
591
- const rows = loadTelemetryEventsFromPath(filePath, options);
618
+ const read = readTelemetryTextWithMeta(filePath, options);
619
+ if (read.truncated) truncatedPaths.push(filePath);
620
+ const raw = read.text.trim();
621
+ const rows = raw ? parseTelemetryLines(raw) : [];
592
622
  for (const row of rows) {
593
623
  const key = JSON.stringify(row);
594
624
  if (seen.has(key)) continue;
@@ -597,7 +627,15 @@ function loadTelemetryEvents(feedbackDir, options = {}) {
597
627
  }
598
628
  }
599
629
 
600
- return merged;
630
+ return {
631
+ events: merged,
632
+ truncated: truncatedPaths.length > 0,
633
+ truncatedPaths,
634
+ };
635
+ }
636
+
637
+ function loadTelemetryEvents(feedbackDir, options = {}) {
638
+ return loadTelemetryEventsWithMeta(feedbackDir, options).events;
601
639
  }
602
640
 
603
641
  function summarizeRecentEvents(events) {
@@ -779,8 +817,12 @@ function getTelemetrySummary(feedbackDir, options = {}) {
779
817
  const telemetryLoadOptions = analyticsWindow.bounded
780
818
  ? { maxBytes: Number(options.telemetryTailBytes || DEFAULT_BOUNDED_TELEMETRY_TAIL_BYTES) }
781
819
  : {};
820
+ // readTextTail() applies an unconditional ceiling even for the unbounded
821
+ // ("lifetime") window, so a large enough telemetry log is read as a tail.
822
+ // Carry that fact through instead of presenting a tail as complete history.
823
+ const telemetryRead = loadTelemetryEventsWithMeta(feedbackDir, telemetryLoadOptions);
782
824
  const events = filterEntriesForWindow(
783
- loadTelemetryEvents(feedbackDir, telemetryLoadOptions),
825
+ telemetryRead.events,
784
826
  analyticsWindow,
785
827
  (entry) => entry && (entry.receivedAt || entry.timestamp)
786
828
  );
@@ -1129,7 +1171,19 @@ function getTelemetrySummary(feedbackDir, options = {}) {
1129
1171
  const proConversions = checkoutPaidConfirmations;
1130
1172
 
1131
1173
  return {
1132
- window: serializeAnalyticsWindow(analyticsWindow),
1174
+ window: {
1175
+ ...serializeAnalyticsWindow(analyticsWindow),
1176
+ // A truncated source means these counts cover only the newest slice of
1177
+ // the log, so `lifetime` would be a lie. Consumers that present
1178
+ // acquisition/conversion rates must check `complete`.
1179
+ truncated: telemetryRead.truncated,
1180
+ complete: !telemetryRead.truncated,
1181
+ },
1182
+ telemetrySource: {
1183
+ truncated: telemetryRead.truncated,
1184
+ truncatedPaths: telemetryRead.truncatedPaths,
1185
+ tailBytes: Number(telemetryLoadOptions.maxBytes) || null,
1186
+ },
1133
1187
  totalEvents: events.length,
1134
1188
  latestSeenAt,
1135
1189
  trafficQuality,
@@ -1493,10 +1547,13 @@ module.exports = {
1493
1547
  TELEMETRY_FILE_NAME,
1494
1548
  sanitizeTelemetryPayload,
1495
1549
  classifyTelemetryAudience,
1550
+ headerHasPrivacyOptOut,
1496
1551
  appendTelemetryPing,
1497
1552
  appendTelemetryEvent,
1498
1553
  getTelemetrySourceDiagnostics,
1499
1554
  loadTelemetryEvents,
1555
+ loadTelemetryEventsWithMeta,
1500
1556
  getTelemetryAnalytics,
1557
+ getTelemetrySummary,
1501
1558
  inferTrafficChannel,
1502
1559
  };
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Temporal Decay Function for Lesson Retrieval Scoring
3
+ *
4
+ * Inspired by episode #1017: "The RAG Mistake Almost Every Team Is Making"
5
+ * Key insight: Traditional vector memory is insufficient. We need temporal filtering
6
+ * to avoid outdated information polluting retrieval results.
7
+ *
8
+ * This module adds:
9
+ * - Temporal decay weighting in retrieval scoring
10
+ * - Configurable TTL for lesson relevance
11
+ * - Half-life-based score degradation
12
+ */
13
+
14
+ const DEFAULT_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1000;
15
+
16
+ /**
17
+ * Coerce a lesson timestamp into a Date without stringifying epoch numbers
18
+ * (String(1785542400000) is not a valid Date input).
19
+ *
20
+ * @param {Date|string|number|null|undefined} lessonTimestamp
21
+ * @returns {Date}
22
+ */
23
+ function coerceLessonDate(lessonTimestamp) {
24
+ if (lessonTimestamp instanceof Date) {
25
+ return lessonTimestamp;
26
+ }
27
+ if (typeof lessonTimestamp === 'number' && Number.isFinite(lessonTimestamp)) {
28
+ return new Date(lessonTimestamp);
29
+ }
30
+ if (typeof lessonTimestamp === 'string' && lessonTimestamp.trim()) {
31
+ const asNumber = Number(lessonTimestamp);
32
+ if (Number.isFinite(asNumber) && /^-?\d+(\.\d+)?$/.test(lessonTimestamp.trim())) {
33
+ return new Date(asNumber);
34
+ }
35
+ return new Date(lessonTimestamp);
36
+ }
37
+ return new Date(NaN);
38
+ }
39
+
40
+ /**
41
+ * Apply temporal decay to an embedding score based on lesson age.
42
+ *
43
+ * @param {number} rawScore - Raw cosine similarity score (0-1)
44
+ * @param {Date|string|number} lessonTimestamp - When the lesson was recorded
45
+ * @param {number} halfLifeMs - Half-life of relevance (default: 30 days)
46
+ * @param {boolean} activeMode - Active investigation mode uses a longer half-life
47
+ * (slower decay) without boosting scores above the raw value
48
+ * @returns {number} Decayed score
49
+ */
50
+ function applyTemporalDecay(rawScore, lessonTimestamp, halfLifeMs = DEFAULT_HALF_LIFE_MS, activeMode = false) {
51
+ if (!rawScore || rawScore <= 0) return rawScore;
52
+
53
+ if (!Number.isFinite(halfLifeMs) || halfLifeMs <= 0) {
54
+ throw new TypeError('halfLifeMs must be a positive finite number');
55
+ }
56
+
57
+ const lessonDate = coerceLessonDate(lessonTimestamp);
58
+
59
+ // Handle invalid timestamps (e.g., from git metadata like "2026-07-31T19:48:50+02:00")
60
+ if (isNaN(lessonDate.getTime())) {
61
+ return rawScore * 0.1; // Penalize stale/untraceable lessons heavily
62
+ }
63
+
64
+ const ageMs = Math.max(0, Date.now() - lessonDate.getTime());
65
+ // Longer half-life = slower decay; never multiply the score itself.
66
+ const effectiveHalfLifeMs = activeMode ? halfLifeMs * 1.5 : halfLifeMs;
67
+ const decayFactor = Math.exp((-Math.log(2) * ageMs) / effectiveHalfLifeMs);
68
+
69
+ return rawScore * decimal(decayFactor, 4);
70
+ }
71
+
72
+ /**
73
+ * Apply combined scoring: base similarity + metadata boost + temporal decay
74
+ */
75
+ function computeContextualScore(rawScore, lesson, config = {}) {
76
+ let totalScore = rawScore;
77
+
78
+ const safeLesson = lesson || {};
79
+
80
+ // Metadata filters from podcast: tag-based relevance boosting
81
+ if (config.metadataFilters) {
82
+ const requiredTags = Array.isArray(config.metadataFilters.tags) ? config.metadataFilters.tags : [];
83
+ const hasTags =
84
+ Array.isArray(safeLesson.tags) && requiredTags.every((tag) => safeLesson.tags.includes(String(tag)));
85
+
86
+ totalScore = hasTags ? totalScore * 1.2 : Math.max(totalScore, rawScore); // Bonus for tag match
87
+ }
88
+
89
+ // Temporal decay always applies (including when no reranker is configured).
90
+ const timestamp =
91
+ safeLesson.timestamp != null && safeLesson.timestamp !== ''
92
+ ? safeLesson.timestamp
93
+ : safeLesson.receivedAt != null && safeLesson.receivedAt !== ''
94
+ ? safeLesson.receivedAt
95
+ : safeLesson.created_at != null && safeLesson.created_at !== ''
96
+ ? safeLesson.created_at
97
+ : undefined;
98
+
99
+ totalScore = applyTemporalDecay(
100
+ totalScore,
101
+ timestamp,
102
+ config.halfLifeMs || DEFAULT_HALF_LIFE_MS,
103
+ Boolean(config.activeMode)
104
+ );
105
+
106
+ // Reranker boost (cross-encoder or similar rerank module): if score > threshold, apply non-linear boost
107
+ if (totalScore > config.rerankThreshold && config.rrfBoost) {
108
+ totalScore = rrfDecay(totalScore * config.rrfBoost, config.rrfPenalty);
109
+ }
110
+
111
+ return decimal(totalScore, 4);
112
+ }
113
+
114
+ /**
115
+ * Re-Rank Decay (RRF-style) to prevent multiple hits from the same topic
116
+ * Episode insight: "post-retrieval semantic filtering" reduces duplication
117
+ */
118
+ function rrfDecay(score, penalty = 0.3) {
119
+ if (!score || score <= 1) return score;
120
+ return decimal((score - penalty) / (2 - penalty), 4);
121
+ }
122
+
123
+ /**
124
+ * Format a number to fixed decimal places
125
+ */
126
+ function decimal(num, places) {
127
+ const mult = Math.pow(10, places);
128
+ return Math.round(num * mult) / mult;
129
+ }
130
+
131
+ // Export for modular use in retrieval pipelines
132
+ module.exports = {
133
+ DEFAULT_HALF_LIFE_MS,
134
+ applyTemporalDecay,
135
+ coerceLessonDate,
136
+ computeContextualScore,
137
+ rrfDecay,
138
+ };
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * test-all — run every `test:*` npm script and AGGREGATE the results.
4
+ *
5
+ * WHY THIS EXISTS
6
+ * ---------------
7
+ * `npm test` is 360 sub-commands joined with `&&`. Two consequences, both bad:
8
+ *
9
+ * 1. `&&` short-circuits. The first failing suite hides the other 359, so CI
10
+ * can report "something failed" but never "these 12 failed". Every red run
11
+ * costs a full re-run per fix.
12
+ * 2. Membership in that chain is hand-maintained, so suites drift out of it.
13
+ * Measured 2026-08-27 on d0bb3768: 405 `test:*` scripts defined, 359 in the
14
+ * chain, **46 never executed** — including test:redteam,
15
+ * test:stealth-memory-injection, test:mcp-policy,
16
+ * test:reward-hacking-guardrails and test:proactive-agent-eval-guardrails.
17
+ * All six were run by hand and PASS (33 assertions). They were green and
18
+ * guarding nothing.
19
+ *
20
+ * This runner discovers suites instead of listing them, so a new `test:*` script
21
+ * is covered the moment it exists. It runs everything, then reports.
22
+ *
23
+ * USAGE
24
+ * node scripts/test-all.js run all, aggregate, exit 1 if any fail
25
+ * node scripts/test-all.js --list print the discovered suites, run none
26
+ * node scripts/test-all.js --orphans print suites missing from `npm test`
27
+ * node scripts/test-all.js --jobs=4 concurrency (default: cpus-1, min 1)
28
+ * node scripts/test-all.js --filter=re only suites whose name matches
29
+ */
30
+ 'use strict';
31
+
32
+ const { spawn } = require('node:child_process');
33
+ const os = require('node:os');
34
+ const path = require('node:path');
35
+ const fs = require('node:fs');
36
+
37
+ const ROOT = path.resolve(__dirname, '..');
38
+ const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
39
+
40
+ const argv = process.argv.slice(2);
41
+ const flag = (name) => argv.includes(`--${name}`);
42
+ const value = (name, dflt) => {
43
+ const hit = argv.find((a) => a.startsWith(`--${name}=`));
44
+ return hit ? hit.slice(name.length + 3) : dflt;
45
+ };
46
+
47
+ const SKIP_SELF = new Set([
48
+ // shells out to the whole suite
49
+ 'test:coverage',
50
+ // this runner itself — including it would recurse
51
+ 'test:all',
52
+ // diagnostic: exits 1 when the hand-maintained chain has drift
53
+ 'test:orphans',
54
+ ]);
55
+
56
+ const ALL = Object.keys(pkg.scripts)
57
+ .filter((n) => n.startsWith('test:'))
58
+ .filter((n) => !SKIP_SELF.has(n))
59
+ // Suite names are ASCII identifiers (`test:foo-bar`), so a byte-wise
60
+ // comparator is deterministic across locales — unlike bare `.sort()`.
61
+ .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
62
+
63
+ const chained = new Set(
64
+ String(pkg.scripts.test || '')
65
+ .split('&&')
66
+ .map((s) => s.trim())
67
+ .filter((s) => s.startsWith('npm run '))
68
+ .map((s) => s.replace('npm run ', '').trim())
69
+ );
70
+ const ORPHANS = ALL.filter((n) => !chained.has(n));
71
+
72
+ const filter = value('filter', null);
73
+ // `--filter` is a literal substring, never a regex. Building a RegExp from a
74
+ // command-line argument is a regex-injection sink (CodeQL js/regex-injection)
75
+ // and buys nothing: suite names are plain `test:foo-bar` identifiers.
76
+ const suites = filter ? ALL.filter((n) => n.includes(filter)) : ALL;
77
+
78
+ if (flag('list')) {
79
+ suites.forEach((s) => console.log(s));
80
+ process.exit(0);
81
+ }
82
+ if (flag('orphans')) {
83
+ console.log(`${ORPHANS.length} test:* scripts are NOT in the \`npm test\` chain:\n`);
84
+ ORPHANS.forEach((s) => console.log(' ' + s));
85
+ process.exit(ORPHANS.length ? 1 : 0);
86
+ }
87
+
88
+ const JOBS = Math.max(1, Number.parseInt(value('jobs', String(Math.max(1, os.cpus().length - 1))), 10) || 1);
89
+
90
+ function run(name) {
91
+ return new Promise((resolve) => {
92
+ const started = Date.now();
93
+ // Invoking `npm` by name is intentional: this runner executes inside a
94
+ // checkout where npm must come from the user's own PATH (brew/apt/scoop/
95
+ // Volta/nvm all install it in different places). The command name is a
96
+ // hard-coded literal and args is an array, so spawn never uses a shell.
97
+ const child = spawn('npm', ['run', '--silent', name], { // NOSONAR javascript:S4036
98
+ cwd: ROOT,
99
+ env: { ...process.env, NO_COLOR: '1' },
100
+ stdio: ['ignore', 'pipe', 'pipe'],
101
+ });
102
+ let out = '';
103
+ child.stdout.on('data', (d) => (out += d));
104
+ child.stderr.on('data', (d) => (out += d));
105
+ child.on('error', (err) =>
106
+ resolve({ name, code: -1, ms: Date.now() - started, out: String(err?.message) })
107
+ );
108
+ child.on('close', (code) => resolve({ name, code, ms: Date.now() - started, out }));
109
+ });
110
+ }
111
+
112
+ const num = (text, re) => {
113
+ const m = text.match(re);
114
+ return m ? Number(m[1]) : 0;
115
+ };
116
+
117
+ (async () => {
118
+ const started = Date.now();
119
+ const results = [];
120
+ let cursor = 0;
121
+
122
+ console.log(
123
+ `test-all: ${suites.length} suites, ${JOBS} parallel` +
124
+ (ORPHANS.length ? ` (${ORPHANS.length} of these are absent from \`npm test\`)` : '')
125
+ );
126
+
127
+ const worker = async () => {
128
+ while (cursor < suites.length) {
129
+ const name = suites[cursor++];
130
+ const r = await run(name);
131
+ results.push(r);
132
+ const mark = r.code === 0 ? 'ok ' : 'FAIL';
133
+ const tag = chained.has(name) ? ' ' : '[+]'; // [+] = previously never run
134
+ process.stdout.write(
135
+ ` ${mark} ${tag} ${name.padEnd(48)} ${String(Math.round(r.ms / 100) / 10).padStart(6)}s\n`
136
+ );
137
+ }
138
+ };
139
+ await Promise.all(Array.from({ length: Math.min(JOBS, suites.length) }, worker));
140
+
141
+ const failed = results.filter((r) => r.code !== 0);
142
+ const assertPass = results.reduce((a, r) => a + num(r.out, /^# pass (\d+)/m), 0);
143
+ const assertFail = results.reduce((a, r) => a + num(r.out, /^# fail (\d+)/m), 0);
144
+
145
+ console.log('\n' + '='.repeat(62));
146
+ console.log(`suites : ${results.length - failed.length}/${results.length} passed`);
147
+ console.log(`asserts : ${assertPass} passed, ${assertFail} failed`);
148
+ console.log(`wall : ${Math.round((Date.now() - started) / 1000)}s`);
149
+
150
+ if (failed.length) {
151
+ // The whole point: report EVERY failure, not just the first.
152
+ console.log(`\n${failed.length} FAILING SUITE(S):`);
153
+ for (const f of failed) {
154
+ const first =
155
+ f.out
156
+ .split('\n')
157
+ // `^` intentionally binds only to the "not ok " prefix; Error /
158
+ // AssertionError / failed may appear anywhere on the line. The
159
+ // grouping makes that precedence explicit (Sonar S5850).
160
+ .find((l) => /^(not ok )|Error|AssertionError|failed/i.test(l)) || '(no summary line)';
161
+ console.log(` ${f.name} exit=${f.code}\n ${first.trim().slice(0, 140)}`);
162
+ }
163
+ }
164
+ process.exit(failed.length ? 1 : 0);
165
+ })();
@@ -168,8 +168,47 @@ function formatTokens(count) {
168
168
  return String(Math.round(n));
169
169
  }
170
170
 
171
+ // Eye on AI / Trustwise (Manoj Saxena, public remarks): one user turn can
172
+ // fan out into 20–50 downstream tool calls (~40x tokens vs a simple prompt).
173
+ // These are modeled bounds for quoting — not ThumbGate production telemetry.
174
+ const AGENTIC_FANOUT_ACTIONS_LOW = 20;
175
+ const AGENTIC_FANOUT_ACTIONS_HIGH = 50;
176
+ const AGENTIC_TOKEN_MULTIPLIER_CAP = 40;
177
+
178
+ function estimateAgenticFanoutSpend(input = {}) {
179
+ const promptIn = clampNumber(input.promptInputTokens, 0);
180
+ const promptOut = clampNumber(input.promptOutputTokens, 0);
181
+ const requested = input.downstreamActions === undefined
182
+ ? AGENTIC_FANOUT_ACTIONS_LOW
183
+ : clampNumber(input.downstreamActions, AGENTIC_FANOUT_ACTIONS_LOW);
184
+ const actions = Math.min(Math.max(requested, 1), AGENTIC_FANOUT_ACTIONS_HIGH);
185
+ const prices = blendedPricePer1M(
186
+ input.modelMix || DEFAULT_MODEL_MIX,
187
+ input.modelPrices || DEFAULT_MODEL_PRICES,
188
+ );
189
+ const simpleUsd = (promptIn * prices.input + promptOut * prices.output) / 1e6;
190
+ const perActionOut = Math.max(promptOut, DEFAULT_AVG_OUTPUT_TOKENS_PER_BLOCK);
191
+ const fanoutUsd = actions * ((promptIn * prices.input + perActionOut * prices.output) / 1e6);
192
+ const rawRatio = simpleUsd > 0 ? fanoutUsd / simpleUsd : 0;
193
+ const modeledMultiplier = Math.min(rawRatio, AGENTIC_TOKEN_MULTIPLIER_CAP);
194
+ const agenticFanoutUsd = simpleUsd > 0
195
+ ? simpleUsd * modeledMultiplier
196
+ : fanoutUsd;
197
+ return {
198
+ modeledNotMeasured: true,
199
+ source: 'eye-on-ai-trustwise-fanout-bounds',
200
+ affiliation: 'Inspired by public Eye on AI conversation with Manoj Saxena / Trustwise. Not affiliated. Not their telemetry.',
201
+ downstreamActions: actions,
202
+ simplePromptUsd: simpleUsd,
203
+ agenticFanoutUsd,
204
+ modeledMultiplier,
205
+ multiplierCap: AGENTIC_TOKEN_MULTIPLIER_CAP,
206
+ };
207
+ }
208
+
171
209
  module.exports = {
172
210
  computeTokenSavings,
211
+ estimateAgenticFanoutSpend,
173
212
  formatDollars,
174
213
  formatTokens,
175
214
  blendedPricePer1M,
@@ -177,4 +216,7 @@ module.exports = {
177
216
  DEFAULT_MODEL_MIX,
178
217
  DEFAULT_AVG_INPUT_TOKENS_PER_BLOCK,
179
218
  DEFAULT_AVG_OUTPUT_TOKENS_PER_BLOCK,
219
+ AGENTIC_FANOUT_ACTIONS_LOW,
220
+ AGENTIC_FANOUT_ACTIONS_HIGH,
221
+ AGENTIC_TOKEN_MULTIPLIER_CAP,
180
222
  };