@githolon/dsl 0.14.0 → 0.16.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.
Files changed (42) hide show
  1. package/dart/example/nomos_webview_transport.dart +2 -2
  2. package/dart/lib/nomos_dsl.dart +4 -4
  3. package/dart/lib/src/{dispatch.dart → offer.dart} +7 -7
  4. package/dart/lib/src/provider.dart +30 -30
  5. package/dart/lib/src/subscriptions.dart +3 -3
  6. package/dart/lib/src/webview_bridge.dart +56 -21
  7. package/dart/test/webview_bridge_test.dart +35 -34
  8. package/package.json +5 -3
  9. package/src/aggregate.ts +9 -1
  10. package/src/authoring.ts +10 -1
  11. package/src/build_package.ts +1 -1
  12. package/src/capability_exports.ts +2 -2
  13. package/src/codegen_dart.ts +34 -24
  14. package/src/codegen_dart_client.ts +74 -12
  15. package/src/codegen_provider_dart.ts +67 -61
  16. package/src/codegen_ts.ts +45 -17
  17. package/src/compile_package_main.ts +103 -36
  18. package/src/ctx.ts +13 -0
  19. package/src/engine_entry.ts +22 -5
  20. package/src/fields.ts +44 -0
  21. package/src/flow_gate.ts +208 -0
  22. package/src/framework/capability.ts +29 -11
  23. package/src/framework/encryption_scope.ts +9 -1
  24. package/src/framework/genesis.ts +5 -1
  25. package/src/framework/impure_capability.ts +73 -67
  26. package/src/framework/relations.ts +23 -4
  27. package/src/framework/signing_identity.ts +1 -1
  28. package/src/framework/sync_lifecycle.ts +3 -3
  29. package/src/framework/workspace_invariant.ts +29 -8
  30. package/src/framework/workspaces.ts +7 -0
  31. package/src/index.ts +25 -1
  32. package/src/manifest.ts +11 -17
  33. package/src/ops.ts +42 -1
  34. package/src/principal_evidence.ts +112 -0
  35. package/src/relation_sugar.ts +10 -4
  36. package/src/shape_compat.ts +208 -0
  37. package/src/usd.ts +287 -0
  38. package/src/usd_layers.ts +213 -26
  39. package/src/wire.ts +8 -0
  40. package/src/wire_encode.ts +75 -2
  41. package/src/workspace_sharding.ts +197 -24
  42. package/src/workspace_type.ts +1 -0
package/src/wire.ts CHANGED
@@ -127,6 +127,14 @@ export interface WireIntent {
127
127
  strikes: string[];
128
128
  events: WireEvent[];
129
129
  reads?: WireRead[];
130
+ /**
131
+ * THE OBSERVED ENDORSE FOOTPRINT (security_model.md — the integrity dual, Slice B): every
132
+ * PrincipalEvidence flow this plan run actually produced `{sink, facet, reason}`. A TRANSIENT
133
+ * verify-input — it rides the plan output the gate checks against the law's `nomosFlowGate.endorse`
134
+ * (author + replay), NEVER the committed intent's events, so it is not OID-hashed. OMITTED when
135
+ * empty so a principal-free intent is byte-identical.
136
+ */
137
+ endorse?: { sink: string; facet: string; reason: string | null }[];
130
138
  }
131
139
 
