@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,8 @@
1
+ import type { Env } from "./env";
2
+ /** Deep-research mode (G10 v1): bounded agentic loop for members —
3
+ * retrieve → sufficiency judge → re-retrieve targeting the gap → answer
4
+ * from the ACCUMULATED evidence. ≤ max_iterations rounds; every
5
+ * iteration's retrieval goes through the same gated pipeline as a
6
+ * normal ask. Workflows (durable, resumable) is the documented upgrade
7
+ * path when runs outgrow a single request. */
8
+ export declare function handleResearch(env: Env, ctx: ExecutionContext, req: Request, session: any): Promise<Response>;
@@ -0,0 +1,4 @@
1
+ import type { Background } from "./ports/runtime.ts";
2
+ import { type ApiKey } from "./lib/http";
3
+ import type { Env } from "./env";
4
+ export declare function handleSearch(env: Env, ctx: Background, req: Request, tier: "anon" | "key" | "member", key: ApiKey | null): Promise<Response>;
@@ -0,0 +1,7 @@
1
+ export interface QueryFilters {
2
+ doctype?: string;
3
+ doc_number?: string;
4
+ edition?: string;
5
+ language?: string;
6
+ }
7
+ export declare function toVectorizeFilter(f: QueryFilters): Record<string, string> | undefined;
@@ -0,0 +1 @@
1
+ export * from "../../shared/session.ts";
@@ -0,0 +1,2 @@
1
+ export declare function handleShareConversation(env: any, ownerSub: string, title: string, messages: any[]): Promise<Response>;
2
+ export declare function handleGetShared(env: any, slug: string): Promise<Response>;
@@ -0,0 +1,27 @@
1
+ import type { Hit } from "./pipeline";
2
+ /** "3.1.2" → [3,1,2]; null for everything else (annex labels, producer
3
+ * UUIDs, overview/family, empty). */
4
+ export declare function parseAnchor(anchor: string | undefined | null): number[] | null;
5
+ /** a is a PROPER ancestor of b ("3.1" ⊳ "3.1.2"). */
6
+ export declare function isAncestorOf(a: number[], b: number[]): boolean;
7
+ /** Document order for dotted numeric anchors ("3" < "3.1" < "3.1.2" < "3.2"). */
8
+ export declare function anchorCompare(a: number[], b: number[]): number;
9
+ /** TreeExpansion-style structural propagation (Eq. 7 of the paper):
10
+ * S(v) = (self + inherited + childAgg)/3, blended into the live score as
11
+ * a spread-scaled adjustment — same idiom as edition steering, so the
12
+ * adjustment can never outrank the cross-encoder's own signal. */
13
+ export declare function structuralPropagation(hits: Hit[]): Hit[];
14
+ /** Position-preserving evidence order (NodeFusion, Algorithm 2): passages
15
+ * of the same publication are fed in document order, publications ordered
16
+ * by their best-ranked member. Document order is the PRODUCER'S ordinal
17
+ * when the metadata carries one (metanorma-document#56) — a sort, never
18
+ * an anchor parse; the anchor compare is the fallback for chunks whose
19
+ * producer doesn't emit ordinals. Structural chunks (overview/family)
20
+ * lead their doc; unnumbered passages follow the numbered ones. */
21
+ export declare function positionOrder(hits: Hit[]): Hit[];
22
+ /** Same-chain near-duplicate collapse: when an ancestor chunk and a
23
+ * descendant chunk of one clause chain carry substantially the same text,
24
+ * the weaker one leaves the window (FABLE keeps the subtree, drops the
25
+ * redundant node). Different-text relatives both stay — a parent clause
26
+ * and a deep sub-clause are usually different content. */
27
+ export declare function ancestorDescendantDedup(hits: Hit[]): Hit[];
@@ -0,0 +1,11 @@
1
+ /** Schema-aware table context composition (TableRAG-class cell
2
+ * selection) over the producer's typed table payload. Pure — the
3
+ * interface is the test surface. */
4
+ /** Schema-aware table context (TableRAG-class cell selection): the
5
+ * producer payload (metadata.table) carries caption/columns/rows; the
6
+ * consumer composes the model-facing serialization — columns whose
7
+ * labels overlap the query, rows whose cells overlap the query or the
8
+ * selected column labels. Full table stays available for rendering;
9
+ * this only shapes the prompt context, and falls back to the stored
10
+ * text when pruning matches nothing (never worse than baseline). */
11
+ export declare function tableContext(meta: any, query: string): string | null;
@@ -0,0 +1,11 @@
1
+ import type { ModelRunner } from "./ports/model.ts";
2
+ import { type QueryUnderstanding } from "./understandContract.ts";
3
+ export type { QueryUnderstanding };
4
+ /** Understand the query with the cheap model. Null = use the regex fallback. */
5
+ export declare function understandQuery(ai: ModelRunner, model: string, query: string, history: Array<{
6
+ role: string;
7
+ content: string;
8
+ }>, entities?: Array<{
9
+ entity: string;
10
+ kind: string;
11
+ }>): Promise<QueryUnderstanding | null>;
@@ -0,0 +1,29 @@
1
+ export interface QueryUnderstanding {
2
+ /** conversational turn (greeting, identity, small talk) vs knowledge seek */
3
+ intent: "conversational" | "knowledge";
4
+ /** normalized document reference, e.g. "OIML R 76-2" — null when none */
5
+ docidentifier: string | null;
6
+ /** base document number for the Vectorize filter, e.g. "60" */
7
+ doc_number: string | null;
8
+ edition?: string | null;
9
+ language?: string | null;
10
+ /** the question is about a process around publications (certify, apply…) */
11
+ process_intent: boolean;
12
+ /** definition-style question whose subject is `term` */
13
+ term: string | null;
14
+ /** corpus-terminology mapping of everyday wording (drift→creep) */
15
+ defined_terms: string[];
16
+ /** self-contained retrieval query: follow-ups folded with context */
17
+ standalone_query: string;
18
+ complexity: "simple" | "complex";
19
+ query_variants: string[];
20
+ sub_queries: string[];
21
+ hypothetical_answer: string;
22
+ /** plausible next questions (conversational UX), in the user's language */
23
+ follow_ups: string[];
24
+ }
25
+ /** Model output text
26
+ * → QueryUnderstanding (or null). Every silent coercion is pinned by
27
+ * tests/understand.test.ts — change the prompt's JSON shape and the
28
+ * test names what moved. */
29
+ export declare function extractJson(text: string): QueryUnderstanding | null;
@@ -0,0 +1,24 @@
1
+ export interface MachineCheck {
2
+ expression: string;
3
+ symbolic: string;
4
+ values: Record<string, number>;
5
+ result: boolean | null;
6
+ }
7
+ export interface Verdict {
8
+ verdict: "pass" | "fail" | "void";
9
+ on_violation?: string;
10
+ violation_meaning?: string;
11
+ missing: string[];
12
+ checks: MachineCheck[];
13
+ }
14
+ export declare function extractChecks(content: unknown): string[];
15
+ export declare function symbolsIn(checks: string[]): string[];
16
+ export declare function extractParams(query: string, symbols: string[]): Record<string, number>;
17
+ export declare function evaluate(content: unknown, query: string): Verdict | null;
18
+ /** The deterministic note the answer model narrates — never recomputes. */
19
+ export declare function verdictNote(v: Verdict, node: {
20
+ node_id: string;
21
+ clause?: {
22
+ urn?: string;
23
+ } | null;
24
+ }): string;
package/docs/API.md ADDED
@@ -0,0 +1,451 @@
1
+ # OIML SMART AI — API Reference
2
+
3
+ *The chatbot API of ai.oimlsmart.org: a conversational contract (ask /
4
+ search / sessions / feedback / UI metadata), not a database surface.
5
+ Machine access to the publication registry is via the [MCP
6
+ server](MCP.md). Authorized users: API keys are issued by the service
7
+ operator (see §1.3).*
8
+
9
+ ---
10
+
11
+ ## 1. Authentication & tiers
12
+
13
+ | Tier | How | Ask quota | Search quota | Model |
14
+ |---|---|---|---|---|
15
+ | **Anonymous** | none (per-IP) | 20/day | 50/day | cost-first lane |
16
+ | **Member** | OIML SMART sign-in (OIDC session cookie) | 300/day | unlimited | member model + ISO/IEC corpus federation |
17
+ | **API key** | `Authorization: Bearer <key>` | per-key (default 2000/day) | per-key | as configured |
18
+
19
+ ### 1.1 Anonymous / member
20
+ Just call the API — the session cookie (set by `/auth/login`) upgrades
21
+ requests automatically. CORS is allowed for `*.oimlsmart.org` origins.
22
+
23
+ ### 1.2 Sign-in flow (members)
24
+ - `GET /auth/login` → redirects to id.oimlsmart.org (OIDC, PKCE)
25
+ - `GET /auth/callback` → sets the session cookie, returns to the app
26
+ - `GET /auth/me` → `{ authenticated, name, email, roles, tier }`
27
+ - `GET|POST /auth/logout` → clears the session
28
+
29
+ ### 1.3 API keys (integrators)
30
+ Keys are `oiml_<hex>`, shown once at creation. Operator issues them:
31
+
32
+ ```
33
+ curl -X POST https://ai.oimlsmart.org/v1/admin/keys \
34
+ -H "authorization: Bearer $ADMIN_TOKEN" \
35
+ -H 'content-type: application/json' \
36
+ -d '{"name": "partner-integration", "day_limit": 5000}'
37
+ ```
38
+ Response includes the plaintext `key` — store it now. List/revoke via
39
+ `GET /v1/admin/keys` (revocation is a D1 update).
40
+
41
+ ### 1.4 The bubble bridge (the assistant embedded in estate properties)
42
+
43
+ The shared chrome's assistant panel (site-shell's `AiBubble`) runs on
44
+ OTHER origins (the platform, www, the minisites). The session cookie
45
+ never crosses origins (`SameSite=Lax`, and the estate bans shared-domain
46
+ cookies — the identity guide's SSO doctrine), so the panel carries the
47
+ session as a Bearer token instead:
48
+
49
+ - `GET /auth/login?mode=bubble&origin=<the page's origin>` — the panel
50
+ opens this in a popup. The origin is validated at flow start
51
+ (`https://oimlsmart.org`, `https://*.oimlsmart.org`, or
52
+ `http://localhost[:port]` for dev) and bound to the OIDC state.
53
+ - `GET /auth/callback` — on a bubble flow, sets the cookie as usual AND
54
+ renders a confirm page ("Continue as <name> to <host>?"); only the
55
+ user's explicit click postMessages `{ type: "oimlsmart-ai-session",
56
+ token, name, expiresAt }` to the validated origin (never `*`).
57
+ - The token is the same HMAC-signed payload the cookie carries (7-day
58
+ TTL). The panel sends it as `Authorization: Bearer <token>`; every
59
+ session-gated route (`/auth/me`, `/api/conversations*`,
60
+ `/api/ask|search` member tier) accepts either form. Stateless — there
61
+ is no server-side revocation; sign-out is the panel discarding the
62
+ token.
63
+
64
+ ## 2. Asking questions
65
+
66
+ ### 2.1 `POST /api/ask` (browser) · `POST /v1/ask` (integrators)
67
+
68
+ Request:
69
+ ```jsonc
70
+ {
71
+ "query": "What is the maximum permissible error for class III?", // required, ≤8000 chars
72
+ "lang": "fr", // optional 2-letter hint; answers follow the question's language
73
+ "stream": true, // SSE (default for /api/ask); false → single JSON
74
+ "fresh": false, // true skips BOTH answer caches (exact + semantic) and
75
+ // regenerates from the live corpus; the refreshed answer re-fills the cache
76
+ "prev": "previous user question", // optional, helps follow-up retrieval
77
+ "history": [ {"role":"user","content":"..."}, {"role":"assistant","content":"..."} ], // last ≤20 turns
78
+ "context": { // optional, the declared context (see §2.1.1)
79
+ "kind": "entity", // "page" | "entity" | "document" (absent = none)
80
+ "label": "this certificate R60/2021-A-EX1-26.01", // ≤120 chars, echoed for display
81
+ "route": "/app/standards/r60/certificates/…", // optional, page/entity
82
+ "doc": "urn:oiml:pub:r:60-1:2021", // optional, entity/document: scopes retrieval
83
+ "edition": "2021" // optional 4-digit override
84
+ }
85
+ }
86
+ ```
87
+
88
+ #### 2.1.1 The declared context (the panel's opt-in chips)
89
+
90
+ The estate's assistant panel (site-shell's `AiBubble`) lets the user pin a
91
+ context per message — the page they're on, the entity the page carries, or
92
+ a corpus document. The service applies it honestly:
93
+
94
+ - `doc` accepts the URN provenance form (`urn:oiml:pub:r:60-1:2021`) or the
95
+ plain docidentifier (`OIML R 60-1:2021` / `R 60`). A resolvable document
96
+ scopes retrieval to the publication FAMILY (the same filter a named
97
+ document in the query gets — an entity's clause provenance spans parts).
98
+ The family must exist in the publications registry, else the answer runs
99
+ on the general corpus and says so.
100
+ - A DECLARED scope is a hard seal: the pipeline's soft-steer widenings (the
101
+ sparse-filter union, the lexical union, the sub-query lanes) are cut back
102
+ to the family before generation, because the panel's context line claims
103
+ the grounding — a citation from outside the declared publication would
104
+ make it a lie. (A document named in the question keeps the soft steer —
105
+ the widen covers sparse publications there.) If nothing survives the
106
+ seal, the answer refuses honestly within the scope.
107
+ - A document named **in the question** always wins over the declared chip —
108
+ the context informs, never overrides the user's explicit words. "Named" is
109
+ read from the question's own text (a letter+number mention like "R 76"),
110
+ never from the understanding stage's inference alone: a topic-prior
111
+ extraction is not the user's words and never steals the chip, and a naming
112
+ the text plainly carries wins even when the extraction misses it.
113
+ - The entity's own data is NOT in scope (the "my account" chip's live-data
114
+ delegation, §2.1.2, is the record-level ground); the grounding is the
115
+ governing publication's clauses.
116
+ - A declared context bypasses both answer caches (the answer depends on the
117
+ declaration, not just the query) and is never written into them.
118
+ - Conversational turns (greetings, identity) never ground in a declared
119
+ context — the echo reports `none`.
120
+
121
+ Every ask response — the SSE `citations`/`done` events and the JSON body —
122
+ echoes what was APPLIED, so the panel's context line never invents a
123
+ grounding:
124
+
125
+ ```jsonc
126
+ "context_applied": {
127
+ "kind": "entity", // the declared kind, or "none"
128
+ "label": "this certificate R60/2021-A-EX1-26.01",
129
+ "scoped_to": "OIML R 60:2021", // null when the declaration did not scope retrieval
130
+ "note": "question-document-wins" // only when a doc-carrying declaration
131
+ } // did not scope: also "document-not-in-corpus"
132
+ ```
133
+
134
+ #### 2.1.2 The "my account" context (TODO.ai-platform/03 — members only)
135
+
136
+ The panel's fourth chip opts the message into the user's OWN live platform
137
+ data — "my account — reads what you can see". The service reads the platform
138
+ EXACTLY as the user, never wider:
139
+
140
+ - The sign-in retains the OP's access token for the session's exchange
141
+ window only (KV keyed by the session token's hash, TTL = the OP token's
142
+ own expiry — never the database, never past the window; sign-out drops
143
+ it deliberately).
144
+ - Each live ask exchanges it at the OP (the RFC 8693 session delegation —
145
+ the identity service's RP guide §9b) for a token scoped
146
+ `<platform>:read`, the account's standing re-judged AT the exchange (a
147
+ role lost mid-session narrows the next answer honestly).
148
+ - The platform reads ride that token; the platform enforces the cones
149
+ exactly as for the user's own browser. The records the answer grounds in
150
+ map 1:1 from the platform's responses — the service can never invent or
151
+ widen one. Org administrators see the exchange + the reads on the audit
152
+ chain (the actor claim names this service), never the conversation.
153
+ - Conversational turns never read the account (a greeting grounds in
154
+ nothing); an account ask whose corpus retrieval is empty still answers
155
+ from the records.
156
+
157
+ The response gains `records` (each `{ store, id, label, url, status?, date?,
158
+ detail? }` — the panel renders the links; the claims about a record name it)
159
+ and the echo carries the live read:
160
+
161
+ ```jsonc
162
+ "context_applied": {
163
+ "kind": "account",
164
+ "label": "my account",
165
+ "scoped_to": null, // the account context never scopes the corpus
166
+ "live": { "read_at": "2026-08-31T09:00:00Z", "stores": ["applications"], "records": 3 }
167
+ }
168
+ ```
169
+
170
+ When the live read cannot honestly happen the answer runs on the corpus and
171
+ the echo says why: `"note": "sign-in-required"` (no member session),
172
+ `"live-window-expired"` (the sign-in's window lapsed — sign in again to
173
+ refresh), `"live-unavailable"` (the exchange refused or the platform was
174
+ unreachable). Never a silent widening, never an invented record.
175
+
176
+ The deployment wiring: `SMART_PLATFORM_API` (the platform's API base) +
177
+ `SMART_PLATFORM_CLIENT_ID` (its client id at the OP — the delegation's
178
+ scope target). Absent, the chip's ask answers `live-unavailable` honestly.
179
+
180
+ #### 2.1.3 The draft acts (TODO.ai-platform/04 — act with confirmation)
181
+
182
+ The assistant can PREPARE an act; the user commits it in the platform's
183
+ real UI. **The service never writes**: the only credential in play is the
184
+ read-scoped delegation (§2.1.2's exchange), and it feeds nothing but the
185
+ role check. The pilot act is the **application prefill**
186
+ (`application_prefill`); the TL dispatch, the review comment and the
187
+ evaluation summary are named follow-ups on the same contract.
188
+
189
+ A draft ask (the user names the act — "draft / prepare / start / submit an
190
+ R 60 application…") bypasses both answer caches and answers with:
191
+
192
+ - `answer` — a deterministic account of what was drafted, what was
193
+ dropped and why, and the boundary: the draft opens in the real form
194
+ with every field editable, and only the user's own click commits it
195
+ (the service never claims a performed act).
196
+ - `draft` — the wire shape below (ABSENT on a refusal).
197
+ - `citations` — the resolved Recommendation the act anchors on.
198
+
199
+ ```jsonc
200
+ "draft": {
201
+ "kind": "draft",
202
+ "act": "application_prefill",
203
+ "version": 1,
204
+ "title": "New OIML R 60:2021 application",
205
+ "prepared_at": "2026-08-31T10:00:00Z",
206
+ "requires_confirmation": true, // ALWAYS — the draft is an input, never a channel
207
+ "fields": {
208
+ "standard_doc": "urn:oiml:pub:r:60:2021", // the estate URN, resolved against the corpus registry
209
+ "standard_label": "OIML R 60:2021",
210
+ "family_designation": "LC series", // only fields the user stated
211
+ "model_designation": "LC-500",
212
+ "description": "…",
213
+ "samples": [{ "serial": "SN-0042", "condition": "NEW" }],
214
+ "scheme": "A" // or "B"
215
+ },
216
+ "dropped": [ // the never-invents account
217
+ { "field": "family_designation", "value": "Phantom-9", "reason": "not stated in your own words" }
218
+ ],
219
+ "notes": ["The technical parameters stay with you: the form derives what the model declares…"]
220
+ }
221
+ ```
222
+
223
+ The honest rules, all eval-gated (the golden suite's `draft-*` legs):
224
+
225
+ - **Never-invents**: the extraction (an LLM pass) only PROPOSES fields;
226
+ every drafted value must trace — through its own source span — to the
227
+ user's own messages, or it lands in `dropped` and the answer names it.
228
+ The instrument model's derivations never ride the draft: the platform's
229
+ form derives them on open, the user confirms each one.
230
+ - **The refusals speak the platform's role vocabulary** (read from the
231
+ exchanged token's `service_roles`, re-judged live at the exchange):
232
+ the anonymous visitor is asked to sign in; a role that cannot perform
233
+ the act (the test-lab operator, the issuing-authority officer, the
234
+ read-only viewer) is told what the account IS and that the act belongs
235
+ to the applicant — no draft. Fail-closed: a role the pilot does not
236
+ recognize refuses. An unresolvable Recommendation (never named by the
237
+ user, or not in the corpus) refuses — the act anchors on a real
238
+ document.
239
+ - **The never-writes must-not**: a crafted prompt ("submit it now with
240
+ my token") yields at most a draft with `requires_confirmation: true` —
241
+ the response NEVER carries a performed-act marker. The platform's
242
+ bearer cone refuses the delegated write class outright either way;
243
+ the commit path is the platform's own form, its own validation, its
244
+ own audit (which marks the act AI-prepared).
245
+
246
+ **SSE**: the `draft` rides the first (`citations`) frame beside
247
+ `context_applied`; `token` frames carry the answer; `done` as usual.
248
+ The draft is ephemeral — the conversations API never persists it (a
249
+ resumed session keeps the honest context line, not a stale draft).
250
+
251
+ #### 2.1.4 The model plane (TODO.ai-platform/05 — model-native grounding)
252
+
253
+ The assistant grounds in the SMART Recommendation MODELS, not only the
254
+ prose corpus. The packages' machine content — the requirements'
255
+ constraints (the machine limits the platform's verdict engine evaluates),
256
+ the applicability rules, the acceptance criteria, the conformance tests,
257
+ the term definitions — indexes **alongside** the prose (Vectorize corpus
258
+ `smart-model` + the D1 `model_nodes` store + the FTS lane). The index
259
+ DERIVES from the primmel packages, the models' single source of truth:
260
+ the smart repo's `derive-model-plane.ts` projects the packages into
261
+ committed bundles (`browser/public/data/model-plane/*.json`, byte-clean-
262
+ guarded by its SSOT gate) and `python -m ingest.cli model-plane` consumes
263
+ them from the sibling smart checkout (`SMART_REPO`). **The freshness is
264
+ gated**: every bundle carries the package's `source_hash`; the committed
265
+ pins (`ingest/model_plane_pins.json`) record what the index derived from,
266
+ and `python -m ingest.cli model-plane --check` (CI: the ingest job) fails
267
+ when a package moved — a package change re-indexes.
268
+
269
+ **The model-aware chips.** "This requirement" on a model surface (the
270
+ platform's requirement / conformance-test / term pages) declares an
271
+ entity context whose label leads with the canonical node id
272
+ (`/req/metrological/mpe — Maximum permissible errors…`). The service
273
+ binds the node EXACTLY (the strict id grammar — never a fuzzy match):
274
+ the declared label's id wins, then a node id the question names; the
275
+ standard comes from the declared or question-named publication only (an
276
+ LLM inference never narrows the bind), and a scope-less id binds only
277
+ when unambiguous across the indexed standards — an ambiguous or
278
+ unindexed id binds NOTHING, honestly. A bound node grounds the answer in
279
+ the node itself: its constraint (quoted verbatim), its applicability,
280
+ its acceptance, its provenance, its tests/preconditions ride the prompt
281
+ as a structured block, the citations lead with the model node
282
+ (`corpus: "smart-model"`), and the echo names the grounding:
283
+
284
+ ```jsonc
285
+ "context_applied": {
286
+ "kind": "entity",
287
+ "label": "this requirement /req/metrological/mpe — Maximum permissible errors on type evaluation",
288
+ "scoped_to": "OIML R 60:2021",
289
+ "model": {
290
+ "node_id": "/req/metrological/mpe",
291
+ "kind": "requirement",
292
+ "standard": "oiml-r60",
293
+ "clause": "urn:oiml:pub:r:60-1:2021#clause-5.3.2"
294
+ }
295
+ }
296
+ ```
297
+
298
+ **The explained verdict.** The applicability/evaluation engines' verdicts
299
+ answer in plain language with the constraint + the clause + the user's
300
+ value ("your class C instrument fails 5.3.2 because the MPE for class C is
301
+ ±…, your declared value is …"). The verdict EXPLANATION is the platform's
302
+ computation — the engine's own trace seam (the smart repo's
303
+ `engine/verdict-explanation.ts`, proven on its golden cases), never a
304
+ constraint this service invents or recomputes. The model plane grounds
305
+ the static half (the requirement's machine limit + its clause + its
306
+ acceptance); a live verdict's trace is the platform's own computation.
307
+
308
+ **The honesty (the clause-drift doctrine's posture).** Where the model
309
+ and the prose disagree, the answer says so and cites both: a model node
310
+ that DECLARES a source discrepancy (the packages' `source_discrepancy`
311
+ annotation — e.g. R 60-1, 5.6.3.1's C_Hmax ≤ 1 v vs R 60-3, 2.1.7's
312
+ C_Hmax ≤ MPE) carries it into the grounding block verbatim and the answer
313
+ must surface it; and every retrieved model-plane passage rides the corpus
314
+ note — where a model passage and a prose passage disagree (a different
315
+ edition's prose included), say so explicitly and cite both.
316
+
317
+ The eval legs (the golden suite's `model-*` cases): the explained-
318
+ verdict shape (the machine limit + the clause + the value in the answer),
319
+ the model-aware chip binding (the echo above), the disagreement posture,
320
+ and the must-not (an unbindable declaration carries NO model echo —
321
+ never an invented grounding).
322
+
323
+ **SSE protocol** (`text/event-stream`, each line `data: {json}`):
324
+ 1. `{"type":"citations","citations":[...],"quota":{"used":n,"limit":m}}` — arrives FIRST so chips render while the answer streams
325
+ 2. `{"type":"token","v":"…"}` — repeated, in order
326
+ 3. `{"type":"done","model":"…","query_hash":"…","follow_ups":["…","…"],"similar":false}`
327
+
328
+ `similar: true` means the answer came from the semantic cache (a
329
+ near-identical recent question). `follow_ups` are suggested next
330
+ questions (present when understanding produced them).
331
+
332
+ **Citation object** (also the shape in the `done`/JSON responses):
333
+ ```jsonc
334
+ {
335
+ "doc_id": "clean:r060/1", "docidentifier": "OIML R 60-1",
336
+ "edition": "2021", "clause_anchor": "4.1.2", "clause_title": "…",
337
+ "snippet": "…", "status": "in-force", // or superseded/withdrawn
338
+ "superseded_by": "OIML R 60-1:2021", // when applicable
339
+ "url": "https://…" // deep link to the rendering when available
340
+ }
341
+ ```
342
+
343
+ **Answer conventions:**
344
+ - Claims cite inline as `[OIML R 60-1:2021 §4.1.2]`
345
+ - Normative values carry quote anchors: `[OIML R 76:2004 §3.2: "the maximum permissible error shall not exceed 0.5e"]` — the quoted phrase is verbatim from the cited passage (mechanically checkable)
346
+ - Out-of-corpus questions return exactly: `I don't have information on this in the indexed OIML publications.` followed by a short redirect
347
+ - Conversational turns (greetings, identity, capability) are answered directly without citations; off-topic SUBJECT questions are still treated as knowledge questions
348
+
349
+ **Non-stream response** (`stream:false`): the same fields as one JSON
350
+ object: `{ answer, citations, model, query_hash, follow_ups, similar?, quota?, cached? }`
351
+ plus `context`: the passages the answer was actually built from
352
+ (`[{doc_id, clause_anchor, text}]`, response-only, never cached) — grounding
353
+ transparency for integrators and the eval battery.
354
+
355
+ ### 2.2 Behavior guarantees
356
+ - Refusals are never cached or served from the semantic cache
357
+ - Conversational/contextual turns are never served from caches
358
+ - Both answer caches are namespaced by INDEX_VERSION (bumped on deploy)
359
+ and a corpus-generation stamp (KV `sys:corpus_gen`, bumped by
360
+ `scripts/invalidate_answer_cache.py` after corpus surgery) — a corpus
361
+ change makes old-generation entries miss, never serve
362
+ - Edition awareness: when the question names a publication, answers are
363
+ steered to the ACTIVE edition (derived publication registry)
364
+ - ISO/IEC corpus: federated for signed-in members only; leakage is
365
+ structurally impossible for anonymous/key tiers
366
+
367
+ ### 2.3 `POST /api/research` (members only)
368
+ ```jsonc
369
+ { "query": "trace the creep and return requirements across R 60-1 and R 76-1", "max_iterations": 3 }
370
+ ```
371
+ Bounded agentic loop (≤3 retrieve→judge→refine iterations) over the same gated retrieval; returns `{ answer, citations, model, blocks?, research: { iterations, passages, elapsed_ms } }`. Not streamed; expect up to ~90s.
372
+
373
+ **Blocks (answer contract v2):** answers may reference typed MKO units as `[[u:<id>]]` tokens inside `text`; the response carries `blocks: [{ unit_id, type: table|formula|figure|term, docidentifier, edition?, payload }]` — producer-validated payloads (MN 116), never model-retyped data. Invalid references are dropped server-side before rendering.
374
+
375
+ ## 3. Retrieval-only
376
+
377
+ ### `POST /api/search` · `POST /v1/search`
378
+ ```jsonc
379
+ { "query": "maximum permissible error", "top_k": 5 } // top_k ≤ 10
380
+ ```
381
+ Response:
382
+ ```jsonc
383
+ { "results": [ { "doc_id","docidentifier","edition","language",
384
+ "clause_anchor","clause_title","status","superseded_by",
385
+ "text","score" } ],
386
+ "filters": { …applied metadata filters… }, "quota": { … } }
387
+ ```
388
+ No generation cost — embedding + hybrid retrieval only.
389
+
390
+ ## 4. UI metadata
391
+
392
+ ### `GET /api/datasets`
393
+ ```jsonc
394
+ { "datasets": [ { "id","label","description","enabled",
395
+ "requires": "an OIML SMART account" } ], // locked corpora
396
+ "suggestions": ["What is R 60?", …] } // starter questions
397
+ ```
398
+
399
+ ## 5. Conversations (members)
400
+
401
+ | Route | Method | Purpose |
402
+ |---|---|---|
403
+ | `/api/conversations` | GET | list synced conversations |
404
+ | `/api/conversations` | POST | create |
405
+ | `/api/conversations/{id}` | GET | one conversation |
406
+ | `/api/conversations/{id}` | PATCH | rename |
407
+ | `/api/conversations/{id}` | DELETE | remove |
408
+ | `/api/conversations/{id}/messages` | POST | append a message |
409
+
410
+ ### Sharing (members)
411
+ - `POST /api/conversations/{id}/share` → `{ "url": "https://…/s/<slug>" }` (10 shares/day)
412
+ - `GET /api/shared/{slug}` → read-only rendered conversation (public link)
413
+
414
+ ## 6. Feedback
415
+
416
+ `POST /api/feedback` `{ "query_hash": "<64-hex>", "rating": 1 | -1 }` —
417
+ thumbs up/down on an answer; logged to D1 for eval.
418
+
419
+ ## 7. Operations (operator token)
420
+
421
+ | Route | Purpose |
422
+ |---|---|
423
+ | `GET /health` | liveness + index version |
424
+ | `POST /v1/admin/keys` / `GET` | API key issue/list |
425
+ | `GET /v1/admin/stats` | 7-day query/spend by model, feedback ratios |
426
+ | `POST /admin/enrich` | contextual-enrichment batches (Bearer ADMIN_TOKEN; internal) |
427
+ | `POST /admin/judge` | RAGAS-style scoring (question/answer/passages; internal eval) |
428
+
429
+ ## 8. Errors
430
+
431
+ | HTTP | code | meaning |
432
+ |---|---|---|
433
+ | 400 | `invalid_input` | bad query/shape |
434
+ | 401 | `unauthorized` | bad key / no session where required |
435
+ | 429 | `quota_exceeded` | daily limit reached |
436
+ | 503 | `generation_disabled` / `retrieval_unavailable` | kill switch / transient — retry |
437
+ | 502 | `generation_failed` | model unavailable — retry |
438
+
439
+ ## 9. Minimal client
440
+
441
+ ```bash
442
+ curl -N https://ai.oimlsmart.org/v1/ask \
443
+ -H "authorization: Bearer $KEY" -H 'content-type: application/json' \
444
+ -d '{"query":"What is OIML R 60?","stream":true}'
445
+ ```
446
+ ```js
447
+ const res = await fetch("/api/ask", {method:"POST",
448
+ headers:{"content-type":"application/json"},
449
+ body: JSON.stringify({query, stream:true})});
450
+ for await (const chunk of res.body) { /* parse data: lines */ }
451
+ ```