@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/testing",
3
- "version": "0.3.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",
@@ -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 : sum(eng, ws, read.id, group).sum;
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
- const before = cap.preTallies.get(key) ?? 0;
409
- if (after !== before) deltas.push({ readId, group, delta: after - before });
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
- if (!before && existsAfter) rowsDelta += 1;
422
- else if (before && !existsAfter) rowsDelta -= 1;
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
- let prevs = null; // snapshot at fromOid
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
- for (const d of contributionDiff(read, before, after)) {
527
- bump(summaryKey(d.readId, d.group), d.delta);
528
- deltas.push(d);
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
- for (const d of deltas) byKey.set(summaryKey(d.readId, d.group), (byKey.get(summaryKey(d.readId, d.group)) ?? 0) + d.delta);
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
- // Total projected rows: enumerate the manifest's aggregate types (an explicit
902
- // O(rows) audit walk — never an implicit read cost).
903
- recomputed = countTenantRows(eng, shardWs);
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, d.group ?? "").sum;
1099
+ recomputed = sum(eng, shardWs, d.readId, group).sum;
906
1100
  } else {
907
- recomputed = count(eng, shardWs, d.readId, d.group ?? "").count;
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: d.group ?? "", coordinator: d.value, shard: recomputed, match });
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}`;