@githolon/dsl 0.32.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.
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
@@ -464,6 +507,27 @@ export interface UsdLayer {
464
507
  readonly reason: string | null;
465
508
  }[];
466
509
  };
510
+ /**
511
+ * FIRST-CLASS VALUE OBJECTS (`value_object.ts`) — the canonical descriptors of every
512
+ * named value object this layer's aggregates carry (deduped, SORTED by name, nested
513
+ * VOs included), so tooling/visualizers SEE the structure a VO field stores. PURELY
514
+ * DESCRIPTIVE: the kernel reads no gate off it (a VO field folds exactly like a
515
+ * `t.jsonObject()` leaf — one wholesale-replace json value). OMITTED for VO-free law
516
+ * (byte-identical hash — the era rule); declaring a VO moves the PACKAGE hash only
517
+ * (the canonical manifest/domainHash is untouched — the VO is not wire law).
518
+ */
519
+ readonly nomosValueObjects?: {
520
+ readonly v: 1;
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[];
530
+ };
467
531
  }
468
532
 
469
533
  /** The reserved variant name declaring a set's DEFAULT (USD has a default variant). */
@@ -486,13 +550,15 @@ function sortPrims(prims: UsdPrim[]): UsdPrim[] {
486
550
  * encoded schema; directive prims map `requiresCapability` → `requires: [cap]`, leave
487
551
  * `scope`/`payloadRef` `undefined` (no static scope today; payload is stage 2).
488
552
  */
489
- function encodeModuleToPrims(layerPath: string, mod: DomainModule): UsdPrim[] {
490
- const manifest = domainManifest(mod);
553
+ function encodeModuleToPrims(layerPath: string, manifest: CanonicalManifest): UsdPrim[] {
491
554
  const prims: UsdPrim[] = [];
492
555
 
493
556
  for (const agg of manifest.aggregates) {
494
557
  prims.push({
495
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.
496
562
  path: `${layerPath}/${agg.id}`,
497
563
  type: agg.id,
498
564
  schema: agg.schema,
@@ -525,6 +591,7 @@ function encodeModuleToPrims(layerPath: string, mod: DomainModule): UsdPrim[] {
525
591
 
526
592
  function aggregateFieldKindsForModule(
527
593
  mod: DomainModule,
594
+ qa: (type: string) => string = (n) => n,
528
595
  ): Record<string, Record<string, UsdFieldKindDescriptor>> {
529
596
  const out: Record<string, Record<string, UsdFieldKindDescriptor>> = {};
530
597
  for (const agg of [...(mod.aggregates as AggregateHandle[])].sort((a, b) =>
@@ -538,7 +605,7 @@ function aggregateFieldKindsForModule(
538
605
  ...(field.mapValueKind !== undefined ? { mapValueKind: field.mapValueKind } : {}),
539
606
  };
540
607
  }
541
- out[agg.id] = fields;
608
+ out[qa(agg.id)] = fields;
542
609
  }
543
610
  return out;
544
611
  }
@@ -565,8 +632,12 @@ function readKeyFieldKind(
565
632
  return descriptor.kind;
566
633
  }
567
634
 
568
- function readProjectionForModule(mod: DomainModule, manifest: CanonicalManifest): UsdReadProjection {
569
- 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);
570
641
  return {
571
642
  aggregateFieldKinds,
572
643
  queries: (manifest.queries ?? []).map((q) => ({
@@ -730,13 +801,35 @@ export function emitUsd(
730
801
  // Carry the domain's declared queries straight from the canonical manifest,
731
802
  // which already applies omit-when-empty: `queries` is present here ONLY when the
732
803
  // module declared them, so a query-free layer is byte-identical to before.
733
- 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;
734
827
  // THE INVARIANT-GATE DECLARATION (#41): presence-only, derived from the SAME module
735
828
  // the lump's executable bodies come from — sorted, omit-when-empty (hash-stable for
736
829
  // invariant-free law; see the `UsdLayer.nomosInvariantGate` banner for the era rule).
737
830
  const invariantAggregates = l.module.aggregates
738
831
  .filter((a) => a.hasInvariant === true)
739
- .map((a) => a.id)
832
+ .map((a) => qaType(a.id))
740
833
  .sort();
741
834
  const wsInvariants = [...(l.module.workspaceInvariants ?? [])]
742
835
  .map((w) => ({ id: w.id, on: w.on }))
@@ -869,6 +962,17 @@ export function emitUsd(
869
962
  .sort();
870
963
  const nomosBirthGate =
871
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;
872
976
  // THE PEER-SOVEREIGN ROOT-OF-TRUST: the home/child law declared it requires a parent-attested-birth
873
977
  // cert chain (the `requireParentAttestedBirth` sentinel). Emit the era key with the LAW-DECLARED owner +
874
978
  // cert locators (so the kernel reads them, names neither); a bare marker (no locators) falls back to the
@@ -995,6 +1099,22 @@ export function emitUsd(
995
1099
  // conversion's declared flow {directiveId, sink, facet, reason}. Hashed into nomosFlowGate +
996
1100
  // witnessed fail-closed at runtime (Slice B). Omit-when-empty (a principal-free domain is
997
1101
  // byte-identical). SORTED for determinism.
1102
+ // FIRST-CLASS VALUE OBJECTS: the module's collected descriptors (composeDomainModule
1103
+ // dedupes + sorts them). Omit-when-empty (era rule — VO-free law is byte-identical).
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
+ );
1110
+ const nomosValueObjects =
1111
+ moduleValueObjects !== undefined && moduleValueObjects.length > 0
1112
+ ? {
1113
+ v: 1 as const,
1114
+ valueObjects: [...moduleValueObjects],
1115
+ ...(voUsages.length > 0 ? { usages: voUsages } : {}),
1116
+ }
1117
+ : undefined;
998
1118
  const flowEndorse = [...l.module.directives]
999
1119
  .flatMap((d) => harvestPrincipalFlows(d as never).map((f) => ({ directiveId: d.id, sink: f.sink, facet: f.facet, reason: f.reason })))
1000
1120
  .sort((a, b) =>
@@ -1004,9 +1124,9 @@ export function emitUsd(
1004
1124
  );
1005
1125
  return {
1006
1126
  path: l.path,
1007
- prims: encodeModuleToPrims(l.path, l.module),
1127
+ prims: encodeModuleToPrims(l.path, manifest),
1008
1128
  nomosIdentityManifest: manifest,
1009
- nomosReadProjection: readProjectionForModule(l.module, manifest),
1129
+ nomosReadProjection: readProjectionForModule(l.module, manifest, qaType),
1010
1130
  ...(manifest.queries !== undefined ? { queries: manifest.queries } : {}),
1011
1131
  ...(manifest.deriveds !== undefined ? { deriveds: manifest.deriveds } : {}),
1012
1132
  ...(manifest.combineds !== undefined ? { combineds: manifest.combineds } : {}),
@@ -1014,7 +1134,9 @@ export function emitUsd(
1014
1134
  ...(manifest.stableIds !== undefined ? { nomosStableIds: manifest.stableIds } : {}),
1015
1135
  // THE STRUCTURAL INTERFACE (#72): the on-ledger OFFER — a client negotiates shape-
1016
1136
  // compatibility against this, reactively, straight from the folded `policy:{hash}.bundle`.
1017
- 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),
1018
1140
  ...(Object.keys(readGateDirectives).length > 0
1019
1141
  ? {
1020
1142
  nomosReadGate: {
@@ -1035,11 +1157,14 @@ export function emitUsd(
1035
1157
  : {}),
1036
1158
  ...(nomosRelations !== undefined ? { nomosRelations } : {}),
1037
1159
  ...(nomosRelationNamespace !== undefined ? { nomosRelationNamespace } : {}),
1160
+ ...(nomosReadProjectionNamespace !== undefined ? { nomosReadProjectionNamespace } : {}),
1038
1161
  ...(nomosRelationGate !== undefined ? { nomosRelationGate } : {}),
1039
1162
  ...(nomosAttestationGate !== undefined ? { nomosAttestationGate } : {}),
1040
1163
  ...(nomosBirthGate !== undefined ? { nomosBirthGate } : {}),
1164
+ ...(nomosBirthRecordGate !== undefined ? { nomosBirthRecordGate } : {}),
1041
1165
  ...(nomosParentAttestedBirth !== undefined ? { nomosParentAttestedBirth } : {}),
1042
1166
  ...(nomosVerifiedFirstEnroll !== undefined ? { nomosVerifiedFirstEnroll } : {}),
1167
+ ...(nomosValueObjects !== undefined ? { nomosValueObjects } : {}),
1043
1168
  ...(Object.keys(visAggregates).length > 0
1044
1169
  ? { nomosReadVisibility: { v: 1 as const, aggregates: visAggregates } }
1045
1170
  : {}),
@@ -1065,6 +1190,12 @@ export function emitUsd(
1065
1190
  // package's per-domain layers: UNION every domain's relations on a shared bare-id type (so nothing
1066
1191
  // is silently dropped) and REFUSE on a divergent shared rewrite (the scalpel). Non-shared /
1067
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);
1068
1199
  return { layers: composeSharedRelationTypes(out) };
1069
1200
  }
1070
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,12 +188,18 @@ 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
  : []),
190
197
  ...(layer.nomosVerifiedFirstEnroll !== undefined
191
198
  ? [`${indent}custom string nomos:verifiedFirstEnrollHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosVerifiedFirstEnroll)))}`]
192
199
  : []),
200
+ ...(layer.nomosValueObjects !== undefined
201
+ ? [`${indent}custom string nomos:valueObjectsHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosValueObjects)))}`]
202
+ : []),
193
203
  ];
194
204
  }
195
205
 
@@ -776,6 +786,18 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
776
786
  }
777
787
  if (ns.size > 0) relationNamespace = { v: 1, delimiter: "__", types: [...ns].sort() };
778
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
+ }
779
801
  // THE RELATION GATE (P4): directiveId → {relation, object} merged later-wins, SORTED. Omit-when-empty.
780
802
  let relationGate: UsdLayer["nomosRelationGate"];
781
803
  {
@@ -817,6 +839,16 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
817
839
  }
818
840
  if (b.size > 0) birthGate = { v: 1, directives: [...b].sort() };
819
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
+ }
820
852
  // THE PEER-SOVEREIGN ROOT-OF-TRUST: the era key is present iff ANY layer declares it (once required,
821
853
  // required). Carry the FIRST layer's declared owner/cert locators verbatim (preserve them — do NOT
822
854
  // reconstruct {v:1}, which would drop the locators the kernel reads). Omit-when-absent (era rule).
@@ -843,6 +875,29 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
843
875
  verifiedFirstEnroll = { v: 1, directives: rec };
844
876
  }
845
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
+ }
846
901
  return {
847
902
  path,
848
903
  prims: [...prims.keys()].sort().map((p) => prims.get(p)!),
@@ -858,11 +913,14 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
858
913
  ...(encryption !== undefined ? { nomosEncryption: encryption } : {}),
859
914
  ...(relations !== undefined ? { nomosRelations: relations } : {}),
860
915
  ...(relationNamespace !== undefined ? { nomosRelationNamespace: relationNamespace } : {}),
916
+ ...(readProjectionNamespace !== undefined ? { nomosReadProjectionNamespace: readProjectionNamespace } : {}),
861
917
  ...(relationGate !== undefined ? { nomosRelationGate: relationGate } : {}),
862
918
  ...(attestationGate !== undefined ? { nomosAttestationGate: attestationGate } : {}),
863
919
  ...(birthGate !== undefined ? { nomosBirthGate: birthGate } : {}),
920
+ ...(birthRecordGate !== undefined ? { nomosBirthRecordGate: birthRecordGate } : {}),
864
921
  ...(parentAttestedBirth !== undefined ? { nomosParentAttestedBirth: parentAttestedBirth } : {}),
865
922
  ...(verifiedFirstEnroll !== undefined ? { nomosVerifiedFirstEnroll: verifiedFirstEnroll } : {}),
923
+ ...(valueObjects !== undefined ? { nomosValueObjects: valueObjects } : {}),
866
924
  };
867
925
  });
868
926
 
@@ -993,10 +1051,13 @@ export function parseUsdaDocument(text: string): UsdDocument {
993
1051
  const relationsByDomain = new Map<string, UsdLayer["nomosRelations"]>();
994
1052
  const relationGateByDomain = new Map<string, UsdLayer["nomosRelationGate"]>();
995
1053
  const relationNamespaceByDomain = new Map<string, UsdLayer["nomosRelationNamespace"]>();
1054
+ const readProjectionNamespaceByDomain = new Map<string, UsdLayer["nomosReadProjectionNamespace"]>();
996
1055
  const attestationGateByDomain = new Map<string, UsdLayer["nomosAttestationGate"]>();
997
1056
  const birthGateByDomain = new Map<string, UsdLayer["nomosBirthGate"]>();
1057
+ const birthRecordGateByDomain = new Map<string, UsdLayer["nomosBirthRecordGate"]>();
998
1058
  const parentAttestedBirthByDomain = new Map<string, UsdLayer["nomosParentAttestedBirth"]>();
999
1059
  const verifiedFirstEnrollByDomain = new Map<string, UsdLayer["nomosVerifiedFirstEnroll"]>();
1060
+ const valueObjectsByDomain = new Map<string, UsdLayer["nomosValueObjects"]>();
1000
1061
  for (const p of prims) {
1001
1062
  if (p.attrs.get("nomos:kind") === "domain") {
1002
1063
  const domain = attrString(p, "nomos:domain");
@@ -1048,6 +1109,11 @@ export function parseUsdaDocument(text: string): UsdDocument {
1048
1109
  if (typeof relNsHex === "string") {
1049
1110
  relationNamespaceByDomain.set(domain, JSON.parse(unhexUtf8(relNsHex)) as UsdLayer["nomosRelationNamespace"]);
1050
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
+ }
1051
1117
  // THE ATTESTATION GATE (home_holons §3/§5): the hex-carried attestation-authorized directive set.
1052
1118
  const attGateHex = p.attrs.get("nomos:attestationGateHex");
1053
1119
  if (typeof attGateHex === "string") {
@@ -1058,6 +1124,11 @@ export function parseUsdaDocument(text: string): UsdDocument {
1058
1124
  if (typeof birthGateHex === "string") {
1059
1125
  birthGateByDomain.set(domain, JSON.parse(unhexUtf8(birthGateHex)) as UsdLayer["nomosBirthGate"]);
1060
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
+ }
1061
1132
  // THE PEER-SOVEREIGN ROOT-OF-TRUST: the hex-carried parent-attested-birth era marker.
1062
1133
  const pabHex = p.attrs.get("nomos:parentAttestedBirthHex");
1063
1134
  if (typeof pabHex === "string") {
@@ -1068,6 +1139,11 @@ export function parseUsdaDocument(text: string): UsdDocument {
1068
1139
  if (typeof vfeHex === "string") {
1069
1140
  verifiedFirstEnrollByDomain.set(domain, JSON.parse(unhexUtf8(vfeHex)) as UsdLayer["nomosVerifiedFirstEnroll"]);
1070
1141
  }
1142
+ // FIRST-CLASS VALUE OBJECTS: the hex-carried descriptor set (descriptive, not a gate).
1143
+ const voHex = p.attrs.get("nomos:valueObjectsHex");
1144
+ if (typeof voHex === "string") {
1145
+ valueObjectsByDomain.set(domain, JSON.parse(unhexUtf8(voHex)) as UsdLayer["nomosValueObjects"]);
1146
+ }
1071
1147
  }
1072
1148
  }
1073
1149
 
@@ -1165,10 +1241,13 @@ export function parseUsdaDocument(text: string): UsdDocument {
1165
1241
  const rel = relationsByDomain.get(domain);
1166
1242
  const relGate = relationGateByDomain.get(domain);
1167
1243
  const relNs = relationNamespaceByDomain.get(domain);
1244
+ const rpNs = readProjectionNamespaceByDomain.get(domain);
1168
1245
  const attGate = attestationGateByDomain.get(domain);
1169
1246
  const birthGate = birthGateByDomain.get(domain);
1247
+ const birthRecordGate = birthRecordGateByDomain.get(domain);
1170
1248
  const parentAttestedBirth = parentAttestedBirthByDomain.get(domain);
1171
1249
  const verifiedFirstEnroll = verifiedFirstEnrollByDomain.get(domain);
1250
+ const valueObjects = valueObjectsByDomain.get(domain);
1172
1251
  return {
1173
1252
  path: `/Nomos/${domain}`,
1174
1253
  prims: [...b.prims].sort(byPath),
@@ -1184,11 +1263,14 @@ export function parseUsdaDocument(text: string): UsdDocument {
1184
1263
  ...(enc !== undefined ? { nomosEncryption: enc } : {}),
1185
1264
  ...(rel !== undefined ? { nomosRelations: rel } : {}),
1186
1265
  ...(relNs !== undefined ? { nomosRelationNamespace: relNs } : {}),
1266
+ ...(rpNs !== undefined ? { nomosReadProjectionNamespace: rpNs } : {}),
1187
1267
  ...(relGate !== undefined ? { nomosRelationGate: relGate } : {}),
1188
1268
  ...(attGate !== undefined ? { nomosAttestationGate: attGate } : {}),
1189
1269
  ...(birthGate !== undefined ? { nomosBirthGate: birthGate } : {}),
1270
+ ...(birthRecordGate !== undefined ? { nomosBirthRecordGate: birthRecordGate } : {}),
1190
1271
  ...(parentAttestedBirth !== undefined ? { nomosParentAttestedBirth: parentAttestedBirth } : {}),
1191
1272
  ...(verifiedFirstEnroll !== undefined ? { nomosVerifiedFirstEnroll: verifiedFirstEnroll } : {}),
1273
+ ...(valueObjects !== undefined ? { nomosValueObjects: valueObjects } : {}),
1192
1274
  };
1193
1275
  })
1194
1276
  .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));