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
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* RAG embedding identity — fail-closed model/dim/provider contract.
|
|
6
|
+
*
|
|
7
|
+
* Research source (2026-08-12): Super Data Science #1017 with Pete Johnson
|
|
8
|
+
* (MongoDB Field CTO of AI) — "The RAG Mistake Almost Every Team Is Making"
|
|
9
|
+
* https://www.youtube.com/watch?v=jAPGTVlNoD4
|
|
10
|
+
*
|
|
11
|
+
* Claim translated into enforcement:
|
|
12
|
+
* - Embedding model choice is the underrated ROI lever (not just the LLM).
|
|
13
|
+
* - Mixing embedding models / dimensions silently destroys retrieval quality.
|
|
14
|
+
* - Matryoshka progressive disclosure (coarse → fine) is a valid cost/quality
|
|
15
|
+
* tradeoff only when truncation is from a single compatible base model.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
normalizeToMatryoshkaDimension,
|
|
20
|
+
buildMatryoshkaConfig,
|
|
21
|
+
EMBEDDING_QUALITY_THRESHOLDS,
|
|
22
|
+
} = require('./matryoshka-embedding');
|
|
23
|
+
|
|
24
|
+
function parseEmbeddingIdentity(fingerprint, dimensionHint) {
|
|
25
|
+
const raw = String(fingerprint || 'unknown').trim() || 'unknown';
|
|
26
|
+
const parts = raw.split(':').map((p) => p.trim()).filter(Boolean);
|
|
27
|
+
const dimensionFromParts = parts
|
|
28
|
+
.map((p) => Number(p))
|
|
29
|
+
.find((n) => Number.isFinite(n) && n > 0);
|
|
30
|
+
const dimension = Number.isFinite(Number(dimensionHint)) && Number(dimensionHint) > 0
|
|
31
|
+
? Number(dimensionHint)
|
|
32
|
+
: dimensionFromParts || null;
|
|
33
|
+
const model = parts.find((p) => /embed|gemini|text-|qwen|openai|voyage|bge|e5|mini/i.test(p)) || parts[1] || null;
|
|
34
|
+
const source = parts[0] || raw;
|
|
35
|
+
return {
|
|
36
|
+
fingerprint: raw,
|
|
37
|
+
source,
|
|
38
|
+
model,
|
|
39
|
+
dimension,
|
|
40
|
+
identityKey: [source, model || 'unspecified', dimension || 'na'].join('|'),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function assertCompatibleEmbeddings({
|
|
45
|
+
queryProvider,
|
|
46
|
+
queryDimension,
|
|
47
|
+
documentProvider,
|
|
48
|
+
documentDimension,
|
|
49
|
+
allowUnknown = false,
|
|
50
|
+
} = {}) {
|
|
51
|
+
const query = parseEmbeddingIdentity(queryProvider, queryDimension);
|
|
52
|
+
const document = parseEmbeddingIdentity(documentProvider, documentDimension);
|
|
53
|
+
const issues = [];
|
|
54
|
+
|
|
55
|
+
if (!allowUnknown && (query.fingerprint === 'unknown' || document.fingerprint === 'unknown')) {
|
|
56
|
+
issues.push({
|
|
57
|
+
code: 'unknown_embedding_identity',
|
|
58
|
+
severity: 'high',
|
|
59
|
+
message: 'Embedding provider fingerprint is unknown; refuse silent semantic ranking.',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (
|
|
64
|
+
query.fingerprint
|
|
65
|
+
&& document.fingerprint
|
|
66
|
+
&& query.fingerprint !== 'unknown'
|
|
67
|
+
&& document.fingerprint !== 'unknown'
|
|
68
|
+
&& query.fingerprint !== document.fingerprint
|
|
69
|
+
) {
|
|
70
|
+
// Same provider family with only dim suffix mismatch is handled below.
|
|
71
|
+
const queryBase = query.fingerprint.replace(/:\d+$/, '');
|
|
72
|
+
const docBase = document.fingerprint.replace(/:\d+$/, '');
|
|
73
|
+
if (queryBase !== docBase) {
|
|
74
|
+
issues.push({
|
|
75
|
+
code: 'embedding_provider_mismatch',
|
|
76
|
+
severity: 'high',
|
|
77
|
+
message: `Query provider ${query.fingerprint} is incompatible with document provider ${document.fingerprint}.`,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (
|
|
83
|
+
Number.isFinite(query.dimension)
|
|
84
|
+
&& Number.isFinite(document.dimension)
|
|
85
|
+
&& query.dimension !== document.dimension
|
|
86
|
+
) {
|
|
87
|
+
issues.push({
|
|
88
|
+
code: 'embedding_dimension_mismatch',
|
|
89
|
+
severity: 'high',
|
|
90
|
+
message: `Query dim ${query.dimension} != document dim ${document.dimension}. Use Matryoshka truncation from one base vector, not mixed spaces.`,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
ok: issues.length === 0,
|
|
96
|
+
query,
|
|
97
|
+
document,
|
|
98
|
+
issues,
|
|
99
|
+
failClosed: issues.some((i) => i.severity === 'high'),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Progressive Matryoshka plan (coarse filter → fine re-rank).
|
|
105
|
+
* Pete Johnson framing: cheaper dims for funnel; full dims for precision.
|
|
106
|
+
*/
|
|
107
|
+
function buildProgressiveRetrievalPlan(options = {}) {
|
|
108
|
+
const config = buildMatryoshkaConfig(options);
|
|
109
|
+
const baseDim = config.baseDimension;
|
|
110
|
+
const coarseDim = normalizeToMatryoshkaDimension(
|
|
111
|
+
Math.min(256, baseDim)
|
|
112
|
+
);
|
|
113
|
+
const midDim = normalizeToMatryoshkaDimension(
|
|
114
|
+
Math.min(768, baseDim)
|
|
115
|
+
);
|
|
116
|
+
return {
|
|
117
|
+
strategy: 'progressive_matryoshka',
|
|
118
|
+
researchSource: {
|
|
119
|
+
episode: 'Super Data Science #1017',
|
|
120
|
+
guest: 'Pete Johnson (MongoDB Field CTO of AI)',
|
|
121
|
+
url: 'https://www.youtube.com/watch?v=jAPGTVlNoD4',
|
|
122
|
+
thesis: 'Embedding model choice + compatible dimensions dominate RAG ROI.',
|
|
123
|
+
},
|
|
124
|
+
baseDimension: baseDim,
|
|
125
|
+
stages: [
|
|
126
|
+
{
|
|
127
|
+
stage: 1,
|
|
128
|
+
name: 'coarse_filter',
|
|
129
|
+
dimension: coarseDim,
|
|
130
|
+
topKMultiplier: 4,
|
|
131
|
+
purpose: 'cheap semantic funnel over full corpus',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
stage: 2,
|
|
135
|
+
name: 'fine_rerank',
|
|
136
|
+
dimension: midDim < baseDim ? midDim : baseDim,
|
|
137
|
+
topKMultiplier: 1,
|
|
138
|
+
purpose: 'precision re-rank of coarse survivors',
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
qualityThresholds: EMBEDDING_QUALITY_THRESHOLDS,
|
|
142
|
+
embeddingModel: config.embeddingModel,
|
|
143
|
+
provider: config.provider,
|
|
144
|
+
rules: [
|
|
145
|
+
'Never cosine-compare vectors from two different embedding models.',
|
|
146
|
+
'Truncate Matryoshka vectors only from a single full base embedding.',
|
|
147
|
+
'Re-embed the whole corpus when provider or base model changes.',
|
|
148
|
+
'Prefer hybrid lexical+dense until embedding identity is proven stable.',
|
|
149
|
+
],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const MIN_GOLDEN_CASES = 6;
|
|
154
|
+
|
|
155
|
+
function scoreEmbeddingRoiReadiness({
|
|
156
|
+
providerPinned = false,
|
|
157
|
+
modelPinned = false,
|
|
158
|
+
dimensionPinned = false,
|
|
159
|
+
hybridEnabled = false,
|
|
160
|
+
goldenRecall = null,
|
|
161
|
+
goldenPrecision = null,
|
|
162
|
+
goldenCaseCount = null,
|
|
163
|
+
perCaseRecalls = null,
|
|
164
|
+
mixedProviderCorpus = false,
|
|
165
|
+
} = {}) {
|
|
166
|
+
const issues = [];
|
|
167
|
+
if (mixedProviderCorpus) {
|
|
168
|
+
issues.push('mixed_provider_corpus');
|
|
169
|
+
}
|
|
170
|
+
if (!providerPinned) issues.push('provider_unpinned');
|
|
171
|
+
if (!modelPinned) issues.push('model_unpinned');
|
|
172
|
+
if (!dimensionPinned) issues.push('dimension_unpinned');
|
|
173
|
+
if (!hybridEnabled) issues.push('hybrid_disabled');
|
|
174
|
+
if (!Number.isFinite(goldenRecall) || goldenRecall < EMBEDDING_QUALITY_THRESHOLDS.recall) {
|
|
175
|
+
issues.push('golden_recall_below_bar');
|
|
176
|
+
}
|
|
177
|
+
if (!Number.isFinite(goldenPrecision) || goldenPrecision < EMBEDDING_QUALITY_THRESHOLDS.precision) {
|
|
178
|
+
issues.push('golden_precision_below_bar');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// AGENTS.md production RAG gate: ≥6 golden cases and 100% per-case recall.
|
|
182
|
+
// Aggregate recall/precision alone must not mark ready (Codex P2).
|
|
183
|
+
const caseCount = Number.isFinite(Number(goldenCaseCount))
|
|
184
|
+
? Number(goldenCaseCount)
|
|
185
|
+
: (Array.isArray(perCaseRecalls) ? perCaseRecalls.length : null);
|
|
186
|
+
if (!Number.isFinite(caseCount) || caseCount < MIN_GOLDEN_CASES) {
|
|
187
|
+
issues.push('golden_case_count_below_bar');
|
|
188
|
+
}
|
|
189
|
+
if (!Array.isArray(perCaseRecalls) || perCaseRecalls.length < MIN_GOLDEN_CASES) {
|
|
190
|
+
issues.push('per_case_recall_missing');
|
|
191
|
+
} else if (perCaseRecalls.some((r) => !Number.isFinite(Number(r)) || Number(r) < 1)) {
|
|
192
|
+
issues.push('per_case_recall_below_bar');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ROI framing from episode: committees+metrics without embedding discipline = no ROI.
|
|
196
|
+
const score = Math.max(0, 100 - issues.length * 12);
|
|
197
|
+
return {
|
|
198
|
+
score,
|
|
199
|
+
ready: issues.length === 0,
|
|
200
|
+
issues,
|
|
201
|
+
minGoldenCases: MIN_GOLDEN_CASES,
|
|
202
|
+
recommendation: issues.includes('mixed_provider_corpus')
|
|
203
|
+
? 'Rebuild the embedding cache under one provider+model+dim before trusting dense recall.'
|
|
204
|
+
: issues.includes('model_unpinned')
|
|
205
|
+
? 'Pin THUMBGATE_EMBED_PROVIDER / model explicitly — embedding choice is the underrated ROI lever.'
|
|
206
|
+
: (issues.includes('golden_case_count_below_bar')
|
|
207
|
+
|| issues.includes('per_case_recall_missing')
|
|
208
|
+
|| issues.includes('per_case_recall_below_bar'))
|
|
209
|
+
? 'Provide ≥6 golden cases with 100% per-case recall before marking embedding ROI ready.'
|
|
210
|
+
: issues.length
|
|
211
|
+
? 'Close embedding identity gaps, then re-run eval:rag golden suite.'
|
|
212
|
+
: 'Embedding identity is pinned; keep progressive Matryoshka + hybrid eval green.',
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = {
|
|
217
|
+
parseEmbeddingIdentity,
|
|
218
|
+
assertCompatibleEmbeddings,
|
|
219
|
+
buildProgressiveRetrievalPlan,
|
|
220
|
+
scoreEmbeddingRoiReadiness,
|
|
221
|
+
};
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
const { listGateTemplates } = require('./gate-templates');
|
|
5
|
+
const { buildMatryoshkaConfig } = require('./matryoshka-embedding');
|
|
5
6
|
|
|
6
7
|
const DOCUMENT_RAG_CATEGORY = 'Document RAG Safety';
|
|
7
8
|
const PRECISION_TEMPLATE_IDS = new Set([
|
|
@@ -22,7 +23,42 @@ function toNumber(value) {
|
|
|
22
23
|
return Number.isFinite(num) ? num : null;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
function normalizeProvider(value) {
|
|
27
|
+
const text = String(value || '').trim().toLowerCase();
|
|
28
|
+
return text || null;
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
function normalizeOptions(options = {}) {
|
|
32
|
+
const baselineProvider = normalizeProvider(options['baseline-provider'] || options.baselineProvider);
|
|
33
|
+
const newProvider = normalizeProvider(options['new-provider'] || options.newProvider || options['embedding-provider']);
|
|
34
|
+
const baselineModel = normalizeProvider(options['baseline-model'] || options.baselineModel);
|
|
35
|
+
const newModel = normalizeProvider(options['new-model'] || options.newModel || options['embedding-model']);
|
|
36
|
+
const baselineDim = toNumber(options['baseline-dim'] || options['baseline-dimensions'] || options.baselineDim);
|
|
37
|
+
const newDim = toNumber(options['new-dim'] || options['new-dimensions'] || options['matryoshka-dim'] || options.newDim);
|
|
38
|
+
const providerChanged = Boolean(
|
|
39
|
+
baselineProvider && newProvider && baselineProvider !== newProvider
|
|
40
|
+
);
|
|
41
|
+
const modelChanged = Boolean(
|
|
42
|
+
baselineModel && newModel && baselineModel !== newModel
|
|
43
|
+
);
|
|
44
|
+
const dimReduced = (
|
|
45
|
+
baselineDim !== null
|
|
46
|
+
&& newDim !== null
|
|
47
|
+
&& newDim > 0
|
|
48
|
+
&& baselineDim > 0
|
|
49
|
+
&& newDim < baselineDim
|
|
50
|
+
);
|
|
51
|
+
const embeddingModelChange = normalizeBoolean(
|
|
52
|
+
options['embedding-model-change']
|
|
53
|
+
|| options['provider-change']
|
|
54
|
+
|| options['model-change']
|
|
55
|
+
) || providerChanged || modelChanged;
|
|
56
|
+
const matryoshkaTruncation = normalizeBoolean(
|
|
57
|
+
options['matryoshka-truncation']
|
|
58
|
+
|| options['matryoshka']
|
|
59
|
+
|| options['dim-truncation']
|
|
60
|
+
) || dimReduced;
|
|
61
|
+
|
|
26
62
|
return {
|
|
27
63
|
ragTool: String(options['rag-tool'] || options.tool || 'agentic-rag').trim() || 'agentic-rag',
|
|
28
64
|
baselineRecall: toNumber(options['baseline-recall'] || options.recall),
|
|
@@ -32,6 +68,14 @@ function normalizeOptions(options = {}) {
|
|
|
32
68
|
topK: toNumber(options['top-k'] || options.k),
|
|
33
69
|
thresholdChanged: normalizeBoolean(options['threshold-change'] || options['threshold-changed']),
|
|
34
70
|
embeddingFineTune: normalizeBoolean(options['embedding-finetune'] || options['embedding-fine-tune'] || options['fine-tune']),
|
|
71
|
+
embeddingModelChange,
|
|
72
|
+
matryoshkaTruncation,
|
|
73
|
+
baselineProvider,
|
|
74
|
+
newProvider,
|
|
75
|
+
baselineModel,
|
|
76
|
+
newModel,
|
|
77
|
+
baselineDim,
|
|
78
|
+
newDim,
|
|
35
79
|
structuralNearMisses: normalizeBoolean(options['structural-near-misses'] || options['near-misses']),
|
|
36
80
|
verifier: normalizeBoolean(options.verifier || options.reranker || options['second-stage']),
|
|
37
81
|
hybridRetrieval: normalizeBoolean(options['hybrid-retrieval'] || options.hybrid),
|
|
@@ -57,6 +101,8 @@ function templateApplicability(template, options) {
|
|
|
57
101
|
if (template.id === 'require-rag-baseline-before-precision-tuning') {
|
|
58
102
|
return options.thresholdChanged ||
|
|
59
103
|
options.embeddingFineTune ||
|
|
104
|
+
options.embeddingModelChange ||
|
|
105
|
+
options.matryoshkaTruncation ||
|
|
60
106
|
options.baselineRecall === null ||
|
|
61
107
|
options.newRecall === null ||
|
|
62
108
|
(recallDropPercent(options) !== null && recallDropPercent(options) > 5);
|
|
@@ -75,6 +121,8 @@ function buildSignals(options) {
|
|
|
75
121
|
const drop = recallDropPercent(options);
|
|
76
122
|
return [
|
|
77
123
|
precisionTuningSignal(options, drop),
|
|
124
|
+
embeddingModelSignal(options),
|
|
125
|
+
matryoshkaDimSignal(options),
|
|
78
126
|
ragCascadeSignal(options),
|
|
79
127
|
verifierLatencySignal(options),
|
|
80
128
|
hybridScaleSignal(options),
|
|
@@ -83,19 +131,63 @@ function buildSignals(options) {
|
|
|
83
131
|
}
|
|
84
132
|
|
|
85
133
|
function precisionTuningSignal(options, drop) {
|
|
86
|
-
if (!(options.thresholdChanged || options.embeddingFineTune ||
|
|
134
|
+
if (!(options.thresholdChanged || options.embeddingFineTune || options.embeddingModelChange
|
|
135
|
+
|| options.matryoshkaTruncation || drop !== null)) return null;
|
|
87
136
|
return {
|
|
88
137
|
id: 'precision_tuning',
|
|
89
138
|
label: 'Precision tuning change',
|
|
90
139
|
values: [
|
|
91
140
|
options.thresholdChanged ? 'threshold changed' : null,
|
|
92
141
|
options.embeddingFineTune ? 'embedding fine-tune' : null,
|
|
142
|
+
options.embeddingModelChange ? 'embedding model/provider change' : null,
|
|
143
|
+
options.matryoshkaTruncation ? 'matryoshka dim truncation' : null,
|
|
93
144
|
drop !== null ? `recall drop ${drop}%` : null,
|
|
94
145
|
].filter(Boolean),
|
|
95
146
|
risk: 'precision wins can hide broad retrieval recall regressions',
|
|
96
147
|
};
|
|
97
148
|
}
|
|
98
149
|
|
|
150
|
+
function embeddingModelSignal(options) {
|
|
151
|
+
if (!options.embeddingModelChange) return null;
|
|
152
|
+
const values = [
|
|
153
|
+
options.baselineProvider && options.newProvider
|
|
154
|
+
? `provider ${options.baselineProvider} -> ${options.newProvider}`
|
|
155
|
+
: null,
|
|
156
|
+
options.baselineModel && options.newModel
|
|
157
|
+
? `model ${options.baselineModel} -> ${options.newModel}`
|
|
158
|
+
: 'embedding model/provider swap proposed',
|
|
159
|
+
options.baselineRecall === null ? 'missing baseline recall@k' : null,
|
|
160
|
+
options.newRecall === null ? 'missing post-change recall@k' : null,
|
|
161
|
+
].filter(Boolean);
|
|
162
|
+
return {
|
|
163
|
+
id: 'embedding_model_choice',
|
|
164
|
+
label: 'Embedding model choice risk',
|
|
165
|
+
values,
|
|
166
|
+
risk: 'embedding model choice is the highest-leverage RAG decision; swaps without a recall baseline are the most common silent regression',
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function matryoshkaDimSignal(options) {
|
|
171
|
+
if (!options.matryoshkaTruncation) return null;
|
|
172
|
+
const values = [
|
|
173
|
+
options.baselineDim !== null && options.newDim !== null
|
|
174
|
+
? `dims ${options.baselineDim} -> ${options.newDim}`
|
|
175
|
+
: 'matryoshka truncation proposed',
|
|
176
|
+
options.baselineDim !== null && options.newDim !== null && options.newDim < options.baselineDim
|
|
177
|
+
? `storage cut ${Math.round((1 - (options.newDim / options.baselineDim)) * 100)}%`
|
|
178
|
+
: null,
|
|
179
|
+
options.baselineRecall === null || options.newRecall === null
|
|
180
|
+
? 'truncation must re-measure recall@k after L2 re-normalization'
|
|
181
|
+
: null,
|
|
182
|
+
].filter(Boolean);
|
|
183
|
+
return {
|
|
184
|
+
id: 'matryoshka_dim_truncation',
|
|
185
|
+
label: 'Matryoshka dimension truncation',
|
|
186
|
+
values,
|
|
187
|
+
risk: 'leading-dim truncation only stays quality-safe after re-normalization and a measured recall baseline — never cut dims as a silent cost optimization',
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
99
191
|
function ragCascadeSignal(options) {
|
|
100
192
|
if (!(options.structuralNearMisses || options.agenticPipeline)) return null;
|
|
101
193
|
return {
|
|
@@ -187,6 +279,14 @@ function buildRagPrecisionGuardrailsPlan(rawOptions = {}, templatesPath) {
|
|
|
187
279
|
recallDropPercent: recallDropPercent(options),
|
|
188
280
|
baselinePrecision: options.baselinePrecision,
|
|
189
281
|
newPrecision: options.newPrecision,
|
|
282
|
+
embeddingModelChange: options.embeddingModelChange,
|
|
283
|
+
matryoshkaTruncation: options.matryoshkaTruncation,
|
|
284
|
+
baselineProvider: options.baselineProvider,
|
|
285
|
+
newProvider: options.newProvider,
|
|
286
|
+
baselineModel: options.baselineModel,
|
|
287
|
+
newModel: options.newModel,
|
|
288
|
+
baselineDim: options.baselineDim,
|
|
289
|
+
newDim: options.newDim,
|
|
190
290
|
hybridRetrieval: options.hybridRetrieval,
|
|
191
291
|
denseRetrieval: options.denseRetrieval,
|
|
192
292
|
sparseRetrieval: options.sparseRetrieval,
|
|
@@ -205,15 +305,25 @@ function buildRagPrecisionGuardrailsPlan(rawOptions = {}, templatesPath) {
|
|
|
205
305
|
},
|
|
206
306
|
signals,
|
|
207
307
|
templates,
|
|
308
|
+
matryoshka: options.matryoshkaTruncation
|
|
309
|
+
? buildMatryoshkaConfig({
|
|
310
|
+
embeddingDim: options.newDim || options.baselineDim || 768,
|
|
311
|
+
embeddingModel: options.newModel || options.baselineModel || undefined,
|
|
312
|
+
provider: options.newProvider || options.baselineProvider || undefined,
|
|
313
|
+
})
|
|
314
|
+
: null,
|
|
208
315
|
nextActions: [
|
|
209
316
|
'Save baseline recall@k, precision@k, answer-with-evidence, and latency before tuning retrieval.',
|
|
317
|
+
'Treat embedding model choice as a first-class change: re-index, re-measure, and keep a rollback provider fingerprint.',
|
|
318
|
+
'When using Matryoshka/MRL truncation, re-L2-normalize truncated vectors and re-run the golden recall suite before shipping the smaller dim.',
|
|
210
319
|
'Block embedding or threshold changes when recall drops without an approved rollback plan.',
|
|
211
320
|
'Use a second-stage verifier or reranker for structural near misses such as negation and role reversal.',
|
|
212
321
|
'Attach verifier latency budgets before routing the retrieval output into autonomous agent actions.',
|
|
213
322
|
'Measure dense recall, sparse recall, reranked relevance, source grounding, ACL filtering, and freshness as separate production gates.',
|
|
214
323
|
'Treat the retrieval layer as the agent ground truth: every autonomous action should carry source evidence and access-control proof.',
|
|
324
|
+
'Promote only structured agentic memory (lessons, rules, outcomes) — never raw transport transcripts or oversized chat dumps.',
|
|
215
325
|
],
|
|
216
|
-
exampleCommand: 'npx thumbgate rag-precision-guardrails --
|
|
326
|
+
exampleCommand: 'npx thumbgate rag-precision-guardrails --embedding-model-change --baseline-provider=nomic --new-provider=gemini --baseline-dim=768 --new-dim=256 --baseline-recall=0.95 --new-recall=0.91 --agentic --json',
|
|
217
327
|
};
|
|
218
328
|
}
|
|
219
329
|
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* remote-feedback-capture.js
|
|
6
|
+
*
|
|
7
|
+
* Hosted capture path for unattended/cloud agents when the Mac-local
|
|
8
|
+
* lessons store is missing. POSTs to THUMBGATE_API_BASE_URL/v1/feedback/capture
|
|
9
|
+
* with Bearer THUMBGATE_API_KEY.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
|
|
14
|
+
const DEFAULT_PATH = '/v1/feedback/capture';
|
|
15
|
+
|
|
16
|
+
function resolveBaseUrl(env = process.env) {
|
|
17
|
+
return String(env.THUMBGATE_API_BASE_URL || env.THUMBGATE_API_URL || '').trim().replace(/\/$/, '');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function resolveApiKey(env = process.env) {
|
|
21
|
+
return String(env.THUMBGATE_API_KEY || '').trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isRemoteCaptureConfigured(env = process.env) {
|
|
25
|
+
return Boolean(resolveBaseUrl(env) && resolveApiKey(env));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {object} params
|
|
30
|
+
* @param {string} params.signal - up|down|positive|negative
|
|
31
|
+
* @param {string} params.context
|
|
32
|
+
* @param {string} [params.whatWentWrong]
|
|
33
|
+
* @param {string} [params.whatToChange]
|
|
34
|
+
* @param {string} [params.whatWorked]
|
|
35
|
+
* @param {string[]|string} [params.tags]
|
|
36
|
+
* @param {typeof fetch} [params.fetchImpl]
|
|
37
|
+
* @param {object} [params.env]
|
|
38
|
+
*/
|
|
39
|
+
async function captureFeedbackRemote(params = {}) {
|
|
40
|
+
const env = params.env || process.env;
|
|
41
|
+
const base = resolveBaseUrl(env);
|
|
42
|
+
const key = resolveApiKey(env);
|
|
43
|
+
if (!base || !key) {
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
error: 'remote_capture_not_configured',
|
|
47
|
+
message: 'Set THUMBGATE_API_BASE_URL and THUMBGATE_API_KEY for hosted capture.',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const tags = Array.isArray(params.tags)
|
|
52
|
+
? params.tags
|
|
53
|
+
: String(params.tags || '')
|
|
54
|
+
.split(',')
|
|
55
|
+
.map((t) => t.trim())
|
|
56
|
+
.filter(Boolean);
|
|
57
|
+
|
|
58
|
+
const body = {
|
|
59
|
+
signal: params.signal || params.feedback || 'down',
|
|
60
|
+
context: params.context || '',
|
|
61
|
+
whatWentWrong: params.whatWentWrong || null,
|
|
62
|
+
whatToChange: params.whatToChange || null,
|
|
63
|
+
whatWorked: params.whatWorked || null,
|
|
64
|
+
tags,
|
|
65
|
+
source: params.source || 'remote-feedback-capture',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const fetchImpl = params.fetchImpl || globalThis.fetch;
|
|
69
|
+
if (typeof fetchImpl !== 'function') {
|
|
70
|
+
return { ok: false, error: 'fetch_unavailable', message: 'global fetch is not available' };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const url = `${base}${DEFAULT_PATH}`;
|
|
74
|
+
try {
|
|
75
|
+
const response = await fetchImpl(url, {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: {
|
|
78
|
+
authorization: `Bearer ${key}`,
|
|
79
|
+
'content-type': 'application/json',
|
|
80
|
+
accept: 'application/json',
|
|
81
|
+
},
|
|
82
|
+
body: JSON.stringify(body),
|
|
83
|
+
signal: AbortSignal.timeout(params.timeoutMs || 20000),
|
|
84
|
+
});
|
|
85
|
+
const text = await response.text();
|
|
86
|
+
let json = null;
|
|
87
|
+
try {
|
|
88
|
+
json = text ? JSON.parse(text) : null;
|
|
89
|
+
} catch {
|
|
90
|
+
json = { raw: text };
|
|
91
|
+
}
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
error: 'remote_capture_http_error',
|
|
96
|
+
status: response.status,
|
|
97
|
+
body: json,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return { ok: true, status: response.status, body: json };
|
|
101
|
+
} catch (err) {
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
error: 'remote_capture_network_error',
|
|
105
|
+
message: err && err.message ? err.message : String(err),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function parseArgs(argv) {
|
|
111
|
+
const out = {};
|
|
112
|
+
for (const arg of argv) {
|
|
113
|
+
if (!arg.startsWith('--')) continue;
|
|
114
|
+
const [k, ...rest] = arg.slice(2).split('=');
|
|
115
|
+
out[k] = rest.length ? rest.join('=') : true;
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function main(argv = process.argv.slice(2)) {
|
|
121
|
+
const args = parseArgs(argv);
|
|
122
|
+
if (args.help) {
|
|
123
|
+
console.log('Usage: node scripts/remote-feedback-capture.js --feedback=down --context="..." [--what-went-wrong=...] [--tags=a,b]');
|
|
124
|
+
process.exit(0);
|
|
125
|
+
}
|
|
126
|
+
const result = await captureFeedbackRemote({
|
|
127
|
+
signal: args.feedback || args.signal || 'down',
|
|
128
|
+
context: args.context || '',
|
|
129
|
+
whatWentWrong: args['what-went-wrong'],
|
|
130
|
+
whatToChange: args['what-to-change'],
|
|
131
|
+
whatWorked: args['what-worked'],
|
|
132
|
+
tags: args.tags,
|
|
133
|
+
});
|
|
134
|
+
console.log(JSON.stringify(result, null, 2));
|
|
135
|
+
process.exit(result.ok ? 0 : 2);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (pathResolveMain()) {
|
|
139
|
+
main().catch((err) => {
|
|
140
|
+
console.error(err);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function pathResolveMain() {
|
|
146
|
+
try {
|
|
147
|
+
return require('node:path').resolve(process.argv[1] || '') === require('node:path').resolve(__filename);
|
|
148
|
+
} catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = {
|
|
154
|
+
captureFeedbackRemote,
|
|
155
|
+
isRemoteCaptureConfigured,
|
|
156
|
+
parseArgs,
|
|
157
|
+
resolveBaseUrl,
|
|
158
|
+
resolveApiKey,
|
|
159
|
+
};
|