@konneal/engine 0.2.54 → 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-OPJXHEZU.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;
@@ -2222,20 +2269,22 @@ async function handleAsk(env, ctx, req, tier, key) {
2222
2269
  const warmQuery = retrievalQuery(q.query, prev);
2223
2270
  const warmEmbed = embedWarm(env, warmQuery);
2224
2271
  const conversationId = typeof body?.conversation_id === "string" && /^[A-Za-z0-9_-]{8,64}$/.test(body.conversation_id) ? body.conversation_id : null;
2225
- if (!effectiveImage && !attachIds.length && conversationId && env.DB && member) {
2226
- const prior = await env.DB.prepare(
2227
- "SELECT m.attachment_id AS aid, a.mime, a.r2_key FROM messages m JOIN attachments a ON a.id = m.attachment_id WHERE m.conversation_id = ?1 AND m.role = 'user' AND m.attachment_id IS NOT NULL ORDER BY m.created_at DESC LIMIT 1"
2228
- ).bind(conversationId, member.sub).first();
2229
- if (prior) {
2230
- try {
2272
+ try {
2273
+ if (!effectiveImage && !attachIds.length && conversationId && env.DB && member) {
2274
+ const prior = await env.DB.prepare(
2275
+ "SELECT m.attachment_id AS aid, a.mime, a.r2_key FROM messages m JOIN attachments a ON a.id = m.attachment_id JOIN conversations c ON c.id = m.conversation_id AND c.sub = ?2 WHERE m.conversation_id = ?1 AND m.role = 'user' AND m.attachment_id IS NOT NULL ORDER BY m.created_at DESC LIMIT 1"
2276
+ ).bind(conversationId, member.sub).first();
2277
+ if (prior) {
2231
2278
  const obj = await env.CHAT_UPLOADS.get(prior.r2_key);
2232
2279
  if (obj) {
2233
- const b64 = btoa(String.fromCharCode(...new Uint8Array(await obj.arrayBuffer())));
2234
- storedImage = `data:${prior.mime};base64,${b64}`;
2280
+ const bytes = new Uint8Array(await obj.arrayBuffer());
2281
+ let bin = "";
2282
+ for (let i = 0; i < bytes.length; i += 32768) bin += String.fromCharCode(...bytes.subarray(i, i + 32768));
2283
+ storedImage = `data:${prior.mime};base64,${btoa(bin)}`;
2235
2284
  }
2236
- } catch {
2237
2285
  }
2238
2286
  }
2287
+ } catch {
2239
2288
  }
2240
2289
  let convEntities = [];
2241
2290
  if (conversationId) {
@@ -2330,7 +2379,17 @@ async function handleAsk(env, ctx, req, tier, key) {
2330
2379
  console.log("understand:", understanding?.intent ?? "null", understanding?.doc_number ? `doc#${understanding.doc_number}${understanding.edition ? "@" + understanding.edition : ""}` : "nodoc", "|", q.query.slice(0, 50));
2331
2380
  const graphDocNumbers = await graphExpand(env, understanding);
2332
2381
  const eNote = await editionNote(env, understanding);
2333
- 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
+ }
2334
2393
  console.log("register-search:", JSON.stringify({ shaped: !!register, tokens: register?.tokens ?? null, rows: register?.rows?.length ?? null, first: register?.rows?.[0]?.num ?? null }));
2335
2394
  const regNote = register ? registerNote(register.rows) : void 0;
2336
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-OPJXHEZU.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-3TAC56RC.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.54",
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
@@ -452,24 +453,30 @@ async function handleAsk(
452
453
  const conversationId = typeof body?.conversation_id === "string" && /^[A-Za-z0-9_-]{8,64}$/.test(body.conversation_id) ? body.conversation_id : null;
453
454
 
454
455
  // Tier-2 continuation: no new image, but the conversation carries one —
455
- // its most recent stored image re-attaches to this question
456
- if (!effectiveImage && !attachIds.length && conversationId && env.DB && member) {
457
- const prior = await env.DB.prepare(
458
- "SELECT m.attachment_id AS aid, a.mime, a.r2_key FROM messages m JOIN attachments a ON a.id = m.attachment_id WHERE m.conversation_id = ?1 AND m.role = 'user' AND m.attachment_id IS NOT NULL ORDER BY m.created_at DESC LIMIT 1",
459
- ).bind(conversationId, member.sub).first<{ aid: string; mime: string; r2_key: string }>();
460
- if (prior) {
461
- try {
462
- const obj = await (env as any).CHAT_UPLOADS.get(prior.r2_key);
463
- if (obj) {
464
- const b64 = btoa(String.fromCharCode(...new Uint8Array(await obj.arrayBuffer())));
465
- storedImage = `data:${prior.mime};base64,${b64}`;
466
- }
467
- } catch {
468
- /* the object's gone */
456
+ // its most recent stored image re-attaches to this question. The query
457
+ // scopes by the CONVERSATION'S OWNER (the messages table carries no
458
+ // sub; ownership rides the conversation row) and the whole resolution
459
+ // fails open: an optional feature never takes the ask down.
460
+ try {
461
+ if (!effectiveImage && !attachIds.length && conversationId && env.DB && member) {
462
+ const prior = await env.DB.prepare(
463
+ "SELECT m.attachment_id AS aid, a.mime, a.r2_key FROM messages m JOIN attachments a ON a.id = m.attachment_id JOIN conversations c ON c.id = m.conversation_id AND c.sub = ?2 WHERE m.conversation_id = ?1 AND m.role = 'user' AND m.attachment_id IS NOT NULL ORDER BY m.created_at DESC LIMIT 1",
464
+ ).bind(conversationId, member.sub).first<{ aid: string; mime: string; r2_key: string }>();
465
+ if (prior) {
466
+ const obj = await (env as any).CHAT_UPLOADS.get(prior.r2_key);
467
+ if (obj) {
468
+ const bytes = new Uint8Array(await obj.arrayBuffer());
469
+ let bin = "";
470
+ for (let i = 0; i < bytes.length; i += 0x8000) bin += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
471
+ storedImage = `data:${prior.mime};base64,${btoa(bin)}`;
472
+ }
469
473
  }
470
474
  }
475
+ } catch {
476
+ /* the reuse path is optional — the ask answers without the image */
471
477
  }
472
- let convEntities: Array<{ entity: string; kind: string }> = [];
478
+
479
+ let convEntities: Array<{ entity: string; kind: string }> = [];
473
480
  if (conversationId) {
474
481
  try {
475
482
  const rows = await env.DB.prepare("SELECT entity, kind FROM conversation_entities WHERE conversation_id = ?1 LIMIT 12").bind(conversationId).all();
@@ -613,7 +620,21 @@ async function handleAsk(
613
620
  // register-shaped question queries the D1 table directly and the rows
614
621
  // join the prompt as an authoritative note — exact matches, never a
615
622
  // similarity guess about certification standing
616
- 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
+ }
617
638
  console.log("register-search:", JSON.stringify({ shaped: !!register, tokens: register?.tokens ?? null, rows: register?.rows?.length ?? null, first: register?.rows?.[0]?.num ?? null }));
618
639
  const regNote = register ? registerNote(register.rows) : undefined;
619
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;