@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,184 @@
1
+ # The SOTA mechanisms — the reference
2
+
3
+ This is the canonical description of every mechanism in the serving
4
+ stack, each with its staircase: what the layer below it cannot do. The
5
+ staircase runs from raw text (fewest dimensions) to executable models
6
+ (most) — every step adds structure, and every added structure unlocks
7
+ questions the previous step could not answer.
8
+
9
+ ## The staircase of representations
10
+
11
+ ```
12
+ plain text → clause-anchored documents → typed units (MKO)
13
+ → machine-readable models (Primmel) → execution
14
+ ```
15
+
16
+ | Step | Carries | Can answer | Cannot answer |
17
+ |---|---|---|---|
18
+ | Plain text | words | topical lookup | anything with a citation anchor — half the chunks carry no clause reference at all |
19
+ | Clause-anchored prose | words + location | "where does R 60 address creep?" with § | values inside tables, cross-references, anything typed |
20
+ | Typed units (MKO) | + tables/formulas/figures as objects, cross-references as links | "which test validates this requirement" (a link, not a sentence); figures | computed conformance |
21
+ | Machine models (Primmel) | + constraints, calculations, sequences, terms — machine-checkable | the machine limit itself; test order; instance parameters | executing them in an answer |
22
+ | Execution | the check, evaluated | **"is D_max 26 000 v valid for E_max 30 000 v?" → INVALID, computed** | — (the frontier top) |
23
+
24
+ Measured across the six comparison lanes (witness-graded, 18 rung-tagged
25
+ probes): plain 2/18 · adoc 3/18 · MKO 6/18 · Primmel 13/18 · ablation
26
+ 12/18 · composed 14/18 · full serving 17–18/18.
27
+
28
+ ## The serving mechanisms
29
+
30
+ ### 1. Query understanding (meaning, never strings)
31
+ One small model reads every question: language, named publication,
32
+ edition, process intent, hypothetical answer, query variants,
33
+ decomposition for complex asks, standalone reformulation of follow-ups.
34
+ No keyword rules decide anything — the same judgements apply in every
35
+ language. *Below this step:* fixed keyword routers, which answer
36
+ differently depending on phrasing.
37
+
38
+ ### 2. Hybrid retrieval (dense + lexical, fused)
39
+ The question's vector searches the corpus alongside a full-corpus BM25
40
+ scan; RRF fusion merges them; a terminology graph adds
41
+ concept→document candidates; multiple phrasings and sub-questions run
42
+ as parallel lanes; a hypothetical answer serves as an extra search key
43
+ (HyDE). *Below:* dense-only retrieval misses exact jargon ("n_LC");
44
+ lexical-only misses paraphrase.
45
+
46
+ ### 3. Contextual enrichment (98.7% of non-synthetic chunks)
47
+ Every chunk carries a model-written preamble stating where it sits in
48
+ its document — written once at index time, its quality persists into
49
+ every future retrieval. The preamble lives in the INDEX (plus a 30-day
50
+ KV context cache), never in the build artifacts — a full index restore
51
+ must replay it from the durable record (`scripts/replay_enrichment.py`,
52
+ binding-embed, zero regeneration). Measured the hard way: a restore that
53
+ skipped the replay dropped enrichment to 0% and identifier queries
54
+ failed first ("what is OIML D 29?" — the preambles carry the
55
+ identifier-rich context that ranks them). *Below:* bare chunks retrieve
56
+ on local wording.
57
+
58
+ ### 4. Structural retrieval over the clause tree
59
+ The corpus IS a tree (clause anchors chain parent→child). A hit's score
60
+ blends its ancestors' and descendants' scores — a section whose clauses
61
+ are collectively relevant rises; final evidence is presented in
62
+ document reading order; same-chain near-duplicates collapse. *Below:*
63
+ flat chunk retrieval — no notion of "the section around this clause".
64
+
65
+ ### 5. Section-summary units (multi-granularity)
66
+ Depth-1 clause summaries are retrievable objects: a summary that ranks
67
+ descends to its quotable child clauses and retires itself — citations
68
+ always quote source text. *Below:* one grain per index — either too
69
+ coarse to cite or too fine to orient.
70
+
71
+ ### 6. Vocabulary binding (the nomenclature bridge)
72
+ Everyday words ("my output keeps drifting") rarely match defined terms
73
+ ("span stability"). A terminology index of the corpus's defined
74
+ concepts links the question to candidate terms (dense candidates +
75
+ cross-encoder rerank); the answer model adjudicates and leads with the
76
+ corpus's own term, quoting its definition. *Below:* every representation
77
+ measured fails the everyday-words→defined-term bridge — the one gap
78
+ that is a vocabulary problem, not a structure problem.
79
+
80
+ ### 7. Reranking, edition steering, typed pin
81
+ A cross-encoder orders candidates (a stronger model re-orders hard
82
+ queries); family-relative steering demotes superseded editions while
83
+ keeping them citable when only they carry content — family is the
84
+ doctype+number, and the demotion is full-strength under either signal:
85
+ the successor edition of the SAME identifier is in the pool, or the
86
+ chunk's own status says superseded/unknown against the family's newest
87
+ (the corpus carries the supersession the registry's missing successor
88
+ links do not; in-force documents are exempt so a newer part-2 never
89
+ demotes a current part-1). A document NAMED in the question scopes
90
+ retrieval deterministically — read from the question text, never left
91
+ to the understanding model's per-call mood ("What is OIML D 29?" was a
92
+ coin flip before). Typed objects (tables/formulas/figures) are pinned a
93
+ window slot so the answer contract can reference them — and a query
94
+ that NAMES the artifact type ("figure", "table", "equation") dominates
95
+ unit selection, so the pin lands the object the question is about
96
+ (feeding multimodal generation for figures). *Below:* similarity-only
97
+ ranking answers from stale editions, guesses at doc scope, and never
98
+ surfaces a typed object.
99
+
100
+ ### 8. The answer contract (claims are checkable)
101
+ Inline citations on every claim; normative values quoted verbatim from
102
+ the cited passage; tables/formulas/figures rendered as typed objects
103
+ exactly from the source, never re-typed; a deterministic post-check
104
+ enforces all of it (quoted spans must be contained in served passages;
105
+ a served table's data must carry its object reference) with one
106
+ corrected retry. *Below:* generated prose whose correctness cannot be
107
+ measured.
108
+
109
+ ### 9. The verdict engine (conformance by execution)
110
+ When a question names a machine-checkable model object, the service
111
+ executes it: the question's values bind to the rule's own parameters, a
112
+ deterministic evaluator runs the check (OCL boolean expressions,
113
+ threshold limits), and the verdict — pass, the standard's own violation
114
+ word, or void naming the missing parameters — is attached as data the
115
+ model must present faithfully. Counterfactuals are free: hypothetical
116
+ values are just values. *Below:* quoting the rule and hoping the reader
117
+ does the arithmetic.
118
+
119
+ ### 10. Provable absence
120
+ "Does R 60 constrain packaging?" enumerates the standard's entire model
121
+ plane and returns a certificate — absent, N nodes enumerated, 0
122
+ matches, scope named — never a bare "I found nothing". *Below:* refusal,
123
+ which asserts a search, not a proof.
124
+
125
+ ### 11. Answer verification
126
+ Any answer can be checked against the corpus: verbatim-quote
127
+ containment, object-reference resolution, citation presence
128
+ (deterministic) plus a judged faithfulness score (labeled as judged).
129
+ *Below:* trust me.
130
+
131
+ ### 12. Multimodal figure interpretation
132
+ A pinned figure unit's pixels ride the generation call (R2 asset →
133
+ base64 image part), so the model interprets the producer's drawing, not
134
+ just its stored caption. Two measured invariants: assets must be
135
+ vision-readable — vector-sourced rasters carrying black strokes on
136
+ transparent alpha flatten to a solid black rectangle inside vision
137
+ pipelines (browsers hid the defect by compositing on white;
138
+ `scripts/fix_figure_assets.py` detects and re-uploads white-flattened) —
139
+ and images ride their own short trailing user message, because long
140
+ passage text + image parts in ONE message triggers nondeterministic
141
+ provider 8005s that scale with payload. A third rule guards the blast
142
+ radius: pixels attach only when the question WANTS a drawing (names a
143
+ figure-ish artifact, or the pinned figure sits in the answering
144
+ clause) — and when the multimodal call flakes down to a text-only
145
+ fallback, the attach note drops with the pixels (a text model answers
146
+ the note, not the question — observed in the wild). *Below:* caption-
147
+ only answers that disclaim "the passage does not list" what the drawing
148
+ plainly shows.
149
+
150
+ ### 13. Dataset scope and permission gating
151
+ Three databases (OIML Publications · OIML SMART Models · ISO/IEC
152
+ Conformity Assessment) are selectable per question: the sidebar toggles
153
+ persist a scope, every ask carries it, and the server intersects it
154
+ with what the SESSION may see — the ISO/IEC corpus requires the estate
155
+ permission `ai-preview` (a role code set in id.oimlsmart.org);
156
+ membership alone is not the bar, and the UI's lock is cosmetic. A
157
+ corpus-scope stage runs last of the pool-assembly stages (after the
158
+ lexical union refills the pool post-rerank) so disabling a database
159
+ actually disables it at the hit level. *Below:* one monolithic corpus
160
+ the user cannot narrow, gated (or not) on login alone.
161
+
162
+ ### 14. The measurement gates
163
+ The golden suite (38 cases: doc-level, definitions, table values,
164
+ refusals, filters, verdicts, auth, French) runs ×3 against the live
165
+ service with witness-span containment grading; the annealment battery
166
+ (18 rung-tagged probes) measures capability per representation; EIR
167
+ (cited/retrieved) watches window precision; leakage probes gate every
168
+ promotion; the deploy pipeline guards branch, tests, version bump and
169
+ smoke. Current: golden 37–38/38 (97–100%), annealment 17–18/18 (mode 18/18 — the residuals are two single-case variances: the L5 cross-standard probe's retrieval and the L1 validity date's phrasing).
170
+
171
+ ## The vector adapter (one door)
172
+
173
+ Every chunk crosses one boundary into any index: a pydantic wire schema
174
+ (corpus registry, size caps, anchor sanity) and target gating — an
175
+ index accepts only the corpora that belong to it. The wire schema
176
+ mirrors the serving contract; new producers register corpora in one
177
+ place, never ad hoc.
178
+
179
+ Index equality with the canonical chunk set is an OPERATIONS loop, not
180
+ a hope: reconciliation enumerates the index and deletes strays (upserts
181
+ never delete — measured 49,553 live vs 31,512 canonical before the
182
+ first run); the enrichment replay restores the contexts a full upsert
183
+ overwrites; the asset sweep keeps unit images vision-readable. Each
184
+ step is a script in `scripts/`, run between surgery and gate.
@@ -0,0 +1,77 @@
1
+ # API surface spec
2
+
3
+ The worker's complete HTTP inventory, mirrored 1:1 by the `ROUTES`
4
+ registry in `workers/worker_public/src/index.ts` (dispatched by
5
+ `lib/router.ts`'s `matchRoute`). Adding a route = one registry entry +
6
+ its handler; this document changes with it. Request/response details
7
+ beyond the inventory: [API.md](API.md).
8
+
9
+ ## Conventions
10
+
11
+ - **Dual publication**: `/api/*` is the browser surface (anonymous, or
12
+ member via session cookie / bubble Bearer); `/v1/*` is the integrator
13
+ surface (`Authorization: Bearer oiml_<hex>` API key). Same handler,
14
+ tier derived from the path prefix.
15
+ - **Admin routes** authenticate with `Bearer $ADMIN_TOKEN` (a worker
16
+ secret, not an API key).
17
+ - Every response is JSON unless noted; errors are
18
+ `{ "error": { "code", "message" } }`. CORS for `*.oimlsmart.org`.
19
+ - Trailing slashes are equivalent (segment-exact matching).
20
+
21
+ ## The inventory
22
+
23
+ | Route | Method | Auth | Handler | Contract |
24
+ |---|---|---|---|---|
25
+ | `/`, `/api/`, `/index.html` | GET | none | `serveIndexPage` | The SPA shell from the assets binding, `must-revalidate` so deploys can never serve HTML referencing deleted fingerprinted assets. |
26
+ | `/auth/login` | GET | none | `handleLogin` | OIDC redirect (PKCE); `?mode=bubble&origin=` starts the embedded-panel flow. |
27
+ | `/auth/callback` | GET | none | `handleCallback` | OIDC callback; mints the session cookie; bubble mode postMessages the token to the validated origin. |
28
+ | `/auth/me` | GET | session | `handleMe` | `{ authenticated, name, email, roles, tier }`. |
29
+ | `/auth/logout` | GET, POST | none | `handleLogout` | Clears the session. |
30
+ | `/api/conversations` | * | session | `conversationsRoute` → `handleConversations` | List (GET) / create (POST) / delete (DELETE) the signed-in user's conversations. |
31
+ | `/api/conversations/:id` | * | session (owner) | `conversationsRoute` | One conversation (GET) / delete (DELETE). |
32
+ | `/api/conversations/:id/messages` | POST | session (owner) | `appendMessageRoute` → `handleAppendMessage` | Append a message turn. |
33
+ | `/api/conversations/:id/share` | POST | session (owner) | `shareRoute` → `handleShareConversation` | Publish a conversation to an unlisted share slug. (Was shadowed by the compound conversations branch pre-route-table — the pattern inventory made the collision visible and fixed it.) |
34
+ | `/api/shared/:slug` | GET | none | `getSharedRoute` → `handleGetShared` | Read a shared conversation. |
35
+ | `/api/datasets` | GET | none (session enriches) | `datasetsRoute` | The corpus catalog + starter questions (the UI's empty state — content from the API, never hardcoded in the client). Session-gated datasets carry `requires` (the estate permission, e.g. `the ai-preview permission (id.oimlsmart.org)`) and `enabled` reflects the permission, not just login. |
36
+ | `/api/memories` | * | session | `memoriesRoute` → `handleMemories` | Personalized memory files (#171): GET list / POST create-update (≤10 files × 8k chars) / DELETE by id — every query owner-filtered by the session sub. Selected ids ride `/api/ask` as `memories: [id…]` (≤4 per ask, injected as one bounded trusted-user-facts note; the selection salts the answer cache). |
37
+ | `/health` | GET | none | `healthRoute` | Liveness + deployed `index_version` (the deploy-drift guard reads this). |
38
+ | `/api/ask`, `/v1/ask` | POST | tier | `askRoute` → `handleAsk` | The answer contract: streamed or JSON answer, citations, typed blocks, context echo. Quotas per tier. Optional `datasets: [id…]` narrows the corpora searched (server-intersected with session permissions; an explicitly-empty list is a 400); optional `memories: [id…]` selects the member's memory files to inject. Both selections salt the answer cache — a scoped or memory-flavored answer never serves a plain ask. |
39
+ | `/api/absence`, `/v1/absence` | POST | tier | `absenceRoute` | Provable absence: exhaustive enumeration over a standard's model plane; `{ verdict: absent \| present, enumerated, matches }`. |
40
+ | `/api/verify`, `/v1/verify` | POST | tier | `verifyRoute` | Self-verification battery over a supplied (query, answer): quote anchors, unit references, citations present + judged faithfulness. |
41
+ | `/api/lane`, `/v1/lane` | POST | tier | `laneRoute` | Direct retrieval against a comparison index (dense + lexical fused), bypassing the full pipeline — for the annealment matrix and the /compare demo. |
42
+ | `/api/search`, `/v1/search` | POST | tier | `searchRoute` → `handleSearch` | Passage search (retrieval without generation). |
43
+ | `/api/feedback` | POST | none | `feedbackRoute` | Thumbs up/down keyed by query hash (privacy: hashes only). |
44
+ | `/admin/enrich`, `/v1/admin/enrich` | POST | ADMIN_TOKEN | `handleEnrich` | Contextual enrichment / embed+upsert through the vector adapter (the ONLY wire-stage upsert door). |
45
+ | `/admin/section`, `/v1/admin/section` | POST | ADMIN_TOKEN | `handleSectionUnit` | Build/serve depth-1 section-summary units. |
46
+ | `/admin/vectors` | POST | ADMIN_TOKEN | `handleVectors` | Read/query vectors for ops. |
47
+ | `/admin/caption` | POST | ADMIN_TOKEN | `handleCaption` | Figure captioning (vision lane). |
48
+ | `/assets/*` | GET | none | `unitAssetRoute` | Immutable unit-keyed figure images (`u:<id>.<ext>`) from R2, 1-year immutable cache. |
49
+ | `/api/research`, `/v1/research` | POST | session (member) | `researchRoute` → `handleResearch` | Deep-research dossier loop — members only (research spend stays with humans). |
50
+ | `/admin/judge`, `/v1/admin/judge` | POST | ADMIN_TOKEN | `handleJudge` | LLM-as-judge scoring (promotion gates). |
51
+ | `/v1/admin/keys` | POST | ADMIN_TOKEN | `handleCreateKey` | Issue an API key (`oiml_<hex>`, shown once). |
52
+ | `/v1/admin/keys` | GET | ADMIN_TOKEN | `handleListKeys` | List keys (no secrets). |
53
+ | `/v1/admin/stats` | GET | ADMIN_TOKEN | `adminStatsRoute` | 7-day telemetry: queries/day/tier, spend by model, feedback, error rate; prunes >90d rows. |
54
+ | `/docs/:slug.html`, `/docs/:slug.anchors.json` | GET | public | `docsRoute` | Rendered publication documents (metanorma-mirror layer 1) served from R2 under `docs/`, immutable cache; the anchors map (clause number → heading anchor id) powers citation deep links. Public OIML content only. |
55
+ | `/admin/enrich` | POST | ADMIN_TOKEN | `handleEnrich` | modes: default (context+embed+upsert in place), `context` (generate only, KV-cached), `ab` (generate at an explicit effort with NO side effects — the experiment lane; accepts an admin-gated prompt override for judged comparisons). |
56
+ | anything else | any | — | — | `404 not_found`. |
57
+
58
+ ## Request semantics worth naming
59
+
60
+ - **`effort` on ask** (`/api/ask`, `/v1/ask`): `"low"` (default, 1 quota unit) or `"medium"` (member lane, raises reasoning effort AND the output budget — `effortBudget()`; 2 quota units). Effort changes the answer, so it salts both answer caches alongside the datasets/memory selections (`requestEffort`/`answerEffort` in config.ts).
61
+ - **`max_iterations` on research**: 1–3, clamped server-side.
62
+
63
+ ## Dispatch semantics
64
+
65
+ `fetch` = OPTIONS preflight (204) → `matchRoute(ROUTES, method, path)` →
66
+ handler with `{ env, req, ctx, url, path, params }` → 404. Patterns are
67
+ segment-exact with `:param` capture and a trailing `*` wildcard
68
+ (`/assets/*`); entry order is irrelevant because no pattern overlaps
69
+ another. The `/api` and `/v1` twins deliberately share one handler so
70
+ the tier split can never diverge between the two publications.
71
+
72
+ ## Isolation invariants (binding lint, `npm run lint:wrangler`)
73
+
74
+ No route in this worker may reference the internal tier: no ISO index
75
+ binding, no internal R2, no internal tokens. Federation happens through
76
+ the `INTERNAL_SERVICE` binding inside the ask pipeline only, gated by a
77
+ member session. The lint fails CI on any violation.
@@ -0,0 +1,126 @@
1
+ # Pipeline stage contracts
2
+
3
+ The retrieval pipeline (`worker_public`) is a fixed-sequence registry of
4
+ stages (`src/stages/index.ts`), each a self-contained module
5
+ implementing the `Stage` interface (`src/stages/types.ts`). `retrieve()`
6
+ (`src/pipeline.ts`) is composition only: it builds the context (the
7
+ parallel embed/lexical prelude), runs the registry, and projects the
8
+ result.
9
+
10
+ ## The contract vocabulary
11
+
12
+ - **Context** (`PipelineContext`) — the mutable state every stage reads
13
+ and writes: `env`, `query` (the user's original wording), `rq` (the
14
+ retrieval query), `u` (query understanding, nullable), `filters` /
15
+ `filter` (the dense doc/edition scope), `vector`, `lexicalHits`,
16
+ `matches` (the candidate pool, Vectorize-match shape), `hits` (the
17
+ ranked pool), `finalHits` (the answer window), `glossary`, `opts`.
18
+ - **Guard** (`when`) — a pure read of the context; absent = always runs.
19
+ - **Failure mode** — `additive`: a throw is logged, the pipeline
20
+ continues with the context as the previous stage left it (the lane's
21
+ results were not written). `blocking` (default): the throw propagates
22
+ to the caller of `retrieve()`.
23
+
24
+ Adding a stage = one file in `src/stages/` + one registry entry. No
25
+ edits to existing stages, the runner, or `retrieve()`. A stage that
26
+ needs a new context field declares it on `PipelineContext` (the field's
27
+ owner is the stage that writes it).
28
+
29
+ ## Registry order and invariants
30
+
31
+ Order is load-bearing. The phase structure:
32
+
33
+ ```
34
+ candidate lanes → pool open → pool-level merges → refinement → window assembly
35
+ ```
36
+
37
+ | # | Stage | Guard | Failure | Writes | Contract |
38
+ |---|-------|-------|---------|--------|----------|
39
+ | 1 | `dense` | — | blocking | `matches`, may mutate `filters.edition` | The primary Vectorize query. Three shapes: optimistic reuse (identical query + no filter only), filtered (with guessed-edition-pin drop and sparse-filter widen — the widen APPENDS unfiltered hits behind the filtered set), plain. Never unions optimistic hits into a diverged query. |
40
+ | 2 | `hyde` | `u.hypothetical_answer && !filter` | additive | appends `matches` | Hypothetical-answer vector search; candidates enter at `hydeDiscount`. |
41
+ | 3 | `glossary` | `env.GLOSSARY && vector` | additive | `glossary` | Vocabulary link: dense glossary candidates + cross-encoder rerank; top-3 distinct concepts. MUST run before the concept-graph and steering stages — they consume `glossary`. |
42
+ | 4 | `concept-graph` | `glossary.length && env.DB && vector` | additive | appends `matches` | Linked terms → defining doc numbers (D1, parallel per-term) → metadata-filtered dense merge at `conceptGraphDiscount`. |
43
+ | 5 | `graph-lane` | `opts.graphDocNumbers && vector` | additive | appends `matches` | Caller-resolved family/successor doc numbers merged at `graphLaneDiscount`. |
44
+ | 6 | `multi-query` | `u.query_variants` | blocking (per-variant catches inside) | REPLACES `matches` | RRF-fuses the primary ranking with each variant's (k=60, top `retrieveK`). |
45
+ | 7 | `sub-query` | `u.complexity === "complex" && u.sub_queries` | blocking (per-sub catches inside) | appends `matches` | Complementary-perspective union at `subQueryDiscount` (no RRF). |
46
+ | 8 | `pool-open` | — | blocking | `hits` | Converts the candidate pool to the `Hit` shape. Last writer of `matches`; every later stage operates on `hits`. |
47
+ | 9 | `lexical-union` | `lexicalHits.length` | blocking | appends `hits` | Full-corpus BM25 hits dense missed (dense metadata wins on id collision). |
48
+ | 10 | `federate` | `opts.federate` | additive (callback catches transport) | appends `hits` | Internal ISO/IEC passages at `federateDiscount`. |
49
+ | 11 | `seal` | `opts.sealScope` | blocking | filters `hits` | The declared context's hard cut: nothing outside the declared family reaches rerank. (The lexical lane is sealed at the SOURCE, in the prelude — not here.) |
50
+ | 12 | `overview-demote` | — | blocking | mutates `hits[].score` | Overview boilerplate demotion (`overviewDemotion`). |
51
+ | 13 | `family-boost` | boost only under `filter.doc_number`; sort always | blocking | mutates `hits[].score`, sorts | Family chunks decisively boosted for doc-scoped queries; the sort establishes the rerank-failure fallback order. |
52
+ | 14 | `rerank` | `hits.length > 1` | additive | `hits[].rerank_score`, sorts, family pin | Cross-encoder scores; vector order is the designed fallback. Post-rerank family pin for doc-scoped queries. |
53
+ | 15 | `lexical-rrf` | `hits.length > 1 && lexicalHits.length` | blocking | REPLACES `hits` order | RRF fusion with the full-corpus lexical ranking. Runs even when rerank failed (additive semantics preserve this). |
54
+ | 16 | `std-ref-nudge` | query names ISO/IEC/ASTM/EN | blocking | `hits[].rerank_score`, sorts | Standard-reference nudge: chunks CARRYING such a citation get `stdRefNudgeSpread × spread` — the citing clause is the answer to "which standard does X invoke", and generic family prose otherwise fills the window (l5a measured). |
55
+ | 17 | `edition-cover` | `!filters.edition && hits.length > 1` | additive | appends `hits` | Registry-driven cover: when a pool holds only stale editions of a document whose ACTIVE edition the documents registry knows, fetch the current edition's chunks and add at `editionCoverDiscount` — steering needs the successor present to demote. |
56
+ | 18 | `corpus-scope` | `opts.datasetScope` | blocking | filters `hits` | Dataset scope (the sidebar toggles): drops hits whose corpus the request excludes. LAST of the pool-assembly stages — the lexical union above refills the pool after rerank, so filtering earlier let excluded corpora back in. Corpora the toggle model doesn't name pass untouched. |
57
+ | 19 | `term-nudge` | `u.term` | blocking | `hits[].rerank_score`, sorts | Clause whose head IS the asked term gets `termNudgeSpread × spread` (decisive). |
58
+ | 20 | `concept-steer` | `glossary.length && hits.length > 1` | blocking | `hits[].rerank_score`, sorts | Vocabulary-link families boosted (`conceptSteerSpread`). |
59
+ | 21 | `edition-steer` | `!filters.edition && hits.length > 1` | blocking | `hits[].rerank_score`, sorts | Cross-pub recency boost + family-relative superseded-edition demotion (spread-scaled). |
60
+ | 22 | `structural-propagate` | — | blocking | REPLACES `hits` | FABLE TreeExpansion: score blends along the clause tree. |
61
+ | 23 | `diversity` | — | blocking | `finalHits` (from `hits`) | Per-publication caps (1 overview / 2–3 clauses; global overview cap 2/6); window cut to `rerankKeep`. FIRST writer of `finalHits`. |
62
+ | 21 | `typed-pin` | pin families resolvable | blocking (inner parent-fetch additive) | `finalHits` | Answer-contract v2: one typed unit guaranteed a slot (+ small-to-big parent fetch at `smallToBigDiscount`). |
63
+ | 22 | `section-descent` | a ranked depth-1 summary has children | additive | `finalHits` | Summary node → top child clauses at `sectionDescentDiscount`; the summary retires when children answer. |
64
+ | 23 | `dedup` | — | blocking | `finalHits` | FABLE ancestor-descendant same-chain collapse (≥0.5 text overlap). |
65
+ | 24 | `window-floor` | — | blocking | filters `finalHits` | Evidence-budget cut at `windowFloorFraction` of top; typed/family/unscored exempt; never fewer than two. |
66
+
67
+ ## Ordering dependencies (why the order is what it is)
68
+
69
+ - **Lanes before the pool opens** (1–7): every candidate lane competes in
70
+ one pool; the pool is frozen at `pool-open`.
71
+ - **`glossary` before `concept-graph`/`concept-steer`/`typed-pin`**: all
72
+ three consume the vocabulary link.
73
+ - **`dense` before any lane that merges**: lanes are additive to the
74
+ primary ranking, never replacements (except `multi-query`, which
75
+ REPLACES `matches` — deliberate: RRF-fused variants subsume the
76
+ primary ranking).
77
+ - **`seal` before `overview-demote`/`rerank`**: the declared context is
78
+ a hard scope, not a preference — steering and reranking happen WITHIN
79
+ it.
80
+ - **`rerank` before every steering stage** (16–18): steering is
81
+ spread-scaled over rerank scores; steering before rerank would be
82
+ erased by the re-sort.
83
+ - **`structural-propagate` after steering, before `diversity`**:
84
+ propagation re-scores the full ranked pool; diversity then reads the
85
+ final order.
86
+ - **`diversity` is the FIRST writer of `finalHits`**: the window
87
+ assembly stages (21–24) operate only on the window.
88
+ - **`window-floor` LAST**: it is the terminal budget cut; anything
89
+ appended after it would escape the evidence-budget principle.
90
+
91
+ ## Prefetch semantics (concurrent lane I/O)
92
+
93
+ A stage may declare `prefetch(c)`: kick its INDEPENDENT I/O off into
94
+ `c.lane[stage.name]` (a promise bag the stage owns). The runner invokes
95
+ every stage's prefetch — guard-checked — BEFORE running any stage, so
96
+ the independent lanes (hyde, glossary, graph-lane, multi-query,
97
+ sub-query) overlap with each other and with the dense lane instead of
98
+ serializing: ~5 summed round trips become ~1. `run()` awaits its own
99
+ promise and merges.
100
+
101
+ The invariants:
102
+
103
+ - **Only pre-pipeline state** — prefetch may read `u`, `vector`, `opts`,
104
+ never another stage's output. concept-graph deliberately does NOT
105
+ prefetch (its D1 lookup consumes glossary's results).
106
+ - **Merge order is registry order** — concurrency changes when I/O
107
+ completes, never when merges apply. Retrieval determinism is
108
+ untouched (the annealment gate re-verifies this).
109
+ - **Failure semantics unchanged** — a rejected prefetch promise throws
110
+ at the await inside `run()`, where the stage's failure mode applies.
111
+
112
+ ## The prelude (not stages, deliberately)
113
+
114
+ `retrieve()` resolves the query fold, runs the dense embed and the
115
+ full-corpus lexical prefilter **in parallel**, and seal-filters the
116
+ lexical lane at the source. This parallelism is load-bearing (Option C:
117
+ the optimistic vector may already be resolved; embed and lexical must
118
+ not serialize) — it stays in the composition layer, not in stages, so
119
+ no stage boundary can introduce a serial round-trip on the hot path.
120
+
121
+ ## Verification
122
+
123
+ - `tests/pipeline.test.ts` — the registry composition and each stage's
124
+ invariant over an in-memory fixture environment (no network).
125
+ - `tests/golden/` ×3 + annealment ×6 — the live gates (every threshold
126
+ change re-runs them; see `config.ts` THRESHOLDS docstrings).
@@ -0,0 +1,88 @@
1
+ # The vector adapter — producer formats → Vectorize, through one door
2
+
3
+ `ingest/vector_adapter.py` is the single boundary every chunk crosses to
4
+ become a vector. Producer projectors (`ingest/primmel.py`, `ingest/mko.py`,
5
+ `ingest/parse.py`) turn producer formats into dicts; the adapter turns
6
+ those dicts into the wire shape — validated, target-checked, and
7
+ schema-locked.
8
+
9
+ ## Why one door (the failure modes it forecloses)
10
+
11
+ Hand-rolled per-script metadata — no tie between a chunk and its TARGET
12
+ index — fails in four characteristic ways, each of which the adapter
13
+ makes structurally impossible:
14
+
15
+ - **cross-index contamination** — a chunk written to an index its corpus
16
+ does not belong to (target gating refuses at adapt time and payload
17
+ time);
18
+ - **corpus vocabulary drift** — the same corpus under different names,
19
+ or edition values in inconsistent forms (the registry and the schema
20
+ are the single source of truth);
21
+ - **unciteable anchors** — producer UUID anchors reaching citations
22
+ (stripped at build time; serving hides any that remain as defense in
23
+ depth);
24
+ - **wire-time size failures** — metadata over the index limit,
25
+ discovered only at the upsert call (validated before any wire call).
26
+
27
+ ## The contract
28
+
29
+ ### ChunkMetaModel — the wire schema
30
+
31
+ Mirrors the serving contract (`workers/worker_public/src/pipeline.ts`
32
+ `ChunkMeta`). Pydantic (the ecosystem serialization rule: framework, not
33
+ hand-rolled `to_json`). `extra="forbid"` — an unknown field from a
34
+ producer is a SCHEMA CHANGE, decided here, never an accident at the wire.
35
+
36
+ Fields: `doc_id, docidentifier, doctype, doc_number, edition, language,
37
+ clause_anchor, clause_title, tier, corpus, text_ref, status,
38
+ superseded_by, unit_id, block, unit_hash, producer, source_lane,
39
+ linked_clause, linked_document, section_summary, child_anchors, ctx,
40
+ chunk_text`.
41
+
42
+ Validators (build-time, before any wire call):
43
+
44
+ - **corpus registry** — the value must be registered (`PRODUCTION_CORPORA`
45
+ ∪ `LANE_CORPORA`). New corpora are added HERE, never ad hoc in a script.
46
+ - **clause_anchor sanity** — producer UUID anchors are stripped to `""`
47
+ (they are unciteable; serving treats them as garbage — this stops them
48
+ at the source).
49
+ - **chunk_text cap** — 2,800 chars (the 40016 lesson).
50
+ - **wire size** — serialized metadata ≤ 9,000 bytes (Vectorize hard limit
51
+ 10KB).
52
+
53
+ ### Target gating — the structural guard
54
+
55
+ `TARGET_CORPORA` maps every index to the corpora it may hold:
56
+
57
+ | Target | Legal corpora |
58
+ |---|---|
59
+ | `production` (`idx_oiml_public_v2`) | oiml, dirty, clean, synthetic, smart-model |
60
+ | `exp_plain` / `exp_adoc` / `exp_mko` / `exp_composed` | the same-named lane corpus |
61
+ | `primmel` | primmel |
62
+ | `primmel_flat` | primmel (the ablation indexes the same corpus, unlinked) |
63
+
64
+ `normalize_chunk(raw, target=…)` refuses a chunk foreign to its target at
65
+ ADAPT time; `chunk.upsert(vector, target)` refuses again at payload-build
66
+ time. A lane corpus can no longer enter production through any code path
67
+ that builds payloads through the adapter.
68
+
69
+ ### Producer remaps live here, not in scripts
70
+
71
+ The production MKO flow remaps `corpus mko → oiml` (+`producer: mko`,
72
+ `tier: curated`) inside `normalize_chunk(target="production")`; the lane
73
+ MKO flow keeps its `exp_mko` namespace. Previously this remap lived in
74
+ `ingest/enrich.py` — buried in a driver, duplicated by hand elsewhere.
75
+
76
+ ## Serving mirror
77
+
78
+ The TypeScript `ChunkMeta` interface in `pipeline.ts` is the serving-side
79
+ read of the same contract (plus its own optional fields). Change either
80
+ side deliberately: adapter schema → wire shape → serving interface.
81
+
82
+ ## Call sites
83
+
84
+ - `scripts/index_comparison_lanes.py` — every lane upsert builds its
85
+ payload through `normalize_chunk(...).upsert(..., target=ADAPTER_TARGET[lane])`.
86
+ - Future producers (and the metanorma-ai deployment package's
87
+ document→vectors pipeline component) adopt the same door: project to
88
+ dicts, adapt through this module, never hand-roll a metadata dict again.
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@konneal/engine",
3
+ "workspaces": [
4
+ "workers/*"
5
+ ],
6
+ "scripts": {
7
+ "typecheck": "tsc -p workers/worker_public",
8
+ "deploy:public": "npm run deploy -w workers/worker_public",
9
+ "dev:public": "npm run dev -w workers/worker_public",
10
+ "deploy": "./scripts/deploy.sh",
11
+ "test:golden": "node scripts/eval.mjs",
12
+ "test:e2e": "node tests/e2e.mjs",
13
+ "lint:wrangler": "python3 scripts/lint_wrangler.py",
14
+ "test:ui": "node tests/ui.mjs",
15
+ "test:bridge": "node --test tests/bridge.test.mjs",
16
+ "test:units": "node --test --experimental-strip-types tests/*.test.ts",
17
+ "build": "rm -rf dist dist-types && esbuild workers/worker_public/src/index.ts workers/worker_public/src/config.ts workers/worker_public/src/refusal.ts workers/worker_public/src/profile.ts workers/worker_public/src/requestScope.ts --bundle --format=esm --splitting --platform=neutral --outdir=dist --loader:.md=text && tsc -p tsconfig.build.json && cp dist-types/worker_public/src/*.d.ts dist/ && mkdir -p dist/prompts && cp workers/worker_public/prompts/*.md dist/prompts/",
18
+ "prepare": "npm run build",
19
+ "lint:ports": "node scripts/lint-ports.mjs"
20
+ },
21
+ "devDependencies": {
22
+ "@cloudflare/workers-types": "^5.20260911.1",
23
+ "esbuild": "^0.28.2",
24
+ "jsdom": "^29.1.1",
25
+ "playwright": "^1.62.1",
26
+ "yaml": "^2.9.1"
27
+ },
28
+ "dependencies": {
29
+ "@astrojs/markdown-satteri": "^0.4.1",
30
+ "@astrojs/mdx": "^4.3.14"
31
+ },
32
+ "version": "0.1.0",
33
+ "description": "The Konneal engine: the publisher-agnostic build pipeline and API plane for standards intelligence (retrieval, answer contract, verdicts, evaluation).",
34
+ "license": "BSD-3-Clause",
35
+ "type": "module",
36
+ "main": "./dist/index.js",
37
+ "exports": {
38
+ ".": {
39
+ "types": "./dist/index.d.ts",
40
+ "default": "./dist/index.js"
41
+ },
42
+ "./setProfile": {
43
+ "types": "./dist/profile.d.ts",
44
+ "default": "./dist/profile.js"
45
+ },
46
+ "./config": {
47
+ "types": "./dist/config.d.ts",
48
+ "default": "./dist/config.js"
49
+ },
50
+ "./refusal": {
51
+ "types": "./dist/refusal.d.ts",
52
+ "default": "./dist/refusal.js"
53
+ },
54
+ "./requestScope": {
55
+ "types": "./dist/requestScope.d.ts",
56
+ "default": "./dist/requestScope.js"
57
+ }
58
+ },
59
+ "files": [
60
+ "dist",
61
+ "workers",
62
+ "docs",
63
+ "profile",
64
+ "scripts/gen_profile.mjs",
65
+ "README.md"
66
+ ],
67
+ "publishConfig": {
68
+ "access": "public"
69
+ }
70
+ }
@@ -0,0 +1,5 @@
1
+ production: [pub, dirty, clean, synthetic, model]
2
+ lanes:
3
+ exp_a: [exp_a]
4
+ exp_b: [exp_b]
5
+ glossary: [glossary]
@@ -0,0 +1,14 @@
1
+ datasets:
2
+ - id: pub
3
+ label: Fixture Publications
4
+ description: The fixture publisher's corpus
5
+ corpora: [pub, dirty, clean, synthetic]
6
+ note: >-
7
+ Some passages come from the fixture corpus — cite them the same
8
+ way as every other passage.
9
+ - id: internal
10
+ label: Internal corpus
11
+ description: An access-restricted corpus proving the permission gate
12
+ session: true
13
+ permission: preview
14
+ corpora: [internal]
@@ -0,0 +1,5 @@
1
+ vars:
2
+ assistant_identity: >-
3
+ the fixture assistant — a public service answering questions about
4
+ the fixture publisher's documents
5
+ refusal_sentence: I don't have information on this in the indexed fixture documents.
@@ -0,0 +1,17 @@
1
+ # The engine's fixture profile — the reference matrix starts here. A
2
+ # real deployment (e.g. oimlsmart/ai) carries its own publisher facts;
3
+ # this fixture exists so the engine's tests and CI run against
4
+ # declared data, never hardcoded ones.
5
+ id: fixture
6
+ name: Fixture
7
+ full_name: The Fixture Publisher
8
+ product_name: Fixture Answers
9
+ description: >-
10
+ A minimal publisher profile exercising every declared surface: an
11
+ open dataset, a permission-gated dataset, production and lane
12
+ corpora, prompt vars and retrieval vocabulary.
13
+ domains:
14
+ public: fixture.example.org
15
+ identity:
16
+ issuer: https://id.fixture.example.org
17
+ codec: plain-slug
@@ -0,0 +1 @@
1
+ process_expansion: " fixture certification system framework application evaluation"
@@ -0,0 +1,5 @@
1
+ corpora:
2
+ clean: { repo: fixtures/corpus, note: the engine's fixture corpus }
3
+ bibliography: {}
4
+ terminology: {}
5
+ models: {}
@@ -0,0 +1,7 @@
1
+ suggestions:
2
+ - What is in the fixture corpus?
3
+ - Which documents does the fixture publisher issue?
4
+ models_disclosure:
5
+ - { role: Answers, model: fixture-answer-model }
6
+ smoke:
7
+ - { label: sanity, query: What is in the fixture corpus?, expect: fixture }