@konneal/engine 0.2.132 → 0.2.134

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,11 +1,11 @@
1
1
  import {
2
2
  handleAsk
3
- } from "./chunk-DNDQQYDT.js";
3
+ } from "./chunk-JYJJ4SYF.js";
4
4
  import "./chunk-J6LBR6GI.js";
5
5
  import "./chunk-7AXXR7GD.js";
6
6
  import "./chunk-TH5XGPD5.js";
7
7
  import "./chunk-W7J5SFXJ.js";
8
- import "./chunk-26VW4ZXU.js";
8
+ import "./chunk-WPOKSNPE.js";
9
9
  import "./chunk-4NXH5ZI6.js";
10
10
  import "./chunk-VT7DR6NQ.js";
11
11
  import "./chunk-GBGRHXRU.js";
@@ -83,7 +83,7 @@ import {
83
83
  canonicalRefusal,
84
84
  exciseMisplacedPin,
85
85
  refusalAnswer
86
- } from "./chunk-26VW4ZXU.js";
86
+ } from "./chunk-WPOKSNPE.js";
87
87
  import {
88
88
  entitlementScope,
89
89
  requestSalt,
@@ -1980,6 +1980,7 @@ async function handleAsk(env, ctx, req, tier, key) {
1980
1980
  }
1981
1981
  }
1982
1982
  let understanding = null;
1983
+ const refusalScope = (cited) => ({ cited, conversational: understanding?.intent === "conversational" });
1983
1984
  let optimisticVec = null;
1984
1985
  let optimisticHits = [];
1985
1986
  const t0 = Date.now();
@@ -2620,7 +2621,7 @@ ${q.query}`.replace("\n\n\n\n", "\n\n") }];
2620
2621
  }
2621
2622
  }
2622
2623
  }
2623
- const canonical0 = usedHits.length > 0 ? exciseMisplacedPin(canonicalRefusal(full), true) : canonicalRefusal(full);
2624
+ const canonical0 = usedHits.length > 0 ? exciseMisplacedPin(canonicalRefusal(full, refusalScope(usedHits.length > 0)), true) : canonicalRefusal(full, refusalScope(false));
2624
2625
  const c2 = canonical0.includes(refusalAnswer()) ? { text: canonical0, blocks: [], dropped: [] } : await contractV2(env.DB, canonical0, usedHits);
2625
2626
  send({
2626
2627
  type: "done",
@@ -2704,7 +2705,7 @@ ${q.query}`.replace("\n\n\n\n", "\n\n") }];
2704
2705
  );
2705
2706
  answer = await generateOnce(env, MODELS.fallback, flat, routeEffort(routed.route));
2706
2707
  }
2707
- if (answer) answer = canonicalRefusal(answer);
2708
+ if (answer) answer = canonicalRefusal(answer, refusalScope(usedHits.length > 0));
2708
2709
  if (answer && usedHits.length > 0) answer = exciseMisplacedPin(answer, true);
2709
2710
  stageTiming.generate = Date.now() - tGen;
2710
2711
  let used = usedHits;
@@ -2729,7 +2730,7 @@ ${q.query}`.replace("\n\n\n\n", "\n\n") }];
2729
2730
  generateRetries += 1;
2730
2731
  const corrected = await generateOnce(env, model, [...messages, { role: "system", content: note }], routeEffort(routed.route));
2731
2732
  if (corrected) {
2732
- const correctedAnswer = canonicalRefusal(corrected);
2733
+ const correctedAnswer = canonicalRefusal(corrected, refusalScope(usedHits.length > 0));
2733
2734
  const retryAnchors = checkQuoteAnchors(correctedAnswer, used.map((h) => h.text));
2734
2735
  const retryRetyped = tableRetyped(correctedAnswer, hasTableUnit);
2735
2736
  if (retryAnchors.violations.length < anchors.violations.length || !retryRetyped && retyped || unreferenced && correctedAnswer.includes("[[u:")) {
@@ -2754,7 +2755,7 @@ ${q.query}`.replace("\n\n\n\n", "\n\n") }];
2754
2755
  const { messages: retryMessages, usedHits: retryUsed } = buildMessages(q.query, retryRetrieve.hits, q.lang, keptHistory, void 0, summary, budget);
