@konneal/engine 0.2.14 → 0.2.16

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,17 @@
1
+ export interface AggregationVerdict {
2
+ operation: "count" | "min" | "max" | "lookup";
3
+ table: string;
4
+ table_title?: string;
5
+ column?: string;
6
+ value: number | string | null;
7
+ unit?: string;
8
+ row?: Record<string, string>;
9
+ note: string;
10
+ }
11
+ /** Evaluate the candidate table nodes against the question's
12
+ * aggregation intent. One verdict: the operation the question names,
13
+ * computed over the best-matching table's typed payload. */
14
+ export declare function evaluateAggregation(nodes: {
15
+ node_id: string;
16
+ content: unknown;
17
+ }[], query: string): AggregationVerdict | null;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  handleAsk
3
- } from "./chunk-B4MH5IR2.js";
3
+ } from "./chunk-XHJI73N2.js";
4
4
  import "./chunk-7UT76RQO.js";
5
5
  import "./chunk-EFQALN2Z.js";
6
6
  import "./chunk-DBBGOOMZ.js";
@@ -128,7 +128,10 @@ async function scoreJudge(ai, model, systemPrompt, userPrompt) {
128
128
  { role: "user", content: userPrompt }
129
129
  ],
130
130
  max_tokens: 6144,
131
- reasoning_effort: "low"
131
+ reasoning_effort: "low",
132
+ // the judge is a measurement: greedy decoding, no sampling
133
+ temperature: 0,
134
+ top_p: 1
132
135
  });
133
136
  const text = typeof res?.response === "string" ? res.response : res?.choices?.[0]?.message?.content;
134
137
  let score = null;
@@ -586,17 +589,17 @@ function parseAndEval(src, params) {
586
589
  const r = add();
587
590
  switch (t.v) {
588
591
  case ">=":
589
- return num2(l) >= num2(r);
592
+ return num3(l) >= num3(r);
590
593
  case "<=":
591
- return num2(l) <= num2(r);
594
+ return num3(l) <= num3(r);
592
595
  case ">":
593
- return num2(l) > num2(r);
596
+ return num3(l) > num3(r);
594
597
  case "<":
595
- return num2(l) < num2(r);
598
+ return num3(l) < num3(r);
596
599
  case "==":
597
- return num2(l) === num2(r);
600
+ return num3(l) === num3(r);
598
601
  default:
599
- return num2(l) !== num2(r);
602
+ return num3(l) !== num3(r);
600
603
  }
601
604
  }
602
605
  return l;
@@ -608,7 +611,7 @@ function parseAndEval(src, params) {
608
611
  if (t && t.t === "op" && (t.v === "+" || t.v === "-")) {
609
612
  p++;
610
613
  const r = mul();
611
- l = t.v === "+" ? num2(l) + num2(r) : num2(l) - num2(r);
614
+ l = t.v === "+" ? num3(l) + num3(r) : num3(l) - num3(r);
612
615
  } else return l;
613
616
  }
614
617
  }
@@ -619,7 +622,7 @@ function parseAndEval(src, params) {
619
622
  if (t && t.t === "op" && (t.v === "*" || t.v === "/")) {
620
623
  p++;
621
624
  const r = unary();
622
- l = t.v === "*" ? num2(l) * num2(r) : num2(l) / num2(r);
625
+ l = t.v === "*" ? num3(l) * num3(r) : num3(l) / num3(r);
623
626
  } else return l;
624
627
  }
625
628
  }
@@ -627,7 +630,7 @@ function parseAndEval(src, params) {
627
630
  const t = peek();
628
631
  if (t && t.t === "op" && t.v === "-") {
629
632
  p++;
630
- return -num2(unary());
633
+ return -num3(unary());
631
634
  }
632
635
  return atom();
633
636
  }
@@ -647,7 +650,7 @@ function parseAndEval(src, params) {
647
650
  throw new Error(`unexpected ${t.v}`);
648
651
  }
649
652
  const truthy = (v) => typeof v === "boolean" ? v : v !== 0;
650
- const num2 = (v) => typeof v === "boolean" ? v ? 1 : 0 : v;
653
+ const num3 = (v) => typeof v === "boolean" ? v ? 1 : 0 : v;
651
654
  const out = or();
652
655
  if (p !== toks.length) throw new Error("trailing tokens");
653
656
  return out;
