@githolon/dsl 0.29.1 → 0.30.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/package.json +1 -1
- package/src/codegen_proof.ts +7 -0
- package/src/engine_entry.ts +34 -3
- package/src/framework/relations.ts +80 -3
- package/src/framework/signing_identity.ts +10 -4
- package/src/framework/workspaces.ts +7 -0
- package/src/index.ts +10 -0
- package/src/relation_schema.ts +91 -0
- package/src/usd.ts +99 -16
- package/src/usd_layers.ts +41 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|
package/src/codegen_proof.ts
CHANGED
|
@@ -45,6 +45,7 @@ import type { RelationTypeDecl, Rewrite } from "./relation_schema.js";
|
|
|
45
45
|
import { finishCount } from "./count.js";
|
|
46
46
|
import { mintedCreateField } from "./workspace_routing.js";
|
|
47
47
|
import { deterministicPorts } from "./ctx.js";
|
|
48
|
+
import { __setDispatchDomain } from "./framework/relations.js";
|
|
48
49
|
import { __resetBirths, __drainBirths } from "./births.js";
|
|
49
50
|
import { __resetAuthoring, __drainAuthoring } from "./authoring.js";
|
|
50
51
|
import { __resetReads, __drainReads } from "./read.js";
|
|
@@ -939,9 +940,15 @@ function withBirthProbeGlobals<T>(run: () => T): T {
|
|
|
939
940
|
return h.toString(16).padStart(8, "0").repeat(8);
|
|
940
941
|
},
|
|
941
942
|
};
|
|
943
|
+
// RELATION-NAMESPACING: a probed seed plan may build a relation OBJECT TOKEN (`selfWorkspace()`),
|
|
944
|
+
// which qualifies its type by the dispatching domain. The proof generator's STATIC probe reads the
|
|
945
|
+
// relation name + subject entry key off the drained ops (never the object string), so a sentinel
|
|
946
|
+
// dispatch domain is enough to let the token resolve without throwing — no real domain is needed.
|
|
947
|
+
__setDispatchDomain("proof");
|
|
942
948
|
try {
|
|
943
949
|
return run();
|
|
944
950
|
} finally {
|
|
951
|
+
__setDispatchDomain(undefined);
|
|
945
952
|
if (had && prior !== undefined) g.nomos = prior; else delete g.nomos;
|
|
946
953
|
}
|
|
947
954
|
}
|
package/src/engine_entry.ts
CHANGED
|
@@ -53,7 +53,8 @@
|
|
|
53
53
|
*/
|
|
54
54
|
import { executeDirectiveToIntent } from "./wire_encode.js";
|
|
55
55
|
import { expandCapabilityExports } from "./capability_exports.js";
|
|
56
|
-
import { RelationTuple } from "./framework/relations.js";
|
|
56
|
+
import { RelationTuple, __setDispatchContext } from "./framework/relations.js";
|
|
57
|
+
import { isRelationTypeDecl } from "./relation_schema.js";
|
|
57
58
|
import { synthesizePreconditionInvariants } from "./precondition.js";
|
|
58
59
|
import { generateOpenApiFragment, mergeOpenApiFragments, type OpenApiDoc } from "./codegen_openapi.js";
|
|
59
60
|
import type { QueryDecl } from "./query.js";
|
|
@@ -263,6 +264,12 @@ interface DomainSlice {
|
|
|
263
264
|
aggInvariants: Map<string, AggregateInvariantFn>;
|
|
264
265
|
/** workspace-invariant id → its declaration (#266 — `reads`/`assert` bodies ship HERE). */
|
|
265
266
|
wsInvariants: Map<string, WorkspaceInvariantDecl>;
|
|
267
|
+
/**
|
|
268
|
+
* RELATION-NAMESPACING classification: `<type>#<relation>` for every LOCAL (non-`.asGlobal()`) authored
|
|
269
|
+
* relation this domain declares — the set a bare `writeTuple` object auto-qualifies against. A GLOBAL
|
|
270
|
+
* type's relations (governance/signer/read-visibility) are EXCLUDED so their bare objects stay bare.
|
|
271
|
+
*/
|
|
272
|
+
qualifiableRelations: Set<string>;
|
|
266
273
|
}
|
|
267
274
|
|
|
268
275
|
/** Build one domain's slice from its merged module exports (the original scans). */
|
|
@@ -370,7 +377,18 @@ function buildDomainSlice(mod: DomainModuleExports): DomainSlice {
|
|
|
370
377
|
for (const w of synthesizePreconditionInvariants(directive)) wsInvariants.set(w.id, w);
|
|
371
378
|
}
|
|
372
379
|
|
|
373
|
-
|
|
380
|
+
// ── RELATION-NAMESPACING classification — the LOCAL non-global `(type,relation)` set (by shape) ──
|
|
381
|
+
// A bare `writeTuple({object:"<type>:<id>", relation})` auto-qualifies iff `<type>#<relation>` is here.
|
|
382
|
+
// Global types (`.asGlobal()` — governance `workspace`, signer `identity`) are excluded so their bare
|
|
383
|
+
// objects stay bare; the synthesized read-visibility `workspace` caps are never authored relationTypes,
|
|
384
|
+
// so they are absent here too (their bare grants stay bare — the read gate hardcodes `workspace:self`).
|
|
385
|
+
const qualifiableRelations = new Set<string>();
|
|
386
|
+
for (const v of Object.values(mod)) {
|
|
387
|
+
if (!isRelationTypeDecl(v) || v.global === true) continue;
|
|
388
|
+
for (const rel of v.relations.keys()) qualifiableRelations.add(`${v.type}#${rel}`);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return { registry, deriveds, combineds, invariants, aggInvariants, wsInvariants, qualifiableRelations };
|
|
374
392
|
}
|
|
375
393
|
|
|
376
394
|
/**
|
|
@@ -797,7 +815,20 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
|
|
|
797
815
|
throw new Error(`engine plan: no directive registered for (${domain}, ${directiveId})`);
|
|
798
816
|
}
|
|
799
817
|
const ctx = portsFromHost(job.capturedPorts?.principal);
|
|
800
|
-
|
|
818
|
+
// RELATION-NAMESPACING: publish the dispatching domain + its LOCAL non-global `(type,relation)`
|
|
819
|
+
// classification, so a plan's `writeTuple` object lands on the SAME qualified id the compiler wrote
|
|
820
|
+
// onto the `.requires(R)` gate — a bare `"workspace:self"` for a local relation AUTO-QUALIFIES to
|
|
821
|
+
// `<domain>__workspace:self`; a global/read-visibility/unknown relation's bare object stays bare; a
|
|
822
|
+
// token qualifies explicitly. (`module.domain` === `intent.domain`.) Synchronous plan execution ⇒
|
|
823
|
+
// cleared in the `finally`.
|
|
824
|
+
const slice = sliceOf(domain)!;
|
|
825
|
+
__setDispatchContext({ domain, qualifiable: slice.qualifiableRelations });
|
|
826
|
+
let wire: ReturnType<typeof executeDirectiveToIntent>;
|
|
827
|
+
try {
|
|
828
|
+
wire = executeDirectiveToIntent(entry.directive, entry.agg, intent.payload as never, ctx);
|
|
829
|
+
} finally {
|
|
830
|
+
__setDispatchContext(undefined);
|
|
831
|
+
}
|
|
801
832
|
// Emit {events, strikes} ONLY when a strike is present, else the bare event array —
|
|
802
833
|
// so every strikes-free directive canonicalizes byte-identically to the pre-strikeout contract.
|
|
803
834
|
// The OBSERVED ENDORSE footprint (Slice B) rides the object form too — TRANSIENT (the gate reads
|
|
@@ -47,6 +47,73 @@ import { directive } from "../directive.js";
|
|
|
47
47
|
import { set, setEntry, withMarker, type PlannedOp } from "../ops.js";
|
|
48
48
|
import { query } from "../query.js";
|
|
49
49
|
import { endorsementOf, type AttributionPrincipal, type EndorsedAuthority } from "../principal_evidence.js";
|
|
50
|
+
import { isRelationObjectToken, qualifyRelationType, type RelationObjectToken } from "../relation_schema.js";
|
|
51
|
+
|
|
52
|
+
// ─────────────────────────────── the dispatch context (relation-namespacing) ───────────────────────────────
|
|
53
|
+
//
|
|
54
|
+
// The engine entry publishes the DISPATCHING domain + its own relation-schema classification around each
|
|
55
|
+
// plan dispatch (`engine_entry.ts::plan`), so a `writeTuple` object lands on the SAME qualified id the
|
|
56
|
+
// compiler wrote onto the `.requires(R)` gate — WITHOUT the author naming a domain. Single-threaded
|
|
57
|
+
// synchronous dispatch ⇒ no re-entrancy; cleared in a `finally`.
|
|
58
|
+
//
|
|
59
|
+
// THE DISAMBIGUATION (the maintainer's rule — the natural bare-string code must be correct): a bare
|
|
60
|
+
// `writeTuple({ object:"workspace:self", relation:R })` is classified by the `(type, relation)` pair
|
|
61
|
+
// against the dispatching domain's OWN schema:
|
|
62
|
+
// - `(type, R)` is a LOCAL (non-`.asGlobal()`) authored relation of this domain → AUTO-QUALIFY the
|
|
63
|
+
// object type to `<domain>__<type>` (it matches the qualified gate object by construction).
|
|
64
|
+
// - `(type, R)` is a GLOBAL type's relation (governance/signer/read-visibility) → leave BARE.
|
|
65
|
+
// - `(type, R)` matches NOTHING in this domain's schema (unknown / per-record) → leave BARE (passthrough).
|
|
66
|
+
// A relation OBJECT TOKEN (`selfWorkspace()`/`selfObject(decl)`) is the OPTIONAL explicit sugar — it ALWAYS
|
|
67
|
+
// qualifies its type by the dispatching domain (the author's explicit intent), no classification needed.
|
|
68
|
+
|
|
69
|
+
interface DispatchContext {
|
|
70
|
+
/** The dispatching domain key — the qualifier (`module.domain === intent.domain`). */
|
|
71
|
+
readonly domain: string;
|
|
72
|
+
/** `<type>#<relation>` for every LOCAL non-global authored relation of this domain — the qualify set. */
|
|
73
|
+
readonly qualifiable: ReadonlySet<string>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const EMPTY_QUALIFIABLE: ReadonlySet<string> = new Set<string>();
|
|
77
|
+
let _ctx: DispatchContext | undefined;
|
|
78
|
+
|
|
79
|
+
/** Engine-entry hook: publish the dispatching domain + its qualifiable `(type,relation)` classification. */
|
|
80
|
+
export function __setDispatchContext(ctx: DispatchContext | undefined): void {
|
|
81
|
+
_ctx = ctx;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Engine-entry / probe hook: set only the dispatching domain (no schema classification). Used where the
|
|
86
|
+
* classification is unavailable (the proof generator's static plan probe): a token still resolves, and a
|
|
87
|
+
* bare string stays bare (the probe reads the relation/subject off the ops, never the object string).
|
|
88
|
+
*/
|
|
89
|
+
export function __setDispatchDomain(domain: string | undefined): void {
|
|
90
|
+
_ctx = domain === undefined ? undefined : { domain, qualifiable: EMPTY_QUALIFIABLE };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Resolve a `writeTuple`/`removeTuple` object for a given relation (token → qualify; string → classify). */
|
|
94
|
+
function resolveTupleObject(object: string | RelationObjectToken, relation: string): string {
|
|
95
|
+
// A TOKEN is explicit sugar — always qualify its type by the dispatching domain.
|
|
96
|
+
if (typeof object !== "string") {
|
|
97
|
+
if (!isRelationObjectToken(object)) {
|
|
98
|
+
throw new Error(`writeTuple: object must be a string or a relation object token; got ${JSON.stringify(object)}`);
|
|
99
|
+
}
|
|
100
|
+
if (_ctx === undefined || _ctx.domain === "") {
|
|
101
|
+
throw new Error(
|
|
102
|
+
"writeTuple: a relation object token was used outside a plan dispatch (no dispatching domain to " +
|
|
103
|
+
"qualify by). Tokens (selfWorkspace()/selfObject()) resolve only inside a directive plan.",
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
return `${qualifyRelationType(_ctx.domain, object.type)}:${object.id}`;
|
|
107
|
+
}
|
|
108
|
+
// A PLAIN STRING is classified by `(type, relation)` against the dispatching domain's schema.
|
|
109
|
+
const colon = object.indexOf(":");
|
|
110
|
+
if (colon < 0 || _ctx === undefined) return object; // malformed / no context ⇒ verbatim
|
|
111
|
+
const type = object.slice(0, colon);
|
|
112
|
+
if (_ctx.qualifiable.has(`${type}#${relation}`)) {
|
|
113
|
+
return `${qualifyRelationType(_ctx.domain, type)}${object.slice(colon)}`;
|
|
114
|
+
}
|
|
115
|
+
return object; // global / unknown / per-record ⇒ BARE
|
|
116
|
+
}
|
|
50
117
|
|
|
51
118
|
/** Tuple subject lifecycle — the same vocabulary the kernel read drops a non-active leaf by. */
|
|
52
119
|
export const RELATION_STATUSES = ["active", "revoked"] as const;
|
|
@@ -139,7 +206,13 @@ function compact(obj: Record<string, unknown>): RelationSubjectLeaf {
|
|
|
139
206
|
}
|
|
140
207
|
|
|
141
208
|
interface WriteTupleInput {
|
|
142
|
-
|
|
209
|
+
/**
|
|
210
|
+
* The tuple OBJECT `<type>:<id>`. A plain STRING passes through verbatim (the bare lane — a global
|
|
211
|
+
* `workspace:self`, a read-visibility grant, an already-qualified literal). A relation OBJECT TOKEN
|
|
212
|
+
* (`selfWorkspace()`/`selfObject(decl)`/`objectOf(decl,id)`) qualifies its type by the dispatching
|
|
213
|
+
* domain at plan runtime — so a seed for a NAMESPACED tenant type matches its qualified gate object.
|
|
214
|
+
*/
|
|
215
|
+
object: string | RelationObjectToken;
|
|
143
216
|
relation: string;
|
|
144
217
|
/**
|
|
145
218
|
* THE AUTHORITY SINK (the endorse model — security_model.md): who is granted the relation. A plain
|
|
@@ -166,7 +239,11 @@ function lowerTuple(
|
|
|
166
239
|
revokedAt?: string | undefined;
|
|
167
240
|
},
|
|
168
241
|
): PlannedOp[] {
|
|
169
|
-
|
|
242
|
+
// Resolve the object ONCE (token → qualified string, or a plain string verbatim) and use it for BOTH
|
|
243
|
+
// the deterministic aggregate id AND the stored `object` field, so the tuple's reltuple id matches the
|
|
244
|
+
// qualified gate object's (`identity/src/relations.rs::reltuple_id`).
|
|
245
|
+
const object = resolveTupleObject(p.object, p.relation);
|
|
246
|
+
const rt = instance(RelationTuple, relationTupleInstanceId(object, p.relation));
|
|
170
247
|
// MATERIALISE PrincipalEvidence subjects/grantedBy → the raw principal (the endorse model). The
|
|
171
248
|
// subject's endorsement is recorded on the setEntry op so the discovery harvest lifts it into
|
|
172
249
|
// nomosFlowGate.endorse; grantedBy is free attribution (materialised, not gated).
|
|
@@ -192,7 +269,7 @@ function lowerTuple(
|
|
|
192
269
|
return [
|
|
193
270
|
// upsert the aggregate head (first relate creates it); without this the referential
|
|
194
271
|
// gate would reject a setEntry on an absent id.
|
|
195
|
-
withMarker(set(rt, "object",
|
|
272
|
+
withMarker(set(rt, "object", object), "ensures"),
|
|
196
273
|
set(rt, "relation", p.relation),
|
|
197
274
|
subjectE ? { ...subjectEntry, endorsement: { facet: subjectE.facet, reason: subjectE.reason } } : subjectEntry,
|
|
198
275
|
];
|
|
@@ -63,10 +63,16 @@ import {
|
|
|
63
63
|
* (`allow: [key()]`, never a userset/wildcard: a `key:*` would let any key claim to author
|
|
64
64
|
* as anyone). A workspace that admits signed authorship composes this into its law.
|
|
65
65
|
*/
|
|
66
|
-
export const identityRelations = relationType("identity")
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
export const identityRelations = relationType("identity")
|
|
67
|
+
// GLOBAL (un-namespaced) — relation-namespacing opt-out. The kernel hardcodes bare
|
|
68
|
+
// `identity:<principal>` (`executor::signer_object`) for captured-principal signer verification, so
|
|
69
|
+
// this type MUST stay bare (qualifying it would need a Rust change). `signerObject()` writes the bare
|
|
70
|
+
// string object; no token, no migration.
|
|
71
|
+
.asGlobal()
|
|
72
|
+
.relation("signer", {
|
|
73
|
+
rewrite: self(),
|
|
74
|
+
allow: [key()],
|
|
75
|
+
});
|
|
70
76
|
|
|
71
77
|
/** The Zanzibar OBJECT for a principal's signer relation — `identity:<principal>` (the gate mirrors
|
|
72
78
|
* this as `executor::signer_object`; the subject is `key:<sha256hex(pubkey)>`). */
|
|
@@ -156,6 +156,13 @@ export const workspaceSelfWriteOnce = workspaceInvariant("WorkspaceSelfWriteOnce
|
|
|
156
156
|
// born platform with no name plumbing. (A pure tenant runs no governance ⇒ no schema ⇒ the gate is
|
|
157
157
|
// inert there; an un-backfilled workspace with no admin tuple refuses fail-closed.)
|
|
158
158
|
export const workspaceRelations = relationType("workspace")
|
|
159
|
+
// GLOBAL (un-namespaced) — relation-namespacing opt-out. The kernel/host address this governance type
|
|
160
|
+
// by its BARE name (`workspace:self#admin`, the operated-probe `expand(workspace:root, admin)`), so it
|
|
161
|
+
// MUST stay bare (qualifying it would need a Rust change). grantAdmin/grantCreation write bare
|
|
162
|
+
// `workspace:self` string tuples + gate on the bare object — all consistent, byte-identical, no
|
|
163
|
+
// migration. (A TENANT that wants a workspace-scoped role uses a namespaced `relationType("workspace")`
|
|
164
|
+
// + a `selfWorkspace()` seed; only THIS framework governance type is global.)
|
|
165
|
+
.asGlobal()
|
|
159
166
|
.relation("admin", { rewrite: self(), allow: [user(), key()] })
|
|
160
167
|
.relation("creator", { rewrite: self(), allow: [user(), key()] })
|
|
161
168
|
.relation("platformCreator", { rewrite: self(), allow: [user(), key()] });
|
package/src/index.ts
CHANGED
|
@@ -413,6 +413,16 @@ export {
|
|
|
413
413
|
anyUser,
|
|
414
414
|
group,
|
|
415
415
|
DEFAULT_RELATION_CAPS,
|
|
416
|
+
// RELATION-TYPE NAMESPACING — the qualifier helpers + the seed-object tokens (selfWorkspace()/
|
|
417
|
+
// selfObject()/objectOf()) that make a namespaced tenant seed match its qualified gate object.
|
|
418
|
+
RELATION_TYPE_DELIMITER,
|
|
419
|
+
qualifyRelationType,
|
|
420
|
+
qualifiedTypeId,
|
|
421
|
+
isRelationObjectToken,
|
|
422
|
+
selfWorkspace,
|
|
423
|
+
selfObject,
|
|
424
|
+
objectOf,
|
|
425
|
+
type RelationObjectToken,
|
|
416
426
|
type RelationTypeDecl,
|
|
417
427
|
type RelationTypeBuilder,
|
|
418
428
|
type RelationDefAst,
|
package/src/relation_schema.ts
CHANGED
|
@@ -91,6 +91,19 @@ export interface RelationTypeDecl {
|
|
|
91
91
|
readonly relations: ReadonlyMap<string, RelationDefAst>;
|
|
92
92
|
/** Per-type cap override; absent ⇒ the schema/default caps apply. */
|
|
93
93
|
readonly caps?: RelationCaps;
|
|
94
|
+
/**
|
|
95
|
+
* GLOBAL (un-namespaced) type — the RELATION-NAMESPACING opt-out. A tenant relation type is
|
|
96
|
+
* QUALIFIED by its owning domain key at compile (`estate` → `estate__estate`) so two domains that
|
|
97
|
+
* each declare the same bare type never collide (kernel last-wins schema merge). A `global` type is
|
|
98
|
+
* EXEMPT: its id, gate object, and tuple object all stay BARE. Reserved for the KERNEL-COUPLED
|
|
99
|
+
* framework types the kernel hardcodes by bare name and which we cannot qualify without a Rust
|
|
100
|
+
* change: the governance `workspace` (`workspace:self#admin`, the operated-probe expand), the signer
|
|
101
|
+
* `identity` (`executor::signer_object` = bare `identity:<principal>`), and the synthesized
|
|
102
|
+
* read-visibility `workspace` (the read gate hardcodes `ObjectRef::new("workspace","self")`). A
|
|
103
|
+
* global-only package emits byte-identically to the pre-namespacing compiler (no era key). Default
|
|
104
|
+
* (absent ⇒ namespaced) is the safe posture — a tenant NEVER wants a bare, collision-prone id.
|
|
105
|
+
*/
|
|
106
|
+
readonly global?: boolean;
|
|
94
107
|
/** Brand so a plain object is never mistaken for a declaration (mirrors workspaceInvariant). */
|
|
95
108
|
readonly __isRelationType: true;
|
|
96
109
|
}
|
|
@@ -99,6 +112,72 @@ export function isRelationTypeDecl(v: unknown): v is RelationTypeDecl {
|
|
|
99
112
|
return typeof v === "object" && v !== null && (v as { __isRelationType?: unknown }).__isRelationType === true;
|
|
100
113
|
}
|
|
101
114
|
|
|
115
|
+
// ─────────────────────────────── relation-type NAMESPACING ───────────────────────────────
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The relation-type QUALIFIER delimiter. COLON-FREE by law: the kernel splits an object address on
|
|
119
|
+
* the FIRST colon (`identity/src/relations.rs::ObjectRef::parse` → `split_once(':')`), so a `:`-bearing
|
|
120
|
+
* qualifier (`estate::workspace`) would corrupt the (type,id) split. `__` is the ratified delimiter.
|
|
121
|
+
*/
|
|
122
|
+
export const RELATION_TYPE_DELIMITER = "__";
|
|
123
|
+
|
|
124
|
+
/** Qualify a bare relation type by its owning domain key: `("estate","workspace") → "estate__workspace"`. */
|
|
125
|
+
export function qualifyRelationType(domainKey: string, type: string): string {
|
|
126
|
+
return `${domainKey}${RELATION_TYPE_DELIMITER}${type}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The qualified id a decl serializes under, given its owning domain key. A `global` decl (a kernel-
|
|
131
|
+
* coupled framework type — see {@link RelationTypeDecl.global}) is NEVER qualified; everything else is.
|
|
132
|
+
*/
|
|
133
|
+
export function qualifiedTypeId(domainKey: string, decl: RelationTypeDecl): string {
|
|
134
|
+
return decl.global === true ? decl.type : qualifyRelationType(domainKey, decl.type);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ─────────────────────────────── relation OBJECT tokens (the namespacing-aware seed lane) ───────────────────────────────
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* A relation OBJECT token — the namespacing-aware replacement for a hardcoded `"workspace:self"` STRING
|
|
141
|
+
* in a plan's `writeTuple({ object })`. A plain string passes through the tuple lowering VERBATIM (the
|
|
142
|
+
* BARE lane — governance's global `workspace:self`, a read-visibility grant, an already-qualified
|
|
143
|
+
* literal). A TOKEN carries only the (type, id); the framework qualifies its `type` by the DISPATCHING
|
|
144
|
+
* domain at plan runtime (`framework/relations.ts::writeTuple`), producing the SAME `<domain>__<type>`
|
|
145
|
+
* the compiler wrote onto the schema + gate object. Authors of a seed for a NAMESPACED tenant type swap
|
|
146
|
+
* `object: "workspace:self"` → `object: selfWorkspace()` — the qualifier flows through, they name no
|
|
147
|
+
* domain. (A namespaced type's tuple object MUST match its qualified gate object or the gate finds no
|
|
148
|
+
* tuple; the token is what guarantees it from one place.)
|
|
149
|
+
*/
|
|
150
|
+
export interface RelationObjectToken {
|
|
151
|
+
readonly __isRelationObjectToken: true;
|
|
152
|
+
/** The BARE object type (`workspace`, or a per-record aggregate type) — qualified at plan runtime. */
|
|
153
|
+
readonly type: string;
|
|
154
|
+
/** The object id (`self` for a workspace-scope object, or a record id). */
|
|
155
|
+
readonly id: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function isRelationObjectToken(v: unknown): v is RelationObjectToken {
|
|
159
|
+
return (
|
|
160
|
+
typeof v === "object" &&
|
|
161
|
+
v !== null &&
|
|
162
|
+
(v as { __isRelationObjectToken?: unknown }).__isRelationObjectToken === true
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** The workspace-scope object token `<domain>__workspace:self` — the default gate-object shape, qualified. */
|
|
167
|
+
export function selfWorkspace(): RelationObjectToken {
|
|
168
|
+
return { __isRelationObjectToken: true, type: "workspace", id: "self" };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** The workspace-scope object token for a specific relation type: `selfObject(EstateRelations)`. */
|
|
172
|
+
export function selfObject(decl: RelationTypeDecl): RelationObjectToken {
|
|
173
|
+
return { __isRelationObjectToken: true, type: decl.type, id: "self" };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** A PER-RECORD object token `<domain>__<type>:<id>` (the objectFrom gate's tuple side). */
|
|
177
|
+
export function objectOf(decl: RelationTypeDecl, id: string): RelationObjectToken {
|
|
178
|
+
return { __isRelationObjectToken: true, type: decl.type, id };
|
|
179
|
+
}
|
|
180
|
+
|
|
102
181
|
// ─────────────────────────────── the rewrite combinators ───────────────────────────────
|
|
103
182
|
|
|
104
183
|
/** `_this` — the directly-stored tuples (and their recursive userset subjects). */
|
|
@@ -174,6 +253,13 @@ export interface RelationTypeBuilder extends RelationTypeDecl {
|
|
|
174
253
|
* tuples — legal but inert; a purely-`computed`/`from` relation needs no `allow`.
|
|
175
254
|
*/
|
|
176
255
|
relation(name: string, opts?: { rewrite?: Rewrite; allow?: AllowedSubject[] }): RelationTypeBuilder;
|
|
256
|
+
/**
|
|
257
|
+
* Mark this type GLOBAL (un-namespaced) — the relation-namespacing opt-out (see
|
|
258
|
+
* {@link RelationTypeDecl.global}). RESERVED for kernel-coupled framework types the kernel hardcodes
|
|
259
|
+
* by bare name; a tenant type must NOT call this (it would re-introduce the cross-domain collision
|
|
260
|
+
* namespacing exists to dissolve).
|
|
261
|
+
*/
|
|
262
|
+
asGlobal(): RelationTypeBuilder;
|
|
177
263
|
}
|
|
178
264
|
|
|
179
265
|
/**
|
|
@@ -206,6 +292,11 @@ export function relationType(type: string, caps?: Partial<RelationCaps>): Relati
|
|
|
206
292
|
relations.set(name, { rewrite: opts?.rewrite ?? null, allowed });
|
|
207
293
|
return builder;
|
|
208
294
|
},
|
|
295
|
+
asGlobal(): RelationTypeBuilder {
|
|
296
|
+
// Mutate in place so a later `.relation(...)` chain still returns the SAME (now-global) builder.
|
|
297
|
+
(builder as { global?: boolean }).global = true;
|
|
298
|
+
return builder;
|
|
299
|
+
},
|
|
209
300
|
};
|
|
210
301
|
return builder;
|
|
211
302
|
}
|
package/src/usd.ts
CHANGED
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
import type { WireSchema, WireRefMode } from "./wire.js";
|
|
43
43
|
import type { DomainModule } from "./codegen_dart.js";
|
|
44
44
|
import { domainInterface } from "./shape_compat.js";
|
|
45
|
-
import { relationType, user, key, anyUser } from "./relation_schema.js";
|
|
45
|
+
import { relationType, user, key, anyUser, qualifiedTypeId, qualifyRelationType } from "./relation_schema.js";
|
|
46
46
|
import { harvestPrincipalFlows } from "./wire_encode.js";
|
|
47
47
|
|
|
48
48
|
/** The referential marker of a directive prim (kernel `RefMode` wire string). */
|
|
@@ -260,6 +260,21 @@ export interface UsdLayer {
|
|
|
260
260
|
readonly v: 1;
|
|
261
261
|
readonly requires: Record<string, { readonly relation: string; readonly object: string }>;
|
|
262
262
|
};
|
|
263
|
+
/**
|
|
264
|
+
* THE RELATION-TYPE NAMESPACE ERA KEY (relation-namespacing). Present IFF a layer qualified ≥1
|
|
265
|
+
* (non-global) `relationType` by its owning domain key (`estate` → `estate__workspace`), so two
|
|
266
|
+
* domains in one package that each declare the same bare type no longer collide at the kernel's
|
|
267
|
+
* last-wins schema merge. `types` is the SORTED set of qualified ids this layer produced; `delimiter`
|
|
268
|
+
* is the ratified colon-free `"__"` (the kernel splits object addresses on the first colon). OMITTED
|
|
269
|
+
* for a layer that declares no relation type OR only GLOBAL (kernel-coupled framework) types — those
|
|
270
|
+
* emit BYTE-IDENTICALLY to the pre-namespacing compiler (the era rule; the marker is a witness, the
|
|
271
|
+
* kernel needs no new arm because the qualification is already baked into the `nomosRelations` ids).
|
|
272
|
+
*/
|
|
273
|
+
readonly nomosRelationNamespace?: {
|
|
274
|
+
readonly v: 1;
|
|
275
|
+
readonly delimiter: string;
|
|
276
|
+
readonly types: readonly string[];
|
|
277
|
+
};
|
|
263
278
|
/**
|
|
264
279
|
* THE CROSS-CUSTODY ATTESTATION GATE (home_holons §3/§5). Present IFF ≥1 directive declares
|
|
265
280
|
* `.attestationAuthorized()`: the SORTED set of their ids. A directive named here is authorized by a
|
|
@@ -632,6 +647,7 @@ function readProjectionForModule(mod: DomainModule, manifest: CanonicalManifest)
|
|
|
632
647
|
*/
|
|
633
648
|
function serializeRelationSchema(
|
|
634
649
|
decls: readonly import("./relation_schema.js").RelationTypeDecl[],
|
|
650
|
+
domainKey: string,
|
|
635
651
|
): UsdLayer["nomosRelations"] {
|
|
636
652
|
if (decls.length === 0) return undefined;
|
|
637
653
|
type AllowedW = { kind: string; type?: string; ns?: string; relation?: string };
|
|
@@ -639,10 +655,15 @@ function serializeRelationSchema(
|
|
|
639
655
|
a.kind === "direct" ? `direct:${a.type}` : a.kind === "wildcard" ? "wildcard" : `userset:${a.type}#${a.relation}`;
|
|
640
656
|
const byType = new Map<string, { relations: Map<string, { rewrite: unknown; allowed: unknown[] }>; caps?: { maxDepth: number; maxFanout: number } }>();
|
|
641
657
|
for (const d of decls) {
|
|
642
|
-
|
|
658
|
+
// RELATION-TYPE NAMESPACING: a tenant type keys under `<domainKey>__<type>` so two domains that each
|
|
659
|
+
// declare the same bare type never collide at the kernel's last-wins schema merge; a `global` type
|
|
660
|
+
// (kernel-coupled framework type) keys BARE. `computed`/`ttu` targets inside a rewrite are
|
|
661
|
+
// type-RELATIVE relation names resolved on the (now-qualified) type — so no rewrite rewrite is needed.
|
|
662
|
+
const typeId = qualifiedTypeId(domainKey, d);
|
|
663
|
+
let entry = byType.get(typeId);
|
|
643
664
|
if (entry === undefined) {
|
|
644
665
|
entry = { relations: new Map(), ...(d.caps ? { caps: d.caps } : {}) };
|
|
645
|
-
byType.set(
|
|
666
|
+
byType.set(typeId, entry);
|
|
646
667
|
} else if (d.caps !== undefined) {
|
|
647
668
|
if (entry.caps !== undefined && (entry.caps.maxDepth !== d.caps.maxDepth || entry.caps.maxFanout !== d.caps.maxFanout)) {
|
|
648
669
|
throw new Error(`relationType("${d.type}"): conflicting caps across declarations`);
|
|
@@ -682,7 +703,10 @@ function synthesizeReadRelationTypes(
|
|
|
682
703
|
if (a.requiresRead !== undefined) caps.add(a.requiresRead);
|
|
683
704
|
}
|
|
684
705
|
if (caps.size === 0) return [];
|
|
685
|
-
|
|
706
|
+
// GLOBAL (un-namespaced): the read gate hardcodes `ObjectRef::new("workspace","self")` in Rust
|
|
707
|
+
// (`wasm_git_holon.rs`), so the synthesized read-visibility `workspace` type MUST stay bare — it is
|
|
708
|
+
// NOT namespaced by the owning domain (site-6 exemption; a Rust change would be needed to qualify it).
|
|
709
|
+
const builder = relationType("workspace").asGlobal();
|
|
686
710
|
// Sorted for byte-determinism (the serializer also sorts, but keep the source stable). PURE
|
|
687
711
|
// DIRECT (`rewrite` omitted ⇒ `null`, not `self()`): a flat membership relation — the read-cap is
|
|
688
712
|
// exactly "who was granted it". (`self()`/`Rewrite::This` inside a rewrite expands to an EMPTY
|
|
@@ -724,29 +748,87 @@ export function emitUsd(
|
|
|
724
748
|
// into. The `nomosReadVisibility` key (type → cap) is RETAINED as the type-to-relation map; the
|
|
725
749
|
// synthesized schema is what makes that cap a checkable relation. SORTED + omit-when-empty.
|
|
726
750
|
const readRelationTypes = synthesizeReadRelationTypes(manifest);
|
|
751
|
+
// THE OWNING DOMAIN KEY — the relation-namespacing qualifier (the SAME key the openapi routes
|
|
752
|
+
// dispatch by, `module.domain ?? module.name`, so `intent.domain` at plan runtime === the key the
|
|
753
|
+
// schema/gate qualified under; the tuple object a token produces then lines up by construction).
|
|
754
|
+
const domainKey = l.module.domain ?? l.module.name;
|
|
755
|
+
// The NON-GLOBAL (namespaced) relation type names this layer declares — the set whose gate objects
|
|
756
|
+
// must be qualified. A `global` type (governance `workspace`, signer `identity`, read-visibility
|
|
757
|
+
// `workspace`) stays bare on BOTH the schema and the gate side, so it is excluded here.
|
|
758
|
+
const namespacedTypes = new Set(
|
|
759
|
+
(l.module.relationTypes ?? []).filter((d) => d.global !== true).map((d) => d.type),
|
|
760
|
+
);
|
|
761
|
+
// THE AMBIGUITY GUARD (relation-namespacing): a bare `writeTuple({object:"workspace:self", relation:R})`
|
|
762
|
+
// is auto-qualified iff `(type,R)` is a LOCAL non-global authored relation, and left BARE iff it is a
|
|
763
|
+
// GLOBAL / read-visibility cap. If ONE `(type,R)` is BOTH — a non-global authored relation AND a
|
|
764
|
+
// synthesized read-visibility cap (always on type `workspace`) — the bare object is genuinely
|
|
765
|
+
// ambiguous. Refuse at compile with a clear message (the maintainer's rule: never silently pick).
|
|
766
|
+
{
|
|
767
|
+
const readCaps = new Set(readRelationTypes.flatMap((rt) => [...rt.relations.keys()]));
|
|
768
|
+
for (const d of l.module.relationTypes ?? []) {
|
|
769
|
+
if (d.global === true || d.type !== "workspace") continue;
|
|
770
|
+
for (const rel of d.relations.keys()) {
|
|
771
|
+
if (readCaps.has(rel)) {
|
|
772
|
+
throw new Error(
|
|
773
|
+
`relation-namespacing conflict in domain "${domainKey}": relation "workspace#${rel}" is ` +
|
|
774
|
+
`declared BOTH as a local authored relationType("workspace") relation AND as a ` +
|
|
775
|
+
`read-visibility cap (a requires("${rel}") aggregate). A bare writeTuple object for ` +
|
|
776
|
+
`"${rel}" would be ambiguous (qualify vs stay bare). Rename one of them.`,
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
// THE RELATION-NAMESPACE ERA KEY (`nomosRelationNamespace`): present IFF this layer qualified ≥1
|
|
783
|
+
// (non-global) relation type — the marker that this bundle's relation ids are domain-qualified.
|
|
784
|
+
// Absent when a layer declares no relation type OR only global ones ⇒ byte-identical to the
|
|
785
|
+
// pre-namespacing compiler (the `nomosRelations`/`nomosInvariantGate` era discipline).
|
|
786
|
+
const qualifiedTypeIds = [...namespacedTypes].map((t) => qualifyRelationType(domainKey, t)).sort();
|
|
787
|
+
const nomosRelationNamespace =
|
|
788
|
+
qualifiedTypeIds.length > 0 ? { v: 1 as const, delimiter: "__", types: qualifiedTypeIds } : undefined;
|
|
789
|
+
// Qualify the TYPE half of a gate object by the owning domain — but ONLY when this layer declares
|
|
790
|
+
// that type as a NON-GLOBAL relation type (so the gate object matches its qualified schema id). A
|
|
791
|
+
// global-type object (`workspace:self` for governance/read-visibility) or a type this layer doesn't
|
|
792
|
+
// declare passes through BARE — byte-identical to the pre-namespacing compiler.
|
|
793
|
+
const qualifyGateObject = (obj: string): string => {
|
|
794
|
+
const colon = obj.indexOf(":");
|
|
795
|
+
if (colon < 0) return obj;
|
|
796
|
+
const ty = obj.slice(0, colon);
|
|
797
|
+
return namespacedTypes.has(ty) ? `${qualifyRelationType(domainKey, ty)}${obj.slice(colon)}` : obj;
|
|
798
|
+
};
|
|
727
799
|
// THE RELATION SCHEMA (P1): per-type rewrites lowered to the nomosRelations AST, UNIONED with the
|
|
728
|
-
// synthesized read relations.
|
|
729
|
-
//
|
|
730
|
-
// relation is an author error (caught in the merge).
|
|
731
|
-
const nomosRelations = serializeRelationSchema(
|
|
732
|
-
...(l.module.relationTypes ?? []),
|
|
733
|
-
|
|
734
|
-
|
|
800
|
+
// synthesized read relations. Keys are QUALIFIED by the owning domain (namespacing) except global
|
|
801
|
+
// types. SORTED keys (byte-determinism); omit-when-empty (era rule). A read-cap name colliding with
|
|
802
|
+
// an authored `workspace` relation is an author error (caught in the merge).
|
|
803
|
+
const nomosRelations = serializeRelationSchema(
|
|
804
|
+
[...(l.module.relationTypes ?? []), ...readRelationTypes],
|
|
805
|
+
domainKey,
|
|
806
|
+
);
|
|
735
807
|
// THE RELATION GATE (P4): directives declaring `.requiresRelation` → directiveId → {relation,
|
|
736
|
-
// object}.
|
|
808
|
+
// object}. The object's TYPE half is qualified to match the (now-namespaced) schema id. SORTED
|
|
809
|
+
// keys; omit-when-empty (era rule — a relation-gate-free layer is byte-identical).
|
|
737
810
|
const relationGateReq: Record<
|
|
738
811
|
string,
|
|
739
812
|
{ relation: string; object: string; objectFrom?: { recordType: string; idField: string } }
|
|
740
813
|
> = {};
|
|
741
814
|
for (const d of [...l.module.directives].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))) {
|
|
742
815
|
if (d.requiresRelationGate !== undefined) {
|
|
816
|
+
const objectFrom = d.requiresRelationGate.objectFrom;
|
|
743
817
|
relationGateReq[d.id] = {
|
|
744
818
|
relation: d.requiresRelationGate.relation,
|
|
745
|
-
object: d.requiresRelationGate.object,
|
|
819
|
+
object: qualifyGateObject(d.requiresRelationGate.object),
|
|
746
820
|
// PER-RECORD object (home_holons §6): carried through to the wasm gate; omitted when absent
|
|
747
|
-
// so a self-relative directive's USD is byte-identical (the era rule).
|
|
748
|
-
|
|
749
|
-
|
|
821
|
+
// so a self-relative directive's USD is byte-identical (the era rule). The recordType is the
|
|
822
|
+
// object type of the per-record tuple, so it namespaces exactly like the gate object type.
|
|
823
|
+
...(objectFrom !== undefined
|
|
824
|
+
? {
|
|
825
|
+
objectFrom: {
|
|
826
|
+
recordType: namespacedTypes.has(objectFrom.recordType)
|
|
827
|
+
? qualifyRelationType(domainKey, objectFrom.recordType)
|
|
828
|
+
: objectFrom.recordType,
|
|
829
|
+
idField: objectFrom.idField,
|
|
830
|
+
},
|
|
831
|
+
}
|
|
750
832
|
: {}),
|
|
751
833
|
};
|
|
752
834
|
}
|
|
@@ -951,6 +1033,7 @@ export function emitUsd(
|
|
|
951
1033
|
}
|
|
952
1034
|
: {}),
|
|
953
1035
|
...(nomosRelations !== undefined ? { nomosRelations } : {}),
|
|
1036
|
+
...(nomosRelationNamespace !== undefined ? { nomosRelationNamespace } : {}),
|
|
954
1037
|
...(nomosRelationGate !== undefined ? { nomosRelationGate } : {}),
|
|
955
1038
|
...(nomosAttestationGate !== undefined ? { nomosAttestationGate } : {}),
|
|
956
1039
|
...(nomosBirthGate !== undefined ? { nomosBirthGate } : {}),
|
package/src/usd_layers.ts
CHANGED
|
@@ -171,6 +171,9 @@ function domainLayerAttrLines(domain: string, layer: UsdLayer, indent: string):
|
|
|
171
171
|
...(layer.nomosRelations !== undefined
|
|
172
172
|
? [`${indent}custom string nomos:relationsHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosRelations)))}`]
|
|
173
173
|
: []),
|
|
174
|
+
...(layer.nomosRelationNamespace !== undefined
|
|
175
|
+
? [`${indent}custom string nomos:relationNamespaceHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosRelationNamespace)))}`]
|
|
176
|
+
: []),
|
|
174
177
|
...(layer.nomosRelationGate !== undefined
|
|
175
178
|
? [`${indent}custom string nomos:relationGateHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosRelationGate)))}`]
|
|
176
179
|
: []),
|
|
@@ -736,7 +739,23 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
|
|
|
736
739
|
const t = new Map<string, NonNullable<UsdLayer["nomosRelations"]>["types"][string]>();
|
|
737
740
|
for (const l of stack) {
|
|
738
741
|
if (l.nomosRelations === undefined) continue;
|
|
739
|
-
for (const [type, spec] of Object.entries(l.nomosRelations.types))
|
|
742
|
+
for (const [type, spec] of Object.entries(l.nomosRelations.types)) {
|
|
743
|
+
// RELATION-NAMESPACING SAFETY: after qualification, two domains' same bare type key under
|
|
744
|
+
// DISTINCT ids (`estate__workspace` vs `home__workspace`) and never reach here as the same key.
|
|
745
|
+
// A collision on the SAME canonical id can now only be a genuinely SHARED id (two layers
|
|
746
|
+
// contributing the same global type, or a future cross-domain shared type). Silent last-wins
|
|
747
|
+
// would DROP one layer's relations — so a collision on a key whose spec DIFFERS is a HARD ERROR
|
|
748
|
+
// (loud, not last-wins). An identical re-contribution (framework dedup) is fine.
|
|
749
|
+
const prior = t.get(type);
|
|
750
|
+
if (prior !== undefined && JSON.stringify(prior) !== JSON.stringify(spec)) {
|
|
751
|
+
throw new Error(
|
|
752
|
+
`nomosRelations: relation type "${type}" is contributed with DIFFERING definitions by two ` +
|
|
753
|
+
`layers — the qualified ids collide. A shared relation type must be identical across ` +
|
|
754
|
+
`domains (or namespaced per-domain). Refusing to silently last-wins.`,
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
t.set(type, spec);
|
|
758
|
+
}
|
|
740
759
|
}
|
|
741
760
|
if (t.size > 0) {
|
|
742
761
|
const typesRec: NonNullable<UsdLayer["nomosRelations"]>["types"] = {};
|
|
@@ -744,6 +763,18 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
|
|
|
744
763
|
relations = { v: 1, types: typesRec };
|
|
745
764
|
}
|
|
746
765
|
}
|
|
766
|
+
// THE RELATION-NAMESPACE ERA KEY (relation-namespacing): the SORTED union of every layer's qualified
|
|
767
|
+
// type ids. Present iff ANY layer namespaced ≥1 type; omit-when-empty (byte-identical for global-only
|
|
768
|
+
// / relation-free packages). `delimiter` is the ratified colon-free `"__"`.
|
|
769
|
+
let relationNamespace: UsdLayer["nomosRelationNamespace"];
|
|
770
|
+
{
|
|
771
|
+
const ns = new Set<string>();
|
|
772
|
+
for (const l of stack) {
|
|
773
|
+
if (l.nomosRelationNamespace === undefined) continue;
|
|
774
|
+
for (const id of l.nomosRelationNamespace.types) ns.add(id);
|
|
775
|
+
}
|
|
776
|
+
if (ns.size > 0) relationNamespace = { v: 1, delimiter: "__", types: [...ns].sort() };
|
|
777
|
+
}
|
|
747
778
|
// THE RELATION GATE (P4): directiveId → {relation, object} merged later-wins, SORTED. Omit-when-empty.
|
|
748
779
|
let relationGate: UsdLayer["nomosRelationGate"];
|
|
749
780
|
{
|
|
@@ -825,6 +856,7 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
|
|
|
825
856
|
...(readVisibility !== undefined ? { nomosReadVisibility: readVisibility } : {}),
|
|
826
857
|
...(encryption !== undefined ? { nomosEncryption: encryption } : {}),
|
|
827
858
|
...(relations !== undefined ? { nomosRelations: relations } : {}),
|
|
859
|
+
...(relationNamespace !== undefined ? { nomosRelationNamespace: relationNamespace } : {}),
|
|
828
860
|
...(relationGate !== undefined ? { nomosRelationGate: relationGate } : {}),
|
|
829
861
|
...(attestationGate !== undefined ? { nomosAttestationGate: attestationGate } : {}),
|
|
830
862
|
...(birthGate !== undefined ? { nomosBirthGate: birthGate } : {}),
|
|
@@ -956,6 +988,7 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
956
988
|
const encryptionByDomain = new Map<string, UsdLayer["nomosEncryption"]>();
|
|
957
989
|
const relationsByDomain = new Map<string, UsdLayer["nomosRelations"]>();
|
|
958
990
|
const relationGateByDomain = new Map<string, UsdLayer["nomosRelationGate"]>();
|
|
991
|
+
const relationNamespaceByDomain = new Map<string, UsdLayer["nomosRelationNamespace"]>();
|
|
959
992
|
const attestationGateByDomain = new Map<string, UsdLayer["nomosAttestationGate"]>();
|
|
960
993
|
const birthGateByDomain = new Map<string, UsdLayer["nomosBirthGate"]>();
|
|
961
994
|
const parentAttestedBirthByDomain = new Map<string, UsdLayer["nomosParentAttestedBirth"]>();
|
|
@@ -1006,6 +1039,11 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
1006
1039
|
if (typeof relGateHex === "string") {
|
|
1007
1040
|
relationGateByDomain.set(domain, JSON.parse(unhexUtf8(relGateHex)) as UsdLayer["nomosRelationGate"]);
|
|
1008
1041
|
}
|
|
1042
|
+
// THE RELATION-NAMESPACE ERA KEY (relation-namespacing): the hex-carried qualified-type witness.
|
|
1043
|
+
const relNsHex = p.attrs.get("nomos:relationNamespaceHex");
|
|
1044
|
+
if (typeof relNsHex === "string") {
|
|
1045
|
+
relationNamespaceByDomain.set(domain, JSON.parse(unhexUtf8(relNsHex)) as UsdLayer["nomosRelationNamespace"]);
|
|
1046
|
+
}
|
|
1009
1047
|
// THE ATTESTATION GATE (home_holons §3/§5): the hex-carried attestation-authorized directive set.
|
|
1010
1048
|
const attGateHex = p.attrs.get("nomos:attestationGateHex");
|
|
1011
1049
|
if (typeof attGateHex === "string") {
|
|
@@ -1122,6 +1160,7 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
1122
1160
|
const enc = encryptionByDomain.get(domain);
|
|
1123
1161
|
const rel = relationsByDomain.get(domain);
|
|
1124
1162
|
const relGate = relationGateByDomain.get(domain);
|
|
1163
|
+
const relNs = relationNamespaceByDomain.get(domain);
|
|
1125
1164
|
const attGate = attestationGateByDomain.get(domain);
|
|
1126
1165
|
const birthGate = birthGateByDomain.get(domain);
|
|
1127
1166
|
const parentAttestedBirth = parentAttestedBirthByDomain.get(domain);
|
|
@@ -1140,6 +1179,7 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
1140
1179
|
...(rv !== undefined ? { nomosReadVisibility: rv } : {}),
|
|
1141
1180
|
...(enc !== undefined ? { nomosEncryption: enc } : {}),
|
|
1142
1181
|
...(rel !== undefined ? { nomosRelations: rel } : {}),
|
|
1182
|
+
...(relNs !== undefined ? { nomosRelationNamespace: relNs } : {}),
|
|
1143
1183
|
...(relGate !== undefined ? { nomosRelationGate: relGate } : {}),
|
|
1144
1184
|
...(attGate !== undefined ? { nomosAttestationGate: attGate } : {}),
|
|
1145
1185
|
...(birthGate !== undefined ? { nomosBirthGate: birthGate } : {}),
|