@konneal/engine 0.2.168 → 0.2.170

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.
Files changed (31) hide show
  1. package/dist/ablate.d.ts +3 -0
  2. package/dist/ask-BKWQEMSY.js +16 -0
  3. package/dist/{chunk-4FZNJR5Q.js → chunk-5UIMQAJZ.js} +10 -0
  4. package/dist/{chunk-BFOCWZ7U.js → chunk-6KEWBP7O.js} +5 -5
  5. package/dist/{chunk-LSZTJI7V.js → chunk-KINFFYQH.js} +6 -2
  6. package/dist/{chunk-QY556RJM.js → chunk-O2XMNKCK.js} +1 -1
  7. package/dist/{chunk-AGL4NGXY.js → chunk-PO3UBDNZ.js} +224 -12
  8. package/dist/{chunk-Y7LWT24U.js → chunk-PQDAL4MX.js} +1 -1
  9. package/dist/{chunk-PTY2OSC4.js → chunk-QS6V53RO.js} +5 -5
  10. package/dist/{chunk-LIZJLRXZ.js → chunk-RZLEHCIA.js} +4 -4
  11. package/dist/{chunk-LU7VIMD7.js → chunk-XWSZWFYP.js} +1 -1
  12. package/dist/config.d.ts +6 -0
  13. package/dist/decisionWire.d.ts +25 -0
  14. package/dist/decisions.d.ts +39 -0
  15. package/dist/{env-LMHGNJTN.js → env-CNO44VN2.js} +1 -1
  16. package/dist/search-RNCRT5BR.js +14 -0
  17. package/dist/{tools-3RP7DHGI.js → tools-HK5RXHBQ.js} +5 -5
  18. package/dist/worker_mcp/src/index.js +2 -2
  19. package/dist/worker_public/src/config.js +1 -1
  20. package/dist/worker_public/src/index.js +11 -11
  21. package/dist/worker_public/src/requestScope.js +2 -2
  22. package/package.json +1 -1
  23. package/workers/worker_public/src/ablate.ts +6 -2
  24. package/workers/worker_public/src/ask.ts +56 -3
  25. package/workers/worker_public/src/config.ts +10 -0
  26. package/workers/worker_public/src/decisionWire.ts +124 -0
  27. package/workers/worker_public/src/decisions.ts +148 -0
  28. package/workers/worker_public/src/ports/cloudflare/adapters.ts +10 -2
  29. package/workers/worker_public/src/ports/model.ts +8 -1
  30. package/dist/ask-RUMLIO35.js +0 -16
  31. package/dist/search-Y4Y46Y57.js +0 -14
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  authenticate
3
- } from "../../chunk-QY556RJM.js";
4
- import "../../chunk-4FZNJR5Q.js";
3
+ } from "../../chunk-O2XMNKCK.js";
4
+ import "../../chunk-5UIMQAJZ.js";
5
5
  import {
6
6
  P,
7
7
  setProfile
@@ -16,7 +16,7 @@ import {
16
16
  roleModel,
17
17
  sha256Hex,
18
18
  today
19
- } from "../../chunk-4FZNJR5Q.js";
19
+ } from "../../chunk-5UIMQAJZ.js";
20
20
  import "../../chunk-3FYJM7LH.js";
21
21
  export {
22
22
  DATASETS,
@@ -4,16 +4,16 @@ import {
4
4
  handleMemories,
5
5
  scoreFaithfulness,
6
6
  scoreJudge
7
- } from "../../chunk-AGL4NGXY.js";
7
+ } from "../../chunk-PO3UBDNZ.js";
8
8
  import "../../chunk-ECOYO7EX.js";
9
9
  import {
10
10
  TOOLS_REGISTRY,
11
11
  bindModelNode,
12
12
  modelGroundingBlock
13
- } from "../../chunk-PTY2OSC4.js";
13
+ } from "../../chunk-QS6V53RO.js";
14
14
  import {
15
15
  handleSearch
16
- } from "../../chunk-BFOCWZ7U.js";
16
+ } from "../../chunk-6KEWBP7O.js";
17
17
  import {
18
18
  buildMessages,
19
19
  citations,
@@ -38,11 +38,11 @@ import {
38
38
  sessionFrom,
39
39
  telemetry,
40
40
  understandQuery
41
- } from "../../chunk-LIZJLRXZ.js";
41
+ } from "../../chunk-RZLEHCIA.js";
42
42
  import {
43
43
  embed,
44
44
  generateOnce
45
- } from "../../chunk-LU7VIMD7.js";
45
+ } from "../../chunk-XWSZWFYP.js";
46
46
  import {
47
47
  authenticate,
48
48
  corsHeaders,
@@ -51,14 +51,14 @@ import {
51
51
  readJson,
52
52
  validateQuery,
53
53
  withCors
54
- } from "../../chunk-QY556RJM.js";
54
+ } from "../../chunk-O2XMNKCK.js";
55
55
  import {
56
56
  canonicalRefusal
57
57
  } from "../../chunk-ETLSWSZ7.js";
