@githolon/dsl 0.33.0 → 0.34.1

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,117 @@
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
+ // COMPILE-TIME DIVERGENCE GUARD for SHARED read-projection keys — the read-layer
6
+ // sibling of `relation_composition.ts` (the scalpel).
7
+ //
8
+ // THE MODEL. The kernel's installed-read-manifest accumulator
9
+ // (`wasm_git_holon.rs::ReadProjectionAccumulator`) UNIONS every active install's
10
+ // `nomosReadProjection` fragment BY KEY — `aggregateFieldKinds` by aggregate TYPE,
11
+ // `queries`/`counts`/`spatials`/`sums`/`mins`/`maxes`/`orderedReads` by declared `id`,
12
+ // `deriveds`/`combineds` by `"<of>\0<id>"` — and (correctly, fail-closed) REFUSES a
13
+ // key declared twice with DIVERGENT shapes: "divergent read projection declaration
14
+ // for key <k>". At RUNTIME that refusal WEDGES the workspace's read plane.
15
+ //
16
+ // Read-projection NAMESPACING (`read_projection_namespace.ts`) makes a NON-GLOBAL
17
+ // aggregate's keys domain-qualified, so tenant domains never share a key. What CAN
18
+ // still share a key inside one package: GLOBAL framework aggregates declared by ≥2
19
+ // domains (byte-identical by construction — same framework version — but a version
20
+ // skew diverges), and same-named declared reads across domains. This guard turns the
21
+ // runtime wedge into a precise COMPILE refusal: identical shared declarations PASS
22
+ // (the kernel dedupes them), divergent ones REFUSE with the key and each domain's
23
+ // shape. Validation only — a conflict-free package is returned untouched (era rule).
24
+
25
+ import type { UsdLayer } from "./usd.js";
26
+ import { domainOfLayerPath } from "./relation_composition.js";
27
+
28
+ /** One shared-key divergence, gathered for the scalpel message. */
29
+ interface ReadProjectionConflict {
30
+ /** The accumulator section (`aggregateFieldKinds`, `queries`, …). */
31
+ readonly section: string;
32
+ /** The colliding accumulator key (the kernel's exact key). */
33
+ readonly key: string;
34
+ /** Each contributing domain's declaration, in package (weak→strong) order. */
35
+ readonly opinions: { readonly domain: string; readonly shape: string }[];
36
+ }
37
+
38
+ function scalpelMessage(conflicts: readonly ReadProjectionConflict[]): string {
39
+ const lines: string[] = [
40
+ `Refusing to compose the package: shared read-projection key(s) carry DIVERGENT declarations ` +
41
+ `across domains — the kernel's installed-read-manifest accumulator refuses exactly this at ` +
42
+ `SERVE time ("divergent read projection declaration for key …"), wedging the workspace's ` +
43
+ `read plane. Fail-closed compile refusal, not a runtime wedge.`,
44
+ ``,
45
+ ];
46
+ for (const c of conflicts) {
47
+ lines.push(
48
+ ` read projection "${c.section}" key "${c.key.replace("\0", " · ")}" — ${c.opinions.length} divergent declarations:`,
49
+ );
50
+ const w = Math.max(...c.opinions.map((o) => o.domain.length));
51
+ for (const o of c.opinions) lines.push(` · ${o.domain.padEnd(w)} ${o.shape}`);
52
+ }
53
+ lines.push(
54
+ ``,
55
+ `Fix: give the shared key ONE canonical declaration (the domains must agree, byte-for-byte — ` +
56
+ `for a framework aggregate that means the SAME framework version in every domain), or make ` +
57
+ `the declarations DISTINCT (a non-global aggregate auto-qualifies by its owning domain; a ` +
58
+ `declared read can simply take a different id).`,
59
+ );
60
+ return lines.join("\n");
61
+ }
62
+
63
+ type Fragment = NonNullable<UsdLayer["nomosReadProjection"]>;
64
+
65
+ /** The accumulator sections keyed by declared `id` (mirrors `merge_projection_array_by_id`). */
66
+ const BY_ID_SECTIONS = ["queries", "counts", "spatials", "sums", "mins", "maxes", "orderedReads"] as const;
67
+ /** The sections keyed by `"<of>\0<id>"` (mirrors `merge_projection_array_by_type_and_id`). */
68
+ const BY_TYPE_AND_ID_SECTIONS = ["deriveds", "combineds"] as const;
69
+
70
+ /**
71
+ * Guard the package's layers against a shared-read-projection-key divergence — the EXACT key
72
+ * semantics the kernel accumulator merges by. Identical shared declarations pass (dedupe);
73
+ * divergent ones throw the scalpel. Deterministic (sorted keys/sections). Returns the layers
74
+ * UNTOUCHED (validation only).
75
+ *
76
+ * @throws a scalpel `Error` naming the section, key, and each domain's declaration on divergence.
77
+ */
78
+ export function guardSharedReadProjectionKeys(layers: readonly UsdLayer[]): readonly UsdLayer[] {
79
+ // section → key → the contributing {domain, canonical JSON} opinions, in package order.
80
+ const seen = new Map<string, Map<string, { domain: string; shape: string }[]>>();
81
+ const note = (section: string, key: string, domain: string, value: unknown): void => {
82
+ const bySection = seen.get(section) ?? new Map<string, { domain: string; shape: string }[]>();
83
+ seen.set(section, bySection);
84
+ const arr = bySection.get(key) ?? [];
85
+ arr.push({ domain, shape: JSON.stringify(value) });
86
+ bySection.set(key, arr);
87
+ };
88
+
89
+ for (const layer of layers) {
90
+ const frag: Fragment | undefined = layer.nomosReadProjection;
91
+ if (frag === undefined) continue;
92
+ const domain = domainOfLayerPath(layer.path);
93
+ for (const [ty, schema] of Object.entries(frag.aggregateFieldKinds ?? {})) {
94
+ note("aggregateFieldKinds", ty, domain, schema);
95
+ }
96
+ const bag = frag as unknown as Record<string, readonly { id: string; of?: string }[] | undefined>;
97
+ for (const section of BY_ID_SECTIONS) {
98
+ for (const item of bag[section] ?? []) note(section, item.id, domain, item);
99
+ }
100
+ for (const section of BY_TYPE_AND_ID_SECTIONS) {
101
+ for (const item of bag[section] ?? []) note(section, `${item.of}\0${item.id}`, domain, item);
102
+ }
103
+ }
104
+
105
+ const conflicts: ReadProjectionConflict[] = [];
106
+ for (const section of [...seen.keys()].sort()) {
107
+ const bySection = seen.get(section)!;
108
+ for (const key of [...bySection.keys()].sort()) {
109
+ const opinions = bySection.get(key)!;
110
+ if (opinions.length < 2) continue;
111
+ const distinct = new Set(opinions.map((o) => o.shape));
112
+ if (distinct.size > 1) conflicts.push({ section, key, opinions });
113
+ }
114
+ }
115
+ if (conflicts.length > 0) throw new Error(scalpelMessage(conflicts));
116
+ return layers;
117
+ }
@@ -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