@githolon/dsl 0.7.1 → 0.9.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/aggregate.ts +19 -16
- package/src/build_package.ts +7 -0
- package/src/codegen_dart.ts +25 -0
- package/src/codegen_ts.ts +18 -10
- package/src/compile_package_main.ts +10 -0
- package/src/directive.ts +102 -16
- package/src/engine_entry.ts +8 -2
- package/src/fields.ts +24 -2
- package/src/framework/encryption_identity.ts +3 -3
- package/src/framework/genesis.ts +88 -0
- package/src/framework/home.ts +149 -0
- package/src/framework/impure_capability.ts +3 -3
- package/src/framework/relations.ts +28 -4
- package/src/framework/signing_identity.ts +97 -0
- package/src/framework/workspace_invariant.ts +5 -3
- package/src/framework/workspaces.ts +158 -103
- package/src/index.ts +88 -19
- package/src/manifest.ts +0 -6
- package/src/ops.ts +50 -1
- package/src/relation_schema.ts +298 -0
- package/src/relation_sugar.ts +324 -0
- package/src/usd.ts +202 -7
- package/src/usd_layers.ts +81 -0
- package/src/wire.ts +18 -3
- package/src/wire_encode.ts +28 -2
- package/src/framework/rbac.ts +0 -418
package/src/usd.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { createHash } from "node:crypto";
|
|
|
30
30
|
import {
|
|
31
31
|
canonicalJson,
|
|
32
32
|
domainManifest,
|
|
33
|
+
type CanonicalManifest,
|
|
33
34
|
type CanonicalProjectionReturn,
|
|
34
35
|
} from "./manifest.js";
|
|
35
36
|
import {
|
|
@@ -39,6 +40,7 @@ import {
|
|
|
39
40
|
} from "./compose.js";
|
|
40
41
|
import type { WireSchema, WireRefMode } from "./wire.js";
|
|
41
42
|
import type { DomainModule } from "./codegen_dart.js";
|
|
43
|
+
import { relationType, user, key, anyUser } from "./relation_schema.js";
|
|
42
44
|
|
|
43
45
|
/** The referential marker of a directive prim (kernel `RefMode` wire string). */
|
|
44
46
|
export type RefMode = WireRefMode;
|
|
@@ -188,6 +190,62 @@ export interface UsdLayer {
|
|
|
188
190
|
/** Declared workspace invariants `{id, on}` the gate executes, sorted by id. */
|
|
189
191
|
readonly workspaceInvariants?: { id: string; on: string }[];
|
|
190
192
|
};
|
|
193
|
+
/**
|
|
194
|
+
* THE NATIVE ZANZIBAR RELATION SCHEMA (P1 — the userset-rewrite law as PURE DATA). Present IFF the
|
|
195
|
+
* layer declares ≥1 `relationType`: the law's EXPLICIT opt-in to the recursive Check/Expand gate.
|
|
196
|
+
* The Rust `RewriteEngine` READS this AST (per-type relation rewrites + fail-closed type restrictions
|
|
197
|
+
* + the schema-declared depth/fanout caps); the JS lump never runs it. OMITTED for relation-free law
|
|
198
|
+
* (byte-identical hash — the exact `nomosInvariantGate` era discipline; pre-feature chains replay
|
|
199
|
+
* green, the relation gate inert). Keys SORTED for byte-determinism (the layered flatten reproduces
|
|
200
|
+
* these bytes; see usd_layers.ts). Relation refs are NAMES in P1; the #58 stable-id swap is the
|
|
201
|
+
* localized follow-up.
|
|
202
|
+
*/
|
|
203
|
+
readonly nomosRelations?: {
|
|
204
|
+
readonly v: 1;
|
|
205
|
+
/** typeName → { relations: {relName → {rewrite, allowed}}, caps? } — keys sorted. */
|
|
206
|
+
readonly types: Record<
|
|
207
|
+
string,
|
|
208
|
+
{
|
|
209
|
+
readonly relations: Record<string, { readonly rewrite: unknown; readonly allowed: unknown[] }>;
|
|
210
|
+
readonly caps?: { readonly maxDepth: number; readonly maxFanout: number };
|
|
211
|
+
}
|
|
212
|
+
>;
|
|
213
|
+
};
|
|
214
|
+
/**
|
|
215
|
+
* THE NATIVE ZANZIBAR WRITE-AUTHZ GATE (P4). Present IFF ≥1 directive declares `.requiresRelation`:
|
|
216
|
+
* the one gate (`executor::admit`) refuses an author who does not HOLD the relation the directive
|
|
217
|
+
* requires (`engine.check` over the folded tuple store). `requires` maps directiveId → {relation,
|
|
218
|
+
* object}. OMITTED for relation-gate-free law (the era rule — byte-identical hash). Keys sorted.
|
|
219
|
+
*/
|
|
220
|
+
readonly nomosRelationGate?: {
|
|
221
|
+
readonly v: 1;
|
|
222
|
+
readonly requires: Record<string, { readonly relation: string; readonly object: string }>;
|
|
223
|
+
};
|
|
224
|
+
/**
|
|
225
|
+
* THE CROSS-CUSTODY ATTESTATION GATE (home_holons §3/§5). Present IFF ≥1 directive declares
|
|
226
|
+
* `.attestationAuthorized()`: the SORTED set of their ids. A directive named here is authorized by a
|
|
227
|
+
* carried §3 signed quote rather than an author block — the one gate verifies the quote (bound to the
|
|
228
|
+
* home's warrant owner) + pins the captured principal to the proven attestation key, and the signed-author
|
|
229
|
+
* doors exempt it. OMITTED for law with no such directive (the era rule — byte-identical hash).
|
|
230
|
+
*/
|
|
231
|
+
readonly nomosAttestationGate?: {
|
|
232
|
+
readonly v: 1;
|
|
233
|
+
readonly directives: Record<
|
|
234
|
+
string,
|
|
235
|
+
{
|
|
236
|
+
readonly ownerObject: string;
|
|
237
|
+
readonly ownerField: string;
|
|
238
|
+
readonly fields: {
|
|
239
|
+
readonly asserterWorkspace: string;
|
|
240
|
+
readonly object: string;
|
|
241
|
+
readonly relation: string;
|
|
242
|
+
readonly attestedAt: string;
|
|
243
|
+
readonly publicKey: string;
|
|
244
|
+
readonly signature: string;
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
>;
|
|
248
|
+
};
|
|
191
249
|
/**
|
|
192
250
|
* STABLE IDENTIFIERS (#58 — names are labels, identity is minted). The layer's
|
|
193
251
|
* domain/aggregate/field sids + label lineages, carried verbatim from the canonical
|
|
@@ -214,15 +272,17 @@ export interface UsdLayer {
|
|
|
214
272
|
readonly directives: Record<string, string[]>;
|
|
215
273
|
};
|
|
216
274
|
/**
|
|
217
|
-
* THE READ-VISIBILITY GATE DECLARATION (
|
|
275
|
+
* THE READ-VISIBILITY GATE DECLARATION (the native-relations read gate).
|
|
218
276
|
* Present IFF ≥1 aggregate of this layer is declared PRIVATE (`visibility:
|
|
219
277
|
* requires(cap)`): the law's EXPLICIT opt-in to the githolon gating its own
|
|
220
278
|
* reads. The one wasm read boundary serves a read touching a private type ONLY to
|
|
221
|
-
* a principal that
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
279
|
+
* a principal that holds the read RELATION `cap` on the workspace-scope object —
|
|
280
|
+
* decided by the native `RewriteEngine.check` over this workspace's own folded
|
|
281
|
+
* relation tuples (the compiler ALSO synthesizes a `workspace#<cap>` relation into
|
|
282
|
+
* `nomosRelations`). OMITTED for all-public law (byte-identical hash — the EXACT
|
|
283
|
+
* `nomosReadGate`/`nomosInvariantGate` era discipline; every pre-feature chain
|
|
284
|
+
* serves reads exactly as before, and a public type is never gated). Keyed by
|
|
285
|
+
* aggregate TYPE name → required capability (= read relation).
|
|
226
286
|
*/
|
|
227
287
|
readonly nomosReadVisibility?: {
|
|
228
288
|
readonly v: 1;
|
|
@@ -285,7 +345,10 @@ function encodeModuleToPrims(layerPath: string, mod: DomainModule): UsdPrim[] {
|
|
|
285
345
|
path: `${layerPath}/${d.id}`,
|
|
286
346
|
target: d.target,
|
|
287
347
|
marker: d.marker,
|
|
288
|
-
|
|
348
|
+
// The legacy RBAC capability set is GONE (full-native teardown) — native write-authz is the
|
|
349
|
+
// `nomosRelationGate` era key (a directive's `.requires(relation)`), read by the one gate, not
|
|
350
|
+
// this prim field. Kept empty so the #137 layer-composition shape is unchanged.
|
|
351
|
+
requires: [],
|
|
289
352
|
// Carry the declared law boundary straight from the canonical manifest, which
|
|
290
353
|
// already applies omit-when-empty: `reads`/`emits` are present here ONLY when the
|
|
291
354
|
// directive declared them, so a boundary-free prim is byte-identical to before.
|
|
@@ -300,6 +363,75 @@ function encodeModuleToPrims(layerPath: string, mod: DomainModule): UsdPrim[] {
|
|
|
300
363
|
return sortPrims(prims);
|
|
301
364
|
}
|
|
302
365
|
|
|
366
|
+
/**
|
|
367
|
+
* Lower a layer's `relationType` decls into the canonical `nomosRelations` wire object (P1) —
|
|
368
|
+
* keys SORTED for byte-determinism, omit-when-empty (the era rule). Multiple decls for one type
|
|
369
|
+
* MERGE their relations (a relation-name collision is an author error); a caps conflict on one
|
|
370
|
+
* type is an error. Rewrite-child ORDER is authored (semantic); `allowed` is sorted (it is a set).
|
|
371
|
+
*/
|
|
372
|
+
function serializeRelationSchema(
|
|
373
|
+
decls: readonly import("./relation_schema.js").RelationTypeDecl[],
|
|
374
|
+
): UsdLayer["nomosRelations"] {
|
|
375
|
+
if (decls.length === 0) return undefined;
|
|
376
|
+
type AllowedW = { kind: string; type?: string; ns?: string; relation?: string };
|
|
377
|
+
const allowedKey = (a: AllowedW): string =>
|
|
378
|
+
a.kind === "direct" ? `direct:${a.type}` : a.kind === "wildcard" ? "wildcard" : `userset:${a.type}#${a.relation}`;
|
|
379
|
+
const byType = new Map<string, { relations: Map<string, { rewrite: unknown; allowed: unknown[] }>; caps?: { maxDepth: number; maxFanout: number } }>();
|
|
380
|
+
for (const d of decls) {
|
|
381
|
+
let entry = byType.get(d.type);
|
|
382
|
+
if (entry === undefined) {
|
|
383
|
+
entry = { relations: new Map(), ...(d.caps ? { caps: d.caps } : {}) };
|
|
384
|
+
byType.set(d.type, entry);
|
|
385
|
+
} else if (d.caps !== undefined) {
|
|
386
|
+
if (entry.caps !== undefined && (entry.caps.maxDepth !== d.caps.maxDepth || entry.caps.maxFanout !== d.caps.maxFanout)) {
|
|
387
|
+
throw new Error(`relationType("${d.type}"): conflicting caps across declarations`);
|
|
388
|
+
}
|
|
389
|
+
entry.caps = d.caps;
|
|
390
|
+
}
|
|
391
|
+
for (const [rel, def] of d.relations) {
|
|
392
|
+
if (entry.relations.has(rel)) throw new Error(`relationType("${d.type}"): relation "${rel}" declared in two relationType() blocks`);
|
|
393
|
+
const allowed = [...def.allowed].sort((x, y) => (allowedKey(x as AllowedW) < allowedKey(y as AllowedW) ? -1 : 1));
|
|
394
|
+
entry.relations.set(rel, { rewrite: def.rewrite, allowed });
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const types: Record<string, { relations: Record<string, { rewrite: unknown; allowed: unknown[] }>; caps?: { maxDepth: number; maxFanout: number } }> = {};
|
|
398
|
+
for (const type of [...byType.keys()].sort()) {
|
|
399
|
+
const entry = byType.get(type)!;
|
|
400
|
+
const relations: Record<string, { rewrite: unknown; allowed: unknown[] }> = {};
|
|
401
|
+
for (const rel of [...entry.relations.keys()].sort()) relations[rel] = entry.relations.get(rel)!;
|
|
402
|
+
types[type] = { relations, ...(entry.caps ? { caps: entry.caps } : {}) };
|
|
403
|
+
}
|
|
404
|
+
return { v: 1 as const, types };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Synthesize the NATIVE read relations a layer's private aggregates imply. Each distinct
|
|
409
|
+
* `requires(cap)` becomes ONE relation `cap` on the workspace-scope object, allowing the leaf
|
|
410
|
+
* subject classes a read principal can be (`user:<uid>`, the `key:<hash>` control-plane subject,
|
|
411
|
+
* and the `user:*` public wildcard). The read gate's `RewriteEngine.check(workspace:self, cap,
|
|
412
|
+
* Subject::parse(principal))` resolves THIS relation against the tuples a `relateSubject` grant
|
|
413
|
+
* folds — so a granted reader is admitted and a non-grantee fail-closes (no tuple). Pure-direct
|
|
414
|
+
* `this` rewrite (a flat membership relation — the read-cap is just "who was granted it").
|
|
415
|
+
*/
|
|
416
|
+
function synthesizeReadRelationTypes(
|
|
417
|
+
manifest: CanonicalManifest,
|
|
418
|
+
): import("./relation_schema.js").RelationTypeDecl[] {
|
|
419
|
+
const caps = new Set<string>();
|
|
420
|
+
for (const a of manifest.aggregates) {
|
|
421
|
+
if (a.requiresRead !== undefined) caps.add(a.requiresRead);
|
|
422
|
+
}
|
|
423
|
+
if (caps.size === 0) return [];
|
|
424
|
+
const builder = relationType("workspace");
|
|
425
|
+
// Sorted for byte-determinism (the serializer also sorts, but keep the source stable). PURE
|
|
426
|
+
// DIRECT (`rewrite` omitted ⇒ `null`, not `self()`): a flat membership relation — the read-cap is
|
|
427
|
+
// exactly "who was granted it". (`self()`/`Rewrite::This` inside a rewrite expands to an EMPTY
|
|
428
|
+
// leaf in v1, which would zero the encryption fan-out roster — the direct form is what Expand reads.)
|
|
429
|
+
for (const cap of [...caps].sort()) {
|
|
430
|
+
builder.relation(cap, { allow: [user(), key(), anyUser()] });
|
|
431
|
+
}
|
|
432
|
+
return [builder];
|
|
433
|
+
}
|
|
434
|
+
|
|
303
435
|
/**
|
|
304
436
|
* Encode each `{ path, module }` into a `UsdLayer` of prims under that path, returning
|
|
305
437
|
* the canonical `UsdDocument`: layers SORTED by path, prims SORTED by path.
|
|
@@ -323,6 +455,66 @@ export function emitUsd(
|
|
|
323
455
|
const wsInvariants = [...(l.module.workspaceInvariants ?? [])]
|
|
324
456
|
.map((w) => ({ id: w.id, on: w.on }))
|
|
325
457
|
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
458
|
+
// THE READ-VISIBILITY → NATIVE RELATION lowering (the one-model teardown): a private
|
|
459
|
+
// aggregate's `requires(cap)` IS a native read relation on the workspace-scope object. Each
|
|
460
|
+
// distinct read cap synthesizes a `relationType("workspace").relation(cap, { allow:[user, key,
|
|
461
|
+
// anyUser] })` so the ONE evaluator (`RewriteEngine.check`) resolves it at the read gate — the
|
|
462
|
+
// SAME relation a `relateSubject({ object:"workspace:self", relation:cap, ... })` grant folds
|
|
463
|
+
// into. The `nomosReadVisibility` key (type → cap) is RETAINED as the type-to-relation map; the
|
|
464
|
+
// synthesized schema is what makes that cap a checkable relation. SORTED + omit-when-empty.
|
|
465
|
+
const readRelationTypes = synthesizeReadRelationTypes(manifest);
|
|
466
|
+
// THE RELATION SCHEMA (P1): per-type rewrites lowered to the nomosRelations AST, UNIONED with the
|
|
467
|
+
// synthesized read relations. SORTED keys (byte-determinism — the layered flatten reproduces these
|
|
468
|
+
// bytes); omit-when-empty (era rule). A read-cap name colliding with an authored `workspace`
|
|
469
|
+
// relation is an author error (caught in the merge).
|
|
470
|
+
const nomosRelations = serializeRelationSchema([
|
|
471
|
+
...(l.module.relationTypes ?? []),
|
|
472
|
+
...readRelationTypes,
|
|
473
|
+
]);
|
|
474
|
+
// THE RELATION GATE (P4): directives declaring `.requiresRelation` → directiveId → {relation,
|
|
475
|
+
// object}. SORTED keys; omit-when-empty (era rule — a relation-gate-free layer is byte-identical).
|
|
476
|
+
const relationGateReq: Record<
|
|
477
|
+
string,
|
|
478
|
+
{ relation: string; object: string; objectFrom?: { recordType: string; idField: string } }
|
|
479
|
+
> = {};
|
|
480
|
+
for (const d of [...l.module.directives].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))) {
|
|
481
|
+
if (d.requiresRelationGate !== undefined) {
|
|
482
|
+
relationGateReq[d.id] = {
|
|
483
|
+
relation: d.requiresRelationGate.relation,
|
|
484
|
+
object: d.requiresRelationGate.object,
|
|
485
|
+
// PER-RECORD object (home_holons §6): carried through to the wasm gate; omitted when absent
|
|
486
|
+
// so a self-relative directive's USD is byte-identical (the era rule).
|
|
487
|
+
...(d.requiresRelationGate.objectFrom !== undefined
|
|
488
|
+
? { objectFrom: d.requiresRelationGate.objectFrom }
|
|
489
|
+
: {}),
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
const nomosRelationGate =
|
|
494
|
+
Object.keys(relationGateReq).length > 0 ? { v: 1 as const, requires: relationGateReq } : undefined;
|
|
495
|
+
// THE CROSS-CUSTODY ATTESTATION GATE (home_holons §3/§5): directives declaring `.attestationAuthorized()`
|
|
496
|
+
// → directiveId → its LAW-DECLARED owner-source {ownerObject, ownerField}. SORTED keys (byte-
|
|
497
|
+
// determinism). Omit-when-empty (era rule). The one gate verifies the carried §3 quote (binding its
|
|
498
|
+
// subject to the named owner) + pins the principal to the attestation key — the kernel hardcodes no id.
|
|
499
|
+
const attestationReq: Record<
|
|
500
|
+
string,
|
|
501
|
+
{
|
|
502
|
+
ownerObject: string;
|
|
503
|
+
ownerField: string;
|
|
504
|
+
fields: { asserterWorkspace: string; object: string; relation: string; attestedAt: string; publicKey: string; signature: string };
|
|
505
|
+
}
|
|
506
|
+
> = {};
|
|
507
|
+
for (const d of [...l.module.directives].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))) {
|
|
508
|
+
if (d.attestationAuthorizedGate !== undefined) {
|
|
509
|
+
attestationReq[d.id] = {
|
|
510
|
+
ownerObject: d.attestationAuthorizedGate.ownerObject,
|
|
511
|
+
ownerField: d.attestationAuthorizedGate.ownerField,
|
|
512
|
+
fields: { ...d.attestationAuthorizedGate.fields },
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const nomosAttestationGate =
|
|
517
|
+
Object.keys(attestationReq).length > 0 ? { v: 1 as const, directives: attestationReq } : undefined;
|
|
326
518
|
// THE CAPTURED-READ GATE DECLARATION (#58): collected from the canonical
|
|
327
519
|
// manifest's per-directive `capturedReads` (omit-when-empty — read-free law's
|
|
328
520
|
// USD is byte-identical apart from the always-on stableIds key).
|
|
@@ -380,6 +572,9 @@ export function emitUsd(
|
|
|
380
572
|
},
|
|
381
573
|
}
|
|
382
574
|
: {}),
|
|
575
|
+
...(nomosRelations !== undefined ? { nomosRelations } : {}),
|
|
576
|
+
...(nomosRelationGate !== undefined ? { nomosRelationGate } : {}),
|
|
577
|
+
...(nomosAttestationGate !== undefined ? { nomosAttestationGate } : {}),
|
|
383
578
|
...(Object.keys(visAggregates).length > 0
|
|
384
579
|
? { nomosReadVisibility: { v: 1 as const, aggregates: visAggregates } }
|
|
385
580
|
: {}),
|
package/src/usd_layers.ts
CHANGED
|
@@ -365,6 +365,15 @@ export function usdaLayerText(
|
|
|
365
365
|
...(layer.nomosEncryption !== undefined
|
|
366
366
|
? [` custom string nomos:encryptionHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosEncryption)))}`]
|
|
367
367
|
: []),
|
|
368
|
+
...(layer.nomosRelations !== undefined
|
|
369
|
+
? [` custom string nomos:relationsHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosRelations)))}`]
|
|
370
|
+
: []),
|
|
371
|
+
...(layer.nomosRelationGate !== undefined
|
|
372
|
+
? [` custom string nomos:relationGateHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosRelationGate)))}`]
|
|
373
|
+
: []),
|
|
374
|
+
...(layer.nomosAttestationGate !== undefined
|
|
375
|
+
? [` custom string nomos:attestationGateHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosAttestationGate)))}`]
|
|
376
|
+
: []),
|
|
368
377
|
``,
|
|
369
378
|
...body,
|
|
370
379
|
` }`,
|
|
@@ -549,6 +558,51 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
|
|
|
549
558
|
encryption = { v: 1, aggregates: aggregatesRec };
|
|
550
559
|
}
|
|
551
560
|
}
|
|
561
|
+
// THE RELATION SCHEMA (P1): per-type relation defs merged later-wins across the stack,
|
|
562
|
+
// SORTED keys (matching the composed emission). Omit-when-empty (the nomosRelations era rule).
|
|
563
|
+
let relations: UsdLayer["nomosRelations"];
|
|
564
|
+
{
|
|
565
|
+
const t = new Map<string, NonNullable<UsdLayer["nomosRelations"]>["types"][string]>();
|
|
566
|
+
for (const l of stack) {
|
|
567
|
+
if (l.nomosRelations === undefined) continue;
|
|
568
|
+
for (const [type, spec] of Object.entries(l.nomosRelations.types)) t.set(type, spec);
|
|
569
|
+
}
|
|
570
|
+
if (t.size > 0) {
|
|
571
|
+
const typesRec: NonNullable<UsdLayer["nomosRelations"]>["types"] = {};
|
|
572
|
+
for (const k of [...t.keys()].sort()) typesRec[k] = t.get(k)!;
|
|
573
|
+
relations = { v: 1, types: typesRec };
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
// THE RELATION GATE (P4): directiveId → {relation, object} merged later-wins, SORTED. Omit-when-empty.
|
|
577
|
+
let relationGate: UsdLayer["nomosRelationGate"];
|
|
578
|
+
{
|
|
579
|
+
const r = new Map<string, { relation: string; object: string }>();
|
|
580
|
+
for (const l of stack) {
|
|
581
|
+
if (l.nomosRelationGate === undefined) continue;
|
|
582
|
+
for (const [dir, spec] of Object.entries(l.nomosRelationGate.requires)) r.set(dir, spec);
|
|
583
|
+
}
|
|
584
|
+
if (r.size > 0) {
|
|
585
|
+
const requiresRec: Record<string, { relation: string; object: string }> = {};
|
|
586
|
+
for (const k of [...r.keys()].sort()) requiresRec[k] = r.get(k)!;
|
|
587
|
+
relationGate = { v: 1, requires: requiresRec };
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
// THE ATTESTATION GATE (home_holons §3/§5): directiveId → owner-source merged later-wins across layers,
|
|
591
|
+
// SORTED keys. Omit-when-empty (era rule).
|
|
592
|
+
let attestationGate: UsdLayer["nomosAttestationGate"];
|
|
593
|
+
{
|
|
594
|
+
type AttSrc = NonNullable<UsdLayer["nomosAttestationGate"]>["directives"][string];
|
|
595
|
+
const a = new Map<string, AttSrc>();
|
|
596
|
+
for (const l of stack) {
|
|
597
|
+
if (l.nomosAttestationGate === undefined) continue;
|
|
598
|
+
for (const [id, src] of Object.entries(l.nomosAttestationGate.directives)) a.set(id, src);
|
|
599
|
+
}
|
|
600
|
+
if (a.size > 0) {
|
|
601
|
+
const rec: Record<string, AttSrc> = {};
|
|
602
|
+
for (const k of [...a.keys()].sort()) rec[k] = a.get(k)!;
|
|
603
|
+
attestationGate = { v: 1, directives: rec };
|
|
604
|
+
}
|
|
605
|
+
}
|
|
552
606
|
return {
|
|
553
607
|
path,
|
|
554
608
|
prims: [...prims.keys()].sort().map((p) => prims.get(p)!),
|
|
@@ -559,6 +613,9 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
|
|
|
559
613
|
...(readGate !== undefined ? { nomosReadGate: readGate } : {}),
|
|
560
614
|
...(readVisibility !== undefined ? { nomosReadVisibility: readVisibility } : {}),
|
|
561
615
|
...(encryption !== undefined ? { nomosEncryption: encryption } : {}),
|
|
616
|
+
...(relations !== undefined ? { nomosRelations: relations } : {}),
|
|
617
|
+
...(relationGate !== undefined ? { nomosRelationGate: relationGate } : {}),
|
|
618
|
+
...(attestationGate !== undefined ? { nomosAttestationGate: attestationGate } : {}),
|
|
562
619
|
};
|
|
563
620
|
});
|
|
564
621
|
|
|
@@ -680,6 +737,9 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
680
737
|
const readGateByDomain = new Map<string, UsdLayer["nomosReadGate"]>();
|
|
681
738
|
const readVisibilityByDomain = new Map<string, UsdLayer["nomosReadVisibility"]>();
|
|
682
739
|
const encryptionByDomain = new Map<string, UsdLayer["nomosEncryption"]>();
|
|
740
|
+
const relationsByDomain = new Map<string, UsdLayer["nomosRelations"]>();
|
|
741
|
+
const relationGateByDomain = new Map<string, UsdLayer["nomosRelationGate"]>();
|
|
742
|
+
const attestationGateByDomain = new Map<string, UsdLayer["nomosAttestationGate"]>();
|
|
683
743
|
for (const p of prims) {
|
|
684
744
|
if (p.attrs.get("nomos:kind") === "domain") {
|
|
685
745
|
const domain = attrString(p, "nomos:domain");
|
|
@@ -703,6 +763,21 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
703
763
|
if (typeof encHex === "string") {
|
|
704
764
|
encryptionByDomain.set(domain, JSON.parse(unhexUtf8(encHex)) as UsdLayer["nomosEncryption"]);
|
|
705
765
|
}
|
|
766
|
+
// THE RELATION SCHEMA (P1): the hex-carried per-type relation rewrite map.
|
|
767
|
+
const relHex = p.attrs.get("nomos:relationsHex");
|
|
768
|
+
if (typeof relHex === "string") {
|
|
769
|
+
relationsByDomain.set(domain, JSON.parse(unhexUtf8(relHex)) as UsdLayer["nomosRelations"]);
|
|
770
|
+
}
|
|
771
|
+
// THE RELATION GATE (P4): the hex-carried directiveId → {relation, object} map.
|
|
772
|
+
const relGateHex = p.attrs.get("nomos:relationGateHex");
|
|
773
|
+
if (typeof relGateHex === "string") {
|
|
774
|
+
relationGateByDomain.set(domain, JSON.parse(unhexUtf8(relGateHex)) as UsdLayer["nomosRelationGate"]);
|
|
775
|
+
}
|
|
776
|
+
// THE ATTESTATION GATE (home_holons §3/§5): the hex-carried attestation-authorized directive set.
|
|
777
|
+
const attGateHex = p.attrs.get("nomos:attestationGateHex");
|
|
778
|
+
if (typeof attGateHex === "string") {
|
|
779
|
+
attestationGateByDomain.set(domain, JSON.parse(unhexUtf8(attGateHex)) as UsdLayer["nomosAttestationGate"]);
|
|
780
|
+
}
|
|
706
781
|
}
|
|
707
782
|
}
|
|
708
783
|
|
|
@@ -794,6 +869,9 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
794
869
|
const rg = readGateByDomain.get(domain);
|
|
795
870
|
const rv = readVisibilityByDomain.get(domain);
|
|
796
871
|
const enc = encryptionByDomain.get(domain);
|
|
872
|
+
const rel = relationsByDomain.get(domain);
|
|
873
|
+
const relGate = relationGateByDomain.get(domain);
|
|
874
|
+
const attGate = attestationGateByDomain.get(domain);
|
|
797
875
|
return {
|
|
798
876
|
path: `/Nomos/${domain}`,
|
|
799
877
|
prims: [...b.prims].sort(byPath),
|
|
@@ -804,6 +882,9 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
804
882
|
...(rg !== undefined ? { nomosReadGate: rg } : {}),
|
|
805
883
|
...(rv !== undefined ? { nomosReadVisibility: rv } : {}),
|
|
806
884
|
...(enc !== undefined ? { nomosEncryption: enc } : {}),
|
|
885
|
+
...(rel !== undefined ? { nomosRelations: rel } : {}),
|
|
886
|
+
...(relGate !== undefined ? { nomosRelationGate: relGate } : {}),
|
|
887
|
+
...(attGate !== undefined ? { nomosAttestationGate: attGate } : {}),
|
|
807
888
|
};
|
|
808
889
|
})
|
|
809
890
|
.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
package/src/wire.ts
CHANGED
|
@@ -39,7 +39,12 @@ export type WireValue =
|
|
|
39
39
|
| { Int: number }
|
|
40
40
|
| { Set: string[] }
|
|
41
41
|
| { Map: Record<string, WireField> }
|
|
42
|
-
| { Evidence: WireEvidenceRef }
|
|
42
|
+
| { Evidence: WireEvidenceRef }
|
|
43
|
+
// ── STRUCTURAL CRDT state values (only seen when reading RAW folded state back; the read MODEL
|
|
44
|
+
// collapses them via the kernel `observed_value` to Int/Str/Set, so typed reads stay scalar) ──
|
|
45
|
+
| { Counter: Record<string, { pos: number; neg: number }> }
|
|
46
|
+
| { Position: string }
|
|
47
|
+
| { TwoPhaseSet: { adds: string[]; removes: string[] } };
|
|
43
48
|
|
|
44
49
|
/** `EvidenceRef { hash: ContentHash, media_type, byte_length, storage_class }` — the
|
|
45
50
|
* kernel serde shape. `ContentHash(String)` serializes transparently as a bare string
|
|
@@ -57,11 +62,21 @@ export interface WireField {
|
|
|
57
62
|
stamp: WireHlc;
|
|
58
63
|
}
|
|
59
64
|
|
|
60
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* `Op` — the kernel's externally-tagged serde shape (`nomos2/kernel/src/lib.rs::Op`). The originals
|
|
67
|
+
* (Set / AddToSet / SetEntry) plus the STRUCTURAL CRDT ops: `AddToTwoPhase`/`RemoveFromTwoPhase`
|
|
68
|
+
* (the `removeWins` 2-phase set), `IncrementCounter`/`DecrementCounter` (the `counter` PN-counter,
|
|
69
|
+
* carrying a bare `u64` amount), and `SetPosition` (the `lastPosition` LWW order-key, a bare string).
|
|
70
|
+
*/
|
|
61
71
|
export type WireOp =
|
|
62
72
|
| { Set: WireValue }
|
|
63
73
|
| { AddToSet: string[] }
|
|
64
|
-
| { SetEntry: { key: string; value: WireValue } }
|
|
74
|
+
| { SetEntry: { key: string; value: WireValue } }
|
|
75
|
+
| { AddToTwoPhase: string[] }
|
|
76
|
+
| { RemoveFromTwoPhase: string[] }
|
|
77
|
+
| { IncrementCounter: number }
|
|
78
|
+
| { DecrementCounter: number }
|
|
79
|
+
| { SetPosition: string };
|
|
65
80
|
|
|
66
81
|
/** `FieldOp { field: FieldId, op: Op }`. */
|
|
67
82
|
export interface WireFieldOp {
|
package/src/wire_encode.ts
CHANGED
|
@@ -100,13 +100,39 @@ function encodeKernelValue(field: Field | undefined, value: unknown): WireValue
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
function encodeKernelFieldOp(op: FieldOp, field: Field | undefined): WireFieldOp {
|
|
103
|
+
// The field's DRIVER selects the kernel op for the structural CRDTs — a `set`/`addToSet` on a
|
|
104
|
+
// position / remove-wins field lowers to `SetPosition` / `AddToTwoPhase`, never the scalar/AddWins op.
|
|
105
|
+
const driver = field?.driver?.kind;
|
|
103
106
|
let wireOp: WireOp;
|
|
104
107
|
switch (op.kind) {
|
|
105
108
|
case "set":
|
|
106
|
-
|
|
109
|
+
// A `lastPosition` field's `set` is the kernel `SetPosition` (LWW order-key, a bare string).
|
|
110
|
+
wireOp =
|
|
111
|
+
driver === "LastPosition"
|
|
112
|
+
? { SetPosition: String(op.value) }
|
|
113
|
+
: { Set: encodeKernelValue(field, op.value) };
|
|
107
114
|
break;
|
|
108
115
|
case "addToSet":
|
|
109
|
-
|
|
116
|
+
// On a remove-wins set the add grows the 2-phase set's `adds`; on AddWins it is the plain union.
|
|
117
|
+
wireOp =
|
|
118
|
+
driver === "RemoveWins"
|
|
119
|
+
? { AddToTwoPhase: [...(op.items ?? [])] }
|
|
120
|
+
: { AddToSet: [...(op.items ?? [])] };
|
|
121
|
+
break;
|
|
122
|
+
case "removeFromSet":
|
|
123
|
+
if (driver !== "RemoveWins") {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`removeFromSet('${op.field}'): only a RemoveWins set has a remove (an AddWins set is ` +
|
|
126
|
+
`add-only by design). Declare the field \`t.set(...).merge(RemoveWins)\`.`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
wireOp = { RemoveFromTwoPhase: [...(op.items ?? [])] };
|
|
130
|
+
break;
|
|
131
|
+
case "increment":
|
|
132
|
+
wireOp = { IncrementCounter: Number(op.value) };
|
|
133
|
+
break;
|
|
134
|
+
case "decrement":
|
|
135
|
+
wireOp = { DecrementCounter: Number(op.value) };
|
|
110
136
|
break;
|
|
111
137
|
case "setEntry":
|
|
112
138
|
wireOp = {
|