@konneal/engine 0.2.55 → 0.2.56

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,6 +1,6 @@
1
1
  import {
2
2
  handleAsk
3
- } from "./chunk-DKQXJN6G.js";
3
+ } from "./chunk-ZVMQHCNL.js";
4
4
  import "./chunk-TPV5GPY2.js";
5
5
  import "./chunk-SPKPH54J.js";
6
6
  import "./chunk-A3QHHUN5.js";
@@ -1866,7 +1866,7 @@ function exactCacheKey(indexVersion, gen, ns, queryHash) {
1866
1866
 
1867
1867
  // workers/worker_public/src/certificates.ts
1868
1868
  function isRegisterShaped(query) {
1869
- 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);
1869
+ 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);
1870
1870
  }
1871
1871
  function queryFamily(query) {
1872
1872
  const m = /\b([RDMB])\s?-?(\d{2,3})\b/i.exec(query);
@@ -1979,6 +1979,53 @@ function registerNote(rows) {
1979
1979
  ].join("\n");
1980
1980
  }
1981
1981
 
1982
+ // workers/worker_public/prompts/nameplate.md
1983
+ var nameplate_default = `You read instrument nameplates. From the photograph, extract the
1984
+ manufacturer's name exactly as printed and the model designation exactly
1985
+ as printed. Answer with ONLY a JSON object on one line:
1986
+
1987
+ {"manufacturer": "\u2026", "model": "\u2026"}
1988
+
1989
+ If the photograph shows no manufacturer or no model, use null for the
1990
+ missing value. Never guess beyond what is printed.
1991
+ `;
1992
+
1993
+ // workers/worker_public/src/nameplate-parse.ts
1994
+ function parseNameplate(text) {
1995
+ const m = /\{[^{}]*\}/.exec(String(text ?? ""));
1996
+ if (!m) return null;
1997
+ try {
1998
+ const j = JSON.parse(m[0]);
1999
+ const clean = (v) => typeof v === "string" && v.trim() ? v.trim().slice(0, 80) : null;
2000
+ const out = { manufacturer: clean(j.manufacturer), model: clean(j.model) };
2001
+ return out.manufacturer || out.model ? out : null;
2002
+ } catch {
2003
+ return null;
2004
+ }
2005
+ }
2006
+ function nameplateRegisterQuery(np, question) {
2007
+ const fam = /\b([RDMB])\s?-?(\d{2,3})\b/i.exec(question);
2008
+ return [np.manufacturer, np.model, fam ? `${fam[1]} ${fam[2]}` : null].filter(Boolean).join(" ");
2009
+ }
2010
+
2011
+ // workers/worker_public/src/nameplate.ts
2012
+ async function extractNameplate(ai, model, image) {
2013
+ try {
2014
+ const res = await ai.run(model, {
2015
+ messages: [
2016
+ { role: "system", content: nameplate_default.trimEnd() },
2017
+ { role: "user", content: [{ type: "image_url", image_url: { url: image } }] }
2018
+ ],
2019
+ max_tokens: 512,
2020
+ reasoning_effort: "low"
2021
+ });
2022
+ const text = typeof res?.response === "string" ? res.response : res?.choices?.[0]?.message?.content;
2023
+ return parseNameplate(text ?? "");
2024
+ } catch {
2025
+ return null;
2026
+ }
2027
+ }
2028
+
1982
2029
  // workers/worker_public/src/ask.ts
