@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.
@@ -482,6 +482,13 @@ export function buildGenesisChain(p: {
482
482
  * chains arm the signature requirement.
483
483
  */
484
484
  requiresWarrant?: boolean;
485
+ /**
486
+ * METERED BIRTH (slice A3 — the descendant budget). When present, the child's genesis includes a
487
+ * `seedBirthPolicy` step so it is BORN BOUNDED (its own default birth allowance per subject).
488
+ * OPT-IN: thread it only when the `lawHash` the order names carries `seedBirthPolicy` (a governance
489
+ * package compiled under the metered-birth DSL) — see the poolSteps note below.
490
+ */
491
+ descendantPool?: number;
485
492
  }): string {
486
493
  const founder = "user:" + p.principal;
487
494
  // THE ORDER-DECLARED LAW INSTALL (G3): the child installs the EXACT law the order names — bytes are NOT in
@@ -508,8 +515,29 @@ export function buildGenesisChain(p: {
508
515
  authorizationHash: "",
509
516
  parentHead: "",
510
517
  });
518
+ // METERED BIRTH (slice A3 — THE POOL CHAINS TO ROOT): when the order carries a `descendantPool`, the
519
+ // child is BORN BOUNDED — a `seedBirthPolicy` genesis step writes the child's own BirthPolicy singleton
520
+ // (its default birth allowance) BEFORE the warrant lands, exactly like every other genesis seed (the
521
+ // still-warrant-free prefix runs unsigned). Root bounds platforms (this pool), platforms bound homes,
522
+ // homes bound estates — the tree meters by induction, with zero cross-workspace reads on any gate.
523
+ // OPT-IN (deliberate): the step folds through the CHILD'S OWN gate under the law hash the order names,
524
+ // so it may only be threaded when that law carries `seedBirthPolicy` (a law compiled under this DSL) —
525
+ // an unconditional default would refuse the genesis of every birth pinning an OLDER law hash. V1
526
+ // BOUNDARY: each node enforces its own count-quota; cross-level pool CONSERVATION arithmetic is v2.
527
+ const poolSteps =
528
+ p.descendantPool !== undefined
529
+ ? [
530
+ {
531
+ domain: "workspaces",
532
+ directiveId: "seedBirthPolicy",
533
+ actor: founder,
534
+ payload: { maxBirthsPerPrincipal: p.descendantPool, setBy: p.principal, setAt: p.bornAt },
535
+ },
536
+ ]
537
+ : [];
511
538
  return JSON.stringify([
512
539
  ...installSteps,
540
+ ...poolSteps,
513
541
  // THE WARRANT-FLIP (THE GOVERNANCE DEFAULT): a governance birth is born WARRANTED by default — a REAL
514
542
  // self-id (the workspace name) so signed control-plane authorship is enforced on every lane. The ONLY
515
543
  // way to stay on the legacy host-stamped lane ("" self-id, byte-identical to a pre-VA chain) is an
@@ -926,6 +954,11 @@ export const birthChild = directive("birthChild")
926
954
  * is born WARRANTED (signed authorship enforced on every lane). Absent/false ⇒ the legacy
927
955
  * host-stamped lane (byte-identical to a pre-VA birth). Only meaningful with a genesis. */
928
956
  requiresWarrant: z.boolean().optional(),
957
+ /** METERED BIRTH (slice A3): the child's own descendant budget — when present, the genesis
958
+ * recipe seeds the child's BirthPolicy (`seedBirthPolicy`) so it is BORN BOUNDED. OPT-IN:
959
+ * only pass it when the `lawHash` the order names carries `seedBirthPolicy` (a governance
960
+ * package compiled under the metered-birth DSL). Only meaningful with a genesis. */
961
+ descendantPool: z.number().int().positive().optional(),
929
962
  })
930
963
  // A genesis birth (a platform) must carry a principal AND a bornAt — it owns a subtree, a lineage
931
964
  // anchor, and a law-authored genesis chain whose warrant is stamped at bornAt.
@@ -946,7 +979,7 @@ export const birthChild = directive("birthChild")
946
979
  // EXPLICITLY opts out (`requiresWarrant === false`). Thread the flag through ONLY when the request set
947
980
  // it — undefined falls to buildGenesisChain's warranted default; an explicit `false` rides the compat
948
981
  // hatch (an era-safe legacy re-build).
949
- const genesisChain = buildGenesisChain({ name: p.name, principal: p.principal, parent: p.parent, tier: p.tier, foundingAdminHash: p.foundingAdminHash, bornAt: p.bornAt as string, requestedAt: p.requestedAt, ...(p.frameworkHash ? { frameworkHash: p.frameworkHash } : {}), ...(p.lawHash ? { lawHash: p.lawHash } : {}), ...(p.requiresWarrant !== undefined ? { requiresWarrant: p.requiresWarrant } : {}) });
982
+ const genesisChain = buildGenesisChain({ name: p.name, principal: p.principal, parent: p.parent, tier: p.tier, foundingAdminHash: p.foundingAdminHash, bornAt: p.bornAt as string, requestedAt: p.requestedAt, ...(p.frameworkHash ? { frameworkHash: p.frameworkHash } : {}), ...(p.lawHash ? { lawHash: p.lawHash } : {}), ...(p.requiresWarrant !== undefined ? { requiresWarrant: p.requiresWarrant } : {}), ...(p.descendantPool !== undefined ? { descendantPool: p.descendantPool } : {}) });
950
983
  // THE GENESIS RECIPE IS ALSO A LINEAGE/AUDIT FACT on the parent's chain (the parent records what it
951
984
  // ORDERED — law naming law, legitimate). The kernel does NOT read this field to detect the birth.
952
985
  rec.set("genesisChain", genesisChain);
@@ -1645,3 +1678,11 @@ export const workspaceQuotaBirth = workspaceInvariant("WorkspaceQuotaBirth")
1645
1678
  .assert((_snapshots, ctx) =>
1646
1679
  quotaVerdict(ctx?.reads ?? [], String((ctx?.intent ?? {}).principal ?? "")),
1647
1680
  );
1681
+
1682
+ // ── METERED BIRTH (slice A — the allowance governance in the workspaces domain) ─────────────────────
1683
+ // Re-exported here so any governance package composed from THIS module carries the birth-allowance
1684
+ // surface: `setBirthPolicy`/`grantBirthAllowance` (admin-gated) and `seedBirthPolicy` — the A3 genesis
1685
+ // seed `buildGenesisChain`'s `descendantPool` step dispatches as `workspaces/seedBirthPolicy` (it must
1686
+ // live in the SAME domain key the genesis recipe names). The meter's structural half (record + quota)
1687
+ // is auto-ensured by `.births()` — see `../birth_meter.ts`.
1688
+ export { setBirthPolicy, seedBirthPolicy, grantBirthAllowance, BirthPolicy, BirthGrant, NomosBirthRecord, birthRecordsByPrincipal, birthPolicy, birthGrantByPrincipal } from "./birth_meter.js";
package/src/index.ts CHANGED
@@ -40,6 +40,26 @@ export {
40
40
  secretField,
41
41
  publicField,
42
42
  } from "./fields.js";
43
+ // FIRST-CLASS COMPOSABLE VALUE OBJECTS (`value_object.ts`): named, reusable, nestable
44
+ // structured values — ONE definition yields the zod (payload validation), the TS type,
45
+ // the generated Dart class, and the `nomosValueObjects` IR descriptor. A VO lowers to a
46
+ // single Lww-class json-object leaf (wholesale-replace merge — no per-subfield merge).
47
+ // `geo.*` are the RFC 7946 geometry VOs (pair `geo.point()` with `spatial(...)`).
48
+ export {
49
+ valueObject,
50
+ geo,
51
+ type ValueObject,
52
+ type ValueObjectValue,
53
+ type OptionalValueObject,
54
+ type ValueObjectShapeEntry,
55
+ type ValueObjectDescriptor,
56
+ type ValueObjectFieldDescriptor,
57
+ type GeometryKind,
58
+ type GeoPointValue,
59
+ type GeoLineStringValue,
60
+ type GeoPolygonValue,
61
+ type ValueObjectUsage,
62
+ } from "./value_object.js";
43
63
  export {
44
64
  type PrincipalEvidence,
45
65
  type AttributionPrincipal,
@@ -312,6 +332,28 @@ export {
312
332
  birthAttestation,
313
333
  parentAttestedBirth,
314
334
  } from "./framework/home.js";
335
+ // METERED BIRTH (slice A — architecture/metered_birth_and_attested_reads.md): the structural birth
336
+ // record + the parent-local birth quota + the allowance governance surface. The record/quota STRUCTURE
337
+ // is auto-ensured on every `.births()` law (zero tenant code); these exports are the GOVERNANCE half a
338
+ // tenant composes in to set/override allowances, plus the constants/predicates tests and tools read.
339
+ export {
340
+ NomosBirthRecord,
341
+ BirthPolicy,
342
+ BirthGrant,
343
+ BIRTH_POLICY_ID,
344
+ NOMOS_BIRTH_RECORD_PREFIX,
345
+ birthRecordId,
346
+ birthGrantId,
347
+ birthRecordsByPrincipal,
348
+ birthPolicy,
349
+ birthGrantByPrincipal,
350
+ DEFAULT_MAX_BIRTHS_PER_PRINCIPAL,
351
+ birthQuotaVerdict,
352
+ birthQuotaInvariantId,
353
+ setBirthPolicy,
354
+ seedBirthPolicy,
355
+ grantBirthAllowance,
356
+ } from "./framework/birth_meter.js";
315
357
  // THE PLATFORM FOUNDING-ADMIN BOOTSTRAP (the platform analog of seedHomeCreator) — a tenant-importable
316
358
  // factory that seats a tenant-named governance relation for a write-once, author-bound founder at genesis.
317
359
  export {
@@ -438,6 +480,17 @@ export {
438
480
  type AllowedSubject,
439
481
  type RelationCaps,
440
482
  } from "./relation_schema.js";
483
+
484
+ // READ-PROJECTION (aggregate-type) NAMESPACING — the read-layer mirror of the relation namespacing:
485
+ // the qualifier, the deliberate framework GLOBAL allowlist, and the classification the compiler +
486
+ // sealed engine share (`aggregate(id, fields, { global: true })` is the dynamic opt-out).
487
+ export {
488
+ READ_PROJECTION_DELIMITER,
489
+ GLOBAL_READ_PROJECTION_AGGREGATE_IDS,
490
+ qualifyReadProjectionType,
491
+ isNamespacedAggregate,
492
+ namespacedAggregateIds,
493
+ } from "./read_projection_namespace.js";
441
494
  // SECURE-BY-DEFAULT SUGAR over the relation schema — plain-English combinators, audiences, presets.
442
495
  // Pure sugar: every export here BUILDS the existing Rewrite/RelationTypeDecl AST (no new wire shape).
443
496
  export {
package/src/read.ts CHANGED
@@ -74,6 +74,28 @@ export function read<T = unknown>(query: QueryDecl, args: Record<string, string>
74
74
  return result;
75
75
  }
76
76
 
77
+ /**
78
+ * THE BIRTH-METER CAPTURE (metered birth, slice A2 — library-internal). Identical semantics to
79
+ * {@link read} — serve the declared query live off `nomos.read` + record `{queryId, args, result}`
80
+ * into the SAME per-dispatch footprint — but with a DISTINCT refusal string, deliberately NOT the
81
+ * {@link CAPTURED_READ_LUMP_MARKER}: the marker greps for TENANT-authored `read()` calls that lack a
82
+ * declaration, whereas this door is only ever invoked by the `.births()` lane (which auto-DECLARES
83
+ * its three queries on the directive), so a lump that merely bundles it must not trip the read-free
84
+ * compile gate. A `.births()` dispatch on a host with no `nomos.read` still halts NAMED (fail-closed).
85
+ */
86
+ export function __captureBirthMeterRead(query: QueryDecl, args: Record<string, string>): void {
87
+ const h = host() as { read?: unknown } | undefined;
88
+ if (typeof h?.read !== "function") {
89
+ throw new Error(
90
+ `birth-meter read('${query.id}'): this host provides no nomos.read port — a .births() ` +
91
+ `dispatch captures its quota reads through the engine/client runtime (tests: stub ` +
92
+ `globalThis.nomos.read).`,
93
+ );
94
+ }
95
+ const result = host().read(query.id, args);
96
+ footprint.push({ queryId: query.id, args, result });
97
+ }
98
+
77
99
  /**
78
100
  * The compile gate's lump sentinel — the EXACT substring of the refusal `read()` throws above.
79
101
  * It exists in a bundled engine lump iff `read` itself survived tree-shaking, i.e. iff some
@@ -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
+ }