@konneal/engine 0.2.122 → 0.2.123

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.
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  handleAsk
3
- } from "./chunk-BRUHQEOT.js";
4
- import "./chunk-FKG5VGOL.js";
3
+ } from "./chunk-U6FXEUZP.js";
4
+ import "./chunk-3L6TJAS6.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-JZFXSTYX.js";
11
+ import "./chunk-IOT75KLU.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-JZFXSTYX.js";
14
+ } from "./chunk-IOT75KLU.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
@@ -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
@@ -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-CO7KX2S2.js");
1184
+ const { searchRegister, registerNote, certificateLinks } = await import("./certificates-TDPN7SA2.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 };
@@ -6,6 +6,61 @@ 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 (authoritative computation over the snapshot): the ${agg.family} family holds ${agg.count} certificate${agg.count === 1 ? "" : "s"} (${agg.valid} valid${agg.suspended ? `, ${agg.suspended} suspended` : ""}).`,
39
+ latest + link,
40
+ "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. State the family total, qualify that the register is a snapshot, and cite these numbers verbatim."
41
+ ].join("\n");
42
+ }
43
+ function aggregateBlock(agg) {
44
+ return {
45
+ type: "register",
46
+ payload: {
47
+ family: agg.family,
48
+ count: agg.count,
49
+ valid: agg.valid,
50
+ ...agg.suspended ? { suspended: agg.suspended } : {},
51
+ ...agg.latest ? {
52
+ latest: {
53
+ num: agg.latest.num,
54
+ holder: agg.latest.holder,
55
+ model: agg.latest.model,
56
+ year: agg.latest.year,
57
+ status: agg.latest.status,
58
+ ...agg.latest.pdf_key ? { url: `/cert-pdf/${agg.latest.pdf_key}` } : {}
59
+ }
60
+ } : {}
61
+ }
62
+ };
63
+ }
9
64
  function queryFamily(query) {
10
65
  const m = /\b([RDMB])\s?-?(\d{2,3})\b/i.exec(query);
11
66
  return m ? `${m[1].toUpperCase()}${m[2].padStart(2, "0")}` : null;
@@ -14,6 +69,13 @@ function printedCertificateNumber(query) {
14
69
  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
70
  return m ? m[1].replace(/\s+/g, "").replace(/-\./, ".") : null;
16
71
  }
72
+ function editionSuffixYears(query) {
73
+ const years = /* @__PURE__ */ new Set();
74
+ for (const m of query.matchAll(/\b([RDMB])\s?-?\d{2,3}(?:-\d+)?\s?:\s?(\d{4})\b/gi)) {
75
+ years.add(m[2]);
76
+ }
77
+ return years;
78
+ }
17
79
  function registerTokens(query) {
18
80
  const stop = /* @__PURE__ */ new Set([
19
81
  "the",
@@ -64,9 +126,12 @@ function registerTokens(query) {
64
126
  ]);
65
127
  const tokens = [];
66
128
  const publisherWord = P().publisher.name.toLowerCase();
129
+ const editionYears = editionSuffixYears(query);
130
+ const stop2 = /* @__PURE__ */ new Set([...stop, "many", "issued", "issue", "issued", "latest", "newest", "recent", "total", "number"]);
67
131
  for (const w of query.split(/[^A-Za-z0-9&-]+/)) {
68
132
  if (w.length < 2) continue;
69
- if (stop.has(w.toLowerCase())) continue;
133
+ if (stop2.has(w.toLowerCase())) continue;
134
+ if (/^\d{4}$/.test(w) && editionYears.has(w)) continue;
70
135
  if (w.toLowerCase() === publisherWord) continue;
71
136
  if (/^\d+$/.test(w) && w.length < 2) continue;
72
137
  if (!tokens.includes(w)) tokens.push(w);
@@ -150,6 +215,10 @@ ${absent.join("\n")}`);
150
215
 
151
216
  export {
152
217
  isRegisterShaped,
218
+ registerAggregateIntent,
219
+ registerAggregate,
220
+ aggregateNote,
221
+ aggregateBlock,
153
222
  queryFamily,
154
223
  printedCertificateNumber,
155
224
  registerTokens,
@@ -22,7 +22,7 @@ import {
22
22
  tableRetyped,
23
23
  toolNote,
24
24
  verdictNote
25
- } from "./chunk-FKG5VGOL.js";
25
+ } from "./chunk-3L6TJAS6.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-JZFXSTYX.js";
111
+ } from "./chunk-IOT75KLU.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
- const regNote = register ? [registerNote(register.rows), certificateLinks(register.rows)].filter(Boolean).join("\n") : void 0;
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,7 +4,7 @@ import {
4
4
  parseToolCall,
5
5
  runTool,
6
6
  toolNote
7
- } from "./chunk-FKG5VGOL.js";
7
+ } from "./chunk-3L6TJAS6.js";
8
8
  import "./chunk-TH5XGPD5.js";
9
9
  import "./chunk-4NXH5ZI6.js";