1983
2030
  function userImageDataUrl(body) {
1984
2031
  const img = body?.image;
@@ -2332,7 +2379,17 @@ async function handleAsk(env, ctx, req, tier, key) {
2332
2379
  console.log("understand:", understanding?.intent ?? "null", understanding?.doc_number ? `doc#${understanding.doc_number}${understanding.edition ? "@" + understanding.edition : ""}` : "nodoc", "|", q.query.slice(0, 50));
2333
2380
  const graphDocNumbers = await graphExpand(env, understanding);
2334
2381
  const eNote = await editionNote(env, understanding);
2335
- const register = await searchRegister(env.DB, q.query);
2382
+ let register = await searchRegister(env.DB, q.query);
2383
+ if (withImage && !register?.rows?.length && /certificate|certified|certification/i.test(q.query)) {
2384
+ const np = await extractNameplate(env.AI, MODELS.member, withImage);
2385
+ if (np?.manufacturer) {
2386
+ const bridged = await searchRegister(env.DB, nameplateRegisterQuery(np, q.query));
2387
+ if (bridged?.rows?.length) {
2388
+ register = bridged;
2389
+ console.log("nameplate-bridge:", np.manufacturer, np.model ?? "", "\u2192", bridged.rows.length, "register rows");
2390
+ }
2391
+ }
2392
+ }
2336
2393
  console.log("register-search:", JSON.stringify({ shaped: !!register, tokens: register?.tokens ?? null, rows: register?.rows?.length ?? null, first: register?.rows?.[0]?.num ?? null }));
2337
2394
  const regNote = register ? registerNote(register.rows) : void 0;
2338
2395
  if (understanding?.intent === "conversational") {
@@ -0,0 +1,8 @@
1
+ export interface Nameplate {
2
+ manufacturer: string | null;
3
+ model: string | null;
4
+ }
5
+ export declare function parseNameplate(text: string): Nameplate | null;
6
+ /** The register query the extraction feeds: the nameplate's tokens lead,
7
+ * the question's family (if named) scopes. */
8
+ export declare function nameplateRegisterQuery(np: Nameplate, question: string): string;
@@ -0,0 +1,5 @@
1
+ import { nameplateRegisterQuery, type Nameplate } from "./nameplate-parse.ts";
2
+ export type { Nameplate };
3
+ export { nameplateRegisterQuery };
4
+ export declare function extractNameplate(ai: any, model: string, image: string): Promise<Nameplate | null>;
5
+ export declare const nameplateModel: () => "@cf/zai-org/glm-5.3-flash";
@@ -0,0 +1,8 @@
1
+ You read instrument nameplates. From the photograph, extract the
2
+ manufacturer's name exactly as printed and the model designation exactly
3
+ as printed. Answer with ONLY a JSON object on one line:
4
+
5
+ {"manufacturer": "…", "model": "…"}
6
+
7
+ If the photograph shows no manufacturer or no model, use null for the
8
+ missing value. Never guess beyond what is printed.
@@ -9,7 +9,7 @@ import {
9
9
  modelGroundingBlock,
10
10
  scoreFaithfulness,
11
11
  scoreJudge
12
- } from "../../chunk-DKQXJN6G.js";
12
+ } from "../../chunk-ZVMQHCNL.js";
13
13
  import {
14
14
  buildMessages,
15
15
  cfBlobs,
@@ -958,7 +958,7 @@ async function handleMcp(env, ctx, req, tier, key) {
958
958
  // stream:false forces the JSON lane (anon defaults to SSE)
959
959
  body: JSON.stringify({ ...args, stream: false })
960
960
  });
961
- const res = name === "ask" ? await (await import("../../ask-PJBGJIXF.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-PBOODCMQ.js")).handleSearch(env, ctx, inner, tier, key);
961
+ const res = name === "ask" ? await (await import("../../ask-5XMD22SJ.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-PBOODCMQ.js")).handleSearch(env, ctx, inner, tier, key);
962
962
  return res.json().catch(() => ({ error: { message: "tool transport failed", status: res.status } }));
963
963
  });
964
964
  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.55",
30
+ "version": "0.2.56",
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",
@@ -0,0 +1,8 @@
1
+ You read instrument nameplates. From the photograph, extract the
2
+ manufacturer's name exactly as printed and the model designation exactly
3
+ as printed. Answer with ONLY a JSON object on one line:
4
+
5
+ {"manufacturer": "…", "model": "…"}
6
+
7
+ If the photograph shows no manufacturer or no model, use null for the
8
+ missing value. Never guess beyond what is printed.
@@ -40,6 +40,7 @@ import { json, err, corsHeaders, readJson, validateQuery, type ApiKey } from "./
40
40
  import { clientIp, checkQuota, telemetry } from "./quota";
41
41
  import { graphExpand, editionNote } from "./graph";
42
42
  import { registerNote, searchRegister } from "./certificates";
43
+ import { extractNameplate, nameplateRegisterQuery } from "./nameplate.ts";
43
44
  import { P } from "./profile.ts";
44
45
 
