@githolon/dsl 0.33.0 → 0.34.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.
@@ -0,0 +1,94 @@
1
+ // NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
2
+ // remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
3
+ // wasm32-wasip1 artifact {kernel · SQLite projection · embedded QuickJS engine}.
4
+ //
5
+ // READ-PROJECTION NAMESPACING — the CANONICAL-MANIFEST label transform (the compile-side
6
+ // rename). BUILD-TIME ONLY, split out of `read_projection_namespace.ts` deliberately: this
7
+ // file types against `manifest.ts` (which imports `node:crypto`/`Buffer` for the domain
8
+ // hash), and the classification/qualifier half is imported by PLATFORM-NEUTRAL engine
9
+ // files (`engine_entry.ts`, `wire_encode.ts`, `framework/relations.ts`) that tenant
10
+ // scaffolds typecheck WITHOUT `@types/node`. Keeping the manifest edge here (reached only
11
+ // via `usd.ts`, the build lane) keeps the neutral type graph node-free — the same
12
+ // build/runtime split `stable_ids.ts` vs `stable_ids_types.ts` ratified.
13
+
14
+ import type { CanonicalManifest } from "./manifest.js";
15
+ import type { StableIds, StableAggregateId } from "./stable_ids_types.js";
16
+ import { qualifyReadProjectionType } from "./read_projection_namespace.js";
17
+
18
+ /**
19
+ * Qualify EVERY aggregate-type label a domain's canonical manifest carries — exactly as
20
+ * if the author had renamed `Entry` → `<domainKey>__Entry` in source, which is the #58
21
+ * rename the whole system already supports (sid identity unmoved; the projection folds
22
+ * old-label rows onto the new label via the `was` lineage). Touches: aggregate ids,
23
+ * directive targets, `queries[].returns`, every maintained read's `of` (counts / sums /
24
+ * mins / maxes / spatials / deriveds / combineds / orderedReads), `combineds[].reads`
25
+ * (a related aggregate TYPE — the cross-aggregate invalidation key), and the stable-ids
26
+ * labels (the BARE name is APPENDED to the sid's `was` lineage so pre-namespacing rows
27
+ * normalize onto the qualified label). Fields keep their bare labels (field names never
28
+ * collide across types — the accumulator keys them under the type). Pure + deterministic;
29
+ * with an empty `namespaced` set the manifest is returned UNTOUCHED (the era rule).
30
+ */
31
+ export function qualifyManifestAggregateLabels(
32
+ manifest: CanonicalManifest,
33
+ domainKey: string,
34
+ namespaced: ReadonlySet<string>,
35
+ ): CanonicalManifest {
36
+ if (namespaced.size === 0) return manifest;
37
+ const qa = (n: string): string => (namespaced.has(n) ? qualifyReadProjectionType(domainKey, n) : n);
38
+
39
+ const stableIds: StableIds | undefined =
40
+ manifest.stableIds === undefined
41
+ ? undefined
42
+ : {
43
+ ...manifest.stableIds,
44
+ aggregates: Object.fromEntries(
45
+ Object.entries(manifest.stableIds.aggregates).map(([name, rec]): [string, StableAggregateId] => {
46
+ if (!namespaced.has(name)) return [name, rec];
47
+ // The bare labels this sid's rows may carry: every prior bare label + the current
48
+ // bare name (pre-namespacing rows; the runtime stamp is qualified going forward).
49
+ const was = [...new Set([...(rec.was ?? []), name])];
50
+ return [qa(name), { ...rec, was }];
51
+ }),
52
+ ),
53
+ };
54
+
55
+ return {
56
+ ...manifest,
57
+ aggregates: manifest.aggregates.map((a) => (namespaced.has(a.id) ? { ...a, id: qa(a.id) } : a)),
58
+ directives: manifest.directives.map((d) => (namespaced.has(d.target) ? { ...d, target: qa(d.target) } : d)),
59
+ ...(stableIds !== undefined ? { stableIds } : {}),
60
+ ...(manifest.queries !== undefined
61
+ ? { queries: manifest.queries.map((q) => (namespaced.has(q.returns) ? { ...q, returns: qa(q.returns) } : q)) }
62
+ : {}),
63
+ ...(manifest.counts !== undefined
64
+ ? { counts: manifest.counts.map((c) => (namespaced.has(c.of) ? { ...c, of: qa(c.of) } : c)) }
65
+ : {}),
66
+ ...(manifest.sums !== undefined
67
+ ? { sums: manifest.sums.map((s) => (namespaced.has(s.of) ? { ...s, of: qa(s.of) } : s)) }
68
+ : {}),
69
+ ...(manifest.mins !== undefined
70
+ ? { mins: manifest.mins.map((e) => (namespaced.has(e.of) ? { ...e, of: qa(e.of) } : e)) }
71
+ : {}),
72
+ ...(manifest.maxes !== undefined
73
+ ? { maxes: manifest.maxes.map((e) => (namespaced.has(e.of) ? { ...e, of: qa(e.of) } : e)) }
74
+ : {}),
75
+ ...(manifest.spatials !== undefined
76
+ ? { spatials: manifest.spatials.map((s) => (namespaced.has(s.of) ? { ...s, of: qa(s.of) } : s)) }
77
+ : {}),
78
+ ...(manifest.deriveds !== undefined
79
+ ? { deriveds: manifest.deriveds.map((d) => (namespaced.has(d.of) ? { ...d, of: qa(d.of) } : d)) }
80
+ : {}),
81
+ ...(manifest.combineds !== undefined
82
+ ? {
83
+ combineds: manifest.combineds.map((c) => ({
84
+ ...c,
85
+ ...(namespaced.has(c.of) ? { of: qa(c.of) } : {}),
86
+ ...(namespaced.has(c.reads) ? { reads: qa(c.reads) } : {}),
87
+ })),
88
+ }
89
+ : {}),
90
+ ...(manifest.orderedReads !== undefined
91
+ ? { orderedReads: manifest.orderedReads.map((r) => (namespaced.has(r.of) ? { ...r, of: qa(r.of) } : r)) }
92
+ : {}),
93
+ };
94
+ }
@@ -0,0 +1,152 @@
1
+ // NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
2
+ // remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
3
+ // wasm32-wasip1 artifact {kernel · SQLite projection · embedded QuickJS engine}.
4
+ //
5
+ // READ-PROJECTION (aggregate-type) NAMESPACING — the read-layer mirror of the shipped
6
+ // relation-type namespacing (`relation_schema.ts::qualifyRelationType`).
7
+ //
8
+ // THE DISEASE. The kernel's installed-read-manifest accumulator
9
+ // (`wasm_git_holon.rs::insert_read_projection_value`) unions every ACTIVE install's
10
+ // `nomosReadProjection` fragment and — correctly, fail-closed — REFUSES when two
11
+ // installs declare the SAME key with DIVERGENT shapes ("divergent read projection
12
+ // declaration for key AuthProvider"). Two different domain keys that each declare a
13
+ // same-named aggregate therefore WEDGE the workspace's read plane at serve time.
14
+ //
15
+ // THE CURE (the maintainer's rule: "Namespaces are a good idea. Use them."): a
16
+ // NON-GLOBAL aggregate's TYPE NAME is QUALIFIED by its owning domain key at compile —
17
+ // `Entry` in domain `guestbook` becomes `guestbook.Entry` — so two domains that each
18
+ // declare the same bare name NEVER share an accumulator key. The delimiter is `.`
19
+ // (NOT relations' `__`: the id-mint gate splits a minted id's type tag on the first
20
+ // `_`, and the type must also stay colon-free — see READ_PROJECTION_DELIMITER). The engine stamps the SAME qualified name onto every event's
21
+ // `__type` at plan runtime (see `wire_encode.ts`), so the projected row's type and the
22
+ // installed schema key agree by construction; the BARE name rides the aggregate's
23
+ // stable-id `was` lineage so pre-namespacing rows normalize onto the qualified label
24
+ // (the #58 rename machinery — a qualification IS a rename).
25
+ //
26
+ // GLOBAL (un-namespaced) aggregates — the opt-out. Framework aggregates the KERNEL
27
+ // reads by bare name / bare instance-id convention (AuthProvider for in-kernel JWT
28
+ // verification, RelationTuple for the native authz fold, the domain-lifecycle law
29
+ // records, …) MUST keep their bare identity. They are enumerated DELIBERATELY in the
30
+ // explicit allowlist below (never a heuristic); a drift test pins the list to the
31
+ // real framework exports. A dynamically-named framework aggregate (a factory product,
32
+ // e.g. `platformAdmin()`'s founder aggregate) opts out via `aggregate(id, fields,
33
+ // { global: true })` instead — the flag rides the handle into BOTH the compiler and
34
+ // the sealed engine, so the two classifications can never disagree.
35
+ //
36
+ // ERA RULE (the law is the era): qualification happens at COMPILE — already-deployed
37
+ // packages are byte-identical untouched, and a qualified key is just a NEW name to the
38
+ // kernel accumulator (no kernel change). The `nomosReadProjectionNamespace` era key is
39
+ // emitted IFF a layer qualified ≥1 aggregate — the marker that this bundle's read keys
40
+ // are domain-qualified (mirrors `nomosRelationNamespace`).
41
+
42
+ /**
43
+ * The read-projection (aggregate-type) qualifier delimiter. NOT the relation namespacing's
44
+ * `__`, deliberately: an aggregate type is also the kernel id-mint TYPE TAG
45
+ * (`id-mint/src/lib.rs::type_tag_of` splits a minted id on the FIRST `_`), so an
46
+ * underscore-bearing type can never mint — `guestbook__Entry_uuid` parses as tag
47
+ * `guestbook` and refuses at the id-mint gate. And it must stay colon-free (`ObjectRef::
48
+ * parse` splits addresses on the first `:`). `.` satisfies both parsers. The ONE surface
49
+ * where `.` is illegal — a real-USD PRIM NAME (identifiers only) — is already covered by the
50
+ * `.usda` text lane's `primName()` sanitizer (`usd_layers.ts`), which round-trips the raw id
51
+ * via `nomos:id`; the TYPE STRING everywhere (prim `type` attr, directive `target`,
52
+ * read-projection keys, the runtime `__type` stamp, stable-id labels, the mint tag) is dotted.
53
+ */
54
+ export const READ_PROJECTION_DELIMITER = ".";
55
+
56
+
57
+ /**
58
+ * THE EXPLICIT GLOBAL ALLOWLIST — every STATICALLY-NAMED framework aggregate. These
59
+ * stay BARE on every surface (read projection, prims, stable-ids, runtime `__type`)
60
+ * because the kernel and the framework machinery address them by bare name /
61
+ * bare-name-derived instance ids (`auth-provider:platform`, the relation fold, the
62
+ * policy/lifecycle law records, the signer spine, the key-fanout gate, …).
63
+ *
64
+ * DELIBERATE ENUMERATION, not a heuristic: `read_projection_namespace.test.ts` pins
65
+ * this set to the real `FRAMEWORK_AGGREGATE_IDS` registry so it can never drift.
66
+ * (A tenant aggregate that merely REUSES one of these names also stays bare — that
67
+ * matches the kernel-couples-by-name reality and is exactly the shared-key case the
68
+ * compile-time divergence guard `read_projection_composition.ts` polices.)
69
+ */
70
+ export const GLOBAL_READ_PROJECTION_AGGREGATE_IDS: ReadonlySet<string> = new Set([
71
+ // disclosure
72
+ "Disclosure",
73
+ // domain lifecycle (the law records themselves)
74
+ "PolicyBundle",
75
+ "DomainInstallation",
76
+ // encryption identity + scope (the operator-blind key machinery)
77
+ "EncKey",
78
+ "KeyScope",
79
+ "WrappedKey",
80
+ // genesis / births
81
+ "GenesisWarrant",
82
+ // metered birth (slice A) — NomosBirthRecord is kernel-read by the birth-record
83
+ // gate (the `nomos-birth-record:` prefix); the policy/grant singletons ride with it
84
+ "NomosBirthRecord",
85
+ "BirthPolicy",
86
+ "BirthGrant",
87
+ // home holons
88
+ "HomeOwner",
89
+ "SharedWithMe",
90
+ "BirthAttestation",
91
+ // identity
92
+ "Actor",
93
+ "User",
94
+ "DeviceKey",
95
+ "Organization",
96
+ "PublicUser",
97
+ // native relations (the one authz spine)
98
+ "RelationTuple",
99
+ // repair
100
+ "RepairRecord",
101
+ // signer spine
102
+ "SignerEpoch",
103
+ "SignerDelegation",
104
+ // governance / workspaces
105
+ "WorkspaceSelf",
106
+ "CloudWorkspace",
107
+ "CloudPolicy",
108
+ "QuotaGrant",
109
+ "PlatformGrant",
110
+ "CapabilityBinding",
111
+ "AuthProvider",
112
+ "HostnameClaim",
113
+ ]);
114
+
115
+ /** The minimal handle shape the classification reads (compiler AND engine — one function). */
116
+ export interface NamespaceClassifiable {
117
+ readonly id: string;
118
+ /** The `aggregate(id, fields, { global: true })` opt-out — kernel-coupled dynamic framework aggregates. */
119
+ readonly global?: boolean;
120
+ }
121
+
122
+ /**
123
+ * TRUE iff this aggregate's type name is NAMESPACED (qualified by its owning domain).
124
+ * FALSE for the explicit framework allowlist and for `{ global: true }` handles.
125
+ * ONE classification, consumed by the compiler (USD lowering) and the sealed engine
126
+ * (the runtime `__type` stamp) — they can never disagree.
127
+ */
128
+ export function isNamespacedAggregate(agg: NamespaceClassifiable): boolean {
129
+ return agg.global !== true && !GLOBAL_READ_PROJECTION_AGGREGATE_IDS.has(agg.id);
130
+ }
131
+
132
+ /**
133
+ * Qualify a bare aggregate type by its owning domain key: `("guestbook","Entry") → "guestbook.Entry"`.
134
+ * The domain-key half is MINT-TAG-SAFE-encoded (`_` → `-`): the qualified type is also the kernel
135
+ * id-mint TYPE TAG, and `type_tag_of` splits a minted id on the FIRST `_` — so NO underscore may
136
+ * appear anywhere in the tag (`co2_platform.X` would parse as tag `co2`). Deterministic; the era
137
+ * key witnesses the encoded form.
138
+ */
139
+ export function qualifyReadProjectionType(domainKey: string, type: string): string {
140
+ return `${domainKey.replace(/_/g, "-")}${READ_PROJECTION_DELIMITER}${type}`;
141
+ }
142
+
143
+ /**
144
+ * The NAMESPACED (non-global) aggregate ids a module declares — the set whose type
145
+ * names qualify. Derived from the live handles (the `global` flag rides the handle),
146
+ * shared by `usd.ts` (compile) and `engine_entry.ts` (the dispatch context).
147
+ */
148
+ export function namespacedAggregateIds(aggregates: readonly NamespaceClassifiable[]): Set<string> {
149
+ const out = new Set<string>();
150
+ for (const a of aggregates) if (isNamespacedAggregate(a)) out.add(a.id);
151
+ return out;
152
+ }
package/src/usd.ts CHANGED
@@ -45,6 +45,14 @@ import { domainInterface } from "./shape_compat.js";
45
45
  import { relationType, user, key, anyUser, qualifiedTypeId, qualifyRelationType } from "./relation_schema.js";