10
10
  import "./chunk-VT7DR6NQ.js";
@@ -4,12 +4,12 @@ import {
4
4
  handleMemories,
5
5
  scoreFaithfulness,
6
6
  scoreJudge
7
- } from "../../chunk-BRUHQEOT.js";
7
+ } from "../../chunk-U6FXEUZP.js";
8
8
  import {
9
9
  TOOLS_REGISTRY,
10
10
  bindModelNode,
11
11
  modelGroundingBlock
12
- } from "../../chunk-FKG5VGOL.js";
12
+ } from "../../chunk-3L6TJAS6.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-JZFXSTYX.js";
71
+ import "../../chunk-IOT75KLU.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-CUKFOW7S.js");
1046
+ const { runTool, TOOLS_REGISTRY: TOOLS_REGISTRY2 } = await import("../../tools-EI6M4AY2.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-J4QFS6Q2.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-OVUHVV35.js")).handleSearch(env, ctx, inner, tier, key);
1052
+ const res = name === "ask" ? await (await import("../../ask-2BYMHPT3.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.122",
30
+ "version": "0.2.123",
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
- const regNote = register ? [registerNote(register.rows), certificateLinks(register.rows)].filter(Boolean).join("\n") : undefined;
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,101 @@ 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 (authoritative computation over the snapshot): the ${agg.family} family holds ${agg.count} certificate${agg.count === 1 ? "" : "s"} (${agg.valid} valid${agg.suspended ? `, ${agg.suspended} suspended` : ""}).`,
93
+ latest + link,
94
+ "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. State the family total, qualify that the register is a snapshot, and cite these numbers verbatim.",
95
+ ].join("\n");
96
+ }
97
+
98
+ /** A typed block for aggregate answers — the UI renders the count and
99
+ * the latest row's link as data, never from prose. */
100
+ export function aggregateBlock(agg: RegisterAggregate) {
101
+ return {
102
+ type: "register",
103
+ payload: {
104
+ family: agg.family,
105
+ count: agg.count,
106
+ valid: agg.valid,
107
+ ...(agg.suspended ? { suspended: agg.suspended } : {}),
108
+ ...(agg.latest
109
+ ? {
110
+ latest: {
111
+ num: agg.latest.num,
112
+ holder: agg.latest.holder,
113
+ model: agg.latest.model,
114
+ year: agg.latest.year,
115
+ status: agg.latest.status,
116
+ ...(agg.latest.pdf_key ? { url: `/cert-pdf/${agg.latest.pdf_key}` } : {}),
117
+ },
118
+ }
119
+ : {}),
120
+ },
121
+ };
122
+ }
123
+
29
124
  /** Search tokens: words that can identify a holder or a model — words of
30
125
  * 2+ characters that are not register question words, plus bare model
31
126
  * numbers ("190", "HM14H1"). Upper-cased tokens match case-insensitively
@@ -46,6 +141,18 @@ export function printedCertificateNumber(query: string): string | null {
46
141
  return m ? m[1].replace(/\s+/g, "").replace(/-\./, ".") : null;
47
142
  }
48
143
 
144
+ function editionSuffixYears(query: string): Set<string> {
145
+ // "R 60:2021", "R 60-1:2021" — the EDITION carries no register
146
+ // meaning (certificates key on family); its bare year token would
147
+ // poison the token arms (the "2021" matched nothing: issue scheme
148
+ // years differ from edition years). Collect them for stripping.
149
+ const years = new Set<string>();
150
+ for (const m of query.matchAll(/\b([RDMB])\s?-?\d{2,3}(?:-\d+)?\s?:\s?(\d{4})\b/gi)) {
151
+ years.add(m[2]!);
152
+ }
153
+ return years;
154
+ }
155
+
49
156
  export function registerTokens(query: string): string[] {
50
157
  const stop = new Set([
51
158
  "the", "a", "an", "is", "are", "was", "were", "still", "currently", "in", "on", "for", "of", "and", "or",
@@ -55,9 +162,12 @@ export function registerTokens(query: string): string[] {
55
162
  ]);
56
163
  const tokens: string[] = [];
57
164
  const publisherWord = P().publisher.name.toLowerCase();
165
+ const editionYears = editionSuffixYears(query);
166
+ const stop2 = new Set([...stop, "many", "issued", "issue", "issued", "latest", "newest", "recent", "total", "number"]);
58
167
  for (const w of query.split(/[^A-Za-z0-9&-]+/)) {
59
168
  if (w.length < 2) continue;
60
- if (stop.has(w.toLowerCase())) continue;
169
+ if (stop2.has(w.toLowerCase())) continue;
170
+ if (/^\d{4}$/.test(w) && editionYears.has(w)) continue; // an edition year, not an issue filter
61
171
  if (w.toLowerCase() === publisherWord) continue;
62
172
  if (/^\d+$/.test(w) && w.length < 2) continue;
63
173
  if (!tokens.includes(w)) tokens.push(w);