@@ -763,20 +766,20 @@ function verdictNote(v, node) {
763
766
  var NUM = String.raw`-?\d+(?:[.,]\d+)?`;
764
767
  function quantitiesIn(query) {
765
768
  const out = {};
766
- const num2 = (s) => Number(s.replace(",", "."));
769
+ const num3 = (s) => Number(s.replace(",", "."));
767
770
  const put = (kind, stated, stated_unit, si) => {
768
771
  if (Number.isFinite(si)) out[kind] = { stated, stated_unit, si };
769
772
  };
770
773
  const tempC = query.match(new RegExp(`(${NUM})\\s*(?:\xB0\\s*)?C\\b`));
771
- if (tempC) put("temperature", num2(tempC[1]), "degC", num2(tempC[1]) + 273.15);
774
+ if (tempC) put("temperature", num3(tempC[1]), "degC", num3(tempC[1]) + 273.15);
772
775
  const tempK = query.match(new RegExp(`(${NUM})\\s*K\\b`));
773
- if (tempK && out.temperature === void 0) put("temperature", num2(tempK[1]), "K", num2(tempK[1]));
776
+ if (tempK && out.temperature === void 0) put("temperature", num3(tempK[1]), "K", num3(tempK[1]));
774
777
  const rh = query.match(new RegExp(`(${NUM})\\s*%\\s*(?:RH\\b|relative\\s+humidity)?`, "i"));
775
- if (rh) put("relative_humidity", num2(rh[1]), "%", num2(rh[1]) / 100);
778
+ if (rh) put("relative_humidity", num3(rh[1]), "%", num3(rh[1]) / 100);
776
779
  const hours = query.match(new RegExp(`(${NUM})\\s*h\\b`, "i"));
777
- if (hours) put("duration", num2(hours[1]), "h", num2(hours[1]) * 3600);
780
+ if (hours) put("duration", num3(hours[1]), "h", num3(hours[1]) * 3600);
778
781
  const days = query.match(new RegExp(`(${NUM})\\s*days?\\b`, "i"));
779
- if (days && out.duration === void 0) put("duration", num2(days[1]), "d", num2(days[1]) * 86400);
782
+ if (days && out.duration === void 0) put("duration", num3(days[1]), "d", num3(days[1]) * 86400);
780
783
  return out;
781
784
  }
782
785
  function scoreSet(entries, q) {
@@ -837,6 +840,217 @@ function evaluateConditionSets(nodes, query) {
837
840
  };
838
841
  }
839
842
 
843
+ // workers/worker_public/src/aggregation.ts
844
+ var NUM2 = String.raw`-?\d+(?:[,\s]?\d{3})*(?:[.,]\d+)?`;
845
+ var UNIT_WORDS = {
846
+ kg: ["mass", "load", "weight"],
847
+ s: ["time", "duration", "second"],
848
+ "km/h": ["speed", "velocity"],
849
+ v: ["load"],
850
+ degC: ["temperature"],
851
+ ppm: ["range", "fraction"]
852
+ };
853
+ function num2(v) {
854
+ if (v === null || v === void 0) return null;
855
+ let s = String(v).trim().replace(/\s/g, "");
856
+ if (!s || /^null$/i.test(s)) return null;
857
+ if (/^\d{1,3}(,\d{3})+([.,]\d+)?$/.test(s)) s = s.replace(/,/g, "");
858
+ else s = s.replace(",", ".");
859
+ const n = Number(s);
860
+ return Number.isFinite(n) ? n : null;
861
+ }
862
+ function tokens(name) {
863
+ return name.toLowerCase().split("_").filter(Boolean);
864
+ }
865
+ function hasWord(queryLower, w) {
866
+ return new RegExp(`\\b${w.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&")}\\b`, "i").test(queryLower);
867
+ }
868
+ function columnScore(col, queryLower) {
869
+ let score = 0;
870
+ for (const t of tokens(col.name)) {
871
+ if (["min", "max", "gt", "of"].includes(t)) continue;
872
+ if (t.length >= 3 && queryLower.includes(t)) score += 1;
873
+ }
874
+ const hints = col.unit ? UNIT_WORDS[col.unit] : void 0;
875
+ if (hints?.some((h) => hasWord(queryLower, h))) score += 1;
876
+ return score;
877
+ }
878
+ function classColumn(cols) {
879
+ return cols.find((c) => c.name === "accuracy_class" || c.name === "metrological_class" || c.name.endsWith("_class"));
880
+ }
881
+ function classToken(query) {
882
+ const m = query.match(/\bclass\s+([a-z0-9.]+)\b/i);
883
+ return m ? m[1].toLowerCase() : null;
884
+ }
885
+ function classAsColumn(cols, token) {
886
+ const exact = cols.find((c) => /^class_[a-z0-9.]+$/.test(c.name) && c.name.slice(6) === token);
887
+ if (exact) return exact;
888
+ return cols.find((c) => /^class_[a-z0-9.]+$/.test(c.name) && c.name.slice(6) === "cd" && (token === "c" || token === "d"));
889
+ }
890
+ function numericColumns(cols) {
891
+ return cols.filter((c) => c.type === "number" || c.type === "integer" || /^class_[a-z0-9.]+$/.test(c.name));
892
+ }
893
+ function intervalPairs(cols) {
894
+ const byName = new Map(cols.map((c) => [c.name, c]));
895
+ const pairs = [];
896
+ for (const c of cols) {
897
+ for (const [suffix, exclusive] of [["min", false], ["gt", true]]) {
898
+ if (!c.name.endsWith(`_${suffix}`)) continue;
899
+ const high = byName.get(`${c.name.slice(0, -suffix.length)}max`);
900
+ if (high && high.unit === c.unit) pairs.push({ low: c, high, exclusiveLow: exclusive });
901
+ }
902
+ }
903
+ return pairs;
904
+ }
905
+ function statedInInterval(query, unit) {
906
+ if (!unit) return null;
907
+ const re = new RegExp(`(${NUM2})\\s*${unit.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&")}\\b`, "i");
908
+ const m = query.match(re);
909
+ return m ? num2(m[1]) : null;
910
+ }
911
+ function selectRow(rows, cols, pair, stated, filterColumn, filterValue) {
912
+ const lowIdx = cols.indexOf(pair.low);
913
+ const highIdx = cols.indexOf(pair.high);
914
+ for (let i = 0; i < rows.length; i++) {
915
+ const row = rows[i];
916
+ if (filterColumn && filterValue !== null) {
917
+ const fIdx = cols.indexOf(filterColumn);
918
+ if (fIdx < 0 || String(row[fIdx] ?? "").trim().toLowerCase() !== filterValue) continue;
919
+ }
920
+ const low = num2(row[lowIdx]);
921
+ const high = num2(row[highIdx]);
922
+ const aboveLow = low === null || (pair.exclusiveLow ? stated > low : stated >= low);
923
+ const belowHigh = high === null || stated <= high;
924
+ if (aboveLow && belowHigh) return { row, index: i };
925
+ }
926
+ return null;
927
+ }
928
+ function cellValue(row, cols, col) {
929
+ const idx = cols.indexOf(col);
930
+ if (idx < 0) return null;
931
+ const raw = row[idx] ?? "";
932
+ return num2(raw) ?? String(raw).trim();
933
+ }
934
+ function pickTable(nodes, queryLower) {
935
+ let best = null;
936
+ for (const n of nodes) {
937
+ const c = n.content ?? {};
938
+ const payload = c.payload ?? {};
939
+ if (!Array.isArray(payload.rows) || !payload.rows.length) continue;
940
+ let score = 0;
941
+ for (const t of tokens(n.node_id.replace("/table/", ""))) {
942
+ if (t.length >= 3 && queryLower.includes(t)) score += 2;
943
+ }
944
+ for (const w of String(c.name ?? "").toLowerCase().split(/[^a-z0-9.]+/)) {
945
+ if (w.length >= 4 && queryLower.includes(w)) score += 1;
946
+ }
947
+ if (!best || score > best.score) best = { node: n, score };
948
+ }
949
+ if (best && best.score > 0) return best.node;
950
+ return nodes.length === 1 ? nodes[0] : null;
951
+ }
952
+ function evaluateAggregation(nodes, query) {
953
+ const qLower = query.toLowerCase();
954
+ const operation = /\bhow many\b|\bnumber of\b/.test(qLower) ? "count" : /\b(minimum|smallest|shortest|lowest|least)\b/.test(qLower) ? "min" : /\b(maximum|largest|longest|highest|greatest)\b/.test(qLower) ? "max" : "lookup";
955
+ if (!nodes.length) return null;
956
+ const node = pickTable(nodes, qLower);
957
+ if (!node) return null;
958
+ const content = node.content && typeof node.content === "object" ? node.content : {};
959
+ const payload = content.payload ?? {};
960
+ const cols = Array.isArray(payload.columns) ? payload.columns : [];
961
+ const rows = (Array.isArray(payload.rows) ? payload.rows : []).filter((r) => Array.isArray(r));
962
+ if (!cols.length || !rows.length) return null;
963
+ const tableTitle = String(content.name ?? content.definition ?? node.node_id.replace("/table/", ""));
964
+ const cite = (what) => `COMPUTED (${operation}) \u2014 ${what}, read from the typed table "${tableTitle}" (${node.node_id}). Present this result and cite the table's clause; the value is machine-computed from the table payload, do not recompute or round it differently.`;
965
+ if (operation === "count") {
966
+ const cc2 = classColumn(cols);
967
+ if (cc2 && /\bclasses?\b/.test(qLower) && tokens(cc2.name).some((t) => t.length >= 3 && qLower.includes(t))) {
968
+ const idx = cols.indexOf(cc2);
969
+ const distinct = new Set(rows.map((r) => String(r[idx] ?? "").trim().toLowerCase()));
970
+ return {
971
+ operation,
972
+ table: node.node_id,
973
+ table_title: tableTitle,
974
+ column: cc2.name,
975
+ value: distinct.size,
976
+ note: cite(`the table defines ${distinct.size} distinct ${cc2.name.replace("_", " ")} values`)
977
+ };
978
+ }
979
+ return {
980
+ operation,
981
+ table: node.node_id,
982
+ table_title: tableTitle,
983
+ value: rows.length,
984
+ note: cite(`the table has ${rows.length} rows`)
985
+ };
986
+ }
987
+ const cToken = classToken(query);
988
+ if (operation === "min" || operation === "max") {
989
+ let col = cToken ? classAsColumn(cols, cToken) : void 0;
990
+ if (!col) {
991
+ let best = null;
992
+ for (const c of numericColumns(cols)) {
993
+ const s = columnScore(c, qLower);
994
+ if (s > 0 && (!best || s > best.score)) best = { col: c, score: s };
995
+ }
996
+ col = best?.col;
997
+ }
998
+ if (!col) return null;
999
+ const values = rows.map((r) => num2(r[cols.indexOf(col)])).filter((v) => v !== null);
1000
+ if (!values.length) return null;
1001
+ const value = operation === "min" ? Math.min(...values) : Math.max(...values);
1002
+ return {
1003
+ operation,
1004
+ table: node.node_id,
1005
+ table_title: tableTitle,
1006
+ column: col.name,
1007
+ value,
1008
+ unit: col.unit,
1009
+ note: cite(`${operation} of ${col.name.replace(/_/g, " ")} across ${values.length} rows is ${value}${col.unit ? ` ${col.unit}` : ""}`)
1010
+ };
1011
+ }
1012
+ const pairs = intervalPairs(cols);
1013
+ const cc = classColumn(cols);
1014
+ const filterColumn = cc && cToken ? cc : void 0;
1015
+ let matched = null;
1016
+ let pairUsed = null;
1017
+ let stated = null;
1018
+ for (const pair of pairs) {
1019
+ const v = statedInInterval(query, pair.low.unit);
1020
+ if (v === null) continue;
1021
+ const r = selectRow(rows, cols, pair, v, filterColumn, cToken);
1022
+ if (r) {
1023
+ matched = r;
1024
+ pairUsed = pair;
1025
+ stated = v;
1026
+ break;
1027
+ }
1028
+ }
1029
+ if (matched && pairUsed && stated !== null) {
1030
+ const returnCol = cToken ? classAsColumn(cols, cToken) : void 0;
1031
+ const valueCols = cols.filter(
1032
+ (c) => c !== pairUsed.low && c !== pairUsed.high && c !== filterColumn && numericColumns(cols).includes(c)
1033
+ );
1034
+ const col = returnCol ?? (valueCols.length === 1 ? valueCols[0] : void 0);
1035
+ const value = col ? cellValue(matched.row, cols, col) : null;
1036
+ const rowObj = {};
1037
+ cols.forEach((c, i) => rowObj[c.name] = String(matched.row[i] ?? "").trim());
1038
+ return {
1039
+ operation: "lookup",
1040
+ table: node.node_id,
1041
+ table_title: tableTitle,
1042
+ column: col?.name,
1043
+ value,
1044
+ unit: col?.unit,
1045
+ row: rowObj,
1046
+ note: cite(
1047
+ `the stated ${stated} ${pairUsed.low.unit ?? ""} falls in the row ${pairUsed.low.name} ${matched.row[cols.indexOf(pairUsed.low)]} / ${pairUsed.high.name} ${matched.row[cols.indexOf(pairUsed.high)]}${cToken ? `, ${filterColumn ? filterColumn.name.replace("_", " ") : "class"} ${cToken}` : ""}`
1048
+ )
1049
+ };
1050
+ }
1051
+ return null;
1052
+ }
1053
+
840
1054
  // workers/worker_public/src/drafts.ts
841
1055
  var ACT_VERB = "(?:draft|prepare|pre-?fill|fill\\s+(?:in|out)|start|submit|file|lodge)";
842
1056
  var ACT_TARGET = "(?:new\\s+)?(?:certification\\s+|type[ -]evaluation\\s+|OIML[- ]CS\\s+)?application";
@@ -963,16 +1177,16 @@ async function resolveStandard(env, named) {
963
1177
  const m = named.match(/^urn:oiml:pub:([rdbge]):(\d{1,3})(?:-[0-9A-Za-z]+)?(?::(\d{4}))?$/i) ?? named.match(/^(?:OIML\s+)?([RDBGE])\s*(\d{1,3})(?:-[0-9A-Za-z]+)?(?:\s*:\s*(\d{4}))?$/i);
964
1178
  if (!m) return null;
965
1179
  const type = m[1].toUpperCase();
966
- const num2 = String(Number(m[2]));
1180
+ const num3 = String(Number(m[2]));
967
1181
  try {
968
1182
  const row = await env.DB.prepare(
969
1183
  "SELECT docidentifier, edition, status, derived_status FROM documents WHERE family = ?1 AND active = 1 ORDER BY (part IS NULL) DESC, edition DESC LIMIT 1"
970
- ).bind(`${type}-${num2}`).first();
1184
+ ).bind(`${type}-${num3}`).first();
971
1185
  if (!row) return null;
972
1186
  const edition = typeof row.edition === "string" ? row.edition : void 0;
973
1187
  return {
974
- urn: `urn:oiml:pub:${type.toLowerCase()}:${num2}${edition ? `:${edition}` : ""}`,
975
- label: typeof row.docidentifier === "string" ? row.docidentifier : `OIML ${type} ${num2}`,
1188
+ urn: `urn:oiml:pub:${type.toLowerCase()}:${num3}${edition ? `:${edition}` : ""}`,
1189
+ label: typeof row.docidentifier === "string" ? row.docidentifier : `OIML ${type} ${num3}`,
976
1190
  ...edition ? { edition } : {},
977
1191
  status: typeof row.derived_status === "string" ? row.derived_status : typeof row.status === "string" ? row.status : void 0
978
1192
  };
@@ -1684,6 +1898,41 @@ ${summary}` }] : [],
1684
1898
  }
1685
1899
  } : null;
1686
1900
  if (conditionVerdict) console.log("condition engine:", conditionVerdict.matched.join("|") || conditionVerdict.nearest.node_id, "\u2192", conditionVerdict.verdict.toUpperCase());
1901
+ let aggregationVerdict = null;
1902
+ let aggregationStandard = null;
1903
+ if (!machineVerdict && !boundModel && !conditionVerdict && P().publisher.features?.model_plane) {
1904
+ const docNum = modelDocHint?.doc_number;
1905
+ const sql = docNum ? "SELECT standard, node_id, content FROM model_nodes WHERE kind = 'table' AND standard LIKE '%' || ?1" : "SELECT standard, node_id, content FROM model_nodes WHERE kind = 'table'";
1906
+ const stmt = docNum ? env.DB.prepare(sql).bind(docNum) : env.DB.prepare(sql);
1907
+ const rows = await stmt.all().catch(() => ({ results: [] }));
1908
+ const candidates = (rows.results ?? []).filter((r) => {
1909
+ const entry = licensedEntryForPackage(String(r.standard));
1910
+ return !entry || (standardKeys?.has(entry.key) ?? false);
1911
+ });
1912
+ const v = evaluateAggregation(
1913
+ candidates.map((r) => ({ node_id: String(r.node_id), content: JSON.parse(String(r.content ?? "{}")) })),
1914
+ q.query
1915
+ );
1916
+ if (v) {
1917
+ aggregationVerdict = v;
1918
+ aggregationStandard = String(rows.results?.[0]?.standard ?? "");
1919
+ }
1920
+ }
1921
+ const aggregationNote = aggregationVerdict ? `${aggregationVerdict.note}${aggregationStandard ? ` (standard ${aggregationStandard}.)` : ""}` : void 0;
1922
+ const aggregationBlock = aggregationVerdict ? {
1923
+ unit_id: aggregationVerdict.table,
1924
+ type: "verdict",
1925
+ docidentifier: `SMART model table${aggregationStandard ? ` (${aggregationStandard})` : ""}`,
1926
+ payload: {
1927
+ check: `${aggregationVerdict.operation}: ${aggregationVerdict.column ?? aggregationVerdict.table_title ?? aggregationVerdict.table} = ${aggregationVerdict.value}${aggregationVerdict.unit ? ` ${aggregationVerdict.unit}` : ""}`,
1928
+ meaning: aggregationVerdict.table_title,
1929
+ operation: aggregationVerdict.operation,
1930
+ value: aggregationVerdict.value,
1931
+ unit: aggregationVerdict.unit,
1932
+ row: aggregationVerdict.row
1933
+ }
1934
+ } : null;
1935
+ if (aggregationVerdict) console.log("aggregation engine:", aggregationVerdict.operation, aggregationVerdict.table, "\u2192", aggregationVerdict.value);
1687
1936
  try {
1688
1937
  const tR = Date.now();
1689
1938
  if (declaredCtx?.kind === "account") {
@@ -1777,7 +2026,7 @@ Answer account questions from these records ONLY: name the record when you use i
1777
2026
  q.lang,
1778
2027
  keptHistory,
1779
2028
  // stage-extracted graph facts (GraphRAG) ride the same note channel
1780
- [processNote, eNote, contextNote(declaredCtx, docScope), accountNote, modelNote, vocabNote, memNote, machineNote, conditionNote, licenseNote, ...retrieved.notes ?? []].filter(Boolean).join("\n") || void 0,
2029
+ [processNote, eNote, contextNote(declaredCtx, docScope), accountNote, modelNote, vocabNote, memNote, machineNote, conditionNote, aggregationNote, licenseNote, ...retrieved.notes ?? []].filter(Boolean).join("\n") || void 0,
1781
2030
  summary,
1782
2031
  budget
1783
2032
  );
@@ -1825,7 +2074,7 @@ Answer account questions from these records ONLY: name the record when you use i
1825
2074
  model,
1826
2075
  query_hash: queryHash,
1827
2076
  follow_ups: understanding?.follow_ups ?? [],
1828
- blocks: [...c2.blocks, ...verdictBlock ? [verdictBlock] : [], ...conditionBlock ? [conditionBlock] : []],
2077
+ blocks: [...c2.blocks, ...verdictBlock ? [verdictBlock] : [], ...conditionBlock ? [conditionBlock] : [], ...aggregationBlock ? [aggregationBlock] : []],
1829
2078
  context_applied: ctxApplied,
1830
2079
  read: readAs(),
1831
2080
  // the evidence view's ground truth: the exact passages this
@@ -1942,7 +2191,7 @@ Answer account questions from these records ONLY: name the record when you use i
1942
2191
  if (completionBlocks.length) console.log("contract completion:", completionBlocks.length, "table block(s) attached server-side");
1943
2192
  }
1944
2193
  completionBlocks.push(...await completeFigures(env.DB, answer, [...c2ns.blocks, ...completionBlocks], used));
1945
- const out = { answer, citations: finalCites, model: MODELS.member, query_hash: queryHash, follow_ups: understanding?.follow_ups ?? [], blocks: [...c2ns.blocks, ...verdictBlock ? [verdictBlock] : [], ...conditionBlock ? [conditionBlock] : [], ...completionBlocks], context_applied: ctxApplied, ...liveRecords ? { records: liveRecords } : {} };
2194
+ const out = { answer, citations: finalCites, model: MODELS.member, query_hash: queryHash, follow_ups: understanding?.follow_ups ?? [], blocks: [...c2ns.blocks, ...verdictBlock ? [verdictBlock] : [], ...conditionBlock ? [conditionBlock] : [], ...aggregationBlock ? [aggregationBlock] : [], ...completionBlocks], context_applied: ctxApplied, ...liveRecords ? { records: liveRecords } : {} };
1946
2195
  const cacheable = !contextual && !declaredCtx && !answer.includes(refusalAnswer()) && finalAnchors.violations.length === 0;
1947
2196
  if (cacheable) {
1948
2197
  const warmVec = await warmEmbed ?? null;
@@ -894,7 +894,7 @@ export interface components {
894
894
  /** Format: uri */
895
895
  url?: string;
896
896
  }[];
897
- /** @description The typed objects the answer carries: tables, figures, condition-set verdicts, conformance verdicts and unit blocks. A verdict block's payload carries the machine evaluation — the verdict (pass, fail or void), one check per machine rule with its expression, the values bound from the question, and the individual result — plus, for condition-set membership, the matched severity set or the nearest set with the violated bands. The verdict is computed server-side; clients render it as data. */
897
+ /** @description The typed objects the answer carries: tables, figures, condition-set verdicts, conformance verdicts and unit blocks. A verdict block's payload carries the machine evaluation — the verdict (pass, fail or void), one check per machine rule with its expression, the values bound from the question, and the individual result — plus, for condition-set membership, the matched severity set or the nearest set with the violated bands; aggregation verdicts carry the machine-computed count, minimum, maximum or interval lookup over a typed table payload. The verdict is computed server-side; clients render it as data. */
898
898
  blocks?: Record<string, never>[];
899
899
  /** @description The model that generated the answer. */
900
900
  model?: string;
@@ -7,7 +7,7 @@ import {
7
7
  handleMemories,
8
8
  scoreJudge,
9
9
  standardForDocNumber
10
- } from "../../chunk-B4MH5IR2.js";
10
+ } from "../../chunk-XHJI73N2.js";
11
11
  import {
12
12
  buildMessages,
13
13
  citations,
@@ -979,7 +979,7 @@ async function handleMcp(env, ctx, req, tier, key) {
979
979
  // stream:false forces the JSON lane (anon defaults to SSE)
980
980
  body: JSON.stringify({ ...args, stream: false })
981
981
  });
982
- const res = name === "ask" ? await (await import("../../ask-J3VYAEMM.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-RFKT7ZZY.js")).handleSearch(env, ctx, inner, tier, key);
982
+ const res = name === "ask" ? await (await import("../../ask-MMJHOZ6Y.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-RFKT7ZZY.js")).handleSearch(env, ctx, inner, tier, key);
983
983
  return res.json().catch(() => ({ error: { message: "tool transport failed", status: res.status } }));
984
984
  });
985
985
  if (out.ok && "accepted" in out) return new Response(null, { status: 202 });
package/package.json CHANGED
@@ -34,7 +34,7 @@
34
34
  "@oimlsmart/oiml-pubid": "^1.2.1"
35
35
  },
36
36
  "dependencies": {},
37
- "version": "0.2.14",
37
+ "version": "0.2.16",
38
38
  "description": "The Konneal engine: the publisher-agnostic build pipeline and API plane for standards intelligence (retrieval, answer contract, verdicts, evaluation).",
39
39
  "license": "BSD-3-Clause",
40
40
  "type": "module",
@@ -1183,8 +1183,10 @@ components:
1183
1183
  with its expression, the values bound from the question, and
1184
1184
  the individual result — plus, for condition-set membership,
1185
1185
  the matched severity set or the nearest set with the violated
1186
- bands. The verdict is computed server-side; clients render it
1187
- as data.
1186
+ bands; aggregation verdicts carry the machine-computed
1187
+ count, minimum, maximum or interval lookup over a typed
1188
+ table payload. The verdict is computed server-side; clients
1189
+ render it as data.
1188
1190
  items:
1189
1191
  type: object
1190
1192
  model:
@@ -0,0 +1,288 @@
1
+ // Table-payload aggregation (konneal/engine#91): count / min / max /
2
+ // interval lookup executed over the typed table nodes' payloads —
3
+ // deterministic arithmetic where the answer IS the table's content.
4
+ // The model narrates the computed value; it never computes. Interval
5
+ // rows follow the standards' own convention: a lower `*_gt` bound is
6
+ // exclusive, `*_min` inclusive, `*_max` inclusive, `null` the open top.
7
+
8
+ export interface AggregationVerdict {
9
+ operation: "count" | "min" | "max" | "lookup";
10
+ table: string;
11
+ table_title?: string;
12
+ column?: string;
13
+ value: number | string | null;
14
+ unit?: string;
15
+ row?: Record<string, string>;
16
+ note: string;
17
+ }
18
+
19
+ interface Column {
20
+ name: string;
21
+ type?: string;
22
+ unit?: string;
23
+ }
24
+
25
+ const NUM = String.raw`-?\d+(?:[,\s]?\d{3})*(?:[.,]\d+)?`;
26
+
27
+ const UNIT_WORDS: Record<string, string[]> = {
28
+ kg: ["mass", "load", "weight"],
29
+ s: ["time", "duration", "second"],
30
+ "km/h": ["speed", "velocity"],
31
+ v: ["load"],
32
+ degC: ["temperature"],
33
+ ppm: ["range", "fraction"],
34
+ };
35
+
36
+ function num(v: unknown): number | null {
37
+ if (v === null || v === undefined) return null;
38
+ let s = String(v).trim().replace(/\s/g, "");
39
+ if (!s || /^null$/i.test(s)) return null;
40
+ // "1,000" is grouping, "0,5" is a decimal comma
41
+ if (/^\d{1,3}(,\d{3})+([.,]\d+)?$/.test(s)) s = s.replace(/,/g, "");
42
+ else s = s.replace(",", ".");
43
+ const n = Number(s);
44
+ return Number.isFinite(n) ? n : null;
45
+ }
46
+
47
+ function tokens(name: string): string[] {
48
+ return name.toLowerCase().split("_").filter(Boolean);
49
+ }
50
+
51
+ function hasWord(queryLower: string, w: string): boolean {
52
+ return new RegExp(`\\b${w.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&")}\\b`, "i").test(queryLower);
53
+ }
54
+
55
+ function columnScore(col: Column, queryLower: string): number {
56
+ let score = 0;
57
+ for (const t of tokens(col.name)) {
58
+ if (["min", "max", "gt", "of"].includes(t)) continue;
59
+ if (t.length >= 3 && queryLower.includes(t)) score += 1;
60
+ }
61
+ const hints = col.unit ? UNIT_WORDS[col.unit] : undefined;
62
+ if (hints?.some((h) => hasWord(queryLower, h))) score += 1;
63
+ return score;
64
+ }
65
+
66
+ function classColumn(cols: Column[]): Column | undefined {
67
+ return cols.find((c) => c.name === "accuracy_class" || c.name === "metrological_class" || c.name.endsWith("_class"));
68
+ }
69
+
70
+ /** "class C", "class cd" → the class id the question names. */
71
+ function classToken(query: string): string | null {
72
+ const m = query.match(/\bclass\s+([a-z0-9.]+)\b/i);
73
+ return m ? m[1]!.toLowerCase() : null;
74
+ }
75
+
76
+ /** The class columns shaped `class_a` / `class_cd` — classes as COLUMNS
77
+ * (R 60's stabilisation table) rather than as row values. */
78
+ function classAsColumn(cols: Column[], token: string): Column | undefined {
79
+ const exact = cols.find((c) => /^class_[a-z0-9.]+$/.test(c.name) && c.name.slice(6) === token);
80
+ if (exact) return exact;
81
+ // classes C and D share one column (class_cd) in R 60-2 Table 1
82
+ return cols.find((c) => /^class_[a-z0-9.]+$/.test(c.name) && c.name.slice(6) === "cd" && (token === "c" || token === "d"));
83
+ }
84
+
85
+ function numericColumns(cols: Column[]): Column[] {
86
+ return cols.filter((c) => c.type === "number" || c.type === "integer" || /^class_[a-z0-9.]+$/.test(c.name));
87
+ }
88
+
89
+ /** Interval pairs sharing a prefix: (a_min, a_max) inclusive-low,
90
+ * (a_gt, a_max) exclusive-low, `null` the open end. */
91
+ function intervalPairs(cols: Column[]): { low: Column; high: Column; exclusiveLow: boolean }[] {
92
+ const byName = new Map(cols.map((c) => [c.name, c]));
93
+ const pairs: { low: Column; high: Column; exclusiveLow: boolean }[] = [];
94
+ for (const c of cols) {
95
+ for (const [suffix, exclusive] of [["min", false], ["gt", true]] as const) {
96
+ if (!c.name.endsWith(`_${suffix}`)) continue;
97
+ const high = byName.get(`${c.name.slice(0, -suffix.length)}max`);
98
+ if (high && high.unit === c.unit) pairs.push({ low: c, high, exclusiveLow: exclusive });
99
+ }
100
+ }
101
+ return pairs;
102
+ }
103
+
104
+ function statedInInterval(query: string, unit: string | undefined): number | null {
105
+ if (!unit) return null;
106
+ const re = new RegExp(`(${NUM})\\s*${unit.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&")}\\b`, "i");
107
+ const m = query.match(re);
108
+ return m ? num(m[1]) : null;
109
+ }
110
+
111
+ function selectRow(
112
+ rows: string[][],
113
+ cols: Column[],
114
+ pair: { low: Column; high: Column; exclusiveLow: boolean },
115
+ stated: number,
116
+ filterColumn: Column | undefined,
117
+ filterValue: string | null,
118
+ ): { row: string[]; index: number } | null {
119
+ const lowIdx = cols.indexOf(pair.low);
120
+ const highIdx = cols.indexOf(pair.high);
121
+ for (let i = 0; i < rows.length; i++) {
122
+ const row = rows[i]!;
123
+ if (filterColumn && filterValue !== null) {
124
+ const fIdx = cols.indexOf(filterColumn);
125
+ if (fIdx < 0 || String(row[fIdx] ?? "").trim().toLowerCase() !== filterValue) continue;
126
+ }
127
+ const low = num(row[lowIdx]);
128
+ const high = num(row[highIdx]);
129
+ const aboveLow = low === null || (pair.exclusiveLow ? stated > low : stated >= low);
130
+ const belowHigh = high === null || stated <= high;
131
+ if (aboveLow && belowHigh) return { row, index: i };
132
+ }
133
+ return null;
134
+ }
135
+
136
+ function cellValue(row: string[], cols: Column[], col: Column): number | string | null {
137
+ const idx = cols.indexOf(col);
138
+ if (idx < 0) return null;
139
+ const raw = row[idx] ?? "";
140
+ return num(raw) ?? String(raw).trim();
141
+ }
142
+
143
+ function pickTable(nodes: { node_id: string; content: any }[], queryLower: string): { node_id: string; content: any } | null {
144
+ let best: { node: { node_id: string; content: any }; score: number } | null = null;
145
+ for (const n of nodes) {
146
+ const c = n.content ?? {};
147
+ const payload = (c.payload ?? {}) as { columns?: Column[]; rows?: unknown[] };
148
+ if (!Array.isArray(payload.rows) || !payload.rows.length) continue;
149
+ let score = 0;
150
+ for (const t of tokens(n.node_id.replace("/table/", ""))) {
151
+ if (t.length >= 3 && queryLower.includes(t)) score += 2;
152
+ }
153
+ for (const w of String(c.name ?? "").toLowerCase().split(/[^a-z0-9.]+/)) {
154
+ if (w.length >= 4 && queryLower.includes(w)) score += 1;
155
+ }
156
+ if (!best || score > best.score) best = { node: n, score };
157
+ }
158
+ // an un-scored pick across several tables would be a guess: refuse
159
+ // unless exactly one candidate survives the doc join
160
+ if (best && best.score > 0) return best.node;
161
+ return nodes.length === 1 ? nodes[0]! : null;
162
+ }
163
+
164
+ /** Evaluate the candidate table nodes against the question's
165
+ * aggregation intent. One verdict: the operation the question names,
166
+ * computed over the best-matching table's typed payload. */
167
+ export function evaluateAggregation(
168
+ nodes: { node_id: string; content: unknown }[],
169
+ query: string,
170
+ ): AggregationVerdict | null {
171
+ const qLower = query.toLowerCase();
172
+ const operation: AggregationVerdict["operation"] | null = /\bhow many\b|\bnumber of\b/.test(qLower)
173
+ ? "count"
174
+ : /\b(minimum|smallest|shortest|lowest|least)\b/.test(qLower)
175
+ ? "min"
176
+ : /\b(maximum|largest|longest|highest|greatest)\b/.test(qLower)
177
+ ? "max"
178
+ : "lookup";
179
+ if (!nodes.length) return null;
180
+ const node = pickTable(nodes, qLower);
181
+ if (!node) return null;
182
+ const content = (node.content && typeof node.content === "object" ? node.content : {}) as Record<string, any>;
183
+ const payload = (content.payload ?? {}) as { columns?: Column[]; rows?: unknown[] };
184
+ const cols = Array.isArray(payload.columns) ? payload.columns : [];
185
+ const rows = (Array.isArray(payload.rows) ? payload.rows : []).filter((r) => Array.isArray(r)) as string[][];
186
+ if (!cols.length || !rows.length) return null;
187
+ const tableTitle = String(content.name ?? content.definition ?? node.node_id.replace("/table/", ""));
188
+
189
+ const cite = (what: string) =>
190
+ `COMPUTED (${operation}) — ${what}, read from the typed table "${tableTitle}" (${node.node_id}). Present this result and cite the table's clause; the value is machine-computed from the table payload, do not recompute or round it differently.`;
191
+
192
+ if (operation === "count") {
193
+ const cc = classColumn(cols);
194
+ if (cc && /\bclasses?\b/.test(qLower) && tokens(cc.name).some((t) => t.length >= 3 && qLower.includes(t))) {
195
+ const idx = cols.indexOf(cc);
196
+ const distinct = new Set(rows.map((r) => String(r[idx] ?? "").trim().toLowerCase()));
197
+ return {
198
+ operation,
199
+ table: node.node_id,
200
+ table_title: tableTitle,
201
+ column: cc.name,
202
+ value: distinct.size,
203
+ note: cite(`the table defines ${distinct.size} distinct ${cc.name.replace("_", " ")} values`),
204
+ };
205
+ }
206
+ return {
207
+ operation,
208
+ table: node.node_id,
209
+ table_title: tableTitle,
210
+ value: rows.length,
211
+ note: cite(`the table has ${rows.length} rows`),
212
+ };
213
+ }
214
+
215
+ const cToken = classToken(query);
216
+
217
+ if (operation === "min" || operation === "max") {
218
+ let col: Column | undefined = cToken ? classAsColumn(cols, cToken) : undefined;
219
+ if (!col) {
220
+ let best: { col: Column; score: number } | null = null;
221
+ for (const c of numericColumns(cols)) {
222
+ const s = columnScore(c, qLower);
223
+ if (s > 0 && (!best || s > best.score)) best = { col: c, score: s };
224
+ }
225
+ col = best?.col;
226
+ }
227
+ if (!col) return null;
228
+ const values = rows.map((r) => num(r[cols.indexOf(col!)])).filter((v): v is number => v !== null);
229
+ if (!values.length) return null;
230
+ const value = operation === "min" ? Math.min(...values) : Math.max(...values);
231
+ return {
232
+ operation,
233
+ table: node.node_id,
234
+ table_title: tableTitle,
235
+ column: col.name,
236
+ value,
237
+ unit: col.unit,
238
+ note: cite(`${operation} of ${col.name.replace(/_/g, " ")} across ${values.length} rows is ${value}${col.unit ? ` ${col.unit}` : ""}`),
239
+ };
240
+ }
241
+
242
+ // lookup: a stated quantity selects the interval row; a class token
243
+ // (as row value or as column) names the cell.
244
+ const pairs = intervalPairs(cols);
245
+ const cc = classColumn(cols);
246
+ const filterColumn = cc && cToken ? cc : undefined;
247
+ let matched: { row: string[] } | null = null;
248
+ let pairUsed: { low: Column; high: Column; exclusiveLow: boolean } | null = null;
249
+ let stated: number | null = null;
250
+ for (const pair of pairs) {
251
+ const v = statedInInterval(query, pair.low.unit);
252
+ if (v === null) continue;
253
+ const r = selectRow(rows, cols, pair, v, filterColumn, cToken);
254
+ if (r) {
255
+ matched = r;
256
+ pairUsed = pair;
257
+ stated = v;
258
+ break;
259
+ }
260
+ }
261
+ if (matched && pairUsed && stated !== null) {
262
+ const returnCol = cToken ? classAsColumn(cols, cToken) : undefined;
263
+ const valueCols = cols.filter(
264
+ (c) =>
265
+ c !== pairUsed!.low &&
266
+ c !== pairUsed!.high &&
267
+ c !== filterColumn &&
268
+ numericColumns(cols).includes(c),
269
+ );
270
+ const col = returnCol ?? (valueCols.length === 1 ? valueCols[0] : undefined);
271
+ const value = col ? cellValue(matched.row, cols, col) : null;
272
+ const rowObj: Record<string, string> = {};
273
+ cols.forEach((c, i) => (rowObj[c.name] = String(matched!.row[i] ?? "").trim()));
274
+ return {
275
+ operation: "lookup",
276
+ table: node.node_id,
277
+ table_title: tableTitle,
278
+ column: col?.name,
279
+ value,
280
+ unit: col?.unit,
281
+ row: rowObj,
282
+ note: cite(
283
+ `the stated ${stated} ${pairUsed.low.unit ?? ""} falls in the row ${pairUsed.low.name} ${matched.row[cols.indexOf(pairUsed.low)]} / ${pairUsed.high.name} ${matched.row[cols.indexOf(pairUsed.high)]}${cToken ? `, ${filterColumn ? filterColumn.name.replace("_", " ") : "class"} ${cToken}` : ""}`,
284
+ ),
285
+ };
286
+ }
287
+ return null;
288
+ }
@@ -22,6 +22,7 @@ import { exchangeForLiveToken, liveDataConfig, resolveLiveAccount, type LiveReco
22
22
  import { bindModelNode, licenseBoundaryNote, licenseBoundaryRefusal, licensedEntryForPackage, modelCitation, modelEcho, modelGroundingBlock, modelNodeRefIn, standardForDocNumber } from "./modelplane";
23
23
  import { evaluate as machineEvaluate, verdictNote } from "./verdict";
24
24
  import { evaluateConditionSets, quantitiesIn, type ConditionVerdict } from "./conditions";
25
+ import { evaluateAggregation, type AggregationVerdict } from "./aggregation";
25
26
  import { detectDraftIntent, prepareDraft } from "./drafts";
26
27
  import { memoryNote } from "./memories";
27
28
  import { entitlementScope, resolveRequestScope, requestSalt } from "./requestScope";
@@ -791,6 +792,52 @@ async function handleAsk(
791
792
  }
792
793
  : null;
793
794
  if (conditionVerdict) console.log("condition engine:", conditionVerdict.matched.join("|") || conditionVerdict.nearest!.node_id, "→", conditionVerdict.verdict.toUpperCase());
795
+ // ── table-payload aggregation (konneal/engine#91): count / min / max
796
+ // / interval lookup over the typed table nodes — deterministic
797
+ // arithmetic where the answer IS the table's content. Fires when no
798
+ // node binding, machine verdict or condition verdict ran; the
799
+ // candidates are license-gated like every model lane.
800
+ let aggregationVerdict: AggregationVerdict | null = null;
801
+ let aggregationStandard: string | null = null;
802
+ if (!machineVerdict && !boundModel && !conditionVerdict && P().publisher.features?.model_plane) {
803
+ const docNum = modelDocHint?.doc_number;
804
+ const sql = docNum
805
+ ? "SELECT standard, node_id, content FROM model_nodes WHERE kind = 'table' AND standard LIKE '%' || ?1"
806
+ : "SELECT standard, node_id, content FROM model_nodes WHERE kind = 'table'";
807
+ const stmt = docNum ? env.DB.prepare(sql).bind(docNum) : env.DB.prepare(sql);
808
+ const rows = await stmt.all().catch(() => ({ results: [] }));
809
+ const candidates = (rows.results ?? []).filter((r: any) => {
810
+ const entry = licensedEntryForPackage(String(r.standard));
811
+ return !entry || (standardKeys?.has(entry.key) ?? false);
812
+ });
813
+ const v = evaluateAggregation(
814
+ candidates.map((r: any) => ({ node_id: String(r.node_id), content: JSON.parse(String(r.content ?? "{}")) })),
815
+ q.query,
816
+ );
817
+ if (v) {
818
+ aggregationVerdict = v;
819
+ aggregationStandard = String((rows.results?.[0] as any)?.standard ?? "");
820
+ }
821
+ }
822
+ const aggregationNote = aggregationVerdict
823
+ ? `${aggregationVerdict.note}${aggregationStandard ? ` (standard ${aggregationStandard}.)` : ""}`
824
+ : undefined;
825
+ const aggregationBlock = aggregationVerdict
826
+ ? {
827
+ unit_id: aggregationVerdict.table,
828
+ type: "verdict",
829
+ docidentifier: `SMART model table${aggregationStandard ? ` (${aggregationStandard})` : ""}`,
830
+ payload: {
831
+ check: `${aggregationVerdict.operation}: ${aggregationVerdict.column ?? aggregationVerdict.table_title ?? aggregationVerdict.table} = ${aggregationVerdict.value}${aggregationVerdict.unit ? ` ${aggregationVerdict.unit}` : ""}`,
832
+ meaning: aggregationVerdict.table_title,
833
+ operation: aggregationVerdict.operation,
834
+ value: aggregationVerdict.value,
835
+ unit: aggregationVerdict.unit,
836
+ row: aggregationVerdict.row,
837
+ },
838
+ }
839
+ : null;
840
+ if (aggregationVerdict) console.log("aggregation engine:", aggregationVerdict.operation, aggregationVerdict.table, "→", aggregationVerdict.value);
794
841
  try {
795
842
  const tR = Date.now();
796
843
  // ── The "my account" live read (TODO.ai-platform/03) — resolved
@@ -940,7 +987,7 @@ async function handleAsk(
940
987
  q.lang,
941
988
  keptHistory,
942
989
  // stage-extracted graph facts (GraphRAG) ride the same note channel
943
- [processNote, eNote, contextNote(declaredCtx, docScope), accountNote, modelNote, vocabNote, memNote, machineNote, conditionNote, licenseNote, ...(retrieved.notes ?? [])].filter(Boolean).join("\n") || undefined,
990
+ [processNote, eNote, contextNote(declaredCtx, docScope), accountNote, modelNote, vocabNote, memNote, machineNote, conditionNote, aggregationNote, licenseNote, ...(retrieved.notes ?? [])].filter(Boolean).join("\n") || undefined,
944
991
  summary,
945
992
  budget,
946
993
  );
@@ -993,7 +1040,7 @@ async function handleAsk(
993
1040
  const c2 = canonical0.includes(refusalAnswer())
994
1041
  ? { text: canonical0, blocks: [], dropped: [] as string[] }
995
1042
  : await contractV2(env.DB, canonical0, usedHits);
996
- send({ type: "done", model, query_hash: queryHash, follow_ups: understanding?.follow_ups ?? [], blocks: [...c2.blocks, ...(verdictBlock ? [verdictBlock] : []), ...(conditionBlock ? [conditionBlock] : [])], context_applied: ctxApplied, read: readAs(),
1043
+ send({ type: "done", model, query_hash: queryHash, follow_ups: understanding?.follow_ups ?? [], blocks: [...c2.blocks, ...(verdictBlock ? [verdictBlock] : []), ...(conditionBlock ? [conditionBlock] : []), ...(aggregationBlock ? [aggregationBlock] : [])], context_applied: ctxApplied, read: readAs(),
997
1044
  // the evidence view's ground truth: the exact passages this
998
1045
  // answer was built from, compact — cache hits carry none,
999
1046
  // because the cache stores the answer and never the passages
@@ -1172,7 +1219,7 @@ async function handleAsk(
1172
1219
  // figure completion (#172) — see ./completion for the rationale
1173
1220
  completionBlocks.push(...(await completeFigures(env.DB, answer, [...c2ns.blocks, ...completionBlocks], used)));
1174
1221
 
1175
- const out = { answer, citations: finalCites, model: MODELS.member, query_hash: queryHash, follow_ups: understanding?.follow_ups ?? [], blocks: [...c2ns.blocks, ...(verdictBlock ? [verdictBlock] : []), ...(conditionBlock ? [conditionBlock] : []), ...completionBlocks], context_applied: ctxApplied, ...(liveRecords ? { records: liveRecords } : {}) };
1222
+ const out = { answer, citations: finalCites, model: MODELS.member, query_hash: queryHash, follow_ups: understanding?.follow_ups ?? [], blocks: [...c2ns.blocks, ...(verdictBlock ? [verdictBlock] : []), ...(conditionBlock ? [conditionBlock] : []), ...(aggregationBlock ? [aggregationBlock] : []), ...completionBlocks], context_applied: ctxApplied, ...(liveRecords ? { records: liveRecords } : {}) };
1176
1223
  const cacheable = !contextual && !declaredCtx && !answer.includes(refusalAnswer()) && finalAnchors.violations.length === 0;
1177
1224
  if (cacheable) {
1178
1225
  const warmVec = (await warmEmbed) ?? null;
@@ -67,6 +67,9 @@ export async function scoreJudge(
67
67
  ],
68
68
  max_tokens: 6144,
69
69
  reasoning_effort: "low",
70
+ // the judge is a measurement: greedy decoding, no sampling
71
+ temperature: 0,
72
+ top_p: 1,
70
73
  });
71
74
  const text = typeof res?.response === "string" ? res.response : res?.choices?.[0]?.message?.content;
72
75
  // reasoning models can emit {...} fragments before the verdict — the