@konneal/engine 0.2.153 → 0.2.157

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.
@@ -0,0 +1,4 @@
1
+ export declare function toolNarration(t: string): boolean;
2
+ export declare const TOOL_NARRATION_NOTICE = "Correction notice: your draft narrated an internal tool call ('the \u2026 tool returned \u2026') instead of answering. Rewrite: answer the question directly from the retrieved passages, cite the clauses, and never mention tools, queries, wire formats, or JSON.";
3
+ export declare function intentPreamble(t: string): boolean;
4
+ export declare const INTENT_PREAMBLE_NOTICE = "Correction notice: your draft announced a plan ('I'll look up \u2026') instead of answering. Do not narrate intent \u2014 answer the question NOW from the retrieved passages: give the values, clause references and citations the question asks for.";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  handleAsk
3
- } from "./chunk-I6HU3EM6.js";
3
+ } from "./chunk-S3O7QZQ7.js";
4
4
  import "./chunk-ECOYO7EX.js";
5
5
  import "./chunk-VYEMFSU5.js";
6
6
  import "./chunk-G2RE4XRV.js";
@@ -1637,7 +1637,7 @@ async function extractNameplate(ai, model, image) {
1637
1637
  return null;
1638
1638
  }
1639
1639
 
1640
- // workers/worker_public/src/ask.ts
1640
+ // workers/worker_public/src/answerGuards.ts
1641
1641
  function toolNarration(t) {
1642
1642
  return /^(?:the\s+)?[a-z][a-z_.]*\.[a-z_]+ tool returned\b/i.test(t.slice(0, 200)) || /tool returned,? for the query\b/i.test(t.slice(0, 300));
1643
1643
  }
@@ -1649,6 +1649,8 @@ function intentPreamble(t) {
1649
1649
  return !/\[\[u:|\[\^?\d+\]|\b\d+\.\d+\b/.test(s);
1650
1650
  }
1651
1651
  var INTENT_PREAMBLE_NOTICE = "Correction notice: your draft announced a plan ('I'll look up \u2026') instead of answering. Do not narrate intent \u2014 answer the question NOW from the retrieved passages: give the values, clause references and citations the question asks for.";
1652
+
1653
+ // workers/worker_public/src/ask.ts
1652
1654
  function userImageDataUrl(body) {
1653
1655
  const img = body?.image;
1654
1656
  if (img == null) return null;
@@ -2912,7 +2914,25 @@ Answer account questions from these records ONLY: name the record when you use i
2912
2914
  ...h.metadata.table_selection ? { sel: h.metadata.table_selection } : {}
2913
2915
  }));
2914
2916
  const tq = await chargeAnswerTokens(env, bucketId, void 0, messageChars(messages), answer.length, probeTokensUsed);
2915
- return json({ ...out, context: contextOut, read: readAs(), quota: { ...quota, ...tq, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview } }, 200, { ...corsHeaders(req), "server-timing": serverTiming() });
2917
+ const manifestOut = body?.manifest === true ? { manifest: {
2918
+ index_version: env.INDEX_VERSION,
2919
+ corpus_gen: gen,
2920
+ model,
2921
+ effort: answerEffort(env),
2922
+ fresh,
2923
+ citations: used.length,
2924
+ retrieval: used.map((h, i) => ({
2925
+ rank: i + 1,
2926
+ doc_id: h.metadata.doc_id,
2927
+ clause_anchor: h.metadata.clause_anchor ?? null,
2928
+ score: h.score,
2929
+ rerank_score: h.rerank_score ?? null
2930
+ })),
2931
+ stages: stageTiming,
2932
+ generate_retries: generateRetries,
2933
+ total_ms: Date.now() - tStart
2934
+ } } : {};
2935
+ return json({ ...out, context: contextOut, read: readAs(), ...manifestOut, quota: { ...quota, ...tq, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview } }, 200, { ...corsHeaders(req), "server-timing": serverTiming() });
2916
2936
  };
