@konneal/engine 0.2.15 → 0.2.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aggregation.d.ts +17 -0
- package/dist/{ask-KYAZRMTS.js → ask-PKKNMLLM.js} +1 -1
- package/dist/{chunk-E5BXYZH6.js → chunk-DRQ37UGZ.js} +290 -23
- package/dist/openapi-types.d.ts +1 -1
- package/dist/worker_public/src/index.js +2 -2
- package/package.json +1 -1
- package/workers/worker_public/openapi.yaml +4 -2
- package/workers/worker_public/src/aggregation.ts +316 -0
- package/workers/worker_public/src/ask.ts +50 -3
|
@@ -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;
|
|
@@ -589,17 +589,17 @@ function parseAndEval(src, params) {
|
|
|
589
589
|
const r = add();
|
|
590
590
|
switch (t.v) {
|
|
591
591
|
case ">=":
|
|
592
|
-
return
|
|
592
|
+
return num3(l) >= num3(r);
|
|
593
593
|
case "<=":
|
|
594
|
-
return
|
|
594
|
+
return num3(l) <= num3(r);
|
|
595
595
|
case ">":
|
|
596
|
-
return
|
|
596
|
+
return num3(l) > num3(r);
|
|
597
597
|
case "<":
|
|
598
|
-
return
|
|
598
|
+
return num3(l) < num3(r);
|
|
599
599
|
case "==":
|
|
600
|
-
return
|
|
600
|
+
return num3(l) === num3(r);
|
|
601
601
|
default:
|
|
602
|
-
return
|
|
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 === "+" ?
|
|
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 === "*" ?
|
|
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 -
|
|
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
|
|
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
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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,238 @@ 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 classValueExists(rows, cols, token) {
|
|
935
|
+
const cc = classColumn(cols);
|
|
936
|
+
if (!cc) return false;
|
|
937
|
+
const idx = cols.indexOf(cc);
|
|
938
|
+
return rows.some((r) => String(r[idx] ?? "").trim().toLowerCase() === token);
|
|
939
|
+
}
|
|
940
|
+
function statedUnits(query) {
|
|
941
|
+
const out = /* @__PURE__ */ new Set();
|
|
942
|
+
const re = new RegExp(`(${NUM2})\\s*([%\xB0a-zA-Z][a-zA-Z/.%\xB0]*\\b)`, "g");
|
|
943
|
+
for (const m of query.matchAll(re)) {
|
|
944
|
+
if (m[2]) out.add(m[2].replace("\u2062", "").trim());
|
|
945
|
+
}
|
|
946
|
+
return out;
|
|
947
|
+
}
|
|
948
|
+
function pickTable(nodes, query) {
|
|
949
|
+
const queryLower = query.toLowerCase();
|
|
950
|
+
const stated = statedUnits(query);
|
|
951
|
+
const cToken = classToken(query);
|
|
952
|
+
let best = null;
|
|
953
|
+
for (const n of nodes) {
|
|
954
|
+
const c = n.content ?? {};
|
|
955
|
+
const payload = c.payload ?? {};
|
|
956
|
+
if (!Array.isArray(payload.rows) || !payload.rows.length) continue;
|
|
957
|
+
const cols = Array.isArray(payload.columns) ? payload.columns : [];
|
|
958
|
+
const rows = (Array.isArray(payload.rows) ? payload.rows : []).filter((r) => Array.isArray(r));
|
|
959
|
+
let score = 0;
|
|
960
|
+
for (const t of tokens(n.node_id.replace("/table/", ""))) {
|
|
961
|
+
if (t.length >= 3 && queryLower.includes(t)) score += 2;
|
|
962
|
+
}
|
|
963
|
+
for (const w of String(c.name ?? "").toLowerCase().split(/[^a-z0-9.]+/)) {
|
|
964
|
+
if (w.length >= 4 && queryLower.includes(w)) score += 1;
|
|
965
|
+
}
|
|
966
|
+
if (intervalPairs(cols).some((p) => p.low.unit && stated.has(p.low.unit))) score += 3;
|
|
967
|
+
if (cToken && (classAsColumn(cols, cToken) || classValueExists(rows, cols, cToken))) score += 2;
|
|
968
|
+
if (!best || score > best.score) best = { node: n, score };
|
|
969
|
+
}
|
|
970
|
+
if (best && best.score > 0) return best.node;
|
|
971
|
+
return nodes.length === 1 ? nodes[0] : null;
|
|
972
|
+
}
|
|
973
|
+
function evaluateAggregation(nodes, query) {
|
|
974
|
+
const qLower = query.toLowerCase();
|
|
975
|
+
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";
|
|
976
|
+
if (!nodes.length) return null;
|
|
977
|
+
const node = pickTable(nodes, query);
|
|
978
|
+
if (!node) return null;
|
|
979
|
+
const content = node.content && typeof node.content === "object" ? node.content : {};
|
|
980
|
+
const payload = content.payload ?? {};
|
|
981
|
+
const cols = Array.isArray(payload.columns) ? payload.columns : [];
|
|
982
|
+
const rows = (Array.isArray(payload.rows) ? payload.rows : []).filter((r) => Array.isArray(r));
|
|
983
|
+
if (!cols.length || !rows.length) return null;
|
|
984
|
+
const tableTitle = String(content.name ?? content.definition ?? node.node_id.replace("/table/", ""));
|
|
985
|
+
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.`;
|
|
986
|
+
if (operation === "count") {
|
|
987
|
+
const cc2 = classColumn(cols);
|
|
988
|
+
if (cc2 && /\bclasses?\b/.test(qLower) && tokens(cc2.name).some((t) => t.length >= 3 && qLower.includes(t))) {
|
|
989
|
+
const idx = cols.indexOf(cc2);
|
|
990
|
+
const distinct = new Set(rows.map((r) => String(r[idx] ?? "").trim().toLowerCase()));
|
|
991
|
+
return {
|
|
992
|
+
operation,
|
|
993
|
+
table: node.node_id,
|
|
994
|
+
table_title: tableTitle,
|
|
995
|
+
column: cc2.name,
|
|
996
|
+
value: distinct.size,
|
|
997
|
+
note: cite(`the table defines ${distinct.size} distinct ${cc2.name.replace("_", " ")} values`)
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
return {
|
|
1001
|
+
operation,
|
|
1002
|
+
table: node.node_id,
|
|
1003
|
+
table_title: tableTitle,
|
|
1004
|
+
value: rows.length,
|
|
1005
|
+
note: cite(`the table has ${rows.length} rows`)
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
const cToken = classToken(query);
|
|
1009
|
+
if (operation === "min" || operation === "max") {
|
|
1010
|
+
let col = cToken ? classAsColumn(cols, cToken) : void 0;
|
|
1011
|
+
if (!col) {
|
|
1012
|
+
let best = null;
|
|
1013
|
+
for (const c of numericColumns(cols)) {
|
|
1014
|
+
const s = columnScore(c, qLower);
|
|
1015
|
+
if (s > 0 && (!best || s > best.score)) best = { col: c, score: s };
|
|
1016
|
+
}
|
|
1017
|
+
col = best?.col;
|
|
1018
|
+
}
|
|
1019
|
+
if (!col) return null;
|
|
1020
|
+
const values = rows.map((r) => num2(r[cols.indexOf(col)])).filter((v) => v !== null);
|
|
1021
|
+
if (!values.length) return null;
|
|
1022
|
+
const value = operation === "min" ? Math.min(...values) : Math.max(...values);
|
|
1023
|
+
return {
|
|
1024
|
+
operation,
|
|
1025
|
+
table: node.node_id,
|
|
1026
|
+
table_title: tableTitle,
|
|
1027
|
+
column: col.name,
|
|
1028
|
+
value,
|
|
1029
|
+
unit: col.unit,
|
|
1030
|
+
note: cite(`${operation} of ${col.name.replace(/_/g, " ")} across ${values.length} rows is ${value}${col.unit ? ` ${col.unit}` : ""}`)
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
const pairs = intervalPairs(cols);
|
|
1034
|
+
const cc = classColumn(cols);
|
|
1035
|
+
const filterColumn = cc && cToken ? cc : void 0;
|
|
1036
|
+
let matched = null;
|
|
1037
|
+
let pairUsed = null;
|
|
1038
|
+
let stated = null;
|
|
1039
|
+
for (const pair of pairs) {
|
|
1040
|
+
const v = statedInInterval(query, pair.low.unit);
|
|
1041
|
+
if (v === null) continue;
|
|
1042
|
+
const r = selectRow(rows, cols, pair, v, filterColumn, cToken);
|
|
1043
|
+
if (r) {
|
|
1044
|
+
matched = r;
|
|
1045
|
+
pairUsed = pair;
|
|
1046
|
+
stated = v;
|
|
1047
|
+
break;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
if (matched && pairUsed && stated !== null) {
|
|
1051
|
+
const returnCol = cToken ? classAsColumn(cols, cToken) : void 0;
|
|
1052
|
+
const valueCols = cols.filter(
|
|
1053
|
+
(c) => c !== pairUsed.low && c !== pairUsed.high && c !== filterColumn && numericColumns(cols).includes(c)
|
|
1054
|
+
);
|
|
1055
|
+
const col = returnCol ?? (valueCols.length === 1 ? valueCols[0] : void 0);
|
|
1056
|
+
const value = col ? cellValue(matched.row, cols, col) : null;
|
|
1057
|
+
const rowObj = {};
|
|
1058
|
+
cols.forEach((c, i) => rowObj[c.name] = String(matched.row[i] ?? "").trim());
|
|
1059
|
+
return {
|
|
1060
|
+
operation: "lookup",
|
|
1061
|
+
table: node.node_id,
|
|
1062
|
+
table_title: tableTitle,
|
|
1063
|
+
column: col?.name,
|
|
1064
|
+
value,
|
|
1065
|
+
unit: col?.unit,
|
|
1066
|
+
row: rowObj,
|
|
1067
|
+
note: cite(
|
|
1068
|
+
`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}` : ""}`
|
|
1069
|
+
)
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
return null;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
843
1075
|
// workers/worker_public/src/drafts.ts
|
|
844
1076
|
var ACT_VERB = "(?:draft|prepare|pre-?fill|fill\\s+(?:in|out)|start|submit|file|lodge)";
|
|
845
1077
|
var ACT_TARGET = "(?:new\\s+)?(?:certification\\s+|type[ -]evaluation\\s+|OIML[- ]CS\\s+)?application";
|
|
@@ -966,16 +1198,16 @@ async function resolveStandard(env, named) {
|
|
|
966
1198
|
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
1199
|
if (!m) return null;
|
|
968
1200
|
const type = m[1].toUpperCase();
|
|
969
|
-
const
|
|
1201
|
+
const num3 = String(Number(m[2]));
|
|
970
1202
|
try {
|
|
971
1203
|
const row = await env.DB.prepare(
|
|
972
1204
|
"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}-${
|
|
1205
|
+
).bind(`${type}-${num3}`).first();
|
|
974
1206
|
if (!row) return null;
|
|
975
1207
|
const edition = typeof row.edition === "string" ? row.edition : void 0;
|
|
976
1208
|
return {
|
|
977
|
-
urn: `urn:oiml:pub:${type.toLowerCase()}:${
|
|
978
|
-
label: typeof row.docidentifier === "string" ? row.docidentifier : `OIML ${type} ${
|
|
1209
|
+
urn: `urn:oiml:pub:${type.toLowerCase()}:${num3}${edition ? `:${edition}` : ""}`,
|
|
1210
|
+
label: typeof row.docidentifier === "string" ? row.docidentifier : `OIML ${type} ${num3}`,
|
|
979
1211
|
...edition ? { edition } : {},
|
|
980
1212
|
status: typeof row.derived_status === "string" ? row.derived_status : typeof row.status === "string" ? row.status : void 0
|
|
981
1213
|
};
|
|
@@ -1687,6 +1919,41 @@ ${summary}` }] : [],
|
|
|
1687
1919
|
}
|
|
1688
1920
|
} : null;
|
|
1689
1921
|
if (conditionVerdict) console.log("condition engine:", conditionVerdict.matched.join("|") || conditionVerdict.nearest.node_id, "\u2192", conditionVerdict.verdict.toUpperCase());
|
|
1922
|
+
let aggregationVerdict = null;
|
|
1923
|
+
let aggregationStandard = null;
|
|
1924
|
+
if (!machineVerdict && !boundModel && !conditionVerdict && P().publisher.features?.model_plane) {
|
|
1925
|
+
const docNum = modelDocHint?.doc_number;
|
|
1926
|
+
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'";
|
|
1927
|
+
const stmt = docNum ? env.DB.prepare(sql).bind(docNum) : env.DB.prepare(sql);
|
|
1928
|
+
const rows = await stmt.all().catch(() => ({ results: [] }));
|
|
1929
|
+
const candidates = (rows.results ?? []).filter((r) => {
|
|
1930
|
+
const entry = licensedEntryForPackage(String(r.standard));
|
|
1931
|
+
return !entry || (standardKeys?.has(entry.key) ?? false);
|
|
1932
|
+
});
|
|
1933
|
+
const v = evaluateAggregation(
|
|
1934
|
+
candidates.map((r) => ({ node_id: String(r.node_id), content: JSON.parse(String(r.content ?? "{}")) })),
|
|
1935
|
+
q.query
|
|
1936
|
+
);
|
|
1937
|
+
if (v) {
|
|
1938
|
+
aggregationVerdict = v;
|
|
1939
|
+
aggregationStandard = String(rows.results?.[0]?.standard ?? "");
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
const aggregationNote = aggregationVerdict ? `${aggregationVerdict.note}${aggregationStandard ? ` (standard ${aggregationStandard}.)` : ""}` : void 0;
|
|
1943
|
+
const aggregationBlock = aggregationVerdict ? {
|
|
1944
|
+
unit_id: aggregationVerdict.table,
|
|
1945
|
+
type: "verdict",
|
|
1946
|
+
docidentifier: `SMART model table${aggregationStandard ? ` (${aggregationStandard})` : ""}`,
|
|
1947
|
+
payload: {
|
|
1948
|
+
check: `${aggregationVerdict.operation}: ${aggregationVerdict.column ?? aggregationVerdict.table_title ?? aggregationVerdict.table} = ${aggregationVerdict.value}${aggregationVerdict.unit ? ` ${aggregationVerdict.unit}` : ""}`,
|
|
1949
|
+
meaning: aggregationVerdict.table_title,
|
|
1950
|
+
operation: aggregationVerdict.operation,
|
|
1951
|
+
value: aggregationVerdict.value,
|
|
1952
|
+
unit: aggregationVerdict.unit,
|
|
1953
|
+
row: aggregationVerdict.row
|
|
1954
|
+
}
|
|
1955
|
+
} : null;
|
|
1956
|
+
if (aggregationVerdict) console.log("aggregation engine:", aggregationVerdict.operation, aggregationVerdict.table, "\u2192", aggregationVerdict.value);
|
|
1690
1957
|
try {
|
|
1691
1958
|
const tR = Date.now();
|
|
1692
1959
|
if (declaredCtx?.kind === "account") {
|
|
@@ -1780,7 +2047,7 @@ Answer account questions from these records ONLY: name the record when you use i
|
|
|
1780
2047
|
q.lang,
|
|
1781
2048
|
keptHistory,
|
|
1782
2049
|
// 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,
|
|
2050
|
+
[processNote, eNote, contextNote(declaredCtx, docScope), accountNote, modelNote, vocabNote, memNote, machineNote, conditionNote, aggregationNote, licenseNote, ...retrieved.notes ?? []].filter(Boolean).join("\n") || void 0,
|
|
1784
2051
|
summary,
|
|
1785
2052
|
budget
|
|
1786
2053
|
);
|
|
@@ -1828,7 +2095,7 @@ Answer account questions from these records ONLY: name the record when you use i
|
|
|
1828
2095
|
model,
|
|
1829
2096
|
query_hash: queryHash,
|
|
1830
2097
|
follow_ups: understanding?.follow_ups ?? [],
|
|
1831
|
-
blocks: [...c2.blocks, ...verdictBlock ? [verdictBlock] : [], ...conditionBlock ? [conditionBlock] : []],
|
|
2098
|
+
blocks: [...c2.blocks, ...verdictBlock ? [verdictBlock] : [], ...conditionBlock ? [conditionBlock] : [], ...aggregationBlock ? [aggregationBlock] : []],
|
|
1832
2099
|
context_applied: ctxApplied,
|
|
1833
2100
|
read: readAs(),
|
|
1834
2101
|
// the evidence view's ground truth: the exact passages this
|
|
@@ -1945,7 +2212,7 @@ Answer account questions from these records ONLY: name the record when you use i
|
|
|
1945
2212
|
if (completionBlocks.length) console.log("contract completion:", completionBlocks.length, "table block(s) attached server-side");
|
|
1946
2213
|
}
|
|
1947
2214
|
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 } : {} };
|
|
2215
|
+
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
2216
|
const cacheable = !contextual && !declaredCtx && !answer.includes(refusalAnswer()) && finalAnchors.violations.length === 0;
|
|
1950
2217
|
if (cacheable) {
|
|
1951
2218
|
const warmVec = await warmEmbed ?? null;
|
package/dist/openapi-types.d.ts
CHANGED
|
@@ -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-
|
|
10
|
+
} from "../../chunk-DRQ37UGZ.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-
|
|
982
|
+
const res = name === "ask" ? await (await import("../../ask-PKKNMLLM.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.
|
|
37
|
+
"version": "0.2.17",
|
|
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
|
|
1187
|
-
|
|
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,316 @@
|
|
|
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
|
+
/** The named class exists as a row VALUE of the table's class column
|
|
144
|
+
* (mpe_tiers' accuracy_class A/B/C/D rows). */
|
|
145
|
+
function classValueExists(rows: string[][], cols: Column[], token: string): boolean {
|
|
146
|
+
const cc = classColumn(cols);
|
|
147
|
+
if (!cc) return false;
|
|
148
|
+
const idx = cols.indexOf(cc);
|
|
149
|
+
return rows.some((r) => String(r[idx] ?? "").trim().toLowerCase() === token);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function statedUnits(query: string): Set<string> {
|
|
153
|
+
const out = new Set<string>();
|
|
154
|
+
const re = new RegExp(`(${NUM})\\s*([%°a-zA-Z][a-zA-Z/.%°]*\\b)`,"g");
|
|
155
|
+
for (const m of query.matchAll(re)) {
|
|
156
|
+
if (m[2]) out.add(m[2].replace("\u2062", "").trim());
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function pickTable(nodes: { node_id: string; content: any }[], query: string): { node_id: string; content: any } | null {
|
|
162
|
+
const queryLower = query.toLowerCase();
|
|
163
|
+
const stated = statedUnits(query);
|
|
164
|
+
const cToken = classToken(query);
|
|
165
|
+
let best: { node: { node_id: string; content: any }; score: number } | null = null;
|
|
166
|
+
for (const n of nodes) {
|
|
167
|
+
const c = n.content ?? {};
|
|
168
|
+
const payload = (c.payload ?? {}) as { columns?: Column[]; rows?: unknown[] };
|
|
169
|
+
if (!Array.isArray(payload.rows) || !payload.rows.length) continue;
|
|
170
|
+
const cols = (Array.isArray(payload.columns) ? payload.columns : []) as Column[];
|
|
171
|
+
const rows = (Array.isArray(payload.rows) ? payload.rows : []).filter((r) => Array.isArray(r)) as string[][];
|
|
172
|
+
let score = 0;
|
|
173
|
+
for (const t of tokens(n.node_id.replace("/table/", ""))) {
|
|
174
|
+
if (t.length >= 3 && queryLower.includes(t)) score += 2;
|
|
175
|
+
}
|
|
176
|
+
for (const w of String(c.name ?? "").toLowerCase().split(/[^a-z0-9.]+/)) {
|
|
177
|
+
if (w.length >= 4 && queryLower.includes(w)) score += 1;
|
|
178
|
+
}
|
|
179
|
+
// the question's stated unit existing as this table's interval unit,
|
|
180
|
+
// and the named class resolving here, are the strongest signals —
|
|
181
|
+
// a table without either cannot hold the lookup
|
|
182
|
+
if (intervalPairs(cols).some((p) => p.low.unit && stated.has(p.low.unit))) score += 3;
|
|
183
|
+
if (cToken && (classAsColumn(cols, cToken) || classValueExists(rows, cols, cToken))) score += 2;
|
|
184
|
+
if (!best || score > best.score) best = { node: n, score };
|
|
185
|
+
}
|
|
186
|
+
// an un-scored pick across several tables would be a guess: refuse
|
|
187
|
+
// unless exactly one candidate survives the doc join
|
|
188
|
+
if (best && best.score > 0) return best.node;
|
|
189
|
+
return nodes.length === 1 ? nodes[0]! : null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Evaluate the candidate table nodes against the question's
|
|
193
|
+
* aggregation intent. One verdict: the operation the question names,
|
|
194
|
+
* computed over the best-matching table's typed payload. */
|
|
195
|
+
export function evaluateAggregation(
|
|
196
|
+
nodes: { node_id: string; content: unknown }[],
|
|
197
|
+
query: string,
|
|
198
|
+
): AggregationVerdict | null {
|
|
199
|
+
const qLower = query.toLowerCase();
|
|
200
|
+
const operation: AggregationVerdict["operation"] | null = /\bhow many\b|\bnumber of\b/.test(qLower)
|
|
201
|
+
? "count"
|
|
202
|
+
: /\b(minimum|smallest|shortest|lowest|least)\b/.test(qLower)
|
|
203
|
+
? "min"
|
|
204
|
+
: /\b(maximum|largest|longest|highest|greatest)\b/.test(qLower)
|
|
205
|
+
? "max"
|
|
206
|
+
: "lookup";
|
|
207
|
+
if (!nodes.length) return null;
|
|
208
|
+
const node = pickTable(nodes, query);
|
|
209
|
+
if (!node) return null;
|
|
210
|
+
const content = (node.content && typeof node.content === "object" ? node.content : {}) as Record<string, any>;
|
|
211
|
+
const payload = (content.payload ?? {}) as { columns?: Column[]; rows?: unknown[] };
|
|
212
|
+
const cols = Array.isArray(payload.columns) ? payload.columns : [];
|
|
213
|
+
const rows = (Array.isArray(payload.rows) ? payload.rows : []).filter((r) => Array.isArray(r)) as string[][];
|
|
214
|
+
if (!cols.length || !rows.length) return null;
|
|
215
|
+
const tableTitle = String(content.name ?? content.definition ?? node.node_id.replace("/table/", ""));
|
|
216
|
+
|
|
217
|
+
const cite = (what: string) =>
|
|
218
|
+
`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.`;
|
|
219
|
+
|
|
220
|
+
if (operation === "count") {
|
|
221
|
+
const cc = classColumn(cols);
|
|
222
|
+
if (cc && /\bclasses?\b/.test(qLower) && tokens(cc.name).some((t) => t.length >= 3 && qLower.includes(t))) {
|
|
223
|
+
const idx = cols.indexOf(cc);
|
|
224
|
+
const distinct = new Set(rows.map((r) => String(r[idx] ?? "").trim().toLowerCase()));
|
|
225
|
+
return {
|
|
226
|
+
operation,
|
|
227
|
+
table: node.node_id,
|
|
228
|
+
table_title: tableTitle,
|
|
229
|
+
column: cc.name,
|
|
230
|
+
value: distinct.size,
|
|
231
|
+
note: cite(`the table defines ${distinct.size} distinct ${cc.name.replace("_", " ")} values`),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
operation,
|
|
236
|
+
table: node.node_id,
|
|
237
|
+
table_title: tableTitle,
|
|
238
|
+
value: rows.length,
|
|
239
|
+
note: cite(`the table has ${rows.length} rows`),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const cToken = classToken(query);
|
|
244
|
+
|
|
245
|
+
if (operation === "min" || operation === "max") {
|
|
246
|
+
let col: Column | undefined = cToken ? classAsColumn(cols, cToken) : undefined;
|
|
247
|
+
if (!col) {
|
|
248
|
+
let best: { col: Column; score: number } | null = null;
|
|
249
|
+
for (const c of numericColumns(cols)) {
|
|
250
|
+
const s = columnScore(c, qLower);
|
|
251
|
+
if (s > 0 && (!best || s > best.score)) best = { col: c, score: s };
|
|
252
|
+
}
|
|
253
|
+
col = best?.col;
|
|
254
|
+
}
|
|
255
|
+
if (!col) return null;
|
|
256
|
+
const values = rows.map((r) => num(r[cols.indexOf(col!)])).filter((v): v is number => v !== null);
|
|
257
|
+
if (!values.length) return null;
|
|
258
|
+
const value = operation === "min" ? Math.min(...values) : Math.max(...values);
|
|
259
|
+
return {
|
|
260
|
+
operation,
|
|
261
|
+
table: node.node_id,
|
|
262
|
+
table_title: tableTitle,
|
|
263
|
+
column: col.name,
|
|
264
|
+
value,
|
|
265
|
+
unit: col.unit,
|
|
266
|
+
note: cite(`${operation} of ${col.name.replace(/_/g, " ")} across ${values.length} rows is ${value}${col.unit ? ` ${col.unit}` : ""}`),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// lookup: a stated quantity selects the interval row; a class token
|
|
271
|
+
// (as row value or as column) names the cell.
|
|
272
|
+
const pairs = intervalPairs(cols);
|
|
273
|
+
const cc = classColumn(cols);
|
|
274
|
+
const filterColumn = cc && cToken ? cc : undefined;
|
|
275
|
+
let matched: { row: string[] } | null = null;
|
|
276
|
+
let pairUsed: { low: Column; high: Column; exclusiveLow: boolean } | null = null;
|
|
277
|
+
let stated: number | null = null;
|
|
278
|
+
for (const pair of pairs) {
|
|
279
|
+
const v = statedInInterval(query, pair.low.unit);
|
|
280
|
+
if (v === null) continue;
|
|
281
|
+
const r = selectRow(rows, cols, pair, v, filterColumn, cToken);
|
|
282
|
+
if (r) {
|
|
283
|
+
matched = r;
|
|
284
|
+
pairUsed = pair;
|
|
285
|
+
stated = v;
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (matched && pairUsed && stated !== null) {
|
|
290
|
+
const returnCol = cToken ? classAsColumn(cols, cToken) : undefined;
|
|
291
|
+
const valueCols = cols.filter(
|
|
292
|
+
(c) =>
|
|
293
|
+
c !== pairUsed!.low &&
|
|
294
|
+
c !== pairUsed!.high &&
|
|
295
|
+
c !== filterColumn &&
|
|
296
|
+
numericColumns(cols).includes(c),
|
|
297
|
+
);
|
|
298
|
+
const col = returnCol ?? (valueCols.length === 1 ? valueCols[0] : undefined);
|
|
299
|
+
const value = col ? cellValue(matched.row, cols, col) : null;
|
|
300
|
+
const rowObj: Record<string, string> = {};
|
|
301
|
+
cols.forEach((c, i) => (rowObj[c.name] = String(matched!.row[i] ?? "").trim()));
|
|
302
|
+
return {
|
|
303
|
+
operation: "lookup",
|
|
304
|
+
table: node.node_id,
|
|
305
|
+
table_title: tableTitle,
|
|
306
|
+
column: col?.name,
|
|
307
|
+
value,
|
|
308
|
+
unit: col?.unit,
|
|
309
|
+
row: rowObj,
|
|
310
|
+
note: cite(
|
|
311
|
+
`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}` : ""}`,
|
|
312
|
+
),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
@@ -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;
|