thumbgate 1.35.0 → 1.37.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/.agents/skills/cyberstrike-compare-not-clone/SKILL.md +36 -0
  2. package/.agents/skills/gitlab-sandbox-allowlist-not-trust/SKILL.md +77 -0
  3. package/.agents/skills/jit-harness-compare-not-clone/SKILL.md +34 -0
  4. package/.agents/skills/openui-catalog-compose-honesty/SKILL.md +64 -0
  5. package/.agents/skills/zvec-grep-compare-not-clone/SKILL.md +34 -0
  6. package/.claude-plugin/plugin.json +1 -1
  7. package/.well-known/llms.txt +1 -0
  8. package/.well-known/mcp/server-card.json +1 -1
  9. package/CONTRIBUTING.md +95 -0
  10. package/README.md +195 -632
  11. package/THIRD_PARTY_NOTICES.md +89 -0
  12. package/adapters/claude/.mcp.json +2 -2
  13. package/adapters/forge/forge.yaml +3 -3
  14. package/adapters/future-agi/.mcp.json +8 -0
  15. package/adapters/future-agi/FUTURE_AGI.md +23 -0
  16. package/adapters/future-agi/config.toml +3 -0
  17. package/adapters/future-agi/future-agi-bridge.js +9 -0
  18. package/adapters/future-agi/opencode.json +8 -0
  19. package/adapters/herdr/herdr-plugin.toml +18 -0
  20. package/adapters/mcp/server-stdio.js +238 -25
  21. package/adapters/opencode/opencode.json +1 -1
  22. package/adapters/workos/WORKOS.md +52 -0
  23. package/bin/cli.js +336 -3
  24. package/bin/futureagi-bridge +9 -0
  25. package/config/gate-templates.json +581 -4
  26. package/config/gates/actor-critic-audit.json +34 -0
  27. package/config/gates/default.json +9 -3
  28. package/config/gates/five-walls-governance.json +34 -0
  29. package/config/gates/future-agi-guardrails.json +34 -0
  30. package/config/gates/radware-threat-defense-2026.json +61 -0
  31. package/config/gates/simatree-data-governance.json +33 -0
  32. package/config/mcp-allowlists.json +4 -0
  33. package/config/merge-quality-checks.json +6 -0
  34. package/config/model-candidates.json +312 -29
  35. package/config/model-tiers.json +18 -0
  36. package/config/post-deploy-marketing-pages.json +10 -0
  37. package/config/progressive/01-wire-only.json +11 -0
  38. package/config/progressive/02-dashboard-empty-ok.json +10 -0
  39. package/config/progressive/03-one-lesson.json +10 -0
  40. package/config/progressive/04-warn-fires.json +11 -0
  41. package/config/progressive/05-strict-optional.json +11 -0
  42. package/config/progressive/README.md +15 -0
  43. package/config/schemas/broker-execution-receipt.schema.json +139 -0
  44. package/config/schemas/provider-execution-attestation-v1.schema.json +58 -0
  45. package/conformance/provider-attestation/vectors.json +320 -0
  46. package/docs/specs/provider-execution-attestation-v1.md +69 -0
  47. package/openapi/openapi.yaml +15 -0
  48. package/package.json +390 -148
  49. package/public/about.html +2 -2
  50. package/public/ai-malpractice-prevention.html +7 -7
  51. package/public/blog/a-10-dollar-vps-is-not-a-computer.html +143 -0
  52. package/public/blog/a-receipt-is-not-world-state.html +388 -0
  53. package/public/blog/git-at-agent-scale.html +374 -0
  54. package/public/blog/no-llm-in-the-gate.html +133 -0
  55. package/public/blog.html +80 -0
  56. package/public/case-studies.html +16 -1
  57. package/public/compare.html +28 -0
  58. package/public/diagnostic.html +216 -7
  59. package/public/docs/connectors.html +39 -0
  60. package/public/federal.html +2 -2
  61. package/public/founders.html +639 -0
  62. package/public/index.html +87 -9
  63. package/public/install.html +8 -8
  64. package/public/learn.html +39 -0
  65. package/public/numbers.html +2 -2
  66. package/public/peter.html +310 -0
  67. package/public/platform-partners.html +119 -0
  68. package/public/pricing.html +24 -3
  69. package/public/privacy.html +117 -0
  70. package/public/pro.html +17 -0
  71. package/public/support.html +62 -0
  72. package/public/terms.html +130 -0
  73. package/public/third-party-notices.html +95 -0
  74. package/public/yt.html +351 -0
  75. package/scripts/action-receipts.js +133 -3
  76. package/scripts/adaptive-governance-arena.js +349 -0
  77. package/scripts/admin-override.js +205 -0
  78. package/scripts/agent-action-inventory.js +869 -0
  79. package/scripts/agent-audit-trace.js +42 -2
  80. package/scripts/agent-egress-policy.js +1117 -0
  81. package/scripts/agent-memory-lifecycle.js +141 -2
  82. package/scripts/agent-operations-planner.js +441 -1
  83. package/scripts/agent-readiness.js +68 -0
  84. package/scripts/agent-security-central.js +647 -0
  85. package/scripts/allowlist-bridge-honesty.js +417 -0
  86. package/scripts/async-job-runner.js +102 -11
  87. package/scripts/audit-trail.js +212 -0
  88. package/scripts/billing.js +1 -1
  89. package/scripts/broker-execution-receipts.js +719 -0
  90. package/scripts/budget-aware-gates-proof.js +423 -0
  91. package/scripts/claude-feedback-sync.js +29 -3
  92. package/scripts/claw-harness-production.js +237 -0
  93. package/scripts/cli-schema.js +163 -1
  94. package/scripts/codex-runbook-flywheel.js +318 -0
  95. package/scripts/context-footprint.js +186 -0
  96. package/scripts/contextfs.js +143 -61
  97. package/scripts/dashboard.js +251 -32
  98. package/scripts/deepseek-v4-runtime-guardrails.js +72 -6
  99. package/scripts/docker-sandbox-planner.js +18 -0
  100. package/scripts/double-blind-eval-protocol.js +252 -0
  101. package/scripts/edotenv-rl-gateway.js +259 -0
  102. package/scripts/ensure-production-search-corpus.js +162 -0
  103. package/scripts/eval-holdout.js +311 -0
  104. package/scripts/feedback-aggregate.js +21 -2
  105. package/scripts/feedback-loop.js +87 -5
  106. package/scripts/feedback-quality.js +9 -0
  107. package/scripts/file-ledger-lock.js +4 -1
  108. package/scripts/financial-control-plane.js +41 -1
  109. package/scripts/find-dormant-requires.js +118 -0
  110. package/scripts/fs-utils.js +84 -8
  111. package/scripts/gates-engine.js +810 -63
  112. package/scripts/generate-case-study-outreach.js +24 -15
  113. package/scripts/git-at-scale.js +628 -0
  114. package/scripts/governance-conflict-audit.js +1650 -0
  115. package/scripts/governance-difficulty-curriculum.js +328 -0
  116. package/scripts/graphrag-retrieval.js +275 -0
  117. package/scripts/gurobi-optimizer.js +324 -0
  118. package/scripts/gurobi_optimizer.py +485 -0
  119. package/scripts/harness-selector.js +82 -1
  120. package/scripts/hidden-entry-points.js +284 -0
  121. package/scripts/human-escalation.js +199 -1
  122. package/scripts/hybrid-feedback-context.js +152 -19
  123. package/scripts/intent-governed-execution.js +602 -0
  124. package/scripts/intervention-policy.js +123 -20
  125. package/scripts/jit-harness-compose.js +628 -0
  126. package/scripts/jsonl-watcher.js +10 -0
  127. package/scripts/lesson-embedding-index.js +95 -12
  128. package/scripts/lesson-retrieval.js +105 -19
  129. package/scripts/local-model-profile.js +19 -2
  130. package/scripts/mailer/resend-mailer.js +1 -1
  131. package/scripts/matryoshka-embedding.js +235 -0
  132. package/scripts/mcp-oauth.js +42 -4
  133. package/scripts/mcp-session-handles.js +1016 -0
  134. package/scripts/mcp-wiring-doctor.js +314 -0
  135. package/scripts/memory-firewall.js +115 -2
  136. package/scripts/memory-scope-readiness.js +299 -0
  137. package/scripts/memory-vs-rag-route.js +161 -0
  138. package/scripts/model-tier-router.js +148 -21
  139. package/scripts/nvidia-specdecode-al-doctor.js +536 -0
  140. package/scripts/openui-catalog-compose-honesty.js +593 -0
  141. package/scripts/operational-integrity.js +19 -1
  142. package/scripts/override-audit.js +213 -0
  143. package/scripts/package-manager-honesty-doctor.js +458 -0
  144. package/scripts/pr-manager.js +63 -1
  145. package/scripts/prove-herdr-adapter.js +52 -0
  146. package/scripts/prove-memory-pyramid-and-symbolic-canvas.js +95 -0
  147. package/scripts/prove-workos.js +73 -0
  148. package/scripts/provider-attestation-conformance.js +192 -0
  149. package/scripts/provider-receipt-contract.js +136 -0
  150. package/scripts/qwen38-max-cost-optimizer.js +401 -0
  151. package/scripts/radware-threat-defense.js +280 -0
  152. package/scripts/rag-embedding-identity.js +221 -0
  153. package/scripts/rag-precision-guardrails.js +112 -2
  154. package/scripts/remote-feedback-capture.js +159 -0
  155. package/scripts/research-agent-harness.js +256 -0
  156. package/scripts/rsi-safety-hillclimb.js +200 -0
  157. package/scripts/rule-sprawl.js +188 -0
  158. package/scripts/schedule-manager.js +147 -0
  159. package/scripts/self-heal.js +8 -0
  160. package/scripts/session-lease.js +415 -0
  161. package/scripts/simatree-data-governance.js +347 -0
  162. package/scripts/slo-alert-engine.js +172 -7
  163. package/scripts/solver-parity.js +539 -0
  164. package/scripts/stealth-memory-injection-gate.js +333 -0
  165. package/scripts/switchyard-router.js +366 -0
  166. package/scripts/telemetry-analytics.js +84 -27
  167. package/scripts/temporal-decay-weighting.js +138 -0
  168. package/scripts/test-all.js +165 -0
  169. package/scripts/token-savings.js +42 -0
  170. package/scripts/tool-kpi-tracker.js +108 -5
  171. package/scripts/tool-registry.js +193 -5
  172. package/scripts/universal-claim-evaluator.js +14 -2
  173. package/scripts/vector-store.js +279 -9
  174. package/scripts/workflow-notebook.js +391 -0
  175. package/scripts/workflow-sentinel.js +111 -12
  176. package/scripts/workos-production-guard.js +260 -0
  177. package/scripts/workspace-search-route.js +515 -0
  178. package/server.json +2 -2
  179. package/src/agent-identity-boundary.js +76 -0
  180. package/src/agent-retrieval-cache.js +155 -0
  181. package/src/alert-noise-ledger.js +502 -0
  182. package/src/api/server.js +724 -153
  183. package/src/git-fast-cache.js +220 -0
  184. package/src/git-wal-sync.js +156 -0
  185. package/src/hash-anchored-edit.js +82 -0
  186. package/src/hermes-platform-protocol.js +475 -0
  187. package/src/hermes-sync-plane.js +241 -0
  188. package/src/index.js +30 -1
  189. package/src/iso42001-compliance-guard.js +97 -0
  190. package/src/latency-budget.js +244 -0
  191. package/src/mcp-writeguard.js +316 -0
  192. package/src/miminions-adapter.js +106 -0
  193. package/src/pipeline-compass.js +104 -0
  194. package/src/ppl-alert-pipeline.js +284 -0
  195. package/src/rendezvous-router.js +90 -0
  196. package/src/security-questionnaire.js +195 -0
@@ -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
+ };