@konneal/engine 0.2.119 → 0.2.120
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/{ask-YVBM6IEV.js → ask-3BJKTVLY.js} +2 -2
- package/dist/{chunk-32UDKBA2.js → chunk-FKG5VGOL.js} +44 -13
- package/dist/{chunk-EGFTNES5.js → chunk-SORXE55V.js} +20 -2
- package/dist/{tools-GSLHFD37.js → tools-CUKFOW7S.js} +1 -1
- package/dist/verdict.d.ts +19 -1
- package/dist/worker_public/src/index.js +4 -4
- package/package.json +1 -1
- package/workers/worker_public/src/ask.ts +24 -2
- package/workers/worker_public/src/verdict.ts +68 -10
|
@@ -268,27 +268,47 @@ function parseNumber(raw) {
|
|
|
268
268
|
s = s.replace(/\.(\d{3})$/, "$1");
|
|
269
269
|
return Number(s.replace(/,(?=\d{3}\b)/g, ""));
|
|
270
270
|
}
|
|
271
|
-
function
|
|
272
|
-
const
|
|
271
|
+
function extractParamsWithUnits(query, symbols) {
|
|
272
|
+
const values = {};
|
|
273
|
+
const units = {};
|
|
273
274
|
for (const sym of symbols) {
|
|
274
275
|
const leaf = sym.split(".").pop() ?? sym;
|
|
275
276
|
const esc = leaf.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
276
|
-
const re = new RegExp(`\\b${esc}\\b\\D{0,14}?([0-9][0-9 ,.]*[0-9])
|
|
277
|
+
const re = new RegExp(`\\b${esc}\\b\\D{0,14}?([0-9][0-9 ,.]*[0-9])\\s*(%|\\u00b0?[A-Za-z\\u00b5\\u03a9]{0,6})?`, "iu");
|
|
277
278
|
const m = query.match(re);
|
|
278
279
|
if (m) {
|
|
279
280
|
const v = parseNumber(m[1]);
|
|
280
|
-
if (Number.isFinite(v))
|
|
281
|
+
if (Number.isFinite(v)) {
|
|
282
|
+
values[sym] = v;
|
|
283
|
+
const u = (m[2] ?? "").trim();
|
|
284
|
+
if (u) units[sym] = u;
|
|
285
|
+
}
|
|
281
286
|
}
|
|
282
287
|
}
|
|
283
|
-
return
|
|
288
|
+
return { values, units };
|
|
289
|
+
}
|
|
290
|
+
function dimensionMismatch(check, units, dimensions) {
|
|
291
|
+
const dims = /* @__PURE__ */ new Map();
|
|
292
|
+
for (const [sym, unit] of Object.entries(units)) {
|
|
293
|
+
const leaf = sym.split(".").pop();
|
|
294
|
+
if (!leaf || !new RegExp(`\\b${leaf}\\b`, "i").test(check)) continue;
|
|
295
|
+
const dim = dimensions[unit.toLowerCase()];
|
|
296
|
+
if (dim) dims.set(sym, dim);
|
|
297
|
+
}
|
|
298
|
+
const distinct = [...new Set(dims.values())];
|
|
299
|
+
if (distinct.length <= 1) return null;
|
|
300
|
+
const parts = [...dims.entries()].map(([sym, dim]) => `${sym} is in ${dim}`);
|
|
301
|
+
return `the stated quantities mix dimensions: ${parts.join(" while ")}`;
|
|
284
302
|
}
|
|
285
|
-
function evaluate(content, query) {
|
|
303
|
+
function evaluate(content, query, opts) {
|
|
286
304
|
const c = content && typeof content === "object" ? content : {};
|
|
287
305
|
const checks = extractChecks(content);
|
|
288
306
|
if (!checks.length) return null;
|
|
289
307
|
const symbols = symbolsIn(checks);
|
|
290
|
-
const
|
|
308
|
+
const bound = extractParamsWithUnits(query, symbols);
|
|
309
|
+
const params = bound.values;
|
|
291
310
|
const missing = symbols.filter((s) => params[s] === void 0);
|
|
311
|
+
let voidReason = null;
|
|
292
312
|
const machine = checks.map((expression) => {
|
|
293
313
|
const values = {};
|
|
294
314
|
try {
|
|
@@ -297,10 +317,15 @@ function evaluate(content, query) {
|
|
|
297
317
|
}
|
|
298
318
|
let result = null;
|
|
299
319
|
if (missing.length === 0) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
320
|
+
const mismatch = opts?.dimensions ? dimensionMismatch(expression, bound.units, opts.dimensions) : null;
|
|
321
|
+
if (mismatch) {
|
|
322
|
+
voidReason = voidReason ?? mismatch;
|
|
323
|
+
} else {
|
|
324
|
+
try {
|
|
325
|
+
result = !!parseAndEval(expression, params);
|
|
326
|
+
} catch {
|
|
327
|
+
result = null;
|
|
328
|
+
}
|
|
304
329
|
}
|
|
305
330
|
}
|
|
306
331
|
return { expression, symbolic: expression, values, result };
|
|
@@ -310,7 +335,10 @@ function evaluate(content, query) {
|
|
|
310
335
|
}
|
|
311
336
|
const failed = machine.some((m) => m.result === false);
|
|
312
337
|
const evaluable = machine.some((m) => m.result !== null);
|
|
313
|
-
if (!evaluable)
|
|
338
|
+
if (!evaluable) {
|
|
339
|
+
if (voidReason) return { verdict: "void", missing: [], void_reason: voidReason, checks: machine };
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
314
342
|
return {
|
|
315
343
|
verdict: failed ? "fail" : "pass",
|
|
316
344
|
on_violation: failed ? String(c.on_violation ?? "invalid") : void 0,
|
|
@@ -328,7 +356,7 @@ function verdictNote(v, node) {
|
|
|
328
356
|
lines.push(`- ${c.expression}${vals ? ` [${vals}]` : ""} \u2192 ${c.result === null ? "not evaluated" : c.result ? "holds" : "VIOLATED"}`);
|
|
329
357
|
}
|
|
330
358
|
if (v.verdict === "void") {
|
|
331
|
-
lines.push(`VERDICT: VOID \u2014 the question does not state: ${v.missing.join(", ")}. Say exactly what is missing; never assume values.`);
|
|
359
|
+
lines.push(v.void_reason ? `VERDICT: VOID \u2014 ${v.void_reason}. State the mismatch plainly; the question cannot be decided as stated.` : `VERDICT: VOID \u2014 the question does not state: ${v.missing.join(", ")}. Say exactly what is missing; never assume values.`);
|
|
332
360
|
} else if (v.verdict === "pass") {
|
|
333
361
|
lines.push(`VERDICT: PASS \u2014 every machine check holds at the stated values. Present this verdict, the arithmetic above, and cite the node's clause.`);
|
|
334
362
|
} else {
|
|
@@ -1179,6 +1207,9 @@ ${r.output}`;
|
|
|
1179
1207
|
}
|
|
1180
1208
|
|
|
1181
1209
|
export {
|
|
1210
|
+
extractChecks,
|
|
1211
|
+
symbolsIn,
|
|
1212
|
+
extractParamsWithUnits,
|
|
1182
1213
|
evaluate,
|
|
1183
1214
|
verdictNote,
|
|
1184
1215
|
standardForDocNumber,
|
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
contractV2,
|
|
6
6
|
evaluate,
|
|
7
7
|
evaluateConditionSets,
|
|
8
|
+
extractChecks,
|
|
9
|
+
extractParamsWithUnits,
|
|
8
10
|
licenseBoundaryNote,
|
|
9
11
|
licenseBoundaryRefusal,
|
|
10
12
|
licensedEntryForPackage,
|
|
@@ -16,10 +18,11 @@ import {
|
|
|
16
18
|
resolveBlocks,
|
|
17
19
|
runTool,
|
|
18
20
|
standardForDocNumber,
|
|
21
|
+
symbolsIn,
|
|
19
22
|
tableRetyped,
|
|
20
23
|
toolNote,
|
|
21
24
|
verdictNote
|
|
22
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-FKG5VGOL.js";
|
|
23
26
|
import {
|
|
24
27
|
NO_CONTEXT,
|
|
25
28
|
STAGE_NAMES,
|
|
@@ -2195,7 +2198,22 @@ ${summary}` }] : [],
|
|
|
2195
2198
|
console.log("model plane: bound", boundModel.node_id, `[${boundModel.standard}]`, boundModel.clause?.urn ?? "no-clause", boundModel.gated ? "(gated: license)" : "");
|
|
2196
2199
|
}
|
|
2197
2200
|
const modelNote = boundModel && !boundModel.gated ? modelGroundingBlock(boundModel) : void 0;
|
|
2198
|
-
|
|
2201
|
+
let machineVerdict = boundModel && !boundModel.gated && !ablate?.noVerdict ? evaluate(boundModel.content, q.query) : null;
|
|
2202
|
+
if (machineVerdict === null && boundModel && !boundModel.gated) {
|
|
2203
|
+
try {
|
|
2204
|
+
const unitTokens = [...new Set(Object.values(extractParamsWithUnits(q.query, symbolsIn(extractChecks(boundModel.content))).units).map((u) => u.toLowerCase()))];
|
|
2205
|
+
if (unitTokens.length) {
|
|
2206
|
+
const marks = unitTokens.map(() => "?").join(",");
|
|
2207
|
+
const rows = await env.DB.prepare(
|
|
2208
|
+
`SELECT lower(coalesce(json_extract(symbols, '$.ascii'), json_extract(symbols, '$.unicode'))) AS sym, dimension_ascii FROM units_db WHERE lower(coalesce(json_extract(symbols, '$.ascii'), json_extract(symbols, '$.unicode'))) IN (${marks})`
|
|
2209
|
+
).bind(...unitTokens).all().catch(() => ({ results: [] }));
|
|
2210
|
+
const dimensions = {};
|
|
2211
|
+
for (const r of rows.results ?? []) dimensions[String(r.sym)] = String(r.dimension_ascii ?? "");
|
|
2212
|
+
machineVerdict = evaluate(boundModel.content, q.query, { dimensions });
|
|
2213
|
+
}
|
|
2214
|
+
} catch {
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2199
2217
|
const machineNote = machineVerdict && boundModel ? verdictNote(machineVerdict, boundModel) : void 0;
|
|
2200
2218
|
const modelNodeRows = async (kind, docNum) => {
|
|
2201
2219
|
const attempt = (num3) => {
|
package/dist/verdict.d.ts
CHANGED
|
@@ -9,12 +9,30 @@ export interface Verdict {
|
|
|
9
9
|
on_violation?: string;
|
|
10
10
|
violation_meaning?: string;
|
|
11
11
|
missing: string[];
|
|
12
|
+
/** why a fully-stated question still cannot evaluate (TODO.sota/04):
|
|
13
|
+
* the stated quantities' units carry conflicting dimensions */
|
|
14
|
+
void_reason?: string;
|
|
12
15
|
checks: MachineCheck[];
|
|
13
16
|
}
|
|
14
17
|
export declare function extractChecks(content: unknown): string[];
|
|
15
18
|
export declare function symbolsIn(checks: string[]): string[];
|
|
16
19
|
export declare function extractParams(query: string, symbols: string[]): Record<string, number>;
|
|
17
|
-
|
|
20
|
+
/** The stated values WITH their unit tokens (TODO.sota/04's unit
|
|
21
|
+
* semantics): "E_max 30000 v" binds e_max=30000 and the unit "v" —
|
|
22
|
+
* the dimension check needs both. */
|
|
23
|
+
export declare function extractParamsWithUnits(query: string, symbols: string[]): {
|
|
24
|
+
values: Record<string, number>;
|
|
25
|
+
units: Record<string, string>;
|
|
26
|
+
};
|
|
27
|
+
/** Dimension coherence (TODO.sota/04 item 2): the units plane gives
|
|
28
|
+
* each unit token a dimension; a parameter set whose bound symbols
|
|
29
|
+
* carry CONFLICTING known dimensions is incoherent — the check
|
|
30
|
+
* compares quantities that are not comparable. Pure: the dimensions
|
|
31
|
+
* map arrives from the caller (the units register at load). */
|
|
32
|
+
export declare function dimensionMismatch(check: string, units: Record<string, string>, dimensions: Record<string, string>): string | null;
|
|
33
|
+
export declare function evaluate(content: unknown, query: string, opts?: {
|
|
34
|
+
dimensions?: Record<string, string>;
|
|
35
|
+
}): Verdict | null;
|
|
18
36
|
/** The deterministic note the answer model narrates — never recomputes. */
|
|
19
37
|
export declare function verdictNote(v: Verdict, node: {
|
|
20
38
|
node_id: string;
|
|
@@ -4,12 +4,12 @@ import {
|
|
|
4
4
|
handleMemories,
|
|
5
5
|
scoreFaithfulness,
|
|
6
6
|
scoreJudge
|
|
7
|
-
} from "../../chunk-
|
|
7
|
+
} from "../../chunk-SORXE55V.js";
|
|
8
8
|
import {
|
|
9
9
|
TOOLS_REGISTRY,
|
|
10
10
|
bindModelNode,
|
|
11
11
|
modelGroundingBlock
|
|
12
|
-
} from "../../chunk-
|
|
12
|
+
} from "../../chunk-FKG5VGOL.js";
|
|
13
13
|
import {
|
|
14
14
|
handleSearch
|
|
15
15
|
} from "../../chunk-C5L55VI7.js";
|
|
@@ -1043,13 +1043,13 @@ async function callToolOnce(env, ctx, _req, tier, key, name, args, t0) {
|
|
|
1043
1043
|
// stream:false forces the JSON lane (anon defaults to SSE)
|
|
1044
1044
|
body: JSON.stringify({ ...args, stream: false })
|
|
1045
1045
|
});
|
|
1046
|
-
const { runTool, TOOLS_REGISTRY: TOOLS_REGISTRY2 } = await import("../../tools-
|
|
1046
|
+
const { runTool, TOOLS_REGISTRY: TOOLS_REGISTRY2 } = await import("../../tools-CUKFOW7S.js");
|
|
1047
1047
|
let payload;
|
|
1048
1048
|
if (TOOLS_REGISTRY2.some((t) => t.name === name && t.audiences.includes("mcp"))) {
|
|
1049
1049
|
const r = await runTool({ ...env, DB: env.DB }, { name, args }, "mcp");
|
|
1050
1050
|
payload = r ? { tool: r.name, query: r.query, result: r.output } : { error: { message: `tool ${name} returned nothing for the given arguments` } };
|
|
1051
1051
|
} else {
|
|
1052
|
-
const res = name === "ask" ? await (await import("../../ask-
|
|
1052
|
+
const res = name === "ask" ? await (await import("../../ask-3BJKTVLY.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-JDSQPI3A.js")).handleSearch(env, ctx, inner, tier, key);
|
|
1053
1053
|
payload = await res.json().catch(() => ({ error: { message: "tool transport failed", status: res.status } }));
|
|
1054
1054
|
}
|
|
1055
1055
|
telemetry(env, ctx, tier, `mcp:${name}`, null, !payload?.error, JSON.stringify(payload).length, await sha256Hex(`${name}:${JSON.stringify(args ?? {})}`), void 0, void 0, { durationMs: Date.now() - t0 });
|
package/package.json
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"playwright": "^1.62.1",
|
|
28
28
|
"yaml": "^2.9.1"
|
|
29
29
|
},
|
|
30
|
-
"version": "0.2.
|
|
30
|
+
"version": "0.2.120",
|
|
31
31
|
"description": "The Konneal engine: the publisher-agnostic build pipeline and API plane for standards intelligence (retrieval, answer contract, verdicts, evaluation).",
|
|
32
32
|
"license": "BSD-3-Clause",
|
|
33
33
|
"type": "module",
|
|
@@ -23,7 +23,7 @@ import { completeTables, completeFigures } from "./completion";
|
|
|
23
23
|
import { NO_CONTEXT, appliedContext, contextNote, namedDocumentIn, parseContext, resolveDocScope, syntheticUnderstanding } from "./context";
|
|
24
24
|
import { liveDataConfig, liveTokenFor, opCfg, opTokenMember, resolveLiveAccount, type LiveRecord } from "./livedata";
|
|
25
25
|
import { bindModelNode, bindConstraintByQuantities, licenseBoundaryNote, licenseBoundaryRefusal, licensedEntryForPackage, modelCitation, modelEcho, modelGroundingBlock, standardForDocNumber } from "./modelplane";
|
|
26
|
-
import { evaluate as machineEvaluate, verdictNote } from "./verdict";
|
|
26
|
+
import { evaluate as machineEvaluate, extractChecks, extractParamsWithUnits, symbolsIn, verdictNote } from "./verdict";
|
|
27
27
|
import { evaluateConditionSets, quantitiesIn, type ConditionVerdict } from "./conditions";
|
|
28
28
|
import { evaluateAggregation, type AggregationVerdict } from "./aggregation";
|
|
29
29
|
import { matchLicensedTopic, boundaryNoteText } from "./boundary";
|
|
@@ -946,7 +946,29 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
946
946
|
// the worker EXECUTES the bound node's machine checks against the
|
|
947
947
|
// question's stated values; the model narrates the computed verdict
|
|
948
948
|
// and the verdict BLOCK is server-built — data, never generated prose
|
|
949
|
-
|
|
949
|
+
let machineVerdict = boundModel && !boundModel.gated && !ablate?.noVerdict ? machineEvaluate(boundModel.content, q.query) : null;
|
|
950
|
+
if (machineVerdict === null && boundModel && !boundModel.gated) {
|
|
951
|
+
// unit semantics (TODO.sota/04): the stated values' units reach the
|
|
952
|
+
// units register for their dimensions; a comparison mixing
|
|
953
|
+
// dimensions voids with the reason instead of answering a number
|
|
954
|
+
try {
|
|
955
|
+
const unitTokens = [...new Set(Object.values(extractParamsWithUnits(q.query, symbolsIn(extractChecks(boundModel.content))).units).map((u) => u.toLowerCase()))];
|
|
956
|
+
if (unitTokens.length) {
|
|
957
|
+
const marks = unitTokens.map(() => "?").join(",");
|
|
958
|
+
// the register keys symbols in the symbols JSON (ascii/unicode),
|
|
959
|
+
// not in `short` (full names) — D1's json_extract reads them
|
|
960
|
+
const rows = await env.DB.prepare(
|
|
961
|
+
`SELECT lower(coalesce(json_extract(symbols, '$.ascii'), json_extract(symbols, '$.unicode'))) AS sym, dimension_ascii FROM units_db WHERE lower(coalesce(json_extract(symbols, '$.ascii'), json_extract(symbols, '$.unicode'))) IN (${marks})`,
|
|
962
|
+
)
|
|
963
|
+
.bind(...unitTokens).all().catch(() => ({ results: [] }));
|
|
964
|
+
const dimensions: Record<string, string> = {};
|
|
965
|
+
for (const r of (rows.results ?? []) as any[]) dimensions[String(r.sym)] = String(r.dimension_ascii ?? "");
|
|
966
|
+
machineVerdict = machineEvaluate(boundModel.content, q.query, { dimensions });
|
|
967
|
+
}
|
|
968
|
+
} catch {
|
|
969
|
+
// the plain evaluation above already ran; nothing to salvage
|
|
970
|
+
}
|
|
971
|
+
}
|
|
950
972
|
const machineNote = machineVerdict && boundModel ? verdictNote(machineVerdict, boundModel) : undefined;
|
|
951
973
|
// ── condition-set membership (konneal/engine#90): the test-method
|
|
952
974
|
// packages' severity menus as machine-verifiable membership. Fires
|
|
@@ -20,6 +20,9 @@ export interface Verdict {
|
|
|
20
20
|
on_violation?: string;
|
|
21
21
|
violation_meaning?: string;
|
|
22
22
|
missing: string[];
|
|
23
|
+
/** why a fully-stated question still cannot evaluate (TODO.sota/04):
|
|
24
|
+
* the stated quantities' units carry conflicting dimensions */
|
|
25
|
+
void_reason?: string;
|
|
23
26
|
checks: MachineCheck[];
|
|
24
27
|
}
|
|
25
28
|
|
|
@@ -190,29 +193,72 @@ function parseNumber(raw: string): number {
|
|
|
190
193
|
}
|
|
191
194
|
|
|
192
195
|
export function extractParams(query: string, symbols: string[]): Record<string, number> {
|
|
193
|
-
|
|
196
|
+
return extractParamsWithUnits(query, symbols).values;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The stated values WITH their unit tokens (TODO.sota/04's unit
|
|
200
|
+
* semantics): "E_max 30000 v" binds e_max=30000 and the unit "v" —
|
|
201
|
+
* the dimension check needs both. */
|
|
202
|
+
export function extractParamsWithUnits(
|
|
203
|
+
query: string,
|
|
204
|
+
symbols: string[],
|
|
205
|
+
): { values: Record<string, number>; units: Record<string, string> } {
|
|
206
|
+
const values: Record<string, number> = {};
|
|
207
|
+
const units: Record<string, string> = {};
|
|
194
208
|
for (const sym of symbols) {
|
|
195
209
|
const leaf = sym.split(".").pop() ?? sym;
|
|
196
210
|
const esc = leaf.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
197
211
|
// symbol, a short non-numeric gap (words like "were"/"of" allowed),
|
|
198
|
-
// then the number
|
|
199
|
-
const re = new RegExp(`\\b${esc}\\b\\D{0,14}?([0-9][0-9 ,.]*[0-9])
|
|
212
|
+
// then the number, then an optional unit token directly after
|
|
213
|
+
const re = new RegExp(`\\b${esc}\\b\\D{0,14}?([0-9][0-9 ,.]*[0-9])\\s*(%|\\u00b0?[A-Za-z\\u00b5\\u03a9]{0,6})?`, "iu");
|
|
200
214
|
const m = query.match(re);
|
|
201
215
|
if (m) {
|
|
202
216
|
const v = parseNumber(m[1]!);
|
|
203
|
-
if (Number.isFinite(v))
|
|
217
|
+
if (Number.isFinite(v)) {
|
|
218
|
+
values[sym] = v;
|
|
219
|
+
const u = (m[2] ?? "").trim();
|
|
220
|
+
if (u) units[sym] = u;
|
|
221
|
+
}
|
|
204
222
|
}
|
|
205
223
|
}
|
|
206
|
-
return
|
|
224
|
+
return { values, units };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Dimension coherence (TODO.sota/04 item 2): the units plane gives
|
|
228
|
+
* each unit token a dimension; a parameter set whose bound symbols
|
|
229
|
+
* carry CONFLICTING known dimensions is incoherent — the check
|
|
230
|
+
* compares quantities that are not comparable. Pure: the dimensions
|
|
231
|
+
* map arrives from the caller (the units register at load). */
|
|
232
|
+
export function dimensionMismatch(
|
|
233
|
+
check: string,
|
|
234
|
+
units: Record<string, string>,
|
|
235
|
+
dimensions: Record<string, string>,
|
|
236
|
+
): string | null {
|
|
237
|
+
// the symbols THIS check compares, each mapped through its stated
|
|
238
|
+
// unit's dimension; more than one distinct dimension in one
|
|
239
|
+
// comparison is incoherent (a length is not a voltage)
|
|
240
|
+
const dims = new Map<string, string>();
|
|
241
|
+
for (const [sym, unit] of Object.entries(units)) {
|
|
242
|
+
const leaf = sym.split(".").pop();
|
|
243
|
+
if (!leaf || !new RegExp(`\\b${leaf}\\b`, "i").test(check)) continue;
|
|
244
|
+
const dim = dimensions[unit.toLowerCase()];
|
|
245
|
+
if (dim) dims.set(sym, dim);
|
|
246
|
+
}
|
|
247
|
+
const distinct = [...new Set(dims.values())];
|
|
248
|
+
if (distinct.length <= 1) return null;
|
|
249
|
+
const parts = [...dims.entries()].map(([sym, dim]) => `${sym} is in ${dim}`);
|
|
250
|
+
return `the stated quantities mix dimensions: ${parts.join(" while ")}`;
|
|
207
251
|
}
|
|
208
252
|
|
|
209
|
-
export function evaluate(content: unknown, query: string): Verdict | null {
|
|
253
|
+
export function evaluate(content: unknown, query: string, opts?: { dimensions?: Record<string, string> }): Verdict | null {
|
|
210
254
|
const c = (content && typeof content === "object" ? content : {}) as Record<string, any>;
|
|
211
255
|
const checks = extractChecks(content);
|
|
212
256
|
if (!checks.length) return null;
|
|
213
257
|
const symbols = symbolsIn(checks);
|
|
214
|
-
const
|
|
258
|
+
const bound = extractParamsWithUnits(query, symbols);
|
|
259
|
+
const params = bound.values;
|
|
215
260
|
const missing = symbols.filter((s) => params[s] === undefined);
|
|
261
|
+
let voidReason: string | null = null;
|
|
216
262
|
const machine: MachineCheck[] = checks.map((expression) => {
|
|
217
263
|
const values: Record<string, number> = {};
|
|
218
264
|
try {
|
|
@@ -220,7 +266,14 @@ export function evaluate(content: unknown, query: string): Verdict | null {
|
|
|
220
266
|
} catch { /* values stay partial */ }
|
|
221
267
|
let result: boolean | null = null;
|
|
222
268
|
if (missing.length === 0) {
|
|
223
|
-
|
|
269
|
+
// unit semantics (TODO.sota/04): a comparison mixing dimensions
|
|
270
|
+
// cannot evaluate — void with the reason, never a number answer
|
|
271
|
+
const mismatch = opts?.dimensions ? dimensionMismatch(expression, bound.units, opts.dimensions) : null;
|
|
272
|
+
if (mismatch) {
|
|
273
|
+
voidReason = voidReason ?? mismatch;
|
|
274
|
+
} else {
|
|
275
|
+
try { result = !!parseAndEval(expression, params); } catch { result = null; }
|
|
276
|
+
}
|
|
224
277
|
}
|
|
225
278
|
return { expression, symbolic: expression, values, result };
|
|
226
279
|
});
|
|
@@ -229,7 +282,10 @@ export function evaluate(content: unknown, query: string): Verdict | null {
|
|
|
229
282
|
}
|
|
230
283
|
const failed = machine.some((m) => m.result === false);
|
|
231
284
|
const evaluable = machine.some((m) => m.result !== null);
|
|
232
|
-
if (!evaluable)
|
|
285
|
+
if (!evaluable) {
|
|
286
|
+
if (voidReason) return { verdict: "void", missing: [], void_reason: voidReason, checks: machine };
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
233
289
|
return {
|
|
234
290
|
verdict: failed ? "fail" : "pass",
|
|
235
291
|
on_violation: failed ? String(c.on_violation ?? "invalid") : undefined,
|
|
@@ -249,7 +305,9 @@ export function verdictNote(v: Verdict, node: { node_id: string; clause?: { urn?
|
|
|
249
305
|
lines.push(`- ${c.expression}${vals ? ` [${vals}]` : ""} → ${c.result === null ? "not evaluated" : c.result ? "holds" : "VIOLATED"}`);
|
|
250
306
|
}
|
|
251
307
|
if (v.verdict === "void") {
|
|
252
|
-
lines.push(
|
|
308
|
+
lines.push(v.void_reason
|
|
309
|
+
? `VERDICT: VOID — ${v.void_reason}. State the mismatch plainly; the question cannot be decided as stated.`
|
|
310
|
+
: `VERDICT: VOID — the question does not state: ${v.missing.join(", ")}. Say exactly what is missing; never assume values.`);
|
|
253
311
|
} else if (v.verdict === "pass") {
|
|
254
312
|
lines.push(`VERDICT: PASS — every machine check holds at the stated values. Present this verdict, the arithmetic above, and cite the node's clause.`);
|
|
255
313
|
} else {
|