2755
2756
  const retryAnswer = await generateOnce(env, model, retryMessages, routeEffort(routed.route));
2756
2757
  if (retryAnswer) {
2757
- answer = canonicalRefusal(retryAnswer);
2758
+ answer = canonicalRefusal(retryAnswer, refusalScope(retryRetrieve.hits.length > 0));
2758
2759
  used = retryUsed;
2759
2760
  }
2760
2761
  }
@@ -2786,7 +2787,7 @@ ${q.query}`.replace("\n\n\n\n", "\n\n") }];
2786
2787
  messages[messages.length - 1]
2787
2788
  ], effort);
2788
2789
  if (regen && !regen.includes(refusalAnswer())) {
2789
- answer = canonicalRefusal(regen);
2790
+ answer = canonicalRefusal(regen, refusalScope(true));
2790
2791
  console.log("blind-answer: regeneration grounded");
2791
2792
  }
2792
2793
  }
@@ -24,16 +24,7 @@ function refusalPatterns(publisher) {
24
24
  // verb is the guard, a scope DISCUSSION inside a real answer
25
25
  // ("this exemption is outside the scope of R 60") never matches
26
26
  /\bI don[’']?t have (?:any )?information on [^.]{0,80}(?:that[’']?s|—)?\s*(?:general|common) knowledge\b/i,
27
- /\b(?:that[’']?s|this is) (?:just )?(?:general|common) knowledge[^.]{0,80}(?:outside|beyond|not covered)\b/i,
28
- // the redirect-only openers (2026-10-11 smoke): the model swaps the
29
- // pinned sentence for a scope OFFER — "I'm happy to help with
30
- // questions about OIML … instead." / "I can answer questions about
31
- // OIML … — such as OIML B 20:2024 …". Anchored at the answer's
32
- // start (a real answer never OPENS with the redirect — the
33
- // redirect TAIL after content survives untouched), the publisher
34
- // token profile-driven.
35
- new RegExp(`^\\s*I(?:['\u2019]m| am) happy to help with questions about ${publisher}\\b[^\\n]{0,220}?\\binstead\\b`, "i"),
36
- new RegExp(`^\\s*I can (?:answer|help with) questions about ${publisher}\\b[^\\n]{0,220}?\\bpublications\\b`, "i")
27
+ /\b(?:that[’']?s|this is) (?:just )?(?:general|common) knowledge[^.]{0,80}(?:outside|beyond|not covered)\b/i
37
28
  ],
38
29
  // the misplaced-pin wall's subset: every pattern here carries a
39
30
  // first-person refusal verb ("I don't have information", "I can't
@@ -61,7 +52,19 @@ function sentenceEnd(answer, i) {
61
52
  const m = /[.!?\n]/.exec(answer.slice(i));
62
53
  return m ? i + m.index + 1 : answer.length;
63
54
  }
64
- function canonicalRefusal(answer) {
55
+ function redirectOfferSentences(answer) {
56
+ const out = [];
57
+ let i = 0;
58
+ while (i < answer.length) {
59
+ const end = sentenceEnd(answer, i);
60
+ const s = answer.slice(i, end);
61
+ if (/\bI\b[^.!?]{0,160}?\bquestions? about\b/i.test(s) || /\bI\b[^.!?]{0,160}?\binstead\b/i.test(s) && !/[§\[\d]/.test(s)) out.push([i, end]);
62
+ i = end;
63
+ while (i < answer.length && /\s/.test(answer[i] ?? "")) i++;
64
+ }
65
+ return out;
66
+ }
67
+ function canonicalRefusal(answer, scope) {
65
68
  const CANON = refusalAnswer();
66
69
  if (answer.includes(CANON)) return answer;
67
70
  const { variant: REFUSAL_VARIANT, drift: REFUSAL_DRIFT } = refusalPatterns(P().publisher.name);
@@ -72,6 +75,10 @@ function canonicalRefusal(answer) {
72
75
  if (!m) continue;
73
76
  return answer.slice(0, sentenceStart(answer, m.index)) + CANON + answer.slice(sentenceEnd(answer, m.index));
74
77
  }
78
+ if (scope && !scope.cited && !scope.conversational) {
79
+ const offer = redirectOfferSentences(answer)[0];
80
+ if (offer) return answer.slice(0, offer[0]) + CANON + answer.slice(offer[1]);
81
+ }
75
82
  return answer;
76
83
  }
77
84
  function exciseMisplacedPin(answer, cites) {
package/dist/refusal.d.ts CHANGED
@@ -2,11 +2,27 @@
2
2
  * Refusals are never cached: a refusal says "retrieval found nothing",
3
3
  * which is a property of the moment, not of the question. */
4
4
  export declare function refusalAnswer(): string;
5
+ /** The refusal scope (2026-10-11, the third smoke shape): the loose
6
+ * redirect-offer family is gated by what the PIPELINE knows, not by
7
+ * ever-more phrase patterns. The model's scope offers keep drifting
8
+ * ("I'm happy to help … instead", "I can answer questions about …",
9
+ * "I'm a metrology assistant — I can help with … instead") — the same
10
+ * surface form that legitimately ANSWERS a conversational question
11
+ * ("what can you help me with?"). The discriminators: a knowledge-seek
12
+ * question (the understanding's own class) and an UNCITED answer — a
13
+ * cited answer's scope talk is content, a conversational turn's scope
14
+ * talk is the answer itself. */
15
+ export interface RefusalScope {
16
+ /** the understanding read the question as a conversational turn */
17
+ conversational?: boolean;
18
+ /** the answer carries citations */
19
+ cited?: boolean;
20
+ }
5
21
  /** Normalize a refusal-shaped answer to the pinned sentence. A drift
6
22
  * refusal occupies its own sentence: swap that sentence (preface
7
23
  * included) for the pinned one — the redirect tail survives. An answer
8
24
  * outside the refusal family is returned byte-identical. */
9
- export declare function canonicalRefusal(answer: string): string;
25
+ export declare function canonicalRefusal(answer: string, scope?: RefusalScope): string;
10
26
  /** The misplaced-pin wall (2026-10-10): an answer that actually cites
11
27
  * passages is NOT a refusal — but the model sometimes drops the
12
28
  * pinned sentence into an otherwise-correct, well-cited answer as a
@@ -4,7 +4,7 @@ import {
4
4
  import "./chunk-7AXXR7GD.js";
5
5
  import "./chunk-TH5XGPD5.js";
6
6
  import "./chunk-W7J5SFXJ.js";
7
- import "./chunk-26VW4ZXU.js";
7
+ import "./chunk-WPOKSNPE.js";
8
8
  import "./chunk-4NXH5ZI6.js";
9
9
  import "./chunk-VT7DR6NQ.js";
10
10
  import "./chunk-3FYJM7LH.js";
@@ -4,7 +4,7 @@ import {
4
4
  handleMemories,
5
5
  scoreFaithfulness,
6
6
  scoreJudge
7
- } from "../../chunk-DNDQQYDT.js";
7
+ } from "../../chunk-JYJJ4SYF.js";
8
8
  import {
9
9
  TOOLS_REGISTRY,
10
10
  bindModelNode,
@@ -54,7 +54,7 @@ import {
54
54
  } from "../../chunk-W7J5SFXJ.js";
55
55
  import {
56
56
  canonicalRefusal
57
- } from "../../chunk-26VW4ZXU.js";
57
+ } from "../../chunk-WPOKSNPE.js";
58
58
  import {
59
59
  entitlementScope,
60
60
  standardKeysFrom
@@ -877,7 +877,7 @@ ${recent.map((h, n) => `[${n + 1}] ${h.metadata.docidentifier ?? ""} \xA7${h.met
877
877
  telemetry(env, ctx, "member", "research", MODELS.research, false, 0, queryHash, q.lang);
878
878
  throw new Error("The generation model is unavailable; please retry.");
879
879
  }
880
- answer = canonicalRefusal(answer);
880
+ answer = canonicalRefusal(answer, { cited: usedHits.length > 0, conversational: false });
881
881
  const anchors = checkQuoteAnchors(answer, used.map((h) => h.text));
882
882
  if (anchors.violations.length) console.log("research anchors:", anchors.violations.length, "unverified");
883
883
  const out = {
@@ -1052,7 +1052,7 @@ async function callToolOnce(env, ctx, _req, tier, key, name, args, t0) {
1052
1052
  const r = await runTool({ ...env, DB: env.DB }, { name, args }, "mcp");
1053
1053
  payload = r ? { tool: r.name, query: r.query, result: r.output } : { error: { message: `tool ${name} returned nothing for the given arguments` } };
1054
1054
  } else {
1055
- const res = name === "ask" ? await (await import("../../ask-XAJDCNFU.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-544LVPGG.js")).handleSearch(env, ctx, inner, tier, key);
1055
+ const res = name === "ask" ? await (await import("../../ask-PFU74MIP.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-NIRCPRMW.js")).handleSearch(env, ctx, inner, tier, key);
1056
1056
  payload = await res.json().catch(() => ({ error: { message: "tool transport failed", status: res.status } }));
1057
1057
  }
1058
1058
  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 });
@@ -2,7 +2,7 @@ import {
2
2
  canonicalRefusal,
3
3
  exciseMisplacedPin,
4
4
  refusalAnswer
5
- } from "../../chunk-26VW4ZXU.js";
5
+ } from "../../chunk-WPOKSNPE.js";
6
6
  import "../../chunk-3FYJM7LH.js";
7
7
  export {
8
8
  canonicalRefusal,
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.132",
30
+ "version": "0.2.134",
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",
@@ -625,6 +625,10 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
625
625
  // take this path (they always run understanding + live retrieval);
626
626
  // fresh already bypassed the exact cache above and bypasses this one.
627
627
  let understanding: any = null;
628
+ // the loose redirect-offer family's gate (refusal.ts): knowledge-seek
629
+ // questions only, uncited answers only — a conversational turn's
630
+ // scope vocabulary is the answer itself
631
+ const refusalScope = (cited: boolean) => ({ cited, conversational: understanding?.intent === "conversational" });
628
632
  // a query naming a model node (/req/…, /term/…) is node-SCOPED: its
629
633
  // ── Option C: optimistic parallel retrieval ──
630
634
  // The dense lane (folded-query embed + unfiltered Vectorize query) runs
@@ -1537,7 +1541,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1537
1541
  }
1538
1542
  // the misplaced-pin wall rides the streamed path too: a
1539
1543
  // CITED stream is not a refusal
1540
- const canonical0 = usedHits.length > 0 ? exciseMisplacedPin(canonicalRefusal(full), true) : canonicalRefusal(full);
1544
+ const canonical0 = usedHits.length > 0 ? exciseMisplacedPin(canonicalRefusal(full, refusalScope(usedHits.length > 0)), true) : canonicalRefusal(full, refusalScope(false));
1541
1545
  // answer contract v2: validate [[u:]] refs, resolve typed blocks
1542
1546
  const c2 = canonical0.includes(refusalAnswer())
1543
1547
  ? { text: canonical0, blocks: [], dropped: [] as string[] }
@@ -1641,7 +1645,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1641
1645
  );
1642
1646
  answer = await generateOnce(env, MODELS.fallback, flat, routeEffort(routed.route));
1643
1647
  }
1644
- if (answer) answer = canonicalRefusal(answer);
1648
+ if (answer) answer = canonicalRefusal(answer, refusalScope(usedHits.length > 0));
1645
1649
  // the misplaced-pin wall: a CITED answer is not a refusal — excise
1646
1650
  // the pinned sentence if the model dropped it in as a disclaimer
1647
1651
  // (measured on chip-scoped asks: correct cited answers carrying the
@@ -1684,7 +1688,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1684
1688
  generateRetries += 1;
1685
1689
  const corrected = await generateOnce(env, model, [...messages, { role: "system", content: note }], routeEffort(routed.route));
1686
1690
  if (corrected) {
1687
- const correctedAnswer = canonicalRefusal(corrected);
1691
+ const correctedAnswer = canonicalRefusal(corrected, refusalScope(usedHits.length > 0));
1688
1692
  const retryAnchors = checkQuoteAnchors(correctedAnswer, used.map((h: Hit) => h.text));
1689
1693
  const retryRetyped = tableRetyped(correctedAnswer, hasTableUnit);
1690
1694
  if (retryAnchors.violations.length < anchors.violations.length || (!retryRetyped && retyped) || (unreferenced && correctedAnswer.includes("[[u:"))) {
@@ -1729,7 +1733,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1729
1733
  const retryAnswer = await generateOnce(env, model, retryMessages, routeEffort(routed.route));
1730
1734
  // the answer now comes from the retry passages — citations must follow
1731
1735
  if (retryAnswer) {
1732
- answer = canonicalRefusal(retryAnswer);
1736
+ answer = canonicalRefusal(retryAnswer, refusalScope(retryRetrieve.hits.length > 0));
1733
1737
  used = retryUsed;
1734
1738
  }
1735
1739
  }
@@ -1781,7 +1785,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1781
1785
  messages[messages.length - 1],
1782
1786
  ], effort);
1783
1787
  if (regen && !regen.includes(refusalAnswer())) {
1784
- answer = canonicalRefusal(regen);
1788
+ answer = canonicalRefusal(regen, refusalScope(true));
1785
1789
  console.log("blind-answer: regeneration grounded");
1786
1790
  }
1787
1791
  }
@@ -29,15 +29,6 @@ function refusalPatterns(publisher: string): { variant: RegExp; drift: RegExp[];
29
29
  // ("this exemption is outside the scope of R 60") never matches
30
30
  /\bI don[’']?t have (?:any )?information on [^.]{0,80}(?:that[’']?s|—)?\s*(?:general|common) knowledge\b/i,
31
31
  /\b(?:that[’']?s|this is) (?:just )?(?:general|common) knowledge[^.]{0,80}(?:outside|beyond|not covered)\b/i,
32
- // the redirect-only openers (2026-10-11 smoke): the model swaps the
33
- // pinned sentence for a scope OFFER — "I'm happy to help with
34
- // questions about OIML … instead." / "I can answer questions about
35
- // OIML … — such as OIML B 20:2024 …". Anchored at the answer's
36
- // start (a real answer never OPENS with the redirect — the
37
- // redirect TAIL after content survives untouched), the publisher
38
- // token profile-driven.
39
- new RegExp(`^\\s*I(?:['’]m| am) happy to help with questions about ${publisher}\\b[^\\n]{0,220}?\\binstead\\b`, "i"),
40
- new RegExp(`^\\s*I can (?:answer|help with) questions about ${publisher}\\b[^\\n]{0,220}?\\bpublications\\b`, "i"),
41
32
  ],
42
33
  // the misplaced-pin wall's subset: every pattern here carries a
43
34
  // first-person refusal verb ("I don't have information", "I can't
@@ -71,11 +62,48 @@ function sentenceEnd(answer: string, i: number): number {
71
62
  return m ? i + m.index + 1 : answer.length;
72
63
  }
73
64
 
65
+ /** The refusal scope (2026-10-11, the third smoke shape): the loose
66
+ * redirect-offer family is gated by what the PIPELINE knows, not by
67
+ * ever-more phrase patterns. The model's scope offers keep drifting
68
+ * ("I'm happy to help … instead", "I can answer questions about …",
69
+ * "I'm a metrology assistant — I can help with … instead") — the same
70
+ * surface form that legitimately ANSWERS a conversational question
71
+ * ("what can you help me with?"). The discriminators: a knowledge-seek
72
+ * question (the understanding's own class) and an UNCITED answer — a
73
+ * cited answer's scope talk is content, a conversational turn's scope
74
+ * talk is the answer itself. */
75
+ export interface RefusalScope {
76
+ /** the understanding read the question as a conversational turn */
77
+ conversational?: boolean;
78
+ /** the answer carries citations */
79
+ cited?: boolean;
80
+ }
81
+
82
+ /** A whole-sentence redirect offer in ONE sentence, first person:
83
+ * the "questions about" marker (four observed shapes carry it —
84
+ * "I'm happy to help with questions about…", "I can answer questions
85
+ * about…", "I'm a metrology assistant — I can answer questions
86
+ * about…", "I'm here for questions about…"), or the "instead" marker
87
+ * in a sentence with NO citation apparatus (no clause anchors, no
88
+ * digits — an offer names no provisions). */
89
+ function redirectOfferSentences(answer: string): Array<[number, number]> {
90
+ const out: Array<[number, number]> = [];
91
+ let i = 0;
92
+ while (i < answer.length) {
93
+ const end = sentenceEnd(answer, i);
94
+ const s = answer.slice(i, end);
95
+ if (/\bI\b[^.!?]{0,160}?\bquestions? about\b/i.test(s) || (/\bI\b[^.!?]{0,160}?\binstead\b/i.test(s) && !/[§\[\d]/.test(s))) out.push([i, end]);
96
+ i = end;
97
+ while (i < answer.length && /\s/.test(answer[i] ?? "")) i++;
98
+ }
99
+ return out;
100
+ }
101
+
74
102
  /** Normalize a refusal-shaped answer to the pinned sentence. A drift
75
103
  * refusal occupies its own sentence: swap that sentence (preface
76
104
  * included) for the pinned one — the redirect tail survives. An answer
77
105
  * outside the refusal family is returned byte-identical. */
78
- export function canonicalRefusal(answer: string): string {
106
+ export function canonicalRefusal(answer: string, scope?: RefusalScope): string {
79
107
  const CANON = refusalAnswer();
80
108
  if (answer.includes(CANON)) return answer;
81
109
  const { variant: REFUSAL_VARIANT, drift: REFUSAL_DRIFT } = refusalPatterns(P().publisher.name);
@@ -86,6 +114,10 @@ export function canonicalRefusal(answer: string): string {
86
114
  if (!m) continue;
87
115
  return answer.slice(0, sentenceStart(answer, m.index)) + CANON + answer.slice(sentenceEnd(answer, m.index));
88
116
  }
117
+ if (scope && !scope.cited && !scope.conversational) {
118
+ const offer = redirectOfferSentences(answer)[0];
119
+ if (offer) return answer.slice(0, offer[0]) + CANON + answer.slice(offer[1]);
120
+ }
89
121
  return answer;
90
122
  }
91
123
 
@@ -164,7 +164,7 @@ async function run(
164
164
  telemetry(env, ctx, "member", "research", MODELS.research, false, 0, queryHash, q.lang);
165
165
  throw new Error("The generation model is unavailable; please retry.");
166
166
  }
167
- answer = canonicalRefusal(answer);
167
+ answer = canonicalRefusal(answer, { cited: usedHits.length > 0, conversational: false });
168
168
  const anchors = checkQuoteAnchors(answer, used.map((h: Hit) => h.text));
169
169
  if (anchors.violations.length) console.log("research anchors:", anchors.violations.length, "unverified");
170
170
  const out = {