46
46
  import { harvestPrincipalFlows } from "./wire_encode.js";
47
47
  import { composeSharedRelationTypes } from "./relation_composition.js";
48
+ import { collectValueObjectUsages } from "./value_object.js";
49
+ import {
50
+ namespacedAggregateIds,
51
+ qualifyReadProjectionType,
52
+ READ_PROJECTION_DELIMITER,
53
+ } from "./read_projection_namespace.js";
54
+ import { qualifyManifestAggregateLabels } from "./read_projection_manifest.js";
55
+ import { guardSharedReadProjectionKeys } from "./read_projection_composition.js";
48
56
 
49
57
  /** The referential marker of a directive prim (kernel `RefMode` wire string). */
50
58
  export type RefMode = WireRefMode;
@@ -276,6 +284,22 @@ export interface UsdLayer {
276
284
  readonly delimiter: string;
277
285
  readonly types: readonly string[];
278
286
  };
287
+ /**
288
+ * THE READ-PROJECTION NAMESPACE ERA KEY (read-projection namespacing — the read-layer mirror of
289
+ * `nomosRelationNamespace`). Present IFF this layer qualified ≥1 (non-global) AGGREGATE TYPE by its
290
+ * owning domain key (`Entry` → `guestbook__Entry`), so two domains / two installs that each declare
291
+ * the same bare aggregate name no longer collide at the kernel's installed-read-manifest accumulator
292
+ * ("divergent read projection declaration for key …" — the fail-closed union stays the trust
293
+ * boundary; a qualified key is simply a NEW name to it). `types` is the SORTED set of qualified type
294
+ * names this layer produced; `delimiter` is the same ratified colon-free `"__"`. OMITTED for a layer
295
+ * whose aggregates are all GLOBAL (the explicit framework allowlist / `{global:true}`) — the marker
296
+ * is a witness; the qualification itself is baked into the layer's keys.
297
+ */
298
+ readonly nomosReadProjectionNamespace?: {
299
+ readonly v: 1;
300
+ readonly delimiter: string;
301
+ readonly types: readonly string[];
302
+ };
279
303
  /**
280
304
  * THE CROSS-CUSTODY ATTESTATION GATE (home_holons §3/§5). Present IFF ≥1 directive declares
281
305
  * `.attestationAuthorized()`: the SORTED set of their ids. A directive named here is authorized by a
@@ -339,6 +363,25 @@ export interface UsdLayer {
339
363
  /** The directive ids declared `.births()` — SORTED. */
