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