@konneal/engine 0.2.15 → 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-E5BXYZH6.js";
3
+ } from "./chunk-XHJI73N2.js";
4
4
  import "./chunk-7UT76RQO.js";
5
5
  import "./chunk-EFQALN2Z.js";
6
6
  import "./chunk-DBBGOOMZ.js";
@@ -589,17 +589,17 @@ function parseAndEval(src, params) {
589
589
  const r = add();
590
590
  switch (t.v) {
591
591
  case ">=":
592
- return num2(l) >= num2(r);
592
+ return num3(l) >= num3(r);
593
593
  case "<=":
594
- return num2(l) <= num2(r);
594
+ return num3(l) <= num3(r);
595
595
  case ">":
596
- return num2(l) > num2(r);
596
+ return num3(l) > num3(r);
597
597
  case "<":
598
- return num2(l) < num2(r);
598
+ return num3(l) < num3(r);
599
599
  case "==":
600
- return num2(l) === num2(r);
600
+ return num3(l) === num3(r);
601
601
  default:
602
- return num2(l) !== num2(r);
602
+ return num3(l) !== num3(r);
603
603
  }
604
604
  }
605
605
  return l;
@@ -611,7 +611,7 @@ function parseAndEval(src, params) {
611
611
  if (t && t.t === "op" && (t.v === "+" || t.v === "-")) {
612
612
  p++;
613
613
  const r = mul();
614
- l = t.v === "+" ? num2(l) + num2(r) : num2(l) - num2(r);
614
+ l = t.v === "+" ? num3(l) + num3(r) : num3(l) - num3(r);
615
615
  } else return l;
616
616
  }
617
617
  }
@@ -622,7 +622,7 @@ function parseAndEval(src, params) {
622
622
  if (t && t.t === "op" && (t.v === "*" || t.v === "/")) {
623
623
  p++;
624
624
  const r = unary();
625
- l = t.v === "*" ? num2(l) * num2(r) : num2(l) / num2(r);
625
+ l = t.v === "*" ? num3(l) * num3(r) : num3(l) / num3(r);
626
626
  } else return l;
627
627
  }
628
628
  }
@@ -630,7 +630,7 @@ function parseAndEval(src, params) {
630
630
  const t = peek();
631
631
  if (t && t.t === "op" && t.v === "-") {
632
632
  p++;
633
- return -num2(unary());
633
+ return -num3(unary());
634
634
  }
635
635
  return atom();
636
636
  }
@@ -650,7 +650,7 @@ function parseAndEval(src, params) {
650
650
  throw new Error(`unexpected ${t.v}`);
651
651
  }
652
652
  const truthy = (v) => typeof v === "boolean" ? v : v !== 0;
653
- const num2 = (v) => typeof v === "boolean" ? v ? 1 : 0 : v;
653
+ const num3 = (v) => typeof v === "boolean" ? v ? 1 : 0 : v;
654
654
  const out = or();
655
655
  if (p !== toks.length) throw new Error("trailing tokens");
656
656
  return out;
@@ -766,20 +766,20 @@ function verdictNote(v, node) {
766
766
  var NUM = String.raw`-?\d+(?:[.,]\d+)?`;
767
767
  function quantitiesIn(query) {
768
768
  const out = {};
769
- const num2 = (s) => Number(s.replace(",", "."));
769
+ const num3 = (s) => Number(s.replace(",", "."));
770
770
  const put = (kind, stated, stated_unit, si) => {
771
771
  if (Number.isFinite(si)) out[kind] = { stated, stated_unit, si };
772
772
  };
773
773
  const tempC = query.match(new RegExp(`(${NUM})\\s*(?:\xB0\\s*)?C\\b`));
774
- 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);
775
775
  const tempK = query.match(new RegExp(`(${NUM})\\s*K\\b`));
776
- 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]));
777
777
  const rh = query.match(new RegExp(`(${NUM})\\s*%\\s*(?:RH\\b|relative\\s+humidity)?`, "i"));