132
140
  /**
@@ -26,7 +26,8 @@ import type { AggregateHandle } from "./aggregate.js";
26
26
  import type { Field, EvidenceRefValue } from "./fields.js";
27
27
  import type { Directive, ReferentialMarker } from "./directive.js";
28
28
  import type { PlannedOp, FieldOp } from "./ops.js";
29
- import type { Ports } from "./ctx.js";
29
+ import { deterministicPorts, type Ports } from "./ctx.js";
30
+ import type { PrincipalFacet } from "./principal_evidence.js";
30
31
  import type {
31
32
  WireEvent,
32
33
  WireHlc,
@@ -290,5 +291,77 @@ export function executeDirectiveToIntent<P>(
290
291
  return { aggregate, marker: eventMarker, ops };
291
292
  });
292
293
  // Attach the captured read footprint — OMITTED when empty so a read-free intent is byte-identical.
293
- return { id: intentIdFromHlc(hlc), hlc, strikes, events, ...(reads.length ? { reads } : {}) };
294
+ // Attach the OBSERVED ENDORSE footprint (Slice B) — the principal flows this run produced; the gate
295
+ // checks them against the law's nomosFlowGate.endorse. Transient verify-input (not OID-hashed),
296
+ // OMITTED when empty so a principal-free intent is byte-identical.
297
+ const endorse = flowsFromOps(ops);
298
+ return {
299
+ id: intentIdFromHlc(hlc),
300
+ hlc,
301
+ strikes,
302
+ events,
303
+ ...(reads.length ? { reads } : {}),
304
+ ...(endorse.length ? { endorse } : {}),
305
+ };
306
+ }
307
+
308
+ /** One declared principal flow harvested from a plan — `sink` is `<AggregateType>.<field>`. */
309
+ export interface PrincipalFlow {
310
+ readonly sink: string;
311
+ readonly facet: PrincipalFacet;
312
+ readonly reason: string | null;
313
+ }
314
+
315
+ /** Extract the principal flows carried by a plan's field ops (those with an `endorsement`). The ONE
316
+ * op→flow mapping shared by the discovery harvest (compile) and the runtime footprint (Slice B). */
317
+ function flowsFromOps(ops: readonly FieldOp[]): PrincipalFlow[] {
318
+ const flows: PrincipalFlow[] = [];
319
+ for (const op of ops) {
320
+ if (op.endorsement) {
321
+ flows.push({ sink: `${op.aggregateType ?? op.aggregateId}.${op.field}`, facet: op.endorsement.facet, reason: op.endorsement.reason });
322
+ }
323
+ }
324
+ return flows.sort((a, b) =>
325
+ a.sink !== b.sink ? (a.sink < b.sink ? -1 : 1) : a.facet < b.facet ? -1 : a.facet > b.facet ? 1 : 0,
326
+ );
327
+ }
328
+
329
+ /**
330
+ * THE DISCOVERY HARVEST (security_model.md — discovery IS law): run a directive's PURE plan with a
331
+ * probe payload + a probe principal, and harvest every PrincipalEvidence conversion's declared flow
332
+ * (`{sink, facet, reason}`). Executes the plan (not its AST) — the same technique as the rule-4 reads
333
+ * probe. Best-effort + fail-open on HARVEST (a probe that can't drive a branch just omits that flow);
334
+ * the omission is SAFE because the runtime/replay witness (Slice B) refuses any observed flow absent
335
+ * from the manifest. The reasons are static law, so they are intent-independent. SORTED for determinism.
336
+ */
337
+ export function harvestPrincipalFlows(directive: Directive<unknown>): PrincipalFlow[] {
338
+ const ctx: Ports = deterministicPorts({ principal: "principal:probe" });
339
+ // A probe payload: any field access yields a benign string. Plans that need a typed shape (an
340
+ // array .map, etc.) may throw — caught below; that directive's flows are then witnessed at runtime.
341
+ const probe = new Proxy({}, { get: () => "probe" });
342
+ // A plan that MINTS (`create(Agg)` — the common `.creates` shape) calls `globalThis.nomos.mint`; the
343
+ // harvest runs at compile, where the engine's real shim is absent. Install a deterministic stub so the
344
+ // probe plan runs to completion and its flows ARE harvested (an un-harvested authority flow would be
345
+ // refused at runtime by the Slice B witness — fail-closed, but a false jam). The minted VALUE is
346
+ // irrelevant — a flow's sink is the field path, not the id. Restore any pre-existing shim after.
347
+ const g = globalThis as unknown as { nomos?: { mint?: (t: string) => string; read?: unknown } };
348
+ const hadNomos = "nomos" in g;
349
+ const prior: { mint?: (t: string) => string; read?: unknown } | undefined = g.nomos;
350
+ if (!hadNomos || typeof prior?.mint !== "function") {
351
+ g.nomos = { ...(prior ?? {}), mint: (typeTag: string) => `${typeTag}_0192f00d-0000-7000-8000-000000000000` };
352
+ }
353
+ let ops: FieldOp[];
354
+ __resetAuthoring();
355
+ __resetReads();
356
+ try {
357
+ const returned = directive.plan(probe as never, ctx);
358
+ ops = [...__drainAuthoring(), ...(Array.isArray(returned) ? returned : [])].filter((op): op is FieldOp => op.kind !== "strike");
359
+ } catch {
360
+ try { __drainAuthoring(); __drainReads(); } catch { /* sink already drained */ }
361
+ return [];
362
+ } finally {
363
+ if (hadNomos && prior !== undefined) g.nomos = prior; else delete g.nomos;
364
+ }
365
+ __drainReads();
366
+ return flowsFromOps(ops);
294
367
  }
