@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,238 @@
1
+ // The declared context (TODO.ai-platform/02): the estate assistant
2
+ // panel's opt-in context chips declare what the answer grounds in —
3
+ // the page the user is on, the entity the page carries, a corpus
4
+ // document, or nothing (the default; an ABSENT context field IS
5
+ // "none"). TODO.ai-platform/03 adds the member-only "account" kind:
6
+ // the user's OWN live platform data, read through the RFC 8693
7
+ // delegation (livedata.ts) — never ambient, member-signed-in only.
8
+ // Opt-in means opt-in: no context the caller didn't declare
9
+ // is ever applied, and every ask response echoes what was APPLIED
10
+ // (context_applied) so the panel's honest context line never invents
11
+ // a grounding. A declared context bypasses both answer caches (the
12
+ // answer depends on the declaration, not just the query) and is never
13
+ // written into them — the caches stay context-clean by construction.
14
+
15
+ export interface DeclaredContext {
16
+ kind: "page" | "entity" | "document" | "account";
17
+ /** display label ("this certificate R60/2021-A-EX1-26.01") — echoed
18
+ * into context_applied for the panel's context line */
19
+ label: string;
20
+ /** the publishing page's route (page/entity contexts) */
21
+ route?: string;
22
+ /** the corpus reference to scope retrieval to (entity/document
23
+ * contexts): the URN the SMART models carry as clause provenance
24
+ * (urn:oiml:pub:r:60-1:2021) or the plain docidentifier */
25
+ doc?: string;
26
+ edition?: string;
27
+ }
28
+
29
+ /** The account context's live-read echo (TODO.ai-platform/03): WHEN the
30
+ * live data was read, WHICH stores answered, and how many records the
31
+ * answer could ground in. Present only on a successful read — a failed
32
+ * or refused read reports through `note` instead, never silently. */
33
+ export interface LiveEcho {
34
+ read_at: string;
35
+ stores: string[];
36
+ records: number;
37
+ }
38
+
39
+ export interface AppliedContext {
40
+ kind: "page" | "entity" | "document" | "account" | "none";
41
+ label?: string;
42
+ /** the publication the DECLARED context actually scoped retrieval to
43
+ * ("OIML R 60:2021"); null when the declaration did not scope this
44
+ * answer (see `note` for the honest why) */
45
+ scoped_to?: string | null;
46
+ /** why a doc-carrying declaration did not scope the answer:
47
+ * the corpus does not carry it, or the question named its own
48
+ * publication (the user's explicit words always win over the chip);
49
+ * for the account kind: why the live data was NOT read (the honest
50
+ * degradation — sign in, the window lapsed, the cone refused) */
51
+ note?: "document-not-in-corpus" | "question-document-wins" | "sign-in-required" | "live-window-expired" | "live-unavailable";
52
+ /** the account kind's live-read echo (TODO.ai-platform/03) */
53
+ live?: LiveEcho;
54
+ /** the model plane's bound node (TODO.ai-platform/05): the model-aware
55
+ * chip grounded this answer in the model node itself — its constraint,
56
+ * its provenance, its tests */
57
+ model?: { node_id: string; kind: string; standard: string; clause?: string };
58
+ }
59
+
60
+ export const NO_CONTEXT: AppliedContext = { kind: "none", scoped_to: null };
61
+
62
+ /** Parse + bound the ask body's optional `context` field. Anything
63
+ * malformed degrades to null (no context), never to a 400 — a context
64
+ * the service can't parse is a context it must not apply. */
65
+ export function parseContext(body: any): DeclaredContext | null {
66
+ const c = body?.context;
67
+ if (!c || typeof c !== "object") return null;
68
+ if (c.kind !== "page" && c.kind !== "entity" && c.kind !== "document" && c.kind !== "account") return null;
69
+ const label = typeof c.label === "string" ? c.label.trim().slice(0, 120) : "";
70
+ const route = typeof c.route === "string" && c.route.trim() ? c.route.trim().slice(0, 200) : undefined;
71
+ const doc = typeof c.doc === "string" && c.doc.trim() ? c.doc.trim().slice(0, 80) : undefined;
72
+ const edition = typeof c.edition === "string" && /^\d{4}$/.test(c.edition.trim()) ? c.edition.trim() : undefined;
73
+ return { kind: c.kind, label, ...(route ? { route } : {}), ...(doc ? { doc } : {}), ...(edition ? { edition } : {}) };
74
+ }
75
+
76
+ export interface DocScope {
77
+ /** the Vectorize doc_number filter value (the publication FAMILY —
78
+ * an entity's clause provenance spans parts: R 60-1 requirements,
79
+ * R 60-2 tests) */
80
+ doc_number: string;
81
+ edition?: string;
82
+ /** the canonical label form for the echo + the prompt note */
83
+ label: string;
84
+ }
85
+
86
+ /** Parse the two reference forms the estate speaks: the URN the SMART
87
+ * models carry as clause provenance (urn:oiml:pub:r:60-1:2021) and the
88
+ * plain docidentifier (OIML R 60-1:2021 / R 60). Part designations
89
+ * parse but do not narrow the scope (the family IS the scope). */
90
+ export function parseDocRef(doc: string, edition?: string): DocScope | null {
91
+ const m =
92
+ doc.match(/^urn:oiml:pub:([rdbge]):(\d{1,3})(?:-[0-9A-Za-z]+)?(?::(\d{4}))?$/i) ??
93
+ doc.match(/^(?:OIML\s+)?([RDBGE])\s*(\d{1,3})(?:-[0-9A-Za-z]+)?(?::(\d{4}))?$/i);
94
+ if (!m) return null;
95
+ const type = m[1].toUpperCase();
96
+ const ed = edition ?? m[3] ?? undefined;
97
+ return { doc_number: m[2], ...(ed ? { edition: ed } : {}), label: `OIML ${type} ${m[2]}${ed ? `:${ed}` : ""}` };
98
+ }
99
+
100
+ /** Read the FIRST publication the question's own text names, in the
101
+ * letter+number forms the corpus speaks ("R 76", "r76-1", "OIML B 18",
102
+ * "R 60:2021"). "A document named in the question wins over the declared
103
+ * chip" is only honest when it is the user's own words that win — the
104
+ * understand stage's doc_number is an LLM extraction that also fires on
105
+ * domain priors ("maximum permissible errors" → R 76, no document named
106
+ * — the merged tree's ctx-document-r111 flake) and can miss a naming the
107
+ * text plainly carries; both directions are decided from the TEXT here.
108
+ * A glued single digit is a class/designation ("E2 weights"), never a
109
+ * naming; part designations parse but do not narrow the family. */
110
+ export function namedDocumentIn(query: string): DocScope | null {
111
+ const re = /\b(OIML\s+)?([RDBGE])(\s*)0*(\d{1,3})(?:\s*[-–]\s*\d+)?(?:\s*:\s*(\d{4}))?/gi;
112
+ for (const m of query.matchAll(re)) {
113
+ const [, oimlPrefix, letter, gap, digits, edition] = m;
114
+ if (digits!.length === 1 && !oimlPrefix && !gap) continue;
115
+ const num = String(Number(digits));
116
+ const type = letter!.toUpperCase();
117
+ return { doc_number: num, ...(edition ? { edition } : {}), label: `OIML ${type} ${num}${edition ? `:${edition}` : ""}` };
118
+ }
119
+ return null;
120
+ }
121
+
122
+ /** Resolve the declared document against the publications registry: the
123
+ * family must exist in the corpus, else the scope honestly does not
124
+ * apply (the answer runs on the general corpus and context_applied's
125
+ * scoped_to stays null). A registry READ failure proceeds with the
126
+ * parsed scope — a scoped refusal is more honest than silently
127
+ * widening to the whole corpus. */
128
+ export async function resolveDocScope(env: any, ctx: DeclaredContext): Promise<DocScope | null> {
129
+ if (!ctx.doc) return null;
130
+ const parsed = parseDocRef(ctx.doc, ctx.edition);
131
+ if (!parsed) return null;
132
+ try {
133
+ const type = parsed.label.split(" ")[1];
134
+ const row = await env.DB.prepare("SELECT 1 FROM documents WHERE family = ?1 LIMIT 1").bind(`${type}-${parsed.doc_number}`).first();
135
+ if (!row) return null;
136
+ } catch {
137
+ // registry unreadable — proceed with the parsed scope
138
+ }
139
+ return parsed;
140
+ }
141
+
142
+ export function appliedContext(
143
+ declared: DeclaredContext | null,
144
+ scope: DocScope | null,
145
+ note?: AppliedContext["note"],
146
+ live?: LiveEcho,
147
+ ): AppliedContext {
148
+ if (!declared) return NO_CONTEXT;
149
+ return {
150
+ kind: declared.kind,
151
+ label: declared.label,
152
+ scoped_to: scope ? scope.label : null,
153
+ ...(note ? { note } : {}),
154
+ ...(live ? { live } : {}),
155
+ };
156
+ }
157
+
158
+ /** Validate a context_applied object arriving from a client (the
159
+ * conversations API stores the echo with the message). Bounded and
160
+ * shape-checked; garbage degrades to null (nothing stored). */
161
+ export function parseAppliedContext(v: any): AppliedContext | null {
162
+ if (!v || typeof v !== "object") return null;
163
+ if (v.kind !== "page" && v.kind !== "entity" && v.kind !== "document" && v.kind !== "account" && v.kind !== "none") return null;
164
+ const label = typeof v.label === "string" && v.label.trim() ? v.label.trim().slice(0, 120) : undefined;
165
+ const scoped = typeof v.scoped_to === "string" && v.scoped_to.trim() ? v.scoped_to.trim().slice(0, 80) : null;
166
+ const note =
167
+ v.note === "document-not-in-corpus" || v.note === "question-document-wins" ||
168
+ v.note === "sign-in-required" || v.note === "live-window-expired" || v.note === "live-unavailable"
169
+ ? v.note
170
+ : undefined;
171
+ // The live echo round-trips bounded (the panel's context line reads
172
+ // it on a resumed session — the "when live data was read" honesty).
173
+ const live =
174
+ v.live && typeof v.live === "object" && typeof v.live.read_at === "string" && Array.isArray(v.live.stores) && typeof v.live.records === "number"
175
+ ? { read_at: v.live.read_at.slice(0, 40), stores: v.live.stores.filter((s: any) => typeof s === "string").slice(0, 8), records: Math.min(Math.max(0, v.live.records), 999) }
176
+ : undefined;
177
+ // The model-plane echo round-trips bounded too (TODO.ai-platform/05):
178
+ // a resumed session keeps the "grounded in this model node" honesty.
179
+ const model =
180
+ v.model && typeof v.model === "object" && typeof v.model.node_id === "string" && typeof v.model.kind === "string" && typeof v.model.standard === "string"
181
+ ? {
182
+ node_id: v.model.node_id.slice(0, 120),
183
+ kind: v.model.kind.slice(0, 40),
184
+ standard: v.model.standard.slice(0, 40),
185
+ ...(typeof v.model.clause === "string" && v.model.clause.trim() ? { clause: v.model.clause.slice(0, 120) } : {}),
186
+ }
187
+ : undefined;
188
+ return { kind: v.kind, ...(label ? { label } : {}), scoped_to: scoped, ...(note ? { note } : {}), ...(live ? { live } : {}), ...(model ? { model } : {}) };
189
+ }
190
+
191
+ /** The prompt note the declared context contributes (rides the
192
+ * retrieval-note slot buildMessages already carries). The entity note
193
+ * is explicit about the wave-02 boundary: the entity's OWN DATA is not
194
+ * in scope (that is wave 03's live-data exchange) — the grounding is
195
+ * the governing publication's clauses. */
196
+ export function contextNote(declared: DeclaredContext | null, scope: DocScope | null): string | undefined {
197
+ if (!declared) return undefined;
198
+ if (declared.kind === "account") {
199
+ // The account note depends on the live read's OUTCOME (the records
200
+ // block on success, the honest degradation on a refusal) — the ask
201
+ // handler composes it (TODO.ai-platform/03); never a static claim.
202
+ return undefined;
203
+ }
204
+ if (declared.kind === "page") {
205
+ return `Context note: the user is viewing ${declared.label || "a page"}${declared.route ? ` (${declared.route})` : ""} in the OIML SMART platform. The passages come from the general corpus; frame procedural guidance for that page when relevant.`;
206
+ }
207
+ if (declared.kind === "entity") {
208
+ return scope
209
+ ? `Context note: the user is asking about ${declared.label || "an entity"} — the passages are scoped to ${scope.label}, the publication that governs it. You do NOT have the entity's own data; answer what the publication requires and say when the question needs the record itself.`
210
+ : `Context note: the user is asking about ${declared.label || "an entity"}. You do NOT have the entity's own data; answer from the corpus passages and say when the question needs the record itself.`;
211
+ }
212
+ return scope
213
+ ? `Context note: the user scoped this question to ${scope.label} — the passages come from that publication. If they cannot answer the question, say so instead of drawing on other documents.`
214
+ : `Context note: the user named ${declared.label || declared.doc || "a document"} as context, but it is not in the indexed corpus — answer from the general corpus and say the document was not found.`;
215
+ }
216
+
217
+ /** A minimal knowledge-intent understanding for when the understand
218
+ * call failed but a declared document still scopes retrieval: the
219
+ * whole doc-scoped machinery (the family boost, the typed pin, the
220
+ * grade skip) keys off understanding.doc_number. */
221
+ export function syntheticUnderstanding(scope: DocScope) {
222
+ return {
223
+ intent: "knowledge" as const,
224
+ docidentifier: scope.label,
225
+ doc_number: scope.doc_number,
226
+ edition: scope.edition ?? null,
227
+ language: null,
228
+ process_intent: false,
229
+ term: null,
230
+ defined_terms: [],
231
+ standalone_query: "",
232
+ complexity: "simple" as const,
233
+ query_variants: [],
234
+ sub_queries: [],
235
+ hypothetical_answer: "",
236
+ follow_ups: [],
237
+ };
238
+ }
@@ -0,0 +1,162 @@
1
+ // Member conversation storage (TODO.impl/15): D1-backed, keyed by the
2
+ // session's `sub`. Anonymous users get 401 — their history is local-only
3
+ // by design. The client is the source of truth; the server is append-only
4
+ // storage with last-write-wins on title/updatedAt.
5
+
6
+ import { LIMITS } from "./config";
7
+ import { json, err } from "./lib/http";
8
+ import { parseAppliedContext } from "./context";
9
+
10
+ const ID_RE = /^[a-zA-Z0-9_-]{8,64}$/;
11
+
12
+ interface ConvRow {
13
+ id: string;
14
+ sub: string;
15
+ title: string;
16
+ created_at: string;
17
+ updated_at: string;
18
+ }
19
+
20
+ async function ownedConversation(env: any, sub: string, id: string): Promise<ConvRow | null> {
21
+ if (!ID_RE.test(id)) return null;
22
+ const row = await env.DB.prepare(
23
+ "SELECT id, sub, title, created_at, updated_at FROM conversations WHERE id = ?1 AND sub = ?2",
24
+ )
25
+ .bind(id, sub)
26
+ .first();
27
+ return (row as ConvRow) ?? null;
28
+ }
29
+
30
+ export async function handleConversations(
31
+ env: any,
32
+ sub: string,
33
+ req: Request,
34
+ route: { method: string; id?: string },
35
+ ): Promise<Response> {
36
+ const { method, id } = route;
37
+ const now = new Date().toISOString();
38
+
39
+ if (method === "GET" && !id) {
40
+ const rows: any = await env.DB.prepare(
41
+ "SELECT c.id, c.title, c.updated_at, COUNT(m.id) AS messages FROM conversations c LEFT JOIN messages m ON m.conversation_id = c.id WHERE c.sub = ?1 GROUP BY c.id ORDER BY c.updated_at DESC LIMIT 50",
42
+ )
43
+ .bind(sub)
44
+ .all();
45
+ return json({ conversations: rows.results ?? [] });
46
+ }
47
+
48
+ if (method === "POST" && !id) {
49
+ const body: any = await req.json().catch(() => null);
50
+ const title = typeof body?.title === "string" ? body.title.slice(0, 120) : "";
51
+ const cid = crypto.randomUUID();
52
+ await env.DB.prepare(
53
+ "INSERT INTO conversations (id, sub, title, created_at, updated_at) VALUES (?1,?2,?3,?4,?4)",
54
+ )
55
+ .bind(cid, sub, title, now)
56
+ .run();
57
+ return json({ id: cid }, 201);
58
+ }
59
+
60
+ if (id && method === "GET") {
61
+ const conv = await ownedConversation(env, sub, id);
62
+ if (!conv) return err(404, "not_found", "No such conversation");
63
+ // migration 0009 (the per-answer context mark) applies out of band —
64
+ // until it lands the column doesn't exist; fall back honestly rather
65
+ // than 500 the conversation load
66
+ let msgs: any;
67
+ try {
68
+ msgs = await env.DB.prepare(
69
+ "SELECT id, role, content, citations, model, context_applied, created_at FROM messages WHERE conversation_id = ?1 ORDER BY created_at ASC",
70
+ )
71
+ .bind(id)
72
+ .all();
73
+ } catch {
74
+ msgs = await env.DB.prepare(
75
+ "SELECT id, role, content, citations, model, created_at FROM messages WHERE conversation_id = ?1 ORDER BY created_at ASC",
76
+ )
77
+ .bind(id)
78
+ .all();
79
+ }
80
+ return json({
81
+ conversation: { id: conv.id, title: conv.title, createdAt: conv.created_at, updatedAt: conv.updated_at },
82
+ messages: (msgs.results ?? []).map((m: any) => ({
83
+ id: m.id,
84
+ role: m.role,
85
+ content: m.content,
86
+ citations: m.citations ? JSON.parse(m.citations) : null,
87
+ model: m.model,
88
+ context_applied: m.context_applied ? JSON.parse(m.context_applied) : null,
89
+ at: m.created_at,
90
+ })),
91
+ });
92
+ }
93
+
94
+ if (id && method === "PATCH") {
95
+ const body: any = await req.json().catch(() => null);
96
+ if (typeof body?.title !== "string" || !body.title.trim() || body.title.length > 120) {
97
+ return err(400, "invalid_input", "title (1-120 chars) is required");
98
+ }
99
+ const conv = await ownedConversation(env, sub, id);
100
+ if (!conv) return err(404, "not_found", "No such conversation");
101
+ await env.DB.prepare("UPDATE conversations SET title = ?1, updated_at = ?2 WHERE id = ?3 AND sub = ?4")
102
+ .bind(body.title.trim(), now, id, sub)
103
+ .run();
104
+ return json({ ok: true });
105
+ }
106
+
107
+ if (id && method === "DELETE") {
108
+ const conv = await ownedConversation(env, sub, id);
109
+ if (!conv) return err(404, "not_found", "No such conversation");
110
+ await env.DB.batch([
111
+ env.DB.prepare("DELETE FROM messages WHERE conversation_id = ?1").bind(id),
112
+ env.DB.prepare("DELETE FROM conversations WHERE id = ?1 AND sub = ?2").bind(id, sub),
113
+ ]);
114
+ return json({ ok: true });
115
+ }
116
+
117
+ return err(405, "method_not_allowed", "Unsupported method");
118
+ }
119
+
120
+ export async function handleAppendMessage(env: any, sub: string, req: Request, convId: string): Promise<Response> {
121
+ const body: any = await req.json().catch(() => null);
122
+ const role = body?.role;
123
+ const content = typeof body?.content === "string" ? body.content : "";
124
+ if ((role !== "user" && role !== "assistant") || !content.trim() || content.length > LIMITS.maxOutputTokens * 4) {
125
+ return err(400, "invalid_input", "role (user|assistant) and content are required");
126
+ }
127
+ let citations: string | null = null;
128
+ if (body?.citations != null) {
129
+ if (!Array.isArray(body.citations) || body.citations.length > 16) {
130
+ return err(400, "invalid_input", "citations must be an array of at most 16 items");
131
+ }
132
+ citations = JSON.stringify(body.citations);
133
+ }
134
+ // the per-answer context mark (TODO.ai-platform/02): the panel stores the
135
+ // service's context_applied echo so a resumed conversation keeps its
136
+ // honest context lines; validated + bounded, never trusted blindly
137
+ const applied = parseAppliedContext(body?.context_applied);
138
+ const conv = await ownedConversation(env, sub, convId);
139
+ if (!conv) return err(404, "not_found", "No such conversation");
140
+ const now = new Date().toISOString();
141
+ const mid = crypto.randomUUID();
142
+ try {
143
+ await env.DB.batch([
144
+ env.DB.prepare(
145
+ "INSERT INTO messages (id, conversation_id, role, content, citations, model, context_applied, created_at) VALUES (?1,?2,?3,?4,?5,?6,?7,?8)",
146
+ ).bind(mid, convId, role, content, citations, typeof body?.model === "string" ? body.model.slice(0, 80) : null, applied ? JSON.stringify(applied) : null, now),
147
+ env.DB.prepare("UPDATE conversations SET updated_at = ?1 WHERE id = ?2 AND sub = ?3").bind(now, convId, sub),
148
+ ]);
149
+ } catch (e) {
150
+ // migration 0009 not yet applied — store the message without the mark
151
+ // rather than lose it (the panel's line stays honest: unrecorded
152
+ // contexts render no line on resume)
153
+ if (!String(e).includes("context_applied")) throw e;
154
+ await env.DB.batch([
155
+ env.DB.prepare(
156
+ "INSERT INTO messages (id, conversation_id, role, content, citations, model, created_at) VALUES (?1,?2,?3,?4,?5,?6,?7)",
157
+ ).bind(mid, convId, role, content, citations, typeof body?.model === "string" ? body.model.slice(0, 80) : null, now),
158
+ env.DB.prepare("UPDATE conversations SET updated_at = ?1 WHERE id = ?2 AND sub = ?3").bind(now, convId, sub),
159
+ ]);
160
+ }
161
+ return json({ id: mid }, 201);
162
+ }