58
58
  import {
59
59
  entitlementScope,
60
60
  standardKeysFrom
61
- } from "../../chunk-Y7LWT24U.js";
61
+ } from "../../chunk-PQDAL4MX.js";
62
62
  import {
63
63
  LIMITS,
64
64
  MODELS,
@@ -69,7 +69,7 @@ import {
69
69
  roleModel,
70
70
  sha256Hex,
71
71
  today
72
- } from "../../chunk-4FZNJR5Q.js";
72
+ } from "../../chunk-5UIMQAJZ.js";
73
73
  import {
74
74
  P,
75
75
  setProfile
@@ -77,7 +77,7 @@ import {
77
77
  import {
78
78
  cfBlobs,
79
79
  portModelRunner
80
- } from "../../chunk-LSZTJI7V.js";
80
+ } from "../../chunk-KINFFYQH.js";
81
81
 
82
82
  // workers/worker_public/src/conversations.ts
83
83
  var ID_RE = /^[a-zA-Z0-9_-]{8,64}$/;
@@ -1119,13 +1119,13 @@ async function callToolOnce(env, ctx, _req, tier, key, name, args, t0) {
1119
1119
  // stream:false forces the JSON lane (anon defaults to SSE)
1120
1120
  body: JSON.stringify({ ...args, stream: false })
1121
1121
  });
1122
- const { runTool, TOOLS_REGISTRY: TOOLS_REGISTRY2 } = await import("../../tools-3RP7DHGI.js");
1122
+ const { runTool, TOOLS_REGISTRY: TOOLS_REGISTRY2 } = await import("../../tools-HK5RXHBQ.js");
1123
1123
  let payload;
1124
1124
  if (TOOLS_REGISTRY2.some((t) => t.name === name && t.audiences.includes("mcp"))) {
1125
1125
  const r = await runTool({ ...env, DB: env.DB }, { name, args }, "mcp");
1126
1126
  payload = r ? { tool: r.name, query: r.query, result: r.output } : { error: { message: `tool ${name} returned nothing for the given arguments` } };
1127
1127
  } else {
1128
- const res = name === "ask" ? await (await import("../../ask-RUMLIO35.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-Y4Y46Y57.js")).handleSearch(env, ctx, inner, tier, key);
1128
+ const res = name === "ask" ? await (await import("../../ask-BKWQEMSY.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-RNCRT5BR.js")).handleSearch(env, ctx, inner, tier, key);
1129
1129
  payload = await res.json().catch(() => ({ error: { message: "tool transport failed", status: res.status } }));
1130
1130
  }
1131
1131
  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 });
@@ -4,8 +4,8 @@ import {
4
4
  requestSalt,
5
5
  resolveRequestScope,
6
6
  standardKeysFrom
7
- } from "../../chunk-Y7LWT24U.js";
8
- import "../../chunk-4FZNJR5Q.js";
7
+ } from "../../chunk-PQDAL4MX.js";
8
+ import "../../chunk-5UIMQAJZ.js";
9
9
  import "../../chunk-3FYJM7LH.js";