778
- 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);
779
779
  const hours = query.match(new RegExp(`(${NUM})\\s*h\\b`, "i"));
780
- 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);
781
781
  const days = query.match(new RegExp(`(${NUM})\\s*days?\\b`, "i"));
782
- 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);
783
783
  return out;
784
784
  }
785
785
  function scoreSet(entries, q) {
@@ -840,6 +840,217 @@ function evaluateConditionSets(nodes, query) {
840
840
  };
841
841
  }
842
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
+
843
1054
  // workers/worker_public/src/drafts.ts
844
1055
  var ACT_VERB = "(?:draft|prepare|pre-?fill|fill\\s+(?:in|out)|start|submit|file|lodge)";
845
1056
  var ACT_TARGET = "(?:new\\s+)?(?:certification\\s+|type[ -]evaluation\\s+|OIML[- ]CS\\s+)?application";
@@ -966,16 +1177,16 @@ async function resolveStandard(env, named) {
966
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);
967
1178
  if (!m) return null;
968
1179
  const type = m[1].toUpperCase();
969
- const num2 = String(Number(m[2]));
1180
+ const num3 = String(Number(m[2]));
970
1181
  try {
971
1182
  const row = await env.DB.prepare(
972
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"
973
- ).bind(`${type}-${num2}`).first();
1184
+ ).bind(`${type}-${num3}`).first();
974
1185
  if (!row) return null;
975
1186
  const edition = typeof row.edition === "string" ? row.edition : void 0;
976
1187
  return {
977
- urn: `urn:oiml:pub:${type.toLowerCase()}:${num2}${edition ? `:${edition}` : ""}`,
978
- 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}`,
979
1190
  ...edition ? { edition } : {},
980
1191
  status: typeof row.derived_status === "string" ? row.derived_status : typeof row.status === "string" ? row.status : void 0
981
1192
  };
@@ -1687,6 +1898,41 @@ ${summary}` }] : [],
1687
1898
  }
1688
1899
  } : null;
1689
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);
1690
1936
  try {
1691
1937
  const tR = Date.now();
1692
1938
  if (declaredCtx?.kind === "account") {
@@ -1780,7 +2026,7 @@ Answer account questions from these records ONLY: name the record when you use i
1780
2026
  q.lang,
1781
2027
  keptHistory,
1782
2028
  // stage-extracted graph facts (GraphRAG) ride the same note channel
1783
- [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,
1784
2030
  summary,
1785
2031
  budget
1786
2032
  );
@@ -1828,7 +2074,7 @@ Answer account questions from these records ONLY: name the record when you use i
1828
2074
  model,
1829
2075
  query_hash: queryHash,
1830
2076
  follow_ups: understanding?.follow_ups ?? [],
1831
- blocks: [...c2.blocks, ...verdictBlock ? [verdictBlock] : [], ...conditionBlock ? [conditionBlock] : []],
2077
+ blocks: [...c2.blocks, ...verdictBlock ? [verdictBlock] : [], ...conditionBlock ? [conditionBlock] : [], ...aggregationBlock ? [aggregationBlock] : []],
1832
2078
  context_applied: ctxApplied,
1833
2079
  read: readAs(),
1834
2080
  // the evidence view's ground truth: the exact passages this
@@ -1945,7 +2191,7 @@ Answer account questions from these records ONLY: name the record when you use i
1945
2191
  if (completionBlocks.length) console.log("contract completion:", completionBlocks.length, "table block(s) attached server-side");
1946
2192
  }
1947
2193
  completionBlocks.push(...await completeFigures(env.DB, answer, [...c2ns.blocks, ...completionBlocks], used));
1948
- 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 } : {} };
1949
2195
  const cacheable = !contextual && !declaredCtx && !answer.includes(refusalAnswer()) && finalAnchors.violations.length === 0;
1950
2196
  if (cacheable) {
1951
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-E5BXYZH6.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-KYAZRMTS.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.15",
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;