@githolon/dsl 0.14.0 → 0.16.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.
Files changed (42) hide show
  1. package/dart/example/nomos_webview_transport.dart +2 -2
  2. package/dart/lib/nomos_dsl.dart +4 -4
  3. package/dart/lib/src/{dispatch.dart → offer.dart} +7 -7
  4. package/dart/lib/src/provider.dart +30 -30
  5. package/dart/lib/src/subscriptions.dart +3 -3
  6. package/dart/lib/src/webview_bridge.dart +56 -21
  7. package/dart/test/webview_bridge_test.dart +35 -34
  8. package/package.json +5 -3
  9. package/src/aggregate.ts +9 -1
  10. package/src/authoring.ts +10 -1
  11. package/src/build_package.ts +1 -1
  12. package/src/capability_exports.ts +2 -2
  13. package/src/codegen_dart.ts +34 -24
  14. package/src/codegen_dart_client.ts +74 -12
  15. package/src/codegen_provider_dart.ts +67 -61
  16. package/src/codegen_ts.ts +45 -17
  17. package/src/compile_package_main.ts +103 -36
  18. package/src/ctx.ts +13 -0
  19. package/src/engine_entry.ts +22 -5
  20. package/src/fields.ts +44 -0
  21. package/src/flow_gate.ts +208 -0
  22. package/src/framework/capability.ts +29 -11
  23. package/src/framework/encryption_scope.ts +9 -1
  24. package/src/framework/genesis.ts +5 -1
  25. package/src/framework/impure_capability.ts +73 -67
  26. package/src/framework/relations.ts +23 -4
  27. package/src/framework/signing_identity.ts +1 -1
  28. package/src/framework/sync_lifecycle.ts +3 -3
  29. package/src/framework/workspace_invariant.ts +29 -8
  30. package/src/framework/workspaces.ts +7 -0
  31. package/src/index.ts +25 -1
  32. package/src/manifest.ts +11 -17
  33. package/src/ops.ts +42 -1
  34. package/src/principal_evidence.ts +112 -0
  35. package/src/relation_sugar.ts +10 -4
  36. package/src/shape_compat.ts +208 -0
  37. package/src/usd.ts +287 -0
  38. package/src/usd_layers.ts +213 -26
  39. package/src/wire.ts +8 -0
  40. package/src/wire_encode.ts +75 -2
  41. package/src/workspace_sharding.ts +197 -24
  42. package/src/workspace_type.ts +1 -0
package/src/usd.ts CHANGED
@@ -33,6 +33,7 @@ import {
33
33
  type CanonicalManifest,
34
34
  type CanonicalProjectionReturn,
35
35
  } from "./manifest.js";
