@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.
- package/dist/answerGuards.d.ts +4 -0
- package/dist/{ask-UIIE5LD2.js → ask-IC5A5LIN.js} +1 -1
- package/dist/{chunk-I6HU3EM6.js → chunk-S3O7QZQ7.js} +22 -2
- package/dist/enrichPolicy.d.ts +29 -0
- package/dist/worker_public/src/index.js +21 -4
- package/package.json +1 -1
- package/workers/worker_public/src/admin.ts +11 -2
- package/workers/worker_public/src/answerGuards.ts +36 -0
- package/workers/worker_public/src/ask.ts +28 -31
- package/workers/worker_public/src/enrichPolicy.ts +40 -0
|
@@ -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.";
|
|
@@ -1637,7 +1637,7 @@ async function extractNameplate(ai, model, image) {
|
|
|
1637
1637
|
return null;
|
|
1638
1638
|
}
|
|
1639
1639
|
|
|
1640
|
-
// workers/worker_public/src/
|
|
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
|
-
|
|
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-
|
|
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 =
|
|
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
|
-
|
|
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-
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|