@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.
- package/LICENSE +29 -0
- package/README.md +13 -0
- package/dist/admin.d.ts +26 -0
- package/dist/ai.d.ts +6 -0
- package/dist/anchors.d.ts +6 -0
- package/dist/answercache.d.ts +22 -0
- package/dist/ask.d.ts +5 -0
- package/dist/auth.d.ts +12 -0
- package/dist/bubble.d.ts +14 -0
- package/dist/chunk-LLWPT2XV.js +49 -0
- package/dist/chunk-MB74PTRM.js +114 -0
- package/dist/chunk-WOGQM7DJ.js +197 -0
- package/dist/chunk-WWNCWKKC.js +42 -0
- package/dist/completion.d.ts +5 -0
- package/dist/config.d.ts +154 -0
- package/dist/config.js +37 -0
- package/dist/context.d.ts +115 -0
- package/dist/conversations.d.ts +5 -0
- package/dist/drafts.d.ts +129 -0
- package/dist/env.d.ts +57 -0
- package/dist/faithfulness.d.ts +5 -0
- package/dist/grader.d.ts +3 -0
- package/dist/graph.d.ts +13 -0
- package/dist/hybrid.d.ts +7 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +5373 -0
- package/dist/internal_gateway.d.ts +14 -0
- package/dist/lexical.d.ts +7 -0
- package/dist/livedata.d.ts +77 -0
- package/dist/memories.d.ts +10 -0
- package/dist/modelplane.d.ts +61 -0
- package/dist/oidc.d.ts +73 -0
- package/dist/pipeline.d.ts +57 -0
- package/dist/profile.d.ts +2 -0
- package/dist/profile.gen.d.ts +70 -0
- package/dist/profile.js +8 -0
- package/dist/projects.d.ts +8 -0
- package/dist/prompts/conversational.md +8 -0
- package/dist/prompts/enrichment.md +3 -0
- package/dist/prompts/faithfulness.md +1 -0
- package/dist/prompts/grader.md +5 -0
- package/dist/prompts/listwise.md +3 -0
- package/dist/prompts/precision.md +1 -0
- package/dist/prompts/reflect.md +1 -0
- package/dist/prompts/relevancy.md +1 -0
- package/dist/prompts/research.md +10 -0
- package/dist/prompts/section-summary.md +5 -0
- package/dist/prompts/summarize.md +1 -0
- package/dist/prompts/system.md +18 -0
- package/dist/prompts/understanding.md +17 -0
- package/dist/quota.d.ts +13 -0
- package/dist/reflect.d.ts +5 -0
- package/dist/refs.d.ts +40 -0
- package/dist/refusal.d.ts +9 -0
- package/dist/refusal.js +9 -0
- package/dist/requestScope.d.ts +26 -0
- package/dist/requestScope.js +10 -0
- package/dist/research.d.ts +8 -0
- package/dist/search.d.ts +4 -0
- package/dist/selfquery.d.ts +7 -0
- package/dist/session.d.ts +1 -0
- package/dist/share.d.ts +2 -0
- package/dist/structural.d.ts +27 -0
- package/dist/tablecontext.d.ts +11 -0
- package/dist/understand.d.ts +11 -0
- package/dist/understandContract.d.ts +29 -0
- package/dist/verdict.d.ts +24 -0
- package/docs/API.md +451 -0
- package/docs/ARCHITECTURE.md +302 -0
- package/docs/AUDIT-2026-08-24.md +71 -0
- package/docs/CONTRIBUTOR-AUDIT-2026-08-25.md +147 -0
- package/docs/INGEST-ARCHITECTURE.md +158 -0
- package/docs/MCP.md +92 -0
- package/docs/METANORMA-AI-SERIALIZATION.md +247 -0
- package/docs/MKO-EXPORT-PIPELINE.md +147 -0
- package/docs/REDESIGN-NORMATIVE-RAG-ETSI.md +485 -0
- package/docs/RESEARCH-SOTA-2026.md +243 -0
- package/docs/ROADMAP-SOTA.md +130 -0
- package/docs/SOTA-STAGE-SPECS.md +509 -0
- package/docs/annealment/F1-verdict.md +27 -0
- package/docs/annealment/F10-notes.md +19 -0
- package/docs/annealment/F11-composition.md +17 -0
- package/docs/annealment/F12-passport.md +17 -0
- package/docs/annealment/F2-counterfactual.md +20 -0
- package/docs/annealment/F3-absence.md +21 -0
- package/docs/annealment/F4-instance.md +18 -0
- package/docs/annealment/F5-workflow.md +21 -0
- package/docs/annealment/F6-impact.md +21 -0
- package/docs/annealment/F7-editions.md +18 -0
- package/docs/annealment/F8-selfverify.md +19 -0
- package/docs/annealment/F9-projection-qa.md +17 -0
- package/docs/annealment/L0-locate.md +19 -0
- package/docs/annealment/L1-extract.md +18 -0
- package/docs/annealment/L2-nomenclature.md +22 -0
- package/docs/annealment/L3-geometry.md +23 -0
- package/docs/annealment/L4-composition.md +21 -0
- package/docs/annealment/L5-cross-standard.md +20 -0
- package/docs/annealment/L6-diachrony.md +21 -0
- package/docs/annealment/L7-perception.md +20 -0
- package/docs/annealment/L8-computation.md +22 -0
- package/docs/annealment/L9-instance-process.md +23 -0
- package/docs/annealment/README.md +10 -0
- package/docs/guidelines-metanorma-ai-programme.md +279 -0
- package/docs/identity-onboarding-rag.md +65 -0
- package/docs/identity-service.md +219 -0
- package/docs/knowledge-annealment.md +273 -0
- package/docs/konneal-extraction-plan.md +481 -0
- package/docs/metanorma-for-ai.md +270 -0
- package/docs/mirror-plan.md +36 -0
- package/docs/multi-sdo-architecture.md +191 -0
- package/docs/paper-annealment-comparison.md +259 -0
- package/docs/paper-assets/architecture.svg +94 -0
- package/docs/paper-assets/contract-v2.svg +94 -0
- package/docs/paper-assets/mko-ingest.svg +91 -0
- package/docs/paper-oiml-bulletin.md +402 -0
- package/docs/paper-oiml-bulletin.mdx +419 -0
- package/docs/product-branding-options.md +172 -0
- package/docs/projects-design.md +88 -0
- package/docs/sota-mechanisms.md +184 -0
- package/docs/spec-api.md +77 -0
- package/docs/spec-pipeline.md +126 -0
- package/docs/vector-adapter.md +88 -0
- package/package.json +70 -0
- package/profile/corpora.yaml +5 -0
- package/profile/datasets.yaml +14 -0
- package/profile/prompts.yaml +5 -0
- package/profile/publisher.yaml +17 -0
- package/profile/retrieval.yaml +1 -0
- package/profile/sources.yaml +5 -0
- package/profile/ui.yaml +7 -0
- package/scripts/gen_profile.mjs +33 -0
- package/workers/shared/ai.ts +21 -0
- package/workers/shared/auth.ts +16 -0
- package/workers/shared/chunk.ts +108 -0
- package/workers/shared/oidc.ts +312 -0
- package/workers/shared/router.ts +45 -0
- package/workers/shared/session.ts +104 -0
- package/workers/worker_internal/src/index.ts +157 -0
- package/workers/worker_internal/tsconfig.json +15 -0
- package/workers/worker_internal/wrangler.toml +32 -0
- package/workers/worker_mcp/src/index.ts +175 -0
- package/workers/worker_mcp/tsconfig.json +13 -0
- package/workers/worker_mcp/wrangler.toml +18 -0
- package/workers/worker_public/migrations/0002_conversations.sql +22 -0
- package/workers/worker_public/migrations/0003_shared_conversations.sql +9 -0
- package/workers/worker_public/migrations/0004_graph.sql +16 -0
- package/workers/worker_public/migrations/0005_documents.sql +19 -0
- package/workers/worker_public/migrations/0006_conversation_entities.sql +11 -0
- package/workers/worker_public/migrations/0007_chunks_fts.sql +43 -0
- package/workers/worker_public/migrations/0008_unit_payloads.sql +15 -0
- package/workers/worker_public/migrations/0009_chunks_unit.sql +8 -0
- package/workers/worker_public/migrations/0009_message_context.sql +7 -0
- package/workers/worker_public/migrations/0010_model_nodes.sql +33 -0
- package/workers/worker_public/migrations/0011_schema_union.sql +38 -0
- package/workers/worker_public/migrations/0012_memories.sql +15 -0
- package/workers/worker_public/migrations/0013_projects.sql +21 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2021-11-03/index.d.ts +16306 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2021-11-03/index.ts +16261 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-01-31/index.d.ts +16373 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-01-31/index.ts +16328 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-03-21/index.d.ts +16382 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-03-21/index.ts +16337 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-08-04/index.d.ts +16383 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-08-04/index.ts +16338 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-10-31/index.d.ts +16403 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-10-31/index.ts +16358 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-11-30/index.d.ts +16408 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-11-30/index.ts +16363 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2023-03-01/index.d.ts +16414 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2023-03-01/index.ts +16369 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2023-07-01/index.d.ts +16414 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2023-07-01/index.ts +16369 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/README.md +135 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/entrypoints.svg +53 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/experimental/index.d.ts +17095 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/experimental/index.ts +17050 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/index.d.ts +16306 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/index.ts +16261 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/latest/index.d.ts +16447 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/latest/index.ts +16402 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/oldest/index.d.ts +16306 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/oldest/index.ts +16261 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/package.json +11 -0
- package/workers/worker_public/package.json +13 -0
- package/workers/worker_public/prompts/conversational.md +8 -0
- package/workers/worker_public/prompts/enrichment.md +3 -0
- package/workers/worker_public/prompts/faithfulness.md +1 -0
- package/workers/worker_public/prompts/grader.md +5 -0
- package/workers/worker_public/prompts/listwise.md +3 -0
- package/workers/worker_public/prompts/precision.md +1 -0
- package/workers/worker_public/prompts/reflect.md +1 -0
- package/workers/worker_public/prompts/relevancy.md +1 -0
- package/workers/worker_public/prompts/research.md +10 -0
- package/workers/worker_public/prompts/section-summary.md +5 -0
- package/workers/worker_public/prompts/summarize.md +1 -0
- package/workers/worker_public/prompts/system.md +18 -0
- package/workers/worker_public/prompts/understanding.md +17 -0
- package/workers/worker_public/public/app.js +166 -0
- package/workers/worker_public/public/index.html +48 -0
- package/workers/worker_public/public/style.css +147 -0
- package/workers/worker_public/schema.sql +248 -0
- package/workers/worker_public/src/admin.ts +358 -0
- package/workers/worker_public/src/ai.ts +71 -0
- package/workers/worker_public/src/anchors.ts +41 -0
- package/workers/worker_public/src/answercache.ts +72 -0
- package/workers/worker_public/src/ask.ts +1094 -0
- package/workers/worker_public/src/auth.ts +252 -0
- package/workers/worker_public/src/bubble.ts +111 -0
- package/workers/worker_public/src/completion.ts +75 -0
- package/workers/worker_public/src/config.ts +238 -0
- package/workers/worker_public/src/context.ts +238 -0
- package/workers/worker_public/src/conversations.ts +162 -0
- package/workers/worker_public/src/drafts.ts +497 -0
- package/workers/worker_public/src/env.ts +90 -0
- package/workers/worker_public/src/faithfulness.ts +63 -0
- package/workers/worker_public/src/grader.ts +89 -0
- package/workers/worker_public/src/graph.ts +63 -0
- package/workers/worker_public/src/hybrid.ts +77 -0
- package/workers/worker_public/src/index.ts +441 -0
- package/workers/worker_public/src/internal_gateway.ts +41 -0
- package/workers/worker_public/src/lexical.ts +86 -0
- package/workers/worker_public/src/lib/hit.ts +4 -0
- package/workers/worker_public/src/lib/http.ts +83 -0
- package/workers/worker_public/src/lib/router.ts +4 -0
- package/workers/worker_public/src/livedata.ts +334 -0
- package/workers/worker_public/src/memories.ts +81 -0
- package/workers/worker_public/src/modelplane.ts +213 -0
- package/workers/worker_public/src/oidc.ts +333 -0
- package/workers/worker_public/src/pipeline.ts +377 -0
- package/workers/worker_public/src/ports/blobs.ts +7 -0
- package/workers/worker_public/src/ports/cloudflare/adapters.ts +177 -0
- package/workers/worker_public/src/ports/kv.ts +8 -0
- package/workers/worker_public/src/ports/model.ts +28 -0
- package/workers/worker_public/src/ports/runtime.ts +13 -0
- package/workers/worker_public/src/ports/store.ts +20 -0
- package/workers/worker_public/src/ports/vector.ts +26 -0
- package/workers/worker_public/src/profile.gen.ts +101 -0
- package/workers/worker_public/src/profile.ts +16 -0
- package/workers/worker_public/src/projects.ts +108 -0
- package/workers/worker_public/src/prompts.d.ts +6 -0
- package/workers/worker_public/src/quota.ts +54 -0
- package/workers/worker_public/src/reflect.ts +67 -0
- package/workers/worker_public/src/refs.ts +107 -0
- package/workers/worker_public/src/refusal.ts +65 -0
- package/workers/worker_public/src/requestScope.ts +71 -0
- package/workers/worker_public/src/research.ts +126 -0
- package/workers/worker_public/src/search.ts +56 -0
- package/workers/worker_public/src/selfquery.ts +25 -0
- package/workers/worker_public/src/session.ts +4 -0
- package/workers/worker_public/src/share.ts +53 -0
- package/workers/worker_public/src/stages/conceptGraph.ts +68 -0
- package/workers/worker_public/src/stages/conceptSteer.ts +39 -0
- package/workers/worker_public/src/stages/corpusScope.ts +25 -0
- package/workers/worker_public/src/stages/dedup.ts +10 -0
- package/workers/worker_public/src/stages/dense.ts +73 -0
- package/workers/worker_public/src/stages/diversity.ts +33 -0
- package/workers/worker_public/src/stages/editionCover.ts +63 -0
- package/workers/worker_public/src/stages/editionSteer.ts +88 -0
- package/workers/worker_public/src/stages/familyBoost.ts +22 -0
- package/workers/worker_public/src/stages/federate.ts +22 -0
- package/workers/worker_public/src/stages/glossary.ts +65 -0
- package/workers/worker_public/src/stages/graphLane.ts +31 -0
- package/workers/worker_public/src/stages/hyde.ts +29 -0
- package/workers/worker_public/src/stages/index.ts +69 -0
- package/workers/worker_public/src/stages/lexicalUnion.ts +21 -0
- package/workers/worker_public/src/stages/multiQuery.ts +57 -0
- package/workers/worker_public/src/stages/overviewDemote.ts +14 -0
- package/workers/worker_public/src/stages/poolOpen.ts +10 -0
- package/workers/worker_public/src/stages/propagate.ts +15 -0
- package/workers/worker_public/src/stages/rerank.ts +47 -0
- package/workers/worker_public/src/stages/seal.ts +16 -0
- package/workers/worker_public/src/stages/sectionDescent.ts +61 -0
- package/workers/worker_public/src/stages/stdRefNudge.ts +35 -0
- package/workers/worker_public/src/stages/subQuery.ts +42 -0
- package/workers/worker_public/src/stages/termNudge.ts +24 -0
- package/workers/worker_public/src/stages/typedPin.ts +131 -0
- package/workers/worker_public/src/stages/types.ts +112 -0
- package/workers/worker_public/src/stages/windowFloor.ts +23 -0
- package/workers/worker_public/src/structural.ts +171 -0
- package/workers/worker_public/src/tablecontext.ts +41 -0
- package/workers/worker_public/src/understand.ts +72 -0
- package/workers/worker_public/src/understandContract.ts +67 -0
- package/workers/worker_public/src/verdict.ts +255 -0
- package/workers/worker_public/tsconfig.json +18 -0
- package/workers/worker_public/wrangler.toml +104 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
// The draft tool (TODO.ai-platform/04) — the act-with-confirmation
|
|
2
|
+
// wave's service half. The assistant PREPARES an act; the user commits
|
|
3
|
+
// it in the platform's real UI. The invariants, and how this module
|
|
4
|
+
// keeps them:
|
|
5
|
+
//
|
|
6
|
+
// - THE AI NEVER WRITES. The draft is composed HERE and rides the
|
|
7
|
+
// answer's wire shape back to the panel, which hands it to the
|
|
8
|
+
// platform's real form; the commit is the user's own click through
|
|
9
|
+
// the platform's own write path (the same validation, the same
|
|
10
|
+
// gates, the same audit). This module is STRUCTURALLY write-free:
|
|
11
|
+
// it performs no network IO at all — the ask handler hands it the
|
|
12
|
+
// already-exchanged delegation token (livedata.ts's RFC 8693
|
|
13
|
+
// machinery, read-scoped), the documents registry (D1) and the
|
|
14
|
+
// Workers-AI binding; there is no fetch in this file. A crafted
|
|
15
|
+
// prompt ("submit it now with my token") therefore cannot produce a
|
|
16
|
+
// write — at most a draft that names the user's own click as the
|
|
17
|
+
// only commit. The platform's bearer cone (oimlsmart/smart's
|
|
18
|
+
// auth/bearer.ts) refuses the delegated write class outright either
|
|
19
|
+
// way; the golden suite's draft-must-not legs prove the end-to-end.
|
|
20
|
+
//
|
|
21
|
+
// - THE PREFILL NEVER INVENTS. The extraction (an LLM pass over the
|
|
22
|
+
// conversation) PROPOSES fields; the traceability guard DISPOSES:
|
|
23
|
+
// every drafted value must trace — through its own source span — to
|
|
24
|
+
// the user's own messages, or it is dropped and the answer names
|
|
25
|
+
// the drop. The values the instrument MODEL derives never ride the
|
|
26
|
+
// draft at all: the platform's form derives them on open (the same
|
|
27
|
+
// derivation layer as every prefill), the user confirms each one.
|
|
28
|
+
//
|
|
29
|
+
// - THE REFUSAL SPEAKS THE PLATFORM'S VOCABULARY. The act's
|
|
30
|
+
// permission reads the exchanged token's service_roles — the same
|
|
31
|
+
// roles the platform enforces, re-judged live at the exchange. A
|
|
32
|
+
// role that cannot perform the act gets the honest why (and no
|
|
33
|
+
// draft), never a silent widening.
|
|
34
|
+
//
|
|
35
|
+
// The pilot act is the application prefill. The act vocabulary is the
|
|
36
|
+
// seam's contract (rag docs/API.md §drafts): the TL dispatch, the
|
|
37
|
+
// review comment and the evaluation summary are named follow-ups the
|
|
38
|
+
// same machinery extends to.
|
|
39
|
+
|
|
40
|
+
// ── the wire shapes (the estate contract — the panel and the platform
|
|
41
|
+
// mirror them; rag docs/API.md is the reference) ───────────────────
|
|
42
|
+
|
|
43
|
+
export interface DraftSample {
|
|
44
|
+
serial: string;
|
|
45
|
+
condition?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ApplicationPrefillFields {
|
|
49
|
+
/** the Recommendation, as the estate URN (urn:oiml:pub:r:60:2021) */
|
|
50
|
+
standard_doc: string;
|
|
51
|
+
/** display label ("OIML R 60:2021") */
|
|
52
|
+
standard_label?: string;
|
|
53
|
+
family_designation?: string;
|
|
54
|
+
group_label?: string;
|
|
55
|
+
model_designation?: string;
|
|
56
|
+
description?: string;
|
|
57
|
+
samples?: DraftSample[];
|
|
58
|
+
scheme?: "A" | "B";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface DraftDrop {
|
|
62
|
+
field: string;
|
|
63
|
+
value: string;
|
|
64
|
+
reason: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface DraftAct {
|
|
68
|
+
kind: "draft";
|
|
69
|
+
act: "application_prefill";
|
|
70
|
+
version: 1;
|
|
71
|
+
title: string;
|
|
72
|
+
prepared_at: string;
|
|
73
|
+
/** ALWAYS true — the draft is an input to the real form, never a
|
|
74
|
+
* channel; the user's own click is the only commit. */
|
|
75
|
+
requires_confirmation: true;
|
|
76
|
+
fields: ApplicationPrefillFields;
|
|
77
|
+
dropped?: DraftDrop[];
|
|
78
|
+
notes?: string[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface DraftCitation {
|
|
82
|
+
docidentifier: string;
|
|
83
|
+
edition?: string;
|
|
84
|
+
status?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type DraftRefusalReason =
|
|
88
|
+
| "sign_in_required"
|
|
89
|
+
| "not_configured"
|
|
90
|
+
| "window_expired"
|
|
91
|
+
| "exchange_refused"
|
|
92
|
+
| "role_refused"
|
|
93
|
+
| "standard_unresolved"
|
|
94
|
+
| "extraction_failed";
|
|
95
|
+
|
|
96
|
+
export type DraftVerdict =
|
|
97
|
+
| { status: "draft"; draft: DraftAct; answer: string; citation: DraftCitation | null }
|
|
98
|
+
| { status: "refused"; reason: DraftRefusalReason; answer: string; citation: DraftCitation | null };
|
|
99
|
+
|
|
100
|
+
/** The delegation outcome the ask handler computed (livedata.ts's
|
|
101
|
+
* exchange; this module never performs it). */
|
|
102
|
+
export type DraftDelegation =
|
|
103
|
+
| { status: "ok"; token: string }
|
|
104
|
+
| { status: "unsigned" | "not_configured" | "window_expired" | "refused" | "op_unreachable" };
|
|
105
|
+
|
|
106
|
+
export interface PrepareOpts {
|
|
107
|
+
act: "application_prefill";
|
|
108
|
+
query: string;
|
|
109
|
+
history: Array<{ role: string; content: string }>;
|
|
110
|
+
member: { sub: string } | null;
|
|
111
|
+
delegation: DraftDelegation;
|
|
112
|
+
/** the platform's client id at the OP (the service_roles key) */
|
|
113
|
+
platformClientId?: string;
|
|
114
|
+
/** the extraction model (the cheap understand lane) */
|
|
115
|
+
model: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── the intent detection ─────────────────────────────────────────────
|
|
119
|
+
// Conservative by design: a false negative simply answers as knowledge
|
|
120
|
+
// (the corpus path handles "how do I apply"); a false positive would
|
|
121
|
+
// offer an act the user never asked for. The act verbs bind the
|
|
122
|
+
// application target in either order; "submit/file" map to the DRAFT
|
|
123
|
+
// act too — the answer honestly reframes them (the service never
|
|
124
|
+
// performs).
|
|
125
|
+
|
|
126
|
+
const ACT_VERB = "(?:draft|prepare|pre-?fill|fill\\s+(?:in|out)|start|submit|file|lodge)";
|
|
127
|
+
const ACT_TARGET = "(?:new\\s+)?(?:certification\\s+|type[ -]evaluation\\s+|OIML[- ]CS\\s+)?application";
|
|
128
|
+
const INTENT_RES = [
|
|
129
|
+
new RegExp(`\\b${ACT_VERB}\\b[\\s\\S]{0,60}?\\b${ACT_TARGET}\\b`, "i"),
|
|
130
|
+
new RegExp(`\\b${ACT_TARGET}\\b[\\s\\S]{0,30}?\\b(?:draft|prepare|pre-?fill|for me)\\b`, "i"),
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
/** The draft intent, or null. "What documents does an application
|
|
134
|
+
* need?" and "where is my application?" never trigger — no act verb. */
|
|
135
|
+
export function detectDraftIntent(query: string): "application_prefill" | null {
|
|
136
|
+
// a STATUS read of an existing application is a question, not an act
|
|
137
|
+
if (/\b(?:status|where|progress|state)\b/i.test(query) && /\bapplication\b/i.test(query) && !INTENT_RES[0].test(query)) return null;
|
|
138
|
+
return INTENT_RES.some((re) => re.test(query)) ? "application_prefill" : null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── the platform's role vocabulary (the refusal speaks it) ───────────
|
|
142
|
+
|
|
143
|
+
/** Decode the exchanged token's roles for the platform (the service's
|
|
144
|
+
* OWN fetched token — decoded, never re-validated; livedata.ts's
|
|
145
|
+
* roleFamilyOf posture). */
|
|
146
|
+
function decodeServiceRoles(token: string, platformClientId: string): string[] {
|
|
147
|
+
try {
|
|
148
|
+
const payload = JSON.parse(atob(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/")));
|
|
149
|
+
const roles = payload?.service_roles?.[platformClientId];
|
|
150
|
+
return Array.isArray(roles) ? roles.filter((r: unknown): r is string => typeof r === "string") : [];
|
|
151
|
+
} catch {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The plain-language label for the account's primary role — the
|
|
157
|
+
* refusal names what the account IS, in the platform's own vocabulary. */
|
|
158
|
+
function roleLabel(role: string): string {
|
|
159
|
+
if (role === "tl_operator") return "a test laboratory operator";
|
|
160
|
+
if (["ia_officer", "case_officer", "certification_officer", "signatory"].includes(role)) return "an issuing authority officer";
|
|
161
|
+
if (role === "viewer") return "a read-only viewer";
|
|
162
|
+
if (["cs_admin", "admin"].includes(role)) return "a scheme administrator";
|
|
163
|
+
return `the "${role}" role`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── the extraction (the LLM proposes) ────────────────────────────────
|
|
167
|
+
|
|
168
|
+
const EXTRACTION_SYSTEM = `You extract the fields of a new OIML certification application from the user's own messages.
|
|
169
|
+
|
|
170
|
+
Rules:
|
|
171
|
+
- Output ONLY a JSON object — no prose, no code fence.
|
|
172
|
+
- Copy every value from the user's own words, and for each field give "source": the exact contiguous span of the user's message you copied it from.
|
|
173
|
+
- NEVER infer, complete, normalize away, or guess a value. If the user did not state it, omit the field entirely.
|
|
174
|
+
- "standard": the Recommendation the user named (e.g. "R 60" or "OIML R 60:2021") — a plain string, or omit when none was named.
|
|
175
|
+
- "scheme": only when the user named scheme A or scheme B explicitly.
|
|
176
|
+
|
|
177
|
+
Schema (every field optional):
|
|
178
|
+
{
|
|
179
|
+
"standard": "R 60",
|
|
180
|
+
"family_designation": { "value": "…", "source": "…" },
|
|
181
|
+
"group_label": { "value": "…", "source": "…" },
|
|
182
|
+
"model_designation": { "value": "…", "source": "…" },
|
|
183
|
+
"description": { "value": "…", "source": "…" },
|
|
184
|
+
"scheme": { "value": "A", "source": "…" },
|
|
185
|
+
"samples": [ { "serial": "…", "condition": "…", "source": "…" } ]
|
|
186
|
+
}`;
|
|
187
|
+
|
|
188
|
+
interface Extraction {
|
|
189
|
+
standard?: string;
|
|
190
|
+
family_designation?: { value?: string; source?: string };
|
|
191
|
+
group_label?: { value?: string; source?: string };
|
|
192
|
+
model_designation?: { value?: string; source?: string };
|
|
193
|
+
description?: { value?: string; source?: string };
|
|
194
|
+
scheme?: { value?: string; source?: string };
|
|
195
|
+
samples?: Array<{ serial?: string; condition?: string; source?: string }>;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The extraction pass: the LLM reads the user's turns and PROPOSES the
|
|
199
|
+
* fields, each with the source span it was copied from. The guard
|
|
200
|
+
* disposes — a proposal the user never stated never rides the draft. */
|
|
201
|
+
async function extractDraftFields(ai: any, model: string, userTurns: string[]): Promise<Extraction | null> {
|
|
202
|
+
const transcript = userTurns.map((t, i) => `${i + 1}. ${t}`).join("\n").slice(0, 12000);
|
|
203
|
+
try {
|
|
204
|
+
const res: any = await ai.run(model, {
|
|
205
|
+
messages: [
|
|
206
|
+
{ role: "system", content: EXTRACTION_SYSTEM },
|
|
207
|
+
{ role: "user", content: `The user's messages, oldest first:\n${transcript}` },
|
|
208
|
+
],
|
|
209
|
+
max_tokens: 1200,
|
|
210
|
+
reasoning_effort: "low",
|
|
211
|
+
temperature: 0.1,
|
|
212
|
+
});
|
|
213
|
+
const text = typeof res?.response === "string" ? res.response : res?.choices?.[0]?.message?.content;
|
|
214
|
+
if (typeof text !== "string") return null;
|
|
215
|
+
const start = text.indexOf("{");
|
|
216
|
+
const end = text.lastIndexOf("}");
|
|
217
|
+
if (start < 0 || end <= start) return null;
|
|
218
|
+
const parsed = JSON.parse(text.slice(start, end + 1));
|
|
219
|
+
return parsed && typeof parsed === "object" ? (parsed as Extraction) : null;
|
|
220
|
+
} catch {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ── the traceability guard (the never-invents must-not) ──────────────
|
|
226
|
+
|
|
227
|
+
/** The guard's normalization: case/punctuation-insensitive, hyphen kept
|
|
228
|
+
* (the serials and designations carry it). */
|
|
229
|
+
const norm = (s: string) =>
|
|
230
|
+
s.toLowerCase().replace(/[^a-z0-9-]+/g, " ").replace(/\s+/g, " ").trim();
|
|
231
|
+
/** The document-name normalization: whitespace-free ("R 60" ≡ "R60"). */
|
|
232
|
+
const docNorm = (s: string) => norm(s).replace(/\s+/g, "");
|
|
233
|
+
|
|
234
|
+
export interface GuardedFields {
|
|
235
|
+
standard?: string;
|
|
236
|
+
family_designation?: string;
|
|
237
|
+
group_label?: string;
|
|
238
|
+
model_designation?: string;
|
|
239
|
+
description?: string;
|
|
240
|
+
scheme?: "A" | "B";
|
|
241
|
+
samples?: DraftSample[];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const DROP_REASON = "not stated in your own words";
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* THE GUARD: a proposed field rides the draft only when its source span
|
|
248
|
+
* appears in the user's own messages AND the value appears in the span —
|
|
249
|
+
* the proposal must be the user's words, never the model's completion.
|
|
250
|
+
* Every rejection is a named drop (the answer accounts for each). */
|
|
251
|
+
export function traceabilityGuard(extraction: Extraction, userTurns: string[]): { kept: GuardedFields; dropped: DraftDrop[] } {
|
|
252
|
+
const haystack = norm(userTurns.join("\n"));
|
|
253
|
+
const docHaystack = docNorm(userTurns.join(" "));
|
|
254
|
+
const kept: GuardedFields = {};
|
|
255
|
+
const dropped: DraftDrop[] = [];
|
|
256
|
+
|
|
257
|
+
const traced = (value: unknown, source: unknown): string | null => {
|
|
258
|
+
if (typeof value !== "string" || !value.trim()) return null;
|
|
259
|
+
if (typeof source !== "string" || !source.trim()) return null;
|
|
260
|
+
const v = norm(value);
|
|
261
|
+
const s = norm(source);
|
|
262
|
+
if (!v || !s) return null;
|
|
263
|
+
return s.includes(v) && haystack.includes(s) ? value.trim() : null;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const scalar = (field: "family_designation" | "group_label" | "model_designation" | "description") => {
|
|
267
|
+
const entry = extraction[field];
|
|
268
|
+
const ok = entry ? traced(entry.value, entry.source) : null;
|
|
269
|
+
if (ok) kept[field] = ok.slice(0, 300);
|
|
270
|
+
else if (entry && typeof entry.value === "string" && entry.value.trim()) {
|
|
271
|
+
dropped.push({ field, value: entry.value.trim().slice(0, 120), reason: DROP_REASON });
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
scalar("family_designation");
|
|
275
|
+
scalar("group_label");
|
|
276
|
+
scalar("model_designation");
|
|
277
|
+
scalar("description");
|
|
278
|
+
|
|
279
|
+
const scheme = extraction.scheme;
|
|
280
|
+
const schemeOk = scheme ? traced(scheme.value, scheme.source) : null;
|
|
281
|
+
if (schemeOk && /^[ab]$/i.test(schemeOk.trim())) kept.scheme = schemeOk.trim().toUpperCase() as "A" | "B";
|
|
282
|
+
else if (scheme && typeof scheme.value === "string" && scheme.value.trim()) {
|
|
283
|
+
dropped.push({ field: "scheme", value: scheme.value.trim().slice(0, 20), reason: DROP_REASON });
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const samples: DraftSample[] = [];
|
|
287
|
+
(Array.isArray(extraction.samples) ? extraction.samples : []).forEach((s, i) => {
|
|
288
|
+
const ok = s ? traced(s.serial, s.source) : null;
|
|
289
|
+
if (ok) {
|
|
290
|
+
const sample: DraftSample = { serial: ok.slice(0, 80) };
|
|
291
|
+
if (typeof s?.condition === "string" && s.condition.trim()) sample.condition = s.condition.trim().slice(0, 20).toUpperCase();
|
|
292
|
+
samples.push(sample);
|
|
293
|
+
} else if (s && typeof s.serial === "string" && s.serial.trim()) {
|
|
294
|
+
dropped.push({ field: `samples[${i}].serial`, value: s.serial.trim().slice(0, 80), reason: DROP_REASON });
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
if (samples.length) kept.samples = samples;
|
|
298
|
+
|
|
299
|
+
// The act's anchor: the Recommendation the USER named (whitespace-free
|
|
300
|
+
// matching — "R 60" ≡ "R60" ≡ "r 60"). Untraced = the user never chose
|
|
301
|
+
// it; the compose refuses honestly below.
|
|
302
|
+
if (typeof extraction.standard === "string" && extraction.standard.trim()) {
|
|
303
|
+
if (docHaystack.includes(docNorm(extraction.standard))) kept.standard = extraction.standard.trim().slice(0, 80);
|
|
304
|
+
else dropped.push({ field: "standard", value: extraction.standard.trim().slice(0, 80), reason: DROP_REASON });
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return { kept, dropped };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ── the standard resolution (the corpus registry anchors the act) ────
|
|
311
|
+
|
|
312
|
+
interface ResolvedStandard {
|
|
313
|
+
urn: string;
|
|
314
|
+
label: string;
|
|
315
|
+
edition?: string;
|
|
316
|
+
status?: string;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Resolve the named Recommendation against the documents registry: the
|
|
320
|
+
* family must exist (the ACTIVE edition is the draft's anchor), else
|
|
321
|
+
* the draft honestly has nothing to anchor on. */
|
|
322
|
+
async function resolveStandard(env: any, named: string): Promise<ResolvedStandard | null> {
|
|
323
|
+
const m =
|
|
324
|
+
named.match(/^urn:oiml:pub:([rdbge]):(\d{1,3})(?:-[0-9A-Za-z]+)?(?::(\d{4}))?$/i) ??
|
|
325
|
+
named.match(/^(?:OIML\s+)?([RDBGE])\s*(\d{1,3})(?:-[0-9A-Za-z]+)?(?:\s*:\s*(\d{4}))?$/i);
|
|
326
|
+
if (!m) return null;
|
|
327
|
+
const type = m[1].toUpperCase();
|
|
328
|
+
const num = String(Number(m[2]));
|
|
329
|
+
try {
|
|
330
|
+
const row = (await env.DB.prepare(
|
|
331
|
+
"SELECT docidentifier, edition, status, derived_status FROM documents WHERE family = ?1 AND active = 1 ORDER BY (part IS NULL) DESC, edition DESC LIMIT 1",
|
|
332
|
+
).bind(`${type}-${num}`).first()) as any;
|
|
333
|
+
if (!row) return null;
|
|
334
|
+
const edition = typeof row.edition === "string" ? row.edition : undefined;
|
|
335
|
+
return {
|
|
336
|
+
urn: `urn:oiml:pub:${type.toLowerCase()}:${num}${edition ? `:${edition}` : ""}`,
|
|
337
|
+
label: typeof row.docidentifier === "string" ? row.docidentifier : `OIML ${type} ${num}`,
|
|
338
|
+
...(edition ? { edition } : {}),
|
|
339
|
+
status: typeof row.derived_status === "string" ? row.derived_status : typeof row.status === "string" ? row.status : undefined,
|
|
340
|
+
};
|
|
341
|
+
} catch {
|
|
342
|
+
// a registry read failure proceeds honestly as unresolved — a scoped
|
|
343
|
+
// refusal is more honest than a guessed anchor (context.ts's posture)
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ── the composition (deterministic — the answer text is never an
|
|
349
|
+
// invention channel either) ────────────────────────────────────────
|
|
350
|
+
|
|
351
|
+
const FIELD_LABELS: Array<[keyof GuardedFields, string]> = [
|
|
352
|
+
["family_designation", "the instrument family"],
|
|
353
|
+
["group_label", "the instrument group"],
|
|
354
|
+
["model_designation", "the model designation"],
|
|
355
|
+
["description", "the description"],
|
|
356
|
+
];
|
|
357
|
+
|
|
358
|
+
function refusal(reason: DraftRefusalReason, answer: string): DraftVerdict {
|
|
359
|
+
return { status: "refused", reason, answer, citation: null };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** The draft act's full path: the member gate → the delegation's honest
|
|
363
|
+
* states → the role permission (the platform's vocabulary) → the
|
|
364
|
+
* extraction → the guard → the registry anchor → the draft. */
|
|
365
|
+
export async function prepareDraft(env: any, opts: PrepareOpts): Promise<DraftVerdict> {
|
|
366
|
+
if (!opts.member || opts.delegation.status === "unsigned") {
|
|
367
|
+
return refusal(
|
|
368
|
+
"sign_in_required",
|
|
369
|
+
"Preparing an act starts from your own account — sign in with your OIML SMART account and ask again. " +
|
|
370
|
+
"The draft would still be yours alone: it opens in the real form and only your own click commits it — I never hold a write credential.",
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
if (opts.delegation.status === "not_configured") {
|
|
374
|
+
return refusal(
|
|
375
|
+
"not_configured",
|
|
376
|
+
"This deployment has not wired the live account link, so I cannot prepare acts here. " +
|
|
377
|
+
"I can still explain what the application asks for — just ask.",
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
if (opts.delegation.status === "window_expired") {
|
|
381
|
+
return refusal(
|
|
382
|
+
"window_expired",
|
|
383
|
+
"Your live access window has lapsed — sign in again to refresh it, and I will prepare the draft. " +
|
|
384
|
+
"It stays a draft either way: only your own click in the real form commits it.",
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
if (opts.delegation.status !== "ok") {
|
|
388
|
+
return refusal(
|
|
389
|
+
"exchange_refused",
|
|
390
|
+
"The live role check was refused, so I cannot prepare the draft honestly — the act needs your account's standing. " +
|
|
391
|
+
"Sign in afresh and ask again.",
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// The permission, in the platform's own vocabulary: the pilot act is
|
|
396
|
+
// the applicant's self-served entry. The exchanged token's roles were
|
|
397
|
+
// re-judged live at the exchange — a standing lost mid-session narrows
|
|
398
|
+
// here, exactly as the platform narrows.
|
|
399
|
+
const roles = opts.platformClientId ? decodeServiceRoles(opts.delegation.token, opts.platformClientId) : [];
|
|
400
|
+
if (!roles.includes("applicant")) {
|
|
401
|
+
const primary = roles[0] ?? "unknown";
|
|
402
|
+
return refusal(
|
|
403
|
+
"role_refused",
|
|
404
|
+
`Your account's platform role — ${roleLabel(primary)} — can't prepare a new certification application: ` +
|
|
405
|
+
"that act belongs to the applicant (the manufacturer's own account). Nothing was drafted. " +
|
|
406
|
+
"I can still walk you through what the application asks for — just ask.",
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// The user turns are the ONLY source a field may come from.
|
|
411
|
+
const userTurns = [
|
|
412
|
+
...opts.history.filter((h) => h.role === "user").map((h) => h.content),
|
|
413
|
+
opts.query,
|
|
414
|
+
];
|
|
415
|
+
const extraction = await extractDraftFields(env.AI, opts.model, userTurns);
|
|
416
|
+
if (!extraction) {
|
|
417
|
+
return refusal(
|
|
418
|
+
"extraction_failed",
|
|
419
|
+
"I could not read your requirements reliably just now — nothing was drafted. Ask again in a moment, " +
|
|
420
|
+
"or start the application directly in the portal: every field there is yours either way.",
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
const { kept, dropped } = traceabilityGuard(extraction, userTurns);
|
|
424
|
+
|
|
425
|
+
if (!kept.standard) {
|
|
426
|
+
const untraced = dropped.find((d) => d.field === "standard");
|
|
427
|
+
return refusal(
|
|
428
|
+
"standard_unresolved",
|
|
429
|
+
untraced
|
|
430
|
+
? `I can't anchor the draft: you haven't named the Recommendation in your own words (the ${untraced.value} reading isn't yours). ` +
|
|
431
|
+
"Name it plainly — for example OIML R 60 — and I'll prepare the draft."
|
|
432
|
+
: "I can't anchor the draft: you haven't named the Recommendation. Name it plainly — for example OIML R 60 — and I'll prepare it.",
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
const standard = await resolveStandard(env, kept.standard);
|
|
436
|
+
if (!standard) {
|
|
437
|
+
return refusal(
|
|
438
|
+
"standard_unresolved",
|
|
439
|
+
`I couldn't resolve ${kept.standard} as a publication in the corpus, so I can't anchor the draft. ` +
|
|
440
|
+
"Name the Recommendation plainly — for example OIML R 60 — and I'll prepare it.",
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const fields: ApplicationPrefillFields = {
|
|
445
|
+
standard_doc: standard.urn,
|
|
446
|
+
standard_label: standard.label,
|
|
447
|
+
...(kept.family_designation ? { family_designation: kept.family_designation } : {}),
|
|
448
|
+
...(kept.group_label ? { group_label: kept.group_label } : {}),
|
|
449
|
+
...(kept.model_designation ? { model_designation: kept.model_designation } : {}),
|
|
450
|
+
...(kept.description ? { description: kept.description } : {}),
|
|
451
|
+
...(kept.samples?.length ? { samples: kept.samples } : {}),
|
|
452
|
+
...(kept.scheme ? { scheme: kept.scheme } : {}),
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
const carries: string[] = [`the Recommendation: ${standard.label}`];
|
|
456
|
+
for (const [key, label] of FIELD_LABELS) {
|
|
457
|
+
const v = kept[key];
|
|
458
|
+
if (typeof v === "string") carries.push(`${label}: ${v}`);
|
|
459
|
+
}
|
|
460
|
+
if (kept.scheme) carries.push(`scheme ${kept.scheme}`);
|
|
461
|
+
if (kept.samples?.length) carries.push(`${kept.samples.length} sample${kept.samples.length === 1 ? "" : "s"}: ${kept.samples.map((s) => s.serial).join(", ")}`);
|
|
462
|
+
|
|
463
|
+
const notes = [
|
|
464
|
+
`The technical parameters (capacities, dimensions, classes) stay with you: the form derives what ${standard.label}'s model declares, and you confirm each value.`,
|
|
465
|
+
"Every field in the real form stays editable — the draft is a starting point, never a decision.",
|
|
466
|
+
];
|
|
467
|
+
|
|
468
|
+
const draft: DraftAct = {
|
|
469
|
+
kind: "draft",
|
|
470
|
+
act: "application_prefill",
|
|
471
|
+
version: 1,
|
|
472
|
+
title: `New ${standard.label} application`,
|
|
473
|
+
prepared_at: new Date().toISOString(),
|
|
474
|
+
requires_confirmation: true,
|
|
475
|
+
fields,
|
|
476
|
+
...(dropped.length ? { dropped } : {}),
|
|
477
|
+
notes,
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const answer =
|
|
481
|
+
`I've prepared a draft for a new ${standard.label} application from your own words.\n\n` +
|
|
482
|
+
`What the draft carries:\n${carries.map((c) => `- ${c}`).join("\n")}\n` +
|
|
483
|
+
(dropped.length
|
|
484
|
+
? `\nI left ${dropped.length === 1 ? "this" : "these"} out because you never stated ${dropped.length === 1 ? "it" : "them"} in your own words: ` +
|
|
485
|
+
`${dropped.map((d) => `${d.field.replace(/\[(\d+)\]/, " $1")} ("${d.value}")`).join("; ")}. Say them plainly and I'll add them.\n`
|
|
486
|
+
: "") +
|
|
487
|
+
`\n${notes[0]}\n\n` +
|
|
488
|
+
"The draft opens in the real application form with every field editable — review it carefully. " +
|
|
489
|
+
"Nothing is submitted until you confirm it there yourself: I never hold a write credential; your own click is the only commit.";
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
status: "draft",
|
|
493
|
+
draft,
|
|
494
|
+
answer,
|
|
495
|
+
citation: { docidentifier: standard.label, ...(standard.edition ? { edition: standard.edition } : {}), ...(standard.status ? { status: standard.status } : {}) },
|
|
496
|
+
};
|
|
497
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// The worker's binding surface (typed once, imported everywhere).
|
|
2
|
+
/** The Workers AI binding — run() only. Domain code goes through
|
|
3
|
+
* portModelRunner(env); a precisely-typed AI closes the door on passing
|
|
4
|
+
* the raw binding where a ModelRunner is expected (the v2.107 outage). */
|
|
5
|
+
export interface AiBinding {
|
|
6
|
+
run(model: string, body: unknown): Promise<unknown>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface Env {
|
|
10
|
+
AI: AiBinding;
|
|
11
|
+
VECTORIZE: any;
|
|
12
|
+
EXP_PRIMMEL: any;
|
|
13
|
+
EXP_COMPOSED: any;
|
|
14
|
+
EXP_PLAIN: any;
|
|
15
|
+
EXP_ADC: any;
|
|
16
|
+
EXP_MKO: any;
|
|
17
|
+
EXP_PFLAT: any;
|
|
18
|
+
GLOSSARY: any;
|
|
19
|
+
EXP_DB: D1Database;
|
|
20
|
+
CACHE: KVNamespace;
|
|
21
|
+
DB: D1Database;
|
|
22
|
+
ASSETS: Fetcher;
|
|
23
|
+
INDEX_VERSION: string;
|
|
24
|
+
UNIT_ASSETS: R2Bucket;
|
|
25
|
+
ANON_DAY_ASK: string;
|
|
26
|
+
ANON_DAY_SEARCH: string;
|
|
27
|
+
KEY_DAY_ASK_DEFAULT: string;
|
|
28
|
+
ANON_DAY_HARD_CAP: string;
|
|
29
|
+
ADMIN_TOKEN?: string;
|
|
30
|
+
MEMBER_DAY_ASK?: string;
|
|
31
|
+
ENRICH_MODEL?: string;
|
|
32
|
+
OIDC_ISSUER?: string;
|
|
33
|
+
OIDC_CLIENT_ID?: string;
|
|
34
|
+
OIDC_CLIENT_SECRET?: string;
|
|
35
|
+
OIDC_REDIRECT_URI?: string;
|
|
36
|
+
SESSION_SECRET?: string;
|
|
37
|
+
/** TODO.ai-platform/03 — the "my account" live-data delegation: the
|
|
38
|
+
* platform instance's API base + its client id at the OP (the
|
|
39
|
+
* delegation's scope target). Absent = the account chip honestly
|
|
40
|
+
* reports the live read unwired on this deployment. */
|
|
41
|
+
SMART_PLATFORM_API?: string;
|
|
42
|
+
SMART_PLATFORM_CLIENT_ID?: string;
|
|
43
|
+
INTERNAL_SERVICE?: { fetch(input: RequestInfo, init?: RequestInit): Promise<Response> };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── The port view of the deployment (konneal extraction §5) ──────────
|
|
47
|
+
// Compose the port interfaces over the raw bindings ONCE, at the edges.
|
|
48
|
+
// Domain modules import `portModelRunner(env)` etc. — never env.AI
|
|
49
|
+
// directly; the purity lint enforces it. Zero runtime change: the
|
|
50
|
+
// adapters forward to the same bindings the call sites used before.
|
|
51
|
+
import { cfModelRunner, cfVectorIndex, cfKv, cfBlobs, cfRuntime } from "./ports/cloudflare/adapters.ts";
|
|
52
|
+
import type { ModelRunner } from "./ports/model.ts";
|
|
53
|
+
import type { VectorIndex } from "./ports/vector.ts";
|
|
54
|
+
import type { Kv } from "./ports/kv.ts";
|
|
55
|
+
import type { Blobs } from "./ports/blobs.ts";
|
|
56
|
+
import type { Runtime } from "./ports/runtime.ts";
|
|
57
|
+
|
|
58
|
+
export function portModelRunner(env: Env): ModelRunner {
|
|
59
|
+
return cfModelRunner(env.AI);
|
|
60
|
+
}
|
|
61
|
+
export function portIndex(env: Env, which: "public" | "primmel" | "composed" | "plain" | "adoc" | "mko" | "pflat" | "glossary" = "public"): VectorIndex {
|
|
62
|
+
const b: unknown =
|
|
63
|
+
which === "public" ? env.VECTORIZE
|
|
64
|
+
: which === "primmel" ? env.EXP_PRIMMEL
|
|
65
|
+
: which === "composed" ? env.EXP_COMPOSED
|
|
66
|
+
: which === "plain" ? env.EXP_PLAIN
|
|
67
|
+
: which === "adoc" ? env.EXP_ADC
|
|
68
|
+
: which === "mko" ? env.EXP_MKO
|
|
69
|
+
: which === "pflat" ? env.EXP_PFLAT
|
|
70
|
+
: env.GLOSSARY;
|
|
71
|
+
return cfVectorIndex(b);
|
|
72
|
+
}
|
|
73
|
+
export function portKv(env: Env): Kv {
|
|
74
|
+
return cfKv(env.CACHE);
|
|
75
|
+
}
|
|
76
|
+
export function portBlobs(env: Env): Blobs {
|
|
77
|
+
return cfBlobs(env.UNIT_ASSETS);
|
|
78
|
+
}
|
|
79
|
+
/** Does the deployment bind this lane index? Presence wiring stays in
|
|
80
|
+
* the ports layer so domain stages never touch raw bindings. */
|
|
81
|
+
export function hasLane(env: Env, which: "glossary"): boolean {
|
|
82
|
+
switch (which) {
|
|
83
|
+
case "glossary":
|
|
84
|
+
return !!env.GLOSSARY;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function portRuntime(ctx: ExecutionContext | undefined): Runtime {
|
|
89
|
+
return cfRuntime(ctx);
|
|
90
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// RAGAS-style faithfulness scorer: LLM-as-judge verifies that every claim
|
|
2
|
+
// in the answer is supported by the retrieved passages. This catches
|
|
3
|
+
// hallucinations the golden-set pattern matcher cannot.
|
|
4
|
+
// Ref: docs.ragas.io/en/stable/concepts/metrics/available_metrics/faithfulness
|
|
5
|
+
|
|
6
|
+
// The prompt is data (prompts/faithfulness.md), bundled as text.
|
|
7
|
+
import faithfulnessPrompt from "../prompts/faithfulness.md";
|
|
8
|
+
|
|
9
|
+
export interface FaithfulnessResult {
|
|
10
|
+
score: number; // 0-1 (1 = every claim grounded)
|
|
11
|
+
ungrounded_claims: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function scoreFaithfulness(
|
|
15
|
+
ai: any,
|
|
16
|
+
model: string,
|
|
17
|
+
answer: string,
|
|
18
|
+
passages: string[],
|
|
19
|
+
): Promise<FaithfulnessResult | null> {
|
|
20
|
+
if (!answer || !passages.length) return null;
|
|
21
|
+
const context = passages
|
|
22
|
+
.slice(0, 8)
|
|
23
|
+
.map((p, i) => `[${i + 1}] ${p.replace(/\s+/g, " ").slice(0, 900)}`)
|
|
24
|
+
.join("\n");
|
|
25
|
+
|
|
26
|
+
const t0 = Date.now();
|
|
27
|
+
const timeout = new Promise<null>((r) => setTimeout(() => { console.log(`faithfulness: timeout (${Date.now() - t0}ms)`); r(null); }, 30000));
|
|
28
|
+
const call = (async () => {
|
|
29
|
+
const res: any = await ai.run(model, {
|
|
30
|
+
messages: [
|
|
31
|
+
{
|
|
32
|
+
role: "system",
|
|
33
|
+
content: faithfulnessPrompt.trimEnd(),
|
|
34
|
+
},
|
|
35
|
+
{ role: "user", content: `Answer:\n${answer.slice(0, 2000)}\n\nPassages:\n${context}` },
|
|
36
|
+
],
|
|
37
|
+
max_tokens: 3072,
|
|
38
|
+
reasoning_effort: "low",
|
|
39
|
+
// DeepSeek-V4 card: temp 1.0 / top_p 1.0
|
|
40
|
+
temperature: 1.0,
|
|
41
|
+
top_p: 1.0,
|
|
42
|
+
});
|
|
43
|
+
const text = typeof res?.response === "string" ? res.response : res?.choices?.[0]?.message?.content;
|
|
44
|
+
// reasoning models can emit several {...} fragments before the final
|
|
45
|
+
// verdict — take the LAST flat object that parses with a numeric score
|
|
46
|
+
let parsed: { score?: unknown; ungrounded_claims?: unknown } | null = null;
|
|
47
|
+
for (const m of (text ?? "").matchAll(/\{[^{}]*\}/g)) {
|
|
48
|
+
try {
|
|
49
|
+
const obj = JSON.parse(m[0]);
|
|
50
|
+
if (typeof obj.score === "number") parsed = obj;
|
|
51
|
+
} catch {
|
|
52
|
+
// not JSON — keep scanning
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (!parsed) { console.log(`faithfulness: no parse (${Date.now() - t0}ms, text ${((text ?? "").length)} chars)`); return null; }
|
|
56
|
+
return {
|
|
57
|
+
score: Math.max(0, Math.min(1, parsed.score as number)),
|
|
58
|
+
ungrounded_claims: Array.isArray(parsed.ungrounded_claims) ? parsed.ungrounded_claims.map(String).slice(0, 5) : [],
|
|
59
|
+
};
|
|
60
|
+
})();
|
|
61
|
+
|
|
62
|
+
return await Promise.race([call, timeout]);
|
|
63
|
+
}
|