@konneal/engine 0.2.35 → 0.2.38
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/dist/{ask-P6KLPBKL.js → ask-VXV2BFNC.js} +2 -2
- package/dist/{chunk-GHDRKT4K.js → chunk-6GAYBMUM.js} +76 -0
- package/dist/{chunk-YSWKFXXG.js → chunk-CNFQNCWA.js} +20 -4
- package/dist/{chunk-Z7AQEW6G.js → chunk-GDQOFP6B.js} +1 -1
- package/dist/env.d.ts +5 -0
- package/dist/livedata.d.ts +36 -0
- package/dist/modelplane.d.ts +1 -0
- package/dist/pipeline.d.ts +1 -0
- package/dist/quality.d.ts +6 -0
- package/dist/{search-IBZ2BK2E.js → search-HBE47CK6.js} +2 -2
- package/dist/worker_public/src/index.js +49 -20
- package/package.json +1 -1
- package/workers/shared/router.ts +4 -1
- package/workers/shared/session.ts +3 -2
- package/workers/worker_public/src/ask.ts +24 -5
- package/workers/worker_public/src/env.ts +5 -0
- package/workers/worker_public/src/index.ts +48 -15
- package/workers/worker_public/src/livedata.ts +94 -0
- package/workers/worker_public/src/modelplane.ts +1 -0
- package/workers/worker_public/src/pipeline.ts +2 -0
- package/workers/worker_public/src/quality.ts +48 -0
|
@@ -61,6 +61,27 @@ async function generateOnce(env, model, messages, effort) {
|
|
|
61
61
|
return null;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
// workers/worker_public/src/quality.ts
|
|
65
|
+
var ORDER = { verified: 0, curated: 1, ocr: 2 };
|
|
66
|
+
function hitQuality(m) {
|
|
67
|
+
if (m.model_node || m.model_version || m.producer === "primmel") return "verified";
|
|
68
|
+
if (m.tier === "curated") return "curated";
|
|
69
|
+
return "ocr";
|
|
70
|
+
}
|
|
71
|
+
function answerQuality(qualities) {
|
|
72
|
+
let worst = null;
|
|
73
|
+
for (const q of qualities) {
|
|
74
|
+
if (!q) continue;
|
|
75
|
+
if (!worst || ORDER[q] > ORDER[worst]) worst = q;
|
|
76
|
+
}
|
|
77
|
+
return worst;
|
|
78
|
+
}
|
|
79
|
+
function qualityNote(q) {
|
|
80
|
+
if (q === "verified") return "Grounded in verified sources: this publication's requirements ride a machine-checkable data model.";
|
|
81
|
+
if (q === "curated") return "Grounded in the edited corpus: Metanorma-authored documents, chunked at clause boundaries.";
|
|
82
|
+
return "Partly grounded in experimental OCR text: verify quotations and table values against the official publication.";
|
|
83
|
+
}
|
|
84
|
+
|
|
64
85
|
// workers/worker_public/src/lexical.ts
|
|
65
86
|
var LEXICAL_K = 40;
|
|
66
87
|
function ftsMatchQuery(query) {
|
|
@@ -23515,6 +23536,7 @@ function citations(hits) {
|
|
|
23515
23536
|
status: h.metadata.status ?? "unknown",
|
|
23516
23537
|
superseded_by: h.metadata.superseded_by || void 0,
|
|
23517
23538
|
corpus: h.metadata.corpus || P().publisher.id,
|
|
23539
|
+
quality: hitQuality(h.metadata),
|
|
23518
23540
|
url: publicationUrl(h.metadata),
|
|
23519
23541
|
snippet: h.text.slice(0, 400),
|
|
23520
23542
|
score: h.rerank_score ?? h.score
|
|
@@ -23803,6 +23825,55 @@ async function resolveLiveAccount(env, sessionRaw, member) {
|
|
|
23803
23825
|
if (!read.ok) return { status: "unavailable", reason: read.reason };
|
|
23804
23826
|
return { status: "ok", records: read.records, stores: read.stores, readAt: read.readAt };
|
|
23805
23827
|
}
|
|
23828
|
+
function deviceClientIds(env) {
|
|
23829
|
+
return String(env.OIDC_DEVICE_CLIENT_IDS ?? "").split(/[\s,]+/).filter(Boolean);
|
|
23830
|
+
}
|
|
23831
|
+
function opCfg(env) {
|
|
23832
|
+
return { issuer: String(env.OIDC_ISSUER ?? "").trim().replace(/\/+$/, ""), clientId: String(env.OIDC_CLIENT_ID ?? "") };
|
|
23833
|
+
}
|
|
23834
|
+
function opMemberCanWrite(member) {
|
|
23835
|
+
if (!member) return false;
|
|
23836
|
+
if (member.via !== "op-token") return true;
|
|
23837
|
+
return /\bwrite\b/.test(member.scope ?? "");
|
|
23838
|
+
}
|
|
23839
|
+
function opMemberFromIntrospection(answer, ids) {
|
|
23840
|
+
if (!answer?.active) return null;
|
|
23841
|
+
const clientId = String(answer.client_id ?? "");
|
|
23842
|
+
if (!ids.includes(clientId)) return null;
|
|
23843
|
+
const sub = String(answer.sub ?? "");
|
|
23844
|
+
if (!sub) return null;
|
|
23845
|
+
return { sub, scope: String(answer.scope ?? ""), via: "op-token" };
|
|
23846
|
+
}
|
|
23847
|
+
async function opTokenMember(env, cfg, req) {
|
|
23848
|
+
const m = /^Bearer\s+(.+)$/i.exec((req.headers.get("Authorization") ?? "").trim());
|
|
23849
|
+
const token = m?.[1]?.trim();
|
|
23850
|
+
if (!token || token.split(".").length === 3) return null;
|
|
23851
|
+
const ids = deviceClientIds(env);
|
|
23852
|
+
if (!ids.length || !cfg.issuer || !cfg.clientId) return null;
|
|
23853
|
+
const key = `opint:${await sha256Hex2(token)}`;
|
|
23854
|
+
let answer = null;
|
|
23855
|
+
try {
|
|
23856
|
+
answer = await env.CACHE.get(key, "json");
|
|
23857
|
+
} catch {
|
|
23858
|
+
}
|
|
23859
|
+
if (answer === void 0 || answer === null) {
|
|
23860
|
+
try {
|
|
23861
|
+
const res = await fetch(`${cfg.issuer}/op/introspect`, {
|
|
23862
|
+
method: "POST",
|
|
23863
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
23864
|
+
body: new URLSearchParams({ token, client_id: cfg.clientId })
|
|
23865
|
+
});
|
|
23866
|
+
answer = res.ok ? await res.json() : { active: false };
|
|
23867
|
+
} catch {
|
|
23868
|
+
return null;
|
|
23869
|
+
}
|
|
23870
|
+
try {
|
|
23871
|
+
await env.CACHE.put(key, JSON.stringify(answer ?? { active: false }), { expirationTtl: 45 });
|
|
23872
|
+
} catch {
|
|
23873
|
+
}
|
|
23874
|
+
}
|
|
23875
|
+
return opMemberFromIntrospection(answer, ids);
|
|
23876
|
+
}
|
|
23806
23877
|
|
|
23807
23878
|
// workers/worker_public/src/oidc.ts
|
|
23808
23879
|
var OidcError = class extends Error {
|
|
@@ -24511,6 +24582,8 @@ async function editionNote(env, u) {
|
|
|
24511
24582
|
export {
|
|
24512
24583
|
embed,
|
|
24513
24584
|
generateOnce,
|
|
24585
|
+
answerQuality,
|
|
24586
|
+
qualityNote,
|
|
24514
24587
|
ftsMatchQuery,
|
|
24515
24588
|
refCodec,
|
|
24516
24589
|
NO_CONTEXT,
|
|
@@ -24537,6 +24610,9 @@ export {
|
|
|
24537
24610
|
liveDataConfig,
|
|
24538
24611
|
liveTokenFor,
|
|
24539
24612
|
resolveLiveAccount,
|
|
24613
|
+
opCfg,
|
|
24614
|
+
opMemberCanWrite,
|
|
24615
|
+
opTokenMember,
|
|
24540
24616
|
handleLogin,
|
|
24541
24617
|
handleCallback,
|
|
24542
24618
|
sessionFrom,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
NO_CONTEXT,
|
|
3
|
+
answerQuality,
|
|
3
4
|
appliedContext,
|
|
4
5
|
buildMessages,
|
|
5
6
|
checkQuota,
|
|
@@ -16,9 +17,12 @@ import {
|
|
|
16
17
|
liveTokenFor,
|
|
17
18
|
machineOffers,
|
|
18
19
|
namedDocumentIn,
|
|
20
|
+
opCfg,
|
|
21
|
+
opTokenMember,
|
|
19
22
|
parseContext,
|
|
20
23
|
portModelRunner,
|
|
21
24
|
portStore,
|
|
25
|
+
qualityNote,
|
|
22
26
|
rawSessionToken,
|
|
23
27
|
refCodec,
|
|
24
28
|
resolveDocScope,
|
|
@@ -30,7 +34,7 @@ import {
|
|
|
30
34
|
syntheticUnderstanding,
|
|
31
35
|
telemetry,
|
|
32
36
|
understandQuery
|
|
33
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-6GAYBMUM.js";
|
|
34
38
|
import {
|
|
35
39
|
corsHeaders,
|
|
36
40
|
err,
|
|
@@ -495,6 +499,7 @@ function modelCitation(node) {
|
|
|
495
499
|
clause_title: `${node.kind.replace(/_/g, " ")} \u2014 ${node.name} (${node.node_id})`,
|
|
496
500
|
status: "in-force",
|
|
497
501
|
corpus: "smart-model",
|
|
502
|
+
quality: "verified",
|
|
498
503
|
url: void 0,
|
|
499
504
|
snippet: `${node.node_id}${node.clause ? ` \xB7 ${node.clause.urn}` : ""}${node.content?.statement ? ` \u2014 ${clip(node.content.statement, 240)}` : ""}`,
|
|
500
505
|
score: 1
|
|
@@ -1895,7 +1900,16 @@ async function handleAsk(env, ctx, req, tier, key) {
|
|
|
1895
1900
|
const declaredCtx = parseContext(body);
|
|
1896
1901
|
const draftAct = P().publisher.features?.drafts ? detectDraftIntent(q.query) : null;
|
|
1897
1902
|
const apiCallIntent = !draftAct && P().publisher.features?.api_call_drafts ? detectApiCallIntent(q.query, declaredCtx) : null;
|
|
1898
|
-
const
|
|
1903
|
+
const session = tier === "member" ? await sessionFrom(req, env) : null;
|
|
1904
|
+
const opMember = !session && tier === "member" ? await opTokenMember(env, opCfg(env), req) : null;
|
|
1905
|
+
const member = session ?? (opMember ? {
|
|
1906
|
+
sub: opMember.sub,
|
|
1907
|
+
roles: [],
|
|
1908
|
+
iat: Math.floor(Date.now() / 1e3),
|
|
1909
|
+
exp: Math.floor(Date.now() / 1e3) + 3600,
|
|
1910
|
+
via: "op-token",
|
|
1911
|
+
scope: opMember.scope
|
|
1912
|
+
} : null);
|
|
1899
1913
|
const effort = requestEffort(env, member, body?.effort);
|
|
1900
1914
|
const limit = tier === "key" ? key.day_limit : tier === "member" || member ? num(env, "MEMBER_DAY_ASK", 300) : num(env, "ANON_DAY_ASK", 20);
|
|
1901
1915
|
const bucketId = tier === "key" ? `key:${key.id}` : member ? `sub:${member.sub}` : clientIp(req);
|
|
@@ -2460,7 +2474,8 @@ Answer account questions from these records ONLY: name the record when you use i
|
|
|
2460
2474
|
|
|
2461
2475
|
`));
|
|
2462
2476
|
send({ type: "read", read: readAs() });
|
|
2463
|
-
|
|
2477
|
+
const sourceQuality = answerQuality(cites.map((c) => c.quality));
|
|
2478
|
+
send({ type: "citations", citations: cites, context_applied: ctxApplied, ...sourceQuality ? { source_quality: sourceQuality, quality_note: qualityNote(sourceQuality) } : {}, ...liveRecords ? { records: liveRecords } : {}, quota });
|
|
2464
2479
|
let full = "";
|
|
2465
2480
|
try {
|
|
2466
2481
|
for await (const tok of sseTokens(stream)) {
|
|
@@ -2593,7 +2608,8 @@ Answer account questions from these records ONLY: name the record when you use i
|
|
|
2593
2608
|
if (completionBlocks.length) console.log("contract completion:", completionBlocks.length, "table block(s) attached server-side");
|
|
2594
2609
|
}
|
|
2595
2610
|
completionBlocks.push(...await completeFigures(env.DB, answer, [...c2ns.blocks, ...completionBlocks], used));
|
|
2596
|
-
const
|
|
2611
|
+
const jsonQuality = answerQuality(finalCites.map((c) => c.quality));
|
|
2612
|
+
const out = { answer, citations: finalCites, ...jsonQuality ? { source_quality: jsonQuality, quality_note: qualityNote(jsonQuality) } : {}, model: MODELS.member, query_hash: queryHash, follow_ups: understanding?.follow_ups ?? [], blocks: [...c2ns.blocks, ...verdictBlock ? [verdictBlock] : [], ...conditionBlock ? [conditionBlock] : [], ...aggregationBlock ? [aggregationBlock] : [], ...completionBlocks], context_applied: ctxApplied, ...liveRecords ? { records: liveRecords } : {} };
|
|
2597
2613
|
const cacheable = !contextual && !declaredCtx && !answer.includes(refusalAnswer()) && finalAnchors.violations.length === 0;
|
|
2598
2614
|
if (cacheable) {
|
|
2599
2615
|
const warmVec = await warmEmbed ?? null;
|
package/dist/env.d.ts
CHANGED
|
@@ -31,6 +31,11 @@ export interface Env {
|
|
|
31
31
|
OIDC_CLIENT_ID?: string;
|
|
32
32
|
OIDC_CLIENT_SECRET?: string;
|
|
33
33
|
OIDC_REDIRECT_URI?: string;
|
|
34
|
+
/** TODO.ai-platform/14 — the Ommisa tier: the OP device-grant client
|
|
35
|
+
* ids (comma/space separated) whose opaque access tokens this
|
|
36
|
+
* deployment admits as member-read credentials via introspection.
|
|
37
|
+
* Empty/absent = the tier is off. */
|
|
38
|
+
OIDC_DEVICE_CLIENT_IDS?: string;
|
|
34
39
|
SESSION_SECRET?: string;
|
|
35
40
|
/** TODO.ai-platform/03 — the "my account" live-data delegation: the
|
|
36
41
|
* platform instance's API base + its client id at the OP (the
|
package/dist/livedata.d.ts
CHANGED
|
@@ -88,3 +88,39 @@ export declare function resolveLiveAccount(env: any, sessionRaw: string | null,
|
|
|
88
88
|
via?: string;
|
|
89
89
|
scope?: string;
|
|
90
90
|
} | null): Promise<LiveAccount>;
|
|
91
|
+
/** The device-grant clients this deployment admits. Empty = the tier is
|
|
92
|
+
* off (a Bearer token that is not a service session stays anonymous). */
|
|
93
|
+
export declare function deviceClientIds(env: any): string[];
|
|
94
|
+
/** The introspection call's OP coordinates, read once per call site. */
|
|
95
|
+
export declare function opCfg(env: any): {
|
|
96
|
+
issuer: string;
|
|
97
|
+
clientId: string;
|
|
98
|
+
};
|
|
99
|
+
export interface OpMember {
|
|
100
|
+
sub: string;
|
|
101
|
+
scope: string;
|
|
102
|
+
via: "op-token";
|
|
103
|
+
}
|
|
104
|
+
/** Mutations through the device-grant bearer (memories, conversations,
|
|
105
|
+
* projects — the member's OWN data) need the token's scope to state a
|
|
106
|
+
* write. The grammar is the OP's PAT vocabulary (`<service>:write`);
|
|
107
|
+
* a read-only approval stays read-only here, and service sessions
|
|
108
|
+
* (the browser) are always full-strength. */
|
|
109
|
+
export declare function opMemberCanWrite(member: {
|
|
110
|
+
via?: string;
|
|
111
|
+
scope?: string;
|
|
112
|
+
} | null): boolean;
|
|
113
|
+
/** The introspection answer → the member credential, judged against the
|
|
114
|
+
* deployment's allowlist. Inactive, foreign-client or subject-less
|
|
115
|
+
* answers all resolve to null — never to a widened guess. */
|
|
116
|
+
export declare function opMemberFromIntrospection(answer: any, ids: string[]): OpMember | null;
|
|
117
|
+
/** The Bearer read + introspection, KV-cached 45s keyed by the token's
|
|
118
|
+
* hash (an ask burst costs one introspection; a deactivation lands
|
|
119
|
+
* within a minute). The cached object is the OP's RAW answer — the
|
|
120
|
+
* allowlist re-judges on every read, so a config change takes effect
|
|
121
|
+
* inside the cache window too. JWTs (3 dot-segments) are NOT opaque:
|
|
122
|
+
* they belong to the delegated lane and never reach the OP here. */
|
|
123
|
+
export declare function opTokenMember(env: any, cfg: {
|
|
124
|
+
issuer: string;
|
|
125
|
+
clientId: string;
|
|
126
|
+
}, req: Request): Promise<OpMember | null>;
|
package/dist/modelplane.d.ts
CHANGED
package/dist/pipeline.d.ts
CHANGED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ChunkMeta } from "../../shared/chunk.ts";
|
|
2
|
+
export type SourceQuality = "verified" | "curated" | "ocr";
|
|
3
|
+
export declare function hitQuality(m: ChunkMeta): SourceQuality;
|
|
4
|
+
export declare function answerQuality(qualities: (SourceQuality | undefined)[]): SourceQuality | null;
|
|
5
|
+
/** The one-line posture the interface renders under the answer. */
|
|
6
|
+
export declare function qualityNote(q: SourceQuality): string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
handleSearch
|
|
3
|
-
} from "../../chunk-
|
|
3
|
+
} from "../../chunk-GDQOFP6B.js";
|
|
4
4
|
import {
|
|
5
5
|
bindModelNode,
|
|
6
6
|
checkQuoteAnchors,
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
modelGroundingBlock,
|
|
10
10
|
scoreJudge,
|
|
11
11
|
standardForDocNumber
|
|
12
|
-
} from "../../chunk-
|
|
12
|
+
} from "../../chunk-CNFQNCWA.js";
|
|
13
13
|
import {
|
|
14
14
|
buildMessages,
|
|
15
15
|
citations,
|
|
@@ -24,6 +24,9 @@ import {
|
|
|
24
24
|
handleLogout,
|
|
25
25
|
handleMe,
|
|
26
26
|
namedDocumentIn,
|
|
27
|
+
opCfg,
|
|
28
|
+
opMemberCanWrite,
|
|
29
|
+
opTokenMember,
|
|
27
30
|
parseAppliedContext,
|
|
28
31
|
portModelRunner,
|
|
29
32
|
promptVars,
|
|
@@ -32,7 +35,7 @@ import {
|
|
|
32
35
|
sessionFrom,
|
|
33
36
|
telemetry,
|
|
34
37
|
understandQuery
|
|
35
|
-
} from "../../chunk-
|
|
38
|
+
} from "../../chunk-6GAYBMUM.js";
|
|
36
39
|
import {
|
|
37
40
|
authenticate,
|
|
38
41
|
corsHeaders,
|
|
@@ -996,7 +999,7 @@ async function handleMcp(env, ctx, req, tier, key) {
|
|
|
996
999
|
// stream:false forces the JSON lane (anon defaults to SSE)
|
|
997
1000
|
body: JSON.stringify({ ...args, stream: false })
|
|
998
1001
|
});
|
|
999
|
-
const res = name === "ask" ? await (await import("../../ask-
|
|
1002
|
+
const res = name === "ask" ? await (await import("../../ask-VXV2BFNC.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-HBE47CK6.js")).handleSearch(env, ctx, inner, tier, key);
|
|
1000
1003
|
return res.json().catch(() => ({ error: { message: "tool transport failed", status: res.status } }));
|
|
1001
1004
|
});
|
|
1002
1005
|
if (out.ok && "accepted" in out) return new Response(null, { status: 202 });
|
|
@@ -1025,8 +1028,9 @@ function routeMatchesPath(pattern, path) {
|
|
|
1025
1028
|
return params;
|
|
1026
1029
|
}
|
|
1027
1030
|
function matchRoute(routes, method, path) {
|
|
1031
|
+
const effective = method === "HEAD" ? "GET" : method;
|
|
1028
1032
|
for (const route of routes) {
|
|
1029
|
-
if (route.method !==
|
|
1033
|
+
if (route.method !== effective && route.method !== "*") continue;
|
|
1030
1034
|
const params = routeMatchesPath(route.pattern, path);
|
|
1031
1035
|
if (params) return { route, params };
|
|
1032
1036
|
}
|
|
@@ -1307,30 +1311,54 @@ async function serveIndexPage(c) {
|
|
|
1307
1311
|
}
|
|
1308
1312
|
return err(404, "not_found", "Page not found");
|
|
1309
1313
|
}
|
|
1310
|
-
|
|
1314
|
+
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
1315
|
+
async function opIdentity(c, signIn) {
|
|
1311
1316
|
const session = await sessionFrom(c.req, c.env);
|
|
1312
|
-
if (
|
|
1313
|
-
|
|
1317
|
+
if (session) return { sub: session.sub, write: true };
|
|
1318
|
+
const member = await opTokenMember(c.env, opCfg(c.env), c.req);
|
|
1319
|
+
if (!member) return err(401, "unauthorized", signIn);
|
|
1320
|
+
return { sub: member.sub, write: opMemberCanWrite(member) };
|
|
1321
|
+
}
|
|
1322
|
+
function opWriteRefused(c, who) {
|
|
1323
|
+
if (!(who instanceof Response) && MUTATING_METHODS.has(c.req.method) && !who.write) {
|
|
1324
|
+
return err(403, "forbidden", "This sign-in grants read only \u2014 request a write scope at login.");
|
|
1325
|
+
}
|
|
1326
|
+
return null;
|
|
1327
|
+
}
|
|
1328
|
+
async function memoriesRoute(c) {
|
|
1329
|
+
const who = await opIdentity(c, "Sign in to use memory files");
|
|
1330
|
+
if (who instanceof Response) return withCors(who, corsHeaders(c.req));
|
|
1331
|
+
const refused = opWriteRefused(c, who);
|
|
1332
|
+
if (refused) return withCors(refused, corsHeaders(c.req));
|
|
1333
|
+
return withCors(await handleMemories(c.env, who.sub, c.req, { method: c.req.method, id: c.params.id }), corsHeaders(c.req));
|
|
1314
1334
|
}
|
|
1315
1335
|
async function projectsRoute(c) {
|
|
1316
|
-
const
|
|
1317
|
-
if (
|
|
1318
|
-
|
|
1336
|
+
const who = await opIdentity(c, "Sign in to use projects");
|
|
1337
|
+
if (who instanceof Response) return withCors(who, corsHeaders(c.req));
|
|
1338
|
+
const refused = opWriteRefused(c, who);
|
|
1339
|
+
if (refused) return withCors(refused, corsHeaders(c.req));
|
|
1340
|
+
return withCors(await handleProjects(c.env, who.sub, c.req, { method: c.req.method, id: c.params.id }), corsHeaders(c.req));
|
|
1319
1341
|
}
|
|
1320
1342
|
async function projectFilesRoute(c) {
|
|
1321
|
-
const
|
|
1322
|
-
if (
|
|
1323
|
-
|
|
1343
|
+
const who = await opIdentity(c, "Sign in to use projects");
|
|
1344
|
+
if (who instanceof Response) return withCors(who, corsHeaders(c.req));
|
|
1345
|
+
const refused = opWriteRefused(c, who);
|
|
1346
|
+
if (refused) return withCors(refused, corsHeaders(c.req));
|
|
1347
|
+
return withCors(await handleProjectFiles(c.env, who.sub, c.req, { method: c.req.method, id: c.params.id }), corsHeaders(c.req));
|
|
1324
1348
|
}
|
|
1325
1349
|
async function conversationsRoute(c) {
|
|
1326
|
-
const
|
|
1327
|
-
if (
|
|
1328
|
-
|
|
1350
|
+
const who = await opIdentity(c, "Sign in to sync your conversations across devices");
|
|
1351
|
+
if (who instanceof Response) return withCors(who, corsHeaders(c.req));
|
|
1352
|
+
const refused = opWriteRefused(c, who);
|
|
1353
|
+
if (refused) return withCors(refused, corsHeaders(c.req));
|
|
1354
|
+
return withCors(await handleConversations(c.env, who.sub, c.req, { method: c.req.method, id: c.params.id }), corsHeaders(c.req));
|
|
1329
1355
|
}
|
|
1330
1356
|
async function appendMessageRoute(c) {
|
|
1331
|
-
const
|
|
1332
|
-
if (
|
|
1333
|
-
|
|
1357
|
+
const who = await opIdentity(c, "Sign in to sync your conversations across devices");
|
|
1358
|
+
if (who instanceof Response) return withCors(who, corsHeaders(c.req));
|
|
1359
|
+
const refused = opWriteRefused(c, who);
|
|
1360
|
+
if (refused) return withCors(refused, corsHeaders(c.req));
|
|
1361
|
+
return withCors(await handleAppendMessage(c.env, who.sub, c.req, c.params.id), corsHeaders(c.req));
|
|
1334
1362
|
}
|
|
1335
1363
|
async function shareRoute(c) {
|
|
1336
1364
|
const session = await sessionFrom(c.req, c.env);
|
|
@@ -1360,6 +1388,7 @@ async function tierFor(c) {
|
|
|
1360
1388
|
}
|
|
1361
1389
|
let tier = isApi ? "key" : "anon";
|
|
1362
1390
|
if (!isApi && await sessionFrom(c.req, c.env)) tier = "member";
|
|
1391
|
+
else if (!isApi && await opTokenMember(c.env, opCfg(c.env), c.req)) tier = "member";
|
|
1363
1392
|
return { tier, key };
|
|
1364
1393
|
}
|
|
1365
1394
|
async function mcpRoute(c) {
|
package/package.json
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"playwright": "^1.62.1",
|
|
28
28
|
"yaml": "^2.9.1"
|
|
29
29
|
},
|
|
30
|
-
"version": "0.2.
|
|
30
|
+
"version": "0.2.38",
|
|
31
31
|
"description": "The Konneal engine: the publisher-agnostic build pipeline and API plane for standards intelligence (retrieval, answer contract, verdicts, evaluation).",
|
|
32
32
|
"license": "BSD-3-Clause",
|
|
33
33
|
"type": "module",
|
package/workers/shared/router.ts
CHANGED
|
@@ -43,8 +43,11 @@ export function routeMatchesPath(pattern: string, path: string): Record<string,
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export function matchRoute(routes: Route[], method: string, path: string): { route: Route; params: Record<string, string> } | null {
|
|
46
|
+
// HEAD rides the GET route (the fetch pipeline strips the body) — a
|
|
47
|
+
// bare HEAD / must not answer 405 to link previewers and probes.
|
|
48
|
+
const effective = method === "HEAD" ? "GET" : method;
|
|
46
49
|
for (const route of routes) {
|
|
47
|
-
if (route.method !==
|
|
50
|
+
if (route.method !== effective && route.method !== "*") continue;
|
|
48
51
|
const params = routeMatchesPath(route.pattern, path);
|
|
49
52
|
if (params) return { route, params };
|
|
50
53
|
}
|
|
@@ -16,8 +16,9 @@ export interface SessionClaims {
|
|
|
16
16
|
exp: number;
|
|
17
17
|
/** the credential kind: absent = the service session; "delegated" =
|
|
18
18
|
* the OP-minted RFC 8693 bearer (TODO.ai-platform/12 — the session
|
|
19
|
-
* bridge; delegated.ts)
|
|
20
|
-
|
|
19
|
+
* bridge; delegated.ts); "op-token" = the device-grant CLI's opaque
|
|
20
|
+
* bearer, admitted through RFC 7662 introspection (livedata.ts) */
|
|
21
|
+
via?: "delegated" | "op-token";
|
|
21
22
|
/** the delegating surface (the delegated bearer's act.sub — the
|
|
22
23
|
* platform instance's OP client) */
|
|
23
24
|
delegator?: string;
|
|
@@ -19,17 +19,18 @@ import { canonicalRefusal } from "./refusal";
|
|
|
19
19
|
import { contractV2, tableRetyped } from "./refs";
|
|
20
20
|
import { completeTables, completeFigures } from "./completion";
|
|
21
21
|
import { NO_CONTEXT, appliedContext, contextNote, namedDocumentIn, parseContext, resolveDocScope, syntheticUnderstanding } from "./context";
|
|
22
|
-
import { liveDataConfig, liveTokenFor, resolveLiveAccount, type LiveRecord } from "./livedata";
|
|
22
|
+
import { liveDataConfig, liveTokenFor, opCfg, opTokenMember, resolveLiveAccount, type LiveRecord } from "./livedata";
|
|
23
23
|
import { bindModelNode, licenseBoundaryNote, licenseBoundaryRefusal, licensedEntryForPackage, modelCitation, modelEcho, modelGroundingBlock, modelNodeRefIn, standardForDocNumber } from "./modelplane";
|
|
24
24
|
import { evaluate as machineEvaluate, verdictNote } from "./verdict";
|
|
25
25
|
import { evaluateConditionSets, quantitiesIn, type ConditionVerdict } from "./conditions";
|
|
26
26
|
import { evaluateAggregation, type AggregationVerdict } from "./aggregation";
|
|
27
27
|
import { matchLicensedTopic, boundaryNoteText } from "./boundary";
|
|
28
|
+
import { answerQuality, qualityNote } from "./quality";
|
|
28
29
|
import { detectDraftIntent, prepareDraft } from "./drafts";
|
|
29
30
|
import { detectApiCallIntent, prepareApiCall } from "./apicalls";
|
|
30
31
|
import { memoryNote } from "./memories";
|
|
31
32
|
import { entitlementScope, resolveRequestScope, requestSalt, standardKeysFrom } from "./requestScope";
|
|
32
|
-
import { rawSessionToken } from "./session";
|
|
33
|
+
import { rawSessionToken, type SessionClaims } from "./session";
|
|
33
34
|
import { cacheKeyMaterial, corpusGen, exactCacheKey, freshRequested, semanticCacheKey } from "./answercache";
|
|
34
35
|
import type { Env } from "./env";
|
|
35
36
|
export type { Env };
|
|
@@ -286,7 +287,23 @@ async function handleAsk(
|
|
|
286
287
|
// account's standing, never on the query alone.
|
|
287
288
|
const apiCallIntent = !draftAct && P().publisher.features?.api_call_drafts ? detectApiCallIntent(q.query, declaredCtx) : null;
|
|
288
289
|
|
|
289
|
-
const
|
|
290
|
+
const session = tier === "member" ? await sessionFrom(req, env as any) : null;
|
|
291
|
+
// The Ommisa tier: no service session but a device-grant CLI's opaque
|
|
292
|
+
// bearer → the OP's introspection answer admits a member-READ
|
|
293
|
+
// credential (livedata.ts). No sessionRaw behind it, so the draft and
|
|
294
|
+
// live-data lanes keep their honest "unsigned" refusals.
|
|
295
|
+
const opMember = !session && tier === "member" ? await opTokenMember(env, opCfg(env), req) : null;
|
|
296
|
+
const member: SessionClaims | null = session
|
|
297
|
+
?? (opMember
|
|
298
|
+
? {
|
|
299
|
+
sub: opMember.sub,
|
|
300
|
+
roles: [],
|
|
301
|
+
iat: Math.floor(Date.now() / 1000),
|
|
302
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
303
|
+
via: "op-token",
|
|
304
|
+
scope: opMember.scope,
|
|
305
|
+
}
|
|
306
|
+
: null);
|
|
290
307
|
// resolved before the quota check: the effort choice prices the ask
|
|
291
308
|
const effort = requestEffort(env, member, (body as any)?.effort);
|
|
292
309
|
|
|
@@ -1129,7 +1146,8 @@ async function handleAsk(
|
|
|
1129
1146
|
// the reading arrives first: the interpretation that steered
|
|
1130
1147
|
// retrieval, before a single token of the answer
|
|
1131
1148
|
send({ type: "read", read: readAs() });
|
|
1132
|
-
|
|
1149
|
+
const sourceQuality = answerQuality(cites.map((c: any) => c.quality));
|
|
1150
|
+
send({ type: "citations", citations: cites, context_applied: ctxApplied, ...(sourceQuality ? { source_quality: sourceQuality, quality_note: qualityNote(sourceQuality) } : {}), ...(liveRecords ? { records: liveRecords } : {}), quota, });
|
|
1133
1151
|
let full = "";
|
|
1134
1152
|
try {
|
|
1135
1153
|
for await (const tok of sseTokens(stream)) {
|
|
@@ -1323,7 +1341,8 @@ async function handleAsk(
|
|
|
1323
1341
|
// figure completion (#172) — see ./completion for the rationale
|
|
1324
1342
|
completionBlocks.push(...(await completeFigures(env.DB, answer, [...c2ns.blocks, ...completionBlocks], used)));
|
|
1325
1343
|
|
|
1326
|
-
const
|
|
1344
|
+
const jsonQuality = answerQuality(finalCites.map((c: any) => c.quality));
|
|
1345
|
+
const out = { answer, citations: finalCites, ...(jsonQuality ? { source_quality: jsonQuality, quality_note: qualityNote(jsonQuality) } : {}), model: MODELS.member, query_hash: queryHash, follow_ups: understanding?.follow_ups ?? [], blocks: [...c2ns.blocks, ...(verdictBlock ? [verdictBlock] : []), ...(conditionBlock ? [conditionBlock] : []), ...(aggregationBlock ? [aggregationBlock] : []), ...completionBlocks], context_applied: ctxApplied, ...(liveRecords ? { records: liveRecords } : {}) };
|
|
1327
1346
|
const cacheable = !contextual && !declaredCtx && !answer.includes(refusalAnswer()) && finalAnchors.violations.length === 0;
|
|
1328
1347
|
if (cacheable) {
|
|
1329
1348
|
const warmVec = (await warmEmbed) ?? null;
|
|
@@ -33,6 +33,11 @@ export interface Env {
|
|
|
33
33
|
OIDC_CLIENT_ID?: string;
|
|
34
34
|
OIDC_CLIENT_SECRET?: string;
|
|
35
35
|
OIDC_REDIRECT_URI?: string;
|
|
36
|
+
/** TODO.ai-platform/14 — the Ommisa tier: the OP device-grant client
|
|
37
|
+
* ids (comma/space separated) whose opaque access tokens this
|
|
38
|
+
* deployment admits as member-read credentials via introspection.
|
|
39
|
+
* Empty/absent = the tier is off. */
|
|
40
|
+
OIDC_DEVICE_CLIENT_IDS?: string;
|
|
36
41
|
SESSION_SECRET?: string;
|
|
37
42
|
/** TODO.ai-platform/03 — the "my account" live-data delegation: the
|
|
38
43
|
* platform instance's API base + its client id at the OP (the
|
|
@@ -3,6 +3,7 @@ export { setProfile } from "./profile.ts";
|
|
|
3
3
|
import { retrieve } from "./pipeline";
|
|
4
4
|
import type { Hit } from "./pipeline";
|
|
5
5
|
import { handleCallback, handleLogin, handleLogout, handleMe, sessionFrom } from "./auth";
|
|
6
|
+
import { opCfg, opMemberCanWrite, opTokenMember } from "./livedata";
|
|
6
7
|
import { handleAppendMessage, handleConversations } from "./conversations";
|
|
7
8
|
import { handleMemories } from "./memories";
|
|
8
9
|
import { handleProjects, handleProjectFiles } from "./projects";
|
|
@@ -60,34 +61,63 @@ async function serveIndexPage(c: RouteContext): Promise<Response> {
|
|
|
60
61
|
return err(404, "not_found", "Page not found");
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
/** The member identity behind one member-data request: the service
|
|
65
|
+
* session first; else the Ommisa tier's introspected device-grant
|
|
66
|
+
* bearer (READ by default — mutations also need `opMemberCanWrite`).
|
|
67
|
+
* The CLI's memories/conversations/projects ride this. */
|
|
68
|
+
const MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
69
|
+
async function opIdentity(c: RouteContext, signIn: string): Promise<{ sub: string; write: boolean } | Response> {
|
|
64
70
|
const session = await sessionFrom(c.req, c.env as any);
|
|
65
|
-
if (
|
|
66
|
-
|
|
71
|
+
if (session) return { sub: session.sub, write: true };
|
|
72
|
+
const member = await opTokenMember(c.env, opCfg(c.env), c.req);
|
|
73
|
+
if (!member) return err(401, "unauthorized", signIn);
|
|
74
|
+
return { sub: member.sub, write: opMemberCanWrite(member) };
|
|
75
|
+
}
|
|
76
|
+
function opWriteRefused(c: RouteContext, who: { write: boolean } | Response): Response | null {
|
|
77
|
+
if (!(who instanceof Response) && MUTATING_METHODS.has(c.req.method) && !who.write) {
|
|
78
|
+
return err(403, "forbidden", "This sign-in grants read only — request a write scope at login.");
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function memoriesRoute(c: RouteContext): Promise<Response> {
|
|
84
|
+
const who = await opIdentity(c, "Sign in to use memory files");
|
|
85
|
+
if (who instanceof Response) return withCors(who, corsHeaders(c.req));
|
|
86
|
+
const refused = opWriteRefused(c, who);
|
|
87
|
+
if (refused) return withCors(refused, corsHeaders(c.req));
|
|
88
|
+
return withCors(await handleMemories(c.env, who.sub, c.req, { method: c.req.method, id: c.params.id }), corsHeaders(c.req));
|
|
67
89
|
}
|
|
68
90
|
|
|
69
91
|
async function projectsRoute(c: RouteContext): Promise<Response> {
|
|
70
|
-
const
|
|
71
|
-
if (
|
|
72
|
-
|
|
92
|
+
const who = await opIdentity(c, "Sign in to use projects");
|
|
93
|
+
if (who instanceof Response) return withCors(who, corsHeaders(c.req));
|
|
94
|
+
const refused = opWriteRefused(c, who);
|
|
95
|
+
if (refused) return withCors(refused, corsHeaders(c.req));
|
|
96
|
+
return withCors(await handleProjects(c.env, who.sub, c.req, { method: c.req.method, id: c.params.id }), corsHeaders(c.req));
|
|
73
97
|
}
|
|
74
98
|
|
|
75
99
|
async function projectFilesRoute(c: RouteContext): Promise<Response> {
|
|
76
|
-
const
|
|
77
|
-
if (
|
|
78
|
-
|
|
100
|
+
const who = await opIdentity(c, "Sign in to use projects");
|
|
101
|
+
if (who instanceof Response) return withCors(who, corsHeaders(c.req));
|
|
102
|
+
const refused = opWriteRefused(c, who);
|
|
103
|
+
if (refused) return withCors(refused, corsHeaders(c.req));
|
|
104
|
+
return withCors(await handleProjectFiles(c.env, who.sub, c.req, { method: c.req.method, id: c.params.id }), corsHeaders(c.req));
|
|
79
105
|
}
|
|
80
106
|
|
|
81
107
|
async function conversationsRoute(c: RouteContext): Promise<Response> {
|
|
82
|
-
const
|
|
83
|
-
if (
|
|
84
|
-
|
|
108
|
+
const who = await opIdentity(c, "Sign in to sync your conversations across devices");
|
|
109
|
+
if (who instanceof Response) return withCors(who, corsHeaders(c.req));
|
|
110
|
+
const refused = opWriteRefused(c, who);
|
|
111
|
+
if (refused) return withCors(refused, corsHeaders(c.req));
|
|
112
|
+
return withCors(await handleConversations(c.env, who.sub, c.req, { method: c.req.method, id: c.params.id }), corsHeaders(c.req));
|
|
85
113
|
}
|
|
86
114
|
|
|
87
115
|
async function appendMessageRoute(c: RouteContext): Promise<Response> {
|
|
88
|
-
const
|
|
89
|
-
if (
|
|
90
|
-
|
|
116
|
+
const who = await opIdentity(c, "Sign in to sync your conversations across devices");
|
|
117
|
+
if (who instanceof Response) return withCors(who, corsHeaders(c.req));
|
|
118
|
+
const refused = opWriteRefused(c, who);
|
|
119
|
+
if (refused) return withCors(refused, corsHeaders(c.req));
|
|
120
|
+
return withCors(await handleAppendMessage(c.env, who.sub, c.req, c.params.id!), corsHeaders(c.req));
|
|
91
121
|
}
|
|
92
122
|
|
|
93
123
|
async function shareRoute(c: RouteContext): Promise<Response> {
|
|
@@ -128,7 +158,10 @@ async function tierFor(c: RouteContext): Promise<{ tier: "anon" | "key" | "membe
|
|
|
128
158
|
// No SESSION_SECRET precondition: the delegated bearer (the platform
|
|
129
159
|
// bubble's OP-minted JWT, delegated.ts) admits the member lane on its
|
|
130
160
|
// own — sessionFrom returns null honestly when neither lane applies.
|
|
161
|
+
// The Ommisa tier rides the same admission: a device-grant CLI's
|
|
162
|
+
// opaque bearer, introspected at the OP (livedata.ts).
|
|
131
163
|
if (!isApi && (await sessionFrom(c.req, c.env as any))) tier = "member";
|
|
164
|
+
else if (!isApi && (await opTokenMember(c.env, opCfg(c.env), c.req))) tier = "member";
|
|
132
165
|
return { tier, key };
|
|
133
166
|
}
|
|
134
167
|
|
|
@@ -390,3 +390,97 @@ export async function resolveLiveAccount(
|
|
|
390
390
|
if (!read.ok) return { status: "unavailable", reason: read.reason };
|
|
391
391
|
return { status: "ok", records: read.records, stores: read.stores, readAt: read.readAt };
|
|
392
392
|
}
|
|
393
|
+
|
|
394
|
+
// ── the Ommisa tier (RFC 7662 introspection) ─────────────────────────
|
|
395
|
+
// The CLI signs in through the OP's device grant (RFC 8628) and holds an
|
|
396
|
+
// OPAQUE access token. This deployment never saw that grant — it learns
|
|
397
|
+
// the token the only honest way: it asks the OP (introspection, as an
|
|
398
|
+
// active registered client), then admits the caller as a member-READ
|
|
399
|
+
// credential. Writes, drafts and the live-data exchange stay session
|
|
400
|
+
// acts (the CLI has no service session to exchange — rawSessionToken is
|
|
401
|
+
// null, so those lanes already refuse honestly).
|
|
402
|
+
|
|
403
|
+
/** The device-grant clients this deployment admits. Empty = the tier is
|
|
404
|
+
* off (a Bearer token that is not a service session stays anonymous). */
|
|
405
|
+
export function deviceClientIds(env: any): string[] {
|
|
406
|
+
return String(env.OIDC_DEVICE_CLIENT_IDS ?? "")
|
|
407
|
+
.split(/[\s,]+/)
|
|
408
|
+
.filter(Boolean);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** The introspection call's OP coordinates, read once per call site. */
|
|
412
|
+
export function opCfg(env: any): { issuer: string; clientId: string } {
|
|
413
|
+
return { issuer: String(env.OIDC_ISSUER ?? "").trim().replace(/\/+$/, ""), clientId: String(env.OIDC_CLIENT_ID ?? "") };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export interface OpMember {
|
|
417
|
+
sub: string;
|
|
418
|
+
scope: string;
|
|
419
|
+
via: "op-token";
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** Mutations through the device-grant bearer (memories, conversations,
|
|
423
|
+
* projects — the member's OWN data) need the token's scope to state a
|
|
424
|
+
* write. The grammar is the OP's PAT vocabulary (`<service>:write`);
|
|
425
|
+
* a read-only approval stays read-only here, and service sessions
|
|
426
|
+
* (the browser) are always full-strength. */
|
|
427
|
+
export function opMemberCanWrite(member: { via?: string; scope?: string } | null): boolean {
|
|
428
|
+
if (!member) return false;
|
|
429
|
+
if (member.via !== "op-token") return true;
|
|
430
|
+
return /\bwrite\b/.test(member.scope ?? "");
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** The introspection answer → the member credential, judged against the
|
|
434
|
+
* deployment's allowlist. Inactive, foreign-client or subject-less
|
|
435
|
+
* answers all resolve to null — never to a widened guess. */
|
|
436
|
+
export function opMemberFromIntrospection(answer: any, ids: string[]): OpMember | null {
|
|
437
|
+
if (!answer?.active) return null;
|
|
438
|
+
const clientId = String(answer.client_id ?? "");
|
|
439
|
+
if (!ids.includes(clientId)) return null;
|
|
440
|
+
const sub = String(answer.sub ?? "");
|
|
441
|
+
if (!sub) return null;
|
|
442
|
+
return { sub, scope: String(answer.scope ?? ""), via: "op-token" };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** The Bearer read + introspection, KV-cached 45s keyed by the token's
|
|
446
|
+
* hash (an ask burst costs one introspection; a deactivation lands
|
|
447
|
+
* within a minute). The cached object is the OP's RAW answer — the
|
|
448
|
+
* allowlist re-judges on every read, so a config change takes effect
|
|
449
|
+
* inside the cache window too. JWTs (3 dot-segments) are NOT opaque:
|
|
450
|
+
* they belong to the delegated lane and never reach the OP here. */
|
|
451
|
+
export async function opTokenMember(
|
|
452
|
+
env: any,
|
|
453
|
+
cfg: { issuer: string; clientId: string },
|
|
454
|
+
req: Request,
|
|
455
|
+
): Promise<OpMember | null> {
|
|
456
|
+
const m = /^Bearer\s+(.+)$/i.exec((req.headers.get("Authorization") ?? "").trim());
|
|
457
|
+
const token = m?.[1]?.trim();
|
|
458
|
+
if (!token || token.split(".").length === 3) return null;
|
|
459
|
+
const ids = deviceClientIds(env);
|
|
460
|
+
if (!ids.length || !cfg.issuer || !cfg.clientId) return null;
|
|
461
|
+
const key = `opint:${await sha256Hex(token)}`;
|
|
462
|
+
let answer: any = null;
|
|
463
|
+
try {
|
|
464
|
+
answer = await env.CACHE.get(key, "json");
|
|
465
|
+
} catch {
|
|
466
|
+
/* a KV hiccup costs one extra introspection */
|
|
467
|
+
}
|
|
468
|
+
if (answer === undefined || answer === null) {
|
|
469
|
+
try {
|
|
470
|
+
const res = await fetch(`${cfg.issuer}/op/introspect`, {
|
|
471
|
+
method: "POST",
|
|
472
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
473
|
+
body: new URLSearchParams({ token, client_id: cfg.clientId }),
|
|
474
|
+
});
|
|
475
|
+
answer = res.ok ? await res.json() : { active: false };
|
|
476
|
+
} catch {
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
try {
|
|
480
|
+
await env.CACHE.put(key, JSON.stringify(answer ?? { active: false }), { expirationTtl: 45 });
|
|
481
|
+
} catch {
|
|
482
|
+
/* cacheless = introspect per ask */
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return opMemberFromIntrospection(answer, ids);
|
|
486
|
+
}
|
|
@@ -287,6 +287,7 @@ export function modelCitation(node: BoundModelNode) {
|
|
|
287
287
|
clause_title: `${node.kind.replace(/_/g, " ")} — ${node.name} (${node.node_id})`,
|
|
288
288
|
status: "in-force",
|
|
289
289
|
corpus: "smart-model",
|
|
290
|
+
quality: "verified",
|
|
290
291
|
url: undefined,
|
|
291
292
|
snippet: `${node.node_id}${node.clause ? ` · ${node.clause.urn}` : ""}${node.content?.statement ? ` — ${clip(node.content.statement, 240)}` : ""}`,
|
|
292
293
|
score: 1,
|
|
@@ -33,6 +33,7 @@ export function fill(template: string, vars: Record<string, string>): string {
|
|
|
33
33
|
return template.replace(/\{\{(\w+)\}\}/g, (_m, k: string) => (k in vars ? vars[k] : ""));
|
|
34
34
|
}
|
|
35
35
|
import { QueryFilters, toVectorizeFilter, standardKeyAllowed } from "./selfquery";
|
|
36
|
+
import { hitQuality } from "./quality";
|
|
36
37
|
import { lexicalPrefilter } from "./lexical";
|
|
37
38
|
import { positionOrder } from "./structural";
|
|
38
39
|
import { STAGES, runStages } from "./stages";
|
|
@@ -404,6 +405,7 @@ export function citations(hits: Hit[]) {
|
|
|
404
405
|
status: h.metadata.status ?? "unknown",
|
|
405
406
|
superseded_by: h.metadata.superseded_by || undefined,
|
|
406
407
|
corpus: h.metadata.corpus || P().publisher.id,
|
|
408
|
+
quality: hitQuality(h.metadata),
|
|
407
409
|
url: publicationUrl(h.metadata),
|
|
408
410
|
snippet: h.text.slice(0, 400),
|
|
409
411
|
score: h.rerank_score ?? h.score,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// The answer-quality ladder (TODO.rag/13): every citation states what
|
|
2
|
+
// kind of text grounds it, so the answer states how far to trust it.
|
|
3
|
+
// Three rungs, derived from provenance the chunk wire already carries —
|
|
4
|
+
// no new ingest state:
|
|
5
|
+
//
|
|
6
|
+
// verified — the model plane: the chunk rides a primmel data model
|
|
7
|
+
// (model_node/model_version), the Recommendation's semantics are
|
|
8
|
+
// machine-checkable and the golden set measures them (R 60 is the
|
|
9
|
+
// gauge). The answer can be WRONG, but it is wrong against a
|
|
10
|
+
// contract we can see.
|
|
11
|
+
// curated — the edited corpus: Metanorma-authored documents
|
|
12
|
+
// (mn-samples-oiml and the editors' own), clause-boundary chunks,
|
|
13
|
+
// enrichment applied.
|
|
14
|
+
// ocr — the experimental OCR corpus: the broad coverage lane,
|
|
15
|
+
// text recovered from scans; quote anchors may wobble, tables can
|
|
16
|
+
// be mangled. Useful, and marked as experimental on purpose.
|
|
17
|
+
//
|
|
18
|
+
// The answer's tier is the WORST rung its citations stand on: one OCR
|
|
19
|
+
// citation makes the whole answer experimental, because the reader
|
|
20
|
+
// cannot tell which sentence leaned on which chunk.
|
|
21
|
+
|
|
22
|
+
import type { ChunkMeta } from "../../shared/chunk.ts";
|
|
23
|
+
|
|
24
|
+
export type SourceQuality = "verified" | "curated" | "ocr";
|
|
25
|
+
|
|
26
|
+
const ORDER: Record<SourceQuality, number> = { verified: 0, curated: 1, ocr: 2 };
|
|
27
|
+
|
|
28
|
+
export function hitQuality(m: ChunkMeta): SourceQuality {
|
|
29
|
+
if (m.model_node || m.model_version || m.producer === "primmel") return "verified";
|
|
30
|
+
if (m.tier === "curated") return "curated";
|
|
31
|
+
return "ocr";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function answerQuality(qualities: (SourceQuality | undefined)[]): SourceQuality | null {
|
|
35
|
+
let worst: SourceQuality | null = null;
|
|
36
|
+
for (const q of qualities) {
|
|
37
|
+
if (!q) continue;
|
|
38
|
+
if (!worst || ORDER[q] > ORDER[worst]) worst = q;
|
|
39
|
+
}
|
|
40
|
+
return worst;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The one-line posture the interface renders under the answer. */
|
|
44
|
+
export function qualityNote(q: SourceQuality): string {
|
|
45
|
+
if (q === "verified") return "Grounded in verified sources: this publication's requirements ride a machine-checkable data model.";
|
|
46
|
+
if (q === "curated") return "Grounded in the edited corpus: Metanorma-authored documents, chunked at clause boundaries.";
|
|
47
|
+
return "Partly grounded in experimental OCR text: verify quotations and table values against the official publication.";
|
|
48
|
+
}
|