@githolon/testing 0.3.0 → 0.4.0
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/package.json +1 -1
- package/vendor/engine/engine.mjs +234 -26
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/testing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "@githolon/testing — law TDD for Nomos domains: boot the REAL engine plane in-process (the exact machinery every cloud DO, heavy container and web client runs), dispatch directives, assert rows, assert TYPED REFUSALS, fork for what-ifs — fully offline inside vitest.",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|
package/vendor/engine/engine.mjs
CHANGED
|
@@ -289,6 +289,15 @@ export function scopedReads(eng) {
|
|
|
289
289
|
for (const s of (m && m.sums) || []) {
|
|
290
290
|
if (s && typeof s.scope === "string") out.push({ id: s.id, kind: "sum", of: s.of, by: s.by ?? null, field: s.sumField, ...(s.where ? { where: s.where } : {}) });
|
|
291
291
|
}
|
|
292
|
+
// SLICE 8 — scoped EXTREMUMS: per-shard min/max ride the same delta lane
|
|
293
|
+
// (events carry post-intent ABSOLUTES; the gate extremizes-on-monotone and
|
|
294
|
+
// requires declared re-derivation on retractions). The oracle is the
|
|
295
|
+
// projection's long-maintained extrema lane (#47's `extremum` RPC).
|
|
296
|
+
for (const [key, kind] of [["mins", "min"], ["maxes", "max"]]) {
|
|
297
|
+
for (const e of (m && m[key]) || []) {
|
|
298
|
+
if (e && typeof e.scope === "string") out.push({ id: e.id, kind, of: e.of, by: e.by ?? null, field: e.valueField, ...(e.where ? { where: e.where } : {}) });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
292
301
|
}
|
|
293
302
|
eng.scopedReadsCache = out;
|
|
294
303
|
return out;
|
|
@@ -358,7 +367,15 @@ const groupOf = (read, data) => {
|
|
|
358
367
|
return v === undefined || v === null ? undefined : String(v);
|
|
359
368
|
};
|
|
360
369
|
const tallyOf = (eng, ws, read, group) =>
|
|
361
|
-
read.kind === "count" ? count(eng, ws, read.id, group).count
|
|
370
|
+
read.kind === "count" ? count(eng, ws, read.id, group).count
|
|
371
|
+
: read.kind === "sum" ? sum(eng, ws, read.id, group).sum
|
|
372
|
+
: extremum(eng, ws, read.id, read.kind, group).value; // min|max — null = empty group, never 0
|
|
373
|
+
/** Is a scoped read an extremum kind (the slice-8 non-additive lane)? */
|
|
374
|
+
const isExtremumRead = (read) => read.kind === "min" || read.kind === "max";
|
|
375
|
+
/** Does `next` extremize-improve (or equal) `prev` for the read's kind? null = empty. */
|
|
376
|
+
const extremumMonotone = (read, prev, next) =>
|
|
377
|
+
(next === null && prev === null) ||
|
|
378
|
+
(next !== null && (prev === null || (read.kind === "min" ? next <= prev : next >= prev)));
|
|
362
379
|
|
|
363
380
|
/**
|
|
364
381
|
* Begin one admitted intent's summary capture: snapshot the touched aggregates +
|
|
@@ -405,21 +422,44 @@ function finishSummaryCapture(eng, ws, ctx, cap, oid, intentId) {
|
|
|
405
422
|
const [readId, group] = splitSummaryKey(key);
|
|
406
423
|
const read = ctx.reads.find((r) => r.id === readId);
|
|
407
424
|
const after = tallyOf(eng, ws, read, group);
|
|
408
|
-
|
|
409
|
-
|
|
425
|
+
if (isExtremumRead(read)) {
|
|
426
|
+
// SLICE 8 — extremum kinds: the event carries the POST-INTENT ABSOLUTE
|
|
427
|
+
// (null = the group emptied — never a fabricated 0); a non-monotone step
|
|
428
|
+
// is a RETRACTION the subtotal must declare (`rederived` — the value IS
|
|
429
|
+
// the projection's maintained re-derivation, the suffix oracle).
|
|
430
|
+
const before = cap.preTallies.has(key) ? cap.preTallies.get(key) : null;
|
|
431
|
+
if (after !== before) {
|
|
432
|
+
deltas.push({ readId, group, delta: after });
|
|
433
|
+
if (!extremumMonotone(read, before, after)) ctx.rederived.add(key);
|
|
434
|
+
}
|
|
435
|
+
} else {
|
|
436
|
+
const before = cap.preTallies.get(key) ?? 0;
|
|
437
|
+
if (after !== before) deltas.push({ readId, group, delta: after - before });
|
|
438
|
+
}
|
|
410
439
|
ctx.values.set(key, { read, group, value: after });
|
|
411
440
|
}
|
|
412
441
|
// The synthetic row-count read: existence flips, with the projection as oracle.
|
|
413
442
|
// The ABSOLUTE rides relative to the coordinator-held value (the HOST closes it
|
|
414
443
|
// at emit time — a shard never enumerates its own whole projection per batch).
|
|
444
|
+
// SLICE 8 — EXACT PER-HOME SUBTOTALS: each flip is also attributed to its HOME
|
|
445
|
+
// (the row id's route tag, or the row id itself for an axis root) so the split
|
|
446
|
+
// packer can move precisely-sized home sets instead of a uniform estimate.
|
|
415
447
|
let rowsDelta = 0;
|
|
416
448
|
for (const [aggregateId, before] of cap.pre) {
|
|
417
449
|
const rows = qById(eng, ws, aggregateId);
|
|
418
450
|
const type = rows.length ? rows[0].type : before ? before.type : undefined;
|
|
419
451
|
if (type === undefined || !tenantTypes(eng).has(type)) continue; // tenant rows only
|
|
420
452
|
const existsAfter = rows.length > 0;
|
|
421
|
-
|
|
422
|
-
|
|
453
|
+
const flip = !before && existsAfter ? 1 : before && !existsAfter ? -1 : 0;
|
|
454
|
+
if (flip === 0) continue;
|
|
455
|
+
rowsDelta += flip;
|
|
456
|
+
const homeKey = ctx.homeKeySet && ctx.homeKeySet.has(aggregateId)
|
|
457
|
+
? aggregateId
|
|
458
|
+
: ctx.homeTags ? ctx.homeTags.get(routeTagHexOfMintedId(aggregateId) ?? "") : undefined;
|
|
459
|
+
if (homeKey !== undefined) {
|
|
460
|
+
deltas.push({ readId: NOMOS_SHARD_ROWS_READ, group: homeKey, delta: flip });
|
|
461
|
+
ctx.rowsByHome.set(homeKey, (ctx.rowsByHome.get(homeKey) ?? 0) + flip);
|
|
462
|
+
}
|
|
423
463
|
}
|
|
424
464
|
if (rowsDelta !== 0) {
|
|
425
465
|
deltas.push({ readId: NOMOS_SHARD_ROWS_READ, group: "", delta: rowsDelta });
|
|
@@ -438,26 +478,36 @@ function finishSummaryCapture(eng, ws, ctx, cap, oid, intentId) {
|
|
|
438
478
|
*/
|
|
439
479
|
function summaryOfCapture(ctx, fromOid, toOid) {
|
|
440
480
|
if (ctx.events.length === 0 || !toOid || fromOid === toOid) return null;
|
|
481
|
+
// Buckets the EVIDENCE touched must carry their claim even when the net change
|
|
482
|
+
// is zero (the gate refuses an event delta no subtotal claims — and a same-value
|
|
483
|
+
// extremum claim may still ride a declared retraction, e.g. add-then-retract).
|
|
484
|
+
const touched = new Set();
|
|
485
|
+
for (const ev of ctx.events) for (const d of ev.deltas) touched.add(summaryKey(d.readId, d.group));
|
|
441
486
|
const subtotals = [];
|
|
442
487
|
for (const [key, cur] of ctx.values) {
|
|
488
|
+
const extremum = isExtremumRead(cur.read);
|
|
443
489
|
const prev = ctx.prevs.get(key);
|
|
444
|
-
const prevValue = prev ? prev.value : 0;
|
|
445
|
-
if (cur.value === prevValue) continue; // untouched-in-net — carries nothing
|
|
490
|
+
const prevValue = prev ? prev.value : extremum ? null : 0;
|
|
491
|
+
if (cur.value === prevValue && !touched.has(key)) continue; // untouched-in-net — carries nothing
|
|
446
492
|
subtotals.push({
|
|
447
493
|
readId: cur.read.id,
|
|
448
494
|
group: cur.group,
|
|
449
495
|
kind: cur.read.kind,
|
|
450
496
|
prev: prevValue,
|
|
451
497
|
value: cur.value,
|
|
498
|
+
...(extremum && ctx.rederived.has(key) ? { rederived: true } : {}),
|
|
452
499
|
});
|
|
453
500
|
}
|
|
454
|
-
if (subtotals.length === 0 && ctx.rowsDelta === 0) return null;
|
|
501
|
+
if (subtotals.length === 0 && ctx.rowsDelta === 0 && ctx.rowsByHome.size === 0) return null;
|
|
455
502
|
return {
|
|
456
503
|
fromOid: fromOid || "",
|
|
457
504
|
toOid,
|
|
458
505
|
subtotals: subtotals.sort((a, b) => (summaryKey(a.readId, a.group) < summaryKey(b.readId, b.group) ? -1 : 1)),
|
|
459
506
|
events: ctx.events,
|
|
460
507
|
rowsDelta: ctx.rowsDelta,
|
|
508
|
+
// SLICE 8: the exact per-home row deltas — the host closes each against the
|
|
509
|
+
// coordinator-held per-home subtotal exactly like the "" grand total.
|
|
510
|
+
rowsByHome: Object.fromEntries([...ctx.rowsByHome.entries()].sort((a, b) => (a[0] < b[0] ? -1 : 1))),
|
|
461
511
|
};
|
|
462
512
|
}
|
|
463
513
|
|
|
@@ -477,12 +527,31 @@ export async function deriveSummarySuffix(eng, ws, fromOid) {
|
|
|
477
527
|
const commits = await git.log({ fs: eng.fs, gitdir, ref: BRANCH });
|
|
478
528
|
commits.reverse(); // genesis → head
|
|
479
529
|
const state = new Map(); // aggregateId -> { type, fields: Map }
|
|
480
|
-
const running = new Map(); // summaryKey -> value
|
|
530
|
+
const running = new Map(); // summaryKey -> value (ADDITIVE kinds)
|
|
481
531
|
const bump = (key, d) => running.set(key, (running.get(key) ?? 0) + d);
|
|
482
|
-
|
|
532
|
+
// SLICE 8 — the EXTREMUM fold state: per bucket, the contributing rows' values
|
|
533
|
+
// (aggregateId -> value) + the running extremum (null = no contributing member).
|
|
534
|
+
// A monotone step extremizes O(1); a retraction recomputes the bucket (the
|
|
535
|
+
// suffix lane IS the re-derivation oracle — explicit, O(bucket)).
|
|
536
|
+
const extRows = new Map(); // summaryKey -> Map(aggregateId -> value)
|
|
537
|
+
const extRunning = new Map(); // summaryKey -> number | null
|
|
538
|
+
const extRederived = new Set(); // buckets retracted INSIDE the suffix
|
|
539
|
+
const extBucketRows = (key) => { let m = extRows.get(key); if (!m) { m = new Map(); extRows.set(key, m); } return m; };
|
|
540
|
+
const recomputeExt = (read, key) => {
|
|
541
|
+
let v = null;
|
|
542
|
+
const m = extRows.get(key);
|
|
543
|
+
if (m) for (const x of m.values()) v = v === null ? x : read.kind === "min" ? Math.min(v, x) : Math.max(v, x);
|
|
544
|
+
return v;
|
|
545
|
+
};
|
|
546
|
+
// SLICE 8 — the per-home attribution table, folded from the chain's OWN receipt
|
|
547
|
+
// rows (the receipt leg rides ahead of the first homed write by construction).
|
|
548
|
+
const homeKeySet = new Set();
|
|
549
|
+
const homeTags = new Map(); // route tag hex -> homeKey
|
|
550
|
+
let prevs = null; // snapshot at fromOid (additive)
|
|
551
|
+
let extPrevs = null; // snapshot at fromOid (extremum)
|
|
483
552
|
let events = [];
|
|
484
553
|
let inSuffix = fromOid === "" || fromOid === undefined;
|
|
485
|
-
if (inSuffix) prevs = new Map();
|
|
554
|
+
if (inSuffix) { prevs = new Map(); extPrevs = new Map(); }
|
|
486
555
|
let sawFrom = inSuffix;
|
|
487
556
|
for (const c of commits) {
|
|
488
557
|
let blob = null;
|
|
@@ -515,44 +584,111 @@ export async function deriveSummarySuffix(eng, ws, fromOid) {
|
|
|
515
584
|
cur.exists = true;
|
|
516
585
|
state.set(ev.aggregate, cur);
|
|
517
586
|
const effType = cur.type ?? newType;
|
|
587
|
+
// The per-home attribution table (slice 8): receipts register their home.
|
|
588
|
+
if (effType === "NomosHomeReceipt") {
|
|
589
|
+
const hk = cur.fields.get("homeKey");
|
|
590
|
+
if (typeof hk === "string" && hk && !homeKeySet.has(hk)) {
|
|
591
|
+
homeKeySet.add(hk);
|
|
592
|
+
homeTags.set(await routeTagHexOfHomeKey(hk), hk);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
518
595
|
if (!existedBefore && effType !== undefined && tenantTypes(eng).has(effType)) {
|
|
519
596
|
bump(summaryKey(NOMOS_SHARD_ROWS_READ, ""), 1);
|
|
520
597
|
deltas.push({ readId: NOMOS_SHARD_ROWS_READ, group: "", delta: 1 });
|
|
598
|
+
// SLICE 8 — the EXACT per-home row subtotal: the same flip, attributed
|
|
599
|
+
// to its home (the id's route-tag slot, or the id itself for an axis root).
|
|
600
|
+
const homeKey = homeKeySet.has(ev.aggregate)
|
|
601
|
+
? ev.aggregate
|
|
602
|
+
: homeTags.get(routeTagHexOfMintedId(ev.aggregate) ?? "");
|
|
603
|
+
if (homeKey !== undefined) {
|
|
604
|
+
bump(summaryKey(NOMOS_SHARD_ROWS_READ, homeKey), 1);
|
|
605
|
+
deltas.push({ readId: NOMOS_SHARD_ROWS_READ, group: homeKey, delta: 1 });
|
|
606
|
+
}
|
|
521
607
|
}
|
|
522
608
|
for (const read of reads) {
|
|
523
609
|
if (read.of !== cur.type) continue;
|
|
524
610
|
const before = tallyBefore.get(read.id) ?? null;
|
|
525
611
|
const after = contribution(read, cur);
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
612
|
+
if (isExtremumRead(read)) {
|
|
613
|
+
// Update the bucket row-sets, then re-extremize each touched bucket.
|
|
614
|
+
const touchedBuckets = new Set();
|
|
615
|
+
if (before) {
|
|
616
|
+
const bk = summaryKey(read.id, before.group);
|
|
617
|
+
if (!after || after.group !== before.group) extBucketRows(bk).delete(ev.aggregate);
|
|
618
|
+
touchedBuckets.add(bk);
|
|
619
|
+
}
|
|
620
|
+
if (after) {
|
|
621
|
+
const ak = summaryKey(read.id, after.group);
|
|
622
|
+
extBucketRows(ak).set(ev.aggregate, after.value);
|
|
623
|
+
touchedBuckets.add(ak);
|
|
624
|
+
}
|
|
625
|
+
for (const bk of touchedBuckets) {
|
|
626
|
+
const ov = extRunning.has(bk) ? extRunning.get(bk) : null;
|
|
627
|
+
const nv = recomputeExt(read, bk);
|
|
628
|
+
if (nv === ov) continue;
|
|
629
|
+
extRunning.set(bk, nv);
|
|
630
|
+
if (inSuffix) {
|
|
631
|
+
if (!extremumMonotone(read, ov, nv)) extRederived.add(bk);
|
|
632
|
+
const [, group] = splitSummaryKey(bk);
|
|
633
|
+
deltas.push({ readId: read.id, group, delta: nv, __extremum: true });
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
} else {
|
|
637
|
+
for (const d of contributionDiff(read, before, after)) {
|
|
638
|
+
bump(summaryKey(d.readId, d.group), d.delta);
|
|
639
|
+
deltas.push(d);
|
|
640
|
+
}
|
|
529
641
|
}
|
|
530
642
|
}
|
|
531
643
|
}
|
|
532
644
|
}
|
|
533
645
|
if (inSuffix && deltas.length) {
|
|
534
|
-
// Coalesce same-(read,group) deltas within one intent
|
|
646
|
+
// Coalesce same-(read,group) deltas within one intent: ADDITIVE entries sum
|
|
647
|
+
// (zero-nets drop); EXTREMUM entries carry absolutes — the LAST one stands.
|
|
535
648
|
const byKey = new Map();
|
|
536
|
-
|
|
649
|
+
const extLast = new Map();
|
|
650
|
+
for (const d of deltas) {
|
|
651
|
+
const k = summaryKey(d.readId, d.group);
|
|
652
|
+
if (d.__extremum) extLast.set(k, d.delta);
|
|
653
|
+
else byKey.set(k, (byKey.get(k) ?? 0) + d.delta);
|
|
654
|
+
}
|
|
537
655
|
const merged = [...byKey.entries()].filter(([, d]) => d !== 0).map(([k, d]) => {
|
|
538
656
|
const [readId, group] = splitSummaryKey(k);
|
|
539
657
|
return { readId, group, delta: d };
|
|
540
658
|
});
|
|
659
|
+
for (const [k, v] of extLast) {
|
|
660
|
+
const [readId, group] = splitSummaryKey(k);
|
|
661
|
+
merged.push({ readId, group, delta: v });
|
|
662
|
+
}
|
|
541
663
|
if (merged.length) events.push({ oid: c.oid, intentId, deltas: merged });
|
|
542
664
|
}
|
|
543
|
-
if (!inSuffix && c.oid === fromOid) { inSuffix = true; sawFrom = true; prevs = new Map(running); }
|
|
665
|
+
if (!inSuffix && c.oid === fromOid) { inSuffix = true; sawFrom = true; prevs = new Map(running); extPrevs = new Map(extRunning); }
|
|
544
666
|
}
|
|
545
667
|
if (!sawFrom) return { ok: false, error: `fromOid ${String(fromOid).slice(0, 12)} is not on this shard's main — cannot derive the suffix` };
|
|
668
|
+
// Buckets the suffix EVIDENCE touched must carry their claim even when net-zero
|
|
669
|
+
// (the gate refuses an event delta no subtotal claims).
|
|
670
|
+
const touched = new Set();
|
|
671
|
+
for (const ev of events) for (const d of ev.deltas) touched.add(summaryKey(d.readId, d.group));
|
|
546
672
|
const subtotals = [];
|
|
547
673
|
const keys = new Set([...running.keys(), ...prevs.keys()]);
|
|
548
674
|
for (const key of keys) {
|
|
549
675
|
const value = running.get(key) ?? 0;
|
|
550
676
|
const prev = prevs.get(key) ?? 0;
|
|
551
|
-
if (value === prev) continue;
|
|
677
|
+
if (value === prev && !touched.has(key)) continue;
|
|
552
678
|
const [readId, group] = splitSummaryKey(key);
|
|
553
679
|
const read = reads.find((r) => r.id === readId);
|
|
554
680
|
subtotals.push({ readId, group, kind: readId === NOMOS_SHARD_ROWS_READ ? "rows" : read ? read.kind : "count", prev, value });
|
|
555
681
|
}
|
|
682
|
+
const extKeys = new Set([...extRunning.keys(), ...(extPrevs ? extPrevs.keys() : [])]);
|
|
683
|
+
for (const key of extKeys) {
|
|
684
|
+
const value = extRunning.has(key) ? extRunning.get(key) : null;
|
|
685
|
+
const prev = extPrevs && extPrevs.has(key) ? extPrevs.get(key) : null;
|
|
686
|
+
if (value === prev && !touched.has(key)) continue;
|
|
687
|
+
const [readId, group] = splitSummaryKey(key);
|
|
688
|
+
const read = reads.find((r) => r.id === readId);
|
|
689
|
+
if (!read) continue;
|
|
690
|
+
subtotals.push({ readId, group, kind: read.kind, prev, value, ...(extRederived.has(key) ? { rederived: true } : {}) });
|
|
691
|
+
}
|
|
556
692
|
if (subtotals.length === 0) return { ok: true, upToDate: true, head };
|
|
557
693
|
return {
|
|
558
694
|
ok: true,
|
|
@@ -577,6 +713,9 @@ function contribution(read, cur) {
|
|
|
577
713
|
if (group === undefined) return null;
|
|
578
714
|
if (read.kind === "count") return { group, value: 1 };
|
|
579
715
|
const raw = cur.fields.get(read.field);
|
|
716
|
+
// EXTREMUM kinds (slice 8): a row whose value field is unfolded CONTRIBUTES
|
|
717
|
+
// NOTHING — fabricating a 0 would forge a fake minimum (the extremum.ts law).
|
|
718
|
+
if (isExtremumRead(read) && typeof raw !== "number") return null;
|
|
580
719
|
return { group, value: typeof raw === "number" ? raw : 0 };
|
|
581
720
|
}
|
|
582
721
|
function contributionDiff(read, before, after) {
|
|
@@ -612,6 +751,26 @@ export function countTenantRows(eng, ws) {
|
|
|
612
751
|
return total;
|
|
613
752
|
}
|
|
614
753
|
|
|
754
|
+
/** The EXACT per-home recount (slice 8 — the explicit O(rows) audit walk, per-home
|
|
755
|
+
* edition): projected TENANT rows attributed to each named home by the row id's
|
|
756
|
+
* route-tag slot (or the id itself for an axis root). Shared by the strike-bearing
|
|
757
|
+
* summary close and deep-verify — the same one definition as the capture/suffix. */
|
|
758
|
+
export async function countTenantRowsByHome(eng, ws, homeKeys) {
|
|
759
|
+
const keySet = new Set(homeKeys);
|
|
760
|
+
const tagOf = new Map();
|
|
761
|
+
for (const hk of homeKeys) tagOf.set(await routeTagHexOfHomeKey(hk), hk);
|
|
762
|
+
const out = new Map();
|
|
763
|
+
for (const hk of homeKeys) out.set(hk, 0);
|
|
764
|
+
for (const type of tenantTypes(eng)) {
|
|
765
|
+
for (const r of query(eng, ws, "aggregatesByType", JSON.stringify({ __type: type }))) {
|
|
766
|
+
const id = String(r.id || "");
|
|
767
|
+
const hk = keySet.has(id) ? id : tagOf.get(routeTagHexOfMintedId(id) ?? "");
|
|
768
|
+
if (hk !== undefined) out.set(hk, (out.get(hk) ?? 0) + 1);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return out;
|
|
772
|
+
}
|
|
773
|
+
|
|
615
774
|
/**
|
|
616
775
|
* Walk a mounted workspace's main (genesis → head) and collect every NON-STRUCK
|
|
617
776
|
* intent homed on one of `homeKeys` — judged by the SAME hash-bearing `routes`
|
|
@@ -731,13 +890,30 @@ export async function summaryCloseAgainstHeld(eng, ws, heldRows, heldFrontier) {
|
|
|
731
890
|
if (head === heldFrontier) return { ok: true, upToDate: true, head };
|
|
732
891
|
const subtotals = [];
|
|
733
892
|
const deltas = [];
|
|
893
|
+
// SLICE 8: the per-home rows recount, computed ONCE over all held per-home rows.
|
|
894
|
+
const heldHomes = heldRows
|
|
895
|
+
.map((row) => row.data || row)
|
|
896
|
+
.filter((d) => d.readId === NOMOS_SHARD_ROWS_READ && typeof d.group === "string" && d.group !== "")
|
|
897
|
+
.map((d) => d.group);
|
|
898
|
+
const byHome = heldHomes.length ? await countTenantRowsByHome(eng, ws, heldHomes) : new Map();
|
|
734
899
|
for (const row of heldRows) {
|
|
735
900
|
const d = row.data || row;
|
|
736
901
|
if (typeof d.readId !== "string") continue;
|
|
737
902
|
const group = typeof d.group === "string" ? d.group : "";
|
|
903
|
+
if (d.kind === "min" || d.kind === "max") {
|
|
904
|
+
// EXTREMUM kinds: the close IS a re-derivation (a §6 seal re-shaped the
|
|
905
|
+
// fold — non-monotone by construction); the projection's maintained
|
|
906
|
+
// extremum is the oracle; the claim rides DECLARED (`rederived: true`).
|
|
907
|
+
const heldVal = d.empty === 1 || typeof d.value !== "number" ? null : d.value;
|
|
908
|
+
const nowVal = extremum(eng, ws, d.readId, d.kind, group).value;
|
|
909
|
+
if (nowVal === heldVal) continue;
|
|
910
|
+
subtotals.push({ readId: d.readId, group, kind: d.kind, prev: heldVal, value: nowVal, rederived: true });
|
|
911
|
+
deltas.push({ readId: d.readId, group, delta: nowVal }); // the post-close ABSOLUTE
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
738
914
|
const held = typeof d.value === "number" ? d.value : 0;
|
|
739
915
|
let now;
|
|
740
|
-
if (d.readId === NOMOS_SHARD_ROWS_READ) now = countTenantRows(eng, ws);
|
|
916
|
+
if (d.readId === NOMOS_SHARD_ROWS_READ) now = group === "" ? countTenantRows(eng, ws) : (byHome.get(group) ?? 0);
|
|
741
917
|
else if (d.kind === "sum") now = sum(eng, ws, d.readId, group).sum;
|
|
742
918
|
else now = count(eng, ws, d.readId, group).count;
|
|
743
919
|
if (now === held) continue;
|
|
@@ -893,22 +1069,40 @@ export async function deepVerifyShard(eng, shardWs, shardLedger, coordinatorRows
|
|
|
893
1069
|
await mountWorkspace(eng, shardWs, shardLedger);
|
|
894
1070
|
const checks = [];
|
|
895
1071
|
let allMatch = true;
|
|
1072
|
+
// SLICE 8: the per-home rows recount, computed ONCE over all held per-home rows.
|
|
1073
|
+
const heldHomes = coordinatorRows
|
|
1074
|
+
.map((row) => row.data || row)
|
|
1075
|
+
.filter((d) => d.readId === NOMOS_SHARD_ROWS_READ && typeof d.group === "string" && d.group !== "")
|
|
1076
|
+
.map((d) => d.group);
|
|
1077
|
+
const byHome = heldHomes.length ? await countTenantRowsByHome(eng, shardWs, heldHomes) : new Map();
|
|
896
1078
|
for (const row of coordinatorRows) {
|
|
897
1079
|
const d = row.data || row;
|
|
898
1080
|
if (typeof d.readId !== "string") continue;
|
|
1081
|
+
const group = d.group ?? "";
|
|
1082
|
+
if (d.kind === "min" || d.kind === "max") {
|
|
1083
|
+
// EXTREMUM rows (slice 8): recompute off the replayed projection's maintained
|
|
1084
|
+
// extrema lane; the coordinator's `empty:1` flag means NULL, never 0.
|
|
1085
|
+
const heldVal = d.empty === 1 || typeof d.value !== "number" ? null : d.value;
|
|
1086
|
+
const recomputed = extremum(eng, shardWs, d.readId, d.kind, group).value;
|
|
1087
|
+
const match = recomputed === heldVal;
|
|
1088
|
+
if (!match) allMatch = false;
|
|
1089
|
+
checks.push({ readId: d.readId, group, kind: d.kind, coordinator: heldVal, shard: recomputed, match });
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
899
1092
|
let recomputed;
|
|
900
1093
|
if (d.readId === NOMOS_SHARD_ROWS_READ) {
|
|
901
|
-
//
|
|
902
|
-
// O(rows) audit walk — never an implicit read cost)
|
|
903
|
-
|
|
1094
|
+
// Projected rows: enumerate the manifest's aggregate types (an explicit
|
|
1095
|
+
// O(rows) audit walk — never an implicit read cost); a per-home row (group
|
|
1096
|
+
// = the home key) recounts only that home's attributed rows.
|
|
1097
|
+
recomputed = group === "" ? countTenantRows(eng, shardWs) : (byHome.get(group) ?? 0);
|
|
904
1098
|
} else if (d.kind === "sum") {
|
|
905
|
-
recomputed = sum(eng, shardWs, d.readId,
|
|
1099
|
+
recomputed = sum(eng, shardWs, d.readId, group).sum;
|
|
906
1100
|
} else {
|
|
907
|
-
recomputed = count(eng, shardWs, d.readId,
|
|
1101
|
+
recomputed = count(eng, shardWs, d.readId, group).count;
|
|
908
1102
|
}
|
|
909
1103
|
const match = recomputed === d.value;
|
|
910
1104
|
if (!match) allMatch = false;
|
|
911
|
-
checks.push({ readId: d.readId, group
|
|
1105
|
+
checks.push({ readId: d.readId, group, coordinator: d.value, shard: recomputed, match });
|
|
912
1106
|
}
|
|
913
1107
|
const head = await git.resolveRef({ fs: eng.fs, gitdir: gitdirOf(shardWs), ref: BRANCH }).catch(() => null);
|
|
914
1108
|
return {
|
|
@@ -1061,8 +1255,22 @@ export async function admitAll(eng, ws, ledger, { refuseDomains = [], shardRouti
|
|
|
1061
1255
|
// O(touched)); the HOST authors the coalesced `nomosPropagateSummary` Order into
|
|
1062
1256
|
// the coordinator afterwards. The coordinator's gate re-verifies everything.
|
|
1063
1257
|
const summaryCtx = shardRouting && shardRouting.selfLabel && scopedReads(eng).length > 0
|
|
1064
|
-
? { reads: scopedReads(eng), prevs: new Map(), values: new Map(), events: [], rowsDelta: 0 }
|
|
1258
|
+
? { reads: scopedReads(eng), prevs: new Map(), values: new Map(), events: [], rowsDelta: 0, rederived: new Set(), rowsByHome: new Map(), homeTags: new Map(), homeKeySet: new Set() }
|
|
1065
1259
|
: null;
|
|
1260
|
+
if (summaryCtx) {
|
|
1261
|
+
// SLICE 8 — the per-home attribution table, off the shard's OWN receipts
|
|
1262
|
+
// (law-state: which homes this chain owns). A row id whose route-tag slot
|
|
1263
|
+
// matches a home's tag — or which IS a home key (an axis root) — attributes
|
|
1264
|
+
// its existence flip to that home's exact row subtotal.
|
|
1265
|
+
try {
|
|
1266
|
+
for (const r of query(eng, ws, "aggregatesByType", JSON.stringify({ __type: "NomosHomeReceipt" }))) {
|
|
1267
|
+
const hk = r.data && typeof r.data.homeKey === "string" ? r.data.homeKey : null;
|
|
1268
|
+
if (!hk) continue;
|
|
1269
|
+
summaryCtx.homeKeySet.add(hk);
|
|
1270
|
+
summaryCtx.homeTags.set(await routeTagHexOfHomeKey(hk), hk);
|
|
1271
|
+
}
|
|
1272
|
+
} catch (e) { log("home-tag-table-error", { ws, error: String(e).slice(0, 200) }); }
|
|
1273
|
+
}
|
|
1066
1274
|
const headBefore = summaryCtx ? await git.resolveRef({ fs: eng.fs, gitdir, ref: BRANCH }).catch(() => null) : null;
|
|
1067
1275
|
for (const [cid, head] of Object.entries(sessions)) {
|
|
1068
1276
|
const ref = `refs/heads/session/${cid}`;
|