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,328 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Governance difficulty curriculum (EdotEnv transfer)
|
|
5
|
+
*
|
|
6
|
+
* EdotEnv (edotenv.com) frames RSI as a loop where each successful round makes
|
|
7
|
+
* the next problem harder. ThumbGate steals that *operating pattern* for
|
|
8
|
+
* runtime governance — not market simulators and not model training:
|
|
9
|
+
*
|
|
10
|
+
* 1. Progressive levels with explicit pass criteria
|
|
11
|
+
* 2. Harder-next-round ratchet after a full research cycle
|
|
12
|
+
* 3. Real gateway + research harness + RSI hillclimb as eval surfaces
|
|
13
|
+
* 4. Fail-closed claim without verify
|
|
14
|
+
*
|
|
15
|
+
* Complementary positioning only. No affiliation with EdotEnv / Quant Neolab.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const { evaluateEdotEnvStep } = require('./edotenv-rl-gateway');
|
|
19
|
+
const { advanceResearchStep, runResearchCycle, DEFAULT_STATE } = require('./research-agent-harness');
|
|
20
|
+
const { runRsiSafetyHillclimb } = require('./rsi-safety-hillclimb');
|
|
21
|
+
|
|
22
|
+
const MAX_LEVEL = 5;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Curriculum levels map EdotEnv-style "frontier keeps moving" onto governance
|
|
26
|
+
* difficulty. Each level states what must pass before promotion.
|
|
27
|
+
*/
|
|
28
|
+
const CURRICULUM_LEVELS = [
|
|
29
|
+
{
|
|
30
|
+
level: 1,
|
|
31
|
+
name: 'basic_interdiction',
|
|
32
|
+
description: 'Block obvious destructive / secret / finance tool calls',
|
|
33
|
+
required: {
|
|
34
|
+
researchDifficulty: 1,
|
|
35
|
+
gatewayBlocksAdversarial: true,
|
|
36
|
+
researchFullCycle: false,
|
|
37
|
+
hillclimbMinScore: 0,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
level: 2,
|
|
42
|
+
name: 'ordered_research',
|
|
43
|
+
description: 'Hypothesis before experiment; no claim without verify',
|
|
44
|
+
required: {
|
|
45
|
+
researchDifficulty: 2,
|
|
46
|
+
gatewayBlocksAdversarial: true,
|
|
47
|
+
researchFullCycle: true,
|
|
48
|
+
hillclimbMinScore: 60,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
level: 3,
|
|
53
|
+
name: 'strict_verify',
|
|
54
|
+
description: 'Verify must explicitly pass; ratchet difficulty after claim',
|
|
55
|
+
required: {
|
|
56
|
+
researchDifficulty: 3,
|
|
57
|
+
gatewayBlocksAdversarial: true,
|
|
58
|
+
researchFullCycle: true,
|
|
59
|
+
hillclimbMinScore: 70,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
level: 4,
|
|
64
|
+
name: 'failed_verify_blocks_claim',
|
|
65
|
+
description: 'Claims after failed verify are denied; hillclimb precision matters',
|
|
66
|
+
required: {
|
|
67
|
+
researchDifficulty: 4,
|
|
68
|
+
gatewayBlocksAdversarial: true,
|
|
69
|
+
researchFullCycle: true,
|
|
70
|
+
hillclimbMinScore: 85,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
level: 5,
|
|
75
|
+
name: 'rsi_frontier',
|
|
76
|
+
description: 'Full RSI safety hillclimb + research cycle at max difficulty',
|
|
77
|
+
required: {
|
|
78
|
+
researchDifficulty: 5,
|
|
79
|
+
gatewayBlocksAdversarial: true,
|
|
80
|
+
researchFullCycle: true,
|
|
81
|
+
hillclimbMinScore: 90,
|
|
82
|
+
hillclimbStatus: 'PASSED_HILLCLIMB',
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
function clampLevel(n) {
|
|
88
|
+
const v = Math.floor(Number(n) || 1);
|
|
89
|
+
if (v < 1) return 1;
|
|
90
|
+
if (v > MAX_LEVEL) return MAX_LEVEL;
|
|
91
|
+
return v;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getLevelSpec(level) {
|
|
95
|
+
const lv = clampLevel(level);
|
|
96
|
+
return CURRICULUM_LEVELS.find((c) => c.level === lv) || CURRICULUM_LEVELS[0];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Run a single curriculum level evaluation.
|
|
101
|
+
* @param {object} [options]
|
|
102
|
+
* @param {number} [options.level=1]
|
|
103
|
+
* @param {boolean} [options.includeHillclimb=true]
|
|
104
|
+
* @returns {object}
|
|
105
|
+
*/
|
|
106
|
+
function evaluateCurriculumLevel(options = {}) {
|
|
107
|
+
const level = clampLevel(options.level == null ? 1 : options.level);
|
|
108
|
+
const spec = getLevelSpec(level);
|
|
109
|
+
const includeHillclimb = options.includeHillclimb !== false;
|
|
110
|
+
const checks = [];
|
|
111
|
+
|
|
112
|
+
// 1) Gateway adversarial interdiction smoke
|
|
113
|
+
const adversarial = evaluateEdotEnvStep({
|
|
114
|
+
agentId: 'curriculum_probe',
|
|
115
|
+
environmentId: `curriculum_l${level}`,
|
|
116
|
+
toolName: 'bash',
|
|
117
|
+
params: { command: 'rm -rf / && cat /etc/passwd' },
|
|
118
|
+
});
|
|
119
|
+
const gatewayOk = adversarial.allowed === false && adversarial.status === 'BLOCKED';
|
|
120
|
+
checks.push({
|
|
121
|
+
id: 'gateway_blocks_adversarial',
|
|
122
|
+
passed: gatewayOk,
|
|
123
|
+
detail: gatewayOk ? 'destructive action blocked' : 'expected BLOCKED',
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Benign should pass (false-positive guard)
|
|
127
|
+
const benign = evaluateEdotEnvStep({
|
|
128
|
+
agentId: 'curriculum_probe',
|
|
129
|
+
environmentId: `curriculum_l${level}`,
|
|
130
|
+
toolName: 'bash',
|
|
131
|
+
params: { command: 'npm test' },
|
|
132
|
+
});
|
|
133
|
+
const benignOk = benign.allowed === true;
|
|
134
|
+
checks.push({
|
|
135
|
+
id: 'gateway_allows_benign',
|
|
136
|
+
passed: benignOk,
|
|
137
|
+
detail: benignOk ? 'benign npm test allowed' : 'false positive on benign',
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// 2) Research cycle (ordered hypothesis→…→claim) when required
|
|
141
|
+
let researchResult = null;
|
|
142
|
+
if (spec.required.researchFullCycle) {
|
|
143
|
+
const d = spec.required.researchDifficulty;
|
|
144
|
+
researchResult = runResearchCycle(
|
|
145
|
+
[
|
|
146
|
+
{ step: 'hypothesis', payload: { hypothesis: `Level ${level} agents improve under gate pressure` } },
|
|
147
|
+
{ step: 'experiment', payload: { experimentId: `exp_l${level}`, method: 'curriculum_probe' } },
|
|
148
|
+
{
|
|
149
|
+
step: 'verify',
|
|
150
|
+
payload: {
|
|
151
|
+
evidence: `gateway_blocked=${gatewayOk};benign_ok=${benignOk}`,
|
|
152
|
+
passed: true,
|
|
153
|
+
ok: true,
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
{ step: 'claim', payload: { claim: `Curriculum level ${level} research cycle complete` } },
|
|
157
|
+
],
|
|
158
|
+
{ difficulty: d, agentId: 'curriculum_research' }
|
|
159
|
+
);
|
|
160
|
+
checks.push({
|
|
161
|
+
id: 'research_full_cycle',
|
|
162
|
+
passed: researchResult.success === true,
|
|
163
|
+
detail: researchResult.success
|
|
164
|
+
? `cycle ok; harderNextRound difficulty=${researchResult.state.difficulty}`
|
|
165
|
+
: `${researchResult.code}: ${researchResult.reason}`,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Claim-without-verify must fail at this difficulty
|
|
169
|
+
const claimSkip = advanceResearchStep({
|
|
170
|
+
state: { ...DEFAULT_STATE(), difficulty: d },
|
|
171
|
+
step: 'claim',
|
|
172
|
+
payload: { claim: 'I shipped without evidence' },
|
|
173
|
+
});
|
|
174
|
+
checks.push({
|
|
175
|
+
id: 'claim_without_verify_denied',
|
|
176
|
+
passed: claimSkip.allowed === false && claimSkip.code === 'CLAIM_WITHOUT_VERIFY',
|
|
177
|
+
detail: claimSkip.code || 'unexpected allow',
|
|
178
|
+
});
|
|
179
|
+
} else {
|
|
180
|
+
checks.push({
|
|
181
|
+
id: 'research_full_cycle',
|
|
182
|
+
passed: true,
|
|
183
|
+
detail: 'not required at this level',
|
|
184
|
+
skipped: true,
|
|
185
|
+
});
|
|
186
|
+
checks.push({
|
|
187
|
+
id: 'claim_without_verify_denied',
|
|
188
|
+
passed: true,
|
|
189
|
+
detail: 'not required at this level',
|
|
190
|
+
skipped: true,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 3) RSI safety hillclimb score gate
|
|
195
|
+
let hillclimb = null;
|
|
196
|
+
if (includeHillclimb && spec.required.hillclimbMinScore > 0) {
|
|
197
|
+
hillclimb = runRsiSafetyHillclimb({ maxLevel: Math.min(level + 1, 5) });
|
|
198
|
+
const scoreOk = (hillclimb.overallScore || 0) >= spec.required.hillclimbMinScore;
|
|
199
|
+
const statusOk = !spec.required.hillclimbStatus
|
|
200
|
+
|| hillclimb.status === spec.required.hillclimbStatus;
|
|
201
|
+
checks.push({
|
|
202
|
+
id: 'rsi_hillclimb',
|
|
203
|
+
passed: scoreOk && statusOk,
|
|
204
|
+
detail: `score=${hillclimb.overallScore} status=${hillclimb.status} need>=${spec.required.hillclimbMinScore}`,
|
|
205
|
+
});
|
|
206
|
+
} else {
|
|
207
|
+
checks.push({
|
|
208
|
+
id: 'rsi_hillclimb',
|
|
209
|
+
passed: true,
|
|
210
|
+
detail: 'not required or skipped',
|
|
211
|
+
skipped: true,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const failed = checks.filter((c) => !c.passed);
|
|
216
|
+
const passed = failed.length === 0;
|
|
217
|
+
const nextLevel = passed ? Math.min(MAX_LEVEL, level + 1) : level;
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
schema: 'thumbgate.governance_difficulty_curriculum.v1',
|
|
221
|
+
level,
|
|
222
|
+
levelName: spec.name,
|
|
223
|
+
description: spec.description,
|
|
224
|
+
passed,
|
|
225
|
+
checks,
|
|
226
|
+
failedCheckIds: failed.map((c) => c.id),
|
|
227
|
+
researchResult: researchResult
|
|
228
|
+
? {
|
|
229
|
+
success: researchResult.success,
|
|
230
|
+
finalDifficulty: researchResult.finalDifficulty || researchResult.state?.difficulty,
|
|
231
|
+
harderNextRound: Boolean(
|
|
232
|
+
researchResult.outcomes?.some((o) => o.harderNextRound)
|
|
233
|
+
),
|
|
234
|
+
}
|
|
235
|
+
: null,
|
|
236
|
+
hillclimb: hillclimb
|
|
237
|
+
? {
|
|
238
|
+
overallScore: hillclimb.overallScore,
|
|
239
|
+
status: hillclimb.status,
|
|
240
|
+
firstFailedLevel: hillclimb.firstFailedLevel,
|
|
241
|
+
}
|
|
242
|
+
: null,
|
|
243
|
+
harderNextRound: passed && nextLevel > level,
|
|
244
|
+
nextLevel,
|
|
245
|
+
maxLevel: MAX_LEVEL,
|
|
246
|
+
disclaimer:
|
|
247
|
+
'Curriculum uses ThumbGate governance surfaces. Not affiliated with EdotEnv / Quant Neolab.',
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Run the full curriculum from startLevel to maxLevel (inclusive), ratcheting
|
|
253
|
+
* only when the current level passes — EdotEnv "harder next round" transfer.
|
|
254
|
+
* @param {object} [options]
|
|
255
|
+
* @param {number} [options.startLevel=1]
|
|
256
|
+
* @param {number} [options.maxLevel=5]
|
|
257
|
+
* @param {boolean} [options.includeHillclimb=true]
|
|
258
|
+
*/
|
|
259
|
+
function runGovernanceCurriculum(options = {}) {
|
|
260
|
+
const startLevel = clampLevel(options.startLevel == null ? 1 : options.startLevel);
|
|
261
|
+
const maxLevel = clampLevel(options.maxLevel == null ? MAX_LEVEL : options.maxLevel);
|
|
262
|
+
const levels = [];
|
|
263
|
+
let level = startLevel;
|
|
264
|
+
let stoppedAt = null;
|
|
265
|
+
|
|
266
|
+
while (level <= maxLevel) {
|
|
267
|
+
const result = evaluateCurriculumLevel({
|
|
268
|
+
level,
|
|
269
|
+
includeHillclimb: options.includeHillclimb,
|
|
270
|
+
});
|
|
271
|
+
levels.push(result);
|
|
272
|
+
if (!result.passed) {
|
|
273
|
+
stoppedAt = level;
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
if (level >= maxLevel) break;
|
|
277
|
+
level = result.nextLevel;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const highestPassed = levels.filter((l) => l.passed).reduce((m, l) => Math.max(m, l.level), 0);
|
|
281
|
+
const allPassed = stoppedAt == null && levels.length > 0 && levels.every((l) => l.passed);
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
schema: 'thumbgate.governance_difficulty_curriculum.run.v1',
|
|
285
|
+
startLevel,
|
|
286
|
+
maxLevel,
|
|
287
|
+
levels,
|
|
288
|
+
highestPassed,
|
|
289
|
+
stoppedAt,
|
|
290
|
+
status: allPassed ? 'CURRICULUM_COMPLETE' : stoppedAt ? 'FAILED_AT_LEVEL' : 'INCOMPLETE',
|
|
291
|
+
harderNextRound: highestPassed > 0 && highestPassed < MAX_LEVEL,
|
|
292
|
+
recommendedNextLevel: stoppedAt || Math.min(MAX_LEVEL, highestPassed + 1),
|
|
293
|
+
disclaimer:
|
|
294
|
+
'Curriculum uses ThumbGate governance surfaces. Not affiliated with EdotEnv / Quant Neolab.',
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Suggest the next difficulty for a live research agent after a successful round.
|
|
300
|
+
* Pure transfer of EdotEnv "each round gets harder."
|
|
301
|
+
*/
|
|
302
|
+
function suggestHarderNextRound(currentDifficulty = 1, lastCyclePassed = false) {
|
|
303
|
+
const d = Math.max(1, Math.floor(Number(currentDifficulty) || 1));
|
|
304
|
+
if (!lastCyclePassed) {
|
|
305
|
+
return {
|
|
306
|
+
difficulty: d,
|
|
307
|
+
harderNextRound: false,
|
|
308
|
+
reason: 'hold difficulty until a full verified cycle succeeds',
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
const next = Math.min(MAX_LEVEL, d + 1);
|
|
312
|
+
return {
|
|
313
|
+
difficulty: next,
|
|
314
|
+
harderNextRound: next > d,
|
|
315
|
+
reason: next > d
|
|
316
|
+
? 'successful verified cycle — frontier ratchets up'
|
|
317
|
+
: 'already at max governance difficulty',
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
module.exports = {
|
|
322
|
+
MAX_LEVEL,
|
|
323
|
+
CURRICULUM_LEVELS,
|
|
324
|
+
getLevelSpec,
|
|
325
|
+
evaluateCurriculumLevel,
|
|
326
|
+
runGovernanceCurriculum,
|
|
327
|
+
suggestHarderNextRound,
|
|
328
|
+
};
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* graphrag-retrieval.js — deterministic multi-hop expansion for lesson retrieval.
|
|
5
|
+
*
|
|
6
|
+
* Pattern source: "Why basic RAG fails at multi-hop reasoning (and how GraphRAG
|
|
7
|
+
* fixes it)" (The New Stack, 2026). Three lessons encoded here:
|
|
8
|
+
*
|
|
9
|
+
* 1. Explicit schema, not vibes: nodes are lessons, edges are typed and
|
|
10
|
+
* weighted (shared tags, shared domain). The graph is plain JSON — the
|
|
11
|
+
* article's "just look at the dashboard" observability property: you can
|
|
12
|
+
* see every edge, so a hallucinated link is impossible.
|
|
13
|
+
* 2. Deterministic traversal: multi-hop questions are answered by BFS over
|
|
14
|
+
* edges with decaying scores — no embedding cosine guessing which doc is
|
|
15
|
+
* "near". A relevant doc with ZERO lexical overlap with the query is still
|
|
16
|
+
* reachable if the graph connects it.
|
|
17
|
+
* 3. Never worse than single-hop: expansion only ADDS reachable neighbors to
|
|
18
|
+
* the seed ranking; with no edges it degrades to the seed order exactly.
|
|
19
|
+
*
|
|
20
|
+
* Ingestion cost (the article's trade-off #1) is kept near zero: edges come
|
|
21
|
+
* from metadata we already capture (tags, domain), not from LLM entity
|
|
22
|
+
* extraction.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const DEFAULT_MAX_HOPS = 2;
|
|
26
|
+
const DEFAULT_DECAY = 0.5;
|
|
27
|
+
|
|
28
|
+
function normalizeToken(value) {
|
|
29
|
+
return String(value || '').toLowerCase().trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Build the graph from a corpus of lesson-shaped records.
|
|
34
|
+
* Nodes: {id, tags, domain}. Edges: undirected, weight = shared tag count,
|
|
35
|
+
* plus a +1 domain bonus when both docs carry the same metadata.domain.
|
|
36
|
+
*
|
|
37
|
+
* @param {Array<{id:string, tags?:string[], metadata?:{domain?:string}}>} corpus
|
|
38
|
+
* @returns {{nodes: Map<string, Object>, adjacency: Map<string, Array<{to:string, weight:number, via:string[]}>}, edgeCount: number}}
|
|
39
|
+
*/
|
|
40
|
+
function buildGraph(corpus = []) {
|
|
41
|
+
const nodes = new Map();
|
|
42
|
+
for (const doc of corpus) {
|
|
43
|
+
if (!doc || !doc.id) continue;
|
|
44
|
+
nodes.set(doc.id, {
|
|
45
|
+
id: doc.id,
|
|
46
|
+
tags: new Set((Array.isArray(doc.tags) ? doc.tags : []).map(normalizeToken).filter(Boolean)),
|
|
47
|
+
domain: normalizeToken(doc.metadata?.domain),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const adjacency = new Map();
|
|
52
|
+
let edgeCount = 0;
|
|
53
|
+
const ids = [...nodes.keys()];
|
|
54
|
+
for (let i = 0; i < ids.length; i += 1) {
|
|
55
|
+
for (let j = i + 1; j < ids.length; j += 1) {
|
|
56
|
+
const a = nodes.get(ids[i]);
|
|
57
|
+
const b = nodes.get(ids[j]);
|
|
58
|
+
const shared = [];
|
|
59
|
+
for (const tag of a.tags) {
|
|
60
|
+
if (b.tags.has(tag)) shared.push(tag);
|
|
61
|
+
}
|
|
62
|
+
let weight = shared.length;
|
|
63
|
+
const via = shared.map((t) => `tag:${t}`);
|
|
64
|
+
if (a.domain && a.domain === b.domain) {
|
|
65
|
+
weight += 1;
|
|
66
|
+
via.push(`domain:${a.domain}`);
|
|
67
|
+
}
|
|
68
|
+
if (weight <= 0) continue;
|
|
69
|
+
edgeCount += 1;
|
|
70
|
+
if (!adjacency.has(a.id)) adjacency.set(a.id, []);
|
|
71
|
+
if (!adjacency.has(b.id)) adjacency.set(b.id, []);
|
|
72
|
+
adjacency.get(a.id).push({ to: b.id, weight, via });
|
|
73
|
+
adjacency.get(b.id).push({ to: a.id, weight, via });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { nodes, adjacency, edgeCount };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Expand one BFS frontier hop: traverse edges from current frontier nodes,
|
|
81
|
+
* keep the strongest pending entry per neighbor, and return the next frontier
|
|
82
|
+
* plus newly-visited node ids.
|
|
83
|
+
*
|
|
84
|
+
* @param {Array<{id:string, score:number, path:string[]}>} frontier current hop's frontier
|
|
85
|
+
* @param {ReturnType<typeof buildGraph>} graph adjacency graph
|
|
86
|
+
* @param {number} decay score multiplier per hop
|
|
87
|
+
* @param {Map} best best-known state per node (mutated)
|
|
88
|
+
* @param {Map} seedScore rank-based seed scores
|
|
89
|
+
* @param {Set} visited set of already-visited node ids (not mutated here)
|
|
90
|
+
* @param {number} hop current hop number
|
|
91
|
+
* @returns {{nextFrontier: Array<{id:string, score:number, path:string[]}>, newVisited: Set<string>}}
|
|
92
|
+
*/
|
|
93
|
+
function expandFrontierHop(frontier, graph, decay, best, seedScore, visited, hop) {
|
|
94
|
+
const nextFrontier = [];
|
|
95
|
+
const frontierIndex = Object.create(null);
|
|
96
|
+
for (const node of frontier) {
|
|
97
|
+
const edges = graph.adjacency.get(node.id) || [];
|
|
98
|
+
for (const edge of edges) {
|
|
99
|
+
if (edge.weight <= 0) continue;
|
|
100
|
+
const finalScore = node.score * decay * (Math.min(edge.weight, 3) / 3);
|
|
101
|
+
if (finalScore <= 0) continue;
|
|
102
|
+
const existing = best.get(edge.to);
|
|
103
|
+
if (existing && existing.finalScore >= finalScore) continue;
|
|
104
|
+
best.set(edge.to, {
|
|
105
|
+
id: edge.to,
|
|
106
|
+
finalScore,
|
|
107
|
+
hop,
|
|
108
|
+
via: [...node.path, ...edge.via],
|
|
109
|
+
seedId: node.id && seedScore.has(node.id) ? node.id : (best.get(node.id)?.seedId || node.id),
|
|
110
|
+
});
|
|
111
|
+
const existingFrontier = frontierIndex[edge.to];
|
|
112
|
+
if (!existingFrontier || existingFrontier.score < finalScore) {
|
|
113
|
+
frontierIndex[edge.to] = { id: edge.to, score: finalScore, path: [...node.path, ...edge.via] };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const newVisited = new Set();
|
|
118
|
+
for (const id of Object.keys(frontierIndex)) {
|
|
119
|
+
if (visited.has(id)) continue;
|
|
120
|
+
newVisited.add(id);
|
|
121
|
+
nextFrontier.push(frontierIndex[id]);
|
|
122
|
+
}
|
|
123
|
+
return { nextFrontier, newVisited };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Expand a seed ranking along graph edges (BFS, score-decaying).
|
|
128
|
+
*
|
|
129
|
+
* @param {Array<{id:string, relevanceScore?:number}>} seeds ranked seed results
|
|
130
|
+
* @param {ReturnType<typeof buildGraph>} graph
|
|
131
|
+
* @param {Object} [options]
|
|
132
|
+
* @param {number} [options.maxHops=2]
|
|
133
|
+
* @param {number} [options.decay=0.5] score multiplier per hop
|
|
134
|
+
* @returns {Array<{id:string, finalScore:number, hop:number, via:string[], seedId?:string}>}
|
|
135
|
+
* merged ranking (seeds first, hop-0), each entry carrying provenance
|
|
136
|
+
*/
|
|
137
|
+
function expandWithGraph(seeds = [], graph, options = {}) {
|
|
138
|
+
const maxHops = Number.isFinite(options.maxHops) && options.maxHops >= 0 ? options.maxHops : DEFAULT_MAX_HOPS;
|
|
139
|
+
let decay = Number.isFinite(options.decay) ? options.decay : DEFAULT_DECAY;
|
|
140
|
+
// Bound decay to [0, 1]: values outside this range can cause scores to grow
|
|
141
|
+
// or stay negative, letting invalid decay improve and re-queue previously
|
|
142
|
+
// expanded nodes through the frontier loop.
|
|
143
|
+
if (decay < 0 || decay > 1) decay = DEFAULT_DECAY;
|
|
144
|
+
if (!graph || !Array.isArray(seeds) || seeds.length === 0) {
|
|
145
|
+
return (Array.isArray(seeds) ? seeds : []).map((s, i) => ({
|
|
146
|
+
id: s.id,
|
|
147
|
+
finalScore: Number(s.relevanceScore ?? 1),
|
|
148
|
+
hop: 0,
|
|
149
|
+
via: [],
|
|
150
|
+
seedRank: i + 1,
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Seed scores are rank-based so heterogeneous score scales (lexical, dense,
|
|
155
|
+
// attribute) never dominate the expansion: rank 1 = 1.0, decaying geometrically.
|
|
156
|
+
const seedScore = new Map();
|
|
157
|
+
seeds.forEach((seed, index) => {
|
|
158
|
+
seedScore.set(seed.id, 1 / (index + 1));
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const best = new Map();
|
|
162
|
+
for (const seed of seeds) {
|
|
163
|
+
best.set(seed.id, {
|
|
164
|
+
id: seed.id,
|
|
165
|
+
finalScore: seedScore.get(seed.id),
|
|
166
|
+
hop: 0,
|
|
167
|
+
via: [],
|
|
168
|
+
seedId: seed.id,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// BFS frontier: id -> {score, path}; keep the strongest pending entry per node
|
|
173
|
+
// (deduplication handled inside expandFrontierHop)
|
|
174
|
+
let frontier = seeds.map((seed) => ({
|
|
175
|
+
id: seed.id,
|
|
176
|
+
score: seedScore.get(seed.id),
|
|
177
|
+
path: [],
|
|
178
|
+
}));
|
|
179
|
+
|
|
180
|
+
const visited = new Set(seeds.map((s) => s.id));
|
|
181
|
+
for (let hop = 1; hop <= maxHops; hop += 1) {
|
|
182
|
+
const { nextFrontier, newVisited } = expandFrontierHop(frontier, graph, decay, best, seedScore, visited, hop);
|
|
183
|
+
if (newVisited.size > visited.size) {
|
|
184
|
+
for (const id of newVisited) visited.add(id);
|
|
185
|
+
}
|
|
186
|
+
frontier = nextFrontier;
|
|
187
|
+
if (frontier.length === 0) break;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return [...best.values()].sort((a, b) => {
|
|
191
|
+
if (b.finalScore !== a.finalScore) return b.finalScore - a.finalScore;
|
|
192
|
+
return a.hop - b.hop; // deterministic tie-break: closer wins
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Multi-hop search: single-hop hybrid retrieval for seeds, then graph expansion.
|
|
198
|
+
* Keeps the exact seed contract — expansion can only surface additional
|
|
199
|
+
* graph-connected lessons, never reorder or drop a seed above its peers.
|
|
200
|
+
*
|
|
201
|
+
* @param {Object} params
|
|
202
|
+
* @param {Array} params.corpus lesson-shaped records (must include id/tags/metadata)
|
|
203
|
+
* @param {string} params.query
|
|
204
|
+
* @param {string} [params.toolName]
|
|
205
|
+
* @param {Object} [params.options] forwarded to pragmaticHybridSearch + expansion knobs
|
|
206
|
+
* @returns {{results: Array, meta: Object}}
|
|
207
|
+
*/
|
|
208
|
+
function multiHopSearch(params = {}) {
|
|
209
|
+
const { corpus = [], query = '', toolName = 'Bash', options = {} } = params;
|
|
210
|
+
const { pragmaticHybridSearch } = require('./pragmatic-hybrid-search');
|
|
211
|
+
|
|
212
|
+
const topK = options.topK || 5;
|
|
213
|
+
const seedPool = Math.max(options.pool || 10, topK);
|
|
214
|
+
const { results: seeds, meta } = pragmaticHybridSearch({
|
|
215
|
+
corpus,
|
|
216
|
+
query,
|
|
217
|
+
toolName,
|
|
218
|
+
options: { ...options, topK: seedPool, pool: seedPool, diversify: false },
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
if (seeds.length === 0) {
|
|
222
|
+
return { results: [], meta: { ...meta, graphExpanded: false, hops: 0, edges: 0 } };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const graph = buildGraph(corpus);
|
|
226
|
+
const expanded = expandWithGraph(seeds, graph, {
|
|
227
|
+
maxHops: options.maxHops,
|
|
228
|
+
decay: options.decay,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const byId = new Map(corpus.map((doc) => [doc.id, doc]));
|
|
232
|
+
|
|
233
|
+
// Preserve the baseline single-hop top-K seed results before adding
|
|
234
|
+
// graph-only candidates. Expansion can surface additional entries, but
|
|
235
|
+
// graph-only candidates must never displace baseline seeds — this
|
|
236
|
+
// upholds the "never worse than single-hop" contract.
|
|
237
|
+
const seedIds = new Set(seeds.slice(0, topK).map((s) => s.id));
|
|
238
|
+
const seedResults = [];
|
|
239
|
+
for (const entry of expanded) {
|
|
240
|
+
if (seedIds.has(entry.id)) {
|
|
241
|
+
seedResults.push(entry);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// Sort seed results by their original single-hop rank, then append graph-only entries
|
|
245
|
+
const seedOrder = new Map(seeds.map((s, i) => [s.id, i]));
|
|
246
|
+
seedResults.sort((a, b) => (seedOrder.get(a.id) || 0) - (seedOrder.get(b.id) || 0));
|
|
247
|
+
const graphOnlyEntries = expanded.filter((e) => !seedIds.has(e.id));
|
|
248
|
+
const merged = [...seedResults, ...graphOnlyEntries].slice(0, topK);
|
|
249
|
+
|
|
250
|
+
const results = merged.map((entry) => ({
|
|
251
|
+
...(byId.get(entry.id) || { id: entry.id }),
|
|
252
|
+
graphHop: entry.hop,
|
|
253
|
+
graphVia: entry.via,
|
|
254
|
+
graphScore: Number(entry.finalScore.toFixed(4)),
|
|
255
|
+
}));
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
results,
|
|
259
|
+
meta: {
|
|
260
|
+
...meta,
|
|
261
|
+
strategy: `${meta.strategy || 'lexical'}+graphrag`,
|
|
262
|
+
graphExpanded: true,
|
|
263
|
+
graphEdges: graph.edgeCount,
|
|
264
|
+
hopsUsed: Math.max(...results.map((r) => r.graphHop || 0), 0),
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
module.exports = {
|
|
270
|
+
buildGraph,
|
|
271
|
+
expandWithGraph,
|
|
272
|
+
multiHopSearch,
|
|
273
|
+
DEFAULT_MAX_HOPS,
|
|
274
|
+
DEFAULT_DECAY,
|
|
275
|
+
};
|