@konneal/engine 0.1.0

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 (285) hide show
  1. package/LICENSE +29 -0
  2. package/README.md +13 -0
  3. package/dist/admin.d.ts +26 -0
  4. package/dist/ai.d.ts +6 -0
  5. package/dist/anchors.d.ts +6 -0
  6. package/dist/answercache.d.ts +22 -0
  7. package/dist/ask.d.ts +5 -0
  8. package/dist/auth.d.ts +12 -0
  9. package/dist/bubble.d.ts +14 -0
  10. package/dist/chunk-LLWPT2XV.js +49 -0
  11. package/dist/chunk-MB74PTRM.js +114 -0
  12. package/dist/chunk-WOGQM7DJ.js +197 -0
  13. package/dist/chunk-WWNCWKKC.js +42 -0
  14. package/dist/completion.d.ts +5 -0
  15. package/dist/config.d.ts +154 -0
  16. package/dist/config.js +37 -0
  17. package/dist/context.d.ts +115 -0
  18. package/dist/conversations.d.ts +5 -0
  19. package/dist/drafts.d.ts +129 -0
  20. package/dist/env.d.ts +57 -0
  21. package/dist/faithfulness.d.ts +5 -0
  22. package/dist/grader.d.ts +3 -0
  23. package/dist/graph.d.ts +13 -0
  24. package/dist/hybrid.d.ts +7 -0
  25. package/dist/index.d.ts +9 -0
  26. package/dist/index.js +5373 -0
  27. package/dist/internal_gateway.d.ts +14 -0
  28. package/dist/lexical.d.ts +7 -0
  29. package/dist/livedata.d.ts +77 -0
  30. package/dist/memories.d.ts +10 -0
  31. package/dist/modelplane.d.ts +61 -0
  32. package/dist/oidc.d.ts +73 -0
  33. package/dist/pipeline.d.ts +57 -0
  34. package/dist/profile.d.ts +2 -0
  35. package/dist/profile.gen.d.ts +70 -0
  36. package/dist/profile.js +8 -0
  37. package/dist/projects.d.ts +8 -0
  38. package/dist/prompts/conversational.md +8 -0
  39. package/dist/prompts/enrichment.md +3 -0
  40. package/dist/prompts/faithfulness.md +1 -0
  41. package/dist/prompts/grader.md +5 -0
  42. package/dist/prompts/listwise.md +3 -0
  43. package/dist/prompts/precision.md +1 -0
  44. package/dist/prompts/reflect.md +1 -0
  45. package/dist/prompts/relevancy.md +1 -0
  46. package/dist/prompts/research.md +10 -0
  47. package/dist/prompts/section-summary.md +5 -0
  48. package/dist/prompts/summarize.md +1 -0
  49. package/dist/prompts/system.md +18 -0
  50. package/dist/prompts/understanding.md +17 -0
  51. package/dist/quota.d.ts +13 -0
  52. package/dist/reflect.d.ts +5 -0
  53. package/dist/refs.d.ts +40 -0
  54. package/dist/refusal.d.ts +9 -0
  55. package/dist/refusal.js +9 -0
  56. package/dist/requestScope.d.ts +26 -0
  57. package/dist/requestScope.js +10 -0
  58. package/dist/research.d.ts +8 -0
  59. package/dist/search.d.ts +4 -0
  60. package/dist/selfquery.d.ts +7 -0
  61. package/dist/session.d.ts +1 -0
  62. package/dist/share.d.ts +2 -0
  63. package/dist/structural.d.ts +27 -0
  64. package/dist/tablecontext.d.ts +11 -0
  65. package/dist/understand.d.ts +11 -0
  66. package/dist/understandContract.d.ts +29 -0
  67. package/dist/verdict.d.ts +24 -0
  68. package/docs/API.md +451 -0
  69. package/docs/ARCHITECTURE.md +302 -0
  70. package/docs/AUDIT-2026-08-24.md +71 -0
  71. package/docs/CONTRIBUTOR-AUDIT-2026-08-25.md +147 -0
  72. package/docs/INGEST-ARCHITECTURE.md +158 -0
  73. package/docs/MCP.md +92 -0
  74. package/docs/METANORMA-AI-SERIALIZATION.md +247 -0
  75. package/docs/MKO-EXPORT-PIPELINE.md +147 -0
  76. package/docs/REDESIGN-NORMATIVE-RAG-ETSI.md +485 -0
  77. package/docs/RESEARCH-SOTA-2026.md +243 -0
  78. package/docs/ROADMAP-SOTA.md +130 -0
  79. package/docs/SOTA-STAGE-SPECS.md +509 -0
  80. package/docs/annealment/F1-verdict.md +27 -0
  81. package/docs/annealment/F10-notes.md +19 -0
  82. package/docs/annealment/F11-composition.md +17 -0
  83. package/docs/annealment/F12-passport.md +17 -0
  84. package/docs/annealment/F2-counterfactual.md +20 -0
  85. package/docs/annealment/F3-absence.md +21 -0
  86. package/docs/annealment/F4-instance.md +18 -0
  87. package/docs/annealment/F5-workflow.md +21 -0
  88. package/docs/annealment/F6-impact.md +21 -0
  89. package/docs/annealment/F7-editions.md +18 -0
  90. package/docs/annealment/F8-selfverify.md +19 -0
  91. package/docs/annealment/F9-projection-qa.md +17 -0
  92. package/docs/annealment/L0-locate.md +19 -0
  93. package/docs/annealment/L1-extract.md +18 -0
  94. package/docs/annealment/L2-nomenclature.md +22 -0
  95. package/docs/annealment/L3-geometry.md +23 -0
  96. package/docs/annealment/L4-composition.md +21 -0
  97. package/docs/annealment/L5-cross-standard.md +20 -0
  98. package/docs/annealment/L6-diachrony.md +21 -0
  99. package/docs/annealment/L7-perception.md +20 -0
  100. package/docs/annealment/L8-computation.md +22 -0
  101. package/docs/annealment/L9-instance-process.md +23 -0
  102. package/docs/annealment/README.md +10 -0
  103. package/docs/guidelines-metanorma-ai-programme.md +279 -0
  104. package/docs/identity-onboarding-rag.md +65 -0
  105. package/docs/identity-service.md +219 -0
  106. package/docs/knowledge-annealment.md +273 -0
  107. package/docs/konneal-extraction-plan.md +481 -0
  108. package/docs/metanorma-for-ai.md +270 -0
  109. package/docs/mirror-plan.md +36 -0
  110. package/docs/multi-sdo-architecture.md +191 -0
  111. package/docs/paper-annealment-comparison.md +259 -0
  112. package/docs/paper-assets/architecture.svg +94 -0
  113. package/docs/paper-assets/contract-v2.svg +94 -0
  114. package/docs/paper-assets/mko-ingest.svg +91 -0
  115. package/docs/paper-oiml-bulletin.md +402 -0
  116. package/docs/paper-oiml-bulletin.mdx +419 -0
  117. package/docs/product-branding-options.md +172 -0
  118. package/docs/projects-design.md +88 -0
  119. package/docs/sota-mechanisms.md +184 -0
  120. package/docs/spec-api.md +77 -0
  121. package/docs/spec-pipeline.md +126 -0
  122. package/docs/vector-adapter.md +88 -0
  123. package/package.json +70 -0
  124. package/profile/corpora.yaml +5 -0
  125. package/profile/datasets.yaml +14 -0
  126. package/profile/prompts.yaml +5 -0
  127. package/profile/publisher.yaml +17 -0
  128. package/profile/retrieval.yaml +1 -0
  129. package/profile/sources.yaml +5 -0
  130. package/profile/ui.yaml +7 -0
  131. package/scripts/gen_profile.mjs +33 -0
  132. package/workers/shared/ai.ts +21 -0
  133. package/workers/shared/auth.ts +16 -0
  134. package/workers/shared/chunk.ts +108 -0
  135. package/workers/shared/oidc.ts +312 -0
  136. package/workers/shared/router.ts +45 -0
  137. package/workers/shared/session.ts +104 -0
  138. package/workers/worker_internal/src/index.ts +157 -0
  139. package/workers/worker_internal/tsconfig.json +15 -0
  140. package/workers/worker_internal/wrangler.toml +32 -0
  141. package/workers/worker_mcp/src/index.ts +175 -0
  142. package/workers/worker_mcp/tsconfig.json +13 -0
  143. package/workers/worker_mcp/wrangler.toml +18 -0
  144. package/workers/worker_public/migrations/0002_conversations.sql +22 -0
  145. package/workers/worker_public/migrations/0003_shared_conversations.sql +9 -0
  146. package/workers/worker_public/migrations/0004_graph.sql +16 -0
  147. package/workers/worker_public/migrations/0005_documents.sql +19 -0
  148. package/workers/worker_public/migrations/0006_conversation_entities.sql +11 -0
  149. package/workers/worker_public/migrations/0007_chunks_fts.sql +43 -0
  150. package/workers/worker_public/migrations/0008_unit_payloads.sql +15 -0
  151. package/workers/worker_public/migrations/0009_chunks_unit.sql +8 -0
  152. package/workers/worker_public/migrations/0009_message_context.sql +7 -0
  153. package/workers/worker_public/migrations/0010_model_nodes.sql +33 -0
  154. package/workers/worker_public/migrations/0011_schema_union.sql +38 -0
  155. package/workers/worker_public/migrations/0012_memories.sql +15 -0
  156. package/workers/worker_public/migrations/0013_projects.sql +21 -0
  157. package/workers/worker_public/node_modules/@cloudflare/workers-types/2021-11-03/index.d.ts +16306 -0
  158. package/workers/worker_public/node_modules/@cloudflare/workers-types/2021-11-03/index.ts +16261 -0
  159. package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-01-31/index.d.ts +16373 -0
  160. package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-01-31/index.ts +16328 -0
  161. package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-03-21/index.d.ts +16382 -0
  162. package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-03-21/index.ts +16337 -0
  163. package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-08-04/index.d.ts +16383 -0
  164. package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-08-04/index.ts +16338 -0
  165. package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-10-31/index.d.ts +16403 -0
  166. package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-10-31/index.ts +16358 -0
  167. package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-11-30/index.d.ts +16408 -0
  168. package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-11-30/index.ts +16363 -0
  169. package/workers/worker_public/node_modules/@cloudflare/workers-types/2023-03-01/index.d.ts +16414 -0
  170. package/workers/worker_public/node_modules/@cloudflare/workers-types/2023-03-01/index.ts +16369 -0
  171. package/workers/worker_public/node_modules/@cloudflare/workers-types/2023-07-01/index.d.ts +16414 -0
  172. package/workers/worker_public/node_modules/@cloudflare/workers-types/2023-07-01/index.ts +16369 -0
  173. package/workers/worker_public/node_modules/@cloudflare/workers-types/README.md +135 -0
  174. package/workers/worker_public/node_modules/@cloudflare/workers-types/entrypoints.svg +53 -0
  175. package/workers/worker_public/node_modules/@cloudflare/workers-types/experimental/index.d.ts +17095 -0
  176. package/workers/worker_public/node_modules/@cloudflare/workers-types/experimental/index.ts +17050 -0
  177. package/workers/worker_public/node_modules/@cloudflare/workers-types/index.d.ts +16306 -0
  178. package/workers/worker_public/node_modules/@cloudflare/workers-types/index.ts +16261 -0
  179. package/workers/worker_public/node_modules/@cloudflare/workers-types/latest/index.d.ts +16447 -0
  180. package/workers/worker_public/node_modules/@cloudflare/workers-types/latest/index.ts +16402 -0
  181. package/workers/worker_public/node_modules/@cloudflare/workers-types/oldest/index.d.ts +16306 -0
  182. package/workers/worker_public/node_modules/@cloudflare/workers-types/oldest/index.ts +16261 -0
  183. package/workers/worker_public/node_modules/@cloudflare/workers-types/package.json +11 -0
  184. package/workers/worker_public/package.json +13 -0
  185. package/workers/worker_public/prompts/conversational.md +8 -0
  186. package/workers/worker_public/prompts/enrichment.md +3 -0
  187. package/workers/worker_public/prompts/faithfulness.md +1 -0
  188. package/workers/worker_public/prompts/grader.md +5 -0
  189. package/workers/worker_public/prompts/listwise.md +3 -0
  190. package/workers/worker_public/prompts/precision.md +1 -0
  191. package/workers/worker_public/prompts/reflect.md +1 -0
  192. package/workers/worker_public/prompts/relevancy.md +1 -0
  193. package/workers/worker_public/prompts/research.md +10 -0
  194. package/workers/worker_public/prompts/section-summary.md +5 -0
  195. package/workers/worker_public/prompts/summarize.md +1 -0
  196. package/workers/worker_public/prompts/system.md +18 -0
  197. package/workers/worker_public/prompts/understanding.md +17 -0
  198. package/workers/worker_public/public/app.js +166 -0
  199. package/workers/worker_public/public/index.html +48 -0
  200. package/workers/worker_public/public/style.css +147 -0
  201. package/workers/worker_public/schema.sql +248 -0
  202. package/workers/worker_public/src/admin.ts +358 -0
  203. package/workers/worker_public/src/ai.ts +71 -0
  204. package/workers/worker_public/src/anchors.ts +41 -0
  205. package/workers/worker_public/src/answercache.ts +72 -0
  206. package/workers/worker_public/src/ask.ts +1094 -0
  207. package/workers/worker_public/src/auth.ts +252 -0
  208. package/workers/worker_public/src/bubble.ts +111 -0
  209. package/workers/worker_public/src/completion.ts +75 -0
  210. package/workers/worker_public/src/config.ts +238 -0
  211. package/workers/worker_public/src/context.ts +238 -0
  212. package/workers/worker_public/src/conversations.ts +162 -0
  213. package/workers/worker_public/src/drafts.ts +497 -0
  214. package/workers/worker_public/src/env.ts +90 -0
  215. package/workers/worker_public/src/faithfulness.ts +63 -0
  216. package/workers/worker_public/src/grader.ts +89 -0
  217. package/workers/worker_public/src/graph.ts +63 -0
  218. package/workers/worker_public/src/hybrid.ts +77 -0
  219. package/workers/worker_public/src/index.ts +441 -0
  220. package/workers/worker_public/src/internal_gateway.ts +41 -0
  221. package/workers/worker_public/src/lexical.ts +86 -0
  222. package/workers/worker_public/src/lib/hit.ts +4 -0
  223. package/workers/worker_public/src/lib/http.ts +83 -0
  224. package/workers/worker_public/src/lib/router.ts +4 -0
  225. package/workers/worker_public/src/livedata.ts +334 -0
  226. package/workers/worker_public/src/memories.ts +81 -0
  227. package/workers/worker_public/src/modelplane.ts +213 -0
  228. package/workers/worker_public/src/oidc.ts +333 -0
  229. package/workers/worker_public/src/pipeline.ts +377 -0
  230. package/workers/worker_public/src/ports/blobs.ts +7 -0
  231. package/workers/worker_public/src/ports/cloudflare/adapters.ts +177 -0
  232. package/workers/worker_public/src/ports/kv.ts +8 -0
  233. package/workers/worker_public/src/ports/model.ts +28 -0
  234. package/workers/worker_public/src/ports/runtime.ts +13 -0
  235. package/workers/worker_public/src/ports/store.ts +20 -0
  236. package/workers/worker_public/src/ports/vector.ts +26 -0
  237. package/workers/worker_public/src/profile.gen.ts +101 -0
  238. package/workers/worker_public/src/profile.ts +16 -0
  239. package/workers/worker_public/src/projects.ts +108 -0
  240. package/workers/worker_public/src/prompts.d.ts +6 -0
  241. package/workers/worker_public/src/quota.ts +54 -0
  242. package/workers/worker_public/src/reflect.ts +67 -0
  243. package/workers/worker_public/src/refs.ts +107 -0
  244. package/workers/worker_public/src/refusal.ts +65 -0
  245. package/workers/worker_public/src/requestScope.ts +71 -0
  246. package/workers/worker_public/src/research.ts +126 -0
  247. package/workers/worker_public/src/search.ts +56 -0
  248. package/workers/worker_public/src/selfquery.ts +25 -0
  249. package/workers/worker_public/src/session.ts +4 -0
  250. package/workers/worker_public/src/share.ts +53 -0
  251. package/workers/worker_public/src/stages/conceptGraph.ts +68 -0
  252. package/workers/worker_public/src/stages/conceptSteer.ts +39 -0
  253. package/workers/worker_public/src/stages/corpusScope.ts +25 -0
  254. package/workers/worker_public/src/stages/dedup.ts +10 -0
  255. package/workers/worker_public/src/stages/dense.ts +73 -0
  256. package/workers/worker_public/src/stages/diversity.ts +33 -0
  257. package/workers/worker_public/src/stages/editionCover.ts +63 -0
  258. package/workers/worker_public/src/stages/editionSteer.ts +88 -0
  259. package/workers/worker_public/src/stages/familyBoost.ts +22 -0
  260. package/workers/worker_public/src/stages/federate.ts +22 -0
  261. package/workers/worker_public/src/stages/glossary.ts +65 -0
  262. package/workers/worker_public/src/stages/graphLane.ts +31 -0
  263. package/workers/worker_public/src/stages/hyde.ts +29 -0
  264. package/workers/worker_public/src/stages/index.ts +69 -0
  265. package/workers/worker_public/src/stages/lexicalUnion.ts +21 -0
  266. package/workers/worker_public/src/stages/multiQuery.ts +57 -0
  267. package/workers/worker_public/src/stages/overviewDemote.ts +14 -0
  268. package/workers/worker_public/src/stages/poolOpen.ts +10 -0
  269. package/workers/worker_public/src/stages/propagate.ts +15 -0
  270. package/workers/worker_public/src/stages/rerank.ts +47 -0
  271. package/workers/worker_public/src/stages/seal.ts +16 -0
  272. package/workers/worker_public/src/stages/sectionDescent.ts +61 -0
  273. package/workers/worker_public/src/stages/stdRefNudge.ts +35 -0
  274. package/workers/worker_public/src/stages/subQuery.ts +42 -0
  275. package/workers/worker_public/src/stages/termNudge.ts +24 -0
  276. package/workers/worker_public/src/stages/typedPin.ts +131 -0
  277. package/workers/worker_public/src/stages/types.ts +112 -0
  278. package/workers/worker_public/src/stages/windowFloor.ts +23 -0
  279. package/workers/worker_public/src/structural.ts +171 -0
  280. package/workers/worker_public/src/tablecontext.ts +41 -0
  281. package/workers/worker_public/src/understand.ts +72 -0
  282. package/workers/worker_public/src/understandContract.ts +67 -0
  283. package/workers/worker_public/src/verdict.ts +255 -0
  284. package/workers/worker_public/tsconfig.json +18 -0
  285. package/workers/worker_public/wrangler.toml +104 -0