36
+ import type { AggregateHandle } from "./aggregate.js";
36
37
  import {
37
38
  mergeRequiresScope,
38
39
  CompositionViolation,
@@ -40,7 +41,9 @@ import {
40
41
  } from "./compose.js";
41
42
  import type { WireSchema, WireRefMode } from "./wire.js";
42
43
  import type { DomainModule } from "./codegen_dart.js";
44
+ import { domainInterface } from "./shape_compat.js";
43
45
  import { relationType, user, key, anyUser } from "./relation_schema.js";
46
+ import { harvestPrincipalFlows } from "./wire_encode.js";
44
47
 
45
48
  /** The referential marker of a directive prim (kernel `RefMode` wire string). */
46
49
  export type RefMode = WireRefMode;
@@ -126,10 +129,46 @@ export interface UsdCombined {
126
129
  readonly returns: CanonicalProjectionReturn;
127
130
  }
128
131
 
132
+ /** One field's read-projection descriptor: the field kind plus a map's value kind. */
133
+ export interface UsdFieldKindDescriptor {
134
+ readonly kind: string;
135
+ readonly mapValueKind?: string;
136
+ }
137
+
138
+ export interface UsdReadProjection {
139
+ readonly aggregateFieldKinds: Record<string, Record<string, UsdFieldKindDescriptor>>;
140
+ readonly queries: { id: string; key: { field: string; kind: string }[]; returns: string }[];
141
+ readonly counts: { id: string; of: string; by: string | null; where?: unknown }[];
142
+ readonly spatials: { id: string; of: string; on: string }[];
143
+ readonly deriveds: { id: string; of: string; returns: CanonicalProjectionReturn }[];
144
+ readonly combineds: { id: string; of: string; refField: string; reads: string; returns: CanonicalProjectionReturn }[];
145
+ readonly sums?: { id: string; of: string; field: string; by: string | null; where?: unknown }[];
146
+ readonly mins?: { id: string; of: string; field: string; by: string | null; where?: unknown }[];
147
+ readonly maxes?: { id: string; of: string; field: string; by: string | null; where?: unknown }[];
148
+ readonly orderedReads?: {
149
+ id: string;
150
+ of: string;
151
+ orderKey: string;
152
+ orderDesc?: true;
153
+ where?: unknown;
154
+ limit: number;
155
+ }[];
156
+ }
157
+
129
158
  /** One USD layer: a stage path + its prims (canonically sorted by prim path). */
130
159
  export interface UsdLayer {
131
160
  readonly path: string;
132
161
  readonly prims: UsdPrim[];
162
+ /**
163
+ * The canonical admission/merge manifest for this layer, committed inside the package.
164
+ * Hosts must not stage or mutate this view; holons derive it from active installed law.
165
+ */
166
+ readonly nomosIdentityManifest?: CanonicalManifest;
167
+ /**
168
+ * The projection schema/read-routing fragment for this layer, committed inside the package.
169
+ * It carries the projection-only field-kind data that cannot be recovered from merge drivers.
170
+ */
171
+ readonly nomosReadProjection?: UsdReadProjection;
133
172
  /**
134
173
  * The layer's NAMED, INDEXED read declarations (read-side closure step 1), SORTED
135
174
  * by id. OMITTED (`undefined`) when the module declares no query, so a query-free
@@ -255,6 +294,15 @@ export interface UsdLayer {
255
294
  * whose old AND new packages both carry this key is DIFFED BY SID at the one gate.
256
295
  */
257
296
  readonly nomosStableIds?: import("./stable_ids_types.js").StableIds;
297
+ /**
298
+ * THE STRUCTURAL INTERFACE (#72) — this domain's directives (payload shapes) + aggregates
299
+ * (read shapes, sid-keyed). ON-LEDGER (it rides the package, which installs as the
300
+ * `policy:{hash}.bundle`), so a client reads the workspace's OFFER straight from the folded
301
+ * law — reactively, host-blind (architecture/shape_compatibility.md). A client negotiates
302
+ * shape-compatibility against THIS, not the package hash. Pre-#72 bundles omit it; a client
303
+ * then reports `offerKnown:false` and never refuses on its absence (era-safe, graceful).
304
+ */
305
+ readonly nomosInterface?: import("./shape_compat.js").DomainInterface;
258
306
  /**
259
307
  * THE CAPTURED-READ GATE DECLARATION (#58). Present IFF ≥1 directive of this layer
260
308
  * declares a captured-read query (`.reads(someQuery)`): the law's EXPLICIT opt-in
@@ -304,6 +352,47 @@ export interface UsdLayer {
304
352
  readonly v: 1;
305
353
  readonly aggregates: Record<string, { scope: string; fields: string[]; delegatable: false }>;
306
354
  };
355
+ /**
356
+ * THE CAPABILITY DECLARATION IN LAW (Full-A — capability_marketplace.md §4): the impure-
357
+ * capability quartets (obligation/complete/fail/block/deadLetter) lowered into the SEALED
358
+ * law, so the holon SURFACES them on the `declared_capabilities` read and the host relays a
359
+ * deploy-time "declared — bind it or obligations wait" notice without reaching into root
360
+ * law-state (boundness stays P6 — it is cross-holon root state). OMITTED for capability-free
361
+ * law (byte-identical hash — the era rule); declaring a capability MOVES the domainHash
362
+ * (named, expected). SORTED by aggregateId.
363
+ */
364
+ readonly nomosCapabilities?: {
365
+ readonly v: 1;
366
+ readonly capabilities: readonly {
367
+ readonly capability: string;
368
+ readonly aggregateId: string;
369
+ readonly obligation: string;
370
+ readonly complete: string;
371
+ readonly fail: string;
372
+ readonly block: string;
373
+ readonly deadLetter: string;
374
+ readonly tasksQueryId: string | null;
375
+ /** The HTTPS endpoint the obligation is offered to (outbox); absent ⇒ no destination. */
376
+ readonly endpoint?: string;
377
+ }[];
378
+ };
379
+ /**
380
+ * THE FLOW GATE WAIVERS (architecture/security_model.md) — both halves of the audit surface,
381
+ * hashed into the law + replay-verified. `declassify` (confidentiality, rule 4): every acknowledged
382
+ * secret-correlated refusal. `endorse` (the integrity dual): every harvested PrincipalEvidence flow
383
+ * into an authority/standing/attribution sink — discovery IS law, witnessed fail-closed at runtime.
384
+ * Each half is OMITTED when empty (the era rule — a flow-free domain is byte-identical).
385
+ */
386
+ readonly nomosFlowGate?: {
387
+ readonly v: 1;
388
+ readonly declassify?: readonly { readonly site: string; readonly reason: string }[];
389
+ readonly endorse?: readonly {
390
+ readonly directiveId: string;
391
+ readonly sink: string;
392
+ readonly facet: string;
393
+ readonly reason: string | null;
394
+ }[];
395
+ };
307
396
  }
308
397
 
309
398
  /** The reserved variant name declaring a set's DEFAULT (USD has a default variant). */
@@ -363,6 +452,123 @@ function encodeModuleToPrims(layerPath: string, mod: DomainModule): UsdPrim[] {
363
452
  return sortPrims(prims);
364
453
  }
365
454
 
455
+ function aggregateFieldKindsForModule(
456
+ mod: DomainModule,
457
+ ): Record<string, Record<string, UsdFieldKindDescriptor>> {
458
+ const out: Record<string, Record<string, UsdFieldKindDescriptor>> = {};
459
+ for (const agg of [...(mod.aggregates as AggregateHandle[])].sort((a, b) =>
460
+ a.id < b.id ? -1 : a.id > b.id ? 1 : 0,
461
+ )) {
462
+ const fields: Record<string, UsdFieldKindDescriptor> = {};
463
+ for (const name of Object.keys(agg.fields).sort()) {
464
+ const field = agg.fields[name]!;
465
+ fields[name] = {
466
+ kind: field.kind,
467
+ ...(field.mapValueKind !== undefined ? { mapValueKind: field.mapValueKind } : {}),
468
+ };
469
+ }
470
+ out[agg.id] = fields;
471
+ }
472
+ return out;
473
+ }
474
+
475
+ function readKeyFieldKind(
476
+ byType: Record<string, Record<string, UsdFieldKindDescriptor>>,
477
+ queryId: string,
478
+ returns: string,
479
+ keyField: string,
480
+ ): string {
481
+ const schema = byType[returns];
482
+ if (schema === undefined) {
483
+ throw new Error(
484
+ `usd: query '${queryId}' returns '${returns}', which has no field schema in nomosReadProjection`,
485
+ );
486
+ }
487
+ const lookup = keyField.includes(".") ? keyField.split(".")[0]! : keyField;
488
+ const descriptor = schema[lookup];
489
+ if (descriptor === undefined) {
490
+ throw new Error(
491
+ `usd: query '${queryId}' is keyed on '${keyField}' (field '${lookup}'), which '${returns}' does not declare`,
492
+ );
493
+ }
494
+ return descriptor.kind;
495
+ }
496
+
497
+ function readProjectionForModule(mod: DomainModule, manifest: CanonicalManifest): UsdReadProjection {
498
+ const aggregateFieldKinds = aggregateFieldKindsForModule(mod);
499
+ return {
500
+ aggregateFieldKinds,
501
+ queries: (manifest.queries ?? []).map((q) => ({
502
+ id: q.id,
503
+ key: q.key.map((field) => ({
504
+ field,
505
+ kind: readKeyFieldKind(aggregateFieldKinds, q.id, q.returns, field),
506
+ })),
507
+ returns: q.returns,
508
+ })),
509
+ counts: (manifest.counts ?? []).map((c) => ({
510
+ id: c.id,
511
+ of: c.of,
512
+ by: c.by ?? null,
513
+ ...(c.where !== undefined ? { where: c.where } : {}),
514
+ })),
515
+ spatials: (manifest.spatials ?? []).map((s) => ({ id: s.id, of: s.of, on: s.on })),
516
+ deriveds: (manifest.deriveds ?? []).map((d) => ({ id: d.id, of: d.of, returns: d.returns })),
517
+ combineds: (manifest.combineds ?? []).map((c) => ({
518
+ id: c.id,
519
+ of: c.of,
520
+ refField: c.refField,
521
+ reads: c.reads,
522
+ returns: c.returns,
523
+ })),
524
+ ...((manifest.sums ?? []).length > 0
525
+ ? {
526
+ sums: manifest.sums!.map((s) => ({
527
+ id: s.id,
528
+ of: s.of,
529
+ field: s.sumField,
530
+ by: s.by ?? null,
531
+ ...(s.where !== undefined ? { where: s.where } : {}),
532
+ })),
533
+ }
534
+ : {}),
535
+ ...((manifest.mins ?? []).length > 0
536
+ ? {
537
+ mins: manifest.mins!.map((e) => ({
538
+ id: e.id,
539
+ of: e.of,
540
+ field: e.valueField,
541
+ by: e.by ?? null,
542
+ ...(e.where !== undefined ? { where: e.where } : {}),
543
+ })),
544
+ }
545
+ : {}),
546
+ ...((manifest.maxes ?? []).length > 0
547
+ ? {
548
+ maxes: manifest.maxes!.map((e) => ({
549
+ id: e.id,
550
+ of: e.of,
551
+ field: e.valueField,
552
+ by: e.by ?? null,
553
+ ...(e.where !== undefined ? { where: e.where } : {}),
554
+ })),
555
+ }
556
+ : {}),
557
+ ...((manifest.orderedReads ?? []).length > 0
558
+ ? {
559
+ orderedReads: manifest.orderedReads!.map((r) => ({
560
+ id: r.id,
561
+ of: r.of,
562
+ orderKey: r.orderKey,
563
+ ...(r.orderDesc ? { orderDesc: true as const } : {}),
564
+ ...(r.where !== undefined ? { where: r.where } : {}),
565
+ limit: r.limit,
566
+ })),
567
+ }
568
+ : {}),
569
+ };
570
+ }
571
+
366
572
  /**
367
573
  * Lower a layer's `relationType` decls into the canonical `nomosRelations` wire object (P1) —
368
574
  * keys SORTED for byte-determinism, omit-when-empty (the era rule). Multiple decls for one type
@@ -546,14 +752,83 @@ export function emitUsd(
546
752
  encAggregates[a.id] = { scope: a.requiresRead, fields: [...a.encryptedFields], delegatable: false };
547
753
  }
548
754
  }
755
+ // THE CAPABILITY DECLARATION IN LAW (Full-A — capability_marketplace.md §4): the
756
+ // impure-capability quartets lowered into the SEALED law, so the holon can SURFACE
757
+ // them on a read (`declared_capabilities`) and the host RELAYS a deploy-time notice
758
+ // ("declares 'x' — bind it or obligations wait") WITHOUT reaching into root law-state.
759
+ // Mirrors the deploy-sidecar `capabilityLowerings` extraction; SORTED by aggregate id;
760
+ // omit-when-empty so capability-free law is byte-identical (the era rule). Declaring a
761
+ // capability now MOVES the domainHash (named, expected — like the stable-ids key).
762
+ const nomosCapabilities = [...((l.module as { impureCapabilities?: readonly unknown[] }).impureCapabilities ?? [])]
763
+ .map((c) => {
764
+ const k = c as {
765
+ aggregate: { id: string };
766
+ obligation: { id: string };
767
+ complete: { id: string };
768
+ fail: { id: string };
769
+ block: { id: string };
770
+ deadLetter: { id: string };
771
+ name?: unknown;
772
+ capability?: unknown;
773
+ endpoint?: unknown;
774
+ tasks?: { id?: unknown };
775
+ };
776
+ const cap = typeof k.name === "string" ? k.name : typeof k.capability === "string" ? k.capability : null;
777
+ if (cap === null) return null; // a binding-free quartet declares nothing (omit-when-absent)
778
+ return {
779
+ capability: cap,
780
+ aggregateId: k.aggregate.id,
781
+ obligation: k.obligation.id,
782
+ complete: k.complete.id,
783
+ fail: k.fail.id,
784
+ block: k.block.id,
785
+ deadLetter: k.deadLetter.id,
786
+ tasksQueryId: typeof k.tasks?.id === "string" ? k.tasks.id : null,
787
+ ...(typeof k.endpoint === "string" ? { endpoint: k.endpoint } : {}),
788
+ };
789
+ })
790
+ .filter((c): c is NonNullable<typeof c> => c !== null)
791
+ .sort((a, b) => (a.aggregateId < b.aggregateId ? -1 : a.aggregateId > b.aggregateId ? 1 : 0));
792
+ // THE DECLASSIFY SITES (nomosFlowGate rule 4 — security_model.md): every acknowledged
793
+ // intended secret-correlated refusal, with its human reason. Hashed into the law (it IS
794
+ // a law-declared waiver) + replay-verified — "the set of declassify sites is the entire
795
+ // audit surface". Omit-when-empty: a domain with no declassify is byte-identical.
796
+ const flowDeclassify = [
797
+ ...[...(l.module.workspaceInvariants ?? [])]
798
+ .filter((wi) => typeof (wi as { declassify?: unknown }).declassify === "string")
799
+ .map((wi) => ({ site: `workspaceInvariant:${wi.id}`, reason: (wi as { declassify: string }).declassify })),
800
+ ...[...(l.module.aggregates as { id: string; invariantDeclassify?: unknown }[])]
801
+ .filter((a) => typeof a.invariantDeclassify === "string")
802
+ .map((a) => ({ site: `aggregateInvariant:${a.id}`, reason: a.invariantDeclassify as string })),
803
+ ...[...((l.module as { impureCapabilities?: readonly { name?: string; capability?: string; aggregate: { id: string }; declassify?: unknown }[] }).impureCapabilities ?? [])]
804
+ .filter((c) => typeof c.declassify === "string")
805
+ .map((c) => ({ site: `capability:${c.name ?? c.capability ?? c.aggregate.id}`, reason: c.declassify as string })),
806
+ ].sort((a, b) => (a.site < b.site ? -1 : a.site > b.site ? 1 : 0));
807
+ // THE ENDORSE FLOWS (the integrity dual — security_model.md): discovery IS law. Run each
808
+ // directive's pure plan with a probe principal + payload and harvest every PrincipalEvidence
809
+ // conversion's declared flow {directiveId, sink, facet, reason}. Hashed into nomosFlowGate +
810
+ // witnessed fail-closed at runtime (Slice B). Omit-when-empty (a principal-free domain is
811
+ // byte-identical). SORTED for determinism.
812
+ const flowEndorse = [...l.module.directives]
813
+ .flatMap((d) => harvestPrincipalFlows(d as never).map((f) => ({ directiveId: d.id, sink: f.sink, facet: f.facet, reason: f.reason })))
814
+ .sort((a, b) =>
815
+ a.directiveId !== b.directiveId ? (a.directiveId < b.directiveId ? -1 : 1)
816
+ : a.sink !== b.sink ? (a.sink < b.sink ? -1 : 1)
817
+ : a.facet < b.facet ? -1 : a.facet > b.facet ? 1 : 0,
818
+ );
549
819
  return {
550
820
  path: l.path,
551
821
  prims: encodeModuleToPrims(l.path, l.module),
822
+ nomosIdentityManifest: manifest,
823
+ nomosReadProjection: readProjectionForModule(l.module, manifest),
552
824
  ...(manifest.queries !== undefined ? { queries: manifest.queries } : {}),
553
825
  ...(manifest.deriveds !== undefined ? { deriveds: manifest.deriveds } : {}),
554
826
  ...(manifest.combineds !== undefined ? { combineds: manifest.combineds } : {}),
555
827
  // STABLE IDS (#58): every post-#58 compile carries them (the era key).
556
828
  ...(manifest.stableIds !== undefined ? { nomosStableIds: manifest.stableIds } : {}),
829
+ // THE STRUCTURAL INTERFACE (#72): the on-ledger OFFER — a client negotiates shape-
830
+ // compatibility against this, reactively, straight from the folded `policy:{hash}.bundle`.
831
+ nomosInterface: domainInterface(l.module, manifest.stableIds),
557
832
  ...(Object.keys(readGateDirectives).length > 0
558
833
  ? {
559
834
  nomosReadGate: {
@@ -581,6 +856,18 @@ export function emitUsd(
581
856
  ...(Object.keys(encAggregates).length > 0
582
857
  ? { nomosEncryption: { v: 1 as const, aggregates: encAggregates } }
583
858
  : {}),
859
+ ...(nomosCapabilities.length > 0
860
+ ? { nomosCapabilities: { v: 1 as const, capabilities: nomosCapabilities } }
861
+ : {}),
862
+ ...(flowDeclassify.length > 0 || flowEndorse.length > 0
863
+ ? {
864
+ nomosFlowGate: {
865
+ v: 1 as const,
866
+ ...(flowDeclassify.length > 0 ? { declassify: flowDeclassify } : {}),
867
+ ...(flowEndorse.length > 0 ? { endorse: flowEndorse } : {}),
868
+ },
869
+ }
870
+ : {}),
584
871
  };
585
872
  })
586
873
  .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
package/src/usd_layers.ts CHANGED
@@ -143,6 +143,43 @@ function sortedSchema(schema: WireSchema): WireSchema {
143
143
  return out;
144
144
  }
145
145
 
146
+ function domainLayerAttrLines(domain: string, layer: UsdLayer, indent: string): string[] {
147
+ return [
148
+ `${indent}custom string nomos:kind = "domain"`,
149
+ `${indent}custom string nomos:domain = ${usdaString(domain)}`,
150
+ ...(layer.nomosIdentityManifest !== undefined
151
+ ? [`${indent}custom string nomos:identityManifestHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosIdentityManifest)))}`]
152
+ : []),
153
+ ...(layer.nomosReadProjection !== undefined
154
+ ? [`${indent}custom string nomos:readProjectionHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosReadProjection)))}`]
155
+ : []),
156
+ ...(layer.nomosStableIds !== undefined
157
+ ? [`${indent}custom string nomos:stableIdsHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosStableIds)))}`]
158
+ : []),
159
+ ...(layer.nomosInterface !== undefined
160
+ ? [`${indent}custom string nomos:interfaceHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosInterface)))}`]
161
+ : []),
162
+ ...(layer.nomosReadGate !== undefined
163
+ ? [`${indent}custom string nomos:readGateHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosReadGate)))}`]
164
+ : []),
165
+ ...(layer.nomosReadVisibility !== undefined
166
+ ? [`${indent}custom string nomos:readVisibilityHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosReadVisibility)))}`]
167
+ : []),
168
+ ...(layer.nomosEncryption !== undefined
169
+ ? [`${indent}custom string nomos:encryptionHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosEncryption)))}`]
170
+ : []),
171
+ ...(layer.nomosRelations !== undefined
172
+ ? [`${indent}custom string nomos:relationsHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosRelations)))}`]
173
+ : []),
174
+ ...(layer.nomosRelationGate !== undefined
175
+ ? [`${indent}custom string nomos:relationGateHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosRelationGate)))}`]
176
+ : []),
177
+ ...(layer.nomosAttestationGate !== undefined
178
+ ? [`${indent}custom string nomos:attestationGateHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosAttestationGate)))}`]
179
+ : []),
180
+ ];
181
+ }
182
+
146
183
  // ── per-module layer emission ────────────────────────────────────────────────────────
147
184
 
148
185
  /** One authored module of a domain's ordered module list (weak → strong). */
@@ -349,31 +386,9 @@ export function usdaLayerText(
349
386
  `{`,
350
387
  ` def Scope ${usdaString(primName(domain))}`,
351
388
  ` {`,
352
- ` custom string nomos:kind = "domain"`,
353
- ` custom string nomos:domain = ${usdaString(domain)}`,
354
- // STABLE IDS + the captured-read gate (#58): layer-level law keys, hex-carried
389
+ // Stable ids + holon-owned manifests: layer-level law keys, hex-carried
355
390
  // so the .usda round-trips byte-faithfully to the IR layer.
356
- ...(layer.nomosStableIds !== undefined
357
- ? [` custom string nomos:stableIdsHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosStableIds)))}`]
358
- : []),
359
- ...(layer.nomosReadGate !== undefined
360
- ? [` custom string nomos:readGateHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosReadGate)))}`]
361
- : []),
362
- ...(layer.nomosReadVisibility !== undefined
363
- ? [` custom string nomos:readVisibilityHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosReadVisibility)))}`]
364
- : []),
365
- ...(layer.nomosEncryption !== undefined
366
- ? [` custom string nomos:encryptionHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosEncryption)))}`]
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
- : []),
391
+ ...domainLayerAttrLines(domain, layer, " "),
377
392
  ``,
378
393
  ...body,
379
394
  ` }`,
@@ -388,8 +403,30 @@ export function usdaLayerText(
388
403
  * list is STRONGEST-FIRST, so the list is emitted REVERSED. Same monoid, opposite
389
404
  * notation — the reversal IS the correspondence, recorded in the doc string.
390
405
  */
391
- export function layeredRootUsda(sublayerRelPathsWeakToStrong: readonly string[]): string {
406
+ export function layeredRootUsda(
407
+ sublayerRelPathsWeakToStrong: readonly string[],
408
+ composed?: UsdDocument,
409
+ ): string {
392
410
  const strongestFirst = [...sublayerRelPathsWeakToStrong].reverse();
411
+ const composedDomainOpinions =
412
+ composed === undefined || composed.layers.length === 0
413
+ ? []
414
+ : [
415
+ ``,
416
+ `over "Nomos"`,
417
+ `{`,
418
+ ...composed.layers.flatMap((layer) => {
419
+ const domain = layer.path.slice(layer.path.lastIndexOf("/") + 1);
420
+ return [
421
+ ` over ${usdaString(primName(domain))}`,
422
+ ` {`,
423
+ ...domainLayerAttrLines(domain, layer, " "),
424
+ ` }`,
425
+ ];
426
+ }),
427
+ `}`,
428
+ ``,
429
+ ];
393
430
  return [
394
431
  `#usda 1.0`,
395
432
  `(`,
@@ -406,6 +443,7 @@ export function layeredRootUsda(sublayerRelPathsWeakToStrong: readonly string[])
406
443
  ),
407
444
  ` ]`,
408
445
  `)`,
446
+ ...composedDomainOpinions,
409
447
  ``,
410
448
  ].join("\n");
411
449
  }
@@ -461,6 +499,97 @@ function foldById<T extends { readonly id: string }>(
461
499
  return [...byId.keys()].sort().map((id) => byId.get(id)!);
462
500
  }
463
501
 
502
+ function foldByKey<T>(
503
+ lists: readonly (readonly T[] | undefined)[],
504
+ keyOf: (item: T) => string,
505
+ ): T[] | undefined {
506
+ const byKey = new Map<string, T>();
507
+ for (const list of lists) for (const item of list ?? []) byKey.set(keyOf(item), item);
508
+ if (byKey.size === 0) return undefined;
509
+ return [...byKey.keys()].sort().map((key) => byKey.get(key)!);
510
+ }
511
+
512
+ function foldRecord<T>(
513
+ records: readonly (Readonly<Record<string, T>> | undefined)[],
514
+ ): Record<string, T> | undefined {
515
+ const byKey = new Map<string, T>();
516
+ for (const record of records) {
517
+ if (record === undefined) continue;
518
+ for (const [key, value] of Object.entries(record)) byKey.set(key, value);
519
+ }
520
+ if (byKey.size === 0) return undefined;
521
+ const out: Record<string, T> = {};
522
+ for (const key of [...byKey.keys()].sort()) out[key] = byKey.get(key)!;
523
+ return out;
524
+ }
525
+
526
+ function foldIdentityManifest(
527
+ stack: readonly UsdLayer[],
528
+ stableIds: UsdLayer["nomosStableIds"],
529
+ ): UsdLayer["nomosIdentityManifest"] {
530
+ const manifests = stack
531
+ .map((l) => l.nomosIdentityManifest)
532
+ .filter((m): m is NonNullable<UsdLayer["nomosIdentityManifest"]> => m !== undefined);
533
+ if (manifests.length === 0) return undefined;
534
+ const domain = manifests[manifests.length - 1]!.domain;
535
+ const workspaceInvariants = foldById(manifests.map((m) => m.workspaceInvariants));
536
+ const queries = foldById(manifests.map((m) => m.queries));
537
+ const counts = foldById(manifests.map((m) => m.counts));
538
+ const spatials = foldById(manifests.map((m) => m.spatials));
539
+ const sums = foldById(manifests.map((m) => m.sums));
540
+ const deriveds = foldById(manifests.map((m) => m.deriveds));
541
+ const combineds = foldById(manifests.map((m) => m.combineds));
542
+ const mins = foldById(manifests.map((m) => m.mins));
543
+ const maxes = foldById(manifests.map((m) => m.maxes));
544
+ const orderedReads = foldById(manifests.map((m) => m.orderedReads));
545
+ const workspaceTypes = foldById(manifests.map((m) => m.workspaceTypes));
546
+ const homes = foldRecord(manifests.map((m) => m.homes));
547
+ const routes = foldByKey(manifests.map((m) => m.routes), (r) => r.directive);
548
+ return {
549
+ domain,
550
+ aggregates: foldById(manifests.map((m) => m.aggregates)) ?? [],
551
+ directives: foldById(manifests.map((m) => m.directives)) ?? [],
552
+ ...(stableIds !== undefined ? { stableIds } : {}),
553
+ ...(workspaceInvariants !== undefined ? { workspaceInvariants } : {}),
554
+ ...(queries !== undefined ? { queries } : {}),
555
+ ...(counts !== undefined ? { counts } : {}),
556
+ ...(spatials !== undefined ? { spatials } : {}),
557
+ ...(sums !== undefined ? { sums } : {}),
558
+ ...(deriveds !== undefined ? { deriveds } : {}),
559
+ ...(combineds !== undefined ? { combineds } : {}),
560
+ ...(mins !== undefined ? { mins } : {}),
561
+ ...(maxes !== undefined ? { maxes } : {}),
562
+ ...(orderedReads !== undefined ? { orderedReads } : {}),
563
+ ...(workspaceTypes !== undefined ? { workspaceTypes } : {}),
564
+ ...(homes !== undefined ? { homes } : {}),
565
+ ...(routes !== undefined ? { routes } : {}),
566
+ };
567
+ }
568
+
569
+ function foldReadProjection(stack: readonly UsdLayer[]): UsdLayer["nomosReadProjection"] {
570
+ const projections = stack
571
+ .map((l) => l.nomosReadProjection)
572
+ .filter((p): p is NonNullable<UsdLayer["nomosReadProjection"]> => p !== undefined);
573
+ if (projections.length === 0) return undefined;
574
+ const aggregateFieldKinds = foldRecord(projections.map((p) => p.aggregateFieldKinds)) ?? {};
575
+ const sums = foldById(projections.map((p) => p.sums));
576
+ const mins = foldById(projections.map((p) => p.mins));
577
+ const maxes = foldById(projections.map((p) => p.maxes));
578
+ const orderedReads = foldById(projections.map((p) => p.orderedReads));
579
+ return {
580
+ aggregateFieldKinds,
581
+ queries: foldById(projections.map((p) => p.queries)) ?? [],
582
+ counts: foldById(projections.map((p) => p.counts)) ?? [],
583
+ spatials: foldById(projections.map((p) => p.spatials)) ?? [],
584
+ deriveds: foldById(projections.map((p) => p.deriveds)) ?? [],
585
+ combineds: foldById(projections.map((p) => p.combineds)) ?? [],
586
+ ...(sums !== undefined ? { sums } : {}),
587
+ ...(mins !== undefined ? { mins } : {}),
588
+ ...(maxes !== undefined ? { maxes } : {}),
589
+ ...(orderedReads !== undefined ? { orderedReads } : {}),
590
+ };
591
+ }
592
+
464
593
  /**
465
594
  * THE ORDERED OPINION-MAP FOLD: flatten module layers (weak → strong within each
466
595
  * stage path) into the effective single-layer-per-domain document. Strongest
@@ -509,6 +638,39 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
509
638
  stableIds = { v: 1, domain: domainSid, aggregates };
510
639
  }
511
640
  }
641
+ // THE STRUCTURAL INTERFACE (#72): fold per-module interfaces — directives + aggregates merged
642
+ // later-wins, SORTED keys (matching the composed emission's sorted-key `domainInterface`).
643
+ let iface: UsdLayer["nomosInterface"];
644
+ {
645
+ type Dir = NonNullable<UsdLayer["nomosInterface"]>["directives"][string];
646
+ type Agg = NonNullable<UsdLayer["nomosInterface"]>["aggregates"][string];
647
+ const dirs = new Map<string, Dir>();
648
+ const aggsI = new Map<string, Agg>();
649
+ let domainI: string | undefined;
650
+ let domainSidI: string | undefined;
651
+ for (const l of stack) {
652
+ if (l.nomosInterface === undefined) continue;
653
+ domainI = l.nomosInterface.domain;
654
+ domainSidI = l.nomosInterface.domainSid;
655
+ for (const [id, spec] of Object.entries(l.nomosInterface.directives)) dirs.set(id, spec);
656
+ for (const [sid, spec] of Object.entries(l.nomosInterface.aggregates)) aggsI.set(sid, spec);
657
+ }
658
+ if (domainI !== undefined) {
659
+ const directivesRec: Record<string, Dir> = {};
660
+ for (const k of [...dirs.keys()].sort()) directivesRec[k] = dirs.get(k)!;
661
+ const aggregatesRec: Record<string, Agg> = {};
662
+ for (const k of [...aggsI.keys()].sort()) aggregatesRec[k] = aggsI.get(k)!;
663
+ iface = {
664
+ v: 1,
665
+ domain: domainI,
666
+ ...(domainSidI !== undefined ? { domainSid: domainSidI } : {}),
667
+ directives: directivesRec,
668
+ aggregates: aggregatesRec,
669
+ };
670
+ }
671
+ }
672
+ const identityManifest = foldIdentityManifest(stack, stableIds);
673
+ const readProjection = foldReadProjection(stack);
512
674
  // The captured-read gate (#58): queries + per-directive read sets merged
513
675
  // later-wins, SORTED keys (matching the composed emission).
514
676
  let readGate: UsdLayer["nomosReadGate"];
@@ -606,10 +768,13 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
606
768
  return {
607
769
  path,
608
770
  prims: [...prims.keys()].sort().map((p) => prims.get(p)!),
771
+ ...(identityManifest !== undefined ? { nomosIdentityManifest: identityManifest } : {}),
772
+ ...(readProjection !== undefined ? { nomosReadProjection: readProjection } : {}),
609
773
  ...(queries !== undefined ? { queries } : {}),
610
774
  ...(deriveds !== undefined ? { deriveds } : {}),
611
775
  ...(combineds !== undefined ? { combineds } : {}),
612
776
  ...(stableIds !== undefined ? { nomosStableIds: stableIds } : {}),
777
+ ...(iface !== undefined ? { nomosInterface: iface } : {}),
613
778
  ...(readGate !== undefined ? { nomosReadGate: readGate } : {}),
614
779
  ...(readVisibility !== undefined ? { nomosReadVisibility: readVisibility } : {}),
615
780
  ...(encryption !== undefined ? { nomosEncryption: encryption } : {}),
@@ -733,7 +898,10 @@ function attrStringArray(prim: ParsedPrim, name: string): string[] {
733
898
  export function parseUsdaDocument(text: string): UsdDocument {
734
899
  const prims = parseUsdaPrims(text);
735
900
  const domainByScope = new Map<string, string>();
901
+ const identityManifestByDomain = new Map<string, UsdLayer["nomosIdentityManifest"]>();
902
+ const readProjectionByDomain = new Map<string, UsdLayer["nomosReadProjection"]>();
736
903
  const stableIdsByDomain = new Map<string, UsdLayer["nomosStableIds"]>();
904
+ const interfaceByDomain = new Map<string, UsdLayer["nomosInterface"]>();
737
905
  const readGateByDomain = new Map<string, UsdLayer["nomosReadGate"]>();
738
906
  const readVisibilityByDomain = new Map<string, UsdLayer["nomosReadVisibility"]>();
739
907
  const encryptionByDomain = new Map<string, UsdLayer["nomosEncryption"]>();
@@ -744,11 +912,24 @@ export function parseUsdaDocument(text: string): UsdDocument {
744
912
  if (p.attrs.get("nomos:kind") === "domain") {
745
913
  const domain = attrString(p, "nomos:domain");
746
914
  domainByScope.set(p.path, domain);
747
- // STABLE IDS + the captured-read gate (#58): the hex-carried layer-level keys.
915
+ // Holon-owned manifests + gates: the hex-carried layer-level law keys.
916
+ const idHex = p.attrs.get("nomos:identityManifestHex");
917
+ if (typeof idHex === "string") {
918
+ identityManifestByDomain.set(domain, JSON.parse(unhexUtf8(idHex)) as UsdLayer["nomosIdentityManifest"]);
919
+ }
920
+ const rpHex = p.attrs.get("nomos:readProjectionHex");
921
+ if (typeof rpHex === "string") {
922
+ readProjectionByDomain.set(domain, JSON.parse(unhexUtf8(rpHex)) as UsdLayer["nomosReadProjection"]);
923
+ }
748
924
  const sidHex = p.attrs.get("nomos:stableIdsHex");
749
925
  if (typeof sidHex === "string") {
750
926
  stableIdsByDomain.set(domain, JSON.parse(unhexUtf8(sidHex)) as UsdLayer["nomosStableIds"]);
751
927
  }
928
+ // THE STRUCTURAL INTERFACE (#72): the on-ledger OFFER, hex-carried per domain layer.
929
+ const ifaceHex = p.attrs.get("nomos:interfaceHex");
930
+ if (typeof ifaceHex === "string") {
931
+ interfaceByDomain.set(domain, JSON.parse(unhexUtf8(ifaceHex)) as UsdLayer["nomosInterface"]);
932
+ }
752
933
  const rgHex = p.attrs.get("nomos:readGateHex");
753
934
  if (typeof rgHex === "string") {
754
935
  readGateByDomain.set(domain, JSON.parse(unhexUtf8(rgHex)) as UsdLayer["nomosReadGate"]);
@@ -865,7 +1046,10 @@ export function parseUsdaDocument(text: string): UsdDocument {
865
1046
  const layers: UsdLayer[] = [...buckets.keys()]
866
1047
  .map((domain) => {
867
1048
  const b = buckets.get(domain)!;
1049
+ const identity = identityManifestByDomain.get(domain);
1050
+ const readProjection = readProjectionByDomain.get(domain);
868
1051
  const sids = stableIdsByDomain.get(domain);
1052
+ const iface = interfaceByDomain.get(domain);
869
1053
  const rg = readGateByDomain.get(domain);
870
1054
  const rv = readVisibilityByDomain.get(domain);
871
1055
  const enc = encryptionByDomain.get(domain);
@@ -875,10 +1059,13 @@ export function parseUsdaDocument(text: string): UsdDocument {
875
1059
  return {
876
1060
  path: `/Nomos/${domain}`,
877
1061
  prims: [...b.prims].sort(byPath),
1062
+ ...(identity !== undefined ? { nomosIdentityManifest: identity } : {}),
1063
+ ...(readProjection !== undefined ? { nomosReadProjection: readProjection } : {}),
878
1064
  ...(b.queries.length > 0 ? { queries: [...b.queries].sort(byId) } : {}),
879
1065
  ...(b.deriveds.length > 0 ? { deriveds: [...b.deriveds].sort(byId) } : {}),
880
1066
  ...(b.combineds.length > 0 ? { combineds: [...b.combineds].sort(byId) } : {}),
881
1067
  ...(sids !== undefined ? { nomosStableIds: sids } : {}),
1068
+ ...(iface !== undefined ? { nomosInterface: iface } : {}),
882
1069
  ...(rg !== undefined ? { nomosReadGate: rg } : {}),
883
1070
  ...(rv !== undefined ? { nomosReadVisibility: rv } : {}),
884
1071
  ...(enc !== undefined ? { nomosEncryption: enc } : {}),