@konneal/engine 0.2.104 → 0.2.109

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 (40) hide show
  1. package/dist/ablate.d.ts +31 -0
  2. package/dist/ask-EAKLN77G.js +15 -0
  3. package/dist/{chunk-25ECO5M7.js → chunk-2R7OVYAO.js} +377 -185
  4. package/dist/{chunk-U4ERFUOK.js → chunk-4NXH5ZI6.js} +1 -1
  5. package/dist/{chunk-DTW4UW2E.js → chunk-DJ65EF5G.js} +1 -1
  6. package/dist/{chunk-NCZHN6QW.js → chunk-G4KBK5XZ.js} +175 -22
  7. package/dist/{chunk-ANUHRJ4Z.js → chunk-G56TYXNN.js} +3 -3
  8. package/dist/{chunk-TFGJ6NYS.js → chunk-HAU5INEJ.js} +5 -5
  9. package/dist/{chunk-KNCRYAX3.js → chunk-TH5XGPD5.js} +1 -1
  10. package/dist/{chunk-BOAITSVV.js → chunk-VT7DR6NQ.js} +11 -1
  11. package/dist/config.d.ts +6 -0
  12. package/dist/openapi-types.d.ts +24 -0
  13. package/dist/prompts/doc-summary.md +1 -0
  14. package/dist/prompts/speculative-verify.md +9 -0
  15. package/dist/route.d.ts +29 -0
  16. package/dist/search-HKGN7F3O.js +13 -0
  17. package/dist/speculative.d.ts +9 -0
  18. package/dist/{tools-EXDADNWX.js → tools-WSWEGJVQ.js} +4 -4
  19. package/dist/worker_mcp/src/index.js +2 -2
  20. package/dist/worker_public/src/config.js +1 -1
  21. package/dist/worker_public/src/index.js +20 -16
  22. package/dist/worker_public/src/requestScope.js +2 -2
  23. package/docs/spec-pipeline.md +12 -1
  24. package/package.json +1 -1
  25. package/workers/worker_public/openapi.yaml +53 -0
  26. package/workers/worker_public/prompts/doc-summary.md +1 -0
  27. package/workers/worker_public/prompts/speculative-verify.md +9 -0
  28. package/workers/worker_public/src/ablate.ts +97 -0
  29. package/workers/worker_public/src/admin.ts +14 -6
  30. package/workers/worker_public/src/ask.ts +120 -16
  31. package/workers/worker_public/src/config.ts +10 -0
  32. package/workers/worker_public/src/pipeline.ts +5 -2
  33. package/workers/worker_public/src/route.ts +77 -0
  34. package/workers/worker_public/src/speculative.ts +39 -0
  35. package/workers/worker_public/src/stages/graphPpr.ts +189 -0
  36. package/workers/worker_public/src/stages/index.ts +19 -0
  37. package/workers/worker_public/src/stages/sectionDescent.ts +91 -7
  38. package/workers/worker_public/src/stages/types.ts +6 -0
  39. package/dist/ask-M3VRAIJB.js +0 -15
  40. package/dist/search-5SNP5CHL.js +0 -13
@@ -6,7 +6,7 @@
6
6
  import { LIMITS, MODELS, THRESHOLDS, num, sha256Hex, roleModel, answerEffort, requestEffort, effortBudget } from "./config";
7
7
  import { portModelRunner } from "./env.ts";
8
8
  import { refCodec } from "./codecs";
9
- import { buildMessages, citations, retrieve, retrievalQuery, identityNote, splitHistory, listwiseRerank, refusalAnswer, Hit } from "./pipeline";
9
+ import { buildMessages, citations, fill, promptVars, retrieve, retrievalQuery, identityNote, splitHistory, listwiseRerank, refusalAnswer, Hit } from "./pipeline";
10
10
  import { sessionFrom } from "./auth";
11
11
  import { retrieveInternal } from "./internal_gateway";
12
12
  import { understandQuery } from "./understand";
@@ -34,6 +34,11 @@ import { memoryNote } from "./memories";
34
34
  import { entitlementScope, resolveRequestScope, requestSalt, standardKeysFrom } from "./requestScope";
35
35
  import { rawSessionToken, type SessionClaims } from "./session";
36
36
  import { cacheKeyMaterial, corpusGen, exactCacheKey, freshRequested } from "./answercache";
37
+ import { parseAblate } from "./ablate";
38
+ import { STAGE_NAMES } from "./stages/index.ts";
39
+ import { routeFor, fastRouteStages } from "./route.ts";
40
+ import { partitionSubsets, type Partitionable } from "./speculative.ts";
41
+ import speculativeVerifyPrompt from "../prompts/speculative-verify.md";
37
42
  import type { Env } from "./env";
38
43
  export type { Env };
39
44
  import { json, err, corsHeaders, readJson, validateQuery, type ApiKey } from "./lib/http";
@@ -59,6 +64,57 @@ function userImageDataUrl(body: any): string | null {
59
64
  return img;
60
65
  }
61
66
 
