@konneal/engine 0.2.122 → 0.2.124
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-J4QFS6Q2.js → ask-XAI5SVJ7.js} +3 -3
- package/dist/{certificates-CO7KX2S2.js → certificates-TMYS7XLD.js} +9 -1
- package/dist/certificates.d.ts +43 -0
- package/dist/{chunk-JZFXSTYX.js → chunk-GBGRHXRU.js} +71 -1
- package/dist/{chunk-FKG5VGOL.js → chunk-J6LBR6GI.js} +1 -1
- package/dist/{chunk-BRUHQEOT.js → chunk-ZO6MMKGD.js} +26 -4
- package/dist/{tools-CUKFOW7S.js → tools-O4JYGVSR.js} +1 -1
- package/dist/worker_public/src/index.js +5 -5
- package/package.json +1 -1
- package/workers/worker_public/src/ask.ts +24 -3
- package/workers/worker_public/src/certificates.ts +112 -1
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
handleAsk
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-ZO6MMKGD.js";
|
|
4
|
+
import "./chunk-J6LBR6GI.js";
|
|
5
5
|
import "./chunk-2JUWER2A.js";
|
|
6
6
|
import "./chunk-TH5XGPD5.js";
|
|
7
7
|
import "./chunk-DJ65EF5G.js";
|
|
8
8
|
import "./chunk-A4A26BCV.js";
|
|
9
9
|
import "./chunk-4NXH5ZI6.js";
|
|
10
10
|
import "./chunk-VT7DR6NQ.js";
|
|
11
|
-
import "./chunk-
|
|
11
|
+
import "./chunk-GBGRHXRU.js";
|
|
12
12
|
import "./chunk-3FYJM7LH.js";
|
|
13
13
|
export {
|
|
14
14
|
handleAsk
|
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
import {
|
|
2
|
+
aggregateBlock,
|
|
3
|
+
aggregateNote,
|
|
2
4
|
buildRegisterQuery,
|
|
3
5
|
certificateLinks,
|
|
4
6
|
isRegisterShaped,
|
|
5
7
|
printedCertificateNumber,
|
|
6
8
|
queryFamily,
|
|
9
|
+
registerAggregate,
|
|
10
|
+
registerAggregateIntent,
|
|
7
11
|
registerNote,
|
|
8
12
|
registerTokens,
|
|
9
13
|
searchRegister
|
|
10
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-GBGRHXRU.js";
|
|
11
15
|
import "./chunk-3FYJM7LH.js";
|
|
12
16
|
export {
|
|
17
|
+
aggregateBlock,
|
|
18
|
+
aggregateNote,
|
|
13
19
|
buildRegisterQuery,
|
|
14
20
|
certificateLinks,
|
|
15
21
|
isRegisterShaped,
|
|
16
22
|
printedCertificateNumber,
|
|
17
23
|
queryFamily,
|
|
24
|
+
registerAggregate,
|
|
25
|
+
registerAggregateIntent,
|
|
18
26
|
registerNote,
|
|
19
27
|
registerTokens,
|
|
20
28
|
searchRegister
|
package/dist/certificates.d.ts
CHANGED
|
@@ -12,6 +12,49 @@ export interface RegisterRow {
|
|
|
12
12
|
* trigger the citation-graph notes already use (query-shaped notes,
|
|
13
13
|
* never a router). */
|
|
14
14
|
export declare function isRegisterShaped(query: string): boolean;
|
|
15
|
+
/** An AGGREGATE register question — "how many certificates are issued
|
|
16
|
+
* for R 60:2021?", "what is the latest certificate issued for R 60" —
|
|
17
|
+
* is served by DETERMINISTIC computation (COUNT, ORDER BY), never by
|
|
18
|
+
* the model counting the rows a note happens to carry. The register
|
|
19
|
+
* lane's own philosophy: the note answers the structure, the model
|
|
20
|
+
* narrates it (TODO.sota/04's machine-checkable ground truth, applied
|
|
21
|
+
* to the register plane 2026-10-11). */
|
|
22
|
+
export declare function registerAggregateIntent(query: string): "count" | "latest" | null;
|
|
23
|
+
export interface RegisterAggregate {
|
|
24
|
+
family: string;
|
|
25
|
+
count: number;
|
|
26
|
+
valid: number;
|
|
27
|
+
suspended: number;
|
|
28
|
+
latest: RegisterRow | null;
|
|
29
|
+
}
|
|
30
|
+
/** The family's aggregate, computed in SQL (the register is the ground
|
|
31
|
+
* truth; editions do not appear in it — certificates are issued per
|
|
32
|
+
* family). The latest row ranks by year then the sequence number's
|
|
33
|
+
* tail (the -YY.NN suffix carries the actual issue ordering). */
|
|
34
|
+
export declare function registerAggregate(db: any, family: string): Promise<RegisterAggregate | null>;
|
|
35
|
+
/** The aggregate's authoritative note: the numbers are DATA — the
|
|
36
|
+
* model narrates, never recomputes. The edition disclaimer rides every
|
|
37
|
+
* family-scoped answer (the register has no edition dimension). */
|
|
38
|
+
export declare function aggregateNote(agg: RegisterAggregate): string;
|
|
39
|
+
/** A typed block for aggregate answers — the UI renders the count and
|
|
40
|
+
* the latest row's link as data, never from prose. */
|
|
41
|
+
export declare function aggregateBlock(agg: RegisterAggregate): {
|
|
42
|
+
type: string;
|
|
43
|
+
payload: {
|
|
44
|
+
latest?: {
|
|
45
|
+
url?: string | undefined;
|
|
46
|
+
num: string;
|
|
47
|
+
holder: string;
|
|
48
|
+
model: string;
|
|
49
|
+
year: string;
|
|
50
|
+
status: string;
|
|
51
|
+
} | undefined;
|
|
52
|
+
suspended?: number | undefined;
|
|
53
|
+
family: string;
|
|
54
|
+
count: number;
|
|
55
|
+
valid: number;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
15
58
|
/** Search tokens: words that can identify a holder or a model — words of
|
|
16
59
|
* 2+ characters that are not register question words, plus bare model
|
|
17
60
|
* numbers ("190", "HM14H1"). Upper-cased tokens match case-insensitively
|
|
@@ -6,6 +6,62 @@ import {
|
|
|
6
6
|
function isRegisterShaped(query) {
|
|
7
7
|
return /\bcertif(ied|icates?|ication)s?\b/i.test(query) && /\b(is|are|still|currently|valid|status|suspended|revoked|was|were|have|has|hold|holds|possess|carry|got)\b/i.test(query);
|
|
8
8
|
}
|
|
9
|
+
function registerAggregateIntent(query) {
|
|
10
|
+
if (!/\bcertif(ied|icates?|ication)s?\b/i.test(query)) return null;
|
|
11
|
+
if (/\b(latest|most recent|newest|last)\b/i.test(query)) return "latest";
|
|
12
|
+
if (/\b(how many|count|number of|total|how much)\b/i.test(query)) return "count";
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
async function registerAggregate(db, family) {
|
|
16
|
+
try {
|
|
17
|
+
const digits = family.replace(/^[A-Za-z]+/, "");
|
|
18
|
+
const rows = await db.prepare(
|
|
19
|
+
"SELECT num, family, holder, model, year, status, pdf_key FROM certificates WHERE family = ?1 ORDER BY CAST(year AS INTEGER) DESC, num DESC LIMIT 1"
|
|
20
|
+
).bind(digits).all();
|
|
21
|
+
const totals = await db.prepare("SELECT COUNT(*) AS n, SUM(CASE WHEN status = 'Valid' THEN 1 ELSE 0 END) AS valid, SUM(CASE WHEN status = 'Suspended' THEN 1 ELSE 0 END) AS suspended FROM certificates WHERE family = ?1").bind(digits).first();
|
|
22
|
+
if (!totals || !totals.n) return null;
|
|
23
|
+
return {
|
|
24
|
+
family: digits,
|
|
25
|
+
count: Number(totals.n),
|
|
26
|
+
valid: Number(totals.valid ?? 0),
|
|
27
|
+
suspended: Number(totals.suspended ?? 0),
|
|
28
|
+
latest: (rows.results ?? [])[0] ?? null
|
|
29
|
+
};
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function aggregateNote(agg) {
|
|
35
|
+
const latest = agg.latest ? `The latest certificate in the snapshot is ${agg.latest.num} (holder ${agg.latest.holder}, model "${agg.latest.model}"${agg.latest.year ? `, issued ${agg.latest.year}` : ""}, status ${agg.latest.status}).` : "No certificate rows are present in the snapshot.";
|
|
36
|
+
const link = agg.latest?.pdf_key ? ` Its document: https://www.oimlsmart.org/cert-pdf/${agg.latest.pdf_key}` : "";
|
|
37
|
+
return [
|
|
38
|
+
`CERTIFICATE REGISTER \u2014 THIS NOTE ANSWERS THE QUESTION. Answer from THIS note; do not refuse for want of passages (the retrieval passages are irrelevant for register questions).`,
|
|
39
|
+
`The ${agg.family} family holds ${agg.count} certificate${agg.count === 1 ? "" : "s"} (${agg.valid} valid${agg.suspended ? `, ${agg.suspended} suspended` : ""}) \u2014 computed over the register snapshot; state these numbers verbatim.`,
|
|
40
|
+
latest + link,
|
|
41
|
+
"Certificates are issued per RECOMMENDATION FAMILY \u2014 the register carries no edition dimension, so an edition named in the question (e.g. 2021) does not narrow it; say so when the question names an edition. Qualify that the register is a snapshot, and cite the register."
|
|
42
|
+
].join("\n");
|
|
43
|
+
}
|
|
44
|
+
function aggregateBlock(agg) {
|
|
45
|
+
return {
|
|
46
|
+
type: "register",
|
|
47
|
+
payload: {
|
|
48
|
+
family: agg.family,
|
|
49
|
+
count: agg.count,
|
|
50
|
+
valid: agg.valid,
|
|
51
|
+
...agg.suspended ? { suspended: agg.suspended } : {},
|
|
52
|
+
...agg.latest ? {
|
|
53
|
+
latest: {
|
|
54
|
+
num: agg.latest.num,
|
|
55
|
+
holder: agg.latest.holder,
|
|
56
|
+
model: agg.latest.model,
|
|
57
|
+
year: agg.latest.year,
|
|
58
|
+
status: agg.latest.status,
|
|
59
|
+
...agg.latest.pdf_key ? { url: `/cert-pdf/${agg.latest.pdf_key}` } : {}
|
|
60
|
+
}
|
|
61
|
+
} : {}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
9
65
|
function queryFamily(query) {
|
|
10
66
|
const m = /\b([RDMB])\s?-?(\d{2,3})\b/i.exec(query);
|
|
11
67
|
return m ? `${m[1].toUpperCase()}${m[2].padStart(2, "0")}` : null;
|
|
@@ -14,6 +70,13 @@ function printedCertificateNumber(query) {
|
|
|
14
70
|
const m = /\b([RDMB]\s?-?\d{2,3}\s?\/\s?\d{4}\s?-\s?[A-Z0-9]+\s?-\s?[^\s,;]+(?:\s?\.\s?\d+)?)/i.exec(query);
|
|
15
71
|
return m ? m[1].replace(/\s+/g, "").replace(/-\./, ".") : null;
|
|
16
72
|
}
|
|
73
|
+
function editionSuffixYears(query) {
|
|
74
|
+
const years = /* @__PURE__ */ new Set();
|
|
75
|
+
for (const m of query.matchAll(/\b([RDMB])\s?-?\d{2,3}(?:-\d+)?\s?:\s?(\d{4})\b/gi)) {
|
|
76
|
+
years.add(m[2]);
|
|
77
|
+
}
|
|
78
|
+
return years;
|
|
79
|
+
}
|
|
17
80
|
function registerTokens(query) {
|
|
18
81
|
const stop = /* @__PURE__ */ new Set([
|
|
19
82
|
"the",
|
|
@@ -64,9 +127,12 @@ function registerTokens(query) {
|
|
|
64
127
|
]);
|
|
65
128
|
const tokens = [];
|
|
66
129
|
const publisherWord = P().publisher.name.toLowerCase();
|
|
130
|
+
const editionYears = editionSuffixYears(query);
|
|
131
|
+
const stop2 = /* @__PURE__ */ new Set([...stop, "many", "issued", "issue", "issued", "latest", "newest", "recent", "total", "number"]);
|
|
67
132
|
for (const w of query.split(/[^A-Za-z0-9&-]+/)) {
|
|
68
133
|
if (w.length < 2) continue;
|
|
69
|
-
if (
|
|
134
|
+
if (stop2.has(w.toLowerCase())) continue;
|
|
135
|
+
if (/^\d{4}$/.test(w) && editionYears.has(w)) continue;
|
|
70
136
|
if (w.toLowerCase() === publisherWord) continue;
|
|
71
137
|
if (/^\d+$/.test(w) && w.length < 2) continue;
|
|
72
138
|
if (!tokens.includes(w)) tokens.push(w);
|
|
@@ -150,6 +216,10 @@ ${absent.join("\n")}`);
|
|
|
150
216
|
|
|
151
217
|
export {
|
|
152
218
|
isRegisterShaped,
|
|
219
|
+
registerAggregateIntent,
|
|
220
|
+
registerAggregate,
|
|
221
|
+
aggregateNote,
|
|
222
|
+
aggregateBlock,
|
|
153
223
|
queryFamily,
|
|
154
224
|
printedCertificateNumber,
|
|
155
225
|
registerTokens,
|
|
@@ -1181,7 +1181,7 @@ var certificatesSearch = {
|
|
|
1181
1181
|
handler: async (env, args) => {
|
|
1182
1182
|
const query = String(args?.query ?? "").trim().slice(0, 160);
|
|
1183
1183
|
if (!query) return null;
|
|
1184
|
-
const { searchRegister, registerNote, certificateLinks } = await import("./certificates-
|
|
1184
|
+
const { searchRegister, registerNote, certificateLinks } = await import("./certificates-TMYS7XLD.js");
|
|
1185
1185
|
const reg = await searchRegister(env.DB, query, true);
|
|
1186
1186
|
const output = reg?.rows?.length ? [registerNote(reg.rows), certificateLinks(reg.rows)].filter(Boolean).join("\n") : `No certificate was found for "${query}" in the certificates database (the register snapshot). State this as the search's result, with the searched string visible.`;
|
|
1187
1187
|
return { name: "certificates.search", query, output };
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
tableRetyped,
|
|
23
23
|
toolNote,
|
|
24
24
|
verdictNote
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-J6LBR6GI.js";
|
|
26
26
|
import {
|
|
27
27
|
NO_CONTEXT,
|
|
28
28
|
STAGE_NAMES,
|
|
@@ -100,10 +100,15 @@ import {
|
|
|
100
100
|
sha256Hex
|
|
101
101
|
} from "./chunk-VT7DR6NQ.js";
|
|
102
102
|
import {
|
|
103
|
+
aggregateBlock,
|
|
104
|
+
aggregateNote,
|
|
103
105
|
certificateLinks,
|
|
106
|
+
queryFamily,
|
|
107
|
+
registerAggregate,
|
|
108
|
+
registerAggregateIntent,
|
|
104
109
|
registerNote,
|
|
105
110
|
searchRegister
|
|
106
|
-
} from "./chunk-
|
|
111
|
+
} from "./chunk-GBGRHXRU.js";
|
|
107
112
|
import {
|
|
108
113
|
P
|
|
109
114
|
} from "./chunk-3FYJM7LH.js";
|
|
@@ -2066,7 +2071,24 @@ async function handleAsk(env, ctx, req, tier, key) {
|
|
|
2066
2071
|
}
|
|
2067
2072
|
}
|
|
2068
2073
|
console.log("register-search:", JSON.stringify({ shaped: !!register, tokens: register?.tokens ?? null, rows: register?.rows?.length ?? null, first: register?.rows?.[0]?.num ?? null }));
|
|
2069
|
-
|
|
2074
|
+
function queryFamilyForAggregate(query) {
|
|
2075
|
+
return queryFamily(query);
|
|
2076
|
+
}
|
|
2077
|
+
let registerBlock = null;
|
|
2078
|
+
let aggregateNoteText;
|
|
2079
|
+
const aggIntent = registerAggregateIntent(q.query);
|
|
2080
|
+
if (aggIntent) {
|
|
2081
|
+
const fam = queryFamilyForAggregate(q.query);
|
|
2082
|
+
if (fam) {
|
|
2083
|
+
const agg = await registerAggregate(env.DB, fam);
|
|
2084
|
+
if (agg) {
|
|
2085
|
+
aggregateNoteText = aggregateNote(agg);
|
|
2086
|
+
registerBlock = aggregateBlock(agg);
|
|
2087
|
+
console.log("register aggregate:", agg.family, "count", agg.count, "latest", agg.latest?.num ?? "none");
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
const regNote = aggregateNoteText ?? (register ? [registerNote(register.rows), certificateLinks(register.rows)].filter(Boolean).join("\n") : void 0);
|
|
2070
2092
|
const ops = await operationsLane(env.DB, q.query);
|
|
2071
2093
|
if (understanding?.intent === "conversational") {
|
|
2072
2094
|
const queryHash2 = await sha256Hex(q.query);
|
|
@@ -2751,7 +2773,7 @@ ${q.query}`.replace("\n\n\n\n", "\n\n") }];
|
|
|
2751
2773
|
if (v.support !== "supported") console.log("entailment:", v.support, raced.score, raced.ungrounded_claims?.slice(0, 2));
|
|
2752
2774
|
}
|
|
2753
2775
|
}
|
|
2754
|
-
const out = { answer, citations: finalCites, ...jsonQuality ? { source_quality: jsonQuality, confidence_note: entailmentNote || qualityNote(jsonQuality), ...entailment ? { entailment } : {}, ...jsonQuality === "ocr" ? { experimental_sources: experimentalSourceLabels(finalCites) } : {} } : {}, 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 } : {}, ...ablate ? { ablate } : {}, route: { route: routed.route, features: routed.features } };
|
|
2776
|
+
const out = { answer, citations: finalCites, ...jsonQuality ? { source_quality: jsonQuality, confidence_note: entailmentNote || qualityNote(jsonQuality), ...entailment ? { entailment } : {}, ...jsonQuality === "ocr" ? { experimental_sources: experimentalSourceLabels(finalCites) } : {} } : {}, model: MODELS.member, query_hash: queryHash, follow_ups: understanding?.follow_ups ?? [], blocks: [...c2ns.blocks, ...verdictBlock ? [verdictBlock] : [], ...conditionBlock ? [conditionBlock] : [], ...aggregationBlock ? [aggregationBlock] : [], ...registerBlock ? [registerBlock] : [], ...completionBlocks], context_applied: ctxApplied, ...liveRecords ? { records: liveRecords } : {}, ...ablate ? { ablate } : {}, route: { route: routed.route, features: routed.features } };
|
|
2755
2777
|
const cacheable = !contextual && !declaredCtx && !ablate && !answer.includes(refusalAnswer()) && finalAnchors.violations.length === 0;
|
|
2756
2778
|
if (cacheable) {
|
|
2757
2779
|
const ck = exactCacheKey(env.INDEX_VERSION, gen, ns, await sha256Hex(cacheKeyMaterial(q.query, q.lang, salt)));
|
|
@@ -4,12 +4,12 @@ import {
|
|
|
4
4
|
handleMemories,
|
|
5
5
|
scoreFaithfulness,
|
|
6
6
|
scoreJudge
|
|
7
|
-
} from "../../chunk-
|
|
7
|
+
} from "../../chunk-ZO6MMKGD.js";
|
|
8
8
|
import {
|
|
9
9
|
TOOLS_REGISTRY,
|
|
10
10
|
bindModelNode,
|
|
11
11
|
modelGroundingBlock
|
|
12
|
-
} from "../../chunk-
|
|
12
|
+
} from "../../chunk-J6LBR6GI.js";
|
|
13
13
|
import {
|
|
14
14
|
handleSearch
|
|
15
15
|
} from "../../chunk-P6IRYZLL.js";
|
|
@@ -68,7 +68,7 @@ import {
|
|
|
68
68
|
sha256Hex,
|
|
69
69
|
today
|
|
70
70
|
} from "../../chunk-VT7DR6NQ.js";
|
|
71
|
-
import "../../chunk-
|
|
71
|
+
import "../../chunk-GBGRHXRU.js";
|
|
72
72
|
import {
|
|
73
73
|
P,
|
|
74
74
|
setProfile
|
|
@@ -1043,13 +1043,13 @@ async function callToolOnce(env, ctx, _req, tier, key, name, args, t0) {
|
|
|
1043
1043
|
// stream:false forces the JSON lane (anon defaults to SSE)
|
|
1044
1044
|
body: JSON.stringify({ ...args, stream: false })
|
|
1045
1045
|
});
|
|
1046
|
-
const { runTool, TOOLS_REGISTRY: TOOLS_REGISTRY2 } = await import("../../tools-
|
|
1046
|
+
const { runTool, TOOLS_REGISTRY: TOOLS_REGISTRY2 } = await import("../../tools-O4JYGVSR.js");
|
|
1047
1047
|
let payload;
|
|
1048
1048
|
if (TOOLS_REGISTRY2.some((t) => t.name === name && t.audiences.includes("mcp"))) {
|
|
1049
1049
|
const r = await runTool({ ...env, DB: env.DB }, { name, args }, "mcp");
|
|
1050
1050
|
payload = r ? { tool: r.name, query: r.query, result: r.output } : { error: { message: `tool ${name} returned nothing for the given arguments` } };
|
|
1051
1051
|
} else {
|
|
1052
|
-
const res = name === "ask" ? await (await import("../../ask-
|
|
1052
|
+
const res = name === "ask" ? await (await import("../../ask-XAI5SVJ7.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-OVUHVV35.js")).handleSearch(env, ctx, inner, tier, key);
|
|
1053
1053
|
payload = await res.json().catch(() => ({ error: { message: "tool transport failed", status: res.status } }));
|
|
1054
1054
|
}
|
|
1055
1055
|
telemetry(env, ctx, tier, `mcp:${name}`, null, !payload?.error, JSON.stringify(payload).length, await sha256Hex(`${name}:${JSON.stringify(args ?? {})}`), void 0, void 0, { durationMs: Date.now() - t0 });
|
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.124",
|
|
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",
|
|
@@ -44,7 +44,7 @@ export type { Env };
|
|
|
44
44
|
import { json, err, corsHeaders, readJson, validateQuery, type ApiKey } from "./lib/http";
|
|
45
45
|
import { chargeTokens, checkQuota, clientIp, estimateTokens, telemetry, tokenBudget, usageTotal } from "./quota";
|
|
46
46
|
import { graphExpand, editionNote } from "./graph";
|
|
47
|
-
import { registerNote, searchRegister, certificateLinks } from "./certificates";
|
|
47
|
+
import { registerNote, searchRegister, certificateLinks, registerAggregateIntent, registerAggregate, aggregateNote, aggregateBlock, queryFamily } from "./certificates";
|
|
48
48
|
import { TOOL_DECLARATION, parseToolCall, runTool, toolNote } from "./tools";
|
|
49
49
|
import { operationsLane } from "./operations.ts";
|
|
50
50
|
import { extractNameplate, nameplateRegisterQuery } from "./nameplate.ts";
|
|
@@ -756,7 +756,28 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
756
756
|
}
|
|
757
757
|
}
|
|
758
758
|
console.log("register-search:", JSON.stringify({ shaped: !!register, tokens: register?.tokens ?? null, rows: register?.rows?.length ?? null, first: register?.rows?.[0]?.num ?? null }));
|
|
759
|
-
|
|
759
|
+
function queryFamilyForAggregate(query: string): string | null {
|
|
760
|
+
return queryFamily(query);
|
|
761
|
+
}
|
|
762
|
+
// the AGGREGATE register intent (TODO 2026-10-11): count/latest
|
|
763
|
+
// questions are answered by deterministic computation — the numbers
|
|
764
|
+
// ride as an authoritative note AND a typed block the UI renders
|
|
765
|
+
let registerBlock: any = null;
|
|
766
|
+
let aggregateNoteText: string | undefined;
|
|
767
|
+
const aggIntent = registerAggregateIntent(q.query);
|
|
768
|
+
if (aggIntent) {
|
|
769
|
+
const fam = queryFamilyForAggregate(q.query);
|
|
770
|
+
if (fam) {
|
|
771
|
+
const agg = await registerAggregate(env.DB, fam);
|
|
772
|
+
if (agg) {
|
|
773
|
+
aggregateNoteText = aggregateNote(agg);
|
|
774
|
+
registerBlock = aggregateBlock(agg);
|
|
775
|
+
console.log("register aggregate:", agg.family, "count", agg.count, "latest", agg.latest?.num ?? "none");
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
const regNote = aggregateNoteText
|
|
780
|
+
?? (register ? [registerNote(register.rows), certificateLinks(register.rows)].filter(Boolean).join("\n") : undefined);
|
|
760
781
|
const ops = await operationsLane(env.DB, q.query);
|
|
761
782
|
|
|
762
783
|
// Conversational route, decided by query UNDERSTANDING (any language, any
|
|
@@ -1735,7 +1756,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1735
1756
|
if (v.support !== "supported") console.log("entailment:", v.support, raced.score, raced.ungrounded_claims?.slice(0, 2));
|
|
1736
1757
|
}
|
|
1737
1758
|
}
|
|
1738
|
-
const out = { answer, citations: finalCites, ...(jsonQuality ? { source_quality: jsonQuality, confidence_note: entailmentNote || qualityNote(jsonQuality), ...(entailment ? { entailment } : {}), ...(jsonQuality === "ocr" ? { experimental_sources: experimentalSourceLabels(finalCites) } : {}) } : {}), 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 } : {}), ...(ablate ? { ablate } : {}), route: { route: routed.route, features: routed.features } };
|
|
1759
|
+
const out = { answer, citations: finalCites, ...(jsonQuality ? { source_quality: jsonQuality, confidence_note: entailmentNote || qualityNote(jsonQuality), ...(entailment ? { entailment } : {}), ...(jsonQuality === "ocr" ? { experimental_sources: experimentalSourceLabels(finalCites) } : {}) } : {}), model: MODELS.member, query_hash: queryHash, follow_ups: understanding?.follow_ups ?? [], blocks: [...c2ns.blocks, ...(verdictBlock ? [verdictBlock] : []), ...(conditionBlock ? [conditionBlock] : []), ...(aggregationBlock ? [aggregationBlock] : []), ...(registerBlock ? [registerBlock] : []), ...completionBlocks], context_applied: ctxApplied, ...(liveRecords ? { records: liveRecords } : {}), ...(ablate ? { ablate } : {}), route: { route: routed.route, features: routed.features } };
|
|
1739
1760
|
const cacheable = !contextual && !declaredCtx && !ablate && !answer.includes(refusalAnswer()) && finalAnchors.violations.length === 0;
|
|
1740
1761
|
if (cacheable) {
|
|
1741
1762
|
const ck = exactCacheKey(env.INDEX_VERSION, gen, ns, await sha256Hex(cacheKeyMaterial(q.query, q.lang, salt)));
|
|
@@ -26,6 +26,102 @@ export function isRegisterShaped(query: string): boolean {
|
|
|
26
26
|
return /\bcertif(ied|icates?|ication)s?\b/i.test(query) && /\b(is|are|still|currently|valid|status|suspended|revoked|was|were|have|has|hold|holds|possess|carry|got)\b/i.test(query);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/** An AGGREGATE register question — "how many certificates are issued
|
|
30
|
+
* for R 60:2021?", "what is the latest certificate issued for R 60" —
|
|
31
|
+
* is served by DETERMINISTIC computation (COUNT, ORDER BY), never by
|
|
32
|
+
* the model counting the rows a note happens to carry. The register
|
|
33
|
+
* lane's own philosophy: the note answers the structure, the model
|
|
34
|
+
* narrates it (TODO.sota/04's machine-checkable ground truth, applied
|
|
35
|
+
* to the register plane 2026-10-11). */
|
|
36
|
+
export function registerAggregateIntent(query: string): "count" | "latest" | null {
|
|
37
|
+
if (!/\bcertif(ied|icates?|ication)s?\b/i.test(query)) return null;
|
|
38
|
+
if (/\b(latest|most recent|newest|last)\b/i.test(query)) return "latest";
|
|
39
|
+
if (/\b(how many|count|number of|total|how much)\b/i.test(query)) return "count";
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface RegisterAggregate {
|
|
44
|
+
family: string;
|
|
45
|
+
count: number;
|
|
46
|
+
valid: number;
|
|
47
|
+
suspended: number;
|
|
48
|
+
latest: RegisterRow | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The family's aggregate, computed in SQL (the register is the ground
|
|
52
|
+
* truth; editions do not appear in it — certificates are issued per
|
|
53
|
+
* family). The latest row ranks by year then the sequence number's
|
|
54
|
+
* tail (the -YY.NN suffix carries the actual issue ordering). */
|
|
55
|
+
export async function registerAggregate(db: any, family: string): Promise<RegisterAggregate | null> {
|
|
56
|
+
try {
|
|
57
|
+
const digits = family.replace(/^[A-Za-z]+/, "");
|
|
58
|
+
const rows = await db
|
|
59
|
+
.prepare(
|
|
60
|
+
"SELECT num, family, holder, model, year, status, pdf_key FROM certificates WHERE family = ?1 ORDER BY CAST(year AS INTEGER) DESC, num DESC LIMIT 1",
|
|
61
|
+
)
|
|
62
|
+
.bind(digits)
|
|
63
|
+
.all();
|
|
64
|
+
const totals = await db
|
|
65
|
+
.prepare("SELECT COUNT(*) AS n, SUM(CASE WHEN status = 'Valid' THEN 1 ELSE 0 END) AS valid, SUM(CASE WHEN status = 'Suspended' THEN 1 ELSE 0 END) AS suspended FROM certificates WHERE family = ?1")
|
|
66
|
+
.bind(digits)
|
|
67
|
+
.first();
|
|
68
|
+
if (!totals || !totals.n) return null;
|
|
69
|
+
return {
|
|
70
|
+
family: digits,
|
|
71
|
+
count: Number(totals.n),
|
|
72
|
+
valid: Number(totals.valid ?? 0),
|
|
73
|
+
suspended: Number(totals.suspended ?? 0),
|
|
74
|
+
latest: ((rows.results ?? [])[0] as RegisterRow) ?? null,
|
|
75
|
+
};
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The aggregate's authoritative note: the numbers are DATA — the
|
|
82
|
+
* model narrates, never recomputes. The edition disclaimer rides every
|
|
83
|
+
* family-scoped answer (the register has no edition dimension). */
|
|
84
|
+
export function aggregateNote(agg: RegisterAggregate): string {
|
|
85
|
+
const latest = agg.latest
|
|
86
|
+
? `The latest certificate in the snapshot is ${agg.latest.num} (holder ${agg.latest.holder}, model "${agg.latest.model}"${agg.latest.year ? `, issued ${agg.latest.year}` : ""}, status ${agg.latest.status}).`
|
|
87
|
+
: "No certificate rows are present in the snapshot.";
|
|
88
|
+
const link = agg.latest?.pdf_key
|
|
89
|
+
? ` Its document: https://www.oimlsmart.org/cert-pdf/${agg.latest.pdf_key}`
|
|
90
|
+
: "";
|
|
91
|
+
return [
|
|
92
|
+
`CERTIFICATE REGISTER — THIS NOTE ANSWERS THE QUESTION. Answer from THIS note; do not refuse for want of passages (the retrieval passages are irrelevant for register questions).`,
|
|
93
|
+
`The ${agg.family} family holds ${agg.count} certificate${agg.count === 1 ? "" : "s"} (${agg.valid} valid${agg.suspended ? `, ${agg.suspended} suspended` : ""}) — computed over the register snapshot; state these numbers verbatim.`,
|
|
94
|
+
latest + link,
|
|
95
|
+
"Certificates are issued per RECOMMENDATION FAMILY — the register carries no edition dimension, so an edition named in the question (e.g. 2021) does not narrow it; say so when the question names an edition. Qualify that the register is a snapshot, and cite the register.",
|
|
96
|
+
].join("\n");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** A typed block for aggregate answers — the UI renders the count and
|
|
100
|
+
* the latest row's link as data, never from prose. */
|
|
101
|
+
export function aggregateBlock(agg: RegisterAggregate) {
|
|
102
|
+
return {
|
|
103
|
+
type: "register",
|
|
104
|
+
payload: {
|
|
105
|
+
family: agg.family,
|
|
106
|
+
count: agg.count,
|
|
107
|
+
valid: agg.valid,
|
|
108
|
+
...(agg.suspended ? { suspended: agg.suspended } : {}),
|
|
109
|
+
...(agg.latest
|
|
110
|
+
? {
|
|
111
|
+
latest: {
|
|
112
|
+
num: agg.latest.num,
|
|
113
|
+
holder: agg.latest.holder,
|
|
114
|
+
model: agg.latest.model,
|
|
115
|
+
year: agg.latest.year,
|
|
116
|
+
status: agg.latest.status,
|
|
117
|
+
...(agg.latest.pdf_key ? { url: `/cert-pdf/${agg.latest.pdf_key}` } : {}),
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
: {}),
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
29
125
|
/** Search tokens: words that can identify a holder or a model — words of
|
|
30
126
|
* 2+ characters that are not register question words, plus bare model
|
|
31
127
|
* numbers ("190", "HM14H1"). Upper-cased tokens match case-insensitively
|
|
@@ -46,6 +142,18 @@ export function printedCertificateNumber(query: string): string | null {
|
|
|
46
142
|
return m ? m[1].replace(/\s+/g, "").replace(/-\./, ".") : null;
|
|
47
143
|
}
|
|
48
144
|
|
|
145
|
+
function editionSuffixYears(query: string): Set<string> {
|
|
146
|
+
// "R 60:2021", "R 60-1:2021" — the EDITION carries no register
|
|
147
|
+
// meaning (certificates key on family); its bare year token would
|
|
148
|
+
// poison the token arms (the "2021" matched nothing: issue scheme
|
|
149
|
+
// years differ from edition years). Collect them for stripping.
|
|
150
|
+
const years = new Set<string>();
|
|
151
|
+
for (const m of query.matchAll(/\b([RDMB])\s?-?\d{2,3}(?:-\d+)?\s?:\s?(\d{4})\b/gi)) {
|
|
152
|
+
years.add(m[2]!);
|
|
153
|
+
}
|
|
154
|
+
return years;
|
|
155
|
+
}
|
|
156
|
+
|
|
49
157
|
export function registerTokens(query: string): string[] {
|
|
50
158
|
const stop = new Set([
|
|
51
159
|
"the", "a", "an", "is", "are", "was", "were", "still", "currently", "in", "on", "for", "of", "and", "or",
|
|
@@ -55,9 +163,12 @@ export function registerTokens(query: string): string[] {
|
|
|
55
163
|
]);
|
|
56
164
|
const tokens: string[] = [];
|
|
57
165
|
const publisherWord = P().publisher.name.toLowerCase();
|
|
166
|
+
const editionYears = editionSuffixYears(query);
|
|
167
|
+
const stop2 = new Set([...stop, "many", "issued", "issue", "issued", "latest", "newest", "recent", "total", "number"]);
|
|
58
168
|
for (const w of query.split(/[^A-Za-z0-9&-]+/)) {
|
|
59
169
|
if (w.length < 2) continue;
|
|
60
|
-
if (
|
|
170
|
+
if (stop2.has(w.toLowerCase())) continue;
|
|
171
|
+
if (/^\d{4}$/.test(w) && editionYears.has(w)) continue; // an edition year, not an issue filter
|
|
61
172
|
if (w.toLowerCase() === publisherWord) continue;
|
|
62
173
|
if (/^\d+$/.test(w) && w.length < 2) continue;
|
|
63
174
|
if (!tokens.includes(w)) tokens.push(w);
|