@konneal/engine 0.1.3 → 0.2.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 (68) hide show
  1. package/dist/admin.d.ts +1 -0
  2. package/dist/ask-NAWBPZPU.js +12 -0
  3. package/dist/chunk-6HCFW5PM.js +2780 -0
  4. package/dist/{chunk-EHJEELVB.js → chunk-LNSDBEKS.js} +1 -1
  5. package/dist/chunk-LNUSF3UK.js +1852 -0
  6. package/dist/{chunk-35ODH64W.js → chunk-Q327B27J.js} +33 -0
  7. package/dist/{chunk-OCNLV7Q7.js → chunk-Q6LI4T7M.js} +6 -1
  8. package/dist/chunk-RFG3QKVY.js +64 -0
  9. package/dist/{chunk-ROF3Q7UC.js → chunk-SN3ANQ3Y.js} +2 -2
  10. package/dist/{chunk-CAEHIVG5.js → chunk-WGXATDXY.js} +1 -1
  11. package/dist/codecs.d.ts +3 -3
  12. package/dist/completion.d.ts +1 -1
  13. package/dist/config.d.ts +9 -0
  14. package/dist/faithfulness.d.ts +1 -0
  15. package/dist/mcp-proto.d.ts +25 -0
  16. package/dist/mcp.d.ts +4 -0
  17. package/dist/openapi-surface.gen.d.ts +9 -0
  18. package/dist/openapi-types.d.ts +2141 -0
  19. package/dist/pipeline.d.ts +3 -0
  20. package/dist/profile.gen.d.ts +1 -0
  21. package/dist/prompts/system.md +1 -0
  22. package/dist/prompts/understanding.md +1 -0
  23. package/dist/quota.d.ts +4 -1
  24. package/dist/search-7XMYAV34.js +11 -0
  25. package/dist/tablecontext.d.ts +7 -0
  26. package/dist/verdict-parse.d.ts +5 -0
  27. package/dist/worker_mcp/src/index.js +3 -3
  28. package/dist/worker_public/src/config.js +4 -2
  29. package/dist/worker_public/src/index.js +1190 -4834
  30. package/dist/worker_public/src/profile.js +1 -1
  31. package/dist/worker_public/src/refusal.js +2 -2
  32. package/dist/worker_public/src/requestScope.js +3 -3
  33. package/docs/INGEST-ARCHITECTURE.md +1 -0
  34. package/docs/projects-design.md +7 -0
  35. package/docs/sota-mechanisms.md +15 -0
  36. package/docs/spec-api.md +27 -13
  37. package/docs/spec-pipeline.md +20 -13
  38. package/package.json +12 -3
  39. package/profile/prompts.yaml +3 -0
  40. package/workers/shared/router.ts +23 -16
  41. package/workers/worker_public/migrations/0014_usage_cache.sql +5 -0
  42. package/workers/worker_public/openapi.yaml +1169 -0
  43. package/workers/worker_public/prompts/system.md +1 -0
  44. package/workers/worker_public/prompts/understanding.md +1 -0
  45. package/workers/worker_public/schema.sql +3 -1
  46. package/workers/worker_public/src/admin.ts +51 -7
  47. package/workers/worker_public/src/ask.ts +97 -24
  48. package/workers/worker_public/src/codecs.ts +35 -10
  49. package/workers/worker_public/src/completion.ts +24 -1
  50. package/workers/worker_public/src/config.ts +10 -0
  51. package/workers/worker_public/src/conversations.ts +1 -1
  52. package/workers/worker_public/src/faithfulness.ts +10 -17
  53. package/workers/worker_public/src/grader.ts +2 -2
  54. package/workers/worker_public/src/index.ts +106 -58
  55. package/workers/worker_public/src/lib/router.ts +1 -1
  56. package/workers/worker_public/src/mcp-proto.ts +71 -0
  57. package/workers/worker_public/src/mcp.ts +47 -0
  58. package/workers/worker_public/src/openapi-surface.gen.ts +318 -0
  59. package/workers/worker_public/src/pipeline.ts +9 -3
  60. package/workers/worker_public/src/profile.gen.ts +1 -0
  61. package/workers/worker_public/src/projects.ts +4 -2
  62. package/workers/worker_public/src/quota.ts +4 -2
  63. package/workers/worker_public/src/research.ts +55 -3
  64. package/workers/worker_public/src/stages/citationProbe.ts +145 -0
  65. package/workers/worker_public/src/stages/index.ts +2 -0
  66. package/workers/worker_public/src/stages/types.ts +4 -0
  67. package/workers/worker_public/src/tablecontext.ts +13 -2
  68. package/workers/worker_public/src/verdict-parse.ts +60 -0
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  P,
3
3
  setProfile
