@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,252 @@
|
|
|
1
|
+
// Auth routes: OIDC authorization-code + PKCE against the estate OP
|
|
2
|
+
// (id.oimlsmart.org), per docs/identity-service.md. The OP declares WHO
|
|
3
|
+
// the user is; quota tiers (what they may do here) are ours.
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
buildAuthorizationUrl,
|
|
7
|
+
buildEndSessionUrl,
|
|
8
|
+
discoverIssuer,
|
|
9
|
+
exchangeCode,
|
|
10
|
+
generatePkce,
|
|
11
|
+
randomToken,
|
|
12
|
+
validateIdToken,
|
|
13
|
+
fetchUserinfo,
|
|
14
|
+
OidcError,
|
|
15
|
+
} from "./oidc";
|
|
16
|
+
import { clearSessionCookie, mintSessionCookie, mintSessionToken, rawSessionToken, readSession, sessionCookieFromToken, SessionClaims } from "./session";
|
|
17
|
+
import { bubbleConfirmPage, isAllowedBubbleOrigin } from "./bubble";
|
|
18
|
+
import { dropOpAccessToken, retainOpAccessToken } from "./livedata";
|
|
19
|
+
|
|
20
|
+
const PLAIN_LANGUAGE: Record<string, string> = {
|
|
21
|
+
not_configured: "Sign-in is not configured for this service yet.",
|
|
22
|
+
discovery: "The sign-in service could not be reached. Please try again shortly.",
|
|
23
|
+
issuer_mismatch: "The sign-in service answered from an unexpected address. Sign-in was refused.",
|
|
24
|
+
exchange: "The sign-in service refused the sign-in. Please try again.",
|
|
25
|
+
state: "That sign-in link has expired. Please start again.",
|
|
26
|
+
token_malformed: "The sign-in service returned an unreadable token. Please try again.",
|
|
27
|
+
token_alg: "The sign-in service returned a token in an unsupported format.",
|
|
28
|
+
token_signature: "The sign-in token could not be verified. Sign-in was refused.",
|
|
29
|
+
token_issuer: "The sign-in token was issued by an unexpected party. Sign-in was refused.",
|
|
30
|
+
token_audience: "The sign-in token was not issued for this service. Sign-in was refused.",
|
|
31
|
+
token_expired: "The sign-in window expired. Please sign in again.",
|
|
32
|
+
token_nonce: "The sign-in response failed its replay check. Please sign in again.",
|
|
33
|
+
origin_not_allowed: "That site may not connect the assistant to your account.",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function authErrorText(reason: string): string {
|
|
37
|
+
return PLAIN_LANGUAGE[reason] ?? "Sign-in failed. Please try again.";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Two tiers only: anonymous (public index) and member (both indexes).
|
|
41
|
+
// Estate roles (mc_member, etc.) are identity claims, NOT access gates
|
|
42
|
+
// here — any authenticated user is a member.
|
|
43
|
+
export interface AuthConfig {
|
|
44
|
+
issuer: string;
|
|
45
|
+
clientId: string;
|
|
46
|
+
redirectUri: string;
|
|
47
|
+
sessionSecret: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function authConfig(env: any): AuthConfig | null {
|
|
51
|
+
const issuer = env.OIDC_ISSUER ?? "https://id.oimlsmart.org";
|
|
52
|
+
const clientId = env.OIDC_CLIENT_ID;
|
|
53
|
+
const redirectUri = env.OIDC_REDIRECT_URI ?? "https://ai.oimlsmart.org/auth/callback";
|
|
54
|
+
const sessionSecret = env.SESSION_SECRET;
|
|
55
|
+
if (!clientId || !sessionSecret) return null;
|
|
56
|
+
return { issuer, clientId, redirectUri, sessionSecret };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const redirectWithError = (reason: string) =>
|
|
60
|
+
new Response(null, {
|
|
61
|
+
status: 302,
|
|
62
|
+
headers: { location: `/?auth_error=${reason}&auth_msg=${encodeURIComponent(authErrorText(reason))}` },
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export async function handleLogin(env: any, req: Request): Promise<Response> {
|
|
66
|
+
const cfg = authConfig(env);
|
|
67
|
+
if (!cfg) return redirectWithError("not_configured");
|
|
68
|
+
// Bubble bridge (bubble.ts): the embedded panel's sign-in. The origin
|
|
69
|
+
// is validated NOW, at flow start, and bound to the state — the
|
|
70
|
+
// callback hands the session token to exactly that origin, never to
|
|
71
|
+
// wherever the request happens to come from later.
|
|
72
|
+
const url0 = new URL(req.url);
|
|
73
|
+
const bubbleMode = url0.searchParams.get("mode") === "bubble";
|
|
74
|
+
const bubbleOrigin = url0.searchParams.get("origin") ?? "";
|
|
75
|
+
if (bubbleMode && !isAllowedBubbleOrigin(bubbleOrigin)) return redirectWithError("origin_not_allowed");
|
|
76
|
+
try {
|
|
77
|
+
const meta = await discoverIssuer(cfg.issuer);
|
|
78
|
+
const state = randomToken();
|
|
79
|
+
const nonce = randomToken();
|
|
80
|
+
const pkce = await generatePkce();
|
|
81
|
+
await env.CACHE.put(
|
|
82
|
+
`oa:${state}`,
|
|
83
|
+
JSON.stringify({ nonce, verifier: pkce.verifier, ...(bubbleMode ? { mode: "bubble", origin: bubbleOrigin } : {}) }),
|
|
84
|
+
{
|
|
85
|
+
expirationTtl: 600,
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
const url = buildAuthorizationUrl(meta, {
|
|
89
|
+
clientId: cfg.clientId,
|
|
90
|
+
redirectUri: cfg.redirectUri,
|
|
91
|
+
scopes: "openid profile email roles",
|
|
92
|
+
state,
|
|
93
|
+
nonce,
|
|
94
|
+
codeChallenge: pkce.challenge,
|
|
95
|
+
});
|
|
96
|
+
return new Response(null, { status: 302, headers: { location: url } });
|
|
97
|
+
} catch (e) {
|
|
98
|
+
return redirectWithError(e instanceof OidcError ? e.reason : "discovery");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function handleCallback(env: any, req: Request): Promise<Response> {
|
|
103
|
+
const cfg = authConfig(env);
|
|
104
|
+
if (!cfg) return redirectWithError("not_configured");
|
|
105
|
+
const url = new URL(req.url);
|
|
106
|
+
const opError = url.searchParams.get("error");
|
|
107
|
+
if (opError) {
|
|
108
|
+
// the OP redirected back with its own failure (user denied consent,
|
|
109
|
+
// session expired at the OP, …) — fail closed, in plain language
|
|
110
|
+
const msg =
|
|
111
|
+
opError === "access_denied"
|
|
112
|
+
? "Sign-in was cancelled."
|
|
113
|
+
: opError === "temporarily_unavailable"
|
|
114
|
+
? "The sign-in service is busy. Please try again in a moment."
|
|
115
|
+
: "The sign-in service reported a problem. Please try again.";
|
|
116
|
+
return new Response(null, {
|
|
117
|
+
status: 302,
|
|
118
|
+
headers: { location: `/?auth_error=${encodeURIComponent(opError)}&auth_msg=${encodeURIComponent(msg)}` },
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const code = url.searchParams.get("code") ?? "";
|
|
122
|
+
const state = url.searchParams.get("state") ?? "";
|
|
123
|
+
if (!code || !state) return redirectWithError("state");
|
|
124
|
+
|
|
125
|
+
const stored = await env.CACHE.get(`oa:${state}`, "json");
|
|
126
|
+
if (!stored) return redirectWithError("state");
|
|
127
|
+
// one-time: the state is consumed whether or not the exchange succeeds
|
|
128
|
+
await env.CACHE.delete(`oa:${state}`);
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const meta = await discoverIssuer(cfg.issuer);
|
|
132
|
+
const token = await exchangeCode(meta, {
|
|
133
|
+
clientId: cfg.clientId,
|
|
134
|
+
clientSecret: env.OIDC_CLIENT_SECRET,
|
|
135
|
+
code,
|
|
136
|
+
redirectUri: cfg.redirectUri,
|
|
137
|
+
codeVerifier: stored.verifier,
|
|
138
|
+
});
|
|
139
|
+
const claims = await validateIdToken(token.id_token, {
|
|
140
|
+
issuer: cfg.issuer,
|
|
141
|
+
clientId: cfg.clientId,
|
|
142
|
+
nonce: stored.nonce,
|
|
143
|
+
jwksUri: meta.jwks_uri,
|
|
144
|
+
});
|
|
145
|
+
const roles = Array.isArray(claims.roles) ? claims.roles.map(String) : [];
|
|
146
|
+
// `picture` is a userinfo claim on this OP (the ID token carries the
|
|
147
|
+
// profile contract only) — fall back to userinfo, sub-checked, when
|
|
148
|
+
// the token happens to omit it
|
|
149
|
+
let picture = typeof claims.picture === "string" ? claims.picture : undefined;
|
|
150
|
+
if (!picture && typeof token.access_token === "string") {
|
|
151
|
+
const ui = await fetchUserinfo(meta, token.access_token);
|
|
152
|
+
if (ui.sub === claims.sub && typeof ui.picture === "string" && ui.picture) picture = ui.picture;
|
|
153
|
+
}
|
|
154
|
+
const sessionClaims = {
|
|
155
|
+
sub: claims.sub,
|
|
156
|
+
name: typeof claims.name === "string" ? claims.name : undefined,
|
|
157
|
+
email: typeof claims.email === "string" ? claims.email : undefined,
|
|
158
|
+
picture,
|
|
159
|
+
roles,
|
|
160
|
+
};
|
|
161
|
+
// Mint ONCE — the cookie and the bubble's Bearer carry the same
|
|
162
|
+
// session token, so the live-data window (TODO.ai-platform/03) keyed
|
|
163
|
+
// off it holds for both presentations.
|
|
164
|
+
const session = await mintSessionToken(cfg.sessionSecret, sessionClaims);
|
|
165
|
+
const cookie = sessionCookieFromToken(session.token);
|
|
166
|
+
// TODO.ai-platform/03: retain the OP access token for the session's
|
|
167
|
+
// exchange window (the "my account" live-data delegation exchanges
|
|
168
|
+
// it per the identity service's RFC 8693 §9b) — KV only, TTL = the
|
|
169
|
+
// OP token's own life, never D1, never past the window.
|
|
170
|
+
if (typeof token.access_token === "string" && typeof token.expires_in === "number") {
|
|
171
|
+
await retainOpAccessToken(env, session.token, token.access_token, token.expires_in);
|
|
172
|
+
}
|
|
173
|
+
// Bubble bridge: hand the session to the embedded panel as a Bearer
|
|
174
|
+
// token via the confirm page — postMessage to the validated origin
|
|
175
|
+
// ONLY, and only on the user's explicit click (bubble.ts). The
|
|
176
|
+
// cookie still sets, so ai.oimlsmart.org itself is signed in too.
|
|
177
|
+
if (stored.mode === "bubble" && typeof stored.origin === "string" && isAllowedBubbleOrigin(stored.origin)) {
|
|
178
|
+
return new Response(
|
|
179
|
+
bubbleConfirmPage({
|
|
180
|
+
name: sessionClaims.name ?? sessionClaims.email ?? "member",
|
|
181
|
+
origin: stored.origin,
|
|
182
|
+
token: session.token,
|
|
183
|
+
expiresAt: session.expiresAt,
|
|
184
|
+
}),
|
|
185
|
+
{ status: 200, headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "set-cookie": cookie } },
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
return new Response(null, { status: 302, headers: { location: "/", "set-cookie": cookie } });
|
|
189
|
+
} catch (e) {
|
|
190
|
+
if (e instanceof OidcError) console.error("auth callback:", e.reason, "—", e.message.slice(0, 200));
|
|
191
|
+
return redirectWithError(e instanceof OidcError ? e.reason : "exchange");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function sessionFrom(req: Request, env: any): Promise<SessionClaims | null> {
|
|
196
|
+
return readSession(req, authConfig(env)?.sessionSecret);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function handleMe(env: any, req: Request): Promise<Response> {
|
|
200
|
+
const cfg = authConfig(env);
|
|
201
|
+
const session = cfg ? await readSession(req, cfg.sessionSecret) : null;
|
|
202
|
+
const headers: Record<string, string> = { "content-type": "application/json" };
|
|
203
|
+
// sliding renewal: a session older than a day re-mints on sight, so
|
|
204
|
+
// active users never hit the 7-day wall mid-conversation
|
|
205
|
+
if (session && cfg && Date.now() - session.iat > 24 * 3600 * 1000) {
|
|
206
|
+
headers["set-cookie"] = await mintSessionCookie(cfg.sessionSecret, {
|
|
207
|
+
sub: session.sub,
|
|
208
|
+
name: session.name,
|
|
209
|
+
email: session.email,
|
|
210
|
+
picture: session.picture,
|
|
211
|
+
roles: session.roles,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return new Response(
|
|
215
|
+
JSON.stringify({
|
|
216
|
+
authenticated: !!session,
|
|
217
|
+
name: session?.name ?? null,
|
|
218
|
+
email: session?.email ?? null,
|
|
219
|
+
picture: session?.picture ?? null,
|
|
220
|
+
roles: session?.roles ?? [],
|
|
221
|
+
tier: session ? "member" : "anon",
|
|
222
|
+
sign_in_available: !!cfg,
|
|
223
|
+
}),
|
|
224
|
+
{ headers },
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export async function handleLogout(env: any, req: Request): Promise<Response> {
|
|
229
|
+
const cfg = authConfig(env);
|
|
230
|
+
const headers: Record<string, string> = { "set-cookie": clearSessionCookie() };
|
|
231
|
+
// The live-data window closes WITH the session (TODO.ai-platform/03 —
|
|
232
|
+
// deliberately, never by the TTL alone).
|
|
233
|
+
const presented = rawSessionToken(req);
|
|
234
|
+
if (presented) await dropOpAccessToken(env, presented);
|
|
235
|
+
if (cfg) {
|
|
236
|
+
try {
|
|
237
|
+
const meta = await discoverIssuer(cfg.issuer);
|
|
238
|
+
const end = buildEndSessionUrl(meta, {
|
|
239
|
+
clientId: cfg.clientId,
|
|
240
|
+
postLogoutRedirectUri: env.OIDC_REDIRECT_URI ?? "https://ai.oimlsmart.org/",
|
|
241
|
+
});
|
|
242
|
+
if (end) {
|
|
243
|
+
headers.location = end;
|
|
244
|
+
return new Response(null, { status: 302, headers });
|
|
245
|
+
}
|
|
246
|
+
} catch {
|
|
247
|
+
// OP-side logout is best-effort; the local session is already cleared
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
headers.location = "/";
|
|
251
|
+
return new Response(null, { status: 302, headers });
|
|
252
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// The bubble bridge (TODO.ai-platform/01 in the smart repo): the
|
|
2
|
+
// assistant panel the shared chrome (@oimlsmart/site-shell's AiBubble)
|
|
3
|
+
// embeds in every estate property cannot ride the rag_session cookie —
|
|
4
|
+
// SameSite=Lax never crosses origins on fetch, and the estate bans
|
|
5
|
+
// shared-domain cookies (the SSO doctrine, identity guide §8a: every
|
|
6
|
+
// property keeps its OWN session, established by its own OP round-trip).
|
|
7
|
+
//
|
|
8
|
+
// The bridge: the panel opens /auth/login?mode=bubble&origin=<its own
|
|
9
|
+
// origin> in a popup. The standard OIDC code+PKCE dance runs unchanged
|
|
10
|
+
// (the OP session makes it one click); the callback then renders a
|
|
11
|
+
// CONFIRM page — never auto-sends — that postMessages the session token
|
|
12
|
+
// (the same HMAC-signed payload the cookie carries) to the requesting
|
|
13
|
+
// origin only. The panel holds the token in memory (sessionStorage
|
|
14
|
+
// survives reloads) and calls the API with `Authorization: Bearer`.
|
|
15
|
+
// The token is the service's OWN session artifact — it can act on this
|
|
16
|
+
// service only, never on the OP or other properties.
|
|
17
|
+
|
|
18
|
+
/** The origins a bubble may ride from: the estate pattern (the same rule
|
|
19
|
+
* corsHeaders applies) plus localhost for the local dev posture. */
|
|
20
|
+
export function isAllowedBubbleOrigin(origin: string): boolean {
|
|
21
|
+
if (origin === "https://oimlsmart.org") return true;
|
|
22
|
+
if (/^https:\/\/[a-z0-9-]+(\.[a-z0-9-]+)*\.oimlsmart\.org$/.test(origin)) return true;
|
|
23
|
+
if (/^http:\/\/localhost(:\d{1,5})?$/.test(origin)) return true;
|
|
24
|
+
if (/^http:\/\/127\.0\.0\.1(:\d{1,5})?$/.test(origin)) return true;
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function escapeHtml(s: string): string {
|
|
29
|
+
return s
|
|
30
|
+
.replace(/&/g, "&")
|
|
31
|
+
.replace(/</g, "<")
|
|
32
|
+
.replace(/>/g, ">")
|
|
33
|
+
.replace(/"/g, """);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The confirm page the callback renders in bubble mode. Static inline
|
|
37
|
+
* HTML+JS, everything escaped, targetOrigin = the validated origin
|
|
38
|
+
* (never "*"). The token leaves ONLY on the user's explicit Continue —
|
|
39
|
+
* a popup the user never asked for (a page opening it under the OP's
|
|
40
|
+
* SSO session) shows the request and stops here. */
|
|
41
|
+
export function bubbleConfirmPage(opts: { name: string; origin: string; token: string; expiresAt: number }): string {
|
|
42
|
+
const who = escapeHtml(opts.name);
|
|
43
|
+
const host = escapeHtml(new URL(opts.origin).host);
|
|
44
|
+
// JSON embedded in a <script> block: escape `<` so a claim value can
|
|
45
|
+
// never close the element early (</script> breakout).
|
|
46
|
+
const jsSafe = (v: unknown) => JSON.stringify(v).replace(/</g, "\\u003c");
|
|
47
|
+
const payload = jsSafe({
|
|
48
|
+
type: "oimlsmart-ai-session",
|
|
49
|
+
token: opts.token,
|
|
50
|
+
name: opts.name,
|
|
51
|
+
expiresAt: opts.expiresAt,
|
|
52
|
+
});
|
|
53
|
+
return `<!DOCTYPE html>
|
|
54
|
+
<html lang="en">
|
|
55
|
+
<head>
|
|
56
|
+
<meta charset="utf-8" />
|
|
57
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
58
|
+
<meta name="robots" content="noindex" />
|
|
59
|
+
<title>OIML SMART AI — sign in</title>
|
|
60
|
+
<style>
|
|
61
|
+
:root { color-scheme: light dark; }
|
|
62
|
+
body { font-family: ui-sans-serif, system-ui, sans-serif; margin: 0; padding: 2rem 1.25rem;
|
|
63
|
+
background: #faf6ee; color: #0a1628; line-height: 1.5; }
|
|
64
|
+
main { max-width: 26rem; margin: 0 auto; }
|
|
65
|
+
h1 { font-size: 1.15rem; margin: 0 0 0.75rem; }
|
|
66
|
+
p { margin: 0 0 1rem; font-size: 0.925rem; }
|
|
67
|
+
.who { font-weight: 600; }
|
|
68
|
+
.row { display: flex; gap: 0.75rem; margin-top: 1.25rem; }
|
|
69
|
+
button { flex: 1; min-height: 44px; font: inherit; font-weight: 600; border-radius: 6px; cursor: pointer; }
|
|
70
|
+
.go { background: #004996; color: #fff; border: 1px solid #004996; }
|
|
71
|
+
.no { background: transparent; color: inherit; border: 1px solid #ddd2bd; }
|
|
72
|
+
@media (prefers-color-scheme: dark) {
|
|
73
|
+
body { background: #0a1628; color: #f5efe4; }
|
|
74
|
+
.go { background: #89b4ef; color: #001230; border-color: #89b4ef; }
|
|
75
|
+
.no { border-color: #1f3357; }
|
|
76
|
+
}
|
|
77
|
+
</style>
|
|
78
|
+
</head>
|
|
79
|
+
<body>
|
|
80
|
+
<main>
|
|
81
|
+
<h1>Continue to the OIML SMART AI assistant?</h1>
|
|
82
|
+
<p>Signed in as <span class="who">${who}</span>. The page at <span class="who">${host}</span>
|
|
83
|
+
asked to connect the assistant to your account, so your conversations sync there.</p>
|
|
84
|
+
<p>The assistant can read the public OIML corpus and your own assistant conversations — nothing else.</p>
|
|
85
|
+
<div class="row">
|
|
86
|
+
<button type="button" class="no" id="cancel">Cancel</button>
|
|
87
|
+
<button type="button" class="go" id="go">Continue</button>
|
|
88
|
+
</div>
|
|
89
|
+
<p id="done" hidden>You can close this window.</p>
|
|
90
|
+
</main>
|
|
91
|
+
<script>
|
|
92
|
+
var TARGET = ${jsSafe(opts.origin)};
|
|
93
|
+
var PAYLOAD = ${payload};
|
|
94
|
+
var done = document.getElementById("done");
|
|
95
|
+
document.getElementById("cancel").addEventListener("click", function () { window.close(); done.hidden = false; });
|
|
96
|
+
document.getElementById("go").addEventListener("click", function () {
|
|
97
|
+
if (window.opener) {
|
|
98
|
+
window.opener.postMessage(PAYLOAD, TARGET);
|
|
99
|
+
window.close();
|
|
100
|
+
}
|
|
101
|
+
done.hidden = false;
|
|
102
|
+
});
|
|
103
|
+
if (!window.opener) {
|
|
104
|
+
document.querySelector(".row").hidden = true;
|
|
105
|
+
document.querySelector("h1").textContent = "Signed in.";
|
|
106
|
+
done.hidden = false;
|
|
107
|
+
}
|
|
108
|
+
</script>
|
|
109
|
+
</body>
|
|
110
|
+
</html>`;
|
|
111
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Contract completion (MECE: ask.ts orchestrates, this module owns the
|
|
2
|
+
// two post-generation completions). Both heal the same gap — the answer
|
|
3
|
+
// references normative objects that contractV2 did not (or could not)
|
|
4
|
+
// resolve into blocks — using the producer's own payloads:
|
|
5
|
+
//
|
|
6
|
+
// table completion: the answer presents a numeric value from a table
|
|
7
|
+
// in the answer's document family → resolve and attach that table
|
|
8
|
+
// figure completion (#172): the model names figure units in PROSE as
|
|
9
|
+
// often as in [[u:…]] tokens, and token refs whose unit wasn't in
|
|
10
|
+
// the used passages drop — either way the answer shows no figure
|
|
11
|
+
// while the payload and asset exist; every u:fig mention in the
|
|
12
|
+
// final text (tokenized or not) resolves from D1
|
|
13
|
+
//
|
|
14
|
+
// Additive throughout: any failure returns what it has.
|
|
15
|
+
import type { StoreQuery } from "./ports/store.ts";
|
|
16
|
+
import { resolveBlocks, type ResolvedBlock } from "./refs.ts";
|
|
17
|
+
import type { Hit } from "./pipeline.ts";
|
|
18
|
+
|
|
19
|
+
export async function completeTables(
|
|
20
|
+
db: StoreQuery,
|
|
21
|
+
answer: string,
|
|
22
|
+
used: Hit[],
|
|
23
|
+
): Promise<ResolvedBlock[]> {
|
|
24
|
+
const blocks: ResolvedBlock[] = [];
|
|
25
|
+
try {
|
|
26
|
+
const answerNums = new Set((answer.match(/\d[\d ,.]{1,8}\d/g) ?? []).map((x) => x.replace(/[ ,.]/g, "")));
|
|
27
|
+
if (!answerNums.size) return blocks;
|
|
28
|
+
// match by docidentifier (the chunks and unit_payloads use different
|
|
29
|
+
// doc_id schemes — 'dirty:r60-1-2006-eng' vs 'mko:oiml-r-60-1' — but
|
|
30
|
+
// both carry the publication name)
|
|
31
|
+
const fams = [...new Set(used.map((h) => h.metadata.docidentifier).filter(Boolean))].slice(0, 3);
|
|
32
|
+
for (const fam of fams) {
|
|
33
|
+
const base = String(fam).replace(/\s*\([A-Z]\)\s*$/, "").split(":")[0].trim();
|
|
34
|
+
const rows = await db
|
|
35
|
+
.prepare("SELECT unit_id, payload FROM unit_payloads WHERE type = 'table' AND docidentifier LIKE ?1 LIMIT 8")
|
|
36
|
+
.bind(`%${base}%`)
|
|
37
|
+
.all<{ unit_id: string; payload: string }>();
|
|
38
|
+
for (const r of rows.results ?? []) {
|
|
39
|
+
const tableNums = new Set((String(r.payload).match(/\d[\d ,.]{1,8}\d/g) ?? []).map((x) => x.replace(/[ ,.]/g, "")));
|
|
40
|
+
let hits = 0;
|
|
41
|
+
for (const n of answerNums) if (tableNums.has(n)) hits++;
|
|
42
|
+
if (hits >= 1) {
|
|
43
|
+
const resolved = await resolveBlocks(db, [r.unit_id]);
|
|
44
|
+
blocks.push(...resolved);
|
|
45
|
+
console.log("contract D1 completion: table", r.unit_id, "in", base, "—", hits, "matching values");
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (blocks.length) break;
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
// additive; primary results stand
|
|
53
|
+
}
|
|
54
|
+
return blocks;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function completeFigures(
|
|
58
|
+
db: StoreQuery,
|
|
59
|
+
answer: string,
|
|
60
|
+
alreadyAttached: ResolvedBlock[],
|
|
61
|
+
): Promise<ResolvedBlock[]> {
|
|
62
|
+
// the model names figures BOTH ways: unit ids (u:fig-3) and bare
|
|
63
|
+
// producer anchors (fig-2a of D 36) — collect both forms; bare
|
|
64
|
+
// anchors get the unit prefix and D1 resolution drops every candidate
|
|
65
|
+
// that is not a real unit (a bare mention can never fabricate a block)
|
|
66
|
+
const unitForm = (answer.match(/u:fig[\w.-]*/g) ?? []).map((x) => x.replace(/[.,;:)]+$/, ""));
|
|
67
|
+
const bareForm = (answer.match(/\bfig-[\w.-]+\b/g) ?? []).map((x) => x.replace(/[.,;:)]+$/, ""));
|
|
68
|
+
const mentioned = [...new Set([...unitForm, ...bareForm.map((x) => (x.startsWith("u:") ? x : "u:" + x))])].slice(0, 6);
|
|
69
|
+
const have = new Set(alreadyAttached.map((b) => b.unit_id));
|
|
70
|
+
const missing = mentioned.filter((id) => !have.has(id));
|
|
71
|
+
if (!missing.length) return [];
|
|
72
|
+
const figs = await resolveBlocks(db, missing);
|
|
73
|
+
if (figs.length) console.log("figure completion:", figs.map((b) => b.unit_id).join(", "), "attached from D1");
|
|
74
|
+
return figs;
|
|
75
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { P } from "./profile.ts";
|
|
2
|
+
|
|
3
|
+
export const MODELS = {
|
|
4
|
+
embed: "@cf/qwen/qwen3-embedding-0.6b", rerank: "@cf/baai/bge-reranker-base",
|
|
5
|
+
// ANSWER MODEL (all tiers, user decision 2026-08-29): glm-5.3-flash —
|
|
6
|
+
// natively multimodal (vision-unified contract), GLM family, flash tier.
|
|
7
|
+
// Vision image-parts land with answer contract v2; text answers work now.
|
|
8
|
+
anon: "@cf/zai-org/glm-5.3-flash",
|
|
9
|
+
member: "@cf/zai-org/glm-5.3-flash",
|
|
10
|
+
// hot-path understanding/summarize stays on the cheap Qwen (cost-first lane)
|
|
11
|
+
understand: "@cf/qwen/qwen3-30b-a3b-fp8",
|
|
12
|
+
// generation fallback when the answer model is unavailable
|
|
13
|
+
fallback: "@cf/qwen/qwen3-30b-a3b-fp8",
|
|
14
|
+
grader: "@cf/deepseek-ai/deepseek-v4-flash-0731", // CRAG grader + judges (unsuffixed slug was retired → silent 5018s)
|
|
15
|
+
// contextual enrichment is the QUALITY-FIRST lane (one-time spend over
|
|
16
|
+
// the corpus, its quality persists into every future retrieval)
|
|
17
|
+
enrich: "@cf/deepseek-ai/deepseek-v4-pro-0813",
|
|
18
|
+
// final-tier listwise reranker for hard/member queries (cascade:
|
|
19
|
+
// cross-encoder prunes, listwise orders jointly)
|
|
20
|
+
listwise: "@cf/zai-org/glm-4.7-flash",
|
|
21
|
+
// deep-research loop (G10): bounded agentic iterations, members-only
|
|
22
|
+
research: "@cf/zai-org/glm-5.3-flash",
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
export const LIMITS = {
|
|
26
|
+
// input sizes: generous — real questions can be long (pasted scenarios,
|
|
27
|
+
// multi-part asks). The context BUDGET is the real governor of what the
|
|
28
|
+
// model sees; these caps only bound abuse.
|
|
29
|
+
maxInputChars: 8000,
|
|
30
|
+
maxOutputTokens: 3072, // qwen3-30b-a3b always reasons; 768 starved the answer entirely
|
|
31
|
+
retrieveK: 50, // Vectorize caps topK at 50 when returnMetadata=all
|
|
32
|
+
rerankKeep: 10, // 8 crowded out dirty-lane goldens once 3k enriched MKO clean chunks entered the pool
|
|
33
|
+
cacheTtlSec: 6 * 3600,
|
|
34
|
+
// context-window budget (estimated tokens) for the assembled prompt —
|
|
35
|
+
// system + summary + history slice + passages must fit or the model
|
|
36
|
+
// request fails. 16k is conservative for the qwen3 tier (pre-budget
|
|
37
|
+
// traffic at ~12k+ never hit a length error); raise via INPUT_TOKEN_BUDGET
|
|
38
|
+
// after watching logs for length rejections
|
|
39
|
+
inputTokenBudget: 16000,
|
|
40
|
+
maxPassageTokens: 900, // per-passage cap (clause chunks with tables can be huge)
|
|
41
|
+
} as const;
|
|
42
|
+
|
|
43
|
+
/** Tuned retrieval thresholds — every number the pipeline steers by.
|
|
44
|
+
* Each carries the measurement that justifies it; changing one without
|
|
45
|
+
* re-running the gates (golden ×3 + annealment ×6) is a guess. */
|
|
46
|
+
export const THRESHOLDS = {
|
|
47
|
+
/** HyDE candidate score discount — hypothetical-answer vectors match
|
|
48
|
+
* differently than question vectors; 0.7 keeps them competitive
|
|
49
|
+
* without letting a bad hypothetical outrank the real query. */
|
|
50
|
+
hydeDiscount: 0.7,
|
|
51
|
+
/** Graph-lane candidate discount — graph-filtered chunks enter the
|
|
52
|
+
* pool below the primary dense lane; they must earn their window
|
|
53
|
+
* slot under the cross-encoder, not by graph membership alone. */
|
|
54
|
+
graphLaneDiscount: 0.75,
|
|
55
|
+
/** Concept-graph candidate discount — same rationale as the graph
|
|
56
|
+
* lane: definitional content enters discounted. */
|
|
57
|
+
conceptGraphDiscount: 0.75,
|
|
58
|
+
/** Multi-hop sub-query discount — sub-question hits are unioned, not
|
|
59
|
+
* RRF-fused; the discount keeps them from dominating the primary
|
|
60
|
+
* ranking on their first appearance. */
|
|
61
|
+
subQueryDiscount: 0.8,
|
|
62
|
+
/** Federated-ISO discount — the public corpus answers by default;
|
|
63
|
+
* internal ISO passages compete but don't preempt. */
|
|
64
|
+
federateDiscount: 0.95,
|
|
65
|
+
/** Standard-reference nudge (L5): when the question asks about an
|
|
66
|
+
* invoked ISO/IEC standard, chunks that CARRY such a citation (their
|
|
67
|
+
* text contains an ISO/IEC identifier) get a spread-scaled boost —
|
|
68
|
+
* the citing clause is the answer, and generic family prose otherwise
|
|
69
|
+
* fills the window (measured: l5a-iso-humidity flips ~1/6 runs). */
|
|
70
|
+
stdRefNudgeSpread: 0.35,
|
|
71
|
+
/** Edition cover — the score multiplier for current-edition chunks the
|
|
72
|
+
* cover stage fetches when a pool holds ONLY stale editions of a
|
|
73
|
+
* document. Just under the top: the point is REPRESENTATION (so
|
|
74
|
+
* edition steering can demote the stale siblings and diversity keeps
|
|
75
|
+
* the current overview), not free ranking. */
|
|
76
|
+
editionCoverDiscount: 0.9,
|
|
77
|
+
/** Overview-chunk demotion — overview chunks repeat title/doctype
|
|
78
|
+
* boilerplate and embed strongly for name-like queries, crowding
|
|
79
|
+
* clause chunks out of the rerank window. */
|
|
80
|
+
overviewDemotion: 0.85,
|
|
81
|
+
/** Glossary-link cosine floor — below this the dense match to a
|
|
82
|
+
* defined term is noise, not a candidate. Calibrated on the
|
|
83
|
+
* drifting→durability probe (durability-def 0.542, noise ~0.3). */
|
|
84
|
+
glossaryCosineFloor: 0.5,
|
|
85
|
+
/** Exact-term nudge (spread multiplier) — clause chunks whose head IS
|
|
86
|
+
* the asked-for term get a decisive nudge because publication headers
|
|
87
|
+
* contain the title words and the reranker alone is unreliable there;
|
|
88
|
+
* >1 so it dominates the spread, unlike the additive steering boosts. */
|
|
89
|
+
termNudgeSpread: 1.5,
|
|
90
|
+
/** Concept-steering rerank boost (spread fraction) — the vocabulary
|
|
91
|
+
* link's defining families get the edition-steering boost idiom.
|
|
92
|
+
* 0.15 is enough to lift in-family content past the cross-encoder's
|
|
93
|
+
* vocabulary bias without overriding genuine relevance. */
|
|
94
|
+
conceptSteerSpread: 0.15,
|
|
95
|
+
/** Cross-publication recency boost (spread fraction) — a
|
|
96
|
+
* current-edition publication ranks over stale ones; composed with
|
|
97
|
+
* the family-relative demotion below. */
|
|
98
|
+
crossPubRecencySpread: 0.1,
|
|
99
|
+
/** Family-relative edition demotion (spread fraction) — superseded
|
|
100
|
+
* editions are demoted when a newer edition of the same publication
|
|
101
|
+
* is in the pool; a sibling one revision back still competes. */
|
|
102
|
+
familyDemoteSpread: 0.4,
|
|
103
|
+
/** Relevance-floored window — passages below this fraction of the
|
|
104
|
+
* top rerank score leave the window (the evidence-budget principle;
|
|
105
|
+
* structural units are exempt). */
|
|
106
|
+
windowFloorFraction: 0.25,
|
|
107
|
+
/** Small-to-big parent fetch discount — the parent clause enters at
|
|
108
|
+
* a discount because it's supplementary grounding, not the answer. */
|
|
109
|
+
smallToBigDiscount: 0.7,
|
|
110
|
+
/** Section-descent child discount — children fetched from a ranked
|
|
111
|
+
* depth-1 summary enter discounted. */
|
|
112
|
+
sectionDescentDiscount: 0.8,
|
|
113
|
+
/** History budget share — the fraction of the context budget the
|
|
114
|
+
* conversation slice may consume; older turns overflow into the
|
|
115
|
+
* compacted summary instead of starving the passages. */
|
|
116
|
+
historyBudgetShare: 0.3,
|
|
117
|
+
} as const;
|
|
118
|
+
|
|
119
|
+
/** Process-expansion — appended to process-intent queries; publisher
|
|
120
|
+
* vocabulary from the profile (read at request time — see profile.ts). */
|
|
121
|
+
export function processExpansion(): string {
|
|
122
|
+
return P().retrieval.process_expansion;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Per-deployment model override: <ROLE>_MODEL (e.g. UNDERSTAND_MODEL)
|
|
126
|
+
* replaces the pinned default for that role. Ops lever for A/B-ing a
|
|
127
|
+
* role's model without a code change; absent/invalid = the default. */
|
|
128
|
+
export function roleModel(env: any, role: keyof typeof MODELS): string {
|
|
129
|
+
const ov = env[`${role.toUpperCase()}_MODEL`];
|
|
130
|
+
return typeof ov === "string" && ov.startsWith("@cf/") ? ov : MODELS[role];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const EFFORTS = new Set(["low", "medium", "high", "max"]);
|
|
134
|
+
|
|
135
|
+
/** Per-deployment effort override for the answer lane (ANSWER_EFFORT).
|
|
136
|
+
* The effort–accuracy curve is front-loaded (DeepSeek-V4.1-Flash report,
|
|
137
|
+
* Fig. 9): serving pins "low" for latency, and any move up must be
|
|
138
|
+
* measured on the golden set — this lever makes that measurable live,
|
|
139
|
+
* same pattern as roleModel. Absent/invalid = "low". */
|
|
140
|
+
export function answerEffort(env: any): string {
|
|
141
|
+
const v = env?.ANSWER_EFFORT;
|
|
142
|
+
return typeof v === "string" && EFFORTS.has(v) ? v : "low";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Per-request effort for the answer lane (the user-facing depth toggle).
|
|
146
|
+
* The vocabulary is validated; elevated efforts (medium+) are a member
|
|
147
|
+
* lane — same cost rule as research — and degrade to the deployment
|
|
148
|
+
* lever for anonymous or silent requests. */
|
|
149
|
+
/** Elevated effort reasons longer — the output budget must grow with it
|
|
150
|
+
* or reasoning starves the content (the GLM-5 rule; measured
|
|
151
|
+
* 2026-09-12: medium at the flat 3072 budget scored 5/38 on the golden
|
|
152
|
+
* set — empty answers — versus 37–38/38 at low). */
|
|
153
|
+
export function effortBudget(effort: string): number {
|
|
154
|
+
if (effort === "medium") return LIMITS.maxOutputTokens * 2;
|
|
155
|
+
if (effort === "high" || effort === "max") return LIMITS.maxOutputTokens * 4;
|
|
156
|
+
return LIMITS.maxOutputTokens;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function requestEffort(env: any, session: unknown, requested: unknown): string {
|
|
160
|
+
if (typeof requested !== "string" || !EFFORTS.has(requested)) return answerEffort(env);
|
|
161
|
+
if (requested === "low") return requested;
|
|
162
|
+
return session ? requested : answerEffort(env);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** The corpus catalog — single source for /api/datasets, the assistant's
|
|
166
|
+
* self-description, and per-corpus model guidance. `session: true`
|
|
167
|
+
* datasets are enabled for signed-in members (federated via the internal
|
|
168
|
+
* service binding). `note` (optional) is injected into the system prompt
|
|
169
|
+
* when passages from this corpus are present — corpus behavior travels
|
|
170
|
+
* with the dataset, not with pipeline code. */
|
|
171
|
+
export interface Dataset {
|
|
172
|
+
id: string;
|
|
173
|
+
label: string;
|
|
174
|
+
description: string;
|
|
175
|
+
/** member-session gated (federated via the internal service binding) */
|
|
176
|
+
session?: boolean;
|
|
177
|
+
/** the estate permission (a role code set in the publisher's
|
|
178
|
+
* identity provider) the session must carry for a session-gated
|
|
179
|
+
* dataset — membership alone is not the bar */
|
|
180
|
+
permission?: string;
|
|
181
|
+
/** the corpora this dataset searches (profile datasets.yaml) — the
|
|
182
|
+
* engine maps no publisher names */
|
|
183
|
+
corpora?: string[];
|
|
184
|
+
note?: string;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// The catalog is publisher data (profile/datasets.yaml, codegen into
|
|
188
|
+
// profile.gen.ts) — the engine ships no publisher facts
|
|
189
|
+
export function DATASETS(): readonly Dataset[] {
|
|
190
|
+
return P().datasets;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** The estate permission gate: a session-gated dataset requires BOTH
|
|
194
|
+
* membership and the named permission (a role code the account carries
|
|
195
|
+
* in id.oimlsmart.org). Enforcement is server-side — the UI's lock is
|
|
196
|
+
* cosmetic; /api/ask federation and the scope intersect here too. */
|
|
197
|
+
export function hasPermission(session: unknown, code: string): boolean {
|
|
198
|
+
const roles = (session as { roles?: unknown } | null)?.roles;
|
|
199
|
+
return Array.isArray(roles) && roles.map(String).includes(code);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function datasetAllowed(d: Dataset, session: unknown): boolean {
|
|
203
|
+
if (!d.session) return true;
|
|
204
|
+
if (!session) return false;
|
|
205
|
+
return hasPermission(session, d.permission ?? "ai-preview");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function datasetsFor(session: unknown): unknown[] {
|
|
209
|
+
return DATASETS().map((d) => ({
|
|
210
|
+
id: d.id,
|
|
211
|
+
label: d.label,
|
|
212
|
+
description: d.description,
|
|
213
|
+
enabled: datasetAllowed(d, session),
|
|
214
|
+
...(d.session
|
|
215
|
+
? { requires: `the ${d.permission ?? "ai-preview"} permission (id.oimlsmart.org)`, authenticated: !!session }
|
|
216
|
+
: {}),
|
|
217
|
+
}));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Empty-state starter questions, served by /api/datasets — UI content
|
|
221
|
+
* comes from the API, never hardcoded in the client. */
|
|
222
|
+
export function SUGGESTIONS(): string[] {
|
|
223
|
+
return [...P().ui.suggestions];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function num(env: Record<string, unknown>, key: string, fallback: number): number {
|
|
227
|
+
const v = Number(env[key]);
|
|
228
|
+
return Number.isFinite(v) && v > 0 ? v : fallback;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function today(): string {
|
|
232
|
+
return new Date().toISOString().slice(0, 10);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function sha256Hex(s: string): Promise<string> {
|
|
236
|
+
const d = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(s));
|
|
237
|
+
return [...new Uint8Array(d)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
238
|
+
}
|