@@ -0,0 +1,377 @@
1
+ import { embed } from "./ai";
2
+ import { P } from "./profile.ts";
3
+ import { LIMITS, MODELS, DATASETS, THRESHOLDS, processExpansion } from "./config";
4
+ import systemPromptText from "../prompts/system.md";
5
+ import conversationalPromptText from "../prompts/conversational.md";
6
+ import listwisePromptText from "../prompts/listwise.md";
7
+ import { tableContext } from "./tablecontext";
8
+
9
+ // the pinned refusal sentence lives with the canonicalizer in ./refusal
10
+ // (refusals are never cached: a refusal says "retrieval found nothing",
11
+ // which is a property of the moment, not of the question); re-exported
12
+ // here so the existing import surface keeps working
13
+ export { refusalAnswer } from "./refusal";
14
+
15
+ /** Fill {{TOKEN}} placeholders in a prompt data file. Unknown/empty tokens
16
+ * resolve to "" so optional lines vanish cleanly. */
17
+ function fill(template: string, vars: Record<string, string>): string {
18
+ return template.replace(/\{\{(\w+)\}\}/g, (_m, k: string) => (k in vars ? vars[k] : ""));
19
+ }
20
+ import { QueryFilters, toVectorizeFilter } from "./selfquery";
21
+ import { lexicalPrefilter } from "./lexical";
22
+ import { positionOrder } from "./structural";
23
+ import { STAGES, runStages } from "./stages";
24
+ import type { PipelineContext, RetrieveOptions, GlossaryEntry } from "./stages/types";
25
+ // the chunk wire contract lives with its pydantic twin (workers/shared/
26
+ // chunk.ts ↔ ingest/vector_adapter.py); re-exported here so the existing
27
+ // import surface keeps working
28
+ export type { ChunkMeta, Hit } from "../../shared/chunk";
29
+ import type { ChunkMeta, Hit } from "../../shared/chunk";
30
+ import { portModelRunner } from "./env.ts";
31
+
32
+ export interface Retrieved {
33
+ hits: Hit[];
34
+ filters: QueryFilters;
35
+ /** vocabulary link (the L2 nomenclature bridge): top defined-term
36
+ * candidates for the question's subject — the answer model adjudicates
37
+ * among them (dense retrieval alone binds everyday words to the wrong
38
+ * term: measured "keeps drifting" → creep 0.69 vs durability 0.54) */
39
+ glossary?: GlossaryEntry[];
40
+ }
41
+
42
+ // Short follow-ups are usually elliptical ("and the limits?") — fold the
43
+ // previous question into the RETRIEVAL query (generation still sees the
44
+ // original wording). Purely structural (word count): whether a question
45
+ // is elliptical is a semantic judgment, and semantics belong to the
46
+ // understanding model, whose standalone_query takes precedence anyway.
47
+ export function retrievalQuery(query: string, prev?: string): string {
48
+ if (!prev || !prev.trim()) return query;
49
+ const words = query.trim().split(/\s+/).length;
50
+ if (words <= 8) return `${prev.trim()} — ${query.trim()}`;
51
+ return query;
52
+ }
53
+
54
+ // colloquial process questions ("how do I get a device certified to R 60")
55
+ // share almost no vocabulary with the B-series prose that answers them —
56
+ // THRESHOLDS.processExpansion is appended to the retrieval query so the
57
+ // window contains the certification-system documents at all
58
+
59
+ // ── the retrieval pipeline (TODO.impl/02) ────────────────────────────────
60
+ // retrieve() is COMPOSITION ONLY: build the context (the parallel
61
+ // embed/lexical prelude — Option C's latency structure is load-bearing),
62
+ // run the stage registry, project the result. Every mechanism lives in
63
+ // its own stage module under src/stages/; the registry order and the
64
+ // per-stage contracts are specified in docs/spec-pipeline.md.
65
+ export async function retrieve(
66
+ env: any,
67
+ query: string,
68
+ opts: RetrieveOptions = {},
69
+ ): Promise<Retrieved> {
70
+ const u = opts.understanding ?? null;
71
+ // Filters and process-intent come ONLY from query understanding — no
72
+ // regex floor, no union. When understanding is unavailable the query
73
+ // runs unfiltered and unexpanded (vanilla retrieval); meaning is never
74
+ // decided by string matching.
75
+ // The declared context's HARD scope outranks the understanding's routing
76
+ // opinion: a certificate chip that says R 60:2021 keeps its dense filter
77
+ // even when the question's vocabulary makes the classifier emit
78
+ // process_intent (certification wording is the chip's subject, not the
79
+ // question's). Without this, dense runs unfiltered, the certification
80
+ // corpus floods the pool, and the pool-level seal — which must stay hard —
81
+ // cuts it to zero (the ctx-entity refusal, diagnosed 2026-09-08).
82
+ const scope: QueryFilters | null =
83
+ u && !u.process_intent && u.doc_number
84
+ ? { doc_number: u.doc_number, ...(u.edition ? { edition: u.edition } : {}) }
85
+ : opts.sealScope
86
+ ? { doc_number: opts.sealScope.doc_number, ...(opts.sealScope.edition ? { edition: opts.sealScope.edition } : {}) }
87
+ : null;
88
+ const filters: QueryFilters | null = scope;
89
+ const filter = filters ? toVectorizeFilter(filters) : null;
90
+ const folded = retrievalQuery(query, opts.prev);
91
+ let rq = opts.queryOverride?.trim() || u?.standalone_query?.trim() || folded;
92
+ if (u?.process_intent) rq += processExpansion();
93
+ // Dense embed + full-corpus BM25 prefilter in parallel (G-ETSI-1 /
94
+ // arXiv:2604.09868 §II-B5). Lexical must scan the whole corpus — the
95
+ // old keywordRank only re-ordered dense hits and could not recover
96
+ // exact-jargon misses. Fail-open: empty lexical list leaves dense alone.
97
+ // Option C: when the query is unchanged (rq === folded) the optimistic
98
+ // vector is already resolved — never await a fresh embed for it.
99
+ const vectorP =
100
+ rq === folded && opts.optimisticVec
101
+ ? Promise.resolve(opts.optimisticVec)
102
+ : rq === folded && opts.warmEmbed
103
+ ? opts.warmEmbed.then((w) => w ?? embed(portModelRunner(env), MODELS.embed, rq))
104
+ : embed(portModelRunner(env), MODELS.embed, rq);
105
+ const lexicalP = lexicalPrefilter(env, rq).catch(() => [] as Hit[]);
106
+ const [vector, lexicalHits0] = await Promise.all([vectorP, lexicalP]);
107
+ // The declared context's seal binds the lexical lane at the SOURCE: the
108
+ // RRF fusion mixes the full-corpus lexical ranking straight into the
109
+ // final hits — past the pool-level seal — so under a seal the lexical
110
+ // lane is the FAMILY's lexical hits only.
111
+ const lexicalHits = opts.sealScope
112
+ ? lexicalHits0.filter((h) => h.metadata.doc_number === opts.sealScope!.doc_number && (!opts.sealScope!.edition || h.metadata.edition === opts.sealScope!.edition))
113
+ : lexicalHits0;
114
+ if (lexicalHits.length) console.log("lexical prefilter:", lexicalHits.length, "hits");
115
+
116
+ const ctx: PipelineContext = {
117
+ env, query, rq, folded, u, filters, filter, vector, lexicalHits,
118
+ matches: [], hits: [], finalHits: [], glossary: [], opts, lane: {},
119
+ };
120
+ await runStages(STAGES, ctx);
121
+ return {
122
+ hits: ctx.finalHits,
123
+ filters: ctx.filters ?? {},
124
+ ...(ctx.glossary?.length ? { glossary: ctx.glossary } : {}),
125
+ };
126
+ }
127
+
128
+ export interface HistoryTurn {
129
+ role: "user" | "assistant";
130
+ content: string;
131
+ }
132
+
133
+ // ── context budget ─────────────────────────────────────────────────────
134
+ // The model window is finite; without a cap, a long conversation plus 8
135
+ // clause chunks (tables can be huge) overflows and the request FAILS
136
+ // outright. Everything is estimated (never exact) and stays well under
137
+ // the smallest context our models accept. Priority when the budget is
138
+ // tight: system + query always fit → newest history gets a bounded slice
139
+ // → passages fill the rest, best-ranked first, worst-ranked dropped.
140
+
141
+ function estTokens(s: string): number {
142
+ // CJK/Arabic/Indic scripts ≈ 1 token per char; Latin ≈ 1 per 4 chars
143
+ const wide = (s.match(/[؀-ۿݐ-ݿऀ-ॿ぀-ヿ㐀-䶿一-鿿가-힯]/g) || []).length;
144
+ return wide + Math.ceil((s.length - wide) / 4);
145
+ }
146
+
147
+ function clipToTokens(s: string, maxTok: number): string {
148
+ if (maxTok < 40 || estTokens(s) <= maxTok) return s;
149
+ const wide = (s.match(/[؀-ۿݐ-ݿऀ-ॿ぀-ヿ㐀-䶿一-鿿가-힯]/g) || []).length;
150
+ const latinChars = Math.max(0, maxTok - wide) * 4;
151
+ return s.slice(0, Math.min(s.length, wide + latinChars)).trimEnd() + " …";
152
+ }
153
+
154
+ export interface BuiltMessages {
155
+ messages: { role: string; content: string }[];
156
+ usedHits: Hit[]; // passages actually included (citations must match these)
157
+ }
158
+
159
+ /** System instruction for a conversational (non-knowledge) turn: the
160
+ * service facts the model speaks from, composed from the DATASETS
161
+ * catalog — the same SSOT /api/datasets serves. Routing is decided by
162
+ * query UNDERSTANDING (understanding.ts), never by string matching. */
163
+ export function identityNote(member: boolean): string {
164
+ const corpora = DATASETS().filter((d) => !d.session || member)
165
+ .map((d) => `- ${d.label}: ${d.description}`)
166
+ .join("\n");
167
+ const locked = DATASETS().filter((d) => d.session && !member);
168
+ const upsell = locked.length
169
+ ? `Signed-in members additionally search: ${locked.map((d) => `${d.label} (${d.description})`).join("; ")}.`
170
+ : "";
171
+ return fill(conversationalPromptText, { CORPORA: corpora, UPSELL: upsell })
172
+ .split("\n")
173
+ .filter((l) => l.trim())
174
+ .join("\n");
175
+ }
176
+
177
+ /** Split history into the turns that fit the budget slice (kept, newest)
178
+ * and the older ones that must be compacted into a summary (overflow). */
179
+ export function splitHistory(
180
+ history: HistoryTurn[],
181
+ budgetTokens: number,
182
+ ): { kept: HistoryTurn[]; overflow: HistoryTurn[] } {
183
+ const historyBudget = Math.floor(budgetTokens * THRESHOLDS.historyBudgetShare);
184
+ let used = 0;
185
+ let cut = 0; // everything before `cut` overflows
186
+ for (let i = history.length - 1; i >= 0; i--) {
187
+ const t = Math.min(estTokens(history[i].content), 600);
188
+ if (used + t > historyBudget) {
189
+ cut = i + 1;
190
+ break;
191
+ }
192
+ used += t;
193
+ }
194
+ return { kept: history.slice(cut), overflow: history.slice(0, cut) };
195
+ }
196
+
197
+
198
+ /** Final-tier LLM listwise rerank: jointly reorders the top passages for
199
+ * hard queries (cascade stage after the cross-encoder). Null = keep the
200
+ * incoming order (timeout/parse failure never blocks serving). */
201
+ export async function listwiseRerank(
202
+ env: any,
203
+ model: string,
204
+ query: string,
205
+ hits: Hit[],
206
+ ): Promise<Hit[] | null> {
207
+ if (hits.length < 4) return null;
208
+ try {
209
+ const listing = hits
210
+ .map((h, i) => {
211
+ const label = `${h.metadata.docidentifier || h.metadata.doc_id}:${h.metadata.edition || ""} §${h.metadata.clause_anchor || ""}`;
212
+ return `[${i + 1}] ${label.replace(/(:|§)+$/g, "")} — ${h.text.replace(/\s+/g, " ").slice(0, 220)}`;
213
+ })
214
+ .join("\n");
215
+ // bounded to 2.5s: this call sits serially before generation starts —
216
+ // a slow reorder must never hold the first token hostage; the
217
+ // cross-encoder order is the fallback and is already good
218
+ const timeout = new Promise<null>((r) => setTimeout(() => r(null), 2500));
219
+ const call = (async () => {
220
+ const res: any = await env.AI.run(model, {
221
+ messages: [
222
+ { role: "system", content: listwisePromptText.trimEnd() },
223
+ { role: "user", content: `Question: ${query}\n\nPassages:\n${listing}` },
224
+ ],
225
+ max_tokens: 700,
226
+ reasoning_effort: "low",
227
+ });
228
+ const text = typeof res?.response === "string" ? res.response : res?.choices?.[0]?.message?.content;
229
+ const m = (text ?? "").match(/\[[\s\S]*?\]/);
230
+ if (!m) return null;
231
+ const order = JSON.parse(m[0]);
232
+ if (!Array.isArray(order) || order.length !== hits.length) return null;
233
+ const idx = order.map((n: unknown) => Number(n) - 1);
234
+ if (idx.some((n: number) => !Number.isInteger(n) || n < 0 || n >= hits.length) || new Set(idx).size !== hits.length) return null;
235
+ return idx.map((n: number) => hits[n]);
236
+ })();
237
+ return await Promise.race([call, timeout]);
238
+ } catch {
239
+ return null;
240
+ }
241
+ }
242
+
243
+ export function buildMessages(
244
+ query: string,
245
+ hits: Hit[],
246
+ lang?: string,
247
+ history: HistoryTurn[] = [],
248
+ retrievalNote?: string,
249
+ conversationSummary?: string,
250
+ budgetTokens: number = LIMITS.inputTokenBudget,
251
+ ): BuiltMessages {
252
+ // per-corpus guidance travels WITH the dataset (config.ts): every
253
+ // dataset whose corpus appears in the passages contributes its note —
254
+ // new corpora need a catalog entry, never pipeline changes
255
+ const corpusNotes = DATASETS().filter(
256
+ (d) => d.note && hits.some((h) => (h.metadata as any).corpus === d.id),
257
+ )
258
+ .map((d) => d.note!)
259
+ .join("\n");
260
+
261
+ // the prompt itself is data (prompts/system.md); one rule per line,
262
+ // joined with spaces exactly as the original array form
263
+ const system = fill(systemPromptText, {
264
+ ...P().prompts.vars,
265
+ HISTORY_CONTEXT: history.length
266
+ ? " Earlier turns of this conversation are provided for context — answer the LATEST question, treating the passages below as the source of truth for facts and citations."
267
+ : "",
268
+ CORPUS_NOTES: corpusNotes,
269
+ LANG_CLAUSE: lang ? ` (explicitly requested: ${lang})` : "",
270
+ })
271
+ .split("\n")
272
+ .map((l) => l.trim())
273
+ .filter(Boolean)
274
+ .join(" ");
275
+
276
+ // history: newest-first into a bounded slice (oldest dropped first);
277
+ // each turn is clipped so accounting and content agree
278
+ const historyBudget = Math.floor(budgetTokens * THRESHOLDS.historyBudgetShare);
279
+ const keptHistory: { role: "user" | "assistant"; content: string }[] = [];
280
+ let historyUsed = 0;
281
+ for (let i = history.length - 1; i >= 0; i--) {
282
+ const content = clipToTokens(history[i].content, 600);
283
+ const t = estTokens(content);
284
+ if (historyUsed + t > historyBudget) break;
285
+ keptHistory.unshift({ role: history[i].role, content });
286
+ historyUsed += t;
287
+ }
288
+
289
+ // passages: best-ranked first into whatever remains
290
+ const summaryBlock = conversationSummary
291
+ ? `Earlier in this conversation (summarized for continuity):\n${conversationSummary}`
292
+ : "";
293
+ let remain =
294
+ budgetTokens - estTokens(system) - estTokens(retrievalNote ?? "") - estTokens(summaryBlock) - estTokens(`Question: ${query}\n\nContext passages:\n`) - historyUsed - 120; // slack for estimator error + output framing
295
+ const passageParts: string[] = [];
296
+ const usedHits: Hit[] = [];
297
+ // Passage label as the MODEL should cite it (it copies these into
298
+ // answers): drop OIML language markers, append the edition only when
299
+ // the identifier doesn't already carry it ("B 18:2025 (E)" + "2025" →
300
+ // no ":2025"; "PD-06 Edition 4" + "4" → no ":4"), and never show a
301
+ // producer UUID as a clause anchor — cite the clause title instead.
302
+ const passageLabel = (m: ChunkMeta): string => {
303
+ const id = (m.docidentifier || m.doc_id || "source").replace(/\s*\(([A-Z])\)\s*$/, "").trim();
304
+ const edition = m.edition && !id.includes(m.edition) ? ":" + m.edition : "";
305
+ const raw = String(m.clause_anchor ?? "");
306
+ const garbage = /^[0-9a-f]{8}-[0-9a-f]{4}-/i.test(raw) || (raw.startsWith("_") && raw.length > 12);
307
+ const anchor = garbage || !raw ? "" : ` §${raw}`;
308
+ return `${id}${edition}${anchor}`;
309
+ };
310
+ // passages in document reading order (FABLE NodeFusion): same-doc
311
+ // clauses read top-to-bottom, docs by best rank — synthesis quality
312
+ // depends on arrangement, not just the selected set
313
+ for (const h of positionOrder(hits)) {
314
+ const st = h.metadata.status === "withdrawn" || h.metadata.status === "superseded" ? ` [${h.metadata.status}]` : "";
315
+ const label = `${passageLabel(h.metadata)}${st}`;
316
+ // answer contract v2: typed passages declare their unit id so the
317
+ // model can reference [[u:<id>]] instead of retyping the object
318
+ const unitTag = (h.metadata as any).unit_id ? ` unit ${(h.metadata as any).unit_id}${(h.metadata as any).block ? ` (${(h.metadata as any).block})` : ""}` : "";
319
+ const head = `[${usedHits.length + 1}] ${label}${unitTag} ${h.metadata.clause_title ? "— " + h.metadata.clause_title : ""}\n`;
320
+ // tables: schema-aware pruning from the producer payload; the
321
+ // stored text is the fallback (pruning never goes below baseline)
322
+ const pruned = (h.metadata as any).block === "table" ? tableContext(h.metadata, query) : null;
323
+ const body = clipToTokens(pruned ?? h.text, LIMITS.maxPassageTokens);
324
+ const t = estTokens(head) + estTokens(body);
325
+ if (t <= remain) {
326
+ passageParts.push(head + body);
327
+ usedHits.push(h);
328
+ remain -= t;
329
+ } else if (usedHits.length < 2) {
330
+ // always keep at least the two best passages, truncated to fit
331
+ passageParts.push(head + clipToTokens(h.text, Math.max(150, remain - estTokens(head))));
332
+ usedHits.push(h);
333
+ remain = 0;
334
+ break;
335
+ } else break;
336
+ }
337
+ const context = passageParts.join("\n\n") || "(no passages)";
338
+
339
+ return {
340
+ messages: [
341
+ { role: "system", content: system },
342
+ ...(retrievalNote ? [{ role: "system", content: retrievalNote }] : []),
343
+ ...(summaryBlock ? [{ role: "system", content: summaryBlock }] : []),
344
+ ...keptHistory.map((h) => ({ role: h.role, content: h.content })),
345
+ { role: "user", content: `Question: ${query}\n\nContext passages:\n${context}` },
346
+ ],
347
+ usedHits,
348
+ };
349
+ }
350
+
351
+ function oimlPublicationUrl(meta: ChunkMeta): string | undefined {
352
+ if (!meta.doctype || !meta.doc_number) return undefined;
353
+ const typeMap: Record<string, string> = { R: "r", D: "d", B: "b", G: "g", E: "e" };
354
+ const t = typeMap[meta.doctype];
355
+ if (!t) return undefined;
356
+ return `https://www.oiml.org/en/publications/${t}${meta.doc_number}`;
357
+ }
358
+
359
+ export function citations(hits: Hit[]) {
360
+ const rank = (s?: string) => (s === "in-force" || s === "joint" ? 0 : s === "unknown" || !s ? 1 : 2);
361
+ return [...hits]
362
+ .map((h) => ({
363
+ doc_id: h.metadata.doc_id,
364
+ docidentifier: h.metadata.docidentifier,
365
+ edition: h.metadata.edition,
366
+ language: h.metadata.language,
367
+ clause_anchor: h.metadata.clause_anchor,
368
+ clause_title: h.metadata.clause_title,
369
+ status: h.metadata.status ?? "unknown",
370
+ superseded_by: h.metadata.superseded_by || undefined,
371
+ corpus: h.metadata.corpus || "oiml",
372
+ url: oimlPublicationUrl(h.metadata),
373
+ snippet: h.text.slice(0, 400),
374
+ score: h.rerank_score ?? h.score,
375
+ }))
376
+ .sort((a, b) => rank(a.status) - rank(b.status)); // in-force first, withdrawn last
377
+ }
@@ -0,0 +1,7 @@
1
+ /** Blobs — immutable object storage (unit assets, rendered documents).
2
+ * Every mainstream provider speaks S3; the reference adapter speaks
3
+ * R2's binding API. Objects are content-typed and read by key. */
4
+ export interface Blobs {
5
+ get(key: string): Promise<{ body: ReadableStream | null; contentType?: string } | null>;
6
+ put(key: string, value: string | ReadableStream | ArrayBuffer, contentType?: string): Promise<void>;
7
+ }
@@ -0,0 +1,177 @@
1
+ /** The Cloudflare reference adapters — the ONLY module where provider
2
+ * APIs appear. Domain modules import the port interfaces; the purity
3
+ * lint (npm run lint:ports) fails any other file that names a
4
+ * provider binding type. Zero runtime change: each adapter forwards
5
+ * to the binding the call sites used to touch directly. */
6
+ import type { ModelRunner, GenerateRequest, GenerateResult } from "../model.ts";
7
+ import type { VectorIndex, VectorMatch, VectorQuery, VectorUpsert } from "../vector.ts";
8
+ import type { Kv } from "../kv.ts";
9
+ import type { Blobs } from "../blobs.ts";
10
+ import type { Runtime } from "../runtime.ts";
11
+
12
+ // Embedding request/response shapes vary across model generations; the
13
+ // probes below try the known shapes in order and remember the winner
14
+ // for the isolate's lifetime. Every wrong-shape attempt burns AI rate
15
+ // budget, so the verified shape goes first. Total failure THROWS — an
16
+ // empty vector would surface downstream as an opaque Vectorize 40006
17
+ // (the 2026-09-14 regression: a probe-less adapter returned [] and the
18
+ // index rejected the 0-dimension query).
19
+
20
+ type EmbedRequestShape = (texts: string[]) => unknown;
21
+
22
+ const EMBED_REQUEST_SHAPES: Record<string, EmbedRequestShape> = {
23
+ // "text" first: the verified request shape for qwen3-embedding-0.6b
24
+ text: (texts) => ({ text: texts }),
25
+ "input.input": (texts) => ({ input: { input: texts } }),
26
+ array: (texts) => ({ input: texts }),
27
+ };
28
+
29
+ let embedRequestWinner: string | null = null;
30
+
31
+ function extractVecBatch(res: unknown, n: number): number[][] | null {
32
+ const r = res as any;
33
+ const d = r?.data ?? r?.result?.data;
34
+ const rows = Array.isArray(d) ? d : Array.isArray(r?.embedding) ? [r.embedding] : null;
35
+ if (!rows) return null;
36
+ const out: number[][] = [];
37
+ for (const row of rows.slice(0, n)) {
38
+ const vec = Array.isArray(row) ? row : Array.isArray(row?.embedding) ? row.embedding : null;
39
+ if (!vec || vec.length === 0) return null;
40
+ out.push(vec.map(Number));
41
+ }
42
+ return out.length === n ? out : null;
43
+ }
44
+
45
+ const by20 = <T>(xs: T[]): T[][] => {
46
+ const out: T[][] = [];
47
+ for (let i = 0; i < xs.length; i += 20) out.push(xs.slice(i, i + 20));
48
+ return out;
49
+ };
50
+
51
+ const RERANK_SHAPES = (query: string, texts: string[]) => [
52
+ { query, contexts: texts.map((t) => ({ text: t })) },
53
+ { query, contexts: texts },
54
+ { query, candidates: texts.map((t, i) => ({ id: String(i), text: t })) },
55
+ { query, passages: texts },
56
+ ];
57
+
58
+ export function cfModelRunner(ai: unknown): ModelRunner {
59
+ const A = ai as any;
60
+ return {
61
+ async embed(texts: string[]) {
62
+ const order = embedRequestWinner ? [embedRequestWinner] : Object.keys(EMBED_REQUEST_SHAPES);
63
+ for (const name of order) {
64
+ for (let attempt = 0; attempt < 3; attempt++) {
65
+ try {
66
+ const res = await A.run("@cf/qwen/qwen3-embedding-0.6b", EMBED_REQUEST_SHAPES[name](texts));
67
+ const vecs = extractVecBatch(res, texts.length);
68
+ if (vecs) {
69
+ embedRequestWinner = name;
70
+ return vecs;
71
+ }
72
+ } catch {
73
+ // retry the same shape, then fall through to the next
74
+ }
75
+ await new Promise((r) => setTimeout(r, 250 * (attempt + 1)));
76
+ }
77
+ }
78
+ throw new Error(`embedding failed for all request shapes (${texts.length} text(s))`);
79
+ },
80
+ async rerank(model, query, texts) {
81
+ for (const body of RERANK_SHAPES(query, texts)) {
82
+ try {
83
+ const res: any = await A.run(model, body);
84
+ const raw = res?.data ?? res?.result?.data ?? res?.response;
85
+ if (!Array.isArray(raw)) continue;
86
+ const scores: number[] = new Array(texts.length).fill(NaN);
87
+ raw.forEach((x: any, i: number) => {
88
+ if (typeof x === "number") {
89
+ scores[i] = x;
90
+ return;
91
+ }
92
+ const id = Number(x?.id ?? x?.index ?? i);
93
+ const s = Number(x?.score ?? x?.relevance_score);
94
+ if (Number.isInteger(id) && id >= 0 && id < texts.length && Number.isFinite(s)) scores[id] = s;
95
+ });
96
+ if (scores.some((s) => Number.isFinite(s))) return scores;
97
+ } catch {
98
+ // next shape
99
+ }
100
+ }
101
+ return null;
102
+ },
103
+ async run(req: GenerateRequest): Promise<GenerateResult> {
104
+ const res: any = await (ai as any).run(req.model, {
105
+ messages: req.messages,
106
+ max_tokens: req.maxTokens,
107
+ ...(req.effort ? { reasoning_effort: req.effort } : {}),
108
+ ...(req.temperature != null ? { temperature: req.temperature } : {}),
109
+ ...(req.topP != null ? { top_p: req.topP } : {}),
110
+ ...(req.topK != null ? { top_k: req.topK } : {}),
111
+ ...(req.stream ? { stream: true } : {}),
112
+ });
113
+ if (req.stream && res && typeof res.getReader === "function") return { text: null, stream: res };
114
+ if (req.stream && res?.body && typeof res.body.getReader === "function") return { text: null, stream: res.body };
115
+ const text = typeof res?.response === "string" ? res.response : res?.choices?.[0]?.message?.content;
116
+ return { text: typeof text === "string" ? text : null };
117
+ },
118
+ };
119
+ }
120
+
121
+ export function cfVectorIndex(index: unknown): VectorIndex {
122
+ const ix = index as any;
123
+ return {
124
+ async query(q: VectorQuery) {
125
+ const r = await ix.query(q.vector, {
126
+ topK: q.topK,
127
+ returnMetadata: "all",
128
+ ...(q.filter ? { filter: q.filter } : {}),
129
+ });
130
+ return (r.matches ?? r).map((m: any) => ({ id: m.id, score: m.score, metadata: m.metadata ?? null }));
131
+ },
132
+ async upsert(vectors: VectorUpsert[]) {
133
+ // the adapter law: batches above the provider cap go in chunks
134
+ for (const group of by20(vectors)) await ix.upsert(group);
135
+ },
136
+ async getByIds(ids: string[]) {
137
+ // getByIds above ~20 returns EMPTY silently — chunk, always
138
+ const out: VectorMatch[] = [];
139
+ for (const group of by20(ids)) {
140
+ const got = await ix.getByIds(group);
141
+ out.push(...(got ?? []).map((m: any) => ({ id: m.id, score: 0, metadata: m.metadata ?? null })));
142
+ }
143
+ return out;
144
+ },
145
+ };
146
+ }
147
+
148
+ export function cfKv(ns: unknown): Kv {
149
+ const kv = ns as any;
150
+ return {
151
+ get: (key) => kv.get(key),
152
+ put: (key, value, opts) => kv.put(key, value, opts?.ttlSec ? { expirationTtl: opts.ttlSec } : undefined),
153
+ };
154
+ }
155
+
156
+ export function cfBlobs(bucket: unknown): Blobs {
157
+ const r2 = bucket as any;
158
+ return {
159
+ async get(key) {
160
+ const obj = await r2.get(key);
161
+ if (!obj) return null;
162
+ return { body: obj.body, contentType: obj.httpMetadata?.contentType };
163
+ },
164
+ async put(key, value, contentType) {
165
+ await r2.put(key, value, contentType ? { httpMetadata: { contentType } } : undefined);
166
+ },
167
+ };
168
+ }
169
+
170
+ export function cfRuntime(ctx: { waitUntil(p: Promise<unknown>): void } | undefined): Runtime {
171
+ return {
172
+ defer(fn) {
173
+ if (ctx) ctx.waitUntil(fn());
174
+ else void fn();
175
+ },
176
+ };
177
+ }
@@ -0,0 +1,8 @@
1
+ /** Kv — the hot read path's get/put with TTL. Ephemeral by contract:
2
+ * answer caches and quota counters treat this as disposable state
3
+ * (generation stamps + TTLs), so any adapter that honors the two
4
+ * operations is conformant. */
5
+ export interface Kv {
6
+ get(key: string): Promise<string | null>;
7
+ put(key: string, value: string, opts?: { ttlSec?: number }): Promise<void>;
8
+ }
@@ -0,0 +1,28 @@
1
+ /** ModelRunner — the port over every model call. Adapters translate
2
+ * into provider catalogs and own provider-specific call shaping:
3
+ * request-shape probing (rerank variants), sampling quirks, the
4
+ * effort-budget coupling. Domain modules never see a provider body.
5
+ *
6
+ * multi-SDO / multi-cloud: docs/konneal-extraction-plan.md §5. */
7
+ export interface GenerateRequest {
8
+ model: string;
9
+ messages: unknown[];
10
+ effort?: string;
11
+ maxTokens?: number;
12
+ temperature?: number;
13
+ topP?: number;
14
+ topK?: number;
15
+ stream?: boolean;
16
+ }
17
+ export interface GenerateResult {
18
+ text: string | null;
19
+ stream?: ReadableStream<Uint8Array> | null;
20
+ }
21
+
22
+ export interface ModelRunner {
23
+ embed(texts: string[]): Promise<number[][]>;
24
+ /** rerank with adapter-side request-shape probing; returns one score
25
+ * per text, or null when the provider degraded (caller falls back) */
26
+ rerank(model: string, query: string, texts: string[]): Promise<number[] | null>;
27
+ run(req: GenerateRequest): Promise<GenerateResult>;
28
+ }
@@ -0,0 +1,13 @@
1
+ /** Runtime — background work after the response. The semantic is
2
+ * fire-and-forget best effort: adapters may await or schedule; the
3
+ * domain must never depend on completion for correctness. */
4
+ export interface Runtime {
5
+ defer(fn: () => Promise<void>): void;
6
+ }
7
+
8
+ /** Background — the raw waitUntil surface. ExecutionContext satisfies
9
+ * this structurally; telemetry and other edge utilities depend on the
10
+ * shape, never on the provider type. */
11
+ export interface Background {
12
+ waitUntil(p: Promise<unknown>): void;
13
+ }
@@ -0,0 +1,20 @@
1
+ /** StoreQuery — the transitional relational port. The engine's
2
+ * repositories speak prepared statements (prepare/bind/first/all/
3
+ * batch); the Cloudflare adapter exposes D1 directly, and a Postgres
4
+ * adapter implements the same five methods over its driver. When the
5
+ * repository extraction completes, this narrows to semantic
6
+ * repositories (documents, sessions, memories…) and the SQL dialect
7
+ * disappears from the port entirely. */
8
+ export interface StoreQuery {
9
+ prepare(sql: string): {
10
+ bind(...values: unknown[]): { first<T = unknown>(): Promise<T | null>; all<T = unknown>(): Promise<{ results: T[] }>; run(): Promise<unknown> };
11
+ first<T = unknown>(): Promise<T | null>;
12
+ all<T = unknown>(): Promise<{ results: T[] }>;
13
+ run(): Promise<unknown>;
14
+ };
15
+ batch(statements: unknown[]): Promise<unknown[]>;
16
+ }
17
+
18
+ // the glossary lane's vector probe: the port view of a lane index
19
+ import type { VectorIndex } from "./vector.ts";
20
+ export type { VectorIndex };
@@ -0,0 +1,26 @@
1
+ /** VectorIndex — dense search with metadata filters. The port contract
2
+ * IS the wire law: metadata values are scalars or string arrays
3
+ * (API error 40017 otherwise); getByIds returns empty above its
4
+ * batch cap, so adapters chunk. Lexical search stays ENGINE-side —
5
+ * adapters need only dense+filters, the weakest common denominator. */
6
+ export interface VectorMatch {
7
+ id: string;
8
+ score: number;
9
+ metadata: Record<string, unknown> | null;
10
+ }
11
+ export interface VectorQuery {
12
+ vector: number[];
13
+ topK: number;
14
+ filter?: Record<string, unknown>;
15
+ }
16
+ export interface VectorUpsert {
17
+ id: string;
18
+ values: number[];
19
+ metadata: Record<string, unknown>;
20
+ }
21
+
22
+ export interface VectorIndex {
23
+ query(q: VectorQuery): Promise<VectorMatch[]>;
24
+ upsert(vectors: VectorUpsert[]): Promise<void>;
25
+ getByIds(ids: string[]): Promise<VectorMatch[]>;
26
+ }