4
- } from "../../chunk-35ODH64W.js";
4
+ } from "../../chunk-Q327B27J.js";
5
5
  export {
6
6
  P,
7
7
  setProfile
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  canonicalRefusal,
3
3
  refusalAnswer
4
- } from "../../chunk-CAEHIVG5.js";
5
- import "../../chunk-35ODH64W.js";
4
+ } from "../../chunk-WGXATDXY.js";
5
+ import "../../chunk-Q327B27J.js";
6
6
  export {
7
7
  canonicalRefusal,
8
8
  refusalAnswer
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  requestSalt,
3
3
  resolveRequestScope
4
- } from "../../chunk-EHJEELVB.js";
5
- import "../../chunk-OCNLV7Q7.js";
6
- import "../../chunk-35ODH64W.js";
4
+ } from "../../chunk-LNSDBEKS.js";
5
+ import "../../chunk-Q6LI4T7M.js";
6
+ import "../../chunk-Q327B27J.js";
7
7
  export {
8
8
  requestSalt,
9
9
  resolveRequestScope
@@ -54,6 +54,7 @@ hand.
54
54
  │ (derived status, active flags — the SSOT) │
55
55
  │ structure typed nodes → ChunkRecordV2 (per block type) │
56
56
  │ derivation registry + graph projection (relaton edges + │
57
+ │ citation edges from the indexed bibliographies │
57
58
  │ glossarist defines edges) │
58
59
  │ enrichment contextual contexts (quality-first lane, KV- │
59
60
  │ cached, content-hash invalidated) │
@@ -2,6 +2,13 @@
2
2
 
3
3
  > The question: should chats group into **projects** that share a
4
4
  > per-project contextual memory? Yes — and the parts already exist.
5
+ >
6
+ > **Shipped (2026-09-15)**: membership is drag-and-drop — a chat drags
7
+ > onto a project row to file, onto the conversations list to unfile,
8
+ > with drop-target highlights and a live hint; a per-row picker covers
9
+ > touch/keyboard; chats carry a project badge. The conversations list
10
+ > carries `project_id` from the server (membership stays server-truth)
11
+ > and the move rides the server conversation id.
5
12
  > This is the design; nothing is implemented yet.
6
13
 
7
14
  ## 1. Prior art — what the best got right and wrong
@@ -97,6 +97,21 @@ unit selection, so the pin lands the object the question is about
97
97
  ranking answers from stale editions, guesses at doc scope, and never
98
98
  surfaces a typed object.
99
99
 
100
+ ### 7b. The citation graph (structured questions get graph answers)
101
+
102
+ Bibliographic citations become graph edges: what each edition's
103
+ bibliography cites is extracted at index time (the identifier grammar
104
+ lives in the publisher codec — ISO/IEC floor generic, publisher series
105
+ on top; ~300 cites edges across the OIML corpus), stored beside the
106
+ publication registry, and keyed by the same normalized pubids
107
+ everywhere ("OIML R 60", never a bare number). When a question asks
108
+ what a publication cites or references, the answer receives the
109
+ cited-standards list per edition as authoritative structured data
110
+ (the `notes` channel), while the bibliography section itself is
111
+ retrieved as the passage that grounds the answer verbatim. Structured
112
+ questions get graph answers; semantic questions get vector answers;
113
+ the hybrid class gets both, fused by the same rerank.
114
+
100
115
  ### 8. The answer contract (claims are checkable)
101
116
  Inline citations on every claim; normative values quoted verbatim from
102
117
  the cited passage; tables/formulas/figures rendered as typed objects
package/docs/spec-api.md CHANGED
@@ -1,9 +1,13 @@
1
1
  # API surface spec
2
2
 
3
- The worker's complete HTTP inventory, mirrored 1:1 by the `ROUTES`
4
- registry in `workers/worker_public/src/index.ts` (dispatched by
5
- `lib/router.ts`'s `matchRoute`). Adding a route = one registry entry +
6
- its handler; this document changes with it. Request/response details
3
+ The worker's complete HTTP inventory. The API routes are generated from
4
+ the OpenAPI document `workers/worker_public/openapi.yaml`
5
+ (`scripts/gen-openapi-routes.mjs` → `src/openapi-surface.gen.ts`), and
6
+ `index.ts`'s `OPENAPI_HANDLERS` binds every operation to its handler;
7
+ `tests/openapi-surface.test.ts` pins the bijection, and the non-API
8
+ routes (pages, unit assets, rendered documents) are declared in
9
+ `INFRA_ROUTES`. Adding a route = the yaml entry + one handler binding +
10
+ regenerate; this document changes with it. Request/response details
7
11
  beyond the inventory: [API.md](API.md).
8
12
 
9
13
  ## Conventions
@@ -27,13 +31,19 @@ beyond the inventory: [API.md](API.md).
27
31
  | `/auth/callback` | GET | none | `handleCallback` | OIDC callback; mints the session cookie; bubble mode postMessages the token to the validated origin. |
28
32
  | `/auth/me` | GET | session | `handleMe` | `{ authenticated, name, email, roles, tier }`. |
29
33
  | `/auth/logout` | GET, POST | none | `handleLogout` | Clears the session. |
30
- | `/api/conversations` | * | session | `conversationsRoute` → `handleConversations` | List (GET) / create (POST) / delete (DELETE) the signed-in user's conversations. |
31
- | `/api/conversations/:id` | * | session (owner) | `conversationsRoute` | One conversation (GET) / delete (DELETE). |
34
+ | `/api/conversations` | GET, POST | session | `conversationsRoute` → `handleConversations` | List / create the signed-in member's conversations. |
35
+ | `/api/conversations/:id` | GET, PATCH, DELETE | session (owner) | `conversationsRoute` | One conversation: read / rename / delete. |
32
36
  | `/api/conversations/:id/messages` | POST | session (owner) | `appendMessageRoute` → `handleAppendMessage` | Append a message turn. |
33
37
  | `/api/conversations/:id/share` | POST | session (owner) | `shareRoute` → `handleShareConversation` | Publish a conversation to an unlisted share slug. (Was shadowed by the compound conversations branch pre-route-table — the pattern inventory made the collision visible and fixed it.) |
34
38
  | `/api/shared/:slug` | GET | none | `getSharedRoute` → `handleGetShared` | Read a shared conversation. |
39
+ | `/api/projects` | GET, POST | session | `projectsRoute` → `handleProjects` | Contexts (#187): list / create / rename by id / file a conversation by `conversation_id` + `project_id` (null unfiles — membership-as-move). |
40
+ | `/api/projects/:id` | DELETE | session (owner) | `projectsRoute` | Delete a context: files detached, conversations returned to the general list. |
41
+ | `/api/projects/:id/files` | GET, POST | session (owner) | `projectFilesRoute` → `handleProjectFiles` | List / attach (or replace by id) a context's files; POST names the context by `project_id` in the body. |
42
+ | `/api/project-files/:id` | DELETE | session (owner) | `projectFilesRoute` | Detach one file. |
35
43
  | `/api/datasets` | GET | none (session enriches) | `datasetsRoute` | The corpus catalog + starter questions (the UI's empty state — content from the API, never hardcoded in the client). Session-gated datasets carry `requires` (the estate permission, e.g. `the ai-preview permission (id.oimlsmart.org)`) and `enabled` reflects the permission, not just login. |
36
- | `/api/memories` | * | session | `memoriesRoute` → `handleMemories` | Personalized memory files (#171): GET list / POST create-update (≤10 files × 8k chars) / DELETE by id — every query owner-filtered by the session sub. Selected ids ride `/api/ask` as `memories: [id…]` (≤4 per ask, injected as one bounded trusted-user-facts note; the selection salts the answer cache). |
44
+ | `/api/memories` | GET, POST | session | `memoriesRoute` → `handleMemories` | Personalized memory files (#171): GET list / POST create-or-replace by id (≤10 files × 8k chars) — every query owner-filtered by the session sub. Selected ids ride `/api/ask` as `memories: [id…]` (≤4 per ask, injected as one bounded trusted-user-facts note; the selection salts the answer cache). |
45
+ | `/api/memories/:id` | DELETE | session (owner) | `memoriesRoute` | Delete one memory file. |
46
+ | `/mcp` | POST | tier | `mcpRoute` → `handleMcp` | The Model Context Protocol server (JSON-RPC 2025-06-18, streamable HTTP); tools: `ask`, `retrieve`. |
37
47
  | `/health` | GET | none | `healthRoute` | Liveness + deployed `index_version` (the deploy-drift guard reads this). |
38
48
  | `/api/ask`, `/v1/ask` | POST | tier | `askRoute` → `handleAsk` | The answer contract: streamed or JSON answer, citations, typed blocks, context echo. Quotas per tier. Optional `datasets: [id…]` narrows the corpora searched (server-intersected with session permissions; an explicitly-empty list is a 400); optional `memories: [id…]` selects the member's memory files to inject. Both selections salt the answer cache — a scoped or memory-flavored answer never serves a plain ask. |
39
49
  | `/api/absence`, `/v1/absence` | POST | tier | `absenceRoute` | Provable absence: exhaustive enumeration over a standard's model plane; `{ verdict: absent \| present, enumerated, matches }`. |
@@ -50,10 +60,11 @@ beyond the inventory: [API.md](API.md).
50
60
  | `/admin/judge`, `/v1/admin/judge` | POST | ADMIN_TOKEN | `handleJudge` | LLM-as-judge scoring (promotion gates). |
51
61
  | `/v1/admin/keys` | POST | ADMIN_TOKEN | `handleCreateKey` | Issue an API key (`oiml_<hex>`, shown once). |
52
62
  | `/v1/admin/keys` | GET | ADMIN_TOKEN | `handleListKeys` | List keys (no secrets). |
63
+ | `/v1/admin/keys/:id` | DELETE | ADMIN_TOKEN | `handleRevokeKey` | Revoke a key. |
53
64
  | `/v1/admin/stats` | GET | ADMIN_TOKEN | `adminStatsRoute` | 7-day telemetry: queries/day/tier, spend by model, feedback, error rate; prunes >90d rows. |
54
65
  | `/docs/:slug.html`, `/docs/:slug.anchors.json` | GET | public | `docsRoute` | Rendered publication documents (metanorma-mirror layer 1) served from R2 under `docs/`, immutable cache; the anchors map (clause number → heading anchor id) powers citation deep links. Public OIML content only. |
55
66
  | `/admin/enrich` | POST | ADMIN_TOKEN | `handleEnrich` | modes: default (context+embed+upsert in place), `context` (generate only, KV-cached), `ab` (generate at an explicit effort with NO side effects — the experiment lane; accepts an admin-gated prompt override for judged comparisons). |
56
- | anything else | any | — | — | `404 not_found`. |
67
+ | anything else | any | — | — | `404 not_found`; a known path under an undeclared method answers `405 method_not_allowed`. |
57
68
 
58
69
  ## Request semantics worth naming
59
70
 
@@ -63,11 +74,14 @@ beyond the inventory: [API.md](API.md).
63
74
  ## Dispatch semantics
64
75
 
65
76
  `fetch` = OPTIONS preflight (204) → `matchRoute(ROUTES, method, path)` →
66
- handler with `{ env, req, ctx, url, path, params }` → 404. Patterns are
67
- segment-exact with `:param` capture and a trailing `*` wildcard
68
- (`/assets/*`); entry order is irrelevant because no pattern overlaps
69
- another. The `/api` and `/v1` twins deliberately share one handler so
70
- the tier split can never diverge between the two publications.
77
+ handler with `{ env, req, ctx, url, path, params }` → 405 when the path
78
+ is served under another method → 404. `ROUTES` is `INFRA_ROUTES` +
79
+ `OPENAPI_SURFACE` (generated from the yaml) mapped through
80
+ `OPENAPI_HANDLERS`. Patterns are segment-exact with `:param` capture and
81
+ a trailing `*` wildcard (`/assets/*`); entry order is irrelevant because
82
+ no pattern overlaps another. The `/api` and `/v1` twins deliberately
83
+ share one handler so the tier split can never diverge between the two
84
+ publications.
71
85
 
72
86
  ## Isolation invariants (binding lint, `npm run lint:wrangler`)
73
87
 
@@ -51,18 +51,25 @@ candidate lanes → pool open → pool-level merges → refinement → window as
51
51
  | 13 | `family-boost` | boost only under `filter.doc_number`; sort always | blocking | mutates `hits[].score`, sorts | Family chunks decisively boosted for doc-scoped queries; the sort establishes the rerank-failure fallback order. |
52
52
  | 14 | `rerank` | `hits.length > 1` | additive | `hits[].rerank_score`, sorts, family pin | Cross-encoder scores; vector order is the designed fallback. Post-rerank family pin for doc-scoped queries. |
53
53
  | 15 | `lexical-rrf` | `hits.length > 1 && lexicalHits.length` | blocking | REPLACES `hits` order | RRF fusion with the full-corpus lexical ranking. Runs even when rerank failed (additive semantics preserve this). |
54
- | 16 | `std-ref-nudge` | query names ISO/IEC/ASTM/EN | blocking | `hits[].rerank_score`, sorts | Standard-reference nudge: chunks CARRYING such a citation get `stdRefNudgeSpread × spread` — the citing clause is the answer to "which standard does X invoke", and generic family prose otherwise fills the window (l5a measured). |
55
- | 17 | `edition-cover` | `!filters.edition && hits.length > 1` | additive | appends `hits` | Registry-driven cover: when a pool holds only stale editions of a document whose ACTIVE edition the documents registry knows, fetch the current edition's chunks and add at `editionCoverDiscount` — steering needs the successor present to demote. |
56
- | 18 | `corpus-scope` | `opts.datasetScope` | blocking | filters `hits` | Dataset scope (the sidebar toggles): drops hits whose corpus the request excludes. LAST of the pool-assembly stages — the lexical union above refills the pool after rerank, so filtering earlier let excluded corpora back in. Corpora the toggle model doesn't name pass untouched. |
57
- | 19 | `term-nudge` | `u.term` | blocking | `hits[].rerank_score`, sorts | Clause whose head IS the asked term gets `termNudgeSpread × spread` (decisive). |
58
- | 20 | `concept-steer` | `glossary.length && hits.length > 1` | blocking | `hits[].rerank_score`, sorts | Vocabulary-link families boosted (`conceptSteerSpread`). |
59
- | 21 | `edition-steer` | `!filters.edition && hits.length > 1` | blocking | `hits[].rerank_score`, sorts | Cross-pub recency boost + family-relative superseded-edition demotion (spread-scaled). |
60
- | 22 | `structural-propagate` | — | blocking | REPLACES `hits` | FABLE TreeExpansion: score blends along the clause tree. |
61
- | 23 | `diversity` | — | blocking | `finalHits` (from `hits`) | Per-publication caps (1 overview / 2–3 clauses; global overview cap 2/6); window cut to `rerankKeep`. FIRST writer of `finalHits`. |
62
- | 21 | `typed-pin` | pin families resolvable | blocking (inner parent-fetch additive) | `finalHits` | Answer-contract v2: one typed unit guaranteed a slot (+ small-to-big parent fetch at `smallToBigDiscount`). |
63
- | 22 | `section-descent` | a ranked depth-1 summary has children | additive | `finalHits` | Summary node → top child clauses at `sectionDescentDiscount`; the summary retires when children answer. |
64
- | 23 | `dedup` | — | blocking | `finalHits` | FABLE ancestor-descendant same-chain collapse (≥0.5 text overlap). |
65
- | 24 | `window-floor` | — | blocking | filters `finalHits` | Evidence-budget cut at `windowFloorFraction` of top; typed/family/unscored exempt; never fewer than two. |
54
+ | 16 | `citation-probe` | citation-question shape + a document named in the TEXT | additive | appends `hits`, appends `notes` | GraphRAG: bibliography-shaped questions get (a) the family's bibliography sections as passages — FTS over the chunk store, deterministic, text fetched from D1 (getByIds returns no text), pushed to `hits` at score 10 — and (b) the graph's `cites` edges for the family as an authoritative per-edition note on the `notes` channel, identifiers codec-normalized. The graph answers the STRUCTURE; the passages ground it verbatim. |
55
+ | 17 | `corpus-scope` | `opts.datasetScope` | blocking | filters `hits` | Dataset scope (the sidebar toggles): drops hits whose corpus the request excludes. LAST of the pool-assembly stages — the lexical union above refills the pool after rerank, so filtering earlier let excluded corpora back in. Corpora the toggle model doesn't name pass untouched. |
56
+ | 18 | `edition-cover` | `!filters.edition && hits.length > 1` | additive | appends `hits` | Registry-driven cover: when a pool holds only stale editions of a document whose ACTIVE edition the documents registry knows, fetch the current edition's chunks and add at `editionCoverDiscount` — steering needs the successor present to demote. |
57
+ | 19 | `std-ref-nudge` | query names ISO/IEC/ASTM/EN | blocking | `hits[].rerank_score`, sorts | Standard-reference nudge: chunks CARRYING such a citation get `stdRefNudgeSpread × spread` — the citing clause is the answer to "which standard does X invoke", and generic family prose otherwise fills the window (l5a measured). |
58
+ | 20 | `term-nudge` | `u.term` | blocking | `hits[].rerank_score`, sorts | Clause whose head IS the asked term gets `termNudgeSpread × spread` (decisive). |
59
+ | 21 | `concept-steer` | `glossary.length && hits.length > 1` | blocking | `hits[].rerank_score`, sorts | Vocabulary-link families boosted (`conceptSteerSpread`). |
60
+ | 22 | `edition-steer` | `!filters.edition && hits.length > 1` | blocking | `hits[].rerank_score`, sorts | Cross-pub recency boost + family-relative superseded-edition demotion (spread-scaled). |
61
+ | 23 | `structural-propagate` | — | blocking | REPLACES `hits` | FABLE TreeExpansion: score blends along the clause tree. |
62
+ | 24 | `diversity` | — | blocking | `finalHits` (from `hits`) | Per-publication caps (1 overview / 2–3 clauses; global overview cap 2/6); window cut to `rerankKeep`. FIRST writer of `finalHits`. |
63
+ | 25 | `typed-pin` | pin families resolvable | blocking (inner parent-fetch additive) | `finalHits` | Answer-contract v2: one typed unit guaranteed a slot (+ small-to-big parent fetch at `smallToBigDiscount`). |
64
+ | 26 | `section-descent` | a ranked depth-1 summary has children | additive | `finalHits` | Summary node → top child clauses at `sectionDescentDiscount`; the summary retires when children answer. |
65
+ | 27 | `dedup` | — | blocking | `finalHits` | FABLE ancestor-descendant same-chain collapse (≥0.5 text overlap). |
66
+ | 28 | `window-floor` | — | blocking | filters `finalHits` | Evidence-budget cut at `windowFloorFraction` of top; typed/family/unscored exempt; never fewer than two. |
67
+
68
+ The `notes` channel (the GraphRAG seam): stages may append structured
69
+ facts to `PipelineContext.notes`; `retrieve()` returns them and the ask
70
+ path merges them into the answer prompt's retrieval note — the same
71
+ channel the vocabulary link rides. `citation-probe` is the first
72
+ writer; any future graph-derived fact uses the same seam.
66
73
 
67
74
  ## Ordering dependencies (why the order is what it is)
68
75
 
@@ -77,7 +84,7 @@ candidate lanes → pool open → pool-level merges → refinement → window as
77
84
  - **`seal` before `overview-demote`/`rerank`**: the declared context is
78
85
  a hard scope, not a preference — steering and reranking happen WITHIN
79
86
  it.
80
- - **`rerank` before every steering stage** (16–18): steering is
87
+ - **`rerank` before every steering stage** (19–22): steering is
81
88
  spread-scaled over rerank scores; steering before rerank would be
82
89
  erased by the re-sort.
83
90
  - **`structural-propagate` after steering, before `diversity`**:
package/package.json CHANGED
@@ -14,7 +14,8 @@
14
14
  "test:ui": "node tests/ui.mjs",
15
15
  "test:bridge": "node --test tests/bridge.test.mjs",
16
16
  "test:units": "node --test --experimental-strip-types tests/*.test.ts",
17
- "build": "rm -rf dist dist-types && esbuild workers/worker_mcp/src/index.ts workers/worker_public/src/index.ts workers/worker_public/src/config.ts workers/worker_public/src/refusal.ts workers/worker_public/src/profile.ts workers/worker_public/src/requestScope.ts --bundle --format=esm --splitting --platform=neutral --outdir=dist --loader:.md=text && tsc -p tsconfig.build.json && cp dist-types/worker_public/src/*.d.ts dist/ && mkdir -p dist/worker_mcp/src && cp dist-types/worker_mcp/src/*.d.ts dist/worker_mcp/src/ && mkdir -p dist/prompts && cp workers/worker_public/prompts/*.md dist/prompts/",
17
+ "gen:openapi-types": "node scripts/gen-openapi-types.mjs",
18
+ "build": "rm -rf dist dist-types && esbuild workers/worker_mcp/src/index.ts workers/worker_public/src/index.ts workers/worker_public/src/config.ts workers/worker_public/src/refusal.ts workers/worker_public/src/profile.ts workers/worker_public/src/requestScope.ts --bundle --format=esm --splitting --platform=neutral --outdir=dist --loader:.md=text && tsc -p tsconfig.build.json && cp dist-types/worker_public/src/*.d.ts dist/ && mkdir -p dist/worker_mcp/src && cp dist-types/worker_mcp/src/*.d.ts dist/worker_mcp/src/ && mkdir -p dist/prompts && cp workers/worker_public/prompts/*.md dist/prompts/ && node scripts/gen-openapi-types.mjs",
18
19
  "prepare": "npm run build",
19
20
  "lint:ports": "node scripts/lint-ports.mjs",
20
21
  "lint:publisher": "node scripts/lint-publisher-leak.mjs",
@@ -24,14 +25,18 @@
24
25
  "@cloudflare/workers-types": "^5.20260911.1",
25
26
  "esbuild": "^0.28.2",
26
27
  "jsdom": "^29.1.1",
28
+ "openapi-typescript": "^7.13.0",
27
29
  "playwright": "^1.62.1",
28
30
  "yaml": "^2.9.1"
29
31
  },
30
32
  "dependencies": {
31
33
  "@astrojs/markdown-satteri": "^0.4.1",
32
- "@astrojs/mdx": "^4.3.14"
34
+ "@astrojs/mdx": "^4.3.14",
35
+ "@konneal/client": "github:konneal/client#22ce599",
36
+ "@konneal/engine": "github:konneal/engine#2d6c39b",
37
+ "@oimlsmart/oiml-pubid": "^1.2.1"
33
38
  },
34
- "version": "0.1.3",
39
+ "version": "0.2.0",
35
40
  "description": "The Konneal engine: the publisher-agnostic build pipeline and API plane for standards intelligence (retrieval, answer contract, verdicts, evaluation).",
36
41
  "license": "BSD-3-Clause",
37
42
  "type": "module",
@@ -57,6 +62,10 @@
57
62
  "types": "./dist/requestScope.d.ts",
58
63
  "default": "./dist/worker_public/src/requestScope.js"
59
64
  },
65
+ "./openapi": "./workers/worker_public/openapi.yaml",
66
+ "./openapi-types": {
67
+ "types": "./dist/openapi-types.d.ts"
68
+ },
60
69
  "./mcp": "./dist/worker_mcp/src/index.js"
61
70
  },
62
71
  "files": [
@@ -1,4 +1,7 @@
1
1
  vars:
2
+ publisher_identity: >-
3
+ the Fixture Organization — a worldwide organization that publishes
4
+ the fixture corpus
2
5
  assistant_identity: >-
3
6
  the fixture assistant — a public service answering questions about
4
7
  the fixture publisher's documents
@@ -21,25 +21,32 @@ export interface Route {
21
21
  handler: RouteHandler;
22
22
  }
23
23
 
24
- export function matchRoute(routes: Route[], method: string, path: string): { route: Route; params: Record<string, string> } | null {
24
+ /** The path half of the match, method-blind: segment-exact with :param
25
+ * capture and a trailing * wildcard. Returns the captured params when
26
+ * the pattern fits, null otherwise. The 405 probe uses it to tell a
27
+ * known path under a wrong method from an unknown path. */
28
+ export function routeMatchesPath(pattern: string, path: string): Record<string, string> | null {
25
29
  const segments = path.split("/").filter(Boolean);
30
+ const patternSegs = pattern.split("/").filter(Boolean);
31
+ if (patternSegs.length !== segments.length && !patternSegs[patternSegs.length - 1]?.startsWith("*")) return null;
32
+ const params: Record<string, string> = {};
33
+ for (let i = 0; i < patternSegs.length; i++) {
34
+ const ps = patternSegs[i];
35
+ if (ps.startsWith("*")) return params; // wildcard matches rest
36
+ if (ps.startsWith(":")) {
37
+ params[ps.slice(1)] = segments[i];
38
+ } else if (ps !== segments[i]) {
39
+ return null;
40
+ }
41
+ }
42
+ return params;
43
+ }
44
+
45
+ export function matchRoute(routes: Route[], method: string, path: string): { route: Route; params: Record<string, string> } | null {
26
46
  for (const route of routes) {
27
47
  if (route.method !== method && route.method !== "*") continue;
28
- const patternSegs = route.pattern.split("/").filter(Boolean);
29
- if (patternSegs.length !== segments.length && !patternSegs[patternSegs.length - 1]?.startsWith("*")) continue;
30
- const params: Record<string, string> = {};
31
- let matched = true;
32
- for (let i = 0; i < patternSegs.length; i++) {
33
- const ps = patternSegs[i];
34
- if (ps.startsWith("*")) break; // wildcard matches rest
35
- if (ps.startsWith(":")) {
36
- params[ps.slice(1)] = segments[i];
37
- } else if (ps !== segments[i]) {
38
- matched = false;
39
- break;
40
- }
41
- }
42
- if (matched) return { route, params };
48
+ const params = routeMatchesPath(route.pattern, path);
49
+ if (params) return { route, params };
43
50
  }
44
51
  return null;
45
52
  }
@@ -0,0 +1,5 @@
1
+ -- Usage telemetry (2026-09-17): the cache mix on the queries ledger.
2
+ -- 'exact' | 'semantic' for served-from-cache answers, NULL for a live
3
+ -- generation — the answer-cache story (hit rates per tier) becomes
4
+ -- measurable instead of inferred.
5
+ ALTER TABLE queries ADD COLUMN cache TEXT;