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.
- package/.agents/skills/cyberstrike-compare-not-clone/SKILL.md +36 -0
- package/.agents/skills/gitlab-sandbox-allowlist-not-trust/SKILL.md +77 -0
- package/.agents/skills/jit-harness-compare-not-clone/SKILL.md +34 -0
- package/.agents/skills/openui-catalog-compose-honesty/SKILL.md +64 -0
- package/.agents/skills/zvec-grep-compare-not-clone/SKILL.md +34 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/llms.txt +1 -0
- package/.well-known/mcp/server-card.json +1 -1
- package/CONTRIBUTING.md +95 -0
- package/README.md +195 -632
- package/THIRD_PARTY_NOTICES.md +89 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/forge/forge.yaml +3 -3
- package/adapters/future-agi/.mcp.json +8 -0
- package/adapters/future-agi/FUTURE_AGI.md +23 -0
- package/adapters/future-agi/config.toml +3 -0
- package/adapters/future-agi/future-agi-bridge.js +9 -0
- package/adapters/future-agi/opencode.json +8 -0
- package/adapters/herdr/herdr-plugin.toml +18 -0
- package/adapters/mcp/server-stdio.js +238 -25
- package/adapters/opencode/opencode.json +1 -1
- package/adapters/workos/WORKOS.md +52 -0
- package/bin/cli.js +336 -3
- package/bin/futureagi-bridge +9 -0
- package/config/gate-templates.json +581 -4
- package/config/gates/actor-critic-audit.json +34 -0
- package/config/gates/default.json +9 -3
- package/config/gates/five-walls-governance.json +34 -0
- package/config/gates/future-agi-guardrails.json +34 -0
- package/config/gates/radware-threat-defense-2026.json +61 -0
- package/config/gates/simatree-data-governance.json +33 -0
- package/config/mcp-allowlists.json +4 -0
- package/config/merge-quality-checks.json +6 -0
- package/config/model-candidates.json +312 -29
- package/config/model-tiers.json +18 -0
- package/config/post-deploy-marketing-pages.json +10 -0
- package/config/progressive/01-wire-only.json +11 -0
- package/config/progressive/02-dashboard-empty-ok.json +10 -0
- package/config/progressive/03-one-lesson.json +10 -0
- package/config/progressive/04-warn-fires.json +11 -0
- package/config/progressive/05-strict-optional.json +11 -0
- package/config/progressive/README.md +15 -0
- package/config/schemas/broker-execution-receipt.schema.json +139 -0
- package/config/schemas/provider-execution-attestation-v1.schema.json +58 -0
- package/conformance/provider-attestation/vectors.json +320 -0
- package/docs/specs/provider-execution-attestation-v1.md +69 -0
- package/openapi/openapi.yaml +15 -0
- package/package.json +390 -148
- package/public/about.html +2 -2
- package/public/ai-malpractice-prevention.html +7 -7
- package/public/blog/a-10-dollar-vps-is-not-a-computer.html +143 -0
- package/public/blog/a-receipt-is-not-world-state.html +388 -0
- package/public/blog/git-at-agent-scale.html +374 -0
- package/public/blog/no-llm-in-the-gate.html +133 -0
- package/public/blog.html +80 -0
- package/public/case-studies.html +16 -1
- package/public/compare.html +28 -0
- package/public/diagnostic.html +216 -7
- package/public/docs/connectors.html +39 -0
- package/public/federal.html +2 -2
- package/public/founders.html +639 -0
- package/public/index.html +87 -9
- package/public/install.html +8 -8
- package/public/learn.html +39 -0
- package/public/numbers.html +2 -2
- package/public/peter.html +310 -0
- package/public/platform-partners.html +119 -0
- package/public/pricing.html +24 -3
- package/public/privacy.html +117 -0
- package/public/pro.html +17 -0
- package/public/support.html +62 -0
- package/public/terms.html +130 -0
- package/public/third-party-notices.html +95 -0
- package/public/yt.html +351 -0
- package/scripts/action-receipts.js +133 -3
- package/scripts/adaptive-governance-arena.js +349 -0
- package/scripts/admin-override.js +205 -0
- package/scripts/agent-action-inventory.js +869 -0
- package/scripts/agent-audit-trace.js +42 -2
- package/scripts/agent-egress-policy.js +1117 -0
- package/scripts/agent-memory-lifecycle.js +141 -2
- package/scripts/agent-operations-planner.js +441 -1
- package/scripts/agent-readiness.js +68 -0
- package/scripts/agent-security-central.js +647 -0
- package/scripts/allowlist-bridge-honesty.js +417 -0
- package/scripts/async-job-runner.js +102 -11
- package/scripts/audit-trail.js +212 -0
- package/scripts/billing.js +1 -1
- package/scripts/broker-execution-receipts.js +719 -0
- package/scripts/budget-aware-gates-proof.js +423 -0
- package/scripts/claude-feedback-sync.js +29 -3
- package/scripts/claw-harness-production.js +237 -0
- package/scripts/cli-schema.js +163 -1
- package/scripts/codex-runbook-flywheel.js +318 -0
- package/scripts/context-footprint.js +186 -0
- package/scripts/contextfs.js +143 -61
- package/scripts/dashboard.js +251 -32
- package/scripts/deepseek-v4-runtime-guardrails.js +72 -6
- package/scripts/docker-sandbox-planner.js +18 -0
- package/scripts/double-blind-eval-protocol.js +252 -0
- package/scripts/edotenv-rl-gateway.js +259 -0
- package/scripts/ensure-production-search-corpus.js +162 -0
- package/scripts/eval-holdout.js +311 -0
- package/scripts/feedback-aggregate.js +21 -2
- package/scripts/feedback-loop.js +87 -5
- package/scripts/feedback-quality.js +9 -0
- package/scripts/file-ledger-lock.js +4 -1
- package/scripts/financial-control-plane.js +41 -1
- package/scripts/find-dormant-requires.js +118 -0
- package/scripts/fs-utils.js +84 -8
- package/scripts/gates-engine.js +810 -63
- package/scripts/generate-case-study-outreach.js +24 -15
- package/scripts/git-at-scale.js +628 -0
- package/scripts/governance-conflict-audit.js +1650 -0
- package/scripts/governance-difficulty-curriculum.js +328 -0
- package/scripts/graphrag-retrieval.js +275 -0
- package/scripts/gurobi-optimizer.js +324 -0
- package/scripts/gurobi_optimizer.py +485 -0
- package/scripts/harness-selector.js +82 -1
- package/scripts/hidden-entry-points.js +284 -0
- package/scripts/human-escalation.js +199 -1
- package/scripts/hybrid-feedback-context.js +152 -19
- package/scripts/intent-governed-execution.js +602 -0
- package/scripts/intervention-policy.js +123 -20
- package/scripts/jit-harness-compose.js +628 -0
- package/scripts/jsonl-watcher.js +10 -0
- package/scripts/lesson-embedding-index.js +95 -12
- package/scripts/lesson-retrieval.js +105 -19
- package/scripts/local-model-profile.js +19 -2
- package/scripts/mailer/resend-mailer.js +1 -1
- package/scripts/matryoshka-embedding.js +235 -0
- package/scripts/mcp-oauth.js +42 -4
- package/scripts/mcp-session-handles.js +1016 -0
- package/scripts/mcp-wiring-doctor.js +314 -0
- package/scripts/memory-firewall.js +115 -2
- package/scripts/memory-scope-readiness.js +299 -0
- package/scripts/memory-vs-rag-route.js +161 -0
- package/scripts/model-tier-router.js +148 -21
- package/scripts/nvidia-specdecode-al-doctor.js +536 -0
- package/scripts/openui-catalog-compose-honesty.js +593 -0
- package/scripts/operational-integrity.js +19 -1
- package/scripts/override-audit.js +213 -0
- package/scripts/package-manager-honesty-doctor.js +458 -0
- package/scripts/pr-manager.js +63 -1
- package/scripts/prove-herdr-adapter.js +52 -0
- package/scripts/prove-memory-pyramid-and-symbolic-canvas.js +95 -0
- package/scripts/prove-workos.js +73 -0
- package/scripts/provider-attestation-conformance.js +192 -0
- package/scripts/provider-receipt-contract.js +136 -0
- package/scripts/qwen38-max-cost-optimizer.js +401 -0
- package/scripts/radware-threat-defense.js +280 -0
- package/scripts/rag-embedding-identity.js +221 -0
- package/scripts/rag-precision-guardrails.js +112 -2
- package/scripts/remote-feedback-capture.js +159 -0
- package/scripts/research-agent-harness.js +256 -0
- package/scripts/rsi-safety-hillclimb.js +200 -0
- package/scripts/rule-sprawl.js +188 -0
- package/scripts/schedule-manager.js +147 -0
- package/scripts/self-heal.js +8 -0
- package/scripts/session-lease.js +415 -0
- package/scripts/simatree-data-governance.js +347 -0
- package/scripts/slo-alert-engine.js +172 -7
- package/scripts/solver-parity.js +539 -0
- package/scripts/stealth-memory-injection-gate.js +333 -0
- package/scripts/switchyard-router.js +366 -0
- package/scripts/telemetry-analytics.js +84 -27
- package/scripts/temporal-decay-weighting.js +138 -0
- package/scripts/test-all.js +165 -0
- package/scripts/token-savings.js +42 -0
- package/scripts/tool-kpi-tracker.js +108 -5
- package/scripts/tool-registry.js +193 -5
- package/scripts/universal-claim-evaluator.js +14 -2
- package/scripts/vector-store.js +279 -9
- package/scripts/workflow-notebook.js +391 -0
- package/scripts/workflow-sentinel.js +111 -12
- package/scripts/workos-production-guard.js +260 -0
- package/scripts/workspace-search-route.js +515 -0
- package/server.json +2 -2
- package/src/agent-identity-boundary.js +76 -0
- package/src/agent-retrieval-cache.js +155 -0
- package/src/alert-noise-ledger.js +502 -0
- package/src/api/server.js +724 -153
- package/src/git-fast-cache.js +220 -0
- package/src/git-wal-sync.js +156 -0
- package/src/hash-anchored-edit.js +82 -0
- package/src/hermes-platform-protocol.js +475 -0
- package/src/hermes-sync-plane.js +241 -0
- package/src/index.js +30 -1
- package/src/iso42001-compliance-guard.js +97 -0
- package/src/latency-budget.js +244 -0
- package/src/mcp-writeguard.js +316 -0
- package/src/miminions-adapter.js +106 -0
- package/src/pipeline-compass.js +104 -0
- package/src/ppl-alert-pipeline.js +284 -0
- package/src/rendezvous-router.js +90 -0
- 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
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
+
})();
|
package/scripts/token-savings.js
CHANGED
|
@@ -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
|
};
|