10
10
  export {
11
11
  entitlementScope,
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.168",
30
+ "version": "0.2.170",
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",
@@ -39,6 +39,9 @@ export interface AblateConfig {
39
39
  * budget before the answer model reads it. Off in serving; the grid
40
40
  * arms it per ask (the row's accuracy-at-token-budget gate). */
41
41
  compress: boolean;
42
+ /** The decision pass (TODO.decisions/01): per-request stand-down for
43
+ * the measurement — the serving switch is DECISIONS_ENABLED. */
44
+ noDecisions: boolean;
42
45
  }
43
46
 
44
47
  export type AblateParse =
@@ -61,10 +64,11 @@ export function parseAblate(
61
64
  const rawRoute = body?.ablate_route;
62
65
  const speculative = body?.ablate_speculative === true;
63
66
  const compress = body?.ablate_compress === true;
67
+ const noDecisions = body?.ablate_no_decisions === true;
64
68
  const namesAblation = rawStages !== undefined && rawStages !== null;
65
69
  const routeAblation = rawRoute !== undefined && rawRoute !== null && rawRoute !== "adaptive";
66
70
 
67
- if (!namesAblation && !noVerdict && !routeAblation && !speculative && !compress) return { ok: true, config: null };
71
+ if (!namesAblation && !noVerdict && !routeAblation && !speculative && !compress && !noDecisions) return { ok: true, config: null };
68
72
 
69
73
  if (!adminToken || presentedToken !== adminToken) {
70
74
  return {
@@ -99,5 +103,5 @@ export function parseAblate(
99
103
  stages = [...new Set(rawStages as string[])];
100
104
  }
101
105
 
102
- return { ok: true, config: { stages, noVerdict, route, speculative, compress } };
106
+ return { ok: true, config: { stages, noVerdict, route, speculative, compress, noDecisions } };
103
107
  }
@@ -35,6 +35,8 @@ import { entitlementScope, resolveRequestScope, requestSalt, standardKeysFrom }
35
35
  import { rawSessionToken, type SessionClaims } from "./session";
36
36
  import { cacheKeyMaterial, corpusGen, exactCacheKey, freshRequested } from "./answercache";
37
37
  import { parseAblate } from "./ablate";
38
+ import { decisionsEnabled, gateQuestion, runDecisionPass } from "./decisionWire";
39
+ import type { DecisionItem } from "./decisions";
38
40
  import { STAGE_NAMES } from "./stages/index.ts";
39
41
  import { routeFor, fastRouteStages } from "./route.ts";
40
42
  import { partitionSubsets, type Partitionable } from "./speculative.ts";
@@ -1303,6 +1305,11 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1303
1305
  // report it (TODO.sota/14.6 v2) — the coarse retrieval grade and the
1304
1306
  // corrective retry's firing/acceptance
1305
1307
  const crag: { grade: string | null; corrective: { fired: boolean; accepted: boolean; grade: string | null } | null } = { grade: null, corrective: null };
1308
+ // the decision pass's gate (TODO.decisions/01): one cheap clef-flash
1309
+ // noul over the question, prefetched in parallel with retrieval — the
1310
+ // pass itself runs only if the gate clears and the ops switch is on
1311
+ const decisionsArmed = decisionsEnabled(env) && !(ablate as any)?.noDecisions;
1312
+ const decisionsGatePromise = decisionsArmed ? gateQuestion(env, q.query) : Promise.resolve(null);
1306
1313
  try {
1307
1314
  const tR = Date.now();
1308
1315
  // ── The "my account" live read (TODO.ai-platform/03) — resolved
@@ -1502,6 +1509,20 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1502
1509
  budget,
1503
1510
  ablate?.compress === true,
1504
1511
  );
1512
+ // the decision pass (TODO.decisions/01): starts here so its calls run
1513
+ // concurrently with generation — the checklist section arrives the
1514
+ // moment the model's last token lands, never delaying the answer
1515
+ const decisionPromise = decisionsArmed && decisionsGatePromise
1516
+ ? decisionsGatePromise.then((g) =>
1517
+ g != null && g >= THRESHOLDS.decisionsGateMin
1518
+ ? runDecisionPass(env, {
1519
+ question: q.query,
1520
+ gate: g,
1521
+ passages: usedHits.map((h: Hit) => h.text),
1522
+ docidentifier: understanding?.docidentifier ?? null,
1523
+ })
1524
+ : null)
1525
+ : Promise.resolve(null);
1505
1526
  await attachFigureImages(env, messages, usedHits, q.query);
1506
1527
  // the agent loop (the owner's 2026-09-28 direction): ONE bounded tool
1507
1528
  // round, armed only where a tool can help. The model may call
@@ -1697,12 +1718,36 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1697
1718
  }
1698
1719
  // the misplaced-pin wall rides the streamed path too: a
1699
1720
  // CITED stream is not a refusal
1700
- const canonical0 = exciseMisplacedPin(canonicalRefusal(full.replace(/^TOOL [a-z_.]+.*$\n?/gm, "").replace(/^\n+/, ""), refusalScope(answerCites(full))), answerCites(full));
1721
+ let canonical0 = exciseMisplacedPin(canonicalRefusal(full.replace(/^TOOL [a-z_.]+.*$\n?/gm, "").replace(/^\n+/, ""), refusalScope(answerCites(full))), answerCites(full));
1701
1722
  // answer contract v2: validate [[u:]] refs, resolve typed blocks
1702
1723
  const c2 = canonical0.includes(refusalAnswer())
1703
1724
  ? { text: canonical0, blocks: [], dropped: [] as string[] }
1704
1725
  : await contractV2(env.DB, canonical0, usedHits);
1705
- send({ type: "done", model, query_hash: queryHash, follow_ups: refusalShaped(canonical0, refusalScope(answerCites(full))) ? [] : (understanding?.follow_ups ?? []), blocks: [...c2.blocks, ...(verdictBlock ? [verdictBlock] : []), ...(conditionBlock ? [conditionBlock] : []), ...(aggregationBlock ? [aggregationBlock] : [])], context_applied: ctxApplied, read: readAs(),
1726
+ // the completions ride the streamed lane too (parity: the UI's
1727
+ // bubble IS this lane — the table/figure blocks never reached it
1728
+ // before): additive D1 reads after the tokens are out, refusal
1729
+ // answers carry nothing to complete
1730
+ let streamedCompletionBlocks: Awaited<ReturnType<typeof completeTables>> = [];
1731
+ // the decision pass's section streams as the answer's tail
1732
+ // (the pass ran concurrently with generation — its arrival is
1733
+ // instant here), before the completions and the done event
1734
+ if (decisionPromise && !canonical0.includes(refusalAnswer())) {
1735
+ const dp = await decisionPromise;
1736
+ if (dp) {
1737
+ canonical0 += dp.section;
1738
+ send({ type: "token", v: dp.section });
1739
+ full += dp.section;
1740
+ console.log("decisions: streamed the checklist section (" + dp.items.length + " items, mode " + dp.mode + ")");
1741
+ }
1742
+ }
1743
+ if (!canonical0.includes(refusalAnswer())) {
1744
+ try {
1745
+ streamedCompletionBlocks = await completeTables(env.DB, canonical0, usedHits);
1746
+ streamedCompletionBlocks.push(...(await completeFigures(env.DB, canonical0, [...c2.blocks, ...streamedCompletionBlocks], usedHits, q.query)));
1747
+ if (streamedCompletionBlocks.length) console.log("streamed completion:", streamedCompletionBlocks.length, "block(s) attached");
1748
+ } catch { /* additive — the answer stands without the blocks */ }
1749
+ }
1750
+ send({ type: "done", model, query_hash: queryHash, follow_ups: refusalShaped(canonical0, refusalScope(answerCites(full))) ? [] : (understanding?.follow_ups ?? []), blocks: [...c2.blocks, ...(verdictBlock ? [verdictBlock] : []), ...(conditionBlock ? [conditionBlock] : []), ...(aggregationBlock ? [aggregationBlock] : []), ...streamedCompletionBlocks], context_applied: ctxApplied, read: readAs(),
1706
1751
  // the evidence view's ground truth: the exact passages this
1707
1752
  // answer was built from, compact — cache hits carry none,
1708
1753
  // because the cache stores the answer and never the passages
@@ -2035,7 +2080,15 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
2035
2080
  if (v.support !== "supported") console.log("entailment:", v.support, raced!.score, raced!.ungrounded_claims?.slice(0, 2));
2036
2081
  }
2037
2082
  }
2038
- const out = { answer, citations: finalCites, ...(jsonQuality ? { source_quality: jsonQuality!, confidence_note: entailmentNote || qualityNote(jsonQuality!), ...(entailment ? { entailment } : {}), ...(jsonQuality === "ocr" ? { experimental_sources: experimentalSourceLabels(finalCites) } : {}) } : {}), model: MODELS.member, query_hash: queryHash, follow_ups: refusalShaped(answer!, refusalScope(answerCites(answer!))) ? [] : (understanding?.follow_ups ?? []), blocks: [...c2ns.blocks, ...(verdictBlock ? [verdictBlock] : []), ...(conditionBlock ? [conditionBlock] : []), ...(aggregationBlock ? [aggregationBlock] : []), ...(registerBlock ? [registerBlock] : []), ...completionBlocks], context_applied: ctxApplied, ...(liveRecords ? { records: liveRecords } : {}), ...(ablate ? { ablate } : {}), route: { route: routed.route, features: routed.features } };
2083
+ let decisionsOut: { mode: string; gate: number; items: DecisionItem[] } | undefined;
2084
+ if (decisionPromise) {
2085
+ const dp = await decisionPromise;
2086
+ if (dp && answer && !answer!.includes(refusalAnswer())) {
2087
+ answer = answer! + dp.section;
2088
+ decisionsOut = { mode: dp.mode, gate: dp.gate, items: dp.items };
2089
+ }
2090
+ }
2091
+ const out = { answer, citations: finalCites, ...(jsonQuality ? { source_quality: jsonQuality!, confidence_note: entailmentNote || qualityNote(jsonQuality!), ...(entailment ? { entailment } : {}), ...(jsonQuality === "ocr" ? { experimental_sources: experimentalSourceLabels(finalCites) } : {}) } : {}), model: MODELS.member, query_hash: queryHash, follow_ups: refusalShaped(answer!, refusalScope(answerCites(answer!))) ? [] : (understanding?.follow_ups ?? []), blocks: [...c2ns.blocks, ...(verdictBlock ? [verdictBlock] : []), ...(conditionBlock ? [conditionBlock] : []), ...(aggregationBlock ? [aggregationBlock] : []), ...(registerBlock ? [registerBlock] : []), ...completionBlocks], context_applied: ctxApplied, ...(liveRecords ? { records: liveRecords } : {}), ...(decisionsOut ? { decisions: decisionsOut } : {}), ...(ablate ? { ablate } : {}), route: { route: routed.route, features: routed.features } };
2039
2092
  const cacheable = !contextual && !declaredCtx && !ablate && cacheableAnswer(answer!, refusalScope(answerCites(answer!))) && finalAnchors.violations.length === 0;
2040
2093
  if (cacheable) {
2041
2094
  const ck = exactCacheKey(env.INDEX_VERSION, gen, ns, await sha256Hex(cacheKeyMaterial(q.query, q.lang, salt)));
@@ -11,6 +11,10 @@ export const MODELS = {
11
11
  // vision-capable MoE scores figure IMAGES against figure-seeking
12
12
  // questions — one bounded call per figure question, ablatable stage
13
13
  vlmFigure: "@cf/deepseek-ai/deepseek-v4.1-flash-1017",
14
+ // the decision pass (TODO.decisions/01): clef-flash gates the
15
+ // requirement-shaped questions, clef decides the derived checklist
16
+ decisionsGate: "@cf/cloudflare/clef-flash",
17
+ decisionsModel: "@cf/cloudflare/clef",
14
18
  // hot-path understanding/summarize stays on the cheap Qwen (cost-first lane)
15
19
  understand: "@cf/qwen/qwen3-30b-a3b-fp8",
16
20
  // the api_call act's intent-to-act mapping (TODO.ai-platform/09) — the
@@ -90,6 +94,12 @@ export const THRESHOLDS = {
90
94
  // re-query runs at this depth and scopes in code (the native filter is
91
95
  // dead — measured 2026-10-05)
92
96
  denseStarveK: 100,
97
+ // the decision pass (TODO.decisions): the gate fires at decisionsGateMin
98
+ // noul; the bands assemble met / not clearly stated / not met
99
+ decisionsGateMin: 0.5,
100
+ decisionsMetBand: 0.8,
101
+ decisionsRefuteBand: 0.2,
102
+ decisionsRequirementsMax: 12,
93
103
  entailmentGate: true,
94
104
  entailmentBudgetMs: 6000,
95
105
  entailmentSupportedFloor: 0.9,
@@ -0,0 +1,124 @@
1
+ // The decision pass orchestrator (TODO.decisions/01): gate → derive →
2
+ // decide → hand back the items + the deterministic section. All I/O
3
+ // through the ports; the pure math lives in decisions.ts. Fired only
4
+ // when DECISIONS_ENABLED, not ablated, not a refusal, no verdict block
5
+ // bound, and the gate's noul clears decisionsGateMin.
6
+ import { portModelRunner } from "./env.ts";
7
+ import { MODELS, THRESHOLDS } from "./config.ts";
8
+ import {
9
+ buildDecisionQuestions, matchAnswers, parseDerivation,
10
+ assembleSection, type DecisionItem, type DecisionMode,
11
+ } from "./decisions.ts";
12
+ import type { Env } from "./env";
13
+
14
+ export interface DecisionPassResult {
15
+ mode: DecisionMode;
16
+ gate: number;
17
+ items: DecisionItem[];
18
+ section: string;
19
+ }
20
+
21
+ export function decisionsEnabled(env: Env): boolean {
22
+ return String((env as any).DECISIONS_ENABLED ?? "true") !== "false";
23
+ }
24
+
25
+ /** The gate: one clef-flash noul over the question — "is this a
26
+ * requirement-shaped question?" Cheap; prefetched in parallel with
27
+ * retrieval by the caller. Returns the probability, or null on any
28
+ * failure (fail-open: the pass just doesn't fire). */
29
+ export async function gateQuestion(env: Env, question: string): Promise<number | null> {
30
+ try {
31
+ const res = await portModelRunner(env).run({
32
+ model: (env as any).DECISIONS_GATE_MODEL || MODELS.decisionsGate,
33
+ input: {
34
+ state: question.slice(0, 500),
35
+ questions: {
36
+ requirements: {
37
+ type: "noul",
38
+ instructions: "Is this question asking whether requirements are met, or asking what requirements apply to something?",
39
+ criteria: {
40
+ true: "the asker wants requirements listed or their own values/products checked against requirements",
41
+ false: "a definitional, lookup, how-to, or conversational question",
42
+ },
43
+ },
44
+ },
45
+ },
46
+ maxTokens: 512,
47
+ });
48
+ const parsed = JSON.parse(res.text ?? "{}") as Record<string, any>;
49
+ const p = parsed?.requirements?.noul;
50
+ return typeof p === "number" ? p : null;
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+
56
+ /** The full pass: derive the checklist candidates from the used
57
+ * passages, decide them in one batched Clef call, assemble the
58
+ * deterministic section. Returns null when the pass stands down. */
59
+ export async function runDecisionPass(
60
+ env: Env,
61
+ opts: {
62
+ question: string;
63
+ gate: number;
64
+ passages: string[];
65
+ docidentifier: string | null;
66
+ statedValues?: string;
67
+ userDocs?: boolean;
68
+ },
69
+ ): Promise<DecisionPassResult | null> {
70
+ const gateMin = THRESHOLDS.decisionsGateMin;
71
+ if (opts.gate == null || opts.gate < gateMin) return null;
72
+ const passages = opts.passages.filter(Boolean).slice(0, 8).map((p) => p.slice(0, 1600));
73
+ if (!passages.length) return null;
74
+ try {
75
+ // 1. derive the checklist candidates from the used passages
76
+ const deriveModel = (env as any).DECISIONS_EXTRACT_MODEL || MODELS.member;
77
+ const derived = await portModelRunner(env).run({
78
+ model: deriveModel,
79
+ messages: [
80
+ { role: "system", content: "You extract requirement checklists from legal-metrology text. Reply with ONLY JSON, no prose." },
81
+ {
82
+ role: "user",
83
+ content:
84
+ `User question: ${opts.question}\n\n` +
85
+ (opts.statedValues ? `The user's stated values: ${opts.statedValues}\n\n` : "") +
86
+ `Passages:\n${passages.map((p, i) => `[${i + 1}] ${p}`).join("\n")}\n\n` +
87
+ `List the DISTINCT requirements relevant to the question as JSON: ` +
88
+ `{"mode":"check"|"list","requirements":[{"text":"...","clause":"..."}]} — ` +
89
+ `"check" when the user's question or values ask whether requirements are MET; ` +
90
+ `"list" when the user asks what the requirements ARE. Max ${THRESHOLDS.decisionsRequirementsMax}. ` +
91
+ `Each requirement's clause = the clause anchor it comes from. If nothing qualifies, return {"mode":"list","requirements":[]}.`,
92
+ },
93
+ ],
94
+ maxTokens: 1536,
95
+ temperature: 0,
96
+ });
97
+ const { mode, requirements } = parseDerivation(derived.text, THRESHOLDS.decisionsRequirementsMax);
98
+ if (!requirements.length) return null;
99
+
100
+ // 2. decide: one batched Clef call over the derived requirements
101
+ const decideModel = (env as any).DECISIONS_MODEL || MODELS.decisionsModel;
102
+ const decided = await portModelRunner(env).run({
103
+ model: decideModel,
104
+ input: {
105
+ state:
106
+ `User question: ${opts.question}\n` +
107
+ (opts.statedValues ? `Stated values: ${opts.statedValues}\n` : "") +
108
+ `Corpus passages:\n${passages.join("\n")}`.slice(0, 12000),
109
+ questions: buildDecisionQuestions(mode, requirements, opts.question, opts.statedValues ?? ""),
110
+ },
111
+ maxTokens: 4096,
112
+ });
113
+ const answers = JSON.parse(decided.text ?? "{}") as Record<string, any>;
114
+
115
+ // 3. band and assemble
116
+ const items = matchAnswers(mode, requirements, answers, THRESHOLDS.decisionsMetBand, THRESHOLDS.decisionsRefuteBand);
117
+ if (!items.length) return null;
118
+ const section = assembleSection(mode, items);
119
+ if (!section) return null;
120
+ return { mode, gate: opts.gate, items, section };
121
+ } catch {
122
+ return null; // additive — the essay answer ships
123
+ }
124
+ }
@@ -0,0 +1,148 @@
1
+ // The decision pass core (TODO.decisions/01): requirement-shaped
2
+ // questions are answered as CHECKLISTS, not essays. Pure and
3
+ // dependency-free — the I/O orchestrator (decisionWire.ts) calls these
4
+ // with the model outputs; everything here is deterministic and
5
+ // node-testable. Every failure of the live pass degrades to the essay
6
+ // answer; nothing in this module can break an ask.
7
+
8
+ export interface DerivedRequirement {
9
+ text: string;
10
+ clause: string;
11
+ }
12
+
13
+ export interface DecisionItem {
14
+ text: string;
15
+ clause: string;
16
+ band: "met" | "not-met" | "unclear";
17
+ probability: number;
18
+ }
19
+
20
+ export type DecisionMode = "check" | "list";
21
+
22
+ /** Parse the derivation model's reply: JSON with
23
+ * {mode: "check"|"list", requirements: [{text, clause}]}.
24
+ * Tolerant of prose around the JSON, of trailing commas-free objects,
25
+ * and of missing clauses (empty string). Returns [] when nothing
26
+ * sane survives — the pass then stands down. */
27
+ export function parseDerivation(text: string | null | undefined, cap: number): {
28
+ mode: DecisionMode;
29
+ requirements: DerivedRequirement[];
30
+ } {
31
+ const fallback = { mode: "list" as DecisionMode, requirements: [] as DerivedRequirement[] };
32
+ if (!text) return fallback;
33
+ const cleaned = text.replace(/```json|```/g, "").trim();
34
+ const start = cleaned.indexOf("{");
35
+ const end = cleaned.lastIndexOf("}");
36
+ if (start === -1 || end <= start) return fallback;
37
+ let parsed: any;
38
+ try {
39
+ parsed = JSON.parse(cleaned.slice(start, end + 1));
40
+ } catch {
41
+ return fallback;
42
+ }
43
+ const reqs = Array.isArray(parsed?.requirements) ? parsed.requirements : [];
44
+ const requirements: DerivedRequirement[] = [];
45
+ for (const r of reqs) {
46
+ const t = typeof r?.text === "string" ? r.text.trim() : "";
47
+ if (t.length < 8) continue;
48
+ requirements.push({ text: t.slice(0, 300), clause: typeof r?.clause === "string" ? r.clause.trim().slice(0, 40) : "" });
49
+ if (requirements.length >= cap) break;
50
+ }
51
+ if (!requirements.length) return fallback;
52
+ const mode: DecisionMode = parsed?.mode === "check" ? "check" : "list";
53
+ return { mode, requirements };
54
+ }
55
+
56
+ /** Build the Clef questions map from the derived requirements. In
57
+ * "check" mode each requirement becomes a noul (met-or-not given the
58
+ * user's stated values); in "list" mode a noul on whether the
59
+ * requirement is genuinely stated in the corpus text. */
60
+ export function buildDecisionQuestions(
61
+ mode: DecisionMode,
62
+ requirements: DerivedRequirement[],
63
+ userQuestion: string,
64
+ statedValues: string,
65
+ ): Record<string, { type: "noul"; instructions: string; criteria?: { true: string; false: string } }> {
66
+ const questions: Record<string, { type: "noul"; instructions: string; criteria?: { true: string; false: string } }> = {};
67
+ requirements.forEach((r, i) => {
68
+ const id = `r${i + 1}`;
69
+ if (mode === "check") {
70
+ questions[id] = {
71
+ type: "noul",
72
+ instructions:
73
+ `Requirement: "${r.text}" (clause ${r.clause || "n/a"}). ` +
74
+ `The user asks: "${userQuestion}". ` +
75
+ `Stated values: ${statedValues || "(none stated — judge whether the state text answers it)"}. ` +
76
+ `Is this requirement met, based only on what the state contains?`,
77
+ criteria: {
78
+ true: "the state shows the requirement is satisfied",
79
+ false: "the state shows it is not satisfied, or the state says nothing about it",
80
+ },
81
+ };
82
+ } else {
83
+ questions[id] = {
84
+ type: "noul",
85
+ instructions: `Is this a genuine, self-contained requirement statement: "${r.text}"?`,
86
+ criteria: {
87
+ true: "a real requirement a reader could check",
88
+ false: "a fragment, duplicate, or not a requirement",
89
+ },
90
+ };
91
+ }
92
+ });
93
+ return questions;
94
+ }
95
+
96
+ export function bandFor(p: number, metBand: number, refuteBand: number): DecisionItem["band"] {
97
+ if (p >= metBand) return "met";
98
+ if (p <= refuteBand) return "not-met";
99
+ return "unclear";
100
+ }
101
+
102
+ /** Match the Clef answers (keyed r1..rN) back onto the requirements and
103
+ * band them. Unmatched/missing answers drop (absence is honest). */
104
+ export function matchAnswers(
105
+ mode: DecisionMode,
106
+ requirements: DerivedRequirement[],
107
+ answers: Record<string, any>,
108
+ metBand: number,
109
+ refuteBand: number,
110
+ ): DecisionItem[] {
111
+ const items: DecisionItem[] = [];
112
+ requirements.forEach((r, i) => {
113
+ const a = answers[`r${i + 1}`];
114
+ let probability: number;
115
+ if (a && a.type === "noul" && typeof a.noul === "number") {
116
+ probability = a.noul;
117
+ } else if (a && typeof a === "number") {
118
+ probability = a;
119
+ } else {
120
+ return;
121
+ }
122
+ if (probability < 0 || probability > 1) return;
123
+ items.push({
124
+ text: r.text,
125
+ clause: r.clause,
126
+ band: mode === "check" ? bandFor(probability, metBand, refuteBand) : "met",
127
+ probability: Math.round(probability * 100) / 100,
128
+ });
129
+ });
130
+ return items;
131
+ }
132
+
133
+ const MARK: Record<DecisionItem["band"], string> = { met: "✓", "not-met": "✗", unclear: "?" };
134
+ const WORD: Record<DecisionItem["band"], string> = { met: "met", "not-met": "not met", unclear: "not clearly stated" };
135
+
136
+ /** The deterministic checklist section appended to the answer. The
137
+ * model never writes this — the server composes it from the decisions. */
138
+ export function assembleSection(mode: DecisionMode, items: DecisionItem[]): string {
139
+ if (!items.length) return "";
140
+ const title = mode === "check" ? "Requirements check (automated)" : "Requirements (from the cited clauses)";
141
+ const lines = items.map((it) => {
142
+ const mark = mode === "check" ? MARK[it.band] : "•";
143
+ const clause = it.clause ? ` — ${it.clause}` : "";
144
+ const prob = mode === "check" ? ` (${Math.round(it.probability * 100)}%)` : "";
145
+ return `- ${mark} ${it.text}${clause}${prob}${mode === "check" ? ` — ${WORD[it.band]}` : ""}`;
146
+ });
147
+ return `\n\n${title}:\n${lines.join("\n")}`;
148
+ }
@@ -101,7 +101,9 @@ export function cfModelRunner(ai: unknown): ModelRunner {
101
101
  return null;
102
102
  },
103
103
  async run(req: GenerateRequest): Promise<GenerateResult> {
104
- const res: any = await (ai as any).run(req.model, {
104
+ // the decision models' native shape ({state, questions}) passes
105
+ // through verbatim — the adapter adds no sampling shaping to it
106
+ const payload: any = req.input !== undefined ? req.input : {
105
107
  messages: req.messages,
106
108
  max_tokens: req.maxTokens,
107
109
  ...(req.effort ? { reasoning_effort: req.effort } : {}),
@@ -109,9 +111,15 @@ export function cfModelRunner(ai: unknown): ModelRunner {
109
111
  ...(req.topP != null ? { top_p: req.topP } : {}),
110
112
  ...(req.topK != null ? { top_k: req.topK } : {}),
111
113
  ...(req.stream ? { stream: true } : {}),
112
- });
114
+ };
115
+ const res: any = await (ai as any).run(req.model, payload);
113
116
  if (req.stream && res && typeof res.getReader === "function") return { text: null, stream: res };
114
117
  if (req.stream && res?.body && typeof res.body.getReader === "function") return { text: null, stream: res.body };
118
+ // the decision models answer with a typed answers object — the
119
+ // lossless form is its JSON, which the caller parses
120
+ if (res && typeof res === "object" && res.answers && typeof res.answers === "object") {
121
+ return { text: JSON.stringify(res.answers) };
122
+ }
115
123
  const text = typeof res?.response === "string" ? res.response : res?.choices?.[0]?.message?.content;
116
124
  return { text: typeof text === "string" ? text : null };
117
125
  },
@@ -6,7 +6,14 @@
6
6
  * multi-SDO / multi-cloud: docs/konneal-extraction-plan.md §5. */
7
7
  export interface GenerateRequest {
8
8
  model: string;
9
- messages: unknown[];
9
+ /** The messages payload — required unless `input` carries a native
10
+ * non-messages shape (the decision models). */
11
+ messages?: unknown[];
12
+ /** Native-shape passthrough for models whose contract is not
13
+ * messages-based (the decision models: {state, questions}). When
14
+ * present it REPLACES the messages payload verbatim — the adapter
15
+ * adds no sampling shaping. */
16
+ input?: unknown;
10
17
  effort?: string;
11
18
  maxTokens?: number;
12
19
  temperature?: number;
@@ -1,16 +0,0 @@
1
- import {
2
- handleAsk
3
- } from "./chunk-AGL4NGXY.js";
4
- import "./chunk-ECOYO7EX.js";
5
- import "./chunk-PTY2OSC4.js";
6
- import "./chunk-LIZJLRXZ.js";
7
- import "./chunk-LU7VIMD7.js";
8
- import "./chunk-QY556RJM.js";
9
- import "./chunk-ETLSWSZ7.js";
10
- import "./chunk-Y7LWT24U.js";
11
- import "./chunk-4FZNJR5Q.js";
12
- import "./chunk-3FYJM7LH.js";
13
- import "./chunk-LSZTJI7V.js";
14
- export {
15
- handleAsk
16
- };
@@ -1,14 +0,0 @@
1
- import {
2
- handleSearch
3
- } from "./chunk-BFOCWZ7U.js";
4
- import "./chunk-LIZJLRXZ.js";
5
- import "./chunk-LU7VIMD7.js";
6
- import "./chunk-QY556RJM.js";
7
- import "./chunk-ETLSWSZ7.js";
8
- import "./chunk-Y7LWT24U.js";
9
- import "./chunk-4FZNJR5Q.js";
10
- import "./chunk-3FYJM7LH.js";
11
- import "./chunk-LSZTJI7V.js";
12
- export {
13
- handleSearch
14
- };