@konneal/engine 0.2.91 → 0.2.92

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.
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  handleAsk
3
- } from "./chunk-ZIDDK32O.js";
4
- import "./chunk-JHATKAWW.js";
3
+ } from "./chunk-EHABW23I.js";
4
+ import "./chunk-QBWZ6HVP.js";
5
5
  import "./chunk-3BL223VD.js";
6
6
  import "./chunk-LOBYXEVD.js";
7
7
  import "./chunk-SPKPH54J.js";
@@ -2,6 +2,8 @@ import {
2
2
  TOOL_DECLARATION,
3
3
  bindModelNode,
4
4
  contractV2,
5
+ evaluate,
6
+ evaluateConditionSets,
5
7
  licenseBoundaryNote,
6
8
  licenseBoundaryRefusal,
7
9
  licensedEntryForPackage,
@@ -9,12 +11,14 @@ import {
9
11
  modelEcho,
10
12
  modelGroundingBlock,
11
13
  parseToolCall,
14
+ quantitiesIn,
12
15
  resolveBlocks,
13
16
  runTool,
14
17
  standardForDocNumber,
15
18
  tableRetyped,
16
- toolNote
17
- } from "./chunk-JHATKAWW.js";
19
+ toolNote,
20
+ verdictNote
21
+ } from "./chunk-QBWZ6HVP.js";
18
22
  import {
19
23
  NO_CONTEXT,
20
24
  answerQuality,
@@ -436,343 +440,8 @@ async function completeFigures(db, answer, alreadyAttached, used = []) {
436
440
  return figs;
437
441
  }
438
442
 
439
- // workers/worker_public/src/verdict.ts
440
- function tokenize(src) {
441
- const toks = [];
442
- let i = 0;
443
- const s = src.replace(/\s+/g, " ");
444
- while (i < s.length) {
445
- const c = s[i];
446
- if (c === " ") {
447
- i++;
448
- continue;
449
- }
450
- if (/[0-9.]/.test(c)) {
451
- const m = s.slice(i).match(/^[0-9]*\.?[0-9]+/);
452
- toks.push({ t: "num", v: Number(m[0]) });
453
- i += m[0].length;
454
- continue;
455
- }
456
- if (/[A-Za-z_]/.test(c)) {
457
- const m = s.slice(i).match(/^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*/);
458
- toks.push({ t: "id", v: m[0] });
459
- i += m[0].length;
460
- continue;
461
- }
462
- const two = s.slice(i, i + 2);
463
- if ([">=", "<=", "==", "!="].includes(two)) {
464
- toks.push({ t: "op", v: two });
465
- i += 2;
466
- continue;
467
- }
468
- if ("+-*/()<>".includes(c)) {
469
- toks.push({ t: "op", v: c });
470
- i++;
471
- continue;
472
- }
473
- throw new Error(`bad char ${c}`);
474
- }
475
- return toks;
476
- }
477
- function parseAndEval(src, params) {
478
- const toks = tokenize(src);
479
- let p = 0;
480
- const peek = () => toks[p];
481
- const eat = (v) => {
482
- const t = toks[p++];
483
- if (v && (!t || t.t !== "op" || t.v !== v)) throw new Error(`expected ${v}`);
484
- return t;
485
- };
486
- const isKw = (k) => {
487
- const t = peek();
488
- return t && t.t === "id" && t.v.toLowerCase() === k;
489
- };
490
- function or() {
491
- let l = and();
492
- while (isKw("or")) {
493
- p++;
494
- const r = and();
495
- l = truthy(l) || truthy(r);
496
- }
497
- return l;
498
- }
499
- function and() {
500
- let l = not();
501
- while (isKw("and")) {
502
- p++;
503
- const r = not();
504
- l = truthy(l) && truthy(r);
505
- }
506
- return l;
507
- }
508
- function not() {
509
- if (isKw("not")) {
510
- p++;
511
- return !truthy(not());
512
- }
513
- return cmp();
514
- }
515
- function cmp() {
516
- const l = add();
517
- const t = peek();
518
- if (t && t.t === "op" && [">=", "<=", ">", "<", "==", "!="].includes(t.v)) {
519
- p++;
520
- const r = add();
521
- switch (t.v) {
522
- case ">=":
523
- return num3(l) >= num3(r);
524
- case "<=":
525
- return num3(l) <= num3(r);
526
- case ">":
527
- return num3(l) > num3(r);
528
- case "<":
529
- return num3(l) < num3(r);
530
- case "==":
531
- return num3(l) === num3(r);
532
- default:
533
- return num3(l) !== num3(r);
534
- }
535
- }
536
- return l;
537
- }
538
- function add() {
539
- let l = mul();
540
- for (; ; ) {
541
- const t = peek();
542
- if (t && t.t === "op" && (t.v === "+" || t.v === "-")) {
543
- p++;
544
- const r = mul();
545
- l = t.v === "+" ? num3(l) + num3(r) : num3(l) - num3(r);
546
- } else return l;
547
- }
548
- }
549
- function mul() {
550
- let l = unary();
551
- for (; ; ) {
552
- const t = peek();
553
- if (t && t.t === "op" && (t.v === "*" || t.v === "/")) {
554
- p++;
555
- const r = unary();
556
- l = t.v === "*" ? num3(l) * num3(r) : num3(l) / num3(r);
557
- } else return l;
558
- }
559
- }
560
- function unary() {
561
- const t = peek();
562
- if (t && t.t === "op" && t.v === "-") {
563
- p++;
564
- return -num3(unary());
565
- }
566
- return atom();
567
- }
568
- function atom() {
569
- const t = eat();
570
- if (!t) throw new Error("unexpected end");
571
- if (t.t === "num") return t.v;
572
- if (t.t === "id") {
573
- if (params[t.v] !== void 0) return params[t.v];
574
- throw new Error(`missing ${t.v}`);
575
- }
576
- if (t.v === "(") {
577
- const v = or();
578
- eat(")");
579
- return v;
580
- }
581
- throw new Error(`unexpected ${t.v}`);
582
- }
583
- const truthy = (v) => typeof v === "boolean" ? v : v !== 0;
584
- const num3 = (v) => typeof v === "boolean" ? v ? 1 : 0 : v;
585
- const out = or();
586
- if (p !== toks.length) throw new Error("trailing tokens");
587
- return out;
588
- }
589
- function oclBody(s) {
590
- const m = String(s ?? "").match(/ocl\{([\s\S]*?)\}/);
591
- return m ? m[1].trim() : null;
592
- }
593
- function extractChecks(content) {
594
- if (!content || typeof content !== "object") return [];
595
- const c = content;
596
- const out = [];
597
- const push = (e) => {
598
- const b = e && oclBody(e);
599
- if (b) out.push(b);
600
- };
601
- push(c.check);
602
- push(c.limit?.expression);
603
- push(c.acceptance_criteria?.limit && !c.acceptance_criteria.limit.expression?.includes("ocl{") ? null : c.acceptance_criteria?.limit?.expression);
604
- const st = c.acceptance_criteria?.limit;
605
- if (st?.expression && st.operator && st.threshold_expression) {
606
- out.push(`${st.expression} ${st.operator} ${st.threshold_expression}`);
607
- }
608
- return [...new Set(out)];
609
- }
610
- function symbolsIn(checks) {
611
- const ids = /* @__PURE__ */ new Set();
612
- const KEYWORDS = /* @__PURE__ */ new Set(["and", "or", "not"]);
613
- for (const chk of checks) {
614
- try {
615
- for (const t of tokenize(chk)) if (t.t === "id" && !KEYWORDS.has(t.v.toLowerCase())) ids.add(t.v);
616
- } catch {
617
- }
618
- }
619
- return [...ids];
620
- }
621
- function parseNumber(raw) {
622
- let s = raw.replace(/[ ,]/g, "");
623
- s = s.replace(/\.(\d{3})$/, "$1");
624
- return Number(s.replace(/,(?=\d{3}\b)/g, ""));
625
- }
626
- function extractParams(query, symbols) {
627
- const params = {};
628
- for (const sym of symbols) {
629
- const leaf = sym.split(".").pop() ?? sym;
630
- const esc = leaf.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
631
- const re = new RegExp(`\\b${esc}\\b\\D{0,14}?([0-9][0-9 ,.]*[0-9])`, "iu");
632
- const m = query.match(re);
633
- if (m) {
634
- const v = parseNumber(m[1]);
635
- if (Number.isFinite(v)) params[sym] = v;
636
- }
637
- }
638
- return params;
639
- }
640
- function evaluate(content, query) {
641
- const c = content && typeof content === "object" ? content : {};
642
- const checks = extractChecks(content);
643
- if (!checks.length) return null;
644
- const symbols = symbolsIn(checks);
645
- const params = extractParams(query, symbols);
646
- const missing = symbols.filter((s) => params[s] === void 0);
647
- const machine = checks.map((expression) => {
648
- const values = {};
649
- try {
650
- for (const t of tokenize(expression)) if (t.t === "id" && params[t.v] !== void 0) values[t.v] = params[t.v];
651
- } catch {
652
- }
653
- let result = null;
654
- if (missing.length === 0) {
655
- try {
656
- result = !!parseAndEval(expression, params);
657
- } catch {
658
- result = null;
659
- }
660
- }
661
- return { expression, symbolic: expression, values, result };
662
- });
663
- if (missing.length) {
664
- return { verdict: "void", missing, checks: machine };
665
- }
666
- const failed = machine.some((m) => m.result === false);
667
- const evaluable = machine.some((m) => m.result !== null);
668
- if (!evaluable) return null;
669
- return {
670
- verdict: failed ? "fail" : "pass",
671
- on_violation: failed ? String(c.on_violation ?? "invalid") : void 0,
672
- violation_meaning: failed ? typeof c.violation_meaning === "string" ? c.violation_meaning : void 0 : void 0,
673
- missing: [],
674
- checks: machine
675
- };
676
- }
677
- function verdictNote(v, node) {
678
- const lines = [
679
- `Machine verdict (deterministic evaluation of node ${node.node_id}${node.clause?.urn ? `, ${node.clause.urn}` : ""}) \u2014 the service EXECUTED the node's machine check against the values stated in the question:`
680
- ];
681
- for (const c of v.checks) {
682
- const vals = Object.entries(c.values).map(([k, n]) => `${k}=${n}`).join(", ");
683
- lines.push(`- ${c.expression}${vals ? ` [${vals}]` : ""} \u2192 ${c.result === null ? "not evaluated" : c.result ? "holds" : "VIOLATED"}`);
684
- }
685
- if (v.verdict === "void") {
686
- lines.push(`VERDICT: VOID \u2014 the question does not state: ${v.missing.join(", ")}. Say exactly what is missing; never assume values.`);
687
- } else if (v.verdict === "pass") {
688
- 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.`);
689
- } else {
690
- lines.push(`VERDICT: ${String(v.on_violation ?? "FAIL").toUpperCase()} \u2014 a machine check is violated. Present this verdict, the arithmetic, the violation meaning verbatim, and cite the node's clause.`);
691
- }
692
- lines.push("This verdict is computed data \u2014 quote it faithfully; do not recompute, soften, or contradict it.");
693
- return lines.join("\n");
694
- }
695
-
696
- // workers/worker_public/src/conditions.ts
697
- var NUM = String.raw`-?\d+(?:[.,]\d+)?`;
698
- function quantitiesIn(query) {
699
- const out = {};
700
- const num3 = (s) => Number(s.replace(",", "."));
701
- const put = (kind, stated, stated_unit, si) => {
702
- if (Number.isFinite(si)) out[kind] = { stated, stated_unit, si };
703
- };
704
- const tempC = query.match(new RegExp(`(${NUM})\\s*(?:\xB0\\s*)?C\\b`));
705
- if (tempC) put("temperature", num3(tempC[1]), "degC", num3(tempC[1]) + 273.15);
706
- const tempK = query.match(new RegExp(`(${NUM})\\s*K\\b`));
707
- if (tempK && out.temperature === void 0) put("temperature", num3(tempK[1]), "K", num3(tempK[1]));
708
- const rh = query.match(new RegExp(`(${NUM})\\s*%\\s*(?:RH\\b|relative\\s+humidity)?`, "i"));
709
- if (rh) put("relative_humidity", num3(rh[1]), "%", num3(rh[1]) / 100);
710
- const hours = query.match(new RegExp(`(${NUM})\\s*h\\b`, "i"));
711
- if (hours) put("duration", num3(hours[1]), "h", num3(hours[1]) * 3600);
712
- const days = query.match(new RegExp(`(${NUM})\\s*days?\\b`, "i"));
713
- if (days && out.duration === void 0) put("duration", num3(days[1]), "d", num3(days[1]) * 86400);
714
- return out;
715
- }
716
- function scoreSet(entries, q) {
717
- const checks = [];
718
- let distance = 0;
719
- let stated = 0;
720
- for (const e of entries) {
721
- const siEntry = e;
722
- const statedQ = q[e.quantity_kind];
723
- if (!statedQ || !siEntry?.si) continue;
724
- const tol = Number(String(siEntry.tolerance ?? "0").replace(",", "."));
725
- const tolSi = (Number.isFinite(tol) ? tol : 0) * (siEntry.si.unit === "K" ? 1 : siEntry.si.unit === "1" ? 0.01 : 1);
726
- const in_band = statedQ.si >= siEntry.si.value - tolSi && statedQ.si <= siEntry.si.value + tolSi;
727
- const gap = Math.max(0, statedQ.si - (siEntry.si.value + tolSi), siEntry.si.value - tolSi - statedQ.si);
728
- distance += gap / Math.max(tolSi, 1);
729
- stated += 1;
730
- checks.push({
731
- quantity_kind: e.quantity_kind,
732
- band: `${siEntry.si.value} ${siEntry.si.unit} \xB1${tolSi}`,
733
- stated: statedQ.stated,
734
- stated_unit: statedQ.stated_unit,
735
- in_band
736
- });
737
- }
738
- return stated ? { checks, distance, stated } : null;
739
- }
740
- function evaluateConditionSets(nodes, query) {
741
- const q = quantitiesIn(query);
742
- const kinds = Object.keys(q);
743
- if (!kinds.length) return null;
744
- const scored = [];
745
- for (const n of nodes) {
746
- const c = n.content && typeof n.content === "object" ? n.content : {};
747
- const payload = c.payload ?? {};
748
- const entries = Array.isArray(payload.entries) ? payload.entries : [];
749
- if (!entries.length) continue;
750
- const s = scoreSet(entries, q);
751
- if (s) scored.push({ node_id: n.node_id, checks: s.checks, distance: s.distance });
752
- }
753
- if (!scored.length) return null;
754
- const matched = scored.filter((s) => s.checks.every((c) => c.in_band));
755
- scored.sort((a, b) => a.distance - b.distance);
756
- if (matched.length) {
757
- return {
758
- verdict: "pass",
759
- matched: matched.map((m) => m.node_id),
760
- checks: matched[0].checks,
761
- note: `VERDICT: PASS \u2014 the stated combination (${kinds.join(", ")}) matches a severity set (${matched[0].checks.map((c) => c.band).join("; ")}). Present this verdict and cite the set's clause. The machine set identifier rides the verdict block as data \u2014 never write it in your prose.`
762
- };
763
- }
764
- const nearest = scored[0];
765
- return {
766
- verdict: "fail",
767
- matched: [],
768
- nearest: { node_id: nearest.node_id, distance: Number(nearest.distance.toFixed(2)), bands: nearest.checks.map((c) => c.band) },
769
- checks: nearest.checks,
770
- note: `VERDICT: FAIL \u2014 no severity set admits the stated combination. The nearest set (bands: ${nearest.checks.map((c) => c.band).join("; ")}) is the closest match. Say the combination is outside the menu and describe the nearest set's bands; never soften it. The machine set identifier rides the verdict block as data \u2014 never write it in your prose.`
771
- };
772
- }
773
-
774
443
  // workers/worker_public/src/aggregation.ts
775
- var NUM2 = String.raw`-?\d+(?:[,\s]?\d{3})*(?:[.,]\d+)?`;
444
+ var NUM = String.raw`-?\d+(?:[,\s]?\d{3})*(?:[.,]\d+)?`;
776
445
  var UNIT_WORDS = {
777
446
  kg: ["mass", "load", "weight"],
778
447
  s: ["time", "duration", "second"],
@@ -836,7 +505,7 @@ function intervalPairs(cols) {
836
505
  }
837
506
  function statedInInterval(query, unit) {
838
507
  if (!unit) return null;
839
- const re = new RegExp(`(${NUM2})\\s*${unit.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&")}\\b`, "i");
508
+ const re = new RegExp(`(${NUM})\\s*${unit.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&")}\\b`, "i");
840
509
  const m = query.match(re);
841
510
  return m ? num2(m[1]) : null;
842
511
  }
@@ -871,7 +540,7 @@ function classValueExists(rows, cols, token) {
871
540
  }
872
541
  function statedUnits(query) {
873
542
  const out = /* @__PURE__ */ new Set();
874
- const re = new RegExp(`(${NUM2})\\s*([%\xB0a-zA-Z][a-zA-Z/.%\xB0]*\\b)`, "g");
543
+ const re = new RegExp(`(${NUM})\\s*([%\xB0a-zA-Z][a-zA-Z/.%\xB0]*\\b)`, "g");
875
544
  for (const m of query.matchAll(re)) {
876
545
  if (m[2]) out.add(m[2].replace("\u2062", "").trim());
877
546
  }
@@ -241,6 +241,341 @@ function modelEcho(node) {
241
241
  };
242
242
  }
243
243
 
244
+ // workers/worker_public/src/verdict.ts
245
+ function tokenize(src) {
246
+ const toks = [];
247
+ let i = 0;
248
+ const s = src.replace(/\s+/g, " ");
249
+ while (i < s.length) {
250
+ const c = s[i];
251
+ if (c === " ") {
252
+ i++;
253
+ continue;
254
+ }
255
+ if (/[0-9.]/.test(c)) {
256
+ const m = s.slice(i).match(/^[0-9]*\.?[0-9]+/);
257
+ toks.push({ t: "num", v: Number(m[0]) });
258
+ i += m[0].length;
259
+ continue;
260
+ }
261
+ if (/[A-Za-z_]/.test(c)) {
262
+ const m = s.slice(i).match(/^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*/);
263
+ toks.push({ t: "id", v: m[0] });
264
+ i += m[0].length;
265
+ continue;
266
+ }
267
+ const two = s.slice(i, i + 2);
268
+ if ([">=", "<=", "==", "!="].includes(two)) {
269
+ toks.push({ t: "op", v: two });
270
+ i += 2;
271
+ continue;
272
+ }
273
+ if ("+-*/()<>".includes(c)) {
274
+ toks.push({ t: "op", v: c });
275
+ i++;
276
+ continue;
277
+ }
278
+ throw new Error(`bad char ${c}`);
279
+ }
280
+ return toks;
281
+ }
282
+ function parseAndEval(src, params) {
283
+ const toks = tokenize(src);
284
+ let p = 0;
285
+ const peek = () => toks[p];
286
+ const eat = (v) => {
287
+ const t = toks[p++];
288
+ if (v && (!t || t.t !== "op" || t.v !== v)) throw new Error(`expected ${v}`);
289
+ return t;
290
+ };
291
+ const isKw = (k) => {
292
+ const t = peek();
293
+ return t && t.t === "id" && t.v.toLowerCase() === k;
294
+ };
295
+ function or() {
296
+ let l = and();
297
+ while (isKw("or")) {
298
+ p++;
299
+ const r = and();
300
+ l = truthy(l) || truthy(r);
301
+ }
302
+ return l;
303
+ }
304
+ function and() {
305
+ let l = not();
306
+ while (isKw("and")) {
307
+ p++;
308
+ const r = not();
309
+ l = truthy(l) && truthy(r);
310
+ }
311
+ return l;
312
+ }
313
+ function not() {
314
+ if (isKw("not")) {
315
+ p++;
316
+ return !truthy(not());
317
+ }
318
+ return cmp();
319
+ }
320
+ function cmp() {
321
+ const l = add();
322
+ const t = peek();
323
+ if (t && t.t === "op" && [">=", "<=", ">", "<", "==", "!="].includes(t.v)) {
324
+ p++;
325
+ const r = add();
326
+ switch (t.v) {
327
+ case ">=":
328
+ return num(l) >= num(r);
329
+ case "<=":
330
+ return num(l) <= num(r);
331
+ case ">":
332
+ return num(l) > num(r);
333
+ case "<":
334
+ return num(l) < num(r);
335
+ case "==":
336
+ return num(l) === num(r);
337
+ default:
338
+ return num(l) !== num(r);
339
+ }
340
+ }
341
+ return l;
342
+ }
343
+ function add() {
344
+ let l = mul();
345
+ for (; ; ) {
346
+ const t = peek();
347
+ if (t && t.t === "op" && (t.v === "+" || t.v === "-")) {
348
+ p++;
349
+ const r = mul();
350
+ l = t.v === "+" ? num(l) + num(r) : num(l) - num(r);
351
+ } else return l;
352
+ }
353
+ }
354
+ function mul() {
355
+ let l = unary();
356
+ for (; ; ) {
357
+ const t = peek();
358
+ if (t && t.t === "op" && (t.v === "*" || t.v === "/")) {
359
+ p++;
360
+ const r = unary();
361
+ l = t.v === "*" ? num(l) * num(r) : num(l) / num(r);
362
+ } else return l;
363
+ }
364
+ }
365
+ function unary() {
366
+ const t = peek();
367
+ if (t && t.t === "op" && t.v === "-") {
368
+ p++;
369
+ return -num(unary());
370
+ }
371
+ return atom();
372
+ }
373
+ function atom() {
374
+ const t = eat();
375
+ if (!t) throw new Error("unexpected end");
376
+ if (t.t === "num") return t.v;
377
+ if (t.t === "id") {
378
+ if (params[t.v] !== void 0) return params[t.v];
379
+ throw new Error(`missing ${t.v}`);
380
+ }
381
+ if (t.v === "(") {
382
+ const v = or();
383
+ eat(")");
384
+ return v;
385
+ }
386
+ throw new Error(`unexpected ${t.v}`);
387
+ }
388
+ const truthy = (v) => typeof v === "boolean" ? v : v !== 0;
389
+ const num = (v) => typeof v === "boolean" ? v ? 1 : 0 : v;
390
+ const out = or();
391
+ if (p !== toks.length) throw new Error("trailing tokens");
392
+ return out;
393
+ }
394
+ function oclBody(s) {
395
+ const m = String(s ?? "").match(/ocl\{([\s\S]*?)\}/);
396
+ return m ? m[1].trim() : null;
397
+ }
398
+ function extractChecks(content) {
399
+ if (!content || typeof content !== "object") return [];
400
+ const c = content;
401
+ const out = [];
402
+ const push = (e) => {
403
+ const b = e && oclBody(e);
404
+ if (b) out.push(b);
405
+ };
406
+ push(c.check);
407
+ push(c.limit?.expression);
408
+ push(c.acceptance_criteria?.limit && !c.acceptance_criteria.limit.expression?.includes("ocl{") ? null : c.acceptance_criteria?.limit?.expression);
409
+ const st = c.acceptance_criteria?.limit;
410
+ if (st?.expression && st.operator && st.threshold_expression) {
411
+ out.push(`${st.expression} ${st.operator} ${st.threshold_expression}`);
412
+ }
413
+ return [...new Set(out)];
414
+ }
415
+ function symbolsIn(checks) {
416
+ const ids = /* @__PURE__ */ new Set();
417
+ const KEYWORDS = /* @__PURE__ */ new Set(["and", "or", "not"]);
418
+ for (const chk of checks) {
419
+ try {
420
+ for (const t of tokenize(chk)) if (t.t === "id" && !KEYWORDS.has(t.v.toLowerCase())) ids.add(t.v);
421
+ } catch {
422
+ }
423
+ }
424
+ return [...ids];
425
+ }
426
+ function parseNumber(raw) {
427
+ let s = raw.replace(/[ ,]/g, "");
428
+ s = s.replace(/\.(\d{3})$/, "$1");
429
+ return Number(s.replace(/,(?=\d{3}\b)/g, ""));
430
+ }
431
+ function extractParams(query, symbols) {
432
+ const params = {};
433
+ for (const sym of symbols) {
434
+ const leaf = sym.split(".").pop() ?? sym;
435
+ const esc = leaf.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
436
+ const re = new RegExp(`\\b${esc}\\b\\D{0,14}?([0-9][0-9 ,.]*[0-9])`, "iu");
437
+ const m = query.match(re);
438
+ if (m) {
439
+ const v = parseNumber(m[1]);
440
+ if (Number.isFinite(v)) params[sym] = v;
441
+ }
442
+ }
443
+ return params;
444
+ }
445
+ function evaluate(content, query) {
446
+ const c = content && typeof content === "object" ? content : {};
447
+ const checks = extractChecks(content);
448
+ if (!checks.length) return null;
449
+ const symbols = symbolsIn(checks);
450
+ const params = extractParams(query, symbols);
451
+ const missing = symbols.filter((s) => params[s] === void 0);
452
+ const machine = checks.map((expression) => {
453
+ const values = {};
454
+ try {
455
+ for (const t of tokenize(expression)) if (t.t === "id" && params[t.v] !== void 0) values[t.v] = params[t.v];
456
+ } catch {
457
+ }
458
+ let result = null;
459
+ if (missing.length === 0) {
460
+ try {
461
+ result = !!parseAndEval(expression, params);
462
+ } catch {
463
+ result = null;
464
+ }
465
+ }
466
+ return { expression, symbolic: expression, values, result };
467
+ });
468
+ if (missing.length) {
469
+ return { verdict: "void", missing, checks: machine };
470
+ }
471
+ const failed = machine.some((m) => m.result === false);
472
+ const evaluable = machine.some((m) => m.result !== null);
473
+ if (!evaluable) return null;
474
+ return {
475
+ verdict: failed ? "fail" : "pass",
476
+ on_violation: failed ? String(c.on_violation ?? "invalid") : void 0,
477
+ violation_meaning: failed ? typeof c.violation_meaning === "string" ? c.violation_meaning : void 0 : void 0,
478
+ missing: [],
479
+ checks: machine
480
+ };
481
+ }
482
+ function verdictNote(v, node) {
483
+ const lines = [
484
+ `Machine verdict (deterministic evaluation of node ${node.node_id}${node.clause?.urn ? `, ${node.clause.urn}` : ""}) \u2014 the service EXECUTED the node's machine check against the values stated in the question:`
485
+ ];
486
+ for (const c of v.checks) {
487
+ const vals = Object.entries(c.values).map(([k, n]) => `${k}=${n}`).join(", ");
488
+ lines.push(`- ${c.expression}${vals ? ` [${vals}]` : ""} \u2192 ${c.result === null ? "not evaluated" : c.result ? "holds" : "VIOLATED"}`);
489
+ }
490
+ if (v.verdict === "void") {
491
+ lines.push(`VERDICT: VOID \u2014 the question does not state: ${v.missing.join(", ")}. Say exactly what is missing; never assume values.`);
492
+ } else if (v.verdict === "pass") {
493
+ 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.`);
494
+ } else {
495
+ lines.push(`VERDICT: ${String(v.on_violation ?? "FAIL").toUpperCase()} \u2014 a machine check is violated. Present this verdict, the arithmetic, the violation meaning verbatim, and cite the node's clause.`);
496
+ }
497
+ lines.push("This verdict is computed data \u2014 quote it faithfully; do not recompute, soften, or contradict it.");
498
+ return lines.join("\n");
499
+ }
500
+
501
+ // workers/worker_public/src/conditions.ts
502
+ var NUM = String.raw`-?\d+(?:[.,]\d+)?`;
503
+ function quantitiesIn(query) {
504
+ const out = {};
505
+ const num = (s) => Number(s.replace(",", "."));
506
+ const put = (kind, stated, stated_unit, si) => {
507
+ if (Number.isFinite(si)) out[kind] = { stated, stated_unit, si };
508
+ };
509
+ const tempC = query.match(new RegExp(`(${NUM})\\s*(?:\xB0\\s*)?C\\b`));
510
+ if (tempC) put("temperature", num(tempC[1]), "degC", num(tempC[1]) + 273.15);
511
+ const tempK = query.match(new RegExp(`(${NUM})\\s*K\\b`));
512
+ if (tempK && out.temperature === void 0) put("temperature", num(tempK[1]), "K", num(tempK[1]));
513
+ const rh = query.match(new RegExp(`(${NUM})\\s*%\\s*(?:RH\\b|relative\\s+humidity)?`, "i"));
514
+ if (rh) put("relative_humidity", num(rh[1]), "%", num(rh[1]) / 100);
515
+ const hours = query.match(new RegExp(`(${NUM})\\s*h\\b`, "i"));
516
+ if (hours) put("duration", num(hours[1]), "h", num(hours[1]) * 3600);
517
+ const days = query.match(new RegExp(`(${NUM})\\s*days?\\b`, "i"));
518
+ if (days && out.duration === void 0) put("duration", num(days[1]), "d", num(days[1]) * 86400);
519
+ return out;
520
+ }
521
+ function scoreSet(entries, q) {
522
+ const checks = [];
523
+ let distance = 0;
524
+ let stated = 0;
525
+ for (const e of entries) {
526
+ const siEntry = e;
527
+ const statedQ = q[e.quantity_kind];
528
+ if (!statedQ || !siEntry?.si) continue;
529
+ const tol = Number(String(siEntry.tolerance ?? "0").replace(",", "."));
530
+ const tolSi = (Number.isFinite(tol) ? tol : 0) * (siEntry.si.unit === "K" ? 1 : siEntry.si.unit === "1" ? 0.01 : 1);
531
+ const in_band = statedQ.si >= siEntry.si.value - tolSi && statedQ.si <= siEntry.si.value + tolSi;
532
+ const gap = Math.max(0, statedQ.si - (siEntry.si.value + tolSi), siEntry.si.value - tolSi - statedQ.si);
533
+ distance += gap / Math.max(tolSi, 1);
534
+ stated += 1;
535
+ checks.push({
536
+ quantity_kind: e.quantity_kind,
537
+ band: `${siEntry.si.value} ${siEntry.si.unit} \xB1${tolSi}`,
538
+ stated: statedQ.stated,
539
+ stated_unit: statedQ.stated_unit,
540
+ in_band
541
+ });
542
+ }
543
+ return stated ? { checks, distance, stated } : null;
544
+ }
545
+ function evaluateConditionSets(nodes, query) {
546
+ const q = quantitiesIn(query);
547
+ const kinds = Object.keys(q);
548
+ if (!kinds.length) return null;
549
+ const scored = [];
550
+ for (const n of nodes) {
551
+ const c = n.content && typeof n.content === "object" ? n.content : {};
552
+ const payload = c.payload ?? {};
553
+ const entries = Array.isArray(payload.entries) ? payload.entries : [];
554
+ if (!entries.length) continue;
555
+ const s = scoreSet(entries, q);
556
+ if (s) scored.push({ node_id: n.node_id, checks: s.checks, distance: s.distance });
557
+ }
558
+ if (!scored.length) return null;
559
+ const matched = scored.filter((s) => s.checks.every((c) => c.in_band));
560
+ scored.sort((a, b) => a.distance - b.distance);
561
+ if (matched.length) {
562
+ return {
563
+ verdict: "pass",
564
+ matched: matched.map((m) => m.node_id),
565
+ checks: matched[0].checks,
566
+ note: `VERDICT: PASS \u2014 the stated combination (${kinds.join(", ")}) matches a severity set (${matched[0].checks.map((c) => c.band).join("; ")}). Present this verdict and cite the set's clause. The machine set identifier rides the verdict block as data \u2014 never write it in your prose.`
567
+ };
568
+ }
569
+ const nearest = scored[0];
570
+ return {
571
+ verdict: "fail",
572
+ matched: [],
573
+ nearest: { node_id: nearest.node_id, distance: Number(nearest.distance.toFixed(2)), bands: nearest.checks.map((c) => c.band) },
574
+ checks: nearest.checks,
575
+ note: `VERDICT: FAIL \u2014 no severity set admits the stated combination. The nearest set (bands: ${nearest.checks.map((c) => c.band).join("; ")}) is the closest match. Say the combination is outside the menu and describe the nearest set's bands; never soften it. The machine set identifier rides the verdict block as data \u2014 never write it in your prose.`
576
+ };
577
+ }
578
+
244
579
  // workers/worker_public/src/tools-content.ts
245
580
  var slug = (docidentifier) => docidentifier.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
246
581
  var unitsGet = {
@@ -414,6 +749,108 @@ var licensedSection = {
414
749
  }
415
750
  }
416
751
  };
752
+ var verdictEvaluate = {
753
+ name: "verdict.evaluate",
754
+ description: "Deterministically evaluate a model-plane node's machine checks (OCL boolean expressions, threshold limits) against the quantities a question states. Returns pass/fail with every check's expression and values, or void with the missing parameter names when the question does not state enough. The machine computes; cite the node's clause.",
755
+ params: [
756
+ { key: "node_id", required: true, description: "the model-plane node id, e.g. /req/metrological/repeatability (node ids appear in answers' verdict blocks)" },
757
+ { key: "question", required: true, description: "the statement carrying the quantities, e.g. 'is mpe 0.02 with n_lc 3000 within the limit?'" }
758
+ ],
759
+ audiences: ["mcp"],
760
+ handler: async (env, args) => {
761
+ const node_id = String(args?.node_id ?? "").trim();
762
+ const question = String(args?.question ?? "").trim();
763
+ if (!node_id.startsWith("/") || question.length < 4) return null;
764
+ try {
765
+ const rows = await env.DB.prepare(
766
+ "SELECT standard, kind, name, content FROM model_nodes WHERE node_id = ?1 LIMIT 2"
767
+ ).bind(node_id).all();
768
+ const found = rows.results ?? [];
769
+ if (found.length !== 1) {
770
+ return {
771
+ name: "verdict.evaluate",
772
+ query: node_id,
773
+ output: found.length ? `The node id ${node_id} is indexed under several standards \u2014 name the standard to disambiguate. State this plainly.` : `No model-plane node ${node_id} is indexed. State this plainly.`
774
+ };
775
+ }
776
+ const node = found[0];
777
+ const entry = licensedEntryForPackage(node.standard);
778
+ if (entry) {
779
+ return {
780
+ name: "verdict.evaluate",
781
+ query: node_id,
782
+ output: licenseBoundaryRefusal(entry.doc_number, null) ?? "License boundary \u2014 the node belongs to a licensed standard the caller's entitlement set does not cover."
783
+ };
784
+ }
785
+ let content;
786
+ try {
787
+ content = JSON.parse(node.content);
788
+ } catch {
789
+ content = null;
790
+ }
791
+ const v = evaluate(content, question);
792
+ if (!v) {
793
+ return {
794
+ name: "verdict.evaluate",
795
+ query: node_id,
796
+ output: `The node ${node_id} (${node.kind}) carries no machine-checkable expressions \u2014 it is not evaluatable by the verdict engine. State this plainly.`
797
+ };
798
+ }
799
+ return {
800
+ name: "verdict.evaluate",
801
+ query: node_id,
802
+ output: JSON.stringify({ standard: node.standard, node_id, kind: node.kind, name: node.name, verdict: v.verdict, on_violation: v.on_violation, missing: v.missing, checks: v.checks })
803
+ };
804
+ } catch {
805
+ return null;
806
+ }
807
+ }
808
+ };
809
+ var conditionsCheck = {
810
+ name: "conditions.check",
811
+ description: "Check a stated combination of environmental quantities (temperature, humidity, duration, cycles) against the indexed severity/condition sets \u2014 the pass answer names the matched set and every band, the fail names the nearest set and its distance. Machine evaluation over the condition-set nodes; cite the matched set's clause.",
812
+ params: [
813
+ { key: "question", required: true, description: "the stated combination, e.g. 'damp heat cyclic test at 55 \xB0C for 2 cycles of 24 h'" },
814
+ { key: "standard", required: false, description: "optional scope, the package id, e.g. iec-60068-2-30 (defaults to every indexed condition set the caller may see)" },
815
+ { key: "licensed_standards", required: false, description: "license entitlement keys for licensed condition sets, validated against the deployment's declared licenses" }
816
+ ],
817
+ audiences: ["mcp"],
818
+ handler: async (env, args) => {
819
+ const question = String(args?.question ?? "").trim();
820
+ if (question.length < 4) return null;
821
+ const standard = args?.standard ? String(args.standard).trim() : null;
822
+ const keys = standardKeysFrom({ licensed_standards: args?.licensed_standards });
823
+ try {
824
+ const rows = standard ? await env.DB.prepare("SELECT node_id, standard, content FROM model_nodes WHERE kind = 'condition_set' AND standard = ?1").bind(standard).all() : await env.DB.prepare("SELECT node_id, standard, content FROM model_nodes WHERE kind = 'condition_set'").all();
825
+ const all = (rows.results ?? []).map((r) => {
826
+ let content = r.content;
827
+ try {
828
+ content = JSON.parse(r.content);
829
+ } catch {
830
+ }
831
+ return { node_id: r.node_id, standard: r.standard, content };
832
+ });
833
+ const visible = all.filter((n) => {
834
+ const entry = licensedEntryForPackage(n.standard);
835
+ return !entry || keys.has(entry.key);
836
+ });
837
+ if (!visible.length) {
838
+ return { name: "conditions.check", query: question, output: "No condition sets are indexed (or visible to this caller's entitlements). State this plainly." };
839
+ }
840
+ const v = evaluateConditionSets(visible, question);
841
+ if (!v) {
842
+ return { name: "conditions.check", query: question, output: "No quantities were recognized in the statement \u2014 state the values (temperature, humidity, duration, cycles). The engine checks stated numbers, never guesses." };
843
+ }
844
+ return {
845
+ name: "conditions.check",
846
+ query: question,
847
+ output: JSON.stringify({ verdict: v.verdict, matched: v.matched, checks: v.checks })
848
+ };
849
+ } catch {
850
+ return null;
851
+ }
852
+ }
853
+ };
417
854
  var glossaryLookup = {
418
855
  name: "glossary.lookup",
419
856
  description: "Look up a defined term in the terminology datasets \u2014 the concept's definition and its defining publication. Bind everyday words to the defined term before interpreting a question.",
@@ -482,7 +919,7 @@ var certificatesSearch = {
482
919
  return { name: "certificates.search", query, output };
483
920
  }
484
921
  };
485
- var TOOLS_REGISTRY = [certificatesSearch, unitsGet, graphCites, docsSection, glossaryLookup, documentsFamily, licensedSection];
922
+ var TOOLS_REGISTRY = [certificatesSearch, unitsGet, graphCites, docsSection, glossaryLookup, documentsFamily, licensedSection, verdictEvaluate, conditionsCheck];
486
923
  var TOOL_DECLARATION = [
487
924
  "You may use one tool before answering, by writing a single line:",
488
925
  ...TOOLS_REGISTRY.filter((t) => t.audiences.includes("agent")).map((t) => {
@@ -513,6 +950,10 @@ export {
513
950
  resolveBlocks,
514
951
  contractV2,
515
952
  tableRetyped,
953
+ evaluate,
954
+ verdictNote,
955
+ quantitiesIn,
956
+ evaluateConditionSets,
516
957
  parseToolCall,
517
958
  TOOLS_REGISTRY,
518
959
  TOOL_DECLARATION,
@@ -4,7 +4,7 @@ import {
4
4
  parseToolCall,
5
5
  runTool,
6
6
  toolNote
7
- } from "./chunk-JHATKAWW.js";
7
+ } from "./chunk-QBWZ6HVP.js";
8
8
  import "./chunk-LOBYXEVD.js";
9
9
  import "./chunk-LVZTWUVJ.js";
10
10
  import "./chunk-2PCUAJJO.js";
@@ -4,4 +4,6 @@ export declare const graphCites: ToolSpec;
4
4
  export declare const docsSection: ToolSpec;
5
5
  export declare const documentsFamily: ToolSpec;
6
6
  export declare const licensedSection: ToolSpec;
7
+ export declare const verdictEvaluate: ToolSpec;
8
+ export declare const conditionsCheck: ToolSpec;
7
9
  export declare const glossaryLookup: ToolSpec;
@@ -4,12 +4,12 @@ import {
4
4
  handleMemories,
5
5
  scoreFaithfulness,
6
6
  scoreJudge
7
- } from "../../chunk-ZIDDK32O.js";
7
+ } from "../../chunk-EHABW23I.js";
8
8
  import {
9
9
  TOOLS_REGISTRY,
10
10
  bindModelNode,
11
11
  modelGroundingBlock
12
- } from "../../chunk-JHATKAWW.js";
12
+ } from "../../chunk-QBWZ6HVP.js";
13
13
  import {
14
14
  handleSearch
15
15
  } from "../../chunk-TJTEBS4S.js";
@@ -973,12 +973,12 @@ async function handleMcp(env, ctx, req, tier, key) {
973
973
  // stream:false forces the JSON lane (anon defaults to SSE)
974
974
  body: JSON.stringify({ ...args, stream: false })
975
975
  });
976
- const { runTool, TOOLS_REGISTRY: TOOLS_REGISTRY2 } = await import("../../tools-4NFFHVTW.js");
976
+ const { runTool, TOOLS_REGISTRY: TOOLS_REGISTRY2 } = await import("../../tools-PI7IXAL4.js");
977
977
  if (TOOLS_REGISTRY2.some((t) => t.name === name && t.audiences.includes("mcp"))) {
978
978
  const r = await runTool({ ...env, DB: env.DB }, { name, args }, "mcp");
979
979
  return r ? { tool: r.name, query: r.query, result: r.output } : { error: { message: `tool ${name} returned nothing for the given arguments` } };
980
980
  }
981
- const res = name === "ask" ? await (await import("../../ask-PUU6HHRZ.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-G76DK4AF.js")).handleSearch(env, ctx, inner, tier, key);
981
+ const res = name === "ask" ? await (await import("../../ask-UG5UYRJV.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-G76DK4AF.js")).handleSearch(env, ctx, inner, tier, key);
982
982
  return res.json().catch(() => ({ error: { message: "tool transport failed", status: res.status } }));
983
983
  });