67
+ // drafts are candidates, not the answer: half the window budget keeps
68
+ // each draft short and keeps the parallel leg inside the single-call
69
+ // latency it must beat
70
+ const DRAFT_TOKEN_BUDGET = 1600;
71
+
72
+ /** The verifier's passage listing: the same [n] label grammar the
73
+ * answer path builds (the final answer cites against it). */
74
+ function speculativeListing(hits: Hit[]): string {
75
+ return hits
76
+ .map((h, i) => {
77
+ const id = (h.metadata.docidentifier || h.metadata.doc_id || "source").replace(/\s*\(([A-Z])\)\s*$/, "").trim();
78
+ const edition = h.metadata.edition && !id.includes(h.metadata.edition) ? ":" + h.metadata.edition : "";
79
+ const anchor = h.metadata.clause_anchor ? ` §${h.metadata.clause_anchor}` : "";
80
+ return `[${i + 1}] ${id}${edition}${anchor} ${h.text.replace(/\s+/g, " ").slice(0, 700)}`;
81
+ })
82
+ .join("\n\n");
83
+ }
84
+
85
+ /** Speculative draft-verify: draft per diversified subset (parallel,
86
+ * cheap model, contract-shaped messages), then ONE strong call
87
+ * verifies every candidate against the FULL window and produces the
88
+ * final answer. Null on any failure — the caller's normal generation
89
+ * stands. */
90
+ async function speculativeDraftVerify(
91
+ env: Env,
92
+ opts: { query: string; hits: Hit[]; lang?: string | null; draftModel: string; verifierModel: string },
93
+ ): Promise<{ answer: string; chosen: number; nDrafts: number } | null> {
94
+ const subsets = partitionSubsets(opts.hits as unknown as Partitionable[]);
95
+ if (!subsets) return null;
96
+ const draftSets = subsets.map((subset) =>
97
+ buildMessages(opts.query, subset as unknown as Hit[], opts.lang ?? undefined, [], undefined, undefined, DRAFT_TOKEN_BUDGET).messages,
98
+ );
99
+ const drafts = await Promise.all(
100
+ draftSets.map((m) => generateOnce(env, opts.draftModel, m, "low").catch(() => null)),
101
+ );
102
+ const candidates = drafts
103
+ .map((d) => (d ?? "").trim())
104
+ .filter((d) => d.length > 0);
105
+ if (!candidates.length) return null;
106
+ const candidateText = candidates
107
+ .map((d, i) => `<candidate ${i + 1} (prepared from a partial view)>\n${d}\n</candidate ${i + 1}>`)
108
+ .join("\n\n");
109
+ const system = fill(speculativeVerifyPrompt.trimEnd(), promptVars({}));
110
+ const answer = await generateOnce(env, opts.verifierModel, [
111
+ { role: "system", content: system },
112
+ { role: "user", content: `Question: ${opts.query}\n\nCandidate draft answers:\n${candidateText}\n\nFull context passages:\n${speculativeListing(opts.hits)}` },
113
+ ], "low").catch(() => null);
114
+ if (!answer || !answer.trim()) return null;
115
+ return { answer, chosen: candidates.length, nDrafts: drafts.length };
116
+ }
117
+
62
118
  async function cacheGet(env: Env, gen: string, ns: string, query: string, lang?: string, salt?: string | null) {
63
119
  const key = exactCacheKey(env.INDEX_VERSION, gen, ns, await sha256Hex(cacheKeyMaterial(query, lang, salt)));
64
120
  const hit = await env.CACHE.get(key, "json");
@@ -298,6 +354,12 @@ async function handleAsk(
298
354
  const body = await readJson(req);
299
355
  const q = validateQuery(body);
300
356
  if (!q) return err(400, "invalid_input", `query is required (1-${LIMITS.maxInputChars} chars)`);
357
+ // the ablation boundary (TODO.sota/09): a runner-scored configuration
358
+ // rides the body behind the admin credential. Failures are loud — a
359
+ // runner must never mistake a rejected configuration for a measured one.
360
+ const ablated = parseAblate(body, env.ADMIN_TOKEN, req.headers.get("x-admin-token"), STAGE_NAMES);
361
+ if (!ablated.ok) return err(ablated.status, ablated.code, ablated.message);
362
+ const ablate = ablated.config;
301
363
 
302
364
  // The declared context (TODO.ai-platform/02): the panel's opt-in chips.
303
365
  // A declared context makes the answer depend on MORE than the query, so
@@ -339,6 +401,13 @@ async function handleAsk(
339
401
  : null);
340
402
  // resolved before the quota check: the effort choice prices the ask
341
403
  const effort = requestEffort(env, member, (body as any)?.effort);
404
+ // the fast route floors GENERATION effort at low (a scoped or
405
+ // definitional question needs no deep reasoning; the caller's
406
+ // declared effort always wins). Quota keeps the declared weight —
407
+ // the floor is a serving decision, not a billing one.
408
+ const declaredEffort = (body as any)?.effort;
409
+ const routeEffort = (route: "fast" | "deep") =>
410
+ route === "fast" && !declaredEffort ? ("low" as const) : effort;
342
411
 
343
412
  const limit =
344
413
  tier === "key" ? key!.day_limit : tier === "member" || member ? num(env as any, "MEMBER_DAY_ASK", 300) : num(env as any, "ANON_DAY_ASK", 20);
@@ -469,7 +538,7 @@ async function handleAsk(
469
538
  // cache.py) and old-generation entries miss (oimlsmart/rag#72)
470
539
  const gen = await corpusGen(env.CACHE);
471
540
  const withImage = effectiveImage || storedImage;
472
- const cached = fresh || contextual || declaredCtx || draftAct || apiCallIntent || withImage ? null : await cacheGet(env, gen, ns, q.query, q.lang, salt);
541
+ const cached = fresh || contextual || declaredCtx || draftAct || apiCallIntent || withImage || ablate ? null : await cacheGet(env, gen, ns, q.query, q.lang, salt);
473
542
  const wantsStream = body?.stream === true || (tier === "anon" && body?.stream !== false);
474
543
 
475
544
  if (cached) {
@@ -877,7 +946,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
877
946
  // the worker EXECUTES the bound node's machine checks against the
878
947
  // question's stated values; the model narrates the computed verdict
879
948
  // and the verdict BLOCK is server-built — data, never generated prose
880
- const machineVerdict = boundModel && !boundModel.gated ? machineEvaluate(boundModel.content, q.query) : null;
949
+ const machineVerdict = boundModel && !boundModel.gated && !ablate?.noVerdict ? machineEvaluate(boundModel.content, q.query) : null;
881
950
  const machineNote = machineVerdict && boundModel ? verdictNote(machineVerdict, boundModel) : undefined;
882
951
  // ── condition-set membership (konneal/engine#90): the test-method
883
952
  // packages' severity menus as machine-verifiable membership. Fires
@@ -903,7 +972,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
903
972
  };
904
973
  let conditionVerdict: ConditionVerdict | null = null;
905
974
  let conditionStandard: string | null = null;
906
- if (!machineVerdict && !boundModel && P().publisher.features?.model_plane) {
975
+ if (!machineVerdict && !boundModel && !ablate?.noVerdict && P().publisher.features?.model_plane) {
907
976
  const ql = q.query.toLowerCase();
908
977
  const severityWord = /\b(severity|test|valid|tolerance|condition|within)\b/.test(ql);
909
978
  const stated = quantitiesIn(q.query);
@@ -1055,6 +1124,18 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1055
1124
  boundaryNote = boundaryNoteText(match, citing);
1056
1125
  }
1057
1126
  }
1127
+ // ── the adaptive route (TODO.sota/05): understanding's features pick
1128
+ // fast (the registry minus the expansion lanes) or deep (full). The
1129
+ // ablation override wins when present — the grid measures the routes
1130
+ // by forcing them; adaptive is the serving default. The decision
1131
+ // reads the FINAL understanding (the declared-context and text-naming
1132
+ // reconciliations above may have written the scope the router trusts).
1133
+ const routeMode = ablate?.route ?? "adaptive";
1134
+ const routed = routeMode === "adaptive"
1135
+ ? routeFor(understanding, q.query)
1136
+ : { route: routeMode, features: [`forced-${routeMode}`] };
1137
+ const routeSubset = ablate?.stages ?? (routed.route === "fast" ? fastRouteStages(STAGE_NAMES) : null);
1138
+ if (routeSubset) console.log("route:", routed.route, routed.features.join("+"), "—", STAGE_NAMES.length - routeSubset.length, "lanes dropped");
1058
1139
  try {
1059
1140
  const tR = Date.now();
1060
1141
  // ── The "my account" live read (TODO.ai-platform/03) — resolved
@@ -1111,7 +1192,8 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1111
1192
  // publications there).
1112
1193
  retrieved = await retrieve(env, q.query, { prev, understanding, federate, warmEmbed, graphDocNumbers,
1113
1194
  sealScope: declaredScoped ? docScope : null, optimisticHits, optimisticVec,
1114
- datasetScope: narrowed ? corpora : null, standardKeys, lexicalBoost, licensedDocNumbers });
1195
+ datasetScope: narrowed ? corpora : null, standardKeys, lexicalBoost, licensedDocNumbers,
1196
+ ...(routeSubset ? { stageSubset: routeSubset } : {}) });
1115
1197
  stageTiming["retrieve-core"] = Date.now() - tR;
1116
1198
  console.log("stage: retrieve", Date.now() - tR, "ms");
1117
1199
  // ── TTFT surgery: the two post-retrieval LLM calls run IN PARALLEL —
@@ -1145,7 +1227,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1145
1227
  if (grade === "weak" && understanding?.docidentifier) {
1146
1228
  const broaden = `${understanding.standalone_query || q.query} ${understanding.docidentifier}`.trim();
1147
1229
  const tc = Date.now();
1148
- const second = await retrieve(env, q.query, { prev, understanding, queryOverride: broaden, federate, datasetScope: narrowed ? corpora : null, standardKeys, sealScope: declaredScoped ? docScope : null, lexicalBoost });
1230
+ const second = await retrieve(env, q.query, { prev, understanding, queryOverride: broaden, federate, datasetScope: narrowed ? corpora : null, standardKeys, sealScope: declaredScoped ? docScope : null, lexicalBoost, ...(routeSubset ? { stageSubset: routeSubset } : {}) });
1149
1231
  const grade2 = await gradeRetrieval(env.AI, roleModel(env, "grader"), q.query, second.hits.map((h: Hit) => h.text));
1150
1232
  stageTiming.corrective = Date.now() - tc;
1151
1233
  if (grade2 === "good") retrieved = second; // corrective retry must be strictly better
@@ -1163,7 +1245,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1163
1245
  // point at the declare flow
1164
1246
  const answer =
1165
1247
  licenseBoundaryRefusal(modelDocHint?.doc_number ?? understanding?.doc_number ?? null, standardKeys) ?? refusalAnswer();
1166
- const out = { answer, citations: [], model, query_hash: await sha256Hex(q.query), context_applied: ctxApplied };
1248
+ const out = { answer, citations: [], model, query_hash: await sha256Hex(q.query), context_applied: ctxApplied, ...(ablate ? { ablate } : {}), route: { route: routed.route, features: routed.features } };
1167
1249
  telemetry(env, ctx, tier, "ask", model, true, answer.length, out.query_hash, q.lang, undefined, telemetryMeta());
1168
1250
  return json({ ...out, quota, });
1169
1251
  }
@@ -1304,7 +1386,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1304
1386
  const cites = boundModel ? [modelCitation(boundModel), ...citations(usedHits)] : citations(usedHits);
1305
1387
 
1306
1388
  if (wantsStream) {
1307
- const stream = await generateStream(env, model, messages, effort);
1389
+ const stream = await generateStream(env, model, messages, routeEffort(routed.route));
1308
1390
  if (stream) {
1309
1391
  const encoder = new TextEncoder();
1310
1392
  const sse = new ReadableStream({
@@ -1349,7 +1431,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1349
1431
  ...messages.slice(0, -1),
1350
1432
  { role: "user", content: "The photograph was attached and the register note lists actual matching certificate rows. Present those rows — number, holder, model, status, and every document link — as the answer. Do not claim that nothing was provided." },
1351
1433
  messages[messages.length - 1],
1352
- ], effort);
1434
+ ], routeEffort(routed.route));
1353
1435
  if (regen) {
1354
1436
  full = "";
1355
1437
  try {
@@ -1401,7 +1483,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1401
1483
  if (streamed.violations.length > 0) {
1402
1484
  console.log("anchors:", streamed.violations.length, "of", streamed.total, "unverified — not caching");
1403
1485
  }
1404
- if (streamed.violations.length === 0 && canonical.length > 0 && !contextual && !declaredCtx && !canonical.includes(refusalAnswer())) {
1486
+ if (streamed.violations.length === 0 && canonical.length > 0 && !contextual && !declaredCtx && !canonical.includes(refusalAnswer()) && !ablate) {
1405
1487
  ctx.waitUntil(
1406
1488
  env.CACHE.put(exactCacheKey(env.INDEX_VERSION, gen, ns, await sha256Hex(cacheKeyMaterial(q.query, q.lang, salt))), JSON.stringify({ answer: canonical, citations: cites, model, query_hash: queryHash, ...(sourceQuality ? { source_quality: sourceQuality, confidence_note: streamedNote || qualityNote(sourceQuality) } : {}), ...(streamedEntailment ? { entailment: streamedEntailment } : {}) }), { expirationTtl: LIMITS.cacheTtlSec }),
1407
1489
  );
@@ -1421,7 +1503,28 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1421
1503
  }
1422
1504
 
1423
1505
  const tGen = Date.now();
1424
- let answer = await generateOnce(env, model, messages, effort);
1506
+ // ── speculative draft-verify (TODO.sota/02 row 2): when the ablation
1507
+ // config arms it, the cheap model drafts per diversified subset in
1508
+ // parallel and one strong call verifies against the FULL window. The
1509
+ // seam replaces only the primary generation call — the contract
1510
+ // checks below run on the verifier's output exactly as on any answer —
1511
+ // and any failure falls through to the normal single-shot path.
1512
+ let answer: string | null = null;
1513
+ if (ablate?.speculative) {
1514
+ const spec = await speculativeDraftVerify(env, {
1515
+ query: q.query, hits: usedHits, lang: q.lang,
1516
+ draftModel: MODELS.member, verifierModel: roleModel(env, "verifier"),
1517
+ }).catch((e: unknown) => {
1518
+ console.log("speculative: failed, falling through —", String(e).slice(0, 120));
1519
+ return null;
1520
+ });
1521
+ if (spec) {
1522
+ answer = spec.answer;
1523
+ stageTiming.speculative = Date.now() - tGen;
1524
+ console.log("speculative:", spec.nDrafts, "drafts,", spec.chosen, "viable — verified");
1525
+ }
1526
+ }
1527
+ if (!answer) answer = await generateOnce(env, model, messages, routeEffort(routed.route));
1425
1528
  if (answer === null) {
1426
1529
  generateRetries += 1;
1427
1530
  // the fallback is a text-only model: image parts must be flattened
@@ -1441,7 +1544,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1441
1544
  ? m
1442
1545
  : { ...m, content: m.content.filter((p: any) => p?.type === "text").map((p: any) => (p?.text ?? "").replace(/\n?\(The user attached an image with this question; interpret it directly when answering\.\)/, "")).join("\n") },
1443
1546
  );
1444
- answer = await generateOnce(env, MODELS.fallback, flat, effort);
1547
+ answer = await generateOnce(env, MODELS.fallback, flat, routeEffort(routed.route));
1445
1548
  }
1446
1549
  if (answer) answer = canonicalRefusal(answer);
1447
1550
  stageTiming.generate = Date.now() - tGen;
@@ -1479,7 +1582,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1479
1582
  ? `Correction notice: your draft reproduced a table as markdown or presented a served table's data without its reference. Rewrite the answer: describe the table in prose, cite the clause, and write the reference token [[u:${tableUnitId ?? "<unit id>"}]] exactly where the table belongs. Do not render any table as markdown.`
1480
1583
  : ANCHOR_CORRECTION_NOTE;
1481
1584
  generateRetries += 1;
1482
- const corrected = await generateOnce(env, model, [...messages, { role: "system", content: note }], effort);
1585
+ const corrected = await generateOnce(env, model, [...messages, { role: "system", content: note }], routeEffort(routed.route));
1483
1586
  if (corrected) {
1484
1587
  const correctedAnswer = canonicalRefusal(corrected);
1485
1588
  const retryAnchors = checkQuoteAnchors(correctedAnswer, used.map((h: Hit) => h.text));
@@ -1519,10 +1622,11 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1519
1622
  sealScope: declaredScoped ? docScope : null,
1520
1623
  datasetScope: narrowed ? corpora : null,
1521
1624
  standardKeys,
1625
+ ...(routeSubset ? { stageSubset: routeSubset } : {}),
1522
1626
  });
1523
1627
  if (retryRetrieve.hits.length > 0) {
1524
1628
  const { messages: retryMessages, usedHits: retryUsed } = buildMessages(q.query, retryRetrieve.hits, q.lang, keptHistory, undefined, summary, budget);
1525
- const retryAnswer = await generateOnce(env, model, retryMessages, effort);
1629
+ const retryAnswer = await generateOnce(env, model, retryMessages, routeEffort(routed.route));
1526
1630
  // the answer now comes from the retry passages — citations must follow
1527
1631
  if (retryAnswer) {
1528
1632
  answer = canonicalRefusal(retryAnswer);
@@ -1602,8 +1706,8 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1602
1706
  if (v.support !== "supported") console.log("entailment:", v.support, raced.score, raced.ungrounded_claims?.slice(0, 2));
1603
1707
  }
1604
1708
  }
1605
- const out = { answer, citations: finalCites, ...(jsonQuality ? { source_quality: jsonQuality, confidence_note: entailmentNote || qualityNote(jsonQuality), ...(entailment ? { entailment } : {}), ...(jsonQuality === "ocr" ? { experimental_sources: experimentalSourceLabels(finalCites) } : {}) } : {}), model: MODELS.member, query_hash: queryHash, follow_ups: understanding?.follow_ups ?? [], blocks: [...c2ns.blocks, ...(verdictBlock ? [verdictBlock] : []), ...(conditionBlock ? [conditionBlock] : []), ...(aggregationBlock ? [aggregationBlock] : []), ...completionBlocks], context_applied: ctxApplied, ...(liveRecords ? { records: liveRecords } : {}) };
1606
- const cacheable = !contextual && !declaredCtx && !answer.includes(refusalAnswer()) && finalAnchors.violations.length === 0;
1709
+ const out = { answer, citations: finalCites, ...(jsonQuality ? { source_quality: jsonQuality, confidence_note: entailmentNote || qualityNote(jsonQuality), ...(entailment ? { entailment } : {}), ...(jsonQuality === "ocr" ? { experimental_sources: experimentalSourceLabels(finalCites) } : {}) } : {}), model: MODELS.member, query_hash: queryHash, follow_ups: understanding?.follow_ups ?? [], blocks: [...c2ns.blocks, ...(verdictBlock ? [verdictBlock] : []), ...(conditionBlock ? [conditionBlock] : []), ...(aggregationBlock ? [aggregationBlock] : []), ...completionBlocks], context_applied: ctxApplied, ...(liveRecords ? { records: liveRecords } : {}), ...(ablate ? { ablate } : {}), route: { route: routed.route, features: routed.features } };
1710
+ const cacheable = !contextual && !declaredCtx && !ablate && !answer.includes(refusalAnswer()) && finalAnchors.violations.length === 0;
1607
1711
  if (cacheable) {
1608
1712
  const ck = exactCacheKey(env.INDEX_VERSION, gen, ns, await sha256Hex(cacheKeyMaterial(q.query, q.lang, salt)));
1609
1713
  ctx.waitUntil(env.CACHE.put(ck, JSON.stringify(out), { expirationTtl: LIMITS.cacheTtlSec }));
@@ -24,6 +24,11 @@ export const MODELS = {
24
24
  listwise: "@cf/zai-org/glm-4.7-flash",
25
25
  // deep-research loop (G10): bounded agentic iterations, members-only
26
26
  research: "@cf/zai-org/glm-5.3-flash",
27
+ // speculative draft-verify (TODO.sota/02 row 2): the strong verifier
28
+ // over the cheap drafts — the research-tier model verifies; the drafter
29
+ // is the standard answer model (MODELS.member). VERIFIER_MODEL swaps it
30
+ // live, same lever as every role.
31
+ verifier: "@cf/zai-org/glm-5.2",
27
32
  } as const;
28
33
 
29
34
  export const LIMITS = {
@@ -62,6 +67,11 @@ export const THRESHOLDS = {
62
67
  * pool below the primary dense lane; they must earn their window
63
68
  * slot under the cross-encoder, not by graph membership alone. */
64
69
  graphLaneDiscount: 0.75,
70
+ /** The PPR lane's merge discount (TODO.sota/06, catalog row 4): graph-
71
+ * propagated candidates arrive at the same half-discount the narrowed
72
+ * graph lane pays (rag#137's law — flooding dilutes the pool), and
73
+ * only the cross-encoder's preference survives the cut. */
74
+ pprDiscount: 0.375,
65
75
  licensedLaneScore: 0.5,
66
76
  /** The entailment gate (TODO.new-era/10): the faithfulness scorer
67
77
  * promoted from eval-only onto the serving path. The confidence line
@@ -36,7 +36,7 @@ import { QueryFilters, toVectorizeFilter, standardKeyAllowed } from "./selfquery
36
36
  import { hitQuality } from "./quality";
37
37
  import { lexicalPrefilter } from "./lexical";
38
38
  import { positionOrder } from "./structural";
39
- import { STAGES, runStages } from "./stages";
39
+ import { STAGES, projectStages, runStages } from "./stages";
40
40
  import type { PipelineContext, RetrieveOptions, GlossaryEntry } from "./stages/types";
41
41
  // the chunk wire contract lives with its pydantic twin (workers/shared/
42
42
  // chunk.ts ↔ ingest/vector_adapter.py); re-exported here so the existing
@@ -144,7 +144,10 @@ export async function retrieve(
144
144
  env, query, rq, folded, u, filters, filter, vector, lexicalHits,
145
145
  matches: [], hits: [], finalHits: [], glossary: [], notes: [], opts, lane: {},
146
146
  };
147
- await runStages(STAGES, ctx);
147
+ // the registry IS the switch: an ablation configuration projects the
148
+ // SAME registry (same modules, same order) to its named subset — a
149
+ // narrower pipeline, never a forked one
150
+ await runStages(opts.stageSubset?.length ? projectStages(opts.stageSubset) : STAGES, ctx);
148
151
  return {
149
152
  hits: ctx.finalHits,
150
153
  filters: ctx.filters ?? {},
@@ -0,0 +1,77 @@
1
+ // ── The adaptive router (TODO.sota/05, the catalog's row 1) ──
2
+ //
3
+ // Query features (understanding's own output, never string hacks) pick
4
+ // the route: FAST drops the candidate-expansion lanes (hyde, multi-
5
+ // query, sub-query — each costs extra embeds and Vectorize queries, and
6
+ // for a scoped, definitional, or short question they rarely fire); DEEP
7
+ // keeps the full registry. The stage registry IS the switch: the route
8
+ // is a projection of the same registry, never a second pipeline.
9
+ //
10
+ // The policy is deliberately conservative — ambiguous questions route
11
+ // DEEP, and the caller-declared effort always wins — because the
12
+ // routing frontier is a measured claim (TODO.sota/09's grid scores
13
+ // route-fast, route-deep and adaptive on the dev half; the policy
14
+ // thresholds tune after that evidence, never before).
15
+ //
16
+ // Self-contained (no imports) so the unit tests run on plain node type
17
+ // stripping, like ablate/answercache/verdict.
18
+
19
+ export type Route = "fast" | "deep";
20
+
21
+ /** Minimal structural shape of the understanding the router reads —
22
+ * the full contract lives in understandContract.ts; this subset keeps
23
+ * the module self-contained. */
24
+ export interface RouteFeatures {
25
+ complexity?: "simple" | "complex" | null;
26
+ doc_number?: string | null;
27
+ term?: string | null;
28
+ defined_terms?: string[];
29
+ sub_queries?: string[];
30
+ process_intent?: boolean;
31
+ }
32
+
33
+ /** THRESHOLDS-adjacent policy numbers (declared here until the grid
34
+ * measures them; every steering number lives in one place — this is
35
+ * the router's place). */
36
+ export const ROUTE_FAST_WORD_CAP = 10;
37
+
38
+ /** The candidate-expansion lanes the fast route drops (the stage
39
+ * names in the registry). */
40
+ export const ROUTE_FAST_DROPS = ["hyde", "multi-query", "sub-query"];
41
+
42
+ /** Route one question from its understanding features. Returns the
43
+ * route and the fired features — the response echoes both, which is
44
+ * the telemetry the routing-accuracy analysis reads (TODO.sota/05). */
45
+ export function routeFor(u: RouteFeatures | null, query: string): {
46
+ route: Route;
47
+ features: string[];
48
+ } {
49
+ const features: string[] = [];
50
+ const words = query.trim().split(/\s+/).filter(Boolean).length;
51
+
52
+ // DEEP features — complexity is the understanding model's own
53
+ // multi-perspective judgment; sub-queries and process intent both
54
+ // need the widest candidate pool
55
+ if (u?.complexity === "complex") features.push("complex");
56
+ if ((u?.sub_queries ?? []).length > 0) features.push("sub-queries");
57
+ if (u?.process_intent) features.push("process-intent");
58
+ if (words > ROUTE_FAST_WORD_CAP) features.push("long-question");
59
+ if (features.length) return { route: "deep", features };
60
+
61
+ // FAST — everything else: a question within the word cap rides the
62
+ // fast registry. The conservative direction is structural here: the
63
+ // long, the complex, the multi-perspective and the process-shaped all
64
+ // routed deep above; under-routing costs accuracy, over-routing costs
65
+ // only latency, and the grid prices both.
66
+ if (u?.doc_number) features.push("doc-scoped");
67
+ if (u?.term) features.push("definitional");
68
+ if ((u?.defined_terms ?? []).length > 0) features.push("terminology");
69
+ features.push("short-question");
70
+ return { route: "fast", features };
71
+ }
72
+
73
+ /** The fast route's stage list: the registry minus the expansion lanes.
74
+ * projectStages owns the ordering and validation semantics. */
75
+ export function fastRouteStages(registry: readonly string[]): string[] {
76
+ return registry.filter((n) => !ROUTE_FAST_DROPS.includes(n));
77
+ }
@@ -0,0 +1,39 @@
1
+ // ── Speculative draft-verify, the geometry (TODO.sota/02, row 2) ──
2
+ //
3
+ // The partition of the answer window into DIVERSIFIED subsets: hits
4
+ // group by document (first-seen order), the groups deal round-robin
5
+ // into the subsets, and each subset keeps reading order. Fewer than two
6
+ // distinct documents means nothing to diversify — the orchestration
7
+ // (ask.ts) falls through to the normal single-shot generation.
8
+ //
9
+ // Self-contained (no imports) so the unit tests run on plain node type
10
+ // stripping, like ablate/answercache/verdict. The LLM legs (draft per
11
+ // subset, the strong verifier) live in ask.ts beside the other
12
+ // ask-path mechanisms; their gate is the ablation grid (TODO.sota/09),
13
+ // never the unit suite.
14
+
15
+ export const SPECULATIVE_SUBSETS = 3;
16
+
17
+ /** Minimal structural shape the partition reads. */
18
+ export interface Partitionable {
19
+ metadata: { docidentifier?: string; doc_id?: string };
20
+ }
21
+
22
+ export function partitionSubsets<T extends Partitionable>(
23
+ hits: T[],
24
+ k: number = SPECULATIVE_SUBSETS,
25
+ ): T[][] | null {
26
+ const groups: T[][] = [];
27
+ const byDoc = new Map<string, T[]>();
28
+ for (const h of hits) {
29
+ const key = h.metadata.docidentifier || h.metadata.doc_id || "";
30
+ const arr = byDoc.get(key) ?? [];
31
+ arr.push(h);
32
+ if (arr.length === 1) groups.push(arr);
33
+ byDoc.set(key, arr);
34
+ }
35
+ if (groups.length < 2) return null;
36
+ const subsets: T[][] = Array.from({ length: Math.min(k, groups.length) }, () => []);
37
+ groups.forEach((g, i) => subsets[i % subsets.length].push(...g));
38
+ return subsets;
39
+ }
@@ -0,0 +1,189 @@
1
+ // Graph PPR lane (additive, TODO.sota/06 — the catalog's row 4,
2
+ // HippoRAG-style multi-hop memory): Personalized-PageRank over the D1
3
+ // graph projection, seeded from the query's resolved entities — the
4
+ // vocabulary link's concepts and the understood publication's family
5
+ // node — spreading over the domain's own law edges (defines, successor,
6
+ // part_of, variant_of, amends, cites). Documents ranked by stationary
7
+ // mass merge their query-relevant candidates like the graph lane does:
8
+ // multi-hop neighborhoods (a term defined by R 87, whose family cites
9
+ // R 61) surface without any hand-written traversal.
10
+ //
11
+ // The neighborhood is bounded (two hops, capped frontier) and the
12
+ // iteration is deterministic power iteration over the undirected
13
+ // adjacency — same query, same ranks, always.
14
+ import { THRESHOLDS } from "../config.ts";
15
+ import { lexicalWithin } from "../lexical.ts";
16
+ import { refCodec } from "../codecs.ts";
17
+ import { portStore } from "../env.ts";
18
+ import type { StoreQuery } from "../ports/store.ts";
19
+ import type { Stage } from "./types.ts";
20
+
21
+ const EDGE_KINDS = "('defines','successor','part_of','variant_of','amends','cites')";
22
+ const PPR_DAMPING = 0.85;
23
+ const PPR_ITERATIONS = 12;
24
+ const PPR_FRONTIER_CAP = 24;
25
+
26
+ /** Deterministic Personalized-PageRank: uniform teleport over the
27
+ * seeds, propagation over the UNDIRECTED adjacency (typed direction
28
+ * matters for semantics, not for spread — a doc defines a concept as
29
+ * much as the concept is defined by the doc). Pure: the unit tests
30
+ * pin the convergence and the seed preference. */
31
+ export function pageRank(
32
+ edges: [string, string][],
33
+ seeds: string[],
34
+ opts: { damping?: number; iterations?: number } = {},
35
+ ): Map<string, number> {
36
+ const damping = opts.damping ?? PPR_DAMPING;
37
+ const iterations = opts.iterations ?? PPR_ITERATIONS;
38
+ const adj = new Map<string, string[]>();
39
+ for (const [a, b] of edges) {
40
+ if (!adj.has(a)) adj.set(a, []);
41
+ if (!adj.has(b)) adj.set(b, []);
42
+ adj.get(a)!.push(b);
43
+ adj.get(b)!.push(a);
44
+ }
45
+ const live = seeds.filter((s) => adj.has(s));
46
+ if (!live.length) return new Map();
47
+ const base = 1 / live.length;
48
+ let rank = new Map<string, number>(live.map((s) => [s, base]));
49
+ for (let it = 0; it < iterations; it++) {
50
+ const next = new Map<string, number>();
51
+ for (const s of live) next.set(s, (1 - damping) * base);
52
+ for (const [node, r] of rank) {
53
+ const nbrs = adj.get(node);
54
+ if (!nbrs?.length) {
55
+ // a dangling node returns its mass to the seeds
56
+ for (const s of live) next.set(s, (next.get(s) ?? 0) + damping * r * base);
57
+ continue;
58
+ }
59
+ const share = (damping * r) / nbrs.length;
60
+ for (const n of nbrs) next.set(n, (next.get(n) ?? 0) + share);
61
+ }
62
+ rank = next;
63
+ }
64
+ return rank;
65
+ }
66
+
67
+ /** Seed node ids for a query: the glossary's concepts (label match,
68
+ * the concept-graph lane's resolution) and the understood
69
+ * publication's family node. */
70
+ async function seedNodes(store: StoreQuery, glossary: { term: string }[], docidentifier: string | null): Promise<string[]> {
71
+ const jobs: Promise<string[]>[] = [];
72
+ for (const gl of glossary.slice(0, 3)) {
73
+ if (gl.term.length < 3) continue;
74
+ jobs.push(
75
+ store.prepare(
76
+ "SELECT id FROM graph_nodes WHERE kind = 'concept' AND (label = ?1 OR label LIKE ?2) LIMIT 6",
77
+ )
78
+ .bind(gl.term, `%${gl.term}%`)
79
+ .all()
80
+ .then((r: any) => (r.results ?? []).map((x: any) => String(x.id)))
81
+ .catch(() => [] as string[]),
82
+ );
83
+ }
84
+ const fam = docidentifier ? refCodec().familyOf(docidentifier) : null;
85
+ if (fam) {
86
+ jobs.push(
87
+ store.prepare("SELECT id FROM graph_nodes WHERE kind = 'family' AND id = ?1")
88
+ .bind(`family:${fam}`)
89
+ .first()
90
+ .then((r: any) => (r?.id ? [String(r.id)] : []))
91
+ .catch(() => [] as string[]),
92
+ );
93
+ }
94
+ const settled = await Promise.all(jobs);
95
+ return [...new Set(settled.flat())].slice(0, 4);
96
+ }
97
+
98
+ /** Two bounded hops of the seed neighborhood: the edge list and the
99
+ * frontier's node kinds (doc nodes are what retrieval can use). */
100
+ async function neighborhood(store: StoreQuery, seeds: string[]): Promise<{ edges: [string, string][]; kinds: Map<string, string> }> {
101
+ const seenEdges = new Set<string>();
102
+ const edges: [string, string][] = [];
103
+ let frontier = seeds.slice(0, PPR_FRONTIER_CAP);
104
+ const visited = new Set(seeds);
105
+ for (let hop = 0; hop < 2 && frontier.length; hop++) {
106
+ const marks = frontier.map(() => "?").join(",");
107
+ const rows = (await store.prepare(
108
+ `SELECT src, dst FROM graph_edges WHERE kind IN ${EDGE_KINDS} AND (src IN (${marks}) OR dst IN (${marks})) LIMIT 400`,
109
+ )
110
+ .bind(...frontier)
111
+ .all()
112
+ .catch(() => ({ results: [] }))) as any;
113
+ const next: string[] = [];
114
+ for (const r of rows.results ?? []) {
115
+ const key = `${r.src}>${r.dst}`;
116
+ if (seenEdges.has(key)) continue;
117
+ seenEdges.add(key);
118
+ edges.push([String(r.src), String(r.dst)]);
119
+ for (const n of [String(r.src), String(r.dst)]) {
120
+ if (!visited.has(n)) {
121
+ visited.add(n);
122
+ if (next.length < PPR_FRONTIER_CAP) next.push(n);
123
+ }
124
+ }
125
+ }
126
+ frontier = next;
127
+ }
128
+ const nodeIds = [...visited].slice(0, 60);
129
+ const kinds = new Map<string, string>();
130
+ for (let i = 0; i < nodeIds.length; i += 30) {
131
+ const chunk = nodeIds.slice(i, i + 30);
132
+ const marks = chunk.map(() => "?").join(",");
133
+ const rows = (await store.prepare(`SELECT id, kind FROM graph_nodes WHERE id IN (${marks})`)
134
+ .bind(...chunk)
135
+ .all()
136
+ .catch(() => ({ results: [] }))) as any;
137
+ for (const r of rows.results ?? []) kinds.set(String(r.id), String(r.kind));
138
+ }
139
+ return { edges, kinds };
140
+ }
141
+
142
+ /** The ranked document numbers for the query's seeds: PPR mass over
143
+ * the neighborhood, doc nodes only, rendered through the codec. */
144
+ export async function pprDocuments(
145
+ store: StoreQuery,
146
+ glossary: { term: string }[],
147
+ docidentifier: string | null,
148
+ ): Promise<string[]> {
149
+ const seeds = await seedNodes(store, glossary, docidentifier);
150
+ if (!seeds.length) return [];
151
+ const { edges, kinds } = await neighborhood(store, seeds);
152
+ if (!edges.length) return [];
153
+ const rank = pageRank(edges, seeds);
154
+ const docs = [...rank.entries()]
155
+ .filter(([id, mass]) => kinds.get(id) === "doc" && mass > 0)
156
+ .sort((a, b) => b[1] - a[1])
157
+ .map(([id]) => refCodec().graphDocNumber(id))
158
+ .filter((n): n is string => !!n);
159
+ return [...new Set(docs)].slice(0, 6);
160
+ }
161
+
162
+ export const graphPpr: Stage = {
163
+ name: "graph-ppr",
164
+ failure: "additive",
165
+ // no raw-binding guard: a deployment without the store degrades
166
+ // through the prefetch's own catch (the lane stays silent)
167
+ when: (c) => (c.glossary.length > 0 || !!c.u?.docidentifier) && c.vector.length > 0,
168
+ prefetch: (c) => {
169
+ c.lane["graph-ppr"] = pprDocuments(portStore(c.env), c.glossary, c.u?.docidentifier ?? null).catch(() => [] as string[]);
170
+ },
171
+ run: async (c) => {
172
+ const docs = (await c.lane["graph-ppr"]!) as string[];
173
+ if (!docs?.length) return;
174
+ // same identity resolution and narrowing law as the graph lane
175
+ // (rag#137): BM25-within-set candidates at the half discount, five
176
+ // at most, cross-encoder preference decides
177
+ const g = await lexicalWithin(c.env, c.rq || c.query, docs, 6);
178
+ const seenIds = new Set(c.matches.map((m: any) => m.id));
179
+ let merged = 0;
180
+ for (const m of (g ?? []).slice(0, 5)) {
181
+ if (!seenIds.has(m.id)) {
182
+ c.matches.push({ id: m.id, score: m.score * THRESHOLDS.pprDiscount, metadata: m.metadata });
183
+ seenIds.add(m.id);
184
+ merged++;
185
+ }
186
+ }
187
+ if (merged) console.log("graph ppr:", docs.join(","), "— merged", merged);
188
+ },
189
+ };