340
364
  readonly directives: string[];
341
365
  };
366
+ /**
367
+ * THE BIRTH-RECORD GATE (metered birth, slice A1 — `architecture/metered_birth_and_attested_reads.md`).
368
+ * Present IFF the layer declares ≥1 `.births()` directive AND was compiled under the metered-birth
369
+ * compiler: the law's EXPLICIT declaration that every birth it performs ALSO writes a structural
370
+ * `NomosBirthRecord` event (deterministic id `<prefix><child>`) in the SAME intent as the birth
371
+ * effect — the `.births()` lane appends it automatically, so the declaration is self-fulfilling for
372
+ * honestly-compiled law and the kernel arm only ever bites a forged/hand-built lump. The kernel's
373
+ * `verify_births_witness` reads THIS declaration (the SAME way it reads `nomosBirthGate` — the
374
+ * bundle declares, the kernel honors; NO kernel version-if) and refuses a new-era birth whose
375
+ * re-derived events carry no record (`birth-record-missing`). OMITTED for pre-feature law — the
376
+ * era rule: every chain sealed before this key replays byte-identical, births exactly as today.
377
+ */
378
+ readonly nomosBirthRecordGate?: {
379
+ readonly v: 1;
380
+ /** The record aggregate TYPE name (the framework's `NomosBirthRecord`). */
381
+ readonly aggregate: string;
382
+ /** The deterministic instance-id prefix — the record for child `w` lives at `<prefix><w>`. */
383
+ readonly prefix: string;
384
+ };
342
385
  /**
343
386
  * THE PEER-SOVEREIGN ROOT-OF-TRUST (the BirthAttestation cert chain). Present IFF the law declares
344
387
  * `requireParentAttestedBirth`: a warranted workspace running this law MUST travel with a SIGNED cert
@@ -476,6 +519,14 @@ export interface UsdLayer {
476
519
  readonly nomosValueObjects?: {
477
520
  readonly v: 1;
478
521
  readonly valueObjects: readonly import("./fields.js").ValueObjectDescriptor[];
522
+ /**
523
+ * THE VO↔FIELD ATTACHMENT (additive, hash-inert): which aggregate FIELD carries
524
+ * which VO — `{aggregate, field, vo}` sorted by aggregate then field. Rides THIS
525
+ * key (never the aggregate prim's wire schema) so the canonical manifest/domainHash
526
+ * of a VO field stays byte-equal to a plain `t.jsonObject()` field. Optional for
527
+ * era tolerance: a 0.33.0-compiled package's key parses unchanged without it.
528
+ */
529
+ readonly usages?: readonly import("./value_object.js").ValueObjectUsage[];
479
530
  };
