@konneal/engine 0.2.40 → 0.2.42
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-QHODBUU2.js → ask-LFU3M2JA.js} +1 -1
- package/dist/certificates.d.ts +23 -0
- package/dist/{chunk-5SLSRFOG.js → chunk-ZAAKM54K.js} +94 -4
- package/dist/worker_public/src/index.js +2 -2
- package/package.json +1 -1
- package/workers/worker_public/src/ask.ts +19 -5
- package/workers/worker_public/src/certificates.ts +78 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface RegisterRow {
|
|
2
|
+
num: string;
|
|
3
|
+
family: string;
|
|
4
|
+
holder: string;
|
|
5
|
+
model: string;
|
|
6
|
+
year: string;
|
|
7
|
+
status: string;
|
|
8
|
+
}
|
|
9
|
+
/** A question is register-shaped when it asks about certification
|
|
10
|
+
* standing of an identifiable model or holder — the deterministic
|
|
11
|
+
* trigger the citation-graph notes already use (query-shaped notes,
|
|
12
|
+
* never a router). */
|
|
13
|
+
export declare function isRegisterShaped(query: string): boolean;
|
|
14
|
+
/** Search tokens: words that can identify a holder or a model — words of
|
|
15
|
+
* 2+ characters that are not register question words, plus bare model
|
|
16
|
+
* numbers ("190", "HM14H1"). Upper-cased tokens match case-insensitively
|
|
17
|
+
* in SQL LIKE. */
|
|
18
|
+
export declare function registerTokens(query: string): string[];
|
|
19
|
+
export declare function searchRegister(db: any, query: string): Promise<{
|
|
20
|
+
rows: RegisterRow[];
|
|
21
|
+
tokens: string[];
|
|
22
|
+
} | null>;
|
|
23
|
+
export declare function registerNote(rows: RegisterRow[]): string;
|
|
@@ -1747,6 +1747,93 @@ function semanticCacheKey(indexVersion, gen, signature) {
|
|
|
1747
1747
|
return `sc:${indexVersion}:g${gen}:${signature}`;
|
|
1748
1748
|
}
|
|
1749
1749
|
|
|
1750
|
+
// workers/worker_public/src/certificates.ts
|
|
1751
|
+
function isRegisterShaped(query) {
|
|
1752
|
+
return /\bcertif(ied|icates?|ication)s?\b/i.test(query) && /\b(is|are|still|currently|valid|status|suspended|revoked|was|were)\b/i.test(query);
|
|
1753
|
+
}
|
|
1754
|
+
function registerTokens(query) {
|
|
1755
|
+
const stop = /* @__PURE__ */ new Set([
|
|
1756
|
+
"the",
|
|
1757
|
+
"a",
|
|
1758
|
+
"an",
|
|
1759
|
+
"is",
|
|
1760
|
+
"are",
|
|
1761
|
+
"was",
|
|
1762
|
+
"were",
|
|
1763
|
+
"still",
|
|
1764
|
+
"currently",
|
|
1765
|
+
"in",
|
|
1766
|
+
"on",
|
|
1767
|
+
"for",
|
|
1768
|
+
"of",
|
|
1769
|
+
"and",
|
|
1770
|
+
"or",
|
|
1771
|
+
"certificate",
|
|
1772
|
+
"certificates",
|
|
1773
|
+
"certified",
|
|
1774
|
+
"certification",
|
|
1775
|
+
"register",
|
|
1776
|
+
"status",
|
|
1777
|
+
"valid",
|
|
1778
|
+
"suspended",
|
|
1779
|
+
"revoked",
|
|
1780
|
+
"recommendation",
|
|
1781
|
+
"per",
|
|
1782
|
+
"under",
|
|
1783
|
+
"by",
|
|
1784
|
+
"with",
|
|
1785
|
+
"what",
|
|
1786
|
+
"which",
|
|
1787
|
+
"who",
|
|
1788
|
+
"does",
|
|
1789
|
+
"do",
|
|
1790
|
+
"load",
|
|
1791
|
+
"cell",
|
|
1792
|
+
"model",
|
|
1793
|
+
"manufacturer",
|
|
1794
|
+
"holder",
|
|
1795
|
+
"company",
|
|
1796
|
+
"r",
|
|
1797
|
+
"how",
|
|
1798
|
+
"to",
|
|
1799
|
+
"list",
|
|
1800
|
+
"show"
|
|
1801
|
+
]);
|
|
1802
|
+
const tokens2 = [];
|
|
1803
|
+
const publisherWord = P().publisher.name.toLowerCase();
|
|
1804
|
+
for (const w of query.split(/[^A-Za-z0-9&-]+/)) {
|
|
1805
|
+
if (w.length < 2) continue;
|
|
1806
|
+
if (stop.has(w.toLowerCase())) continue;
|
|
1807
|
+
if (w.toLowerCase() === publisherWord) continue;
|
|
1808
|
+
if (/^\d+$/.test(w) && w.length < 2) continue;
|
|
1809
|
+
if (!tokens2.includes(w)) tokens2.push(w);
|
|
1810
|
+
}
|
|
1811
|
+
return tokens2.slice(0, 6);
|
|
1812
|
+
}
|
|
1813
|
+
async function searchRegister(db, query) {
|
|
1814
|
+
if (!isRegisterShaped(query)) return null;
|
|
1815
|
+
const tokens2 = registerTokens(query);
|
|
1816
|
+
if (!tokens2.length) return null;
|
|
1817
|
+
const clauses = tokens2.map(() => "(holder LIKE ?1 OR model LIKE ?1 OR num LIKE ?1)").join(" OR ");
|
|
1818
|
+
const params = tokens2.map((t) => `%${t}%`);
|
|
1819
|
+
try {
|
|
1820
|
+
const res = await db.prepare(`SELECT num, family, holder, model, year, status FROM certificates WHERE ${clauses} LIMIT 6`).bind(...params).all();
|
|
1821
|
+
return { rows: res.results ?? [], tokens: tokens2 };
|
|
1822
|
+
} catch {
|
|
1823
|
+
return { rows: [], tokens: tokens2 };
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
function registerNote(rows) {
|
|
1827
|
+
if (!rows.length) {
|
|
1828
|
+
return `Certificate register: NO certificate matching the asked holder or model appears in the ${P().publisher.name}-CS register snapshot. State plainly that no such certificate is in this register, and that the register is a snapshot rather than the live certification status.`;
|
|
1829
|
+
}
|
|
1830
|
+
const lines = rows.map((r) => `- ${r.num}: holder ${r.holder}, model "${r.model}"${r.year ? `, issued ${r.year}` : ""} \u2014 status ${r.status}`);
|
|
1831
|
+
return [
|
|
1832
|
+
"Certificate register \u2014 the following certificates matched the asked holder or model. Quote the certificate number, the holder and the status verbatim; the register is a snapshot, so qualify any statement about current certification accordingly.",
|
|
1833
|
+
...lines
|
|
1834
|
+
].join("\n");
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1750
1837
|
// workers/worker_public/src/ask.ts
|
|
1751
1838
|
function userImageDataUrl(body) {
|
|
1752
1839
|
const img = body?.image;
|
|
@@ -2077,6 +2164,8 @@ async function handleAsk(env, ctx, req, tier, key) {
|
|
|
2077
2164
|
console.log("understand:", understanding?.intent ?? "null", understanding?.doc_number ? `doc#${understanding.doc_number}${understanding.edition ? "@" + understanding.edition : ""}` : "nodoc", "|", q.query.slice(0, 50));
|
|
2078
2165
|
const graphDocNumbers = await graphExpand(env, understanding);
|
|
2079
2166
|
const eNote = await editionNote(env, understanding);
|
|
2167
|
+
const register = await searchRegister(env.DB, q.query);
|
|
2168
|
+
const regNote = register ? registerNote(register.rows) : void 0;
|
|
2080
2169
|
if (understanding?.intent !== "conversational" && !nodeScoped && !contextual && !declaredCtx && !draftAct && !apiCallIntent && !userImage && !fresh) {
|
|
2081
2170
|
const warmVec = await warmEmbed ?? null;
|
|
2082
2171
|
if (warmVec) {
|
|
@@ -2443,14 +2532,15 @@ Answer account questions from these records ONLY: name the record when you use i
|
|
|
2443
2532
|
q.lang,
|
|
2444
2533
|
keptHistory,
|
|
2445
2534
|
// stage-extracted graph facts (GraphRAG) ride the same note channel
|
|
2446
|
-
[processNote, eNote, contextNote(declaredCtx, docScope), accountNote, modelNote, vocabNote, memNote, machineNote, conditionNote, aggregationNote, boundaryNote, licenseNote, ...retrieved.notes ?? []].filter(Boolean).join("\n") || void 0,
|
|
2535
|
+
[processNote, eNote, regNote, contextNote(declaredCtx, docScope), accountNote, modelNote, vocabNote, memNote, machineNote, conditionNote, aggregationNote, boundaryNote, licenseNote, ...retrieved.notes ?? []].filter(Boolean).join("\n") || void 0,
|
|
2447
2536
|
summary,
|
|
2448
2537
|
budget
|
|
2449
2538
|
);
|
|
2450
2539
|
await attachFigureImages(env, messages, usedHits, q.query);
|
|
2451
2540
|
if (userImage) {
|
|
2541
|
+
const wantsMarkings = /\b(mark(ing|ings)?|nameplate|label|inscription|engrav|sticker|plate)\b|certificat/i.test(q.query);
|
|
2452
2542
|
const last = messages[messages.length - 1];
|
|
2453
|
-
const note = "\n\n(The user attached an image with this question; interpret it directly when answering.)";
|
|
2543
|
+
const note = wantsMarkings ? "\n\n(The user attached a photo with this question. First transcribe every inscription you can actually read in the image \u2014 names, model references, accuracy classes, numeric values with their units, certificate or approval numbers \u2014 quoting them verbatim. Then answer the question from the numbered passages, citing the requirement each interpretation rests on. If an inscription is unreadable, say so; never invent a marking.)" : "\n\n(The user attached an image with this question; interpret it directly when answering.)";
|
|
2454
2544
|
if (Array.isArray(last.content)) {
|
|
2455
2545
|
const textPart = last.content.find((p) => p.type === "text");
|
|
2456
2546
|
if (textPart) textPart.text += note;
|
|
@@ -2476,7 +2566,7 @@ Answer account questions from these records ONLY: name the record when you use i
|
|
|
2476
2566
|
`));
|
|
2477
2567
|
send({ type: "read", read: readAs() });
|
|
2478
2568
|
const sourceQuality = answerQuality(cites.map((c) => c.quality));
|
|
2479
|
-
send({ type: "citations", citations: cites, context_applied: ctxApplied, ...sourceQuality ? { source_quality: sourceQuality,
|
|
2569
|
+
send({ type: "citations", citations: cites, context_applied: ctxApplied, ...sourceQuality ? { source_quality: sourceQuality, confidence_note: qualityNote(sourceQuality), ...sourceQuality === "ocr" ? { experimental_sources: experimentalSourceLabels(cites) } : {} } : {}, ...liveRecords ? { records: liveRecords } : {}, quota });
|
|
2480
2570
|
let full = "";
|
|
2481
2571
|
try {
|
|
2482
2572
|
for await (const tok of sseTokens(stream)) {
|
|
@@ -2610,7 +2700,7 @@ Answer account questions from these records ONLY: name the record when you use i
|
|
|
2610
2700
|
}
|
|
2611
2701
|
completionBlocks.push(...await completeFigures(env.DB, answer, [...c2ns.blocks, ...completionBlocks], used));
|
|
2612
2702
|
const jsonQuality = answerQuality(finalCites.map((c) => c.quality));
|
|
2613
|
-
const out = { answer, citations: finalCites, ...jsonQuality ? { source_quality: jsonQuality,
|
|
2703
|
+
const out = { answer, citations: finalCites, ...jsonQuality ? { source_quality: jsonQuality, confidence_note: qualityNote(jsonQuality), ...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 } : {} };
|
|
2614
2704
|
const cacheable = !contextual && !declaredCtx && !answer.includes(refusalAnswer()) && finalAnchors.violations.length === 0;
|
|
2615
2705
|
if (cacheable) {
|
|
2616
2706
|
const warmVec = await warmEmbed ?? null;
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
modelGroundingBlock,
|
|
10
10
|
scoreJudge,
|
|
11
11
|
standardForDocNumber
|
|
12
|
-
} from "../../chunk-
|
|
12
|
+
} from "../../chunk-ZAAKM54K.js";
|
|
13
13
|
import {
|
|
14
14
|
buildMessages,
|
|
15
15
|
citations,
|
|
@@ -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-LFU3M2JA.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-QZDQVB57.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.42",
|
|
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",
|
|
@@ -37,6 +37,7 @@ export type { Env };
|
|
|
37
37
|
import { json, err, corsHeaders, readJson, validateQuery, type ApiKey } from "./lib/http";
|
|
38
38
|
import { clientIp, checkQuota, telemetry } from "./quota";
|
|
39
39
|
import { graphExpand, editionNote } from "./graph";
|
|
40
|
+
import { registerNote, searchRegister } from "./certificates";
|
|
40
41
|
import { P } from "./profile.ts";
|
|
41
42
|
|
|
42
43
|
/** User-uploaded image for multimodal questions: a data URL
|
|
@@ -577,6 +578,12 @@ async function handleAsk(
|
|
|
577
578
|
console.log("understand:", understanding?.intent ?? "null", understanding?.doc_number ? `doc#${understanding.doc_number}${understanding.edition ? "@" + understanding.edition : ""}` : "nodoc", "|", q.query.slice(0, 50));
|
|
578
579
|
const graphDocNumbers = await graphExpand(env, understanding);
|
|
579
580
|
const eNote = await editionNote(env, understanding);
|
|
581
|
+
// the certificate register as a database (TODO.new-era/6): a
|
|
582
|
+
// register-shaped question queries the D1 table directly and the rows
|
|
583
|
+
// join the prompt as an authoritative note — exact matches, never a
|
|
584
|
+
// similarity guess about certification standing
|
|
585
|
+
const register = await searchRegister(env.DB, q.query);
|
|
586
|
+
const regNote = register ? registerNote(register.rows) : undefined;
|
|
580
587
|
|
|
581
588
|
// semantic cache: near-duplicate of a recently answered question —
|
|
582
589
|
// serves the stored answer with a `similar: true` marker (checked only
|
|
@@ -1108,16 +1115,23 @@ async function handleAsk(
|
|
|
1108
1115
|
q.lang,
|
|
1109
1116
|
keptHistory,
|
|
1110
1117
|
// stage-extracted graph facts (GraphRAG) ride the same note channel
|
|
1111
|
-
[processNote, eNote, contextNote(declaredCtx, docScope), accountNote, modelNote, vocabNote, memNote, machineNote, conditionNote, aggregationNote, boundaryNote, licenseNote, ...(retrieved.notes ?? [])].filter(Boolean).join("\n") || undefined,
|
|
1118
|
+
[processNote, eNote, regNote, contextNote(declaredCtx, docScope), accountNote, modelNote, vocabNote, memNote, machineNote, conditionNote, aggregationNote, boundaryNote, licenseNote, ...(retrieved.notes ?? [])].filter(Boolean).join("\n") || undefined,
|
|
1112
1119
|
summary,
|
|
1113
1120
|
budget,
|
|
1114
1121
|
);
|
|
1115
1122
|
await attachFigureImages(env, messages, usedHits, q.query);
|
|
1116
1123
|
if (userImage) {
|
|
1117
1124
|
// the user's own image rides on the question message — retrieval stays
|
|
1118
|
-
// text-driven; the answer model reads the image as question context
|
|
1125
|
+
// text-driven; the answer model reads the image as question context.
|
|
1126
|
+
// A markings/nameplate/certificate photo gets a TRANSCRIPTION note:
|
|
1127
|
+
// read the inscriptions verbatim first, then interpret from the
|
|
1128
|
+
// passages (the marking requirements are corpus content) — never
|
|
1129
|
+
// invent a marking that cannot be read.
|
|
1130
|
+
const wantsMarkings = /\b(mark(ing|ings)?|nameplate|label|inscription|engrav|sticker|plate)\b|certificat/i.test(q.query);
|
|
1119
1131
|
const last = messages[messages.length - 1];
|
|
1120
|
-
const note =
|
|
1132
|
+
const note = wantsMarkings
|
|
1133
|
+
? "\n\n(The user attached a photo with this question. First transcribe every inscription you can actually read in the image — names, model references, accuracy classes, numeric values with their units, certificate or approval numbers — quoting them verbatim. Then answer the question from the numbered passages, citing the requirement each interpretation rests on. If an inscription is unreadable, say so; never invent a marking.)"
|
|
1134
|
+
: "\n\n(The user attached an image with this question; interpret it directly when answering.)";
|
|
1121
1135
|
if (Array.isArray(last.content)) {
|
|
1122
1136
|
const textPart = last.content.find((p: any) => p.type === "text");
|
|
1123
1137
|
if (textPart) textPart.text += note;
|
|
@@ -1147,7 +1161,7 @@ async function handleAsk(
|
|
|
1147
1161
|
// retrieval, before a single token of the answer
|
|
1148
1162
|
send({ type: "read", read: readAs() });
|
|
1149
1163
|
const sourceQuality = answerQuality(cites.map((c: any) => c.quality));
|
|
1150
|
-
send({ type: "citations", citations: cites, context_applied: ctxApplied, ...(sourceQuality ? { source_quality: sourceQuality,
|
|
1164
|
+
send({ type: "citations", citations: cites, context_applied: ctxApplied, ...(sourceQuality ? { source_quality: sourceQuality, confidence_note: qualityNote(sourceQuality), ...(sourceQuality === "ocr" ? { experimental_sources: experimentalSourceLabels(cites) } : {}) } : {}), ...(liveRecords ? { records: liveRecords } : {}), quota, });
|
|
1151
1165
|
let full = "";
|
|
1152
1166
|
try {
|
|
1153
1167
|
for await (const tok of sseTokens(stream)) {
|
|
@@ -1342,7 +1356,7 @@ async function handleAsk(
|
|
|
1342
1356
|
completionBlocks.push(...(await completeFigures(env.DB, answer, [...c2ns.blocks, ...completionBlocks], used)));
|
|
1343
1357
|
|
|
1344
1358
|
const jsonQuality = answerQuality(finalCites.map((c: any) => c.quality));
|
|
1345
|
-
const out = { answer, citations: finalCites, ...(jsonQuality ? { source_quality: jsonQuality,
|
|
1359
|
+
const out = { answer, citations: finalCites, ...(jsonQuality ? { source_quality: jsonQuality, confidence_note: qualityNote(jsonQuality), ...(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 } : {}) };
|
|
1346
1360
|
const cacheable = !contextual && !declaredCtx && !answer.includes(refusalAnswer()) && finalAnchors.violations.length === 0;
|
|
1347
1361
|
if (cacheable) {
|
|
1348
1362
|
const warmVec = (await warmEmbed) ?? null;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// The certificate register as a STRUCTURED search surface (TODO.new-era/6):
|
|
2
|
+
// "is model X from holder Y still certified?" is an exact-match question,
|
|
3
|
+
// not a similarity question. The register rows live in D1 (the
|
|
4
|
+
// certificates plane's `certificates` table); this stage decides when a
|
|
5
|
+
// question is register-shaped, queries the table, and returns the rows as
|
|
6
|
+
// an authoritative note for the prompt. A miss is a MISS: "not in this
|
|
7
|
+
// register snapshot" is stated verbatim, never softened into a guess.
|
|
8
|
+
|
|
9
|
+
import { P } from './profile.ts';
|
|
10
|
+
|
|
11
|
+
export interface RegisterRow {
|
|
12
|
+
num: string;
|
|
13
|
+
family: string;
|
|
14
|
+
holder: string;
|
|
15
|
+
model: string;
|
|
16
|
+
year: string;
|
|
17
|
+
status: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A question is register-shaped when it asks about certification
|
|
21
|
+
* standing of an identifiable model or holder — the deterministic
|
|
22
|
+
* trigger the citation-graph notes already use (query-shaped notes,
|
|
23
|
+
* never a router). */
|
|
24
|
+
export function isRegisterShaped(query: string): boolean {
|
|
25
|
+
return /\bcertif(ied|icates?|ication)s?\b/i.test(query) && /\b(is|are|still|currently|valid|status|suspended|revoked|was|were)\b/i.test(query);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Search tokens: words that can identify a holder or a model — words of
|
|
29
|
+
* 2+ characters that are not register question words, plus bare model
|
|
30
|
+
* numbers ("190", "HM14H1"). Upper-cased tokens match case-insensitively
|
|
31
|
+
* in SQL LIKE. */
|
|
32
|
+
export function registerTokens(query: string): string[] {
|
|
33
|
+
const stop = new Set([
|
|
34
|
+
"the", "a", "an", "is", "are", "was", "were", "still", "currently", "in", "on", "for", "of", "and", "or",
|
|
35
|
+
"certificate", "certificates", "certified", "certification", "register", "status", "valid", "suspended",
|
|
36
|
+
"revoked", "recommendation", "per", "under", "by", "with", "what", "which", "who", "does", "do",
|
|
37
|
+
"load", "cell", "model", "manufacturer", "holder", "company", "r", "how", "to", "list", "show",
|
|
38
|
+
]);
|
|
39
|
+
const tokens: string[] = [];
|
|
40
|
+
const publisherWord = P().publisher.name.toLowerCase();
|
|
41
|
+
for (const w of query.split(/[^A-Za-z0-9&-]+/)) {
|
|
42
|
+
if (w.length < 2) continue;
|
|
43
|
+
if (stop.has(w.toLowerCase())) continue;
|
|
44
|
+
if (w.toLowerCase() === publisherWord) continue;
|
|
45
|
+
if (/^\d+$/.test(w) && w.length < 2) continue;
|
|
46
|
+
if (!tokens.includes(w)) tokens.push(w);
|
|
47
|
+
}
|
|
48
|
+
return tokens.slice(0, 6);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function searchRegister(db: any, query: string): Promise<{ rows: RegisterRow[]; tokens: string[] } | null> {
|
|
52
|
+
if (!isRegisterShaped(query)) return null;
|
|
53
|
+
const tokens = registerTokens(query);
|
|
54
|
+
if (!tokens.length) return null;
|
|
55
|
+
const clauses = tokens.map(() => "(holder LIKE ?1 OR model LIKE ?1 OR num LIKE ?1)").join(" OR ");
|
|
56
|
+
const params = tokens.map((t) => `%${t}%`);
|
|
57
|
+
try {
|
|
58
|
+
const res = await db
|
|
59
|
+
.prepare(`SELECT num, family, holder, model, year, status FROM certificates WHERE ${clauses} LIMIT 6`)
|
|
60
|
+
.bind(...params)
|
|
61
|
+
.all();
|
|
62
|
+
return { rows: (res.results ?? []) as RegisterRow[], tokens };
|
|
63
|
+
} catch {
|
|
64
|
+
// a missing table or a D1 hiccup degrades to no-note honestly
|
|
65
|
+
return { rows: [], tokens };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function registerNote(rows: RegisterRow[]): string {
|
|
70
|
+
if (!rows.length) {
|
|
71
|
+
return `Certificate register: NO certificate matching the asked holder or model appears in the ${P().publisher.name}-CS register snapshot. State plainly that no such certificate is in this register, and that the register is a snapshot rather than the live certification status.`;
|
|
72
|
+
}
|
|
73
|
+
const lines = rows.map((r) => `- ${r.num}: holder ${r.holder}, model "${r.model}"${r.year ? `, issued ${r.year}` : ""} — status ${r.status}`);
|
|
74
|
+
return [
|
|
75
|
+
"Certificate register — the following certificates matched the asked holder or model. Quote the certificate number, the holder and the status verbatim; the register is a snapshot, so qualify any statement about current certification accordingly.",
|
|
76
|
+
...lines,
|
|
77
|
+
].join("\n");
|
|
78
|
+
}
|