45
46
  /** User-uploaded image for multimodal questions: a data URL
@@ -619,7 +620,21 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
619
620
  // register-shaped question queries the D1 table directly and the rows
620
621
  // join the prompt as an authoritative note — exact matches, never a
621
622
  // similarity guess about certification standing
622
- const register = await searchRegister(env.DB, q.query);
623
+ let register = await searchRegister(env.DB, q.query);
624
+ // the nameplate bridge (TODO.new-era/6's image lane): a certificate
625
+ // question WITH a photograph carries its manufacturer in the pixels —
626
+ // one vision call extracts it, and those tokens re-query the register
627
+ // so the answer cites the actual certificate rows
628
+ if (withImage && !register?.rows?.length && /certificate|certified|certification/i.test(q.query)) {
629
+ const np = await extractNameplate((env as any).AI, MODELS.member, withImage);
630
+ if (np?.manufacturer) {
631
+ const bridged = await searchRegister(env.DB, nameplateRegisterQuery(np, q.query));
632
+ if (bridged?.rows?.length) {
633
+ register = bridged;
634
+ console.log("nameplate-bridge:", np.manufacturer, np.model ?? "", "→", bridged.rows.length, "register rows");
635
+ }
636
+ }
637
+ }
623
638
  console.log("register-search:", JSON.stringify({ shaped: !!register, tokens: register?.tokens ?? null, rows: register?.rows?.length ?? null, first: register?.rows?.[0]?.num ?? null }));
624
639
  const regNote = register ? registerNote(register.rows) : undefined;
625
640
 
@@ -22,7 +22,7 @@ export interface RegisterRow {
22
22
  * trigger the citation-graph notes already use (query-shaped notes,
23
23
  * never a router). */
24
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);
25
+ 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);
26
26
  }
27
27
 
28
28
  /** Search tokens: words that can identify a holder or a model — words of
@@ -0,0 +1,29 @@
1
+ // The nameplate bridge's pure half (TODO.new-era/6's image lane): parse
2
+ // the vision call's answer into {manufacturer, model}. Lives apart from
3
+ // the AI-call module so the plain-node suites can exercise it (the
4
+ // prompt is a bundled .md, which only esbuild can load).
5
+
6
+ export interface Nameplate {
7
+ manufacturer: string | null;
8
+ model: string | null;
9
+ }
10
+
11
+ export function parseNameplate(text: string): Nameplate | null {
12
+ const m = /\{[^{}]*\}/.exec(String(text ?? ""));
13
+ if (!m) return null;
14
+ try {
15
+ const j = JSON.parse(m[0]) as { manufacturer?: unknown; model?: unknown };
16
+ const clean = (v: unknown) => (typeof v === "string" && v.trim() ? v.trim().slice(0, 80) : null);
17
+ const out = { manufacturer: clean(j.manufacturer), model: clean(j.model) };
18
+ return out.manufacturer || out.model ? out : null;
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ /** The register query the extraction feeds: the nameplate's tokens lead,
25
+ * the question's family (if named) scopes. */
26
+ export function nameplateRegisterQuery(np: Nameplate, question: string): string {
27
+ const fam = /\b([RDMB])\s?-?(\d{2,3})\b/i.exec(question);
28
+ return [np.manufacturer, np.model, fam ? `${fam[1]} ${fam[2]}` : null].filter(Boolean).join(" ");
29
+ }
@@ -0,0 +1,33 @@
1
+ // The nameplate bridge (TODO.new-era/6, the image lane): a certificate
2
+ // question that arrives WITH a photograph carries its manufacturer only
3
+ // in the pixels. A single low-effort vision call extracts the printed
4
+ // manufacturer and model, and those tokens feed the certificate
5
+ // register's D1 lane — so "does this load cell's manufacturer hold the
6
+ // certificate" answers with the actual register rows instead of advice.
7
+ // The shape rules are the 2026-09-09 multimodal findings: the image
8
+ // rides its OWN short user message, never a long passage.
9
+ import nameplatePrompt from "../prompts/nameplate.md";
10
+ import { parseNameplate, nameplateRegisterQuery, type Nameplate } from "./nameplate-parse.ts";
11
+ import { MODELS } from "./config.ts";
12
+
13
+ export type { Nameplate };
14
+ export { nameplateRegisterQuery };
15
+
16
+ export async function extractNameplate(ai: any, model: string, image: string): Promise<Nameplate | null> {
17
+ try {
18
+ const res: any = await ai.run(model, {
19
+ messages: [
20
+ { role: "system", content: nameplatePrompt.trimEnd() },
21
+ { role: "user", content: [{ type: "image_url", image_url: { url: image } }] },
22
+ ],
23
+ max_tokens: 512,
24
+ reasoning_effort: "low",
25
+ });
26
+ const text = typeof res?.response === "string" ? res.response : res?.choices?.[0]?.message?.content;
27
+ return parseNameplate(text ?? "");
28
+ } catch {
29
+ return null; // the bridge is optional — the ask never depends on it
30
+ }
31
+ }
32
+
33
+ export const nameplateModel = () => MODELS.member;