480
531
  }
481
532
 
@@ -499,13 +550,15 @@ function sortPrims(prims: UsdPrim[]): UsdPrim[] {
499
550
  * encoded schema; directive prims map `requiresCapability` → `requires: [cap]`, leave
500
551
  * `scope`/`payloadRef` `undefined` (no static scope today; payload is stage 2).
501
552
  */
502
- function encodeModuleToPrims(layerPath: string, mod: DomainModule): UsdPrim[] {
503
- const manifest = domainManifest(mod);
553
+ function encodeModuleToPrims(layerPath: string, manifest: CanonicalManifest): UsdPrim[] {
504
554
  const prims: UsdPrim[] = [];
505
555
 
506
556
  for (const agg of manifest.aggregates) {
507
557
  prims.push({
508
558
  kind: "aggregate",
559
+ // The IR prim path carries the (possibly dotted-qualified) type verbatim; the `.usda`
560
+ // TEXT lane sanitizes the prim NAME (`usd_layers.ts::primName`) and round-trips the raw
561
+ // id via `nomos:id`, so real-USD identifier rules are already satisfied downstream.
509
562
  path: `${layerPath}/${agg.id}`,
510
563
  type: agg.id,
511
564
  schema: agg.schema,
@@ -538,6 +591,7 @@ function encodeModuleToPrims(layerPath: string, mod: DomainModule): UsdPrim[] {
538
591
 
539
592
  function aggregateFieldKindsForModule(
540
593
  mod: DomainModule,
594
+ qa: (type: string) => string = (n) => n,
541
595
  ): Record<string, Record<string, UsdFieldKindDescriptor>> {
542
596
  const out: Record<string, Record<string, UsdFieldKindDescriptor>> = {};
543
597
  for (const agg of [...(mod.aggregates as AggregateHandle[])].sort((a, b) =>
@@ -551,7 +605,7 @@ function aggregateFieldKindsForModule(
551
605
  ...(field.mapValueKind !== undefined ? { mapValueKind: field.mapValueKind } : {}),
552
606
  };
553
607
  }
554
- out[agg.id] = fields;
608
+ out[qa(agg.id)] = fields;
555
609
  }
556
610
  return out;
557
611
  }
@@ -578,8 +632,12 @@ function readKeyFieldKind(
578
632
  return descriptor.kind;
579
633
  }
580
634
 
581
- function readProjectionForModule(mod: DomainModule, manifest: CanonicalManifest): UsdReadProjection {
582
- const aggregateFieldKinds = aggregateFieldKindsForModule(mod);
635
+ function readProjectionForModule(
636
+ mod: DomainModule,
637
+ manifest: CanonicalManifest,
638
+ qa: (type: string) => string = (n) => n,
639
+ ): UsdReadProjection {
640
+ const aggregateFieldKinds = aggregateFieldKindsForModule(mod, qa);
583
641
  return {
584
642
  aggregateFieldKinds,
585
643
  queries: (manifest.queries ?? []).map((q) => ({
@@ -743,13 +801,35 @@ export function emitUsd(
743
801
  // Carry the domain's declared queries straight from the canonical manifest,
744
802
  // which already applies omit-when-empty: `queries` is present here ONLY when the
745
803
  // module declared them, so a query-free layer is byte-identical to before.
746
- const manifest = domainManifest(l.module);
804
+ const bareManifest = domainManifest(l.module);
805
+ // READ-PROJECTION NAMESPACING: every NON-GLOBAL aggregate's type name is qualified by the
806
+ // owning domain key — on the prims, the identity manifest, the stable-ids labels (bare name
807
+ // appended to `was` — the rename lineage), the read projection, and every `of`/`returns`
808
+ // reference — so two domains / two installs that each declare the same bare name never share
809
+ // an installed-read-manifest accumulator key. GLOBAL (framework allowlist / `{global:true}`)
810
+ // aggregates stay bare (the kernel addresses them by bare name). The engine stamps the SAME
811
+ // qualified name at plan runtime (`wire_encode.ts` via the dispatch context), so rows and
812
+ // schema keys agree by construction. `module.domain ?? module.name` — the SAME qualifier key
813
+ // relation namespacing uses (=== `intent.domain` at plan runtime).
814
+ const rpDomainKey = l.module.domain ?? l.module.name;
815
+ const namespacedAggs = namespacedAggregateIds(
816
+ l.module.aggregates as unknown as readonly { id: string; global?: boolean }[],
817
+ );
818
+ const qaType = (n: string): string =>
819
+ namespacedAggs.has(n) ? qualifyReadProjectionType(rpDomainKey, n) : n;
820
+ const manifest = qualifyManifestAggregateLabels(bareManifest, rpDomainKey, namespacedAggs);
821
+ // THE READ-PROJECTION NAMESPACE ERA KEY: present iff ≥1 aggregate was qualified (the witness
822
+ // marker — mirrors `nomosRelationNamespace`; the qualification itself is baked into the keys).
823
+ const nomosReadProjectionNamespace =
824
+ namespacedAggs.size > 0
825
+ ? { v: 1 as const, delimiter: READ_PROJECTION_DELIMITER, types: [...namespacedAggs].map(qaType).sort() }
826
+ : undefined;
747
827
  // THE INVARIANT-GATE DECLARATION (#41): presence-only, derived from the SAME module
748
828
  // the lump's executable bodies come from — sorted, omit-when-empty (hash-stable for
749
829
  // invariant-free law; see the `UsdLayer.nomosInvariantGate` banner for the era rule).
750
830
  const invariantAggregates = l.module.aggregates
751
831
  .filter((a) => a.hasInvariant === true)
752
- .map((a) => a.id)
832
+ .map((a) => qaType(a.id))
753
833
  .sort();
754
834
  const wsInvariants = [...(l.module.workspaceInvariants ?? [])]
755
835
  .map((w) => ({ id: w.id, on: w.on }))
@@ -882,6 +962,17 @@ export function emitUsd(
882
962
  .sort();
883
963
  const nomosBirthGate =
884
964
  birthDirectives.length > 0 ? { v: 1 as const, directives: birthDirectives } : undefined;
965
+ // THE BIRTH-RECORD GATE (metered birth, slice A1 — the framework's new era key). Present IFF
966
+ // this layer declares ≥1 `.births()` directive AND was compiled under this compiler: the law's
967
+ // EXPLICIT declaration that every birth it performs ALSO writes a structural NomosBirthRecord
968
+ // in the SAME intent (the `.births()` lane appends it automatically). The kernel's
969
+ // `verify_births_witness` reads THIS key (never a version-if): a carried birth whose re-derived
970
+ // events contain no `<prefix><child>` record event is a typed refusal (`birth-record-missing`).
971
+ // Omit-when-empty (the era rule): every pre-feature bundle lacks the key and births as today.
972
+ const nomosBirthRecordGate =
973
+ birthDirectives.length > 0
974
+ ? { v: 1 as const, aggregate: "NomosBirthRecord", prefix: "nomos-birth-record:" }
975
+ : undefined;
885
976
  // THE PEER-SOVEREIGN ROOT-OF-TRUST: the home/child law declared it requires a parent-attested-birth
886
977
  // cert chain (the `requireParentAttestedBirth` sentinel). Emit the era key with the LAW-DECLARED owner +
887
978
  // cert locators (so the kernel reads them, names neither); a bare marker (no locators) falls back to the
@@ -1011,9 +1102,18 @@ export function emitUsd(
1011
1102
  // FIRST-CLASS VALUE OBJECTS: the module's collected descriptors (composeDomainModule
1012
1103
  // dedupes + sorts them). Omit-when-empty (era rule — VO-free law is byte-identical).
1013
1104
  const moduleValueObjects = (l.module as { valueObjects?: readonly import("./fields.js").ValueObjectDescriptor[] }).valueObjects;
1105
+ // THE VO↔FIELD ATTACHMENT: the usage pointers ride the SAME additive key (see
1106
+ // the UsdLayer type) — computed from the live Field metas, sorted, omit-when-empty.
1107
+ const voUsages = collectValueObjectUsages(
1108
+ l.module.aggregates as unknown as readonly { id: string; fields: Record<string, import("./fields.js").Field> }[],
1109
+ );
1014
1110
  const nomosValueObjects =
1015
1111
  moduleValueObjects !== undefined && moduleValueObjects.length > 0
1016
- ? { v: 1 as const, valueObjects: [...moduleValueObjects] }
1112
+ ? {
1113
+ v: 1 as const,
1114
+ valueObjects: [...moduleValueObjects],
1115
+ ...(voUsages.length > 0 ? { usages: voUsages } : {}),
1116
+ }
1017
1117
  : undefined;
1018
1118
  const flowEndorse = [...l.module.directives]
1019
1119
  .flatMap((d) => harvestPrincipalFlows(d as never).map((f) => ({ directiveId: d.id, sink: f.sink, facet: f.facet, reason: f.reason })))
@@ -1024,9 +1124,9 @@ export function emitUsd(
1024
1124
  );
1025
1125
  return {
1026
1126
  path: l.path,
1027
- prims: encodeModuleToPrims(l.path, l.module),
1127
+ prims: encodeModuleToPrims(l.path, manifest),
1028
1128
  nomosIdentityManifest: manifest,
1029
- nomosReadProjection: readProjectionForModule(l.module, manifest),
1129
+ nomosReadProjection: readProjectionForModule(l.module, manifest, qaType),
1030
1130
  ...(manifest.queries !== undefined ? { queries: manifest.queries } : {}),
1031
1131
  ...(manifest.deriveds !== undefined ? { deriveds: manifest.deriveds } : {}),
1032
1132
  ...(manifest.combineds !== undefined ? { combineds: manifest.combineds } : {}),
@@ -1034,7 +1134,9 @@ export function emitUsd(
1034
1134
  ...(manifest.stableIds !== undefined ? { nomosStableIds: manifest.stableIds } : {}),
1035
1135
  // THE STRUCTURAL INTERFACE (#72): the on-ledger OFFER — a client negotiates shape-
1036
1136
  // compatibility against this, reactively, straight from the folded `policy:{hash}.bundle`.
1037
- nomosInterface: domainInterface(l.module, manifest.stableIds),
1137
+ // The client-facing structural offer stays BARE-labelled (labels are cosmetic there;
1138
+ // shape-compat negotiates by sid) — pass the pre-namespacing stable-ids.
1139
+ nomosInterface: domainInterface(l.module, bareManifest.stableIds),
1038
1140
  ...(Object.keys(readGateDirectives).length > 0
1039
1141
  ? {
1040
1142
  nomosReadGate: {
@@ -1055,9 +1157,11 @@ export function emitUsd(
1055
1157
  : {}),
1056
1158
  ...(nomosRelations !== undefined ? { nomosRelations } : {}),
1057
1159
  ...(nomosRelationNamespace !== undefined ? { nomosRelationNamespace } : {}),
1160
+ ...(nomosReadProjectionNamespace !== undefined ? { nomosReadProjectionNamespace } : {}),
1058
1161
  ...(nomosRelationGate !== undefined ? { nomosRelationGate } : {}),
1059
1162
  ...(nomosAttestationGate !== undefined ? { nomosAttestationGate } : {}),
1060
1163
  ...(nomosBirthGate !== undefined ? { nomosBirthGate } : {}),
1164
+ ...(nomosBirthRecordGate !== undefined ? { nomosBirthRecordGate } : {}),
1061
1165
  ...(nomosParentAttestedBirth !== undefined ? { nomosParentAttestedBirth } : {}),
1062
1166
  ...(nomosVerifiedFirstEnroll !== undefined ? { nomosVerifiedFirstEnroll } : {}),
1063
1167
  ...(nomosValueObjects !== undefined ? { nomosValueObjects } : {}),
@@ -1086,6 +1190,12 @@ export function emitUsd(
1086
1190
  // package's per-domain layers: UNION every domain's relations on a shared bare-id type (so nothing
1087
1191
  // is silently dropped) and REFUSE on a divergent shared rewrite (the scalpel). Non-shared /
1088
1192
  // auto-qualified types are untouched → a package with no shared global type is byte-identical.
1193
+ // THE READ-PROJECTION DIVERGENCE GUARD (the scalpel, extended to the read layer): for keys that
1194
+ // remain SHARED across this package's domains (GLOBAL framework aggregates / same-named declared
1195
+ // reads), identical declarations dedupe at the kernel accumulator; a DIVERGENT shared key would
1196
+ // wedge the workspace at serve time — refuse it AT COMPILE instead, naming the key and each
1197
+ // domain's shape. Validation only (never transforms), so a conflict-free package is untouched.
1198
+ guardSharedReadProjectionKeys(out);
1089
1199
  return { layers: composeSharedRelationTypes(out) };
1090
1200
  }
1091
1201
 
package/src/usd_layers.ts CHANGED
@@ -58,6 +58,7 @@
58
58
  * the runtime barrel.
59
59
  */
60
60
  import type { AggregateHandle } from "./aggregate.js";
61
+ import { READ_PROJECTION_DELIMITER } from "./read_projection_namespace.js";
61
62
  import type { Field } from "./fields.js";
62
63
  import type { DomainModule } from "./codegen_dart.js";
63
64
  import type { WireDriver, WireRefMode, WireSchema } from "./wire.js";
@@ -175,6 +176,9 @@ function domainLayerAttrLines(domain: string, layer: UsdLayer, indent: string):
175
176
  ...(layer.nomosRelationNamespace !== undefined
176
177
  ? [`${indent}custom string nomos:relationNamespaceHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosRelationNamespace)))}`]
177
178
  : []),
179
+ ...(layer.nomosReadProjectionNamespace !== undefined
180
+ ? [`${indent}custom string nomos:readProjectionNamespaceHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosReadProjectionNamespace)))}`]
181
+ : []),
178
182
  ...(layer.nomosRelationGate !== undefined
179
183
  ? [`${indent}custom string nomos:relationGateHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosRelationGate)))}`]
180
184
  : []),
@@ -184,6 +188,9 @@ function domainLayerAttrLines(domain: string, layer: UsdLayer, indent: string):
184
188
  ...(layer.nomosBirthGate !== undefined
185
189
  ? [`${indent}custom string nomos:birthGateHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosBirthGate)))}`]
186
190
  : []),
191
+ ...(layer.nomosBirthRecordGate !== undefined
192
+ ? [`${indent}custom string nomos:birthRecordGateHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosBirthRecordGate)))}`]
193
+ : []),
187
194
  ...(layer.nomosParentAttestedBirth !== undefined
188
195
  ? [`${indent}custom string nomos:parentAttestedBirthHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosParentAttestedBirth)))}`]
189
196
  : []),
@@ -779,6 +786,18 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
779
786
  }
780
787
  if (ns.size > 0) relationNamespace = { v: 1, delimiter: "__", types: [...ns].sort() };
781
788
  }
789
+ // THE READ-PROJECTION NAMESPACE ERA KEY (read-projection namespacing): the SORTED union of every
790
+ // layer's qualified aggregate type names. Present iff ANY layer namespaced >=1 aggregate;
791
+ // omit-when-empty (byte-identical for global-only packages). Delimiter is the ratified "__".
792
+ let readProjectionNamespace: UsdLayer["nomosReadProjectionNamespace"];
793
+ {
794
+ const ns = new Set<string>();
795
+ for (const l of stack) {
796
+ if (l.nomosReadProjectionNamespace === undefined) continue;
797
+ for (const id of l.nomosReadProjectionNamespace.types) ns.add(id);
798
+ }
799
+ if (ns.size > 0) readProjectionNamespace = { v: 1, delimiter: READ_PROJECTION_DELIMITER, types: [...ns].sort() };
800
+ }
782
801
  // THE RELATION GATE (P4): directiveId → {relation, object} merged later-wins, SORTED. Omit-when-empty.
783
802
  let relationGate: UsdLayer["nomosRelationGate"];
784
803
  {
@@ -820,6 +839,16 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
820
839
  }
821
840
  if (b.size > 0) birthGate = { v: 1, directives: [...b].sort() };
822
841
  }
842
+ // THE BIRTH-RECORD GATE (metered birth, A1): present iff ANY layer declares it (once declared,
843
+ // declared). Carry the FIRST layer's aggregate/prefix verbatim (the framework constants — do NOT
844
+ // reconstruct). Omit-when-absent (era rule).
845
+ let birthRecordGate: UsdLayer["nomosBirthRecordGate"];
846
+ for (const l of stack) {
847
+ if (l.nomosBirthRecordGate !== undefined) {
848
+ birthRecordGate = l.nomosBirthRecordGate;
849
+ break;
850
+ }
851
+ }
823
852
  // THE PEER-SOVEREIGN ROOT-OF-TRUST: the era key is present iff ANY layer declares it (once required,
824
853
  // required). Carry the FIRST layer's declared owner/cert locators verbatim (preserve them — do NOT
825
854
  // reconstruct {v:1}, which would drop the locators the kernel reads). Omit-when-absent (era rule).
@@ -846,6 +875,29 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
846
875
  verifiedFirstEnroll = { v: 1, directives: rec };
847
876
  }
848
877
  }
878
+ // FIRST-CLASS VALUE OBJECTS (+ the VO↔field usage pointers): descriptors merged
879
+ // later-wins per NAME, usages later-wins per (aggregate, field), both SORTED —
880
+ // so the layered fold stays byte-homomorphic with `emitUsd`'s composed emission.
881
+ // Omit-when-empty (the era rule); `usages` omitted when none (0.33.0 tolerance).
882
+ let valueObjects: UsdLayer["nomosValueObjects"];
883
+ {
884
+ type VoDesc = NonNullable<UsdLayer["nomosValueObjects"]>["valueObjects"][number];
885
+ type VoUse = NonNullable<NonNullable<UsdLayer["nomosValueObjects"]>["usages"]>[number];
886
+ const byName = new Map<string, VoDesc>();
887
+ const byField = new Map<string, VoUse>();
888
+ for (const l of stack) {
889
+ if (l.nomosValueObjects === undefined) continue;
890
+ for (const d of l.nomosValueObjects.valueObjects) byName.set(d.name, d);
891
+ for (const u of l.nomosValueObjects.usages ?? []) byField.set(`${u.aggregate}\u0000${u.field}`, u);
892
+ }
893
+ if (byName.size > 0) {
894
+ const descs = [...byName.values()].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
895
+ const uses = [...byField.values()].sort((a, b) =>
896
+ a.aggregate !== b.aggregate ? (a.aggregate < b.aggregate ? -1 : 1) : a.field < b.field ? -1 : a.field > b.field ? 1 : 0,
897
+ );
898
+ valueObjects = { v: 1, valueObjects: descs, ...(uses.length > 0 ? { usages: uses } : {}) };
899
+ }
900
+ }
849
901
  return {
850
902
  path,
851
903
  prims: [...prims.keys()].sort().map((p) => prims.get(p)!),
@@ -861,11 +913,14 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
861
913
  ...(encryption !== undefined ? { nomosEncryption: encryption } : {}),
862
914
  ...(relations !== undefined ? { nomosRelations: relations } : {}),
863
915
  ...(relationNamespace !== undefined ? { nomosRelationNamespace: relationNamespace } : {}),
916
+ ...(readProjectionNamespace !== undefined ? { nomosReadProjectionNamespace: readProjectionNamespace } : {}),
864
917
  ...(relationGate !== undefined ? { nomosRelationGate: relationGate } : {}),
865
918
  ...(attestationGate !== undefined ? { nomosAttestationGate: attestationGate } : {}),
866
919
  ...(birthGate !== undefined ? { nomosBirthGate: birthGate } : {}),
920
+ ...(birthRecordGate !== undefined ? { nomosBirthRecordGate: birthRecordGate } : {}),
867
921
  ...(parentAttestedBirth !== undefined ? { nomosParentAttestedBirth: parentAttestedBirth } : {}),
868
922
  ...(verifiedFirstEnroll !== undefined ? { nomosVerifiedFirstEnroll: verifiedFirstEnroll } : {}),
923
+ ...(valueObjects !== undefined ? { nomosValueObjects: valueObjects } : {}),
869
924
  };
870
925
  });
871
926
 
@@ -996,8 +1051,10 @@ export function parseUsdaDocument(text: string): UsdDocument {
996
1051
  const relationsByDomain = new Map<string, UsdLayer["nomosRelations"]>();
997
1052
  const relationGateByDomain = new Map<string, UsdLayer["nomosRelationGate"]>();
998
1053
  const relationNamespaceByDomain = new Map<string, UsdLayer["nomosRelationNamespace"]>();
1054
+ const readProjectionNamespaceByDomain = new Map<string, UsdLayer["nomosReadProjectionNamespace"]>();
999
1055
  const attestationGateByDomain = new Map<string, UsdLayer["nomosAttestationGate"]>();
1000
1056
  const birthGateByDomain = new Map<string, UsdLayer["nomosBirthGate"]>();
1057
+ const birthRecordGateByDomain = new Map<string, UsdLayer["nomosBirthRecordGate"]>();
1001
1058
  const parentAttestedBirthByDomain = new Map<string, UsdLayer["nomosParentAttestedBirth"]>();
1002
1059
  const verifiedFirstEnrollByDomain = new Map<string, UsdLayer["nomosVerifiedFirstEnroll"]>();
1003
1060
  const valueObjectsByDomain = new Map<string, UsdLayer["nomosValueObjects"]>();
@@ -1052,6 +1109,11 @@ export function parseUsdaDocument(text: string): UsdDocument {
1052
1109
  if (typeof relNsHex === "string") {
1053
1110
  relationNamespaceByDomain.set(domain, JSON.parse(unhexUtf8(relNsHex)) as UsdLayer["nomosRelationNamespace"]);
1054
1111
  }
1112
+ // THE READ-PROJECTION-NAMESPACE ERA KEY: the hex-carried qualified-aggregate witness.
1113
+ const rpNsHex = p.attrs.get("nomos:readProjectionNamespaceHex");
1114
+ if (typeof rpNsHex === "string") {
1115
+ readProjectionNamespaceByDomain.set(domain, JSON.parse(unhexUtf8(rpNsHex)) as UsdLayer["nomosReadProjectionNamespace"]);
1116
+ }
1055
1117
  // THE ATTESTATION GATE (home_holons §3/§5): the hex-carried attestation-authorized directive set.
1056
1118
  const attGateHex = p.attrs.get("nomos:attestationGateHex");
1057
1119
  if (typeof attGateHex === "string") {
@@ -1062,6 +1124,11 @@ export function parseUsdaDocument(text: string): UsdDocument {
1062
1124
  if (typeof birthGateHex === "string") {
1063
1125
  birthGateByDomain.set(domain, JSON.parse(unhexUtf8(birthGateHex)) as UsdLayer["nomosBirthGate"]);
1064
1126
  }
1127
+ // THE BIRTH-RECORD GATE (metered birth, A1): the hex-carried record declaration.
1128
+ const birthRecordGateHex = p.attrs.get("nomos:birthRecordGateHex");
1129
+ if (typeof birthRecordGateHex === "string") {
1130
+ birthRecordGateByDomain.set(domain, JSON.parse(unhexUtf8(birthRecordGateHex)) as UsdLayer["nomosBirthRecordGate"]);
1131
+ }
1065
1132
  // THE PEER-SOVEREIGN ROOT-OF-TRUST: the hex-carried parent-attested-birth era marker.
1066
1133
  const pabHex = p.attrs.get("nomos:parentAttestedBirthHex");
1067
1134
  if (typeof pabHex === "string") {
@@ -1174,8 +1241,10 @@ export function parseUsdaDocument(text: string): UsdDocument {
1174
1241
  const rel = relationsByDomain.get(domain);
1175
1242
  const relGate = relationGateByDomain.get(domain);
1176
1243
  const relNs = relationNamespaceByDomain.get(domain);
1244
+ const rpNs = readProjectionNamespaceByDomain.get(domain);
1177
1245
  const attGate = attestationGateByDomain.get(domain);
1178
1246
  const birthGate = birthGateByDomain.get(domain);
1247
+ const birthRecordGate = birthRecordGateByDomain.get(domain);
1179
1248
  const parentAttestedBirth = parentAttestedBirthByDomain.get(domain);
1180
1249
  const verifiedFirstEnroll = verifiedFirstEnrollByDomain.get(domain);
1181
1250
  const valueObjects = valueObjectsByDomain.get(domain);
@@ -1194,9 +1263,11 @@ export function parseUsdaDocument(text: string): UsdDocument {
1194
1263
  ...(enc !== undefined ? { nomosEncryption: enc } : {}),
1195
1264
  ...(rel !== undefined ? { nomosRelations: rel } : {}),
1196
1265
  ...(relNs !== undefined ? { nomosRelationNamespace: relNs } : {}),
1266
+ ...(rpNs !== undefined ? { nomosReadProjectionNamespace: rpNs } : {}),
1197
1267
  ...(relGate !== undefined ? { nomosRelationGate: relGate } : {}),
1198
1268
  ...(attGate !== undefined ? { nomosAttestationGate: attGate } : {}),
1199
1269
  ...(birthGate !== undefined ? { nomosBirthGate: birthGate } : {}),
1270
+ ...(birthRecordGate !== undefined ? { nomosBirthRecordGate: birthRecordGate } : {}),
1200
1271
  ...(parentAttestedBirth !== undefined ? { nomosParentAttestedBirth: parentAttestedBirth } : {}),
1201
1272
  ...(verifiedFirstEnroll !== undefined ? { nomosVerifiedFirstEnroll: verifiedFirstEnroll } : {}),
1202
1273
  ...(valueObjects !== undefined ? { nomosValueObjects: valueObjects } : {}),