@@ -10,7 +10,7 @@
10
10
  * invariants, slice 3 delta lane + least-loaded placement).
11
11
  *
12
12
  * `nomos-compile` APPENDS this module to a TAXONOMY-BEARING domain's module list —
13
- * both in the canonical manifests (the placement law is hash-bearing law) and in the
13
+ * both in the compiled package metadata (the placement law is hash-bearing law) and in the
14
14
  * generated engine entry (the plans ship in the lump). A taxonomy-FREE package never
15
15
  * imports it (subpath `@githolon/dsl/workspace-sharding`, never the runtime barrel —
16
16
  * the slice-1 hash-stability law: its bytes must not enter other tenants' lumps).
@@ -73,7 +73,10 @@ import { t } from "./fields.js";
73
73
  import { Lww } from "./drivers.js";
74
74
  import { directive } from "./directive.js";
75
75
  import { set, strike, withMarker, type PlannedOp } from "./ops.js";
76
+ import { create } from "./authoring.js";
77
+ import { impureCapabilityAggregate } from "./framework/impure_capability.js";
76
78
  import { query } from "./query.js";
79
+ import { read } from "./read.js";
77
80
  import { sum } from "./sum.js";
78
81
  import { min, max } from "./extremum.js";
79
82
  import { refAs, workspaceInvariant } from "./framework/workspace_invariant.js";
@@ -110,7 +113,7 @@ export interface ShardingRouteSpec {
110
113
  export interface ShardingLawSpec {
111
114
  readonly axes: readonly ShardingAxisSpec[];
112
115
  /**
113
- * THE DERIVED ROUTES (slice 2's canonical-manifest `routes`, fed back into the law):
116
+ * THE DERIVED ROUTES (slice 2's package route facts, fed back into the law):
114
117
  * per routed directive, the homing invariant `nomosWrongHome:<directive>` is derived
115
118
  * so THE CHAIN GATE ITSELF refuses a wrong-home write (#41 — slice 3a; the edge
116
119
  * bailiff stays as defense-in-depth). OMITTED routes ⇒ no invariants (slice-2 shape).
@@ -156,6 +159,15 @@ export const checkpointSealRowId = (seq: number | string): string => `nomos-chec
156
159
  /** The LATEST seal (the head pointer — same aggregate, well-known id; O(1) to find). */
157
160
  export const NOMOS_CHECKPOINT_HEAD_ID = "nomos-checkpoint-seal:head";
158
161
 
162
+ /** THE MOVE OBLIGATION id (P6 — sharding orchestration as outbound offers): one obligation per
163
+ * (move, leg), keyed deterministically so re-entry is idempotent (the receipt-leg obligation re-folds
164
+ * to the same row, never a duplicate). `<from>-<to>` names the move; the leg names the step. */
165
+ export const moveObligationRowId = (fromShard: string, toShard: string, leg: string): string =>
166
+ `nomos-move-obligation:${fromShard}-${toShard}:${leg}`;
167
+ /** The move-leg sequence the coordinator emits as outbound offers (architecture/sharding_as_outbound_offers.md). */
168
+ export const MOVE_LEGS = ["receipt", "migrate", "seal", "open", "handoff"] as const;
169
+ export type MoveLeg = (typeof MOVE_LEGS)[number];
170
+
159
171
  /** The estate-total bucket of a scoped read's group (`nomosEstateSummary` group key).
160
172
  * `|` — printable (the kernel's canonical event decode refuses raw control bytes);
161
173
  * unambiguous because a SCOPED read's id may not contain `|` (compile-refused);
@@ -201,6 +213,13 @@ export const NomosShardAssignment = aggregate("NomosShardAssignment", {
201
213
  placedAt: t.string().merge(Lww),
202
214
  });
203
215
 
216
+ /** The home's CURRENT assignment row, keyed by home key. Top-level so the §6 move directives can
217
+ * CAPTURED-READ it (P6 S3a — the map version is derived in law, not by the host). Also routed by
218
+ * the law module under the same id. */
219
+ export const nomosShardAssignmentByHomeKey = query("nomosShardAssignmentByHomeKey")
220
+ .key("homeKey")
221
+ .returns(NomosShardAssignment);
222
+
204
223
  /**
205
224
  * THE SHARD'S OWN IDENTITY — law-state in the shard's OWN chain (§3's "its assignment"
206
225
  * fact, the slice-3a slice of it): which shard label this workspace IS. An Ensure
@@ -313,11 +332,17 @@ export const NomosSummarySubtotal = aggregate("NomosSummarySubtotal", {
313
332
  empty: t.int().merge(Lww).optional(),
314
333
  });
315
334
 
335
+ /** Per-(shard) committed subtotal rows, keyed by shard. Top-level so the derived placement directive can
336
+ * CAPTURED-READ each open shard's load (the `nomosShardRows` subtotal) to pick LEAST-LOADED IN THE PLAN
337
+ * (holon-native placement, P6 slice 3 — the host no longer picks). Also routed by the law module. */
338
+ export const nomosSummarySubtotalByShard = query("nomosSummarySubtotalByShard")
339
+ .key("shard")
340
+ .returns(NomosSummarySubtotal);
341
+
316
342
  /** THE PER-SHARD DELTA FRONTIER watermark (§5.2 step 1 — contiguity law-state). */
317
343
  export const NomosSummaryFrontier = aggregate("NomosSummaryFrontier", {
318
344
  shard: t.string().merge(Lww),
319
345
  frontier: t.string().merge(Lww), // the shard main oid the subtotals are true at
320
- readManifestHash: t.string().merge(Lww),
321
346
  updatedAt: t.string().merge(Lww),
322
347
  });
323
348
 
@@ -395,7 +420,36 @@ export const nomosReceiveAssignment = directive("nomosReceiveAssignment")
395
420
  // `shard = target` and the map version BUMPED (wrong-home refusals carry it;
396
421
  // clients refresh and retry — §4's self-healing lane).
397
422
 
398
- /** Begin a move: flip each home's assignment to `moving:<toShard>` (gate-verified). */
423
+ /**
424
+ * THE MOVE OBLIGATION (P6 — sharding orchestration as outbound offers, slice S1): one
425
+ * impure-capability-shaped row per (move, leg), driven through the obligation lifecycle the
426
+ * KERNEL already enforces (`scan_requested` emits it on the outbox; `OrphanReceipt`/terminal-once
427
+ * gate its receipt). The coordinator EMITS the move as data; the bailiff delivers each leg + relays
428
+ * the shard's gate verdict as a receipt; each receipt mints the next leg (S2). It carries the
429
+ * `requested`-status lifecycle envelope (`scheduledAt`/`deadlineAt` are required ints — the reactor
430
+ * skips a row missing them) PLUS the move-leg params. S1 is ADDITIVE: `nomosSplitShard` mints the
431
+ * receipt-leg obligation ALONGSIDE the unchanged assignment flip; the host orchestration still runs
432
+ * the move (the obligation is observable on the outbox, not yet consumed). `architecture/sharding_as_outbound_offers.md`.
433
+ */
434
+ export const NomosMoveObligation = impureCapabilityAggregate("NomosMoveObligation", {
435
+ // the move-leg params (the non-envelope fields scan_requested returns as the order's `params`):
436
+ fromShard: t.string().merge(Lww),
437
+ toShard: t.string().merge(Lww),
438
+ homeKeys: t.json().merge(Lww), // the moving home keys (the leg's work list)
439
+ leg: t.string().merge(Lww), // one of MOVE_LEGS
440
+ mapVersion: t.int().merge(Lww), // the law-derived map version (S3a), ferried leg-to-leg
441
+ // S3b — the target shard's ADMITTED set, accumulated migrate→seal (the cross-leg state seal-home
442
+ // needs to compute `missing` against the source chain). The LAW unions it (the adapter only ferries
443
+ // arrays it read off the obligation + the shard response — it never accumulates), so the host holds no
444
+ // cross-leg move state and an adapter restart re-derives everything from the on-chain obligation.
445
+ migratedIds: t.json().merge(Lww), // JSON string array; "[]" until the migrate leg's receipt
446
+ });
447
+
448
+ /** S1 receipt-leg obligation window (ms) — the leg's soft deadline past its scheduledAt. */
449
+ const MOVE_OBLIGATION_WINDOW_MS = 300_000;
450
+
451
+ /** Begin a move: flip each home's assignment to `moving:<toShard>` (gate-verified) AND mint the
452
+ * receipt-leg move obligation (P6 S1 — additive; observable on the outbox, not yet consumed). */
399
453
  export const nomosSplitShard = directive("nomosSplitShard")
400
454
  .mutates(NomosShardAssignment)
401
455
  .payload(
@@ -404,6 +458,9 @@ export const nomosSplitShard = directive("nomosSplitShard")
404
458
  toShard: z.string().regex(/^s\d+$/),
405
459
  homeKeys: z.array(z.string().min(1)).min(1).max(64),
406
460
  startedAt: z.string(),
461
+ // P6 S1: the caller-stamped epoch-ms the move begins (the obligation's scheduledAt). The
462
+ // timestamp rides IN the payload (the determinism doctrine — no clock in the plan).
463
+ scheduledAt: z.number().int().min(0),
407
464
  }),
408
465
  )
409
466
  .plan((p) => {
@@ -413,10 +470,107 @@ export const nomosSplitShard = directive("nomosSplitShard")
413
470
  if (new Set(p.homeKeys).size !== p.homeKeys.length) {
414
471
  throw new Error("split-duplicate: a home key appears twice in one split");
415
472
  }
416
- return p.homeKeys.flatMap((homeKey) => {
473
+ const ops = p.homeKeys.flatMap((homeKey) => {
417
474
  const row = instance(NomosShardAssignment, shardAssignmentRowId(homeKey));
418
475
  return [set(row, "status", movingStatus(p.toShard))];
419
476
  });
477
+ // P6 S3a — THE MAP VERSION IS DERIVED IN LAW, not by the host. CAPTURED-READ each moving home's
478
+ // CURRENT assignment (the pre-apply committed state; the gate re-derives at verify, CAS-refusing on
479
+ // drift) and bump: newMapVersion = max(1, max held) + 1 — the SAME arithmetic `_runSplitMigration`
480
+ // used to do in JS, now on the chain. The floor of 1 guarantees ≥2 (the handoff gate's minimum). The
481
+ // obligation carries it forward (nomosAdvanceMove ferries it leg-to-leg) so the host NEVER computes it.
482
+ let maxHeld = 1;
483
+ for (const homeKey of p.homeKeys) {
484
+ const rows = read(nomosShardAssignmentByHomeKey, { homeKey }) as Array<{ data?: { mapVersion?: number } }>;
485
+ for (const r of rows) {
486
+ const v = r?.data?.mapVersion;
487
+ if (typeof v === "number" && v > maxHeld) maxHeld = v;
488
+ }
489
+ }
490
+ const newMapVersion = maxHeld + 1;
491
+ // P6 S1/S2 — mint the receipt-leg obligation (the standard obligation API: `create` mints the id; its
492
+ // ops are ambient-harvested + merged with these returned ops). status:'requested' + the lifecycle
493
+ // envelope ⇒ the outbox (`scan_requested`) surfaces it as an outbound offer; the move-leg params ride
494
+ // as the order's params. Re-entry is idempotent via the MOVE GATE (a double `nomosSplitShard` refuses
495
+ // `not-active:moving:*` before this runs). The host orchestration is UNCHANGED — observable, not consumed.
496
+ create(NomosMoveObligation)
497
+ .set("requestedBy", "coordinator")
498
+ .set("requestedAt", p.scheduledAt)
499
+ .set("scheduledAt", p.scheduledAt)
500
+ .set("deadlineAt", p.scheduledAt + MOVE_OBLIGATION_WINDOW_MS)
501
+ .set("status", "requested")
502
+ .set("fromShard", p.fromShard)
503
+ .set("toShard", p.toShard)
504
+ .set("homeKeys", JSON.stringify(p.homeKeys)) // t.json() stores the stringified JSON
505
+ .set("leg", "receipt")
506
+ .set("mapVersion", newMapVersion)
507
+ .set("migratedIds", "[]"); // nothing migrated yet — the migrate leg's receipt accumulates it
508
+ return ops;
509
+ })
510
+ .reads(nomosShardAssignmentByHomeKey);
511
+
512
+ /**
513
+ * P6 S2 — THE MOVE RECEIPT (the leg sequence becomes LAW): a leg's receipt COMPLETES its obligation
514
+ * (a terminal status ⇒ the kernel's receipt gate enforces terminal-once + no-orphan, since
515
+ * NomosMoveObligation carries the full impure-capability envelope) AND MINTS THE NEXT leg's obligation —
516
+ * so the order receipt→migrate→seal→open→handoff lives on the chain, not in the host's `_runSplitMigration`
517
+ * JS. The bailiff offers this after delivering a leg + taking the shard's gate verdict (that wiring is S3;
518
+ * S2 drives it directly to prove the chain). The SEAL leg, when `missing` writes raced in on the source,
519
+ * re-mints `migrate` (the bounded re-migrate) instead of advancing to `open`. The LAST leg (handoff) mints
520
+ * nothing — the move is done. (architecture/sharding_as_outbound_offers.md)
521
+ */
522
+ export const nomosAdvanceMove = directive("nomosAdvanceMove")
523
+ .mutates(NomosMoveObligation)
524
+ .payload(
525
+ z.object({
526
+ requestId: z.string().min(1), // the leg obligation being completed (its runtime-minted id)
527
+ leg: z.enum(MOVE_LEGS),
528
+ fromShard: z.string().regex(/^s\d+$/),
529
+ toShard: z.string().regex(/^s\d+$/),
530
+ homeKeys: z.array(z.string().min(1)).min(1).max(64),
531
+ mapVersion: z.number().int().min(2), // the law-derived version (S3a), ferried leg-to-leg
532
+ scheduledAt: z.number().int().min(0),
533
+ completedAt: z.number().int().min(0),
534
+ missing: z.array(z.string()).optional(), // seal leg: writes that raced in, still on the source
535
+ // S3b — the migrated-id ferry (the adapter passes through arrays it READ; the LAW unions them).
536
+ // `priorMigratedIds`: the current obligation's accumulated set (off the obligation). `migratedIds`:
537
+ // THIS leg's shard delta (off the migrate-in response; [] for non-migrate legs). The plan unions
538
+ // them onto the next obligation, so the seal leg's obligation carries the full admitted set.
539
+ priorMigratedIds: z.array(z.string()).default([]),
540
+ migratedIds: z.array(z.string()).default([]),
541
+ }),
542
+ )
543
+ .plan((p) => {
544
+ // THE RECEIPT — drive the current leg obligation TERMINAL (the kernel's terminal-once anchors on this
545
+ // terminal-status set; a second receipt on the same obligation refuses ReceiptOnTerminalObligation).
546
+ const cur = instance(NomosMoveObligation, p.requestId);
547
+ const ops = [
548
+ set(cur, "status", "completed"),
549
+ set(cur, "completedAt", p.completedAt),
550
+ set(cur, "closesObligation", p.requestId), // the closure link (the O(1) closure read)
551
+ ];
552
+ // MINT THE NEXT LEG — the order is law. Seal + raced `missing` re-mints `migrate` (bounded re-migrate).
553
+ const idx = MOVE_LEGS.indexOf(p.leg);
554
+ let nextLeg: MoveLeg | undefined = MOVE_LEGS[idx + 1];
555
+ if (p.leg === "seal" && p.missing !== undefined && p.missing.length > 0) nextLeg = "migrate";
556
+ if (nextLeg !== undefined) {
557
+ // THE LAW UNIONS the migrated ids (the host never accumulates): the next obligation carries the
558
+ // accumulated admitted set forward, so the seal leg POSTs the full set + a re-migrate keeps growing it.
559
+ const accMigrated = [...new Set([...p.priorMigratedIds, ...p.migratedIds])].sort();
560
+ create(NomosMoveObligation)
561
+ .set("requestedBy", "coordinator")
562
+ .set("requestedAt", p.completedAt)
563
+ .set("scheduledAt", p.scheduledAt)
564
+ .set("deadlineAt", p.scheduledAt + MOVE_OBLIGATION_WINDOW_MS)
565
+ .set("status", "requested")
566
+ .set("fromShard", p.fromShard)
567
+ .set("toShard", p.toShard)
568
+ .set("homeKeys", JSON.stringify(p.homeKeys))
569
+ .set("leg", nextLeg)
570
+ .set("mapVersion", p.mapVersion) // carry the law-derived version forward (S3a)
571
+ .set("migratedIds", JSON.stringify(accMigrated)); // S3b — the accumulated admitted set
572
+ }
573
+ return ops;
420
574
  });
421
575
 
422
576
  /**
@@ -530,7 +684,6 @@ export const nomosAmendSummaryFrontier = directive("nomosAmendSummaryFrontier")
530
684
  z.object({
531
685
  shard: z.string().regex(/^s\d+$/),
532
686
  frontier: z.string(),
533
- readManifestHash: z.string(),
534
687
  updatedAt: z.string(),
535
688
  }),
536
689
  )
@@ -539,7 +692,6 @@ export const nomosAmendSummaryFrontier = directive("nomosAmendSummaryFrontier")
539
692
  return [
540
693
  withMarker(set(wm, "shard", p.shard), "ensures"),
541
694
  set(wm, "frontier", p.frontier),
542
- set(wm, "readManifestHash", p.readManifestHash),
543
695
  set(wm, "updatedAt", p.updatedAt),
544
696
  ];
545
697
  });
@@ -656,7 +808,6 @@ export const nomosPropagateSummary = directive("nomosPropagateSummary")
656
808
  shard: z.string().regex(/^s\d+$/),
657
809
  fromOid: z.string(), // "" = the shard's first delta (a virgin coordinator record)
658
810
  toOid: z.string().min(1),
659
- readManifestHash: z.string(),
660
811
  propagatedAt: z.string(),
661
812
  subtotals: z.array(summarySubtotalSchema).min(1).max(1024),
662
813
  events: z.array(summaryEventSchema).max(4096),
@@ -762,7 +913,6 @@ export const nomosPropagateSummary = directive("nomosPropagateSummary")
762
913
  ops.push(
763
914
  withMarker(set(wm, "shard", p.shard), "ensures"),
764
915
  set(wm, "frontier", p.toOid),
765
- set(wm, "readManifestHash", p.readManifestHash),
766
916
  set(wm, "updatedAt", p.propagatedAt),
767
917
  );
768
918
  return ops;
@@ -947,6 +1097,7 @@ export function shardingLawModule(spec: ShardingLawSpec): Record<string, unknown
947
1097
  NomosSummaryFrontier,
948
1098
  NomosDeepVerify,
949
1099
  NomosCheckpointSeal,
1100
+ NomosMoveObligation, // P6 S1 — the move-leg obligation `nomosSplitShard` emits
950
1101
  nomosDeclareShardIdentity,
951
1102
  nomosReceiveAssignment,
952
1103
  nomosOpenShard,
@@ -956,17 +1107,14 @@ export function shardingLawModule(spec: ShardingLawSpec): Record<string, unknown
956
1107
  nomosRecordDeepVerify,
957
1108
  nomosCheckpointSeal,
958
1109
  nomosSplitShard,
1110
+ nomosAdvanceMove, // P6 S2 — the move receipt: completes a leg + mints the next
959
1111
  nomosSealHandoff,
960
1112
  nomosSealHome,
961
- nomosShardAssignmentByHomeKey: query("nomosShardAssignmentByHomeKey")
962
- .key("homeKey")
963
- .returns(NomosShardAssignment),
1113
+ nomosShardAssignmentByHomeKey, // top-level (P6 S3a — captured-read by the move directives)
964
1114
  nomosShardIdentityByScope: query("nomosShardIdentityByScope")
965
1115
  .key("scope")
966
1116
  .returns(NomosShardIdentity),
967
- nomosSummarySubtotalByShard: query("nomosSummarySubtotalByShard")
968
- .key("shard")
969
- .returns(NomosSummarySubtotal),
1117
+ nomosSummarySubtotalByShard, // top-level (P6 slice 3 — captured-read by the placement directive)
970
1118
  nomosShardRegistryByStatus: query("nomosShardRegistryByStatus")
971
1119
  .key("status")
972
1120
  .returns(NomosShardRegistry),
@@ -1233,27 +1381,52 @@ export function shardingLawModule(spec: ShardingLawSpec): Record<string, unknown
1233
1381
  // by the dispatching client/worker over committed load subtotals — law-state,
1234
1382
  // never a live read a plan takes); the row id is the home key (ONE active
1235
1383
  // assignment per home, by construction); a conflicting re-offer refuses typed.
1384
+ // HOLON-NATIVE PLACEMENT (P6 slice 3): the PLAN picks least-loaded — the host no longer chooses the
1385
+ // shard. `shard` is GONE from the payload. The plan CAPTURED-READS the home's held assignment (idempotent
1386
+ // reuse) and each open shard's committed `nomosShardRows` subtotal (least-loaded, ties → lowest ordinal),
1387
+ // both #58 declared reads the gate re-derives at verify (CAS — a load that moved refuses typed). Pure over
1388
+ // committed coordinator state ⇒ every peer places identically. (architecture/holon_native_routing.md)
1389
+ const pool = axis.shardCount;
1236
1390
  mod[dirId] = directive(dirId)
1237
1391
  .ensures(NomosShardAssignment)
1238
1392
  .payload(
1239
1393
  z.object({
1240
1394
  [keyField]: z.string().min(1),
1241
- shard: z.string().regex(/^s\d+$/),
1242
1395
  placedAt: z.string(),
1243
1396
  }),
1244
1397
  )
1245
1398
  .plan((p: Record<string, string>) => {
1246
1399
  const homeKey = p[keyField]!;
1247
1400
  const row = instance(NomosShardAssignment, shardAssignmentRowId(homeKey));
1401
+ // IDEMPOTENCY: a home already placed reuses ITS shard (re-offer is a no-op; the uniqueness
1402
+ // invariant guards a mid-move / moved home). A fresh home picks least-loaded over the open pool.
1403
+ const held = read(nomosShardAssignmentByHomeKey, { homeKey }) as Array<{ data?: { shard?: string } }>;
1404
+ const heldShard = held[0]?.data?.shard;
1405
+ let shard: string;
1406
+ if (typeof heldShard === "string" && heldShard) {
1407
+ shard = heldShard;
1408
+ } else {
1409
+ let best = "s0";
1410
+ let bestLoad = Infinity;
1411
+ for (let i = 0; i < pool; i++) {
1412
+ const label = `s${i}`;
1413
+ const subs = read(nomosSummarySubtotalByShard, { shard: label }) as Array<{ data?: { readId?: string; value?: number } }>;
1414
+ const sub = subs.find((r) => r?.data?.readId === NOMOS_SHARD_ROWS_READ);
1415
+ const load = typeof sub?.data?.value === "number" ? sub.data!.value! : 0;
1416
+ if (load < bestLoad) { best = label; bestLoad = load; } // strict < ⇒ ties keep the lowest ordinal
1417
+ }
1418
+ shard = best;
1419
+ }
1248
1420
  return [
1249
1421
  withMarker(set(row, "homeType", axisType), "ensures"),
1250
1422
  set(row, "homeKey", homeKey),
1251
- set(row, "shard", p.shard!),
1423
+ set(row, "shard", shard),
1252
1424
  set(row, "mapVersion", 1),
1253
1425
  set(row, "status", "active"),
1254
1426
  set(row, "placedAt", p.placedAt!),
1255
1427
  ];
1256
- });
1428
+ })
1429
+ .reads(nomosShardAssignmentByHomeKey, nomosSummarySubtotalByShard);
1257
1430
  // PLACEMENT UNIQUENESS (task #42 item 5): an idempotent re-offer (same shard)
1258
1431
  // re-folds the same row; a CONFLICTING one refuses typed with the held shard.
1259
1432
  // THE MOVE-LANE GUARD (slice 5): a re-offer may only reproduce a FRESH placement
@@ -1267,17 +1440,17 @@ export function shardingLawModule(spec: ShardingLawSpec): Record<string, unknown
1267
1440
  .reads(({ intent }) => [
1268
1441
  refAs("held", "NomosShardAssignment", `pre:${shardAssignmentRowId(String(intent[keyField] ?? ""))}`),
1269
1442
  ])
1270
- .assert((snapshots, ctx) => {
1443
+ .assert((snapshots) => {
1271
1444
  const held = snapshots["held"] ?? {};
1272
1445
  const heldShard = held["shard"];
1273
- if (typeof heldShard !== "string" || heldShard.length === 0) return { accept: true };
1446
+ if (typeof heldShard !== "string" || heldShard.length === 0) return { accept: true }; // a virgin home — place it
1274
1447
  const target = movingTargetOf(held["status"]);
1275
1448
  if (target !== undefined) return { reject: `home-moving:${target}` };
1276
1449
  const heldVersion = typeof held["mapVersion"] === "number" ? (held["mapVersion"] as number) : 1;
1277
- const offered = ctx?.intent?.["shard"];
1278
- return offered === heldShard && heldVersion === 1
1279
- ? { accept: true } // the idempotent re-offer — same home, same shard, fresh era
1280
- : { reject: `placement-exists:${heldShard}` };
1450
+ // The plan REUSES the held shard for an already-placed home (holon-native placement, slice 3), so a
1451
+ // re-offer to a FRESH placement (v1) is byte-identical — admit. A MOVED home (v>1) would regress its
1452
+ // version to 1, so it refuses typed (the routed client answers such re-offers from the held row).
1453
+ return heldVersion === 1 ? { accept: true } : { reject: `placement-exists:${heldShard}` };
1281
1454
  });
1282
1455
  }
1283
1456
  return mod;
@@ -367,6 +367,7 @@ export function deriveHoming(
367
367
  "NomosShardAssignment", "NomosShardIdentity", "NomosHomeReceipt", "NomosShardRegistry",
368
368
  "NomosShardPolicy", "NomosSummarySubtotal", "NomosSummaryFrontier", "NomosDeepVerify",
369
369
  "NomosCheckpointSeal",
370
+ "NomosMoveObligation", // P6 — the move-leg obligation is coordinator law-state (no homing t.ref)
370
371
  ]);
371
372
  const childIdsAll = new Set([...types.values()].flatMap((t) => [...t.childIds]));
372
373
  const coordinators = [...types.values()]