@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,101 @@
1
+ // GENERATED from profile/*.yaml — regenerate: node scripts/gen_profile.mjs
2
+ // (never edit; the drift test compares this file to the sources)
3
+ export const PROFILE = {
4
+ "publisher": {
5
+ "id": "fixture",
6
+ "name": "Fixture",
7
+ "full_name": "The Fixture Publisher",
8
+ "product_name": "Fixture Answers",
9
+ "description": "A minimal publisher profile exercising every declared surface: an open dataset, a permission-gated dataset, production and lane corpora, prompt vars and retrieval vocabulary.",
10
+ "domains": {
11
+ "public": "fixture.example.org"
12
+ },
13
+ "identity": {
14
+ "issuer": "https://id.fixture.example.org"
15
+ },
16
+ "codec": "plain-slug"
17
+ },
18
+ "datasets": [
19
+ {
20
+ "id": "pub",
21
+ "label": "Fixture Publications",
22
+ "description": "The fixture publisher's corpus",
23
+ "corpora": [
24
+ "pub",
25
+ "dirty",
26
+ "clean",
27
+ "synthetic"
28
+ ],
29
+ "note": "Some passages come from the fixture corpus — cite them the same way as every other passage."
30
+ },
31
+ {
32
+ "id": "internal",
33
+ "label": "Internal corpus",
34
+ "description": "An access-restricted corpus proving the permission gate",
35
+ "session": true,
36
+ "permission": "preview",
37
+ "corpora": [
38
+ "internal"
39
+ ]
40
+ }
41
+ ],
42
+ "corpora": {
43
+ "production": [
44
+ "pub",
45
+ "dirty",
46
+ "clean",
47
+ "synthetic",
48
+ "model"
49
+ ],
50
+ "lanes": {
51
+ "exp_a": [
52
+ "exp_a"
53
+ ],
54
+ "exp_b": [
55
+ "exp_b"
56
+ ],
57
+ "glossary": [
58
+ "glossary"
59
+ ]
60
+ }
61
+ },
62
+ "sources": {
63
+ "corpora": {
64
+ "clean": {
65
+ "repo": "fixtures/corpus",
66
+ "note": "the engine's fixture corpus"
67
+ }
68
+ },
69
+ "bibliography": {},
70
+ "terminology": {},
71
+ "models": {}
72
+ },
73
+ "ui": {
74
+ "suggestions": [
75
+ "What is in the fixture corpus?",
76
+ "Which documents does the fixture publisher issue?"
77
+ ],
78
+ "models_disclosure": [
79
+ {
80
+ "role": "Answers",
81
+ "model": "fixture-answer-model"
82
+ }
83
+ ],
84
+ "smoke": [
85
+ {
86
+ "label": "sanity",
87
+ "query": "What is in the fixture corpus?",
88
+ "expect": "fixture"
89
+ }
90
+ ]
91
+ },
92
+ "retrieval": {
93
+ "process_expansion": " fixture certification system framework application evaluation"
94
+ },
95
+ "prompts": {
96
+ "vars": {
97
+ "assistant_identity": "the fixture assistant — a public service answering questions about the fixture publisher's documents",
98
+ "refusal_sentence": "I don't have information on this in the indexed fixture documents."
99
+ }
100
+ }
101
+ } as const;
@@ -0,0 +1,16 @@
1
+ // The profile registry (konneal item 06/09): a deployment injects its
2
+ // OWN publisher profile at the entry (setProfile) before the first
3
+ // request; every read goes through P() at request time so module-eval
4
+ // order can never bake the wrong profile. The default is the engine's
5
+ // fixture, so the engine runs and tests pass standalone.
6
+ import { PROFILE as FIXTURE } from "./profile.gen.ts";
7
+
8
+ let current: unknown = FIXTURE;
9
+
10
+ export function setProfile(profile: unknown): void {
11
+ current = profile;
12
+ }
13
+
14
+ export function P<T = any>(): T {
15
+ return current as T;
16
+ }
@@ -0,0 +1,108 @@
1
+ // Projects (#187 design): a container owning memory files and a set of
2
+ // conversations. Every query is owner-filtered through the owning
3
+ // project's sub; file caps mirror the personal-memory tier (10 × 8k).
4
+ // Membership is a MOVE (conversations.project_id set/clear) — the next
5
+ // answer re-scopes, history keeps its recorded grounding.
6
+ import { json, err } from "./lib/http";
7
+
8
+ const MAX_FILES = 10;
9
+ const MAX_CONTENT = 8_000;
10
+ const MAX_NAME = 64;
11
+ const PROJECT_ID_RE = /^p:[a-f0-9]{16}$/;
12
+ const FILE_ID_RE = /^pf:[a-f0-9]{16}$/;
13
+
14
+ const hex16 = () => [...crypto.getRandomValues(new Uint8Array(8))].map((b) => b.toString(16).padStart(2, "0")).join("");
15
+
16
+ async function ownedProject(env: any, sub: string, id: string) {
17
+ if (!PROJECT_ID_RE.test(id)) return null;
18
+ return await env.DB.prepare("SELECT id, name, created_at FROM projects WHERE id = ?1 AND sub = ?2").bind(id, sub).first();
19
+ }
20
+
21
+ export async function handleProjects(env: any, sub: string, req: Request, route: { method: string; id?: string }): Promise<Response> {
22
+ const { method, id } = route;
23
+ if (method === "GET") {
24
+ const rows = (await env.DB.prepare(
25
+ "SELECT p.id, p.name, p.created_at, (SELECT COUNT(*) FROM project_files f WHERE f.project_id = p.id) AS file_count, "
26
+ + "(SELECT COUNT(*) FROM conversations c WHERE c.project_id = p.id) AS conversation_count "
27
+ + "FROM projects p WHERE p.sub = ?1 ORDER BY p.created_at DESC",
28
+ ).bind(sub).all()).results ?? [];
29
+ return json({ projects: rows });
30
+ }
31
+ if (method === "POST") {
32
+ const body: any = await req.json().catch(() => null);
33
+ if (typeof body?.project_id === "string") {
34
+ // move a conversation in/out of a project (membership-as-move)
35
+ const convId = String(body.conversation_id ?? "");
36
+ if (!/^[a-zA-Z0-9_-]{8,64}$/.test(convId)) return err(400, "invalid_input", "bad conversation id");
37
+ const target = body.project_id === null ? null : String(body.project_id);
38
+ if (target && !(await ownedProject(env, sub, target))) return err(404, "not_found", "no such project");
39
+ const r = await env.DB.prepare("UPDATE conversations SET project_id = ?1 WHERE id = ?2 AND sub = ?3").bind(target, convId, sub).run();
40
+ if (!r.meta?.changes) return err(404, "not_found", "no such conversation");
41
+ return json({ ok: true });
42
+ }
43
+ if (typeof body?.id === "string" && PROJECT_ID_RE.test(body.id)) {
44
+ if (!(await ownedProject(env, sub, body.id))) return err(404, "not_found", "no such project");
45
+ const name = typeof body.name === "string" ? body.name.trim().slice(0, MAX_NAME) : "";
46
+ if (!name) return err(400, "invalid_input", "name required");
47
+ await env.DB.prepare("UPDATE projects SET name = ?1 WHERE id = ?2 AND sub = ?3").bind(name, body.id, sub).run();
48
+ return json({ ok: true, id: body.id });
49
+ }
50
+ const name = typeof body?.name === "string" ? body.name.trim().slice(0, MAX_NAME) : "";
51
+ if (!name) return err(400, "invalid_input", "name required");
52
+ const pid = "p:" + hex16();
53
+ await env.DB.prepare("INSERT INTO projects (id, sub, name, created_at) VALUES (?1, ?2, ?3, ?4)").bind(pid, sub, name, Date.now()).run();
54
+ return json({ ok: true, id: pid });
55
+ }
56
+ if (method === "DELETE") {
57
+ if (!id || !PROJECT_ID_RE.test(id)) return err(400, "invalid_input", "bad project id");
58
+ if (!(await ownedProject(env, sub, id))) return err(404, "not_found", "no such project");
59
+ await env.DB.batch([
60
+ env.DB.prepare("UPDATE conversations SET project_id = NULL WHERE project_id = ?1 AND sub = ?2").bind(id, sub),
61
+ env.DB.prepare("DELETE FROM project_files WHERE project_id = ?1").bind(id),
62
+ env.DB.prepare("DELETE FROM projects WHERE id = ?1 AND sub = ?2").bind(id, sub),
63
+ ]);
64
+ return json({ ok: true });
65
+ }
66
+ return err(405, "method_not_allowed", "GET, POST or DELETE");
67
+ }
68
+
69
+ export async function handleProjectFiles(env: any, sub: string, req: Request, route: { method: string; id?: string }): Promise<Response> {
70
+ const { method, id } = route;
71
+ if (method === "GET") {
72
+ if (!id || !PROJECT_ID_RE.test(id)) return err(400, "invalid_input", "bad project id");
73
+ if (!(await ownedProject(env, sub, id))) return err(404, "not_found", "no such project");
74
+ const rows = (await env.DB.prepare("SELECT id, name, content, updated_at FROM project_files WHERE project_id = ?1 ORDER BY updated_at DESC").bind(id).all()).results ?? [];
75
+ return json({ files: rows });
76
+ }
77
+ if (method === "POST") {
78
+ const body: any = await req.json().catch(() => null);
79
+ const projectId = typeof body?.project_id === "string" ? body.project_id : "";
80
+ if (!PROJECT_ID_RE.test(projectId) || !(await ownedProject(env, sub, projectId))) return err(404, "not_found", "no such project");
81
+ const name = typeof body?.name === "string" ? body.name.trim().slice(0, MAX_NAME) : "";
82
+ const content = typeof body?.content === "string" ? body.content.slice(0, MAX_CONTENT) : "";
83
+ if (!name || !content.trim()) return err(400, "invalid_input", "name and content are required");
84
+ const now = Date.now();
85
+ if (typeof body?.id === "string" && FILE_ID_RE.test(body.id)) {
86
+ const r = await env.DB.prepare(
87
+ "UPDATE project_files SET name = ?1, content = ?2, updated_at = ?3 WHERE id = ?4 AND project_id = ?5",
88
+ ).bind(name, content, now, body.id, projectId).run();
89
+ if (!r.meta?.changes) return err(404, "not_found", "no such file");
90
+ return json({ ok: true, id: body.id });
91
+ }
92
+ const count = Number((await env.DB.prepare("SELECT COUNT(*) AS n FROM project_files WHERE project_id = ?1").bind(projectId).first() as any)?.n ?? 0);
93
+ if (count >= MAX_FILES) return err(400, "quota_exceeded", `at most ${MAX_FILES} files per project`);
94
+ const fid = "pf:" + hex16();
95
+ await env.DB.prepare(
96
+ "INSERT INTO project_files (id, project_id, name, content, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?5)",
97
+ ).bind(fid, projectId, name, content, now).run();
98
+ return json({ ok: true, id: fid });
99
+ }
100
+ if (method === "DELETE") {
101
+ if (!id || !FILE_ID_RE.test(id)) return err(400, "invalid_input", "bad file id");
102
+ await env.DB.prepare(
103
+ "DELETE FROM project_files WHERE id = ?1 AND project_id IN (SELECT id FROM projects WHERE sub = ?2)",
104
+ ).bind(id, sub).run();
105
+ return json({ ok: true });
106
+ }
107
+ return err(405, "method_not_allowed", "GET, POST or DELETE");
108
+ }
@@ -0,0 +1,6 @@
1
+ // Prompt data files (../prompts/*.md) are bundled by wrangler's Text
2
+ // module rule and imported as strings.
3
+ declare module "*.md" {
4
+ const content: string;
5
+ export default content;
6
+ }
@@ -0,0 +1,54 @@
1
+ // Quota + telemetry: per-bucket daily counters (KV) and the D1
2
+ // queries/spend ledger writes (TODO.impl/23).
3
+ import { sha256Hex, today } from "./config";
4
+ import type { Env } from "./env";
5
+ import type { Background } from "./ports/runtime.ts";
6
+
7
+ export async function kvIncr(cache: KVNamespace, key: string, step = 1): Promise<number> {
8
+ const cur = Number((await cache.get(key)) ?? "0");
9
+ const next = cur + step;
10
+ await cache.put(key, String(next), { expirationTtl: 90000 });
11
+ return next;
12
+ }
13
+
14
+ export function clientIp(req: Request): string {
15
+ return req.headers.get("cf-connecting-ip") ?? "unknown";
16
+ }
17
+
18
+ /** Daily ask quota. `weight` is the effort multiplier: a thorough
19
+ * (elevated-effort) answer consumes more of the day's budget —
20
+ * different reasoning efforts naturally cost different quota. */
21
+ export async function checkQuota(
22
+ env: Env,
23
+ bucket: string,
24
+ id: string,
25
+ limit: number,
26
+ weight = 1,
27
+ ): Promise<{ ok: boolean; used: number; limit: number }> {
28
+ const used = await kvIncr(env.CACHE, `q:${today()}:${bucket}:${await sha256Hex(id)}`, weight);
29
+ return { ok: used <= limit, used, limit };
30
+ }
31
+
32
+ export function telemetry(
33
+ env: Env,
34
+ ctx: Background,
35
+ tier: string,
36
+ route: string,
37
+ model: string | null,
38
+ ok: boolean,
39
+ answerChars: number,
40
+ queryHash: string,
41
+ lang?: string,
42
+ ) {
43
+ const day = today();
44
+ ctx.waitUntil(
45
+ env.DB.batch([
46
+ env.DB.prepare(
47
+ "INSERT INTO queries (ts, day, tier, route, model, ok, answer_chars, query_hash, lang) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9)",
48
+ ).bind(new Date().toISOString(), day, tier, route, model, ok ? 1 : 0, answerChars, queryHash, lang ?? null),
49
+ env.DB.prepare(
50
+ "INSERT INTO spend (day, tier, model, requests) VALUES (?1,?2,?3,1) ON CONFLICT(day, tier, model) DO UPDATE SET requests = requests + 1",
51
+ ).bind(day, tier, model ?? "none"),
52
+ ]),
53
+ );
54
+ }
@@ -0,0 +1,67 @@
1
+ // Self-RAG style reflection loop: after generating the answer, the model
2
+ // critiques whether every claim is grounded in the passages. If NOT, it
3
+ // identifies what's missing and the system retries retrieval.
4
+ // Ref: selfrag.github.io; Meilisearch self-RAG guide
5
+
6
+ // The prompt is data (prompts/reflect.md), bundled as text.
7
+ import reflectPrompt from "../prompts/reflect.md";
8
+
9
+ export interface ReflectionResult {
10
+ grounded: boolean;
11
+ missing_info: string;
12
+ }
13
+
14
+ export async function reflect(
15
+ ai: any,
16
+ model: string,
17
+ question: string,
18
+ answer: string,
19
+ passages: string[],
20
+ ): Promise<ReflectionResult | null> {
21
+ if (!answer || !passages.length) return null;
22
+ const ctx = passages
23
+ .slice(0, 8)
24
+ .map((p, i) => `[${i + 1}] ${p.replace(/\s+/g, " ").slice(0, 300)}`)
25
+ .join("\n");
26
+
27
+ const timeout = new Promise<null>((r) => setTimeout(() => r(null), 6000));
28
+ const call = (async () => {
29
+ const res: any = await ai.run(model, {
30
+ messages: [
31
+ {
32
+ role: "system",
33
+ content: reflectPrompt.trimEnd(),
34
+ },
35
+ {
36
+ role: "user",
37
+ content: `Question: ${question}\n\nAnswer:\n${answer.slice(0, 1500)}\n\nPassages:\n${ctx}`,
38
+ },
39
+ ],
40
+ // reasoning shares this budget — starved budgets silently disable
41
+ // the reflection layer (null = no retry ever fires); DeepSeek-V4
42
+ // card: keep reasoning on with headroom + temp 1.0 / top_p 1.0
43
+ max_tokens: 3072,
44
+ reasoning_effort: "low",
45
+ temperature: 1.0,
46
+ top_p: 1.0,
47
+ });
48
+ const text = typeof res?.response === "string" ? res.response : res?.choices?.[0]?.message?.content;
49
+ const m = (text ?? "").match(/\{[\s\S]*\}/);
50
+ if (!m) return null;
51
+ try {
52
+ const raw = JSON.parse(m[0]);
53
+ return {
54
+ grounded: raw.grounded === true,
55
+ missing_info: typeof raw.missing_info === "string" ? raw.missing_info.slice(0, 200) : "",
56
+ };
57
+ } catch {
58
+ return null;
59
+ }
60
+ })();
61
+
62
+ try {
63
+ return await Promise.race([call, timeout]);
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
@@ -0,0 +1,107 @@
1
+ // Answer contract v2 — symbolic unit references (SymGen-style,
2
+ // arXiv:2311.09188): the model may write [[u:<id>]] tokens pointing at
3
+ // typed MKO units among the passages. Every reference is VALIDATED
4
+ // against the units actually present in usedHits; invalid refs are
5
+ // dropped from the rendered text (logged, never shown). The payload
6
+ // data itself never passes through the LLM — blocks are resolved from
7
+ // the producer-validated D1 unit_payloads table.
8
+
9
+ import type { StoreQuery } from "./ports/store.ts";
10
+ export interface ResolvedBlock {
11
+ unit_id: string;
12
+ type: string;
13
+ docidentifier: string;
14
+ edition?: string;
15
+ payload: Record<string, unknown>;
16
+ }
17
+
18
+ const REF = /\[\[(u:[A-Za-z0-9_-]+)\]\]/g;
19
+
20
+ /** unit ids the passages actually contain (chunk metadata carries
21
+ * unit_id on typed MKO chunks). */
22
+ export function availableUnitIds(hits: ReadonlyArray<{ metadata: { unit_id?: string | undefined } }>): Set<string> {
23
+ const ids = new Set<string>();
24
+ for (const h of hits) {
25
+ const u = h.metadata?.unit_id;
26
+ if (u) ids.add(u);
27
+ }
28
+ return ids;
29
+ }
30
+
31
+ export function parseRefs(text: string): string[] {
32
+ return [...text.matchAll(REF)].map((m) => m[1]);
33
+ }
34
+
35
+ /** Drop refs that don't resolve to a used passage unit — a dangling ref
36
+ * renders as nothing (the token removed), and the violation is logged. */
37
+ export function sanitizeRefs(text: string, available: ReadonlySet<string>): { text: string; dropped: string[] } {
38
+ const dropped: string[] = [];
39
+ const out = text.replace(REF, (full, id: string) => {
40
+ if (available.has(id)) return full;
41
+ dropped.push(id);
42
+ return "";
43
+ });
44
+ return { text: out, dropped };
45
+ }
46
+
47
+ /** Resolve validated refs to producer payloads from D1 (unit_payloads).
48
+ * Unknown-to-D1 ids are skipped — a ref without a payload renders as a
49
+ * plain token, never as fabricated data. */
50
+ export async function resolveBlocks(
51
+ db: StoreQuery,
52
+ refs: string[],
53
+ ): Promise<ResolvedBlock[]> {
54
+ if (!refs.length) return [];
55
+ const uniq = [...new Set(refs)].slice(0, 12);
56
+ const blocks: ResolvedBlock[] = [];
57
+ for (let i = 0; i < uniq.length; i += 20) {
58
+ const batch = uniq.slice(i, i + 20);
59
+ const placeholders = batch.map((_, n) => `?${n + 1}`).join(",");
60
+ try {
61
+ const res = await db
62
+ .prepare(`SELECT unit_id, type, docidentifier, edition, payload FROM unit_payloads WHERE unit_id IN (${placeholders})`)
63
+ .bind(...batch)
64
+ .all<{ unit_id: string; type: string; docidentifier: string | null; edition: string | null; payload: string }>();
65
+ for (const r of res.results) {
66
+ let payload: Record<string, unknown> = {};
67
+ try {
68
+ payload = JSON.parse(String(r.payload));
69
+ } catch {
70
+ continue;
71
+ }
72
+ blocks.push({
73
+ unit_id: String(r.unit_id),
74
+ type: String(r.type),
75
+ docidentifier: String(r.docidentifier ?? ""),
76
+ edition: r.edition ? String(r.edition) : undefined,
77
+ payload,
78
+ });
79
+ }
80
+ } catch (e) {
81
+ console.log("resolveBlocks failed:", String(e).slice(0, 150));
82
+ }
83
+ }
84
+ return blocks;
85
+ }
86
+
87
+ /** One pass: validate + resolve + strip invalid tokens. */
88
+ export async function contractV2(
89
+ db: StoreQuery,
90
+ answer: string,
91
+ usedHits: ReadonlyArray<{ metadata: { unit_id?: string | undefined } }>,
92
+ ): Promise<{ text: string; blocks: ResolvedBlock[]; dropped: string[] }> {
93
+ const available = availableUnitIds(usedHits);
94
+ const { text, dropped } = sanitizeRefs(answer, available);
95
+ if (dropped.length) console.log("refs: dropped", dropped.length, "invalid (not in passages)");
96
+ const refs = parseRefs(text);
97
+ const blocks = await resolveBlocks(db, refs);
98
+ return { text, blocks, dropped };
99
+ }
100
+
101
+ /** Detect table-retyping: a markdown table in the answer while a typed
102
+ * table unit was available to reference. Enforcement signal for the
103
+ * corrective regen (same pattern as quote-anchor violations). */
104
+ export function tableRetyped(text: string, availableTable: boolean): boolean {
105
+ if (!availableTable) return false;
106
+ return /(^|\n)\s*\|[^\n]+\|\s*(\n\s*\|[-: |]+\|\s*)?(\n|$)/.test(text) && (text.match(/\|/g) ?? []).length >= 6;
107
+ }
@@ -0,0 +1,65 @@
1
+ import { P } from "./profile.ts";
2
+ /** The one sanctioned refusal sentence (also in prompts/system.md).
3
+ * Refusals are never cached: a refusal says "retrieval found nothing",
4
+ * which is a property of the moment, not of the question. */
5
+ export function refusalAnswer(): string {
6
+ return P().prompts.vars.refusal_sentence;
7
+ }
8
+
9
+ // the model occasionally paraphrases the refusal sentence ("...information
10
+ // on how to make lasagna in the indexed..."); the API contract is the
11
+ // exact canonical sentence — normalize variants, keep the redirect tail
12
+ const REFUSAL_VARIANT = /^\s*I don[’']?t have information on .{1,120}? in the indexed OIML(?: \w+){0,2} (?:publications|passages|documents|corpus)\.?/i;
13
+
14
+ // Upstream wording drift (rag#88, the golden refusal pin recalibrated
15
+ // 2026-09-01): the model now also refuses with no pinned sentence at all —
16
+ // a "can't help" redirect naming the OIML publications, or a "no real
17
+ // answer to give" paraphrase. The runtime and the harness pins
18
+ // (scripts/eval.mjs, tests/e2e.mjs, tests/eval-suite.mjs) carry the SAME
19
+ // family: every shape the tests accept canonicalizes to the pinned
20
+ // sentence here, and nothing else does.
21
+ const REFUSAL_DRIFT: RegExp[] = [
22
+ /\b(can'?t|cannot|couldn'?t|unable)\b[^.]{0,120}?\b(indexed )?OIML(?: \w+){0,2} publications\b/i,
23
+ /\bno real answer to give\b[^.]{0,120}?\bOIML\b/i,
24
+ /\b(?:falls|well) outside\b[^.]{0,120}?\b(?:what I can answer|my scope|the scope of)\b/i,
25
+ /\boutside (?:of )?what (?:I|this service) can answer\b/i,
26
+ // "I can't answer that — weather forecasting is outside my scope":
27
+ // requires the refusal verb, so a scope DISCUSSION inside a real answer
28
+ // ("this exemption is outside the scope of R 60") never matches
29
+ /\bI can[’']?t answer\b[^.]{0,100}?\bscope\b/i,
30
+ /^\s*I don[’']?t have any indexed OIML \w+(?:s)? (?:covering|about|on)\b/im,
31
+ ];
32
+
33
+ /** Start of the sentence containing offset `i` (after the nearest ". ",
34
+ * "! ", "? ", or newline before it, else the string start). */
35
+ function sentenceStart(answer: string, i: number): number {
36
+ let s = 0;
37
+ for (const sep of [". ", "! ", "? ", "\n"]) {
38
+ const j = answer.lastIndexOf(sep, i);
39
+ if (j >= 0) s = Math.max(s, j + sep.length);
40
+ }
41
+ return s;
42
+ }
43
+
44
+ /** End (terminator included) of the sentence containing offset `i`. */
45
+ function sentenceEnd(answer: string, i: number): number {
46
+ const m = /[.!?\n]/.exec(answer.slice(i));
47
+ return m ? i + m.index + 1 : answer.length;
48
+ }
49
+
50
+ /** Normalize a refusal-shaped answer to the pinned sentence. A drift
51
+ * refusal occupies its own sentence: swap that sentence (preface
52
+ * included) for the pinned one — the redirect tail survives. An answer
53
+ * outside the refusal family is returned byte-identical. */
54
+ export function canonicalRefusal(answer: string): string {
55
+ const CANON = refusalAnswer();
56
+ if (answer.includes(CANON)) return answer;
57
+ const variant = answer.match(REFUSAL_VARIANT);
58
+ if (variant) return answer.replace(variant[0], CANON);
59
+ for (const drift of REFUSAL_DRIFT) {
60
+ const m = drift.exec(answer);
61
+ if (!m) continue;
62
+ return answer.slice(0, sentenceStart(answer, m.index)) + CANON + answer.slice(sentenceEnd(answer, m.index));
63
+ }
64
+ return answer;
65
+ }
@@ -0,0 +1,71 @@
1
+ // The per-request scope model (MECE: ask.ts orchestrates, this module
2
+ // owns the derivation): which DATASETS the request searches (the sidebar
3
+ // toggles, server-intersected with session permissions) and which
4
+ // memory files ride it — plus the answer-cache SALT both selections
5
+ // produce. Pure: D1 access stays with the caller (memoryNote); this
6
+ // module only derives.
7
+ import { DATASETS, datasetAllowed } from "./config.ts";
8
+
9
+ export interface RequestScope {
10
+ /** dataset ids the request may search */
11
+ scopeIds: string[];
12
+ /** corpus values the scope maps to (for the corpus-scope stage) */
13
+ corpora: Set<string>;
14
+ /** true when the scope is narrower than the session default — the
15
+ * stage only runs when narrowed (the default costs nothing) */
16
+ narrowed: boolean;
17
+ /** ISO federation on? (gates the internal binding) */
18
+ isoOn: boolean;
19
+ /** raw (validated) memory ids from the body — empty for anon */
20
+ memoryIds: string[];
21
+ }
22
+
23
+ /** Validate + intersect. Returns { error } when the request explicitly
24
+ * disables every dataset (a user error, not a scope). The corpora a
25
+ * dataset searches travel WITH the declaration (profile datasets.yaml,
26
+ * `corpora:`) — the engine maps no publisher names. */
27
+ export function resolveRequestScope(body: any, member: unknown): RequestScope | { error: "empty-datasets" } {
28
+ const allIds = DATASETS().map((d) => d.id);
29
+ const requested = Array.isArray(body?.datasets)
30
+ ? (body.datasets as unknown[]).filter((x): x is string => typeof x === "string" && allIds.includes(x))
31
+ : null;
32
+ if (requested !== null && requested.length === 0) return { error: "empty-datasets" };
33
+ const permittedIds = allIds.filter((id) => {
34
+ const d = DATASETS().find((x) => x.id === id)!;
35
+ return d.session ? datasetAllowed(d, member) : true;
36
+ });
37
+ // requested ∩ permitted — an explicit request NEVER widens past the
38
+ // session's permissions (a member without ai-preview asking
39
+ // datasets:["iso"] intersects to nothing, exactly like the default
40
+ // path; the intersection, not the caller, is the gate)
41
+ const scopeIds = (requested ?? permittedIds).filter((id) => permittedIds.includes(id));
42
+ const corpora = new Set<string>();
43
+ for (const id of scopeIds) {
44
+ const d = DATASETS().find((x) => x.id === id);
45
+ for (const v of d?.corpora ?? [id]) corpora.add(v);
46
+ }
47
+ const memoryIds = member && Array.isArray(body?.memories)
48
+ ? (body.memories as unknown[]).filter((x): x is string => typeof x === "string").slice(0, 4)
49
+ : [];
50
+ return {
51
+ scopeIds,
52
+ corpora,
53
+ narrowed: scopeIds.length < permittedIds.length,
54
+ // federation flag: any session-gated (federated) dataset in scope
55
+ isoOn: scopeIds.some((id) => DATASETS().find((x) => x.id === id)?.session === true),
56
+ memoryIds,
57
+ };
58
+ }
59
+
60
+ /** The answer-cache salt: request-scoped context that materially changes
61
+ * the answer (dataset scope, memory selection). Requests differing only
62
+ * in salt share query text — an unsalted key would serve a scoped (or
63
+ * memory-flavored) answer to a plain ask. Null = default scope, no
64
+ * memory: keys stay byte-identical to the pre-salt era. */
65
+ export function requestSalt(scope: RequestScope, memoryUsed: string[]): string | null {
66
+ if (!scope.narrowed && !memoryUsed.length) return null;
67
+ return JSON.stringify({
68
+ ...(scope.narrowed ? { d: [...scope.corpora].sort() } : {}),
69
+ ...(memoryUsed.length ? { m: [...memoryUsed].sort() } : {}),
70
+ });
71
+ }