984
984
  if (out.ok && "accepted" in out) return new Response(null, { status: 202 });
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.91",
30
+ "version": "0.2.92",
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",
@@ -11,7 +11,9 @@ import { portIndex, portModelRunner, hasLane } from "./env.ts";
11
11
  import { embed } from "./ai.ts";
12
12
  import { THRESHOLDS } from "./config.ts";
13
13
  import { standardKeysFrom } from "./requestScope.ts";
14
- import { licenseBoundaryRefusal, licensedEntryForDocNumber } from "./modelplane.ts";
14
+ import { licenseBoundaryRefusal, licensedEntryForDocNumber, licensedEntryForPackage } from "./modelplane.ts";
15
+ import { evaluate as evaluateVerdict } from "./verdict.ts";
16
+ import { evaluateConditionSets } from "./conditions.ts";
15
17
 
16
18
  const slug = (docidentifier: string) =>
17
19
  docidentifier
@@ -219,6 +221,124 @@ export const licensedSection: ToolSpec = {
219
221
  },
220
222
  };
221
223
 
224
+
225
+ // The deterministic engines as tools (the owner's 2026-10-04
226
+ // direction): computing with the standard, not reading it. Both wrap
227
+ // the engines the ask path already runs — no new evaluation logic
228
+ // lives here; the tool is the door, the engine is the authority.
229
+ // Void-when-parameters-miss is the honest contract: the tool states
230
+ // WHICH parameters the question did not state, never guesses.
231
+
232
+ export const verdictEvaluate: ToolSpec = {
233
+ name: "verdict.evaluate",
234
+ description:
235
+ "Deterministically evaluate a model-plane node's machine checks (OCL boolean expressions, threshold limits) against the quantities a question states. Returns pass/fail with every check's expression and values, or void with the missing parameter names when the question does not state enough. The machine computes; cite the node's clause.",
236
+ params: [
237
+ { key: "node_id", required: true, description: "the model-plane node id, e.g. /req/metrological/repeatability (node ids appear in answers' verdict blocks)" },
238
+ { key: "question", required: true, description: "the statement carrying the quantities, e.g. 'is mpe 0.02 with n_lc 3000 within the limit?'" },
239
+ ],
240
+ audiences: ["mcp"],
241
+ handler: async (env, args) => {
242
+ const node_id = String(args?.node_id ?? "").trim();
243
+ const question = String(args?.question ?? "").trim();
244
+ if (!node_id.startsWith("/") || question.length < 4) return null;
245
+ try {
246
+ // the same zero-or-ambiguous rule bindModelNode applies: a node
247
+ // id that indexes under exactly one standard resolves; several
248
+ // or none resolve to nothing — no silent pick
249
+ const rows = await env.DB.prepare(
250
+ "SELECT standard, kind, name, content FROM model_nodes WHERE node_id = ?1 LIMIT 2",
251
+ )
252
+ .bind(node_id)
253
+ .all();
254
+ const found = (rows.results ?? []) as { standard: string; kind: string; name: string | null; content: string }[];
255
+ if (found.length !== 1) {
256
+ return {
257
+ name: "verdict.evaluate",
258
+ query: node_id,
259
+ output: found.length
260
+ ? `The node id ${node_id} is indexed under several standards — name the standard to disambiguate. State this plainly.`
261
+ : `No model-plane node ${node_id} is indexed. State this plainly.`,
262
+ };
263
+ }
264
+ const node = found[0]!;
265
+ const entry = licensedEntryForPackage(node.standard);
266
+ if (entry) {
267
+ return {
268
+ name: "verdict.evaluate",
269
+ query: node_id,
270
+ output: licenseBoundaryRefusal(entry.doc_number, null) ?? "License boundary — the node belongs to a licensed standard the caller's entitlement set does not cover.",
271
+ };
272
+ }
273
+ let content: unknown;
274
+ try { content = JSON.parse(node.content); } catch { content = null; }
275
+ const v = evaluateVerdict(content, question);
276
+ if (!v) {
277
+ return {
278
+ name: "verdict.evaluate",
279
+ query: node_id,
280
+ output: `The node ${node_id} (${node.kind}) carries no machine-checkable expressions — it is not evaluatable by the verdict engine. State this plainly.`,
281
+ };
282
+ }
283
+ return {
284
+ name: "verdict.evaluate",
285
+ query: node_id,
286
+ output: JSON.stringify({ standard: node.standard, node_id, kind: node.kind, name: node.name, verdict: v.verdict, on_violation: v.on_violation, missing: v.missing, checks: v.checks }),
287
+ };
288
+ } catch {
289
+ return null;
290
+ }
291
+ },
292
+ };
293
+
294
+ export const conditionsCheck: ToolSpec = {
295
+ name: "conditions.check",
296
+ description:
297
+ "Check a stated combination of environmental quantities (temperature, humidity, duration, cycles) against the indexed severity/condition sets — the pass answer names the matched set and every band, the fail names the nearest set and its distance. Machine evaluation over the condition-set nodes; cite the matched set's clause.",
298
+ params: [
299
+ { key: "question", required: true, description: "the stated combination, e.g. 'damp heat cyclic test at 55 °C for 2 cycles of 24 h'" },
300
+ { key: "standard", required: false, description: "optional scope, the package id, e.g. iec-60068-2-30 (defaults to every indexed condition set the caller may see)" },
301
+ { key: "licensed_standards", required: false, description: 'license entitlement keys for licensed condition sets, validated against the deployment\'s declared licenses' },
302
+ ],
303
+ audiences: ["mcp"],
304
+ handler: async (env, args) => {
305
+ const question = String(args?.question ?? "").trim();
306
+ if (question.length < 4) return null;
307
+ const standard = args?.standard ? String(args.standard).trim() : null;
308
+ const keys = standardKeysFrom({ licensed_standards: args?.licensed_standards });
309
+ try {
310
+ const rows = standard
311
+ ? await env.DB.prepare("SELECT node_id, standard, content FROM model_nodes WHERE kind = 'condition_set' AND standard = ?1").bind(standard).all()
312
+ : await env.DB.prepare("SELECT node_id, standard, content FROM model_nodes WHERE kind = 'condition_set'").all();
313
+ const all = ((rows.results ?? []) as { node_id: string; standard: string; content: string }[]).map((r) => {
314
+ let content: unknown = r.content;
315
+ try { content = JSON.parse(r.content); } catch { /* verbatim */ }
316
+ return { node_id: r.node_id, standard: r.standard, content };
317
+ });
318
+ // the ask path's own gate: licensed condition sets compete only
319
+ // for entitled callers — the same licensedEntryForPackage filter
320
+ const visible = all.filter((n) => {
321
+ const entry = licensedEntryForPackage(n.standard);
322
+ return !entry || keys.has(entry.key);
323
+ });
324
+ if (!visible.length) {
325
+ return { name: "conditions.check", query: question, output: "No condition sets are indexed (or visible to this caller's entitlements). State this plainly." };
326
+ }
327
+ const v = evaluateConditionSets(visible, question);
328
+ if (!v) {
329
+ return { name: "conditions.check", query: question, output: "No quantities were recognized in the statement — state the values (temperature, humidity, duration, cycles). The engine checks stated numbers, never guesses." };
330
+ }
331
+ return {
332
+ name: "conditions.check",
333
+ query: question,
334
+ output: JSON.stringify({ verdict: v.verdict, matched: v.matched, checks: v.checks }),
335
+ };
336
+ } catch {
337
+ return null;
338
+ }
339
+ },
340
+ };
341
+
222
342
  export const glossaryLookup: ToolSpec = {
223
343
  name: "glossary.lookup",
224
344
  description:
@@ -64,9 +64,9 @@ const certificatesSearch: ToolSpec = {
64
64
  },
65
65
  };
66
66
 
67
- import { unitsGet, graphCites, docsSection, glossaryLookup, documentsFamily, licensedSection } from "./tools-content.ts";
67
+ import { unitsGet, graphCites, docsSection, glossaryLookup, documentsFamily, licensedSection, verdictEvaluate, conditionsCheck } from "./tools-content.ts";
68
68
 
69
- export const TOOLS_REGISTRY: ToolSpec[] = [certificatesSearch, unitsGet, graphCites, docsSection, glossaryLookup, documentsFamily, licensedSection];
69
+ export const TOOLS_REGISTRY: ToolSpec[] = [certificatesSearch, unitsGet, graphCites, docsSection, glossaryLookup, documentsFamily, licensedSection, verdictEvaluate, conditionsCheck];
70
70
 
71
71
  /** The agent loop's protocol declaration, GENERATED from the registry —
72
72
  * the model's tool vocabulary is always the registry's, never a