@konneal/engine 0.2.37 → 0.2.39
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-J6NGXPOD.js → ask-UGTXNPPY.js} +2 -2
- package/dist/{chunk-YEAAGFHB.js → chunk-EVCERBIH.js} +1 -1
- package/dist/{chunk-37FSCXBQ.js → chunk-NBE7BCQ7.js} +39 -5
- package/dist/{chunk-SEOQWW77.js → chunk-OHYAHQQV.js} +8 -3
- package/dist/env.d.ts +4 -0
- package/dist/livedata.d.ts +11 -3
- package/dist/modelplane.d.ts +1 -0
- package/dist/pipeline.d.ts +1 -0
- package/dist/quality.d.ts +6 -0
- package/dist/{search-V6M4KASL.js → search-G23CY5I6.js} +2 -2
- package/dist/worker_public/src/index.js +4 -4
- package/package.json +1 -1
- package/workers/worker_public/src/ask.ts +5 -2
- package/workers/worker_public/src/env.ts +4 -0
- package/workers/worker_public/src/livedata.ts +27 -7
- 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
|
|
@@ -23814,13 +23836,23 @@ function opMemberCanWrite(member) {
|
|
|
23814
23836
|
if (member.via !== "op-token") return true;
|
|
23815
23837
|
return /\bwrite\b/.test(member.scope ?? "");
|
|
23816
23838
|
}
|
|
23817
|
-
function opMemberFromIntrospection(answer, ids) {
|
|
23839
|
+
function opMemberFromIntrospection(answer, ids, patServices = []) {
|
|
23818
23840
|
if (!answer?.active) return null;
|
|
23819
|
-
const clientId = String(answer.client_id ?? "");
|
|
23820
|
-
if (!ids.includes(clientId)) return null;
|
|
23821
23841
|
const sub = String(answer.sub ?? "");
|
|
23822
23842
|
if (!sub) return null;
|
|
23823
|
-
|
|
23843
|
+
const clientId = String(answer.client_id ?? "");
|
|
23844
|
+
if (clientId) {
|
|
23845
|
+
if (!ids.includes(clientId)) return null;
|
|
23846
|
+
return { sub, scope: String(answer.scope ?? ""), via: "op-token" };
|
|
23847
|
+
}
|
|
23848
|
+
if (!patServices.length) return null;
|
|
23849
|
+
const scope = String(answer.scope ?? "");
|
|
23850
|
+
const services = scope.split(/\s+/).filter(Boolean).map((s8) => s8.split(":")[0]);
|
|
23851
|
+
if (!services.some((s8) => patServices.includes(s8))) return null;
|
|
23852
|
+
return { sub, scope, via: "op-token" };
|
|
23853
|
+
}
|
|
23854
|
+
function patServiceIds(env) {
|
|
23855
|
+
return String(env.OIDC_PAT_SERVICES ?? "").split(/[\s,]+/).filter(Boolean);
|
|
23824
23856
|
}
|
|
23825
23857
|
async function opTokenMember(env, cfg, req) {
|
|
23826
23858
|
const m = /^Bearer\s+(.+)$/i.exec((req.headers.get("Authorization") ?? "").trim());
|
|
@@ -23850,7 +23882,7 @@ async function opTokenMember(env, cfg, req) {
|
|
|
23850
23882
|
} catch {
|
|
23851
23883
|
}
|
|
23852
23884
|
}
|
|
23853
|
-
return opMemberFromIntrospection(answer, ids);
|
|
23885
|
+
return opMemberFromIntrospection(answer, ids, patServiceIds(env));
|
|
23854
23886
|
}
|
|
23855
23887
|
|
|
23856
23888
|
// workers/worker_public/src/oidc.ts
|
|
@@ -24560,6 +24592,8 @@ async function editionNote(env, u) {
|
|
|
24560
24592
|
export {
|
|
24561
24593
|
embed,
|
|
24562
24594
|
generateOnce,
|
|
24595
|
+
answerQuality,
|
|
24596
|
+
qualityNote,
|
|
24563
24597
|
ftsMatchQuery,
|
|
24564
24598
|
refCodec,
|
|
24565
24599
|
NO_CONTEXT,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
NO_CONTEXT,
|
|
3
|
+
answerQuality,
|
|
3
4
|
appliedContext,
|
|
4
5
|
buildMessages,
|
|
5
6
|
checkQuota,
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
parseContext,
|
|
22
23
|
portModelRunner,
|
|
23
24
|
portStore,
|
|
25
|
+
qualityNote,
|
|
24
26
|
rawSessionToken,
|
|
25
27
|
refCodec,
|
|
26
28
|
resolveDocScope,
|
|
@@ -32,7 +34,7 @@ import {
|
|
|
32
34
|
syntheticUnderstanding,
|
|
33
35
|
telemetry,
|
|
34
36
|
understandQuery
|
|
35
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-NBE7BCQ7.js";
|
|
36
38
|
import {
|
|
37
39
|
corsHeaders,
|
|
38
40
|
err,
|
|
@@ -497,6 +499,7 @@ function modelCitation(node) {
|
|
|
497
499
|
clause_title: `${node.kind.replace(/_/g, " ")} \u2014 ${node.name} (${node.node_id})`,
|
|
498
500
|
status: "in-force",
|
|
499
501
|
corpus: "smart-model",
|
|
502
|
+
quality: "verified",
|
|
500
503
|
url: void 0,
|
|
501
504
|
snippet: `${node.node_id}${node.clause ? ` \xB7 ${node.clause.urn}` : ""}${node.content?.statement ? ` \u2014 ${clip(node.content.statement, 240)}` : ""}`,
|
|
502
505
|
score: 1
|
|
@@ -2471,7 +2474,8 @@ Answer account questions from these records ONLY: name the record when you use i
|
|
|
2471
2474
|
|
|
2472
2475
|
`));
|
|
2473
2476
|
send({ type: "read", read: readAs() });
|
|
2474
|
-
|
|
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 });
|
|
2475
2479
|
let full = "";
|
|
2476
2480
|
try {
|
|
2477
2481
|
for await (const tok of sseTokens(stream)) {
|
|
@@ -2604,7 +2608,8 @@ Answer account questions from these records ONLY: name the record when you use i
|
|
|
2604
2608
|
if (completionBlocks.length) console.log("contract completion:", completionBlocks.length, "table block(s) attached server-side");
|
|
2605
2609
|
}
|
|
2606
2610
|
completionBlocks.push(...await completeFigures(env.DB, answer, [...c2ns.blocks, ...completionBlocks], used));
|
|
2607
|
-
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 } : {} };
|
|
2608
2613
|
const cacheable = !contextual && !declaredCtx && !answer.includes(refusalAnswer()) && finalAnchors.violations.length === 0;
|
|
2609
2614
|
if (cacheable) {
|
|
2610
2615
|
const warmVec = await warmEmbed ?? null;
|
package/dist/env.d.ts
CHANGED
|
@@ -36,6 +36,10 @@ export interface Env {
|
|
|
36
36
|
* deployment admits as member-read credentials via introspection.
|
|
37
37
|
* Empty/absent = the tier is off. */
|
|
38
38
|
OIDC_DEVICE_CLIENT_IDS?: string;
|
|
39
|
+
/** the services whose OP-minted personal access tokens (the PAT form
|
|
40
|
+
* of introspection — no client_id on the wire) this deployment admits
|
|
41
|
+
* as member-read credentials. Empty/absent = PATs stay anonymous. */
|
|
42
|
+
OIDC_PAT_SERVICES?: string;
|
|
39
43
|
SESSION_SECRET?: string;
|
|
40
44
|
/** TODO.ai-platform/03 — the "my account" live-data delegation: the
|
|
41
45
|
* platform instance's API base + its client id at the OP (the
|
package/dist/livedata.d.ts
CHANGED
|
@@ -111,9 +111,17 @@ export declare function opMemberCanWrite(member: {
|
|
|
111
111
|
scope?: string;
|
|
112
112
|
} | null): boolean;
|
|
113
113
|
/** The introspection answer → the member credential, judged against the
|
|
114
|
-
* deployment's
|
|
115
|
-
*
|
|
116
|
-
|
|
114
|
+
* deployment's two allowlists. The OP answers client-bearing tokens
|
|
115
|
+
* WITH client_id (the access-token table, the machine JWTs) and its
|
|
116
|
+
* PAT form WITHOUT one — a PAT's identity on the wire is its granted
|
|
117
|
+
* SERVICES (the PAT grammar's whole vocabulary). So the deployment
|
|
118
|
+
* names device-grant clients (OIDC_DEVICE_CLIENT_IDS) and, separately,
|
|
119
|
+
* the services whose personal access tokens it admits
|
|
120
|
+
* (OIDC_PAT_SERVICES). Both lists empty = the tier stays off; a token
|
|
121
|
+
* matching neither list resolves to null — never a widened guess. */
|
|
122
|
+
export declare function opMemberFromIntrospection(answer: any, ids: string[], patServices?: string[]): OpMember | null;
|
|
123
|
+
/** The PAT-services allowlist (see opMemberFromIntrospection). */
|
|
124
|
+
export declare function patServiceIds(env: any): string[];
|
|
117
125
|
/** The Bearer read + introspection, KV-cached 45s keyed by the token's
|
|
118
126
|
* hash (an ask burst costs one introspection; a deactivation lands
|
|
119
127
|
* within a minute). The cached object is the OP's RAW answer — the
|
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-EVCERBIH.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-OHYAHQQV.js";
|
|
13
13
|
import {
|
|
14
14
|
buildMessages,
|
|
15
15
|
citations,
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
sessionFrom,
|
|
36
36
|
telemetry,
|
|
37
37
|
understandQuery
|
|
38
|
-
} from "../../chunk-
|
|
38
|
+
} from "../../chunk-NBE7BCQ7.js";
|
|
39
39
|
import {
|
|
40
40
|
authenticate,
|
|
41
41
|
corsHeaders,
|
|
@@ -999,7 +999,7 @@ async function handleMcp(env, ctx, req, tier, key) {
|
|
|
999
999
|
// stream:false forces the JSON lane (anon defaults to SSE)
|
|
1000
1000
|
body: JSON.stringify({ ...args, stream: false })
|
|
1001
1001
|
});
|
|
1002
|
-
const res = name === "ask" ? await (await import("../../ask-
|
|
1002
|
+
const res = name === "ask" ? await (await import("../../ask-UGTXNPPY.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-G23CY5I6.js")).handleSearch(env, ctx, inner, tier, key);
|
|
1003
1003
|
return res.json().catch(() => ({ error: { message: "tool transport failed", status: res.status } }));
|
|
1004
1004
|
});
|
|
1005
1005
|
if (out.ok && "accepted" in out) return new Response(null, { status: 202 });
|
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.39",
|
|
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",
|
|
@@ -25,6 +25,7 @@ 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";
|
|
@@ -1145,7 +1146,8 @@ async function handleAsk(
|
|
|
1145
1146
|
// the reading arrives first: the interpretation that steered
|
|
1146
1147
|
// retrieval, before a single token of the answer
|
|
1147
1148
|
send({ type: "read", read: readAs() });
|
|
1148
|
-
|
|
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, });
|
|
1149
1151
|
let full = "";
|
|
1150
1152
|
try {
|
|
1151
1153
|
for await (const tok of sseTokens(stream)) {
|
|
@@ -1339,7 +1341,8 @@ async function handleAsk(
|
|
|
1339
1341
|
// figure completion (#172) — see ./completion for the rationale
|
|
1340
1342
|
completionBlocks.push(...(await completeFigures(env.DB, answer, [...c2ns.blocks, ...completionBlocks], used)));
|
|
1341
1343
|
|
|
1342
|
-
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 } : {}) };
|
|
1343
1346
|
const cacheable = !contextual && !declaredCtx && !answer.includes(refusalAnswer()) && finalAnchors.violations.length === 0;
|
|
1344
1347
|
if (cacheable) {
|
|
1345
1348
|
const warmVec = (await warmEmbed) ?? null;
|
|
@@ -38,6 +38,10 @@ export interface Env {
|
|
|
38
38
|
* deployment admits as member-read credentials via introspection.
|
|
39
39
|
* Empty/absent = the tier is off. */
|
|
40
40
|
OIDC_DEVICE_CLIENT_IDS?: string;
|
|
41
|
+
/** the services whose OP-minted personal access tokens (the PAT form
|
|
42
|
+
* of introspection — no client_id on the wire) this deployment admits
|
|
43
|
+
* as member-read credentials. Empty/absent = PATs stay anonymous. */
|
|
44
|
+
OIDC_PAT_SERVICES?: string;
|
|
41
45
|
SESSION_SECRET?: string;
|
|
42
46
|
/** TODO.ai-platform/03 — the "my account" live-data delegation: the
|
|
43
47
|
* platform instance's API base + its client id at the OP (the
|
|
@@ -431,15 +431,35 @@ export function opMemberCanWrite(member: { via?: string; scope?: string } | null
|
|
|
431
431
|
}
|
|
432
432
|
|
|
433
433
|
/** The introspection answer → the member credential, judged against the
|
|
434
|
-
* deployment's
|
|
435
|
-
*
|
|
436
|
-
|
|
434
|
+
* deployment's two allowlists. The OP answers client-bearing tokens
|
|
435
|
+
* WITH client_id (the access-token table, the machine JWTs) and its
|
|
436
|
+
* PAT form WITHOUT one — a PAT's identity on the wire is its granted
|
|
437
|
+
* SERVICES (the PAT grammar's whole vocabulary). So the deployment
|
|
438
|
+
* names device-grant clients (OIDC_DEVICE_CLIENT_IDS) and, separately,
|
|
439
|
+
* the services whose personal access tokens it admits
|
|
440
|
+
* (OIDC_PAT_SERVICES). Both lists empty = the tier stays off; a token
|
|
441
|
+
* matching neither list resolves to null — never a widened guess. */
|
|
442
|
+
export function opMemberFromIntrospection(answer: any, ids: string[], patServices: string[] = []): OpMember | null {
|
|
437
443
|
if (!answer?.active) return null;
|
|
438
|
-
const clientId = String(answer.client_id ?? "");
|
|
439
|
-
if (!ids.includes(clientId)) return null;
|
|
440
444
|
const sub = String(answer.sub ?? "");
|
|
441
445
|
if (!sub) return null;
|
|
442
|
-
|
|
446
|
+
const clientId = String(answer.client_id ?? "");
|
|
447
|
+
if (clientId) {
|
|
448
|
+
if (!ids.includes(clientId)) return null;
|
|
449
|
+
return { sub, scope: String(answer.scope ?? ""), via: "op-token" };
|
|
450
|
+
}
|
|
451
|
+
if (!patServices.length) return null;
|
|
452
|
+
const scope = String(answer.scope ?? "");
|
|
453
|
+
const services = scope.split(/\s+/).filter(Boolean).map((s) => s.split(":")[0]);
|
|
454
|
+
if (!services.some((s) => patServices.includes(s))) return null;
|
|
455
|
+
return { sub, scope, via: "op-token" };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** The PAT-services allowlist (see opMemberFromIntrospection). */
|
|
459
|
+
export function patServiceIds(env: any): string[] {
|
|
460
|
+
return String(env.OIDC_PAT_SERVICES ?? "")
|
|
461
|
+
.split(/[\s,]+/)
|
|
462
|
+
.filter(Boolean);
|
|
443
463
|
}
|
|
444
464
|
|
|
445
465
|
/** The Bearer read + introspection, KV-cached 45s keyed by the token's
|
|
@@ -482,5 +502,5 @@ export async function opTokenMember(
|
|
|
482
502
|
/* cacheless = introspect per ask */
|
|
483
503
|
}
|
|
484
504
|
}
|
|
485
|
-
return opMemberFromIntrospection(answer, ids);
|
|
505
|
+
return opMemberFromIntrospection(answer, ids, patServiceIds(env));
|
|
486
506
|
}
|
|
@@ -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
|
+
}
|