@githolon/testing 0.3.0 → 0.5.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.5.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",
@@ -53,7 +53,7 @@ function seedManifests(m, strings) {
53
53
  m.set("identity-manifests.json", new File(enc.encode(strings.identity)));
54
54
  }
55
55
 
56
- export const installPayload = (hash, usda, installedBy) => ({ domainHash: hash, packageUsda: usda, installedBy, authorityScope: "workspace/root", dependencies: [], finalizers: [] });
56
+ export const installPayload = (hash, usda, installedBy, dispositions) => ({ domainHash: hash, packageUsda: usda, installedBy, authorityScope: "workspace/root", dependencies: [], finalizers: [], ...(dispositions ? { dispositions } : {}) });
57
57
 
58
58
  /**
59
59
  * Boot the resident wasm + /work tree. `wasmModule` is a PRECOMPILED WebAssembly.Module
@@ -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 {
@@ -955,6 +1149,22 @@ export function unmount(eng, ws) {
955
1149
 
956
1150
  function writeWork(eng, name, bytes) { eng.preopen.dir.contents.set(name, new File(bytes)); }
957
1151
 
1152
+ /** THE EVOLVE DRY-RUN (managed_rollout.md slice 1 — "would this upgrade admit?"):
1153
+ * the evolve gate's pure judgment over the mounted workspace's folded state and a
1154
+ * CANDIDATE package, without installing. Returns {admits} or {admits:false,
1155
+ * domain, detail} — byte-identical to the refusal the real install would meet. */
1156
+ export function evolveDryRun(eng, ws, usda, domainHash, dispositions = null) {
1157
+ return JSON.parse(call(eng.ex, "evolve_dryrun", { repoArg: repoArgOf(ws), branch: BRANCH, bundle: usda, hash: domainHash, ...(dispositions ? { dispositions } : {}) }, eng.STDERR));
1158
+ }
1159
+
1160
+ /** THE FRONT-DOOR MINT (engine plane): reserve a kernel-minted typed id
1161
+ * (`<TypeTag>_<uuidv7>`) via the wasm `mint` arm — the same mint the web client
1162
+ * and the generated clients call. The minted id rides IN the payload, so replay
1163
+ * reads the captured id and never re-mints (determinism via capture). */
1164
+ export function mintId(eng, typeTag) {
1165
+ return JSON.parse(call(eng.ex, "mint", { typeTag, nowMillis: Date.now() }, eng.STDERR)).id;
1166
+ }
1167
+
958
1168
  export function author(eng, ws, domain, directiveId, payload, controllerHash, opts = {}) {
959
1169
  const seq = eng.seq++;
960
1170
  // rngDraws: the captured-entropy budget this intent's plan may consume (each minted id
@@ -1044,7 +1254,7 @@ async function mainIntentIds(eng, ws) {
1044
1254
  * engine has no durable store), obvious attacks drop. Admitted intents merge to main,
1045
1255
  * durably push, the branch is consumed.
1046
1256
  */
1047
- export async function admitAll(eng, ws, ledger, { refuseDomains = [], shardRouting = null } = {}) {
1257
+ export async function admitAll(eng, ws, ledger, { refuseDomains = [], receiptFence = null, shardRouting = null } = {}) {
1048
1258
  const refuse = new Set(refuseDomains);
1049
1259
  const routes = shardRouting ? directiveRoutes(eng) : null;
1050
1260
  const gitdir = gitdirOf(ws);
@@ -1061,8 +1271,22 @@ export async function admitAll(eng, ws, ledger, { refuseDomains = [], shardRouti
1061
1271
  // O(touched)); the HOST authors the coalesced `nomosPropagateSummary` Order into
1062
1272
  // the coordinator afterwards. The coordinator's gate re-verifies everything.
1063
1273
  const summaryCtx = shardRouting && shardRouting.selfLabel && scopedReads(eng).length > 0
1064
- ? { reads: scopedReads(eng), prevs: new Map(), values: new Map(), events: [], rowsDelta: 0 }
1274
+ ? { 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
1275
  : null;
1276
+ if (summaryCtx) {
1277
+ // SLICE 8 — the per-home attribution table, off the shard's OWN receipts
1278
+ // (law-state: which homes this chain owns). A row id whose route-tag slot
1279
+ // matches a home's tag — or which IS a home key (an axis root) — attributes
1280
+ // its existence flip to that home's exact row subtotal.
1281
+ try {
1282
+ for (const r of query(eng, ws, "aggregatesByType", JSON.stringify({ __type: "NomosHomeReceipt" }))) {
1283
+ const hk = r.data && typeof r.data.homeKey === "string" ? r.data.homeKey : null;
1284
+ if (!hk) continue;
1285
+ summaryCtx.homeKeySet.add(hk);
1286
+ summaryCtx.homeTags.set(await routeTagHexOfHomeKey(hk), hk);
1287
+ }
1288
+ } catch (e) { log("home-tag-table-error", { ws, error: String(e).slice(0, 200) }); }
1289
+ }
1066
1290
  const headBefore = summaryCtx ? await git.resolveRef({ fs: eng.fs, gitdir, ref: BRANCH }).catch(() => null) : null;
1067
1291
  for (const [cid, head] of Object.entries(sessions)) {
1068
1292
  const ref = `refs/heads/session/${cid}`;
@@ -1097,6 +1321,20 @@ export async function admitAll(eng, ws, ledger, { refuseDomains = [], shardRouti
1097
1321
  deadLetters.push({ intentId, oid: oid.slice(0, 10), session: cid, error, blob, domain: dom, directiveId: dirId });
1098
1322
  continue;
1099
1323
  }
1324
+ // THE RECEIPT FENCE (capability_marketplace.md slice 1.5 — the ratified
1325
+ // authz condition): a capability RECEIPT (complete/fail/block/deadLetter)
1326
+ // is an EXECUTOR/OWNER write — the open session lane never folds one, so
1327
+ // a hostile peer cannot terminally close orders it didn't fulfill, and a
1328
+ // forged "result" never reaches any watcher. PARKED, not dropped: an
1329
+ // honest-but-misconfigured self-hosted executor sees its receipt in the
1330
+ // DLQ, and the OWNER-GATED retry is the owner-sanctioned unjam (the
1331
+ // lawful lane for a credential-holding self-hoster is /author).
1332
+ if (receiptFence && dirId && Array.isArray(receiptFence[dom]) && receiptFence[dom].includes(dirId)) {
1333
+ const error = `'${dom}/${dirId}' is a capability RECEIPT — receipts are executor/owner writes, never open-session writes. Author it via POST /v1/workspaces/${ws}/author (owner Bearer), or — if this parked receipt is sanctioned — the owner unjams it: POST /v1/workspaces/${ws}/dead-letters/retry`;
1334
+ rejected.push({ oid: oid.slice(0, 10), error, deadLettered: true });
1335
+ deadLetters.push({ intentId, oid: oid.slice(0, 10), session: cid, error, blob, domain: dom, directiveId: dirId });
1336
+ continue;
1337
+ }
1100
1338
  // THE WRONG-HOME GATE (sharding slice 2): a routed directive's home must be
1101
1339
  // assigned to THIS workspace by the coordinator's law-state shard map.
1102
1340
  // Misrouted → the typed `wrong-home` refusal naming the correct workspace
@@ -1309,8 +1547,12 @@ export async function boot(eng, ws, ledger) {
1309
1547
  }
1310
1548
 
1311
1549
  /** Install a tenant package under the nomos controller (validation is the HOST's job). */
1312
- export async function deploy(eng, ws, ledger, usda, domainHash) {
1313
- const result = author(eng, ws, "nomos", "installDomain", installPayload(domainHash, usda, eng.installedBy), eng.hashes.nomos);
1550
+ export async function deploy(eng, ws, ledger, usda, domainHash, dispositions) {
1551
+ // THE EVOLVE-GATE ANSWER LANE (#58): `dispositions` ({retired, retyped, rebinds,
1552
+ // ackBy}) ride the committed installDomain payload — IN-HISTORY facts the one gate
1553
+ // adjudicates the stable-id diff against. Absent for the common (non-destructive)
1554
+ // upgrade; the gate's typed refusal names the remedy when one is needed.
1555
+ const result = author(eng, ws, "nomos", "installDomain", installPayload(domainHash, usda, eng.installedBy, dispositions), eng.hashes.nomos);
1314
1556
  // THE SHARD-IDENTITY ENSURE (#41 — slice 3a): a taxonomy-bearing law carries the
1315
1557
  // homing invariant, which judges against the workspace's OWN declared identity
1316
1558
  // (`NomosShardIdentity`, law-state in the shard's own chain). The worker is the