@githolon/dsl 0.31.1 → 0.33.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/dart/lib/src/errors.dart +105 -1
- package/dart/lib/src/webview_bridge.dart +6 -4
- package/dart/test/webview_bridge_test.dart +65 -0
- package/package.json +1 -1
- package/src/build_package.ts +25 -0
- package/src/codegen_dart.ts +347 -3
- package/src/codegen_ts.ts +74 -1
- package/src/compile_package_main.ts +53 -3
- package/src/fields.ts +84 -0
- package/src/framework/domain_lifecycle.ts +21 -2
- package/src/index.ts +19 -0
- package/src/relation_composition.ts +286 -0
- package/src/usd.ts +27 -1
- package/src/usd_layers.ts +16 -1
- package/src/value_object.ts +413 -0
package/src/fields.ts
CHANGED
|
@@ -72,6 +72,62 @@ export const evidenceRefSchema: z.ZodType<EvidenceRefValue> = z.object({
|
|
|
72
72
|
storageClass: z.enum(["git", "external"]),
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
+
// ── VALUE OBJECTS (composable value objects — `value_object.ts`) ─────────────────────
|
|
76
|
+
// The descriptor types live HERE (dependency-free) so `Field` can carry the metadata
|
|
77
|
+
// without a `fields.ts → value_object.ts` cycle; `value_object.ts` re-exports them.
|
|
78
|
+
|
|
79
|
+
/** The RFC 7946 geometry kinds a `geo.*` value object can declare (Point is the
|
|
80
|
+
* spatial-index-blessed one; see `spatial.ts`). */
|
|
81
|
+
export type GeometryKind = "Point" | "LineString" | "Polygon";
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* One field of a value-object DESCRIPTOR — the canonical, JSON-schema-ish shape that
|
|
85
|
+
* rides the package IR (`nomosValueObjects`) and drives TS/Dart codegen. `optional`
|
|
86
|
+
* is PRESENT only when true (omit-when-false — descriptor bytes are deterministic).
|
|
87
|
+
* `array.items` is a nested descriptor WITHOUT `optional` (an array of optionals is
|
|
88
|
+
* not expressible — the v1 constrained set).
|
|
89
|
+
*/
|
|
90
|
+
export type ValueObjectFieldDescriptor =
|
|
91
|
+
| { readonly type: "string"; readonly optional?: true }
|
|
92
|
+
| { readonly type: "number"; readonly optional?: true }
|
|
93
|
+
| { readonly type: "boolean"; readonly optional?: true }
|
|
94
|
+
| { readonly type: "enum"; readonly values: readonly string[]; readonly optional?: true }
|
|
95
|
+
| { readonly type: "array"; readonly items: ValueObjectFieldDescriptor; readonly optional?: true }
|
|
96
|
+
/** A nested value object, referenced BY NAME (its own descriptor rides alongside). */
|
|
97
|
+
| { readonly type: "object"; readonly ref: string; readonly optional?: true }
|
|
98
|
+
/** A nested GEO value object (RFC 7946 geometry), referenced by name. */
|
|
99
|
+
| { readonly type: "geometry"; readonly ref: string; readonly geometry: GeometryKind; readonly optional?: true };
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The canonical descriptor of ONE named value object. A structural VO carries
|
|
103
|
+
* `fields` (keys SORTED — byte-determinism); a `geo.*` VO carries `geometry`
|
|
104
|
+
* instead (its shape is fixed by RFC 7946, not authored).
|
|
105
|
+
*/
|
|
106
|
+
export interface ValueObjectDescriptor {
|
|
107
|
+
readonly name: string;
|
|
108
|
+
readonly fields?: Readonly<Record<string, ValueObjectFieldDescriptor>>;
|
|
109
|
+
readonly geometry?: GeometryKind;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The value-object metadata a VO-typed `Field` carries (`ValueObject.field()` sets it).
|
|
114
|
+
* FRONTEND/IR-ONLY: it never enters the kernel wire `Schema` (driver-only — see
|
|
115
|
+
* `encodeKernelSchema`) nor the canonical manifest, so a VO field's domainHash is
|
|
116
|
+
* IDENTICAL to a hand-rolled `t.jsonObject()` field. The additive `nomosValueObjects`
|
|
117
|
+
* USD key (built by `composeDomainModule`/`usd.ts` FROM this metadata) is what makes
|
|
118
|
+
* the structure visible to tooling.
|
|
119
|
+
*/
|
|
120
|
+
export interface ValueObjectFieldMeta {
|
|
121
|
+
/** The VO's declared name (the generated TS interface / Dart class name). */
|
|
122
|
+
readonly name: string;
|
|
123
|
+
/** This VO's own canonical descriptor. */
|
|
124
|
+
readonly descriptor: ValueObjectDescriptor;
|
|
125
|
+
/** Every TRANSITIVELY nested VO's descriptor (self excluded), sorted by name. */
|
|
126
|
+
readonly dependencies: readonly ValueObjectDescriptor[];
|
|
127
|
+
/** Present iff this VO is a `geo.*` geometry (the spatial-index pairing check). */
|
|
128
|
+
readonly geometry?: GeometryKind;
|
|
129
|
+
}
|
|
130
|
+
|
|
75
131
|
export interface Field<T = unknown, K extends FieldKind = FieldKind> {
|
|
76
132
|
/** The kind tag. `K` is the LITERAL kind so the query DSL can constrain `.ref`
|
|
77
133
|
* to ONLY `kind:'ref'` fields at compile time (`t.ref` → `Field<string,'ref'>`).
|
|
@@ -113,6 +169,15 @@ export interface Field<T = unknown, K extends FieldKind = FieldKind> {
|
|
|
113
169
|
* `encodeKernelSchema`) — so it changes NO hash (identical to `mapValueKind`/`isOptional`).
|
|
114
170
|
*/
|
|
115
171
|
readonly jsonShape?: "object";
|
|
172
|
+
/**
|
|
173
|
+
* For a `json`-kind field lowered from a NAMED VALUE OBJECT (`ValueObject.field()`):
|
|
174
|
+
* the VO metadata (name + canonical descriptor + nested-VO dependencies). Drives the
|
|
175
|
+
* generated TS interface / Dart class and the additive `nomosValueObjects` USD key.
|
|
176
|
+
* FRONTEND/IR-ONLY — never in the kernel wire `Schema` (driver-only), so the field's
|
|
177
|
+
* canonical-manifest bytes are identical to a plain `t.jsonObject()` (see
|
|
178
|
+
* {@link ValueObjectFieldMeta}). `undefined` for every non-VO field.
|
|
179
|
+
*/
|
|
180
|
+
readonly valueObject?: ValueObjectFieldMeta;
|
|
116
181
|
/**
|
|
117
182
|
* Whether a folded aggregate may LACK this field. A SCALAR field
|
|
118
183
|
* (`string`/`int`/`enum`/`ref`/`json`) that a `.creates` directive does not
|
|
@@ -184,6 +249,7 @@ function makeField<T, K extends FieldKind = FieldKind>(
|
|
|
184
249
|
isOptional?: boolean;
|
|
185
250
|
mapValueKind?: FieldKind;
|
|
186
251
|
jsonShape?: "object";
|
|
252
|
+
valueObject?: ValueObjectFieldMeta;
|
|
187
253
|
encrypted?: true;
|
|
188
254
|
meta?: true;
|
|
189
255
|
facet?: FieldFacet;
|
|
@@ -201,6 +267,7 @@ function makeField<T, K extends FieldKind = FieldKind>(
|
|
|
201
267
|
...(extra.viaField !== undefined ? { viaField: extra.viaField } : {}),
|
|
202
268
|
...(extra.mapValueKind !== undefined ? { mapValueKind: extra.mapValueKind } : {}),
|
|
203
269
|
...(extra.jsonShape !== undefined ? { jsonShape: extra.jsonShape } : {}),
|
|
270
|
+
...(extra.valueObject !== undefined ? { valueObject: extra.valueObject } : {}),
|
|
204
271
|
...(extra.encrypted ? { isEncrypted: true as const } : {}),
|
|
205
272
|
...(extra.meta ? { isMeta: true as const } : {}),
|
|
206
273
|
merge(d: Driver) {
|
|
@@ -401,3 +468,20 @@ function refImpl(
|
|
|
401
468
|
}
|
|
402
469
|
return makeField<string, "ref">("ref", z.string(), Conflict, { refAggregateId: a.id });
|
|
403
470
|
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* INTERNAL (`value_object.ts` — `ValueObject.field()`): construct the JSON-OBJECT value
|
|
474
|
+
* leaf a named value object lowers to. Storage/merge-identical to `t.jsonObject()`
|
|
475
|
+
* (kind `"json"`, `jsonShape:"object"`, driver Lww — the whole VO replaces WHOLESALE;
|
|
476
|
+
* per-subfield merge is what aggregates/maps are for), PLUS the {@link
|
|
477
|
+
* ValueObjectFieldMeta} that names the VO for codegen + the `nomosValueObjects` IR key.
|
|
478
|
+
* The zod is the VO's TYPED object schema, so `set(handle, field, value)` and payload
|
|
479
|
+
* embedding validate the real structure. Not part of `t.*` — reach it via
|
|
480
|
+
* `valueObject(...)`/`geo.*`'s `.field()`.
|
|
481
|
+
*/
|
|
482
|
+
export function valueObjectField<T>(
|
|
483
|
+
zod: z.ZodType<T>,
|
|
484
|
+
valueObject: ValueObjectFieldMeta,
|
|
485
|
+
): Field<T, "json"> {
|
|
486
|
+
return makeField<T, "json">("json", zod, Lww, { jsonShape: "object", valueObject });
|
|
487
|
+
}
|
|
@@ -28,6 +28,7 @@ export const DOMAIN_INSTALLATION_PHASES = [
|
|
|
28
28
|
"Disabled",
|
|
29
29
|
"Superseded",
|
|
30
30
|
"Retiring",
|
|
31
|
+
"Retired",
|
|
31
32
|
"Failed",
|
|
32
33
|
] as const;
|
|
33
34
|
export type DomainInstallationPhase = (typeof DOMAIN_INSTALLATION_PHASES)[number];
|
|
@@ -48,6 +49,7 @@ export const DomainInstallation = aggregate("DomainInstallation", {
|
|
|
48
49
|
"metadata.generation": t.int().merge(Lww),
|
|
49
50
|
"status.phase": t.enum(DOMAIN_INSTALLATION_PHASES).merge(Lww),
|
|
50
51
|
"status.observedGeneration": t.int().merge(Lww),
|
|
52
|
+
"status.supersededBy": t.string().merge(Lww).optional(),
|
|
51
53
|
"status.conditions": t.map(t.json()).merge(MapOf(Lww)),
|
|
52
54
|
});
|
|
53
55
|
|
|
@@ -77,6 +79,13 @@ export const installDomain = directive("installDomain")
|
|
|
77
79
|
authorityScope: z.string().default("workspace/root"),
|
|
78
80
|
dependencies: z.array(z.string()).default([]),
|
|
79
81
|
supersedes: z.string().optional(),
|
|
82
|
+
// THE FRONTIER TRANSITION (install-lifecycle): `replaces` names a PRIOR install this
|
|
83
|
+
// deploy makes current UNEQUIVOCALLY — the prior install is flipped out of the served
|
|
84
|
+
// frontier (`Superseded`, or `Retired` when `retireReplaced`) and gets the reverse
|
|
85
|
+
// `status.supersededBy` edge to this install. Absent ⇒ install-BESIDE (backward-compat:
|
|
86
|
+
// byte-identical ops to the pre-lifecycle plan). `replaces` also sets `spec.supersedes`.
|
|
87
|
+
replaces: z.string().optional(),
|
|
88
|
+
retireReplaced: z.boolean().default(false),
|
|
80
89
|
finalizers: z.array(z.string()).default([]),
|
|
81
90
|
}),
|
|
82
91
|
)
|
|
@@ -97,8 +106,18 @@ export const installDomain = directive("installDomain")
|
|
|
97
106
|
if (p.dependencies.length > 0) {
|
|
98
107
|
ops.push(addToSet(install, "spec.dependencies", p.dependencies));
|
|
99
108
|
}
|
|
100
|
-
|
|
101
|
-
|
|
109
|
+
// The predecessor back-link: `supersedes` (explicit) or `replaces` (the actionable form).
|
|
110
|
+
const supersedes = p.supersedes ?? p.replaces;
|
|
111
|
+
if (supersedes !== undefined) {
|
|
112
|
+
ops.push(set(install, "spec.supersedes", supersedes));
|
|
113
|
+
}
|
|
114
|
+
// The transition on the PRIOR install: drop it from the served frontier + point it here.
|
|
115
|
+
if (p.replaces !== undefined) {
|
|
116
|
+
const replaced = domainInstallationInstance(p.replaces);
|
|
117
|
+
ops.push(
|
|
118
|
+
set(replaced, "status.phase", p.retireReplaced ? "Retired" : "Superseded"),
|
|
119
|
+
);
|
|
120
|
+
ops.push(set(replaced, "status.supersededBy", p.domainHash));
|
|
102
121
|
}
|
|
103
122
|
if (p.finalizers.length > 0) {
|
|
104
123
|
ops.push(addToSet(install, "spec.finalizers", p.finalizers));
|
package/src/index.ts
CHANGED
|
@@ -40,6 +40,25 @@ export {
|
|
|
40
40
|
secretField,
|
|
41
41
|
publicField,
|
|
42
42
|
} from "./fields.js";
|
|
43
|
+
// FIRST-CLASS COMPOSABLE VALUE OBJECTS (`value_object.ts`): named, reusable, nestable
|
|
44
|
+
// structured values — ONE definition yields the zod (payload validation), the TS type,
|
|
45
|
+
// the generated Dart class, and the `nomosValueObjects` IR descriptor. A VO lowers to a
|
|
46
|
+
// single Lww-class json-object leaf (wholesale-replace merge — no per-subfield merge).
|
|
47
|
+
// `geo.*` are the RFC 7946 geometry VOs (pair `geo.point()` with `spatial(...)`).
|
|
48
|
+
export {
|
|
49
|
+
valueObject,
|
|
50
|
+
geo,
|
|
51
|
+
type ValueObject,
|
|
52
|
+
type ValueObjectValue,
|
|
53
|
+
type OptionalValueObject,
|
|
54
|
+
type ValueObjectShapeEntry,
|
|
55
|
+
type ValueObjectDescriptor,
|
|
56
|
+
type ValueObjectFieldDescriptor,
|
|
57
|
+
type GeometryKind,
|
|
58
|
+
type GeoPointValue,
|
|
59
|
+
type GeoLineStringValue,
|
|
60
|
+
type GeoPolygonValue,
|
|
61
|
+
} from "./value_object.js";
|
|
43
62
|
export {
|
|
44
63
|
type PrincipalEvidence,
|
|
45
64
|
type AttributionPrincipal,
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
|
|
2
|
+
// remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
|
|
3
|
+
// wasm32-wasip1 artifact {kernel · projection · embedded QuickJS engine}.
|
|
4
|
+
//
|
|
5
|
+
// COMPILE-TIME USD-LAYER COMPOSITION for SHARED (global) relation types + the
|
|
6
|
+
// fail-closed DIVERGENCE GUARD.
|
|
7
|
+
//
|
|
8
|
+
// THE MODEL. When a package bundles multiple domains, each domain lowers its own
|
|
9
|
+
// `relationType` decls into its own `UsdLayer.nomosRelations`. A NON-global type is
|
|
10
|
+
// auto-qualified by its owning domain (`estate` → `estate__workspace`), so two
|
|
11
|
+
// domains that each declare the same bare type NEVER collide — their qualified ids
|
|
12
|
+
// differ, and they stay separate layers. A `.asGlobal()` type (a kernel-coupled
|
|
13
|
+
// framework type, e.g. governance/read-visibility `workspace`) stays a BARE shared
|
|
14
|
+
// name, so MULTIPLE domains that each contribute to one global type land on the SAME
|
|
15
|
+
// canonical id. That cross-domain merge USED to happen only at the KERNEL (runtime
|
|
16
|
+
// `merge_layers`, a blunt per-type last-wins that SILENTLY DROPS the losing layer's
|
|
17
|
+
// relations — the bug that broke co2's `createSite`). We move it to COMPILE time, in
|
|
18
|
+
// TS, using USD's composition MODEL (LIVRPS sublayer strength):
|
|
19
|
+
//
|
|
20
|
+
// * COMPOSE (deliverable #1): for each SHARED global type, UNION the relations
|
|
21
|
+
// across the contributing domains — a prim-set union, so EVERY domain's relations
|
|
22
|
+
// survive (nothing is dropped). The composed union is written back into every
|
|
23
|
+
// contributing layer so the downstream kernel merge (union OR last-wins) yields
|
|
24
|
+
// the full union regardless of its policy. This is idempotent and byte-stable.
|
|
25
|
+
//
|
|
26
|
+
// * GUARD (deliverable #2): when two or more domains declare the SAME relation on a
|
|
27
|
+
// SHARED global type with DIVERGENT rewrites (co2's `workspace.admin`: `self()` in
|
|
28
|
+
// estate/platform vs `union(self, computed(creator))` in home), REFUSE AT COMPILE
|
|
29
|
+
// with a scalpel message naming the type, the relation, and each domain's rewrite.
|
|
30
|
+
// Single-owner relations and byte-identical agreeing opinions PASS (union). This
|
|
31
|
+
// turns the kernel's blunt `None` / "no-relation-schema" into a precise compile
|
|
32
|
+
// refusal — the era rule ("the law is the era") is preserved because a package with
|
|
33
|
+
// no shared global type composes to itself, byte-identical.
|
|
34
|
+
//
|
|
35
|
+
// SUBLAYER STRENGTH ORDER (documented, deliverable #1): the package's domain order is
|
|
36
|
+
// WEAK → STRONG (Nomos module-fold notation — later wins), which maps onto USD
|
|
37
|
+
// `subLayers` STRONGEST-FIRST by reversal (the same left-biased monoid, opposite list
|
|
38
|
+
// notation — the convention `usd_layers.ts::layeredRootUsda` already records). For a
|
|
39
|
+
// NON-divergent shared type the union is order-INDEPENDENT (every domain's opinion on a
|
|
40
|
+
// given relation is identical, so which layer "wins" is immaterial); order matters ONLY
|
|
41
|
+
// to the visualization flatten, never to the composed IR.
|
|
42
|
+
|
|
43
|
+
import type { UsdLayer } from "./usd.js";
|
|
44
|
+
|
|
45
|
+
/** A single relation's definition as it rides in `nomosRelations` (`{rewrite, allowed}`). */
|
|
46
|
+
type RelDef = NonNullable<UsdLayer["nomosRelations"]>["types"][string]["relations"][string];
|
|
47
|
+
/** One relation TYPE's spec (`{relations, caps?}`). */
|
|
48
|
+
type RelSpec = NonNullable<UsdLayer["nomosRelations"]>["types"][string];
|
|
49
|
+
|
|
50
|
+
/** Strip the `/Nomos/` prim-scope prefix to recover the owning domain key from a layer path. */
|
|
51
|
+
export function domainOfLayerPath(path: string): string {
|
|
52
|
+
return path.startsWith("/Nomos/") ? path.slice("/Nomos/".length) : path;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Render a rewrite AST to the author-facing surface syntax (`self()` / `computed(x)` /
|
|
57
|
+
* `union(...)` / `via(t, c)` / …) — the readable form the scalpel message + the layered
|
|
58
|
+
* `.usda` provenance attr carry. A `null`/absent rewrite is a pure-direct relation (`this`).
|
|
59
|
+
*/
|
|
60
|
+
export function renderRewrite(rw: unknown): string {
|
|
61
|
+
if (rw === null || rw === undefined) return "this";
|
|
62
|
+
const r = rw as { kind?: string; [k: string]: unknown };
|
|
63
|
+
switch (r.kind) {
|
|
64
|
+
case "this":
|
|
65
|
+
return "self()";
|
|
66
|
+
case "computed":
|
|
67
|
+
return `computed(${String(r.relation)})`;
|
|
68
|
+
case "ttu":
|
|
69
|
+
return `${r.all ? "viaAll" : "via"}(${String(r.tupleset)}, ${String(r.computed)})`;
|
|
70
|
+
case "union":
|
|
71
|
+
return `union(${(r.children as unknown[]).map(renderRewrite).join(", ")})`;
|
|
72
|
+
case "intersection":
|
|
73
|
+
return `intersection(${(r.children as unknown[]).map(renderRewrite).join(", ")})`;
|
|
74
|
+
case "exclusion":
|
|
75
|
+
return `exclusion(${renderRewrite(r.base)}, ${renderRewrite(r.subtract)})`;
|
|
76
|
+
default:
|
|
77
|
+
return JSON.stringify(rw);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A per-relation divergence, gathered for the scalpel message. */
|
|
82
|
+
interface RelationConflict {
|
|
83
|
+
readonly type: string;
|
|
84
|
+
readonly relation: string;
|
|
85
|
+
/** Each contributing domain's opinion, in package (weak→strong) order. */
|
|
86
|
+
readonly opinions: { readonly domain: string; readonly rewrite: string }[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Format the accumulated divergences into the fail-closed refusal (mirrors `failclosed_check.py`). */
|
|
90
|
+
function scalpelMessage(conflicts: readonly RelationConflict[]): string {
|
|
91
|
+
const lines: string[] = [
|
|
92
|
+
`Refusing to compose the package: shared relation type(s) carry DIVERGENT rewrites across ` +
|
|
93
|
+
`domains — the composed flatten would SILENTLY take the strongest layer's opinion (LIVRPS ` +
|
|
94
|
+
`last-wins), so this is a fail-closed compile refusal, not a silent pick.`,
|
|
95
|
+
``,
|
|
96
|
+
];
|
|
97
|
+
for (const c of conflicts) {
|
|
98
|
+
lines.push(` relation type "${c.type}" relation "${c.relation}" — ${c.opinions.length} divergent opinions:`);
|
|
99
|
+
const w = Math.max(...c.opinions.map((o) => o.domain.length));
|
|
100
|
+
for (const o of c.opinions) lines.push(` · ${o.domain.padEnd(w)} ${o.rewrite}`);
|
|
101
|
+
}
|
|
102
|
+
lines.push(
|
|
103
|
+
``,
|
|
104
|
+
`Fix: give the type ONE canonical definition of each divergent relation (the domains must ` +
|
|
105
|
+
`agree, byte-for-byte), or use DISTINCT / domain-qualified relation types (drop \`.asGlobal()\` ` +
|
|
106
|
+
`so each domain's type auto-qualifies and never shares an id).`,
|
|
107
|
+
);
|
|
108
|
+
return lines.join("\n");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Compose the SHARED (global, bare-id) relation types across a package's layers — UNION the
|
|
113
|
+
* relations per shared type, GUARD divergent rewrites fail-closed, and write the union back into
|
|
114
|
+
* every contributing layer. Non-shared (single-owner / auto-qualified) types are untouched, so a
|
|
115
|
+
* package with no shared global type returns byte-identical layers (the era rule). Deterministic:
|
|
116
|
+
* sorted keys, sorted diagnostics.
|
|
117
|
+
*
|
|
118
|
+
* @throws a scalpel `Error` naming the type, relation, and each domain's rewrite on any divergence.
|
|
119
|
+
*/
|
|
120
|
+
export function composeSharedRelationTypes(layers: readonly UsdLayer[]): UsdLayer[] {
|
|
121
|
+
// type id → the layers (domains) that contribute it, in package (weak→strong) order.
|
|
122
|
+
const contributors = new Map<string, { domain: string; spec: RelSpec }[]>();
|
|
123
|
+
for (const layer of layers) {
|
|
124
|
+
const rels = layer.nomosRelations;
|
|
125
|
+
if (rels === undefined) continue;
|
|
126
|
+
for (const [type, spec] of Object.entries(rels.types)) {
|
|
127
|
+
const arr = contributors.get(type) ?? [];
|
|
128
|
+
arr.push({ domain: domainOfLayerPath(layer.path), spec });
|
|
129
|
+
contributors.set(type, arr);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const composed = new Map<string, RelSpec>();
|
|
134
|
+
const conflicts: RelationConflict[] = [];
|
|
135
|
+
for (const type of [...contributors.keys()].sort()) {
|
|
136
|
+
const contribs = contributors.get(type)!;
|
|
137
|
+
// A type contributed by ONE domain is single-owner — nothing to compose (untouched).
|
|
138
|
+
if (contribs.length < 2) continue;
|
|
139
|
+
|
|
140
|
+
// caps must agree across the contributing domains (a divergent bound is a hard error).
|
|
141
|
+
let caps: RelSpec["caps"];
|
|
142
|
+
for (const c of contribs) {
|
|
143
|
+
if (c.spec.caps === undefined) continue;
|
|
144
|
+
if (caps === undefined) caps = c.spec.caps;
|
|
145
|
+
else if (caps.maxDepth !== c.spec.caps.maxDepth || caps.maxFanout !== c.spec.caps.maxFanout) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Refusing to compose the package: shared relation type "${type}" is declared with ` +
|
|
148
|
+
`CONFLICTING caps across domains (${contribs.map((x) => x.domain).join(", ")}). Give the ` +
|
|
149
|
+
`type one canonical \`caps\`.`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Gather every domain's opinion on each relation (in package order).
|
|
155
|
+
const opinions = new Map<string, { domain: string; def: RelDef }[]>();
|
|
156
|
+
for (const c of contribs) {
|
|
157
|
+
for (const [rel, def] of Object.entries(c.spec.relations)) {
|
|
158
|
+
const arr = opinions.get(rel) ?? [];
|
|
159
|
+
arr.push({ domain: c.domain, def });
|
|
160
|
+
opinions.set(rel, arr);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const unioned: Record<string, RelDef> = {};
|
|
165
|
+
for (const rel of [...opinions.keys()].sort()) {
|
|
166
|
+
const ops = opinions.get(rel)!;
|
|
167
|
+
const distinct = new Set(ops.map((o) => JSON.stringify(o.def)));
|
|
168
|
+
if (distinct.size > 1) {
|
|
169
|
+
conflicts.push({
|
|
170
|
+
type,
|
|
171
|
+
relation: rel,
|
|
172
|
+
opinions: ops.map((o) => ({ domain: o.domain, rewrite: renderRewrite((o.def as { rewrite: unknown }).rewrite) })),
|
|
173
|
+
});
|
|
174
|
+
} else {
|
|
175
|
+
unioned[rel] = ops[0]!.def; // agreeing opinions / single-owner relation — keep it
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Sorted relation keys (byte-determinism against the per-layer emission).
|
|
179
|
+
const relations: Record<string, RelDef> = {};
|
|
180
|
+
for (const k of Object.keys(unioned).sort()) relations[k] = unioned[k]!;
|
|
181
|
+
composed.set(type, { relations, ...(caps !== undefined ? { caps } : {}) });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (conflicts.length > 0) throw new Error(scalpelMessage(conflicts));
|
|
185
|
+
if (composed.size === 0) return [...layers]; // nothing shared — byte-identical (era rule)
|
|
186
|
+
|
|
187
|
+
// Write the composed union back into every contributing layer, keeping type keys sorted.
|
|
188
|
+
return layers.map((layer) => {
|
|
189
|
+
const rels = layer.nomosRelations;
|
|
190
|
+
if (rels === undefined) return layer;
|
|
191
|
+
let touched = false;
|
|
192
|
+
const merged: Record<string, RelSpec> = {};
|
|
193
|
+
for (const [type, spec] of Object.entries(rels.types)) {
|
|
194
|
+
const c = composed.get(type);
|
|
195
|
+
if (c !== undefined) {
|
|
196
|
+
merged[type] = c;
|
|
197
|
+
touched = true;
|
|
198
|
+
} else {
|
|
199
|
+
merged[type] = spec;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (!touched) return layer;
|
|
203
|
+
const sorted: Record<string, RelSpec> = {};
|
|
204
|
+
for (const k of Object.keys(merged).sort()) sorted[k] = merged[k]!;
|
|
205
|
+
return { ...layer, nomosRelations: { v: 1 as const, types: sorted } };
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── the REAL layered `.usda` emission (deliverable #3 — for usdview / usdcat --flatten) ──────────
|
|
210
|
+
|
|
211
|
+
/** One domain's contribution to the layered relation emission: its bare-id relation types. */
|
|
212
|
+
export interface RelationLayerInput {
|
|
213
|
+
readonly domain: string;
|
|
214
|
+
/** The domain's `nomosRelations.types` (UNCOMPOSED — the layers ARE the sublayers usdcat composes). */
|
|
215
|
+
readonly types: Record<string, RelSpec>;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** `"a"` → escaped USDA string literal (minimal — relation/rewrite tokens are `[A-Za-z0-9_.,()]`). */
|
|
219
|
+
function usda(s: string): string {
|
|
220
|
+
return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Emit ONE domain's relation opinions as a real `.usda` sublayer: `def "<Type>" { def "<relation>"
|
|
225
|
+
* { custom string nomos:rewrite = "…"; custom string nomos:from = "<domain>" } }`. Structured so
|
|
226
|
+
* `usdcat --flatten` composes the relation prims across sublayers (a stronger layer's `nomos:rewrite`
|
|
227
|
+
* wins; the relation SET is the union). Deterministic: types + relations sorted.
|
|
228
|
+
*/
|
|
229
|
+
export function emitRelationSublayerUsda(input: RelationLayerInput): string {
|
|
230
|
+
const lines: string[] = [`#usda 1.0`, `(`, ` doc = "Nomos relation layer — domain ${input.domain}"`, `)`, ``];
|
|
231
|
+
for (const type of Object.keys(input.types).sort()) {
|
|
232
|
+
lines.push(`def ${usda(type)}`, `{`);
|
|
233
|
+
const relations = input.types[type]!.relations;
|
|
234
|
+
for (const rel of Object.keys(relations).sort()) {
|
|
235
|
+
const def = relations[rel]! as { rewrite: unknown };
|
|
236
|
+
lines.push(
|
|
237
|
+
` def ${usda(rel)}`,
|
|
238
|
+
` {`,
|
|
239
|
+
` custom string nomos:rewrite = ${usda(renderRewrite(def.rewrite))}`,
|
|
240
|
+
` custom string nomos:from = ${usda(input.domain)}`,
|
|
241
|
+
` }`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
lines.push(`}`, ``);
|
|
245
|
+
}
|
|
246
|
+
return lines.join("\n");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** The result of {@link emitLayeredRelationUsda}: a root document + one sublayer file per domain. */
|
|
250
|
+
export interface LayeredRelationEmit {
|
|
251
|
+
/** The root `.usda` (`subLayers = [...]`, STRONGEST-FIRST) that usdcat --flatten composes. */
|
|
252
|
+
readonly root: string;
|
|
253
|
+
/** Per-domain sublayer files (`name` is a bare filename; write beside the root). */
|
|
254
|
+
readonly files: { readonly name: string; readonly text: string }[];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Emit a REAL layered relation `.usda` set: one sublayer per domain (its relation opinions as prims)
|
|
259
|
+
* + a root with `subLayers` STRONGEST-FIRST, so `usdcat --flatten <root>` composes the package's
|
|
260
|
+
* shared relation types. `inputs` are in package order (WEAK → STRONG); the root reverses them to
|
|
261
|
+
* strongest-first (the documented convention). Deterministic (sorted within each layer, stable file
|
|
262
|
+
* names). Additive build artifact — never feeds the package hash.
|
|
263
|
+
*/
|
|
264
|
+
export function emitLayeredRelationUsda(inputs: readonly RelationLayerInput[], packageName: string): LayeredRelationEmit {
|
|
265
|
+
const safe = (s: string) => s.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
266
|
+
const files = inputs.map((input) => ({
|
|
267
|
+
name: `${safe(input.domain)}.usda`,
|
|
268
|
+
text: emitRelationSublayerUsda(input),
|
|
269
|
+
}));
|
|
270
|
+
// subLayers is STRONGEST-FIRST → reverse the weak→strong package order.
|
|
271
|
+
const strongestFirst = [...files].reverse();
|
|
272
|
+
const root = [
|
|
273
|
+
`#usda 1.0`,
|
|
274
|
+
`(`,
|
|
275
|
+
` doc = """Nomos relation-composition layers for package ${packageName}. subLayers are`,
|
|
276
|
+
`STRONGEST-FIRST (USD layer-stack order); the Nomos domain list composes weak→strong (later`,
|
|
277
|
+
`wins). \`usdcat --flatten\` composes the shared relation types; the compiler's TS composition`,
|
|
278
|
+
`(composeSharedRelationTypes) is the fail-closed guard layered on top."""`,
|
|
279
|
+
` subLayers = [`,
|
|
280
|
+
...strongestFirst.map((f, i) => ` @./${f.name}@${i < strongestFirst.length - 1 ? "," : ""}`),
|
|
281
|
+
` ]`,
|
|
282
|
+
`)`,
|
|
283
|
+
``,
|
|
284
|
+
].join("\n");
|
|
285
|
+
return { root, files };
|
|
286
|
+
}
|
package/src/usd.ts
CHANGED
|
@@ -44,6 +44,7 @@ import type { DomainModule } from "./codegen_dart.js";
|
|
|
44
44
|
import { domainInterface } from "./shape_compat.js";
|
|
45
45
|
import { relationType, user, key, anyUser, qualifiedTypeId, qualifyRelationType } from "./relation_schema.js";
|
|
46
46
|
import { harvestPrincipalFlows } from "./wire_encode.js";
|
|
47
|
+
import { composeSharedRelationTypes } from "./relation_composition.js";
|
|
47
48
|
|
|
48
49
|
/** The referential marker of a directive prim (kernel `RefMode` wire string). */
|
|
49
50
|
export type RefMode = WireRefMode;
|
|
@@ -463,6 +464,19 @@ export interface UsdLayer {
|
|
|
463
464
|
readonly reason: string | null;
|
|
464
465
|
}[];
|
|
465
466
|
};
|
|
467
|
+
/**
|
|
468
|
+
* FIRST-CLASS VALUE OBJECTS (`value_object.ts`) — the canonical descriptors of every
|
|
469
|
+
* named value object this layer's aggregates carry (deduped, SORTED by name, nested
|
|
470
|
+
* VOs included), so tooling/visualizers SEE the structure a VO field stores. PURELY
|
|
471
|
+
* DESCRIPTIVE: the kernel reads no gate off it (a VO field folds exactly like a
|
|
472
|
+
* `t.jsonObject()` leaf — one wholesale-replace json value). OMITTED for VO-free law
|
|
473
|
+
* (byte-identical hash — the era rule); declaring a VO moves the PACKAGE hash only
|
|
474
|
+
* (the canonical manifest/domainHash is untouched — the VO is not wire law).
|
|
475
|
+
*/
|
|
476
|
+
readonly nomosValueObjects?: {
|
|
477
|
+
readonly v: 1;
|
|
478
|
+
readonly valueObjects: readonly import("./fields.js").ValueObjectDescriptor[];
|
|
479
|
+
};
|
|
466
480
|
}
|
|
467
481
|
|
|
468
482
|
/** The reserved variant name declaring a set's DEFAULT (USD has a default variant). */
|
|
@@ -994,6 +1008,13 @@ export function emitUsd(
|
|
|
994
1008
|
// conversion's declared flow {directiveId, sink, facet, reason}. Hashed into nomosFlowGate +
|
|
995
1009
|
// witnessed fail-closed at runtime (Slice B). Omit-when-empty (a principal-free domain is
|
|
996
1010
|
// byte-identical). SORTED for determinism.
|
|
1011
|
+
// FIRST-CLASS VALUE OBJECTS: the module's collected descriptors (composeDomainModule
|
|
1012
|
+
// dedupes + sorts them). Omit-when-empty (era rule — VO-free law is byte-identical).
|
|
1013
|
+
const moduleValueObjects = (l.module as { valueObjects?: readonly import("./fields.js").ValueObjectDescriptor[] }).valueObjects;
|
|
1014
|
+
const nomosValueObjects =
|
|
1015
|
+
moduleValueObjects !== undefined && moduleValueObjects.length > 0
|
|
1016
|
+
? { v: 1 as const, valueObjects: [...moduleValueObjects] }
|
|
1017
|
+
: undefined;
|
|
997
1018
|
const flowEndorse = [...l.module.directives]
|
|
998
1019
|
.flatMap((d) => harvestPrincipalFlows(d as never).map((f) => ({ directiveId: d.id, sink: f.sink, facet: f.facet, reason: f.reason })))
|
|
999
1020
|
.sort((a, b) =>
|
|
@@ -1039,6 +1060,7 @@ export function emitUsd(
|
|
|
1039
1060
|
...(nomosBirthGate !== undefined ? { nomosBirthGate } : {}),
|
|
1040
1061
|
...(nomosParentAttestedBirth !== undefined ? { nomosParentAttestedBirth } : {}),
|
|
1041
1062
|
...(nomosVerifiedFirstEnroll !== undefined ? { nomosVerifiedFirstEnroll } : {}),
|
|
1063
|
+
...(nomosValueObjects !== undefined ? { nomosValueObjects } : {}),
|
|
1042
1064
|
...(Object.keys(visAggregates).length > 0
|
|
1043
1065
|
? { nomosReadVisibility: { v: 1 as const, aggregates: visAggregates } }
|
|
1044
1066
|
: {}),
|
|
@@ -1060,7 +1082,11 @@ export function emitUsd(
|
|
|
1060
1082
|
};
|
|
1061
1083
|
})
|
|
1062
1084
|
.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
1063
|
-
|
|
1085
|
+
// COMPILE-TIME COMPOSITION + fail-closed GUARD for SHARED (global) relation types across the
|
|
1086
|
+
// package's per-domain layers: UNION every domain's relations on a shared bare-id type (so nothing
|
|
1087
|
+
// is silently dropped) and REFUSE on a divergent shared rewrite (the scalpel). Non-shared /
|
|
1088
|
+
// auto-qualified types are untouched → a package with no shared global type is byte-identical.
|
|
1089
|
+
return { layers: composeSharedRelationTypes(out) };
|
|
1064
1090
|
}
|
|
1065
1091
|
|
|
1066
1092
|
/** Two aggregate prims at the same path are identical iff same type + same schema. */
|
package/src/usd_layers.ts
CHANGED
|
@@ -70,6 +70,7 @@ import {
|
|
|
70
70
|
type UsdPrim,
|
|
71
71
|
type UsdQuery,
|
|
72
72
|
} from "./usd.js";
|
|
73
|
+
import { composeSharedRelationTypes } from "./relation_composition.js";
|
|
73
74
|
|
|
74
75
|
/** The layer-file format marker (customLayerData `nomos:format`). */
|
|
75
76
|
export const NOMOS_USD_LAYER_FORMAT = "nomos.usd-layer.v1";
|
|
@@ -189,6 +190,9 @@ function domainLayerAttrLines(domain: string, layer: UsdLayer, indent: string):
|
|
|
189
190
|
...(layer.nomosVerifiedFirstEnroll !== undefined
|
|
190
191
|
? [`${indent}custom string nomos:verifiedFirstEnrollHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosVerifiedFirstEnroll)))}`]
|
|
191
192
|
: []),
|
|
193
|
+
...(layer.nomosValueObjects !== undefined
|
|
194
|
+
? [`${indent}custom string nomos:valueObjectsHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosValueObjects)))}`]
|
|
195
|
+
: []),
|
|
192
196
|
];
|
|
193
197
|
}
|
|
194
198
|
|
|
@@ -865,7 +869,10 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
|
|
|
865
869
|
};
|
|
866
870
|
});
|
|
867
871
|
|
|
868
|
-
|
|
872
|
+
// Apply the SAME cross-layer shared-relation composition + guard the composed emission
|
|
873
|
+
// (`usd.ts::emitUsd`) applies, so the layered fold stays byte-homomorphic with it (a divergent
|
|
874
|
+
// shared global type refuses identically on both paths).
|
|
875
|
+
return { layers: composeSharedRelationTypes(out) };
|
|
869
876
|
}
|
|
870
877
|
|
|
871
878
|
// ── usda → IR (the round-trip parser for the pxr/usdcat conformance lane) ──────────
|
|
@@ -993,6 +1000,7 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
993
1000
|
const birthGateByDomain = new Map<string, UsdLayer["nomosBirthGate"]>();
|
|
994
1001
|
const parentAttestedBirthByDomain = new Map<string, UsdLayer["nomosParentAttestedBirth"]>();
|
|
995
1002
|
const verifiedFirstEnrollByDomain = new Map<string, UsdLayer["nomosVerifiedFirstEnroll"]>();
|
|
1003
|
+
const valueObjectsByDomain = new Map<string, UsdLayer["nomosValueObjects"]>();
|
|
996
1004
|
for (const p of prims) {
|
|
997
1005
|
if (p.attrs.get("nomos:kind") === "domain") {
|
|
998
1006
|
const domain = attrString(p, "nomos:domain");
|
|
@@ -1064,6 +1072,11 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
1064
1072
|
if (typeof vfeHex === "string") {
|
|
1065
1073
|
verifiedFirstEnrollByDomain.set(domain, JSON.parse(unhexUtf8(vfeHex)) as UsdLayer["nomosVerifiedFirstEnroll"]);
|
|
1066
1074
|
}
|
|
1075
|
+
// FIRST-CLASS VALUE OBJECTS: the hex-carried descriptor set (descriptive, not a gate).
|
|
1076
|
+
const voHex = p.attrs.get("nomos:valueObjectsHex");
|
|
1077
|
+
if (typeof voHex === "string") {
|
|
1078
|
+
valueObjectsByDomain.set(domain, JSON.parse(unhexUtf8(voHex)) as UsdLayer["nomosValueObjects"]);
|
|
1079
|
+
}
|
|
1067
1080
|
}
|
|
1068
1081
|
}
|
|
1069
1082
|
|
|
@@ -1165,6 +1178,7 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
1165
1178
|
const birthGate = birthGateByDomain.get(domain);
|
|
1166
1179
|
const parentAttestedBirth = parentAttestedBirthByDomain.get(domain);
|
|
1167
1180
|
const verifiedFirstEnroll = verifiedFirstEnrollByDomain.get(domain);
|
|
1181
|
+
const valueObjects = valueObjectsByDomain.get(domain);
|
|
1168
1182
|
return {
|
|
1169
1183
|
path: `/Nomos/${domain}`,
|
|
1170
1184
|
prims: [...b.prims].sort(byPath),
|
|
@@ -1185,6 +1199,7 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
1185
1199
|
...(birthGate !== undefined ? { nomosBirthGate: birthGate } : {}),
|
|
1186
1200
|
...(parentAttestedBirth !== undefined ? { nomosParentAttestedBirth: parentAttestedBirth } : {}),
|
|
1187
1201
|
...(verifiedFirstEnroll !== undefined ? { nomosVerifiedFirstEnroll: verifiedFirstEnroll } : {}),
|
|
1202
|
+
...(valueObjects !== undefined ? { nomosValueObjects: valueObjects } : {}),
|
|
1188
1203
|
};
|
|
1189
1204
|
})
|
|
1190
1205
|
.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|