2917
2937
  if (liveResponse) {
2918
2938
  void pipeline(live, q).catch(() => {
@@ -0,0 +1,29 @@
1
+ /** The model's raw response → the situating context: trimmed, the
2
+ * wrapping quotes stripped, capped at 400 chars. Empty string = the
3
+ * model had nothing to say (the deterministic-empty class). */
4
+ export declare function normalizeContext(raw: unknown): string;
5
+ export type EnrichOutcome = {
6
+ action: "serve";
7
+ context: string;
8
+ cached: boolean;
9
+ } | {
10
+ action: "serve-raw";
11
+ } | {
12
+ action: "fail";
13
+ error: string;
14
+ };
15
+ /** The per-chunk decision, given the cache read and the generation.
16
+ * - a context (cached or generated) serves enriched;
17
+ * - an EMPTY generation in the production flow serves the chunk RAW
18
+ * (the deterministic-empty class, 2026-10-20: real corpus content —
19
+ * B 18's thin terms sections, two R 134-1 clauses — must not vanish
20
+ * from the index because its enrichment has nothing to say; a later
21
+ * forced re-enrich overwrites the vector in place);
22
+ * - an empty generation in mode:context / mode:ab FAILS: those
23
+ * builders want the signal, not a side-effecting write. */
24
+ export declare function enrichOutcome(opts: {
25
+ cached: string | null;
26
+ generated: string;
27
+ contextOnly: boolean;
28
+ abMode: boolean;
29
+ }): EnrichOutcome;
@@ -4,7 +4,7 @@ import {
4
4
  handleMemories,
5
5
  scoreFaithfulness,
6
6
  scoreJudge
7
- } from "../../chunk-I6HU3EM6.js";
7
+ } from "../../chunk-S3O7QZQ7.js";
8
8
  import "../../chunk-ECOYO7EX.js";
9
9
  import {
10
10
  TOOLS_REGISTRY,
@@ -351,6 +351,18 @@ async function handleGetShared(env, slug) {
351
351
  // workers/worker_public/prompts/enrichment.md
352
352
  var enrichment_default = "You write a retrieval context for a passage from {{CORPUS_KIND}}. The context is prepended to the passage before embedding so a semantic search can locate the passage even when the query uses different vocabulary than the passage itself.\n\nWrite ONE concise sentence (at most 40 words) that situates the passage: name the publication by its exact {{PUBLISHER_NAME}} identifier (including part or annex when applicable) and what the passage covers \u2014 paraphrasing the topic in words DIFFERENT from the passage's own. Do not copy the passage verbatim, do not add facts that are not derivable from the passage or its header, do not answer or explain the content. Reply with the context sentence only \u2014 no quotes, no preamble.\n";
353
353
 
354
+ // workers/worker_public/src/enrichPolicy.ts
355
+ function normalizeContext(raw) {
356
+ return typeof raw === "string" ? raw.trim().replace(/^["\']|["']$/g, "").slice(0, 400) : "";
357
+ }
358
+ function enrichOutcome(opts) {
359
+ const fromCache = typeof opts.cached === "string" && opts.cached;
360
+ if (fromCache) return { action: "serve", context: fromCache, cached: true };
361
+ if (opts.generated) return { action: "serve", context: opts.generated, cached: false };
362
+ if (opts.contextOnly || opts.abMode) return { action: "fail", error: "empty enrichment" };
363
+ return { action: "serve-raw" };
364
+ }
365
+
354
366
  // workers/worker_public/prompts/doc-summary.md
355
367
  var doc_summary_default = "You write the document-level summary for a legal-metrology publication in {{PUBLISHER_NAME}}'s retrieval index. The section summaries below cover the publication clause by clause. Write one compact abstract, three to five sentences: what the publication regulates or provides, its scope and applicability, and the structure a reader should know (parts, annexes, where the normative core sits). Use the publication's own terms exactly. No preamble, no headings.\n";
356
368
 
@@ -408,10 +420,15 @@ ${c.text.slice(0, 1500)}` }
408
420
  reasoning_effort: abMode ? effort : "high"
409
421
  });
410
422
  const raw = typeof res?.response === "string" && res.response.trim() ? res.response : res?.choices?.[0]?.message?.content;
411
- context = typeof raw === "string" ? raw.trim().replace(/^["\']|[\"']$/g, "").slice(0, 400) : "";
423
+ context = normalizeContext(raw);
412
424
  if (!context) {
413
425
  console.log("enrich raw keys:", Object.keys(res ?? {}), "sample:", JSON.stringify(res).slice(0, 300));
414
- return { id: c.id, ok: false, error: "empty enrichment" };
426
+ const outcome = enrichOutcome({ cached: null, generated: "", contextOnly, abMode });
427
+ if (outcome.action === "fail") return { id: c.id, ok: false, error: outcome.error };
428
+ const rawText = typeof c.metadata.chunk_text === "string" && c.metadata.chunk_text ? c.metadata.chunk_text : c.text;
429
+ const rawVector = await embed(portModelRunner(env), MODELS.embed, rawText.slice(0, 6e3));
430
+ await env.VECTORIZE.upsert([{ id: c.id, values: rawVector, metadata: { ...c.metadata, chunk_text: rawText } }]);
431
+ return { id: c.id, ok: true, raw: true, cached: false, context: "" };
415
432
  }
416
433
  if (res?.usage) {
417
434
  usage.prompt_tokens += Number(res.usage.prompt_tokens ?? 0);
@@ -1107,7 +1124,7 @@ async function callToolOnce(env, ctx, _req, tier, key, name, args, t0) {
1107
1124
  const r = await runTool({ ...env, DB: env.DB }, { name, args }, "mcp");
1108
1125
  payload = r ? { tool: r.name, query: r.query, result: r.output } : { error: { message: `tool ${name} returned nothing for the given arguments` } };
1109
1126
  } else {
1110
- const res = name === "ask" ? await (await import("../../ask-UIIE5LD2.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-GAU5SEZI.js")).handleSearch(env, ctx, inner, tier, key);
1127
+ const res = name === "ask" ? await (await import("../../ask-IC5A5LIN.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-GAU5SEZI.js")).handleSearch(env, ctx, inner, tier, key);
1111
1128
  payload = await res.json().catch(() => ({ error: { message: "tool transport failed", status: res.status } }));
1112
1129
  }
1113
1130
  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.153",
30
+ "version": "0.2.157",
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",
@@ -6,6 +6,7 @@ import { embed } from "./ai";
6
6
  import { err, json, corsHeaders, readJson, authenticate } from "./lib/http";
7
7
  import type { Env } from "./env";
8
8
  import enrichmentPrompt from "../prompts/enrichment.md";
9
+ import { normalizeContext, enrichOutcome } from "./enrichPolicy.ts";
9
10
  import docSummaryPrompt from "../prompts/doc-summary.md";
10
11
  import familySummaryPrompt from "../prompts/family-summary.md";
11
12
  import sectionSummaryPrompt from "../prompts/section-summary.md";
@@ -75,10 +76,18 @@ export async function handleEnrich(env: Env, ctx: ExecutionContext, req: Request
75
76
  const raw = typeof res?.response === "string" && res.response.trim()
76
77
  ? res.response
77
78
  : res?.choices?.[0]?.message?.content;
78
- context = typeof raw === "string" ? raw.trim().replace(/^["\']|[\"']$/g, "").slice(0, 400) : "";
79
+ context = normalizeContext(raw);
79
80
  if (!context) {
80
81
  console.log("enrich raw keys:", Object.keys(res ?? {}), "sample:", JSON.stringify(res).slice(0, 300));
81
- return { id: c.id, ok: false, error: "empty enrichment" };
82
+ // serve-raw: the chunk's own text is real corpus content —
83
+ // embed it without the context band, upsert without the ctx
84
+ // marker; a later forced re-enrich overwrites in place.
85
+ const outcome = enrichOutcome({ cached: null, generated: "", contextOnly, abMode });
86
+ if (outcome.action === "fail") return { id: c.id, ok: false, error: outcome.error };
87
+ const rawText = typeof c.metadata.chunk_text === "string" && c.metadata.chunk_text ? c.metadata.chunk_text : c.text;
88
+ const rawVector = await embed(portModelRunner(env), MODELS.embed, rawText.slice(0, 6000));
89
+ await env.VECTORIZE.upsert([{ id: c.id, values: rawVector, metadata: { ...c.metadata, chunk_text: rawText } }]);
90
+ return { id: c.id, ok: true, raw: true, cached: false, context: "" };
82
91
  }
83
92
  if (res?.usage) {
84
93
  usage.prompt_tokens += Number(res.usage.prompt_tokens ?? 0);
@@ -0,0 +1,36 @@
1
+ // The answer-contract's text guards, dependency-free so plain node can
2
+ // load them for unit tests (ask.ts imports .md prompts that only the
3
+ // bundler resolves — the guards were module-local there and rode e2e
4
+ // alone). Both lanes share them: the JSON lane's contract retry and the
5
+ // streamed lane's 80-char head ruling.
6
+
7
+ // The tool-narration leak (2026-10-14, the R 134-1 probe): the answer
8
+ // model sometimes NARRATES the agent loop's call in prose — 'The
9
+ // db.find tool returned, for the query "…": {…json…}' — instead of
10
+ // answering. The TOOL-line scrub cannot catch prose. The JSON lane
11
+ // regenerates through the contract retry; the streamed lane buffers
12
+ // the head and sends the retry sentinel (the blind-answer guard's
13
+ // client contract) before streaming the fresh answer. One bounded
14
+ // restart per lane.
15
+ export function toolNarration(t: string): boolean {
16
+ return /^(?:the\s+)?[a-z][a-z_.]*\.[a-z_]+ tool returned\b/i.test(t.slice(0, 200)) ||
17
+ /tool returned,? for the query\b/i.test(t.slice(0, 300));
18
+ }
19
+ export const TOOL_NARRATION_NOTICE =
20
+ "Correction notice: your draft narrated an internal tool call ('the … tool returned …') instead of answering. Rewrite: answer the question directly from the retrieved passages, cite the clauses, and never mention tools, queries, wire formats, or JSON.";
21
+
22
+ // The intent preamble (2026-10-19, the ~200-word technical-file
23
+ // question): the draft is a PLAN, not an answer — "I'll look up the R
24
+ // 76-1 MPE clause and classification details in the corpus." — shipped
25
+ // while ten citations rode the same response. Narrow by construction:
26
+ // it must OPEN with a first-person intent verb AND carry no substance —
27
+ // short, no clause citation, no normative numbers (a real answer's
28
+ // opening names values or clauses).
29
+ export function intentPreamble(t: string): boolean {
30
+ const s = (t ?? "").trim();
31
+ if (s.length > 240) return false;
32
+ if (!/^(?:i'll|i will|let me|let's|i'm going to|i am going to)\b/i.test(s)) return false;
33
+ return !/\[\[u:|\[\^?\d+\]|\b\d+\.\d+\b/.test(s);
34
+ }
35
+ export const INTENT_PREAMBLE_NOTICE =
36
+ "Correction notice: your draft announced a plan ('I'll look up …') instead of answering. Do not narrate intent — answer the question NOW from the retrieved passages: give the values, clause references and citations the question asks for.";
@@ -49,37 +49,8 @@ import { TOOL_DECLARATION, parseToolCall, runTool, toolNote } from "./tools";
49
49
  import { operationsLane } from "./operations.ts";
50
50
  import { extractNameplate, nameplateRegisterQuery } from "./nameplate.ts";
51
51
  import { P } from "./profile.ts";
52
+ import { toolNarration, intentPreamble, TOOL_NARRATION_NOTICE, INTENT_PREAMBLE_NOTICE } from "./answerGuards.ts";
52
53
 
53
- // the tool-narration leak (2026-10-14, the R 134-1 probe): the answer
54
- // model sometimes NARRATES the agent loop's call in prose — 'The
55
- // db.find tool returned, for the query "…": {…json…}' — instead of
56
- // answering. The TOOL-line scrub cannot catch prose. The JSON lane
57
- // regenerates through the contract retry; the streamed lane buffers
58
- // the head and sends the retry sentinel (the blind-answer guard's
59
- // client contract) before streaming the fresh answer. One bounded
60
- // restart per lane.
61
- function toolNarration(t: string): boolean {
62
- return /^(?:the\s+)?[a-z][a-z_.]*\.[a-z_]+ tool returned\b/i.test(t.slice(0, 200)) ||
63
- /tool returned,? for the query\b/i.test(t.slice(0, 300));
64
- }
65
- const TOOL_NARRATION_NOTICE =
66
- "Correction notice: your draft narrated an internal tool call ('the … tool returned …') instead of answering. Rewrite: answer the question directly from the retrieved passages, cite the clauses, and never mention tools, queries, wire formats, or JSON.";
67
-
68
- // The intent preamble (2026-10-19, the ~200-word technical-file
69
- // question): the draft is a PLAN, not an answer — "I'll look up the R
70
- // 76-1 MPE clause and classification details in the corpus." — shipped
71
- // while ten citations rode the same response. Narrow by construction:
72
- // it must OPEN with a first-person intent verb AND carry no substance —
73
- // short, no clause citation, no normative numbers (a real answer's
74
- // opening names values or clauses).
75
- function intentPreamble(t: string): boolean {
76
- const s = (t ?? "").trim();
77
- if (s.length > 240) return false;
78
- if (!/^(?:i'll|i will|let me|let's|i'm going to|i am going to)\b/i.test(s)) return false;
79
- return !/\[\[u:|\[\^?\d+\]|\b\d+\.\d+\b/.test(s);
80
- }
81
- const INTENT_PREAMBLE_NOTICE =
82
- "Correction notice: your draft announced a plan ('I'll look up …') instead of answering. Do not narrate intent — answer the question NOW from the retrieved passages: give the values, clause references and citations the question asks for.";
83
54
 
84
55
  /** User-uploaded image for multimodal questions: a data URL
85
56
  * (data:image/(png|jpeg|webp|gif);base64,…) up to 6 MB of payload. The
@@ -1998,7 +1969,33 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1998
1969
  ...((h.metadata as any).table_selection ? { sel: (h.metadata as any).table_selection } : {}),
1999
1970
  }));
2000
1971
  const tq = await chargeAnswerTokens(env, bucketId, undefined, messageChars(messages!), answer!.length, probeTokensUsed);
2001
- return json({ ...out, context: contextOut, read: readAs(), quota: { ...quota, ...tq, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview } }, 200, { ...corsHeaders(req), "server-timing": serverTiming() });
1972
+ // the reproducibility manifest (TODO.sota/14.6, v1): an opt-in, one-object
1973
+ // derivation record — the served snapshot identity, the answer model, the
1974
+ // retrieval anatomy (rank, corpus score, rerank score per used passage),
1975
+ // the stage timings, and the enforcement counters. Everything here is
1976
+ // already computed; the flag only ASSEMBLES it for the harness and for
1977
+ // external auditors. v2 names the CRAG grades per passage.
1978
+ const manifestOut = body?.manifest === true
1979
+ ? { manifest: {
1980
+ index_version: env.INDEX_VERSION,
1981
+ corpus_gen: gen,
1982
+ model,
1983
+ effort: answerEffort(env),
1984
+ fresh,
1985
+ citations: used.length,
1986
+ retrieval: used.map((h: Hit, i: number) => ({
1987
+ rank: i + 1,
1988
+ doc_id: h.metadata.doc_id,
1989
+ clause_anchor: h.metadata.clause_anchor ?? null,
1990
+ score: h.score,
1991
+ rerank_score: h.rerank_score ?? null,
1992
+ })),
1993
+ stages: stageTiming,
1994
+ generate_retries: generateRetries,
1995
+ total_ms: Date.now() - tStart,
1996
+ } }
1997
+ : {};
1998
+ return json({ ...out, context: contextOut, read: readAs(), ...manifestOut, quota: { ...quota, ...tq, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview } }, 200, { ...corsHeaders(req), "server-timing": serverTiming() });
2002
1999
  }
2003
2000
 
2004
2001
  // THE DISPATCH: the streamed lane's response went out at the top — the
@@ -0,0 +1,40 @@
1
+ // The enrichment route's decision policy, dependency-free so plain node
2
+ // can load it for unit tests (admin.ts imports .md prompts that only
3
+ // the bundler resolves — the serve-raw policy rode the live
4
+ // verification alone until this extraction). The handler keeps the
5
+ // I/O: cache, generation, embedding, upsert; this module owns the
6
+ // DECISION table — what a chunk's outcome is, given what came back.
7
+
8
+ /** The model's raw response → the situating context: trimmed, the
9
+ * wrapping quotes stripped, capped at 400 chars. Empty string = the
10
+ * model had nothing to say (the deterministic-empty class). */
11
+ export function normalizeContext(raw: unknown): string {
12
+ return typeof raw === "string" ? raw.trim().replace(/^["\']|["']$/g, "").slice(0, 400) : "";
13
+ }
14
+
15
+ export type EnrichOutcome =
16
+ | { action: "serve"; context: string; cached: boolean }
17
+ | { action: "serve-raw" }
18
+ | { action: "fail"; error: string };
19
+
20
+ /** The per-chunk decision, given the cache read and the generation.
21
+ * - a context (cached or generated) serves enriched;
22
+ * - an EMPTY generation in the production flow serves the chunk RAW
23
+ * (the deterministic-empty class, 2026-10-20: real corpus content —
24
+ * B 18's thin terms sections, two R 134-1 clauses — must not vanish
25
+ * from the index because its enrichment has nothing to say; a later
26
+ * forced re-enrich overwrites the vector in place);
27
+ * - an empty generation in mode:context / mode:ab FAILS: those
28
+ * builders want the signal, not a side-effecting write. */
29
+ export function enrichOutcome(opts: {
30
+ cached: string | null;
31
+ generated: string;
32
+ contextOnly: boolean;
33
+ abMode: boolean;
34
+ }): EnrichOutcome {
35
+ const fromCache = typeof opts.cached === "string" && opts.cached;
36
+ if (fromCache) return { action: "serve", context: fromCache, cached: true };
37
+ if (opts.generated) return { action: "serve", context: opts.generated, cached: false };
38
+ if (opts.contextOnly || opts.abMode) return { action: "fail", error: "empty enrichment" };
39
+ return { action: "serve-raw" };
40
+ }