@githolon/dsl 0.102.5 → 0.103.1
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/compile_package.mjs +43 -2
- package/compile_proof.d.mts +47 -0
- package/compile_proof.mjs +343 -0
- package/dart/CHANGELOG.md +14 -0
- package/dart/lib/src/dev_cloud.dart +8 -8
- package/dart/lib/src/nms_generated_runtime.dart +2 -0
- package/dart/lib/src/runner/node-entry.mjs +5539 -1228
- package/dart/lib/src/runner/offline-install.mjs +4578 -528
- package/dart/lib/src/test_harness.dart +206 -152
- package/dart/lib/src/webview_bridge.dart +239 -102
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart/test/acquire_session_credentials_test.dart +4 -3
- package/dart/test/acquire_session_test.dart +9 -0
- package/dart/test/adv_realm_interference_test.dart +7 -6
- package/dart/test/cloud_bound_topology_test.dart +3 -3
- package/dart/test/cluster_births_test.dart +4 -3
- package/dart/test/cluster_device_keyed_acceptance_test.dart +10 -16
- package/dart/test/cluster_local_autosync_test.dart +3 -2
- package/dart/test/cluster_scale_test.dart +3 -2
- package/dart/test/dlq_session_surface_test.dart +7 -6
- package/dart/test/first_app_gauntlet_test.dart +14 -21
- package/dart/test/fixtures/wedge/fake-install.mjs +1 -1
- package/dart/test/four_layer_nested_topology_test.dart +4 -4
- package/dart/test/harness_phase_timeout_test.dart +2 -2
- package/dart/test/local_sync_inert_test.dart +1 -1
- package/dart/test/parent_signer_recovery_test.dart +2 -2
- package/dart/test/sealing_client_test.dart +4 -3
- package/dart/test/session_bound_test.dart +8 -7
- package/dart/test/test_harness_test.dart +13 -12
- package/dart/test/vo_read_typing_test.dart +4 -3
- package/dart/test/watch_after_offer_stall_test.dart +4 -3
- package/dart/test/webview_bridge_test.dart +102 -5
- package/dart_types/CHANGELOG.md +5 -0
- package/dart_types/lib/nomos_types.dart +9 -0
- package/dart_types/lib/src/nomos_ref.dart +20 -0
- package/dart_types/lib/src/optional_ref.dart +85 -0
- package/dart_types/lib/src/sha256_digest.dart +41 -0
- package/dart_types/lib/src/versioned_nomos_ref.dart +81 -0
- package/dart_types/pubspec.yaml +1 -1
- package/dart_types/test/optional_ref_test.dart +44 -0
- package/dart_types/test/sha256_digest_test.dart +18 -0
- package/package.json +9 -5
- package/runtime/generated-application-host.js +5365 -1105
- package/src/aggregate.ts +18 -0
- package/src/application_composition.ts +6 -32
- package/src/attested_core.ts +6 -3
- package/src/birth_meter.ts +1 -0
- package/src/birth_meter_ensure.ts +28 -2
- package/src/births.ts +1 -1
- package/src/build_package.ts +162 -538
- package/src/business_birth.ts +19 -3
- package/src/canonical_json.ts +21 -0
- package/src/check_package_main.ts +73 -33
- package/src/clear_directive.ts +19 -8
- package/src/codegen_dart.ts +305 -22
- package/src/codegen_dart_application.ts +862 -292
- package/src/codegen_dart_client.ts +1084 -407
- package/src/codegen_dart_scenario.ts +12 -12
- package/src/codegen_proof.ts +169 -39
- package/src/codegen_provider_dart.ts +17 -1
- package/src/codegen_ts.ts +298 -106
- package/src/compat_manifest.ts +2 -2
- package/src/compat_package_main.ts +8 -12
- package/src/compile_package_main.ts +1860 -592
- package/src/compose.ts +1 -1
- package/src/directive.ts +208 -15
- package/src/engine_entry.ts +165 -111
- package/src/evolve_preview.ts +29 -69
- package/src/extremum.ts +1 -1
- package/src/fields.ts +183 -4
- package/src/flow_gate.ts +75 -2
- package/src/framework/application_home.ts +2 -0
- package/src/framework/auth_provider_law.ts +2 -2
- package/src/framework/birth_meter.ts +7 -19
- package/src/framework/bootstrap.ts +0 -2
- package/src/framework/business_corrections.ts +9 -5
- package/src/framework/domain_lifecycle.ts +15 -29
- package/src/framework/encryption_scope.ts +2 -1
- package/src/framework/framework_aggregates.ts +2 -0
- package/src/framework/genesis.ts +16 -14
- package/src/framework/home.ts +20 -14
- package/src/framework/owned_custody.ts +9 -3
- package/src/framework/platform_admin.ts +3 -3
- package/src/framework/relations.ts +21 -13
- package/src/framework/signing_identity.ts +45 -40
- package/src/framework/workspace_invariant.ts +245 -0
- package/src/framework/workspaces.ts +23 -29
- package/src/index.ts +36 -16
- package/src/intent.ts +178 -6
- package/src/law_check.ts +32 -11
- package/src/law_graph.ts +2902 -0
- package/src/law_graph_usda.ts +1799 -0
- package/src/law_identity.ts +435 -0
- package/src/law_value_type.ts +230 -0
- package/src/lsp_server.ts +58 -29
- package/src/ops.ts +7 -0
- package/src/package_config.ts +141 -0
- package/src/package_dependencies.ts +108 -0
- package/src/package_dependency_usage.ts +150 -0
- package/src/package_source_imports.ts +71 -0
- package/src/plan_source_slice.ts +335 -0
- package/src/point_guard.ts +192 -0
- package/src/precondition.ts +44 -9
- package/src/read_projection_namespace.ts +1 -1
- package/src/runtime_zod.ts +44 -0
- package/src/secondary_ensure.ts +67 -14
- package/src/shape_compat.ts +1 -1
- package/src/stable_id_identity_bridge.ts +149 -0
- package/src/stable_id_lockfile.ts +140 -6
- package/src/stable_ids.ts +1 -1
- package/src/stable_ids_for_module.ts +348 -0
- package/src/stable_ids_types.ts +1 -1
- package/src/sum.ts +1 -1
- package/src/usd_state.ts +1 -1
- package/src/usda_relationship_integrity.ts +66 -0
- package/src/usda_text.ts +18 -0
- package/src/usdz.ts +78 -0
- package/src/value_object.ts +95 -0
- package/src/wire.ts +19 -0
- package/src/wire_encode.ts +22 -0
- package/src/workspace_sharding.ts +52 -36
- package/src/workspace_sharing.ts +28 -4
- package/src/zod_introspection.ts +1 -1
- package/dart/test/cluster_attested_test.dart +0 -201
- package/src/compile_engine.ts +0 -595
- package/src/manifest.ts +0 -1476
- package/src/read_projection_composition.ts +0 -117
- package/src/read_projection_manifest.ts +0 -115
- package/src/relation_composition.ts +0 -286
- package/src/usd.ts +0 -1967
- package/src/usd_layers.ts +0 -1776
|
@@ -0,0 +1,1799 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
LawAggregateNode,
|
|
3
|
+
LawAnonymousValueFieldNode,
|
|
4
|
+
LawAnonymousValueTypeNode,
|
|
5
|
+
LawAnonymousValueVariantNode,
|
|
6
|
+
LawContractNode,
|
|
7
|
+
LawDependencyRef,
|
|
8
|
+
LawCapabilityNode,
|
|
9
|
+
LawFieldNode,
|
|
10
|
+
LawFlowNode,
|
|
11
|
+
LawForeignReadNode,
|
|
12
|
+
LawGraph,
|
|
13
|
+
LawIntentEffectNode,
|
|
14
|
+
LawIntentNode,
|
|
15
|
+
LawPayloadContractNode,
|
|
16
|
+
LawPolicyNode,
|
|
17
|
+
LawPredicateNode,
|
|
18
|
+
LawReadContractNode,
|
|
19
|
+
LawReadNode,
|
|
20
|
+
LawRelationNode,
|
|
21
|
+
LawRelationObjectTypeNode,
|
|
22
|
+
LawRelationPredicateNode,
|
|
23
|
+
LawInvariantNode,
|
|
24
|
+
LawTargetRef,
|
|
25
|
+
LawTypeNode,
|
|
26
|
+
LawWorkspaceTypeNode,
|
|
27
|
+
LawWorkspaceRelationNode,
|
|
28
|
+
} from "./law_graph.js";
|
|
29
|
+
|
|
30
|
+
const FRAMEWORK_SCALAR: Readonly<Record<string, string>> = {
|
|
31
|
+
string: "/Nomos/Framework/Types/t_0000000000000001",
|
|
32
|
+
integer: "/Nomos/Framework/Types/t_0000000000000002",
|
|
33
|
+
boolean: "/Nomos/Framework/Types/t_0000000000000004",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const FRAMEWORK_RELATIONS = [
|
|
37
|
+
{ key: "workspace#admin", id: "r_0000000000000001", object: "o_0000000000000001", subjects: ["user", "key"] },
|
|
38
|
+
{ key: "workspace#creator", id: "r_0000000000000002", object: "o_0000000000000001", subjects: ["user", "key"] },
|
|
39
|
+
{ key: "workspace#platformCreator", id: "r_0000000000000003", object: "o_0000000000000001", subjects: ["user", "key"] },
|
|
40
|
+
{ key: "workspace#installDomain", id: "r_0000000000000004", object: "o_0000000000000001", subjects: ["user", "key"] },
|
|
41
|
+
{ key: "identity#signer", id: "r_0000000000000005", object: "o_0000000000000002", subjects: ["key"] },
|
|
42
|
+
] as const;
|
|
43
|
+
|
|
44
|
+
const FRAMEWORK_RELATION_PATHS = new Map(
|
|
45
|
+
FRAMEWORK_RELATIONS.map((relation) => [
|
|
46
|
+
relation.key,
|
|
47
|
+
`/Nomos/Framework/Relations/${relation.id}`,
|
|
48
|
+
] as const),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
function frameworkRelationPath(relation: LawRelationNode): string | undefined {
|
|
52
|
+
const path = FRAMEWORK_RELATION_PATHS.get(
|
|
53
|
+
relation.entity.label as typeof FRAMEWORK_RELATIONS[number]["key"],
|
|
54
|
+
);
|
|
55
|
+
if (path === undefined) return undefined;
|
|
56
|
+
const frameworkObjectType = relation.entity.label.slice(0, relation.entity.label.indexOf("#"));
|
|
57
|
+
return relation.objectType === frameworkObjectType ? path : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The one compiler-owned v3 framework layer. Domain layers only author sparse opinions under
|
|
61
|
+
* `/Nomos/Domains`; their scalar relationships resolve here instead of repeating these definitions in
|
|
62
|
+
* every package/domain layer. Keep this vocabulary byte-stable with the W1 oracle. */
|
|
63
|
+
export function emitFrameworkLawUsda(): string {
|
|
64
|
+
return [
|
|
65
|
+
"#usda 1.0",
|
|
66
|
+
"(",
|
|
67
|
+
" customLayerData = {",
|
|
68
|
+
' string "nomos:format" = "nomos.law-layer.v3"',
|
|
69
|
+
' string "nomos:logicalPath" = "framework"',
|
|
70
|
+
" }",
|
|
71
|
+
")",
|
|
72
|
+
"",
|
|
73
|
+
'def Scope "Nomos"',
|
|
74
|
+
"{",
|
|
75
|
+
' def Scope "Framework"',
|
|
76
|
+
" {",
|
|
77
|
+
' def Scope "Types"',
|
|
78
|
+
" {",
|
|
79
|
+
...[
|
|
80
|
+
["t_0000000000000001", "String", "string"],
|
|
81
|
+
["t_0000000000000002", "Integer", "int"],
|
|
82
|
+
["t_0000000000000003", "Timestamp", "timestamp"],
|
|
83
|
+
["t_0000000000000004", "Boolean", "bool"],
|
|
84
|
+
].flatMap(([id, label, scalar]) => [
|
|
85
|
+
` def Scope "${id}"`,
|
|
86
|
+
" {",
|
|
87
|
+
' custom uniform token nomos:typeKind = "scalar"',
|
|
88
|
+
` custom uniform string nomos:label = "${label}"`,
|
|
89
|
+
` custom uniform token nomos:scalar = "${scalar}"`,
|
|
90
|
+
" }",
|
|
91
|
+
"",
|
|
92
|
+
]),
|
|
93
|
+
" }",
|
|
94
|
+
' def Scope "ObjectTypes"',
|
|
95
|
+
" {",
|
|
96
|
+
' def NomosObjectType "o_0000000000000001"',
|
|
97
|
+
" {",
|
|
98
|
+
' custom uniform string nomos:label = "workspace"',
|
|
99
|
+
" }",
|
|
100
|
+
"",
|
|
101
|
+
' def NomosObjectType "o_0000000000000002"',
|
|
102
|
+
" {",
|
|
103
|
+
' custom uniform string nomos:label = "identity"',
|
|
104
|
+
" }",
|
|
105
|
+
" }",
|
|
106
|
+
"",
|
|
107
|
+
' def Scope "Relations"',
|
|
108
|
+
" {",
|
|
109
|
+
...FRAMEWORK_RELATIONS.flatMap((relation) => {
|
|
110
|
+
const label = relation.key.slice(relation.key.indexOf("#") + 1);
|
|
111
|
+
const predicate = `p_${relation.id.slice(2)}`;
|
|
112
|
+
return [
|
|
113
|
+
` def NomosRelation "${relation.id}"`,
|
|
114
|
+
" {",
|
|
115
|
+
' custom uniform token nomos:relationKind = "authorization"',
|
|
116
|
+
` custom uniform string nomos:label = ${quote(label)}`,
|
|
117
|
+
` custom uniform string nomos:relation = ${quote(label)}`,
|
|
118
|
+
" custom uniform int nomos:maxDepth = 12",
|
|
119
|
+
" custom uniform int nomos:maxFanout = 1024",
|
|
120
|
+
` custom rel nomos:objectType = </Nomos/Framework/ObjectTypes/${relation.object}>`,
|
|
121
|
+
` custom uniform token nomos:objectNamespace = ${quote(relation.key.slice(0, relation.key.indexOf("#")))}`,
|
|
122
|
+
` custom uniform token[] nomos:subjectTypes = ${stringList(relation.subjects)}`,
|
|
123
|
+
` custom rel nomos:rewrite = </Nomos/Framework/Relations/${relation.id}/Predicates/${predicate}>`,
|
|
124
|
+
"",
|
|
125
|
+
' def Scope "Predicates"',
|
|
126
|
+
" {",
|
|
127
|
+
` def NomosDirectUserset "${predicate}"`,
|
|
128
|
+
" {",
|
|
129
|
+
' custom uniform token nomos:operator = "this"',
|
|
130
|
+
" }",
|
|
131
|
+
" }",
|
|
132
|
+
" }",
|
|
133
|
+
"",
|
|
134
|
+
];
|
|
135
|
+
}),
|
|
136
|
+
" }",
|
|
137
|
+
" }",
|
|
138
|
+
"",
|
|
139
|
+
' def Scope "Domains"',
|
|
140
|
+
" {",
|
|
141
|
+
" }",
|
|
142
|
+
"}",
|
|
143
|
+
"",
|
|
144
|
+
].join("\n");
|
|
145
|
+
}
|
|
146
|
+
type ResolveDependency = (dependency: LawDependencyRef<any>) => string;
|
|
147
|
+
|
|
148
|
+
function entityLabelOpinions(
|
|
149
|
+
entity: { readonly label: string; readonly priorLabels: readonly string[] },
|
|
150
|
+
indent: number,
|
|
151
|
+
): string[] {
|
|
152
|
+
const pad = " ".repeat(indent);
|
|
153
|
+
return [
|
|
154
|
+
`${pad}custom uniform string nomos:label = ${quote(entity.label)}`,
|
|
155
|
+
...(entity.priorLabels.length === 0
|
|
156
|
+
? []
|
|
157
|
+
: [`${pad}custom uniform string[] nomos:was = ${stringList(entity.priorLabels)}`]),
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** First W4.4 vertical slice: render domain, aggregate, stored-field and reusable-contract opinions. */
|
|
162
|
+
export function emitSparseLawGraphUsda(
|
|
163
|
+
graph: LawGraph,
|
|
164
|
+
logicalPath: string,
|
|
165
|
+
closure: readonly LawGraph[] = [graph],
|
|
166
|
+
): string {
|
|
167
|
+
const domain = graph.domain;
|
|
168
|
+
const aggregates = graph.nodes.filter((node): node is LawAggregateNode => node.kind === "aggregate");
|
|
169
|
+
const contracts = graph.nodes.filter((node): node is LawContractNode => node.kind === "contract");
|
|
170
|
+
const payloadContracts = graph.nodes.filter((node): node is LawPayloadContractNode =>
|
|
171
|
+
node.kind === "payload-contract");
|
|
172
|
+
const fields = graph.nodes.filter((node): node is LawFieldNode => node.kind === "field");
|
|
173
|
+
const types = graph.nodes.filter((node): node is LawTypeNode => node.kind === "type");
|
|
174
|
+
const objectTypes = graph.nodes.filter((node): node is LawRelationObjectTypeNode => node.kind === "object-type");
|
|
175
|
+
const allRelations = graph.nodes.filter((node): node is LawRelationNode => node.kind === "relation");
|
|
176
|
+
// Framework relations have one canonical identity and definition in `/Nomos/Framework`.
|
|
177
|
+
// Domain modules may re-export the handles for authoring convenience, but those re-exports are
|
|
178
|
+
// references, not fresh policy definitions.
|
|
179
|
+
const relations = allRelations.filter((relation) => frameworkRelationPath(relation) === undefined);
|
|
180
|
+
const intents = graph.nodes.filter((node): node is LawIntentNode => node.kind === "intent");
|
|
181
|
+
const reads = graph.nodes.filter((node): node is LawReadNode => node.kind === "read");
|
|
182
|
+
const readContracts = graph.nodes.filter((node): node is LawReadContractNode => node.kind === "read-contract");
|
|
183
|
+
const readPredicates = graph.nodes.filter((node): node is LawPredicateNode =>
|
|
184
|
+
node.kind === "structural" && node.structuralKind === "predicate");
|
|
185
|
+
const effects = graph.nodes.filter((node): node is LawIntentEffectNode =>
|
|
186
|
+
node.kind === "structural" && node.structuralKind === "effect");
|
|
187
|
+
const foreignReads = graph.nodes.filter((node): node is LawForeignReadNode =>
|
|
188
|
+
node.kind === "structural" && node.structuralKind === "foreign-read");
|
|
189
|
+
const workspaceRelations = graph.nodes.filter((node): node is LawWorkspaceRelationNode =>
|
|
190
|
+
node.kind === "structural" && node.structuralKind === "workspace-relation");
|
|
191
|
+
const policies = graph.nodes.filter((node): node is LawPolicyNode => node.kind === "policy");
|
|
192
|
+
const invariants = graph.nodes.filter((node): node is LawInvariantNode => node.kind === "invariant");
|
|
193
|
+
const capabilities = graph.nodes.filter((node): node is LawCapabilityNode => node.kind === "capability");
|
|
194
|
+
const workspaceTypes = graph.nodes.filter((node): node is LawWorkspaceTypeNode => node.kind === "workspace-type");
|
|
195
|
+
const flows = graph.nodes.filter((node): node is LawFlowNode =>
|
|
196
|
+
node.kind === "structural" && node.structuralKind === "flow");
|
|
197
|
+
const relationPredicates = graph.nodes.filter((node): node is LawRelationPredicateNode =>
|
|
198
|
+
node.kind === "structural" && node.structuralKind === "relation-predicate");
|
|
199
|
+
const anonymousTypes = graph.nodes.filter((node): node is LawAnonymousValueTypeNode =>
|
|
200
|
+
node.kind === "structural" && node.structuralKind === "value-type");
|
|
201
|
+
const anonymousFields = graph.nodes.filter((node): node is LawAnonymousValueFieldNode =>
|
|
202
|
+
node.kind === "structural" && node.structuralKind === "value-field");
|
|
203
|
+
const anonymousVariants = graph.nodes.filter((node): node is LawAnonymousValueVariantNode =>
|
|
204
|
+
node.kind === "structural" && node.structuralKind === "value-variant");
|
|
205
|
+
const anonymousTypeById = new Map<string, LawAnonymousValueTypeNode>(
|
|
206
|
+
anonymousTypes.map((node) => [node.entity.id, node] as const),
|
|
207
|
+
);
|
|
208
|
+
const anonymousFieldsByOwner = new Map<string, LawAnonymousValueFieldNode[]>();
|
|
209
|
+
for (const field of anonymousFields) {
|
|
210
|
+
const owned = anonymousFieldsByOwner.get(field.owner.id) ?? [];
|
|
211
|
+
owned.push(field);
|
|
212
|
+
anonymousFieldsByOwner.set(field.owner.id, owned);
|
|
213
|
+
}
|
|
214
|
+
const anonymousVariantsByOwner = new Map<string, LawAnonymousValueVariantNode[]>();
|
|
215
|
+
for (const variant of anonymousVariants) {
|
|
216
|
+
const owned = anonymousVariantsByOwner.get(variant.owner.id) ?? [];
|
|
217
|
+
owned.push(variant);
|
|
218
|
+
anonymousVariantsByOwner.set(variant.owner.id, owned);
|
|
219
|
+
}
|
|
220
|
+
const pathById = new Map<string, string>([[domain.entity.id, `/Nomos/Domains/${domain.entity.id}`]]);
|
|
221
|
+
const dependencyPaths = new Map<string, string[]>();
|
|
222
|
+
const addDependency = (domainLabel: string, kind: string, label: string, path: string): void => {
|
|
223
|
+
for (const key of [`${domainLabel}:${kind}:${label}`, `*:${kind}:${label}`]) {
|
|
224
|
+
const values = dependencyPaths.get(key) ?? [];
|
|
225
|
+
if (!values.includes(path)) values.push(path);
|
|
226
|
+
dependencyPaths.set(key, values);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
for (const candidate of closure) {
|
|
230
|
+
const domainPath = `/Nomos/Domains/${candidate.domain.entity.id}`;
|
|
231
|
+
pathById.set(candidate.domain.entity.id, domainPath);
|
|
232
|
+
addDependency(candidate.domain.entity.label, "domain", candidate.domain.entity.label, domainPath);
|
|
233
|
+
for (const node of candidate.nodes) {
|
|
234
|
+
let path: string | undefined;
|
|
235
|
+
let kind: string | undefined;
|
|
236
|
+
switch (node.kind) {
|
|
237
|
+
case "aggregate": path = `${domainPath}/Aggregates/${node.entity.id}`; kind = "aggregate"; break;
|
|
238
|
+
case "contract":
|
|
239
|
+
case "payload-contract":
|
|
240
|
+
case "read-contract": path = `${domainPath}/Contracts/${node.entity.id}`; kind = "contract"; break;
|
|
241
|
+
case "type": path = `${domainPath}/Types/${node.entity.id}`; kind = "type"; break;
|
|
242
|
+
case "object-type": path = `${domainPath}/ObjectTypes/${node.entity.id}`; kind = "object-type"; break;
|
|
243
|
+
case "intent": path = `${domainPath}/Intents/${node.entity.id}`; kind = "intent"; break;
|
|
244
|
+
case "read": path = `${domainPath}/Reads/${node.entity.id}`; kind = "read"; break;
|
|
245
|
+
case "relation":
|
|
246
|
+
path = frameworkRelationPath(node) ?? `${domainPath}/Relations/${node.entity.id}`;
|
|
247
|
+
kind = "relation";
|
|
248
|
+
break;
|
|
249
|
+
case "policy": path = `${domainPath}/Policies/${node.entity.id}`; kind = "policy"; break;
|
|
250
|
+
case "invariant": path = `${domainPath}/Invariants/${node.entity.id}`; kind = "invariant"; break;
|
|
251
|
+
case "capability": path = `${domainPath}/Capabilities/${node.entity.id}`; kind = "capability"; break;
|
|
252
|
+
case "workspace-type": path = `${domainPath}/WorkspaceTypes/s_${node.entity.sid}`; kind = "workspace-type"; break;
|
|
253
|
+
case "field":
|
|
254
|
+
case "structural": break;
|
|
255
|
+
}
|
|
256
|
+
if (path !== undefined && kind !== undefined) {
|
|
257
|
+
pathById.set(node.entity.id, path);
|
|
258
|
+
addDependency(candidate.domain.entity.label, kind, node.entity.label, path);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const entityLabels = new Map(candidate.nodes.map((node) => [node.entity.id, node.entity.label] as const));
|
|
262
|
+
for (const node of candidate.nodes) {
|
|
263
|
+
if (node.kind !== "field") continue;
|
|
264
|
+
const ownerPath = pathById.get(node.owner.id);
|
|
265
|
+
if (ownerPath === undefined) continue;
|
|
266
|
+
const path = `${ownerPath}/Fields/${node.entity.id}`;
|
|
267
|
+
pathById.set(node.entity.id, path);
|
|
268
|
+
const ownerLabel = entityLabels.get(node.owner.id);
|
|
269
|
+
addDependency(
|
|
270
|
+
candidate.domain.entity.label,
|
|
271
|
+
"field",
|
|
272
|
+
ownerLabel === undefined ? node.entity.label : `${ownerLabel}.${node.entity.label}`,
|
|
273
|
+
path,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const resolveDependency = (dependency: LawDependencyRef<any>): string => {
|
|
278
|
+
if (dependency.entityKind === "relation") {
|
|
279
|
+
const framework = FRAMEWORK_RELATION_PATHS.get(
|
|
280
|
+
dependency.label as typeof FRAMEWORK_RELATIONS[number]["key"],
|
|
281
|
+
);
|
|
282
|
+
if (framework !== undefined) return framework;
|
|
283
|
+
}
|
|
284
|
+
const key = `${dependency.domain ?? "*"}:${dependency.entityKind}:${dependency.label}`;
|
|
285
|
+
let candidates = dependencyPaths.get(key) ?? [];
|
|
286
|
+
// A runtime aggregate can live in `catalogue` while its reusable public shape is defined once
|
|
287
|
+
// in the package's schema-only `reference_data` layer. Domain qualification remains strict for
|
|
288
|
+
// aggregates and relations; contracts and their fields may fall back to the closure-wide index
|
|
289
|
+
// only when there is exactly one definition. Multiple shared definitions still refuse below.
|
|
290
|
+
if (
|
|
291
|
+
candidates.length === 0 &&
|
|
292
|
+
dependency.domain !== undefined &&
|
|
293
|
+
(dependency.entityKind === "contract" || dependency.entityKind === "field")
|
|
294
|
+
) {
|
|
295
|
+
candidates = dependencyPaths.get(`*:${dependency.entityKind}:${dependency.label}`) ?? [];
|
|
296
|
+
}
|
|
297
|
+
if (candidates.length === 0) {
|
|
298
|
+
fail(`no closure path resolves dependency '${dependency.domain ?? "?"}:${dependency.entityKind}:${dependency.label}'`);
|
|
299
|
+
}
|
|
300
|
+
if (candidates.length > 1) {
|
|
301
|
+
fail(`dependency '${dependency.domain ?? "?"}:${dependency.entityKind}:${dependency.label}' is ambiguous in the closure`);
|
|
302
|
+
}
|
|
303
|
+
return candidates[0]!;
|
|
304
|
+
};
|
|
305
|
+
for (const aggregate of aggregates) {
|
|
306
|
+
pathById.set(aggregate.entity.id, `/Nomos/Domains/${domain.entity.id}/Aggregates/${aggregate.entity.id}`);
|
|
307
|
+
}
|
|
308
|
+
for (const contract of contracts) {
|
|
309
|
+
pathById.set(contract.entity.id, `/Nomos/Domains/${domain.entity.id}/Contracts/${contract.entity.id}`);
|
|
310
|
+
}
|
|
311
|
+
for (const contract of payloadContracts) {
|
|
312
|
+
pathById.set(contract.entity.id, `/Nomos/Domains/${domain.entity.id}/Contracts/${contract.entity.id}`);
|
|
313
|
+
}
|
|
314
|
+
for (const type of types) {
|
|
315
|
+
pathById.set(type.entity.id, `/Nomos/Domains/${domain.entity.id}/Types/${type.entity.id}`);
|
|
316
|
+
}
|
|
317
|
+
for (const objectType of objectTypes) {
|
|
318
|
+
pathById.set(objectType.entity.id, `/Nomos/Domains/${domain.entity.id}/ObjectTypes/${objectType.entity.id}`);
|
|
319
|
+
}
|
|
320
|
+
for (const relation of allRelations) {
|
|
321
|
+
pathById.set(
|
|
322
|
+
relation.entity.id,
|
|
323
|
+
frameworkRelationPath(relation) ?? `/Nomos/Domains/${domain.entity.id}/Relations/${relation.entity.id}`,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
for (const intent of intents) {
|
|
327
|
+
pathById.set(intent.entity.id, `/Nomos/Domains/${domain.entity.id}/Intents/${intent.entity.id}`);
|
|
328
|
+
}
|
|
329
|
+
for (const effect of effects) {
|
|
330
|
+
const owner = pathById.get(effect.owner.id);
|
|
331
|
+
if (owner !== undefined) pathById.set(effect.entity.id, `${owner}/Effects/e_${effect.entity.sid}`);
|
|
332
|
+
}
|
|
333
|
+
for (const read of foreignReads) {
|
|
334
|
+
const owner = pathById.get(read.owner.id);
|
|
335
|
+
if (owner !== undefined) pathById.set(read.entity.id, `${owner}/ForeignReads/x_${read.entity.sid}`);
|
|
336
|
+
}
|
|
337
|
+
for (const read of reads) {
|
|
338
|
+
pathById.set(read.entity.id, `/Nomos/Domains/${domain.entity.id}/Reads/${read.entity.id}`);
|
|
339
|
+
}
|
|
340
|
+
for (const predicate of readPredicates) {
|
|
341
|
+
const owner = pathById.get(predicate.owner.id);
|
|
342
|
+
if (owner !== undefined) pathById.set(predicate.entity.id, `${owner}/Predicates/p_${predicate.entity.sid}`);
|
|
343
|
+
}
|
|
344
|
+
for (const policy of policies) {
|
|
345
|
+
pathById.set(policy.entity.id, `/Nomos/Domains/${domain.entity.id}/Policies/${policy.entity.id}`);
|
|
346
|
+
}
|
|
347
|
+
for (const invariant of invariants) {
|
|
348
|
+
pathById.set(invariant.entity.id, `/Nomos/Domains/${domain.entity.id}/Invariants/${invariant.entity.id}`);
|
|
349
|
+
}
|
|
350
|
+
for (const capability of capabilities) {
|
|
351
|
+
pathById.set(capability.entity.id, `/Nomos/Domains/${domain.entity.id}/Capabilities/${capability.entity.id}`);
|
|
352
|
+
}
|
|
353
|
+
for (const workspaceType of workspaceTypes) {
|
|
354
|
+
pathById.set(workspaceType.entity.id, `/Nomos/Domains/${domain.entity.id}/WorkspaceTypes/s_${workspaceType.entity.sid}`);
|
|
355
|
+
}
|
|
356
|
+
for (const flow of flows) {
|
|
357
|
+
pathById.set(flow.entity.id, `/Nomos/Domains/${domain.entity.id}/Flows/w_${flow.entity.sid}`);
|
|
358
|
+
}
|
|
359
|
+
for (const predicate of relationPredicates) {
|
|
360
|
+
const owner = pathById.get(predicate.owner.id);
|
|
361
|
+
if (owner !== undefined) pathById.set(predicate.entity.id, `${owner}/Predicates/p_${predicate.entity.sid}`);
|
|
362
|
+
}
|
|
363
|
+
const resolveAnonymousTypePath = (id: string, active = new Set<string>()): string => {
|
|
364
|
+
const prior = pathById.get(id);
|
|
365
|
+
if (prior !== undefined) return prior;
|
|
366
|
+
if (active.has(id)) fail(`anonymous type '${id}' contains an unsupported wrapper cycle`);
|
|
367
|
+
const node = anonymousTypeById.get(id);
|
|
368
|
+
if (node === undefined) fail(`anonymous type '${id}' is missing from the graph`);
|
|
369
|
+
active.add(id);
|
|
370
|
+
const expression = node.expression;
|
|
371
|
+
let path: string;
|
|
372
|
+
if (expression.kind === "string" && expression.constraints.length === 0) {
|
|
373
|
+
path = FRAMEWORK_SCALAR.string!;
|
|
374
|
+
} else if (expression.kind === "integer" && expression.constraints.length === 0) {
|
|
375
|
+
path = FRAMEWORK_SCALAR.integer!;
|
|
376
|
+
} else if (expression.kind === "boolean") {
|
|
377
|
+
path = FRAMEWORK_SCALAR.boolean!;
|
|
378
|
+
} else if (expression.kind === "nullable" || expression.kind === "default") {
|
|
379
|
+
path = resolveAnonymousTypePath(expression.value.id, active);
|
|
380
|
+
} else {
|
|
381
|
+
path = `/Nomos/Domains/${domain.entity.id}/Types/t_${node.entity.sid}`;
|
|
382
|
+
}
|
|
383
|
+
active.delete(id);
|
|
384
|
+
pathById.set(id, path);
|
|
385
|
+
return path;
|
|
386
|
+
};
|
|
387
|
+
for (const type of anonymousTypes) resolveAnonymousTypePath(type.entity.id);
|
|
388
|
+
|
|
389
|
+
// Anonymous value types are structural values, not independently lived declarations. The graph
|
|
390
|
+
// deliberately retains their source owner for diagnostics, but USDA must not turn that provenance
|
|
391
|
+
// into thousands of duplicate runtime prims. Compute a semantic key which excludes owner identity,
|
|
392
|
+
// then point every equivalent type graph at one canonical prim. Declared types remain untouched.
|
|
393
|
+
// Recursive graphs keep their owner-scoped identity for now: aliasing a cycle needs an explicit
|
|
394
|
+
// recursion-isomorphism proof, whereas every acyclic case below is exact by construction.
|
|
395
|
+
const semanticTypeKeyMemo = new Map<string, string | null>();
|
|
396
|
+
const semanticTypeKey = (id: string, active = new Set<string>()): string | undefined => {
|
|
397
|
+
const memoized = semanticTypeKeyMemo.get(id);
|
|
398
|
+
if (memoized !== undefined) return memoized ?? undefined;
|
|
399
|
+
if (active.has(id)) {
|
|
400
|
+
semanticTypeKeyMemo.set(id, null);
|
|
401
|
+
return undefined;
|
|
402
|
+
}
|
|
403
|
+
const node = anonymousTypeById.get(id);
|
|
404
|
+
if (node === undefined) return undefined;
|
|
405
|
+
active.add(id);
|
|
406
|
+
const childKey = (childId: string): string | undefined => semanticTypeKey(childId, active);
|
|
407
|
+
let key: string | undefined;
|
|
408
|
+
switch (node.expression.kind) {
|
|
409
|
+
case "string":
|
|
410
|
+
case "number":
|
|
411
|
+
case "integer":
|
|
412
|
+
case "boolean":
|
|
413
|
+
case "null":
|
|
414
|
+
case "unknown":
|
|
415
|
+
case "enum":
|
|
416
|
+
case "literal":
|
|
417
|
+
key = JSON.stringify(node.expression);
|
|
418
|
+
break;
|
|
419
|
+
case "list": {
|
|
420
|
+
const child = childKey(node.expression.element.id);
|
|
421
|
+
if (child !== undefined) key = JSON.stringify(["list", child]);
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
case "map": {
|
|
425
|
+
const child = childKey(node.expression.value.id);
|
|
426
|
+
if (child !== undefined) key = JSON.stringify(["map", child]);
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
case "nullable":
|
|
430
|
+
case "default": {
|
|
431
|
+
// These wrappers already resolve to their carried type rather than owning a USDA prim.
|
|
432
|
+
key = childKey(node.expression.value.id);
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
case "record": {
|
|
436
|
+
const members: Array<readonly [string, boolean, string]> = [];
|
|
437
|
+
for (const field of anonymousFieldsByOwner.get(id) ?? []) {
|
|
438
|
+
const child = childKey(field.value.id);
|
|
439
|
+
if (child === undefined) break;
|
|
440
|
+
members.push([field.name, field.required, child]);
|
|
441
|
+
}
|
|
442
|
+
if (members.length === (anonymousFieldsByOwner.get(id)?.length ?? 0)) {
|
|
443
|
+
members.sort(([left], [right]) => left.localeCompare(right));
|
|
444
|
+
key = JSON.stringify(["record", members]);
|
|
445
|
+
}
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
case "sum": {
|
|
449
|
+
const members: string[] = [];
|
|
450
|
+
for (const variant of anonymousVariantsByOwner.get(id) ?? []) {
|
|
451
|
+
const child = childKey(variant.value.id);
|
|
452
|
+
if (child === undefined) break;
|
|
453
|
+
members.push(child);
|
|
454
|
+
}
|
|
455
|
+
if (members.length === (anonymousVariantsByOwner.get(id)?.length ?? 0)) {
|
|
456
|
+
members.sort();
|
|
457
|
+
key = JSON.stringify(["sum", members]);
|
|
458
|
+
}
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
case "recursive":
|
|
462
|
+
key = undefined;
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
active.delete(id);
|
|
466
|
+
semanticTypeKeyMemo.set(id, key ?? null);
|
|
467
|
+
return key;
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const canonicalTypeBySemanticKey = new Map<string, LawAnonymousValueTypeNode>();
|
|
471
|
+
const canonicalTypeIdById = new Map<string, string>();
|
|
472
|
+
for (const type of [...anonymousTypes].sort((left, right) => left.entity.sid.localeCompare(right.entity.sid))) {
|
|
473
|
+
const ownPath = pathById.get(type.entity.id);
|
|
474
|
+
if (ownPath?.endsWith(`/Types/t_${type.entity.sid}`) !== true) continue;
|
|
475
|
+
const key = semanticTypeKey(type.entity.id);
|
|
476
|
+
if (key === undefined) continue;
|
|
477
|
+
const canonical = canonicalTypeBySemanticKey.get(key);
|
|
478
|
+
if (canonical === undefined) {
|
|
479
|
+
canonicalTypeBySemanticKey.set(key, type);
|
|
480
|
+
canonicalTypeIdById.set(type.entity.id, type.entity.id);
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
canonicalTypeIdById.set(type.entity.id, canonical.entity.id);
|
|
484
|
+
pathById.set(type.entity.id, requiredPath(canonical.entity.id, pathById));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Wrapper types do not own a USDA prim: their identity is the final path of the carried type.
|
|
488
|
+
// `resolveAnonymousTypePath` ran before semantic sharing, so a wrapper around a type which was
|
|
489
|
+
// subsequently aliased still holds the now-suppressed occurrence's old path. Refresh wrappers
|
|
490
|
+
// after all canonical aliases are known so contracts cannot retain a dangling relationship.
|
|
491
|
+
const finalAnonymousTypePath = (id: string, active = new Set<string>()): string => {
|
|
492
|
+
if (active.has(id)) fail(`anonymous type '${id}' contains an unsupported wrapper cycle`);
|
|
493
|
+
const node = anonymousTypeById.get(id);
|
|
494
|
+
if (node === undefined) return requiredPath(id, pathById);
|
|
495
|
+
if (node.expression.kind !== "nullable" && node.expression.kind !== "default") {
|
|
496
|
+
return requiredPath(id, pathById);
|
|
497
|
+
}
|
|
498
|
+
active.add(id);
|
|
499
|
+
const path = finalAnonymousTypePath(node.expression.value.id, active);
|
|
500
|
+
active.delete(id);
|
|
501
|
+
return path;
|
|
502
|
+
};
|
|
503
|
+
for (const type of anonymousTypes) {
|
|
504
|
+
if (type.expression.kind === "nullable" || type.expression.kind === "default") {
|
|
505
|
+
pathById.set(type.entity.id, finalAnonymousTypePath(type.entity.id));
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
for (const field of anonymousFields) {
|
|
509
|
+
const owner = pathById.get(field.owner.id);
|
|
510
|
+
if (owner !== undefined) pathById.set(field.entity.id, `${owner}/Fields/f_${field.entity.sid}`);
|
|
511
|
+
}
|
|
512
|
+
for (const variant of anonymousVariants) {
|
|
513
|
+
const owner = pathById.get(variant.owner.id);
|
|
514
|
+
if (owner !== undefined) pathById.set(variant.entity.id, `${owner}/Variants/v_${variant.entity.sid}`);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// When two record/sum types share one canonical prim, their nested structural references must
|
|
518
|
+
// share the canonical child paths too. This preserves effects which target a nested payload field;
|
|
519
|
+
// no dangling path is allowed merely because its source owner was the non-canonical occurrence.
|
|
520
|
+
for (const [duplicateId, canonicalId] of canonicalTypeIdById) {
|
|
521
|
+
if (duplicateId === canonicalId) continue;
|
|
522
|
+
const canonicalFields = new Map<string, LawAnonymousValueFieldNode>();
|
|
523
|
+
for (const field of anonymousFieldsByOwner.get(canonicalId) ?? []) {
|
|
524
|
+
const child = semanticTypeKey(field.value.id);
|
|
525
|
+
if (child !== undefined) canonicalFields.set(JSON.stringify([field.name, field.required, child]), field);
|
|
526
|
+
}
|
|
527
|
+
for (const field of anonymousFieldsByOwner.get(duplicateId) ?? []) {
|
|
528
|
+
const child = semanticTypeKey(field.value.id);
|
|
529
|
+
const canonical = child === undefined
|
|
530
|
+
? undefined
|
|
531
|
+
: canonicalFields.get(JSON.stringify([field.name, field.required, child]));
|
|
532
|
+
if (canonical !== undefined) {
|
|
533
|
+
pathById.set(field.entity.id, requiredPath(canonical.entity.id, pathById));
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const canonicalVariants = new Map<string, LawAnonymousValueVariantNode[]>();
|
|
538
|
+
for (const variant of anonymousVariantsByOwner.get(canonicalId) ?? []) {
|
|
539
|
+
const child = semanticTypeKey(variant.value.id);
|
|
540
|
+
if (child === undefined) continue;
|
|
541
|
+
const matches = canonicalVariants.get(child) ?? [];
|
|
542
|
+
matches.push(variant);
|
|
543
|
+
canonicalVariants.set(child, matches);
|
|
544
|
+
}
|
|
545
|
+
for (const variant of anonymousVariantsByOwner.get(duplicateId) ?? []) {
|
|
546
|
+
const child = semanticTypeKey(variant.value.id);
|
|
547
|
+
if (child === undefined) continue;
|
|
548
|
+
const canonical = canonicalVariants.get(child)?.shift();
|
|
549
|
+
if (canonical !== undefined) {
|
|
550
|
+
pathById.set(variant.entity.id, requiredPath(canonical.entity.id, pathById));
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
for (const field of fields) {
|
|
555
|
+
const owner = pathById.get(field.owner.id);
|
|
556
|
+
if (owner !== undefined) pathById.set(field.entity.id, `${owner}/Fields/${field.entity.id}`);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const lines = [
|
|
560
|
+
"#usda 1.0",
|
|
561
|
+
"(",
|
|
562
|
+
" customLayerData = {",
|
|
563
|
+
' string "nomos:format" = "nomos.law-layer.v3"',
|
|
564
|
+
` string "nomos:logicalPath" = ${quote(logicalPath)}`,
|
|
565
|
+
" }",
|
|
566
|
+
")",
|
|
567
|
+
"",
|
|
568
|
+
'over "Nomos"',
|
|
569
|
+
"{",
|
|
570
|
+
' over "Domains"',
|
|
571
|
+
" {",
|
|
572
|
+
` def Scope "${domain.entity.id}"`,
|
|
573
|
+
" {",
|
|
574
|
+
...entityLabelOpinions(domain.entity, 12),
|
|
575
|
+
];
|
|
576
|
+
if (domain.parentAttestedBirth !== undefined) {
|
|
577
|
+
lines.push(
|
|
578
|
+
` custom uniform string nomos:parentOwnerObject = ${quote(domain.parentAttestedBirth.ownerObject)}`,
|
|
579
|
+
` custom uniform string nomos:parentOwnerField = ${quote(domain.parentAttestedBirth.ownerField)}`,
|
|
580
|
+
` custom uniform string nomos:parentCertObject = ${quote(domain.parentAttestedBirth.certObject)}`,
|
|
581
|
+
` custom uniform string nomos:parentCertField = ${quote(domain.parentAttestedBirth.certField)}`,
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
lines.push("");
|
|
585
|
+
emitContainer(lines, "Aggregates", aggregates, 12, (aggregate, indent) =>
|
|
586
|
+
emitAggregate(aggregate, fields, pathById, resolveDependency, indent));
|
|
587
|
+
const emittedAnonymousTypes = anonymousTypes.filter((type) =>
|
|
588
|
+
pathById.get(type.entity.id)?.endsWith(`/Types/t_${type.entity.sid}`) === true);
|
|
589
|
+
if (types.length > 0 || emittedAnonymousTypes.length > 0) {
|
|
590
|
+
lines.push("");
|
|
591
|
+
const emittedTypes: Array<LawTypeNode | LawAnonymousValueTypeNode> = [...types, ...emittedAnonymousTypes]
|
|
592
|
+
.sort((a, b) => requiredPath(a.entity.id, pathById).localeCompare(requiredPath(b.entity.id, pathById)));
|
|
593
|
+
emitContainer(lines, "Types", emittedTypes, 12, (type, indent) =>
|
|
594
|
+
type.kind === "type"
|
|
595
|
+
? emitType(type, fields, anonymousVariants, pathById, resolveDependency, indent)
|
|
596
|
+
: emitAnonymousType(type, anonymousFields, anonymousVariants, anonymousTypeById, pathById, indent),
|
|
597
|
+
(type) => type.kind === "type" ? type.entity.id : `t_${type.entity.sid}`);
|
|
598
|
+
}
|
|
599
|
+
if (objectTypes.length > 0) {
|
|
600
|
+
lines.push("");
|
|
601
|
+
emitContainer(lines, "ObjectTypes", objectTypes, 12, (objectType, indent) =>
|
|
602
|
+
[
|
|
603
|
+
...entityLabelOpinions(objectType.entity, indent),
|
|
604
|
+
`${" ".repeat(indent)}custom uniform token nomos:objectNamespace = ${quote(objectType.namespace)}`,
|
|
605
|
+
], undefined, () => "NomosObjectType");
|
|
606
|
+
}
|
|
607
|
+
if (contracts.length > 0 || payloadContracts.length > 0) {
|
|
608
|
+
lines.push("");
|
|
609
|
+
const emittedContracts: Array<LawContractNode | LawPayloadContractNode> = [...contracts, ...payloadContracts]
|
|
610
|
+
.sort((a, b) => a.entity.id.localeCompare(b.entity.id));
|
|
611
|
+
emitContainer(lines, "Contracts", emittedContracts, 12, (contract, indent) =>
|
|
612
|
+
contract.kind === "contract"
|
|
613
|
+
? emitContract(contract, fields, pathById, resolveDependency, indent)
|
|
614
|
+
: emitPayloadContract(contract, anonymousFields, anonymousTypeById, pathById, indent));
|
|
615
|
+
}
|
|
616
|
+
if (relations.length > 0) {
|
|
617
|
+
lines.push("");
|
|
618
|
+
emitContainer(lines, "Relations", relations, 12, (relation, indent) =>
|
|
619
|
+
emitRelation(relation, relationPredicates, pathById, indent), undefined,
|
|
620
|
+
(relation) => authorizationDefinitionType(relation));
|
|
621
|
+
}
|
|
622
|
+
if (intents.length > 0) {
|
|
623
|
+
lines.push("");
|
|
624
|
+
emitContainer(lines, "Intents", intents, 12, (intent, indent) =>
|
|
625
|
+
emitIntent(intent, effects, foreignReads, workspaceRelations, pathById, resolveDependency, indent));
|
|
626
|
+
}
|
|
627
|
+
if (reads.length > 0) {
|
|
628
|
+
lines.push("");
|
|
629
|
+
emitContainer(lines, "Reads", reads, 12, (read, indent) =>
|
|
630
|
+
emitRead(read, readContracts, readPredicates, pathById, indent));
|
|
631
|
+
}
|
|
632
|
+
if (policies.length > 0) {
|
|
633
|
+
lines.push("");
|
|
634
|
+
emitContainer(lines, "Policies", policies, 12, (policy, indent) =>
|
|
635
|
+
emitPolicy(policy, pathById, indent));
|
|
636
|
+
}
|
|
637
|
+
if (invariants.length > 0) {
|
|
638
|
+
lines.push("");
|
|
639
|
+
emitContainer(lines, "Invariants", invariants, 12, (invariant, indent) =>
|
|
640
|
+
emitInvariant(invariant, pathById, indent));
|
|
641
|
+
}
|
|
642
|
+
if (capabilities.length > 0) {
|
|
643
|
+
lines.push("");
|
|
644
|
+
emitContainer(lines, "Capabilities", capabilities, 12, (capability, indent) =>
|
|
645
|
+
emitCapability(capability, pathById, indent));
|
|
646
|
+
}
|
|
647
|
+
if (workspaceTypes.length > 0) {
|
|
648
|
+
lines.push("");
|
|
649
|
+
emitContainer(lines, "WorkspaceTypes", workspaceTypes, 12, (workspaceType, indent) =>
|
|
650
|
+
emitWorkspaceType(workspaceType, pathById, indent),
|
|
651
|
+
(workspaceType) => `s_${workspaceType.entity.sid}`);
|
|
652
|
+
}
|
|
653
|
+
if (flows.length > 0) {
|
|
654
|
+
lines.push("");
|
|
655
|
+
emitContainer(lines, "Flows", flows, 12, (flow, indent) => emitFlow(flow, pathById, indent),
|
|
656
|
+
(flow) => `w_${flow.entity.sid}`);
|
|
657
|
+
}
|
|
658
|
+
lines.push(" }", " }", "}", "");
|
|
659
|
+
return lines.join("\n");
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function emitAggregate(
|
|
663
|
+
aggregate: LawAggregateNode,
|
|
664
|
+
fields: readonly LawFieldNode[],
|
|
665
|
+
paths: ReadonlyMap<string, string>,
|
|
666
|
+
resolveDependency: ResolveDependency,
|
|
667
|
+
indent: number,
|
|
668
|
+
): string[] {
|
|
669
|
+
const pad = " ".repeat(indent);
|
|
670
|
+
const body = [
|
|
671
|
+
...entityLabelOpinions(aggregate.entity, indent),
|
|
672
|
+
`${pad}custom uniform string nomos:wireType = ${quote(aggregate.wireType)}`,
|
|
673
|
+
`${pad}custom uniform bool nomos:public = ${aggregate.public}`,
|
|
674
|
+
];
|
|
675
|
+
if (aggregate.cap !== undefined) body.push(`${pad}custom uniform uint64 nomos:cap = ${aggregate.cap}`);
|
|
676
|
+
if (aggregate.implements.length > 0) {
|
|
677
|
+
body.push(`${pad}custom rel nomos:implements = ${targets(aggregate.implements, paths, resolveDependency)}`);
|
|
678
|
+
}
|
|
679
|
+
if (aggregate.readPolicy !== undefined) {
|
|
680
|
+
body.push(`${pad}custom rel nomos:readPolicy = ${target(aggregate.readPolicy, paths, resolveDependency)}`);
|
|
681
|
+
}
|
|
682
|
+
if (aggregate.ownerField !== undefined) {
|
|
683
|
+
body.push(`${pad}custom rel nomos:ownerField = <${requiredPath(aggregate.ownerField.id, paths)}>`);
|
|
684
|
+
}
|
|
685
|
+
const owned = fields.filter((field) => field.owner.id === aggregate.entity.id);
|
|
686
|
+
if (owned.length > 0) {
|
|
687
|
+
body.push("", `${pad}def Scope "Fields"`, `${pad}{`);
|
|
688
|
+
for (const [index, field] of owned.entries()) {
|
|
689
|
+
if (index > 0) body.push("");
|
|
690
|
+
body.push(...emitField(field, paths, resolveDependency, indent + 4));
|
|
691
|
+
}
|
|
692
|
+
body.push(`${pad}}`);
|
|
693
|
+
}
|
|
694
|
+
return body;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function emitPolicy(
|
|
698
|
+
policy: LawPolicyNode,
|
|
699
|
+
paths: ReadonlyMap<string, string>,
|
|
700
|
+
indent: number,
|
|
701
|
+
): string[] {
|
|
702
|
+
const pad = " ".repeat(indent);
|
|
703
|
+
const body = [
|
|
704
|
+
`${pad}custom uniform token nomos:policyKind = ${quote(policy.policyKind)}`,
|
|
705
|
+
...entityLabelOpinions(policy.entity, indent),
|
|
706
|
+
`${pad}custom uniform bool nomos:monotone = ${policy.monotone}`,
|
|
707
|
+
`${pad}custom uniform token nomos:onDishonor = ${quote(policy.onDishonor)}`,
|
|
708
|
+
];
|
|
709
|
+
if (policy.maxAgeMs !== undefined) body.push(`${pad}custom uniform uint64 nomos:maxAgeMs = ${policy.maxAgeMs}`);
|
|
710
|
+
if (policy.minSourceEpoch !== undefined) {
|
|
711
|
+
body.push(typeof policy.minSourceEpoch === "number"
|
|
712
|
+
? `${pad}custom uniform uint64 nomos:minSourceEpoch = ${policy.minSourceEpoch}`
|
|
713
|
+
: `${pad}custom uniform token nomos:minSourceEpochMode = ${quote(policy.minSourceEpoch)}`);
|
|
714
|
+
}
|
|
715
|
+
if (policy.policyKind === "pinned-source") {
|
|
716
|
+
body.push(
|
|
717
|
+
`${pad}custom uniform string nomos:workspace = ${quote(policy.workspace)}`,
|
|
718
|
+
`${pad}custom uniform string nomos:genesis = ${quote(policy.genesis)}`,
|
|
719
|
+
`${pad}custom uniform token nomos:roster = ${quote(policy.roster)}`,
|
|
720
|
+
);
|
|
721
|
+
if (policy.keyHashes.length > 0) {
|
|
722
|
+
body.push(`${pad}custom uniform string[] nomos:keyHashes = ${stringList(policy.keyHashes)}`);
|
|
723
|
+
}
|
|
724
|
+
} else {
|
|
725
|
+
body.push(
|
|
726
|
+
`${pad}custom rel nomos:allowList = <${requiredPath(policy.allowList.id, paths)}>`,
|
|
727
|
+
`${pad}custom rel nomos:genesisField = <${requiredPath(policy.genesisField.id, paths)}>`,
|
|
728
|
+
`${pad}custom rel nomos:keysField = <${requiredPath(policy.keysField.id, paths)}>`,
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
return body;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function emitInvariant(
|
|
735
|
+
invariant: LawInvariantNode,
|
|
736
|
+
paths: ReadonlyMap<string, string>,
|
|
737
|
+
indent: number,
|
|
738
|
+
): string[] {
|
|
739
|
+
const pad = " ".repeat(indent);
|
|
740
|
+
const target = invariant.invariantKind === "aggregate" ? invariant.onAggregate.id : invariant.on.id;
|
|
741
|
+
const body = [
|
|
742
|
+
...entityLabelOpinions(invariant.entity, indent),
|
|
743
|
+
`${pad}custom rel nomos:on = <${requiredPath(target, paths)}>`,
|
|
744
|
+
...(invariant.invariantKind === "workspace" && invariant.evaluator !== undefined
|
|
745
|
+
? [`${pad}custom uniform token nomos:evaluator = ${quote(invariant.evaluator)}`]
|
|
746
|
+
: []),
|
|
747
|
+
];
|
|
748
|
+
if (invariant.invariantKind === "workspace" && invariant.reads !== undefined) {
|
|
749
|
+
body.push(`${pad}custom uniform token nomos:readEvaluator = "point-v1"`);
|
|
750
|
+
for (const [index, read] of invariant.reads.entries()) {
|
|
751
|
+
body.push(
|
|
752
|
+
"",
|
|
753
|
+
`${pad}def NomosInvariantRead "Read_${index}"`,
|
|
754
|
+
`${pad}{`,
|
|
755
|
+
`${pad} custom uniform string nomos:binding = ${quote(read.binding)}`,
|
|
756
|
+
`${pad} custom rel nomos:aggregate = <${requiredPath(read.aggregate.id, paths)}>`,
|
|
757
|
+
`${pad} custom uniform token nomos:state = ${quote(read.state)}`,
|
|
758
|
+
`${pad} custom uniform token nomos:idSource = ${quote(read.id.source)}`,
|
|
759
|
+
);
|
|
760
|
+
if (read.eachPayloadPath !== undefined) {
|
|
761
|
+
body.push(`${pad} custom uniform token[] nomos:eachPayloadPath = ${stringList(read.eachPayloadPath)}`);
|
|
762
|
+
}
|
|
763
|
+
if (read.id.source === "literal") {
|
|
764
|
+
body.push(`${pad} custom uniform string nomos:idLiteral = ${quote(read.id.value)}`);
|
|
765
|
+
} else if (read.id.source === "payload") {
|
|
766
|
+
body.push(
|
|
767
|
+
`${pad} custom uniform token[] nomos:idPayloadPath = ${stringList(read.id.path)}`,
|
|
768
|
+
`${pad} custom uniform string nomos:idPrefix = ${quote(read.id.prefix)}`,
|
|
769
|
+
`${pad} custom uniform string nomos:idSuffix = ${quote(read.id.suffix)}`,
|
|
770
|
+
`${pad} custom uniform bool nomos:optional = ${read.id.optional}`,
|
|
771
|
+
);
|
|
772
|
+
} else {
|
|
773
|
+
body.push(
|
|
774
|
+
`${pad} custom uniform bool nomos:optional = ${read.id.optional}`,
|
|
775
|
+
`${pad} def NomosReadIdTemplate "Id"`,
|
|
776
|
+
`${pad} {`,
|
|
777
|
+
);
|
|
778
|
+
for (const [partIndex, part] of read.id.parts.entries()) {
|
|
779
|
+
body.push(
|
|
780
|
+
`${pad} def NomosReadIdPart "Part_${partIndex}"`,
|
|
781
|
+
`${pad} {`,
|
|
782
|
+
`${pad} custom uniform token nomos:source = ${quote(part.source)}`,
|
|
783
|
+
);
|
|
784
|
+
if (part.source === "literal") {
|
|
785
|
+
body.push(`${pad} custom uniform string nomos:value = ${quote(part.value)}`);
|
|
786
|
+
} else {
|
|
787
|
+
body.push(
|
|
788
|
+
`${pad} custom uniform token[] nomos:path = ${stringList(part.path)}`,
|
|
789
|
+
`${pad} custom uniform token nomos:transform = ${quote(part.transform)}`,
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
body.push(`${pad} }`);
|
|
793
|
+
}
|
|
794
|
+
body.push(`${pad} }`);
|
|
795
|
+
}
|
|
796
|
+
body.push(`${pad}}`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
if (invariant.invariantKind === "workspace" && invariant.pointGuard !== undefined) {
|
|
800
|
+
const guard = invariant.pointGuard;
|
|
801
|
+
body.push(
|
|
802
|
+
`${pad}custom rel nomos:aggregate = <${requiredPath(guard.aggregate.id, paths)}>`,
|
|
803
|
+
`${pad}custom uniform token nomos:binding = ${quote(guard.binding)}`,
|
|
804
|
+
`${pad}custom uniform token nomos:state = ${quote(guard.state)}`,
|
|
805
|
+
`${pad}custom uniform token[] nomos:idPayloadPath = ${stringList(guard.idPayloadPath)}`,
|
|
806
|
+
`${pad}custom uniform string nomos:rejectCode = ${quote(guard.rejectCode)}`,
|
|
807
|
+
"",
|
|
808
|
+
...emitPointGuardPredicate(guard.predicate, "Predicate", indent),
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
return body;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function emitPointGuardPredicate(
|
|
815
|
+
predicate: NonNullable<Extract<LawInvariantNode, { invariantKind: "workspace" }>["pointGuard"]>["predicate"],
|
|
816
|
+
name: string,
|
|
817
|
+
indent: number,
|
|
818
|
+
): string[] {
|
|
819
|
+
const pad = " ".repeat(indent);
|
|
820
|
+
const type = predicate.kind === "exists" ? "NomosExists"
|
|
821
|
+
: predicate.kind === "absent" ? "NomosAbsent"
|
|
822
|
+
: predicate.kind === "present" ? "NomosPathPresent"
|
|
823
|
+
: predicate.kind === "missing" ? "NomosPathMissing"
|
|
824
|
+
: predicate.kind === "compare" ? "NomosCompare"
|
|
825
|
+
: predicate.kind === "all" ? "NomosAll"
|
|
826
|
+
: predicate.kind === "any" ? "NomosAny"
|
|
827
|
+
: "NomosNot";
|
|
828
|
+
const body = [`${pad}def ${type} ${quote(name)}`, `${pad}{`];
|
|
829
|
+
const prop = " ".repeat(indent + 4);
|
|
830
|
+
if (predicate.kind === "present" || predicate.kind === "missing") {
|
|
831
|
+
body.push(`${prop}custom uniform token[] nomos:path = ${stringList(predicate.operand.path)}`);
|
|
832
|
+
} else if (predicate.kind === "compare") {
|
|
833
|
+
body.push(
|
|
834
|
+
`${prop}custom uniform token nomos:operator = ${quote(predicate.operator)}`,
|
|
835
|
+
`${prop}custom uniform token[] nomos:leftPath = ${stringList(predicate.left.path)}`,
|
|
836
|
+
`${prop}custom uniform token nomos:rightSource = ${quote(predicate.right.source)}`,
|
|
837
|
+
);
|
|
838
|
+
if (predicate.right.source === "literal") {
|
|
839
|
+
const value = predicate.right.value;
|
|
840
|
+
if (typeof value === "string") body.push(`${prop}custom uniform string nomos:stringValue = ${quote(value)}`);
|
|
841
|
+
else if (typeof value === "boolean") body.push(`${prop}custom uniform bool nomos:boolValue = ${value}`);
|
|
842
|
+
else body.push(`${prop}custom uniform int64 nomos:intValue = ${value}`);
|
|
843
|
+
} else {
|
|
844
|
+
body.push(`${prop}custom uniform token[] nomos:rightPath = ${stringList(predicate.right.path)}`);
|
|
845
|
+
}
|
|
846
|
+
} else if (predicate.kind === "all" || predicate.kind === "any") {
|
|
847
|
+
body.push(`${prop}def Scope "Terms"`, `${prop}{`);
|
|
848
|
+
for (const [index, term] of predicate.terms.entries()) {
|
|
849
|
+
body.push(...emitPointGuardPredicate(term, `t_${index.toString().padStart(4, "0")}`, indent + 8));
|
|
850
|
+
}
|
|
851
|
+
body.push(`${prop}}`);
|
|
852
|
+
} else if (predicate.kind === "not") {
|
|
853
|
+
body.push(...emitPointGuardPredicate(predicate.term, "Term", indent + 4));
|
|
854
|
+
}
|
|
855
|
+
body.push(`${pad}}`);
|
|
856
|
+
return body;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function emitCapability(
|
|
860
|
+
capability: LawCapabilityNode,
|
|
861
|
+
paths: ReadonlyMap<string, string>,
|
|
862
|
+
indent: number,
|
|
863
|
+
): string[] {
|
|
864
|
+
const pad = " ".repeat(indent);
|
|
865
|
+
return [
|
|
866
|
+
...entityLabelOpinions(capability.entity, indent),
|
|
867
|
+
`${pad}custom uniform token nomos:capability = ${quote(capability.capability ?? capability.entity.label)}`,
|
|
868
|
+
...(capability.endpoint === undefined
|
|
869
|
+
? []
|
|
870
|
+
: [`${pad}custom uniform asset nomos:endpoint = @${capability.endpoint.replaceAll("@", "@@")}@`]),
|
|
871
|
+
`${pad}custom rel nomos:aggregate = <${requiredPath(capability.task.id, paths)}>`,
|
|
872
|
+
`${pad}custom rel nomos:obligation = <${requiredPath(capability.obligation.id, paths)}>`,
|
|
873
|
+
`${pad}custom rel nomos:complete = <${requiredPath(capability.complete.id, paths)}>`,
|
|
874
|
+
`${pad}custom rel nomos:fail = <${requiredPath(capability.fail.id, paths)}>`,
|
|
875
|
+
`${pad}custom rel nomos:block = <${requiredPath(capability.block.id, paths)}>`,
|
|
876
|
+
`${pad}custom rel nomos:deadLetter = <${requiredPath(capability.deadLetter.id, paths)}>`,
|
|
877
|
+
...(capability.tasks === undefined
|
|
878
|
+
? []
|
|
879
|
+
: [`${pad}custom rel nomos:tasks = <${requiredPath(capability.tasks.id, paths)}>`]),
|
|
880
|
+
];
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function emitWorkspaceType(
|
|
884
|
+
workspaceType: LawWorkspaceTypeNode,
|
|
885
|
+
paths: ReadonlyMap<string, string>,
|
|
886
|
+
indent: number,
|
|
887
|
+
): string[] {
|
|
888
|
+
const pad = " ".repeat(indent);
|
|
889
|
+
const body = [
|
|
890
|
+
...entityLabelOpinions(workspaceType.entity, indent),
|
|
891
|
+
`${pad}custom uniform token nomos:mode = ${quote(workspaceType.mode)}`,
|
|
892
|
+
];
|
|
893
|
+
if (workspaceType.root !== undefined) body.push(`${pad}custom rel nomos:root = <${requiredPath(workspaceType.root.id, paths)}>`);
|
|
894
|
+
if (workspaceType.children.length > 0) {
|
|
895
|
+
body.push(`${pad}custom rel nomos:children = ${relList(workspaceType.children
|
|
896
|
+
.map((child) => `<${requiredPath(child.id, paths)}>`))}`);
|
|
897
|
+
}
|
|
898
|
+
if (workspaceType.globals.length > 0) {
|
|
899
|
+
body.push(`${pad}custom rel nomos:globals = ${relList(workspaceType.globals
|
|
900
|
+
.map((aggregate) => `<${requiredPath(aggregate.id, paths)}>`))}`);
|
|
901
|
+
}
|
|
902
|
+
if (workspaceType.coordinatorLocal.length > 0) {
|
|
903
|
+
body.push(`${pad}custom rel nomos:coordinatorLocal = ${relList(workspaceType.coordinatorLocal
|
|
904
|
+
.map((aggregate) => `<${requiredPath(aggregate.id, paths)}>`))}`);
|
|
905
|
+
}
|
|
906
|
+
return body;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function emitFlow(flow: LawFlowNode, paths: ReadonlyMap<string, string>, indent: number): string[] {
|
|
910
|
+
const pad = " ".repeat(indent);
|
|
911
|
+
const body = [
|
|
912
|
+
`${pad}custom uniform token nomos:flowKind = ${quote(flow.flowKind)}`,
|
|
913
|
+
`${pad}custom rel nomos:on = <${requiredPath(flow.owner.id, paths)}>`,
|
|
914
|
+
`${pad}custom uniform string nomos:site = ${quote(flow.site)}`,
|
|
915
|
+
`${pad}custom uniform string nomos:reason = ${quote(flow.reason ?? "")}`,
|
|
916
|
+
];
|
|
917
|
+
if (flow.sink !== undefined) body.push(`${pad}custom rel nomos:sink = <${requiredPath(flow.sink.id, paths)}>`);
|
|
918
|
+
if (flow.facet !== undefined) body.push(`${pad}custom uniform token nomos:facet = ${quote(flow.facet)}`);
|
|
919
|
+
return body;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function emitIntent(
|
|
923
|
+
intent: LawIntentNode,
|
|
924
|
+
effects: readonly LawIntentEffectNode[],
|
|
925
|
+
foreignReads: readonly LawForeignReadNode[],
|
|
926
|
+
workspaceRelations: readonly LawWorkspaceRelationNode[],
|
|
927
|
+
paths: ReadonlyMap<string, string>,
|
|
928
|
+
resolveDependency: ResolveDependency,
|
|
929
|
+
indent: number,
|
|
930
|
+
): string[] {
|
|
931
|
+
const pad = " ".repeat(indent);
|
|
932
|
+
const body = [
|
|
933
|
+
...entityLabelOpinions(intent.entity, indent),
|
|
934
|
+
`${pad}custom uniform token nomos:marker = ${quote(markerToken(intent.marker))}`,
|
|
935
|
+
`${pad}custom uniform bool nomos:active = true`,
|
|
936
|
+
`${pad}custom uniform bool nomos:public = ${intent.public}`,
|
|
937
|
+
`${pad}custom uniform string nomos:publicRoute = ${quote(intent.publicRoute ?? intent.entity.label)}`,
|
|
938
|
+
`${pad}custom rel nomos:target = ${targets([intent.target], paths, resolveDependency)}`,
|
|
939
|
+
`${pad}custom rel nomos:accepts = <${requiredPath(intent.accepts.id, paths)}>`,
|
|
940
|
+
...(intent.publicAccepts === undefined
|
|
941
|
+
? []
|
|
942
|
+
: [`${pad}custom rel nomos:publicAccepts = <${requiredPath(intent.publicAccepts.id, paths)}>`]),
|
|
943
|
+
];
|
|
944
|
+
if (intent.reads.length > 0) {
|
|
945
|
+
body.push(`${pad}custom rel nomos:reads = ${targets(intent.reads, paths, resolveDependency)}`);
|
|
946
|
+
}
|
|
947
|
+
if (intent.authorization !== undefined) {
|
|
948
|
+
body.push(
|
|
949
|
+
`${pad}custom rel nomos:requires = ${targets([intent.authorization.relation], paths, resolveDependency)}`,
|
|
950
|
+
`${pad}custom uniform string nomos:requiresObject = ${quote(intent.authorization.object)}`,
|
|
951
|
+
);
|
|
952
|
+
if (intent.authorization.objectFrom !== undefined) {
|
|
953
|
+
body.push(
|
|
954
|
+
`${pad}custom uniform token nomos:requiresObjectType = ${quote(intent.authorization.objectFrom.recordType)}`,
|
|
955
|
+
`${pad}custom rel nomos:requiresObjectIdField = <${requiredPath(intent.authorization.objectFrom.idField.id, paths)}>`,
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
if (intent.requiresHostCapabilities.length > 0) {
|
|
960
|
+
body.push(
|
|
961
|
+
`${pad}custom uniform token[] nomos:requiresHostCapability = ${stringList(intent.requiresHostCapabilities)}`,
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
if (intent.resolves !== undefined) {
|
|
965
|
+
body.push(`${pad}custom rel nomos:resolves = <${requiredPath(intent.resolves.id, paths)}>`);
|
|
966
|
+
}
|
|
967
|
+
if (intent.births) body.push(`${pad}custom uniform bool nomos:births = true`);
|
|
968
|
+
if (intent.birthRecord !== undefined) {
|
|
969
|
+
body.push(
|
|
970
|
+
`${pad}custom rel nomos:birthRecord = <${requiredPath(intent.birthRecord.id, paths)}>`,
|
|
971
|
+
`${pad}custom uniform string nomos:birthPrefix = ${quote(intent.birthPrefix ?? "")}`,
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
if (intent.attestation !== undefined) {
|
|
975
|
+
body.push(
|
|
976
|
+
`${pad}custom uniform string nomos:attestationOwnerObject = ${quote(intent.attestation.ownerObject)}`,
|
|
977
|
+
`${pad}custom uniform string nomos:attestationOwnerField = ${quote(intent.attestation.ownerField)}`,
|
|
978
|
+
`${pad}custom rel nomos:attestationAsserterWorkspace = <${requiredPath(intent.attestation.asserterWorkspace.id, paths)}>`,
|
|
979
|
+
`${pad}custom rel nomos:attestationObject = <${requiredPath(intent.attestation.object.id, paths)}>`,
|
|
980
|
+
`${pad}custom rel nomos:attestationRelation = <${requiredPath(intent.attestation.relation.id, paths)}>`,
|
|
981
|
+
`${pad}custom rel nomos:attestationAt = <${requiredPath(intent.attestation.attestedAt.id, paths)}>`,
|
|
982
|
+
`${pad}custom rel nomos:attestationPublicKey = <${requiredPath(intent.attestation.publicKey.id, paths)}>`,
|
|
983
|
+
`${pad}custom rel nomos:attestationSignature = <${requiredPath(intent.attestation.signature.id, paths)}>`,
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
if (intent.delegation !== undefined) {
|
|
987
|
+
body.push(
|
|
988
|
+
`${pad}custom uniform token nomos:delegatedRelation = ${quote(intent.delegation.relation)}`,
|
|
989
|
+
`${pad}custom rel nomos:delegatedSubject = <${requiredPath(intent.delegation.subjectField.id, paths)}>`,
|
|
990
|
+
`${pad}custom rel nomos:delegationCertificate = <${requiredPath(intent.delegation.certField.id, paths)}>`,
|
|
991
|
+
`${pad}custom uniform string nomos:delegationParentKeyObject = ${quote(intent.delegation.parentKeyObject)}`,
|
|
992
|
+
`${pad}custom uniform string nomos:delegationParentKeyField = ${quote(intent.delegation.parentKeyField)}`,
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
if (intent.verifiedFirstEnroll !== undefined) {
|
|
996
|
+
body.push(
|
|
997
|
+
`${pad}custom rel nomos:firstEnrollToken = <${requiredPath(intent.verifiedFirstEnroll.tokenField.id, paths)}>`,
|
|
998
|
+
`${pad}custom rel nomos:firstEnrollKeyHash = <${requiredPath(intent.verifiedFirstEnroll.keyHashField.id, paths)}>`,
|
|
999
|
+
`${pad}custom rel nomos:firstEnrollVerifiedVia = <${requiredPath(intent.verifiedFirstEnroll.verifiedViaField.id, paths)}>`,
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
if (intent.eraCertification !== undefined) {
|
|
1003
|
+
body.push(`${pad}custom rel nomos:eraCertificate = <${requiredPath(intent.eraCertification.certField.id, paths)}>`);
|
|
1004
|
+
}
|
|
1005
|
+
if (intent.foreignReads.length > 0) {
|
|
1006
|
+
const byId = new Map(foreignReads.map((read) => [read.entity.id, read] as const));
|
|
1007
|
+
body.push("", `${pad}def Scope "ForeignReads"`, `${pad}{`);
|
|
1008
|
+
for (const ref of intent.foreignReads) {
|
|
1009
|
+
const read = byId.get(ref.id);
|
|
1010
|
+
if (read === undefined) fail(`intent '${intent.entity.label}' is missing foreign read '${ref.id}'`);
|
|
1011
|
+
body.push(
|
|
1012
|
+
`${pad} def Scope "x_${read.entity.sid}"`,
|
|
1013
|
+
`${pad} {`,
|
|
1014
|
+
`${pad} custom uniform string nomos:query = ${quote(read.query)}`,
|
|
1015
|
+
`${pad} custom rel nomos:source = <${requiredPath(read.source.id, paths)}>`,
|
|
1016
|
+
...(read.fromField === undefined ? [] : [
|
|
1017
|
+
`${pad} custom rel nomos:fromField = <${requiredPath(read.fromField.id, paths)}>`,
|
|
1018
|
+
]),
|
|
1019
|
+
`${pad} }`,
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
body.push(`${pad}}`);
|
|
1023
|
+
}
|
|
1024
|
+
const ownedWorkspaceRelations = intent.workspaceRelations ?? [];
|
|
1025
|
+
if (ownedWorkspaceRelations.length > 0) {
|
|
1026
|
+
const byId = new Map(workspaceRelations.map((relation) => [relation.entity.id, relation] as const));
|
|
1027
|
+
body.push("", `${pad}def Scope "WorkspaceRelations"`, `${pad}{`);
|
|
1028
|
+
for (const ref of ownedWorkspaceRelations) {
|
|
1029
|
+
const relation = byId.get(ref.id);
|
|
1030
|
+
if (relation === undefined) fail(`intent '${intent.entity.label}' is missing workspace relation '${ref.id}'`);
|
|
1031
|
+
body.push(
|
|
1032
|
+
`${pad} def NomosWorkspaceRelation "w_${relation.entity.sid}"`,
|
|
1033
|
+
`${pad} {`,
|
|
1034
|
+
`${pad} custom uniform string nomos:label = ${quote(relation.relation)}`,
|
|
1035
|
+
`${pad} custom uniform token nomos:sourceType = ${quote(relation.sourceType)}`,
|
|
1036
|
+
`${pad} custom uniform token nomos:targetType = ${quote(relation.targetType)}`,
|
|
1037
|
+
`${pad} custom rel nomos:proposes = <${requiredPath(intent.entity.id, paths)}>`,
|
|
1038
|
+
`${pad} custom uniform token nomos:evidenceAggregate = ${quote(relation.evidenceAggregate)}`,
|
|
1039
|
+
`${pad} custom uniform token[] nomos:evidenceSelect = ${stringList(relation.evidenceSelect)}`,
|
|
1040
|
+
`${pad} custom uniform token nomos:evidenceKind = ${quote(relation.evidenceKind)}`,
|
|
1041
|
+
`${pad} custom uniform bool nomos:hasInvariant = ${relation.hasInvariant}`,
|
|
1042
|
+
);
|
|
1043
|
+
if (relation.bindings.length > 0) {
|
|
1044
|
+
body.push("", `${pad} def Scope "Bindings"`, `${pad} {`);
|
|
1045
|
+
for (const [index, binding] of relation.bindings.entries()) {
|
|
1046
|
+
body.push(
|
|
1047
|
+
`${pad} def NomosEvidenceBinding "b_${index.toString().padStart(4, "0")}"`,
|
|
1048
|
+
`${pad} {`,
|
|
1049
|
+
`${pad} custom uniform string nomos:evidenceField = ${quote(binding.evidenceField)}`,
|
|
1050
|
+
`${pad} custom uniform string nomos:directiveParameter = ${quote(binding.directiveParameter)}`,
|
|
1051
|
+
`${pad} }`,
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
body.push(`${pad} }`);
|
|
1055
|
+
}
|
|
1056
|
+
body.push(`${pad} }`);
|
|
1057
|
+
}
|
|
1058
|
+
body.push(`${pad}}`);
|
|
1059
|
+
}
|
|
1060
|
+
const byId = new Map(effects.map((effect) => [effect.entity.id, effect] as const));
|
|
1061
|
+
const owned = intent.effects.map((effect) => {
|
|
1062
|
+
const node = byId.get(effect.id);
|
|
1063
|
+
if (node === undefined) fail(`intent '${intent.entity.label}' is missing effect '${effect.id}'`);
|
|
1064
|
+
return node;
|
|
1065
|
+
});
|
|
1066
|
+
if (owned.length > 0) {
|
|
1067
|
+
body.push("", `${pad}def Scope "Effects"`, `${pad}{`);
|
|
1068
|
+
for (const [index, effect] of owned.entries()) {
|
|
1069
|
+
if (index > 0) body.push("");
|
|
1070
|
+
body.push(
|
|
1071
|
+
`${pad} def Scope "e_${effect.entity.sid}"`,
|
|
1072
|
+
`${pad} {`,
|
|
1073
|
+
`${pad} custom uniform token nomos:marker = ${quote(markerToken(effect.marker))}`,
|
|
1074
|
+
...(effect.cardinality === "many" ? [] : [`${pad} custom uniform int nomos:max = 1`]),
|
|
1075
|
+
`${pad} custom rel nomos:target = ${targets([effect.aggregate], paths, resolveDependency)}`,
|
|
1076
|
+
`${pad} }`,
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
body.push(`${pad}}`);
|
|
1080
|
+
}
|
|
1081
|
+
return body;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function markerToken(marker: LawIntentNode["marker"]): "Create" | "Mutate" | "Ensure" | "Archive" {
|
|
1085
|
+
switch (marker) {
|
|
1086
|
+
case "creates": return "Create";
|
|
1087
|
+
case "mutates": return "Mutate";
|
|
1088
|
+
case "ensures": return "Ensure";
|
|
1089
|
+
case "archives": return "Archive";
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function emitRead(
|
|
1094
|
+
read: LawReadNode,
|
|
1095
|
+
contracts: readonly LawReadContractNode[],
|
|
1096
|
+
predicates: readonly LawPredicateNode[],
|
|
1097
|
+
paths: ReadonlyMap<string, string>,
|
|
1098
|
+
indent: number,
|
|
1099
|
+
): string[] {
|
|
1100
|
+
const pad = " ".repeat(indent);
|
|
1101
|
+
const contract = contracts.find((candidate) => candidate.entity.id === read.contract.id);
|
|
1102
|
+
if (contract === undefined) fail(`read '${read.entity.label}' is missing its result contract`);
|
|
1103
|
+
const returns = contract.returns.kind === "rows"
|
|
1104
|
+
? contract.returns.aggregate.id
|
|
1105
|
+
: contract.returns.value.id;
|
|
1106
|
+
const body = [
|
|
1107
|
+
...entityLabelOpinions(read.entity, indent),
|
|
1108
|
+
`${pad}custom uniform token nomos:readKind = ${quote(read.readKind)}`,
|
|
1109
|
+
`${pad}custom uniform bool nomos:public = ${read.public ?? false}`,
|
|
1110
|
+
`${pad}custom rel nomos:of = <${requiredPath(read.of.id, paths)}>`,
|
|
1111
|
+
`${pad}custom rel nomos:returns = <${requiredPath(returns, paths)}>`,
|
|
1112
|
+
];
|
|
1113
|
+
const fieldTarget = (field: { readonly field: { readonly id: string }; readonly path: readonly { readonly id: string }[] }): string => {
|
|
1114
|
+
const terminal = field.path[field.path.length - 1] ?? field.field;
|
|
1115
|
+
return `<${requiredPath(terminal.id, paths)}>`;
|
|
1116
|
+
};
|
|
1117
|
+
switch (read.readKind) {
|
|
1118
|
+
case "query":
|
|
1119
|
+
body.push(`${pad}custom rel nomos:key = ${relList(read.key.map(fieldTarget))}`);
|
|
1120
|
+
body.push(`${pad}custom uniform string[] nomos:keyPath = ${stringList(read.key.map((field) => field.authoredPath))}`);
|
|
1121
|
+
break;
|
|
1122
|
+
case "count":
|
|
1123
|
+
case "exists":
|
|
1124
|
+
if (read.groupBy !== undefined) {
|
|
1125
|
+
body.push(`${pad}custom rel nomos:groupBy = ${fieldTarget(read.groupBy)}`);
|
|
1126
|
+
body.push(`${pad}custom uniform string nomos:groupByPath = ${quote(read.groupBy.authoredPath)}`);
|
|
1127
|
+
}
|
|
1128
|
+
break;
|
|
1129
|
+
case "sum":
|
|
1130
|
+
case "min":
|
|
1131
|
+
case "max":
|
|
1132
|
+
body.push(`${pad}custom rel nomos:field = ${fieldTarget(read.value)}`);
|
|
1133
|
+
body.push(`${pad}custom uniform string nomos:fieldPath = ${quote(read.value.authoredPath)}`);
|
|
1134
|
+
if (read.groupBy !== undefined) {
|
|
1135
|
+
body.push(`${pad}custom rel nomos:groupBy = ${fieldTarget(read.groupBy)}`);
|
|
1136
|
+
body.push(`${pad}custom uniform string nomos:groupByPath = ${quote(read.groupBy.authoredPath)}`);
|
|
1137
|
+
}
|
|
1138
|
+
break;
|
|
1139
|
+
case "ordered":
|
|
1140
|
+
body.push(`${pad}custom rel nomos:orderKey = ${fieldTarget(read.orderBy)}`);
|
|
1141
|
+
body.push(`${pad}custom uniform string nomos:orderKeyPath = ${quote(read.orderBy.authoredPath)}`);
|
|
1142
|
+
body.push(`${pad}custom uniform bool nomos:descending = ${read.descending}`);
|
|
1143
|
+
body.push(`${pad}custom uniform int nomos:limit = ${read.limit}`);
|
|
1144
|
+
break;
|
|
1145
|
+
case "spatial":
|
|
1146
|
+
body.push(`${pad}custom rel nomos:spatialOn = ${fieldTarget(read.geometry)}`);
|
|
1147
|
+
body.push(`${pad}custom uniform string nomos:spatialPath = ${quote(read.geometry.authoredPath)}`);
|
|
1148
|
+
break;
|
|
1149
|
+
case "derived":
|
|
1150
|
+
break;
|
|
1151
|
+
case "combined":
|
|
1152
|
+
body.push(`${pad}custom rel nomos:refField = ${fieldTarget(read.via)}`);
|
|
1153
|
+
body.push(`${pad}custom uniform string nomos:refFieldPath = ${quote(read.via.authoredPath)}`);
|
|
1154
|
+
body.push(`${pad}custom rel nomos:reads = <${requiredPath(read.reads.id, paths)}>`);
|
|
1155
|
+
break;
|
|
1156
|
+
}
|
|
1157
|
+
if (read.predicate !== undefined) {
|
|
1158
|
+
body.push(`${pad}custom rel nomos:where = <${requiredPath(read.predicate.id, paths)}>`);
|
|
1159
|
+
const owned = predicates.filter((predicate) => predicate.owner.id === read.entity.id);
|
|
1160
|
+
body.push("", `${pad}def Scope "Predicates"`, `${pad}{`);
|
|
1161
|
+
for (const [index, predicate] of owned.entries()) {
|
|
1162
|
+
if (index > 0) body.push("");
|
|
1163
|
+
body.push(...emitReadPredicate(predicate, paths, indent + 4));
|
|
1164
|
+
}
|
|
1165
|
+
body.push(`${pad}}`);
|
|
1166
|
+
}
|
|
1167
|
+
return body;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function emitReadPredicate(
|
|
1171
|
+
predicate: LawPredicateNode,
|
|
1172
|
+
paths: ReadonlyMap<string, string>,
|
|
1173
|
+
indent: number,
|
|
1174
|
+
): string[] {
|
|
1175
|
+
const pad = " ".repeat(indent);
|
|
1176
|
+
const expression = predicate.expression;
|
|
1177
|
+
const body = [
|
|
1178
|
+
`${pad}def Scope "p_${predicate.entity.sid}"`,
|
|
1179
|
+
`${pad}{`,
|
|
1180
|
+
`${pad} custom uniform token nomos:operator = ${quote(expression.kind)}`,
|
|
1181
|
+
];
|
|
1182
|
+
if ("field" in expression) {
|
|
1183
|
+
const terminal = expression.field.path[expression.field.path.length - 1] ?? expression.field.field;
|
|
1184
|
+
body.push(`${pad} custom rel nomos:field = <${requiredPath(terminal.id, paths)}>`);
|
|
1185
|
+
body.push(`${pad} custom uniform string nomos:fieldPath = ${quote(expression.field.authoredPath)}`);
|
|
1186
|
+
body.push(`${pad} custom uniform string nomos:value = ${quote(expression.value)}`);
|
|
1187
|
+
} else {
|
|
1188
|
+
body.push(`${pad} custom rel nomos:terms = ${relList(expression.terms
|
|
1189
|
+
.map((term) => `<${requiredPath(term.id, paths)}>`))}`);
|
|
1190
|
+
}
|
|
1191
|
+
body.push(`${pad}}`);
|
|
1192
|
+
return body;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function emitRelation(
|
|
1196
|
+
relation: LawRelationNode,
|
|
1197
|
+
predicates: readonly LawRelationPredicateNode[],
|
|
1198
|
+
paths: ReadonlyMap<string, string>,
|
|
1199
|
+
indent: number,
|
|
1200
|
+
): string[] {
|
|
1201
|
+
const pad = " ".repeat(indent);
|
|
1202
|
+
const objectType = requiredPath(relation.object.id, paths);
|
|
1203
|
+
const body = [
|
|
1204
|
+
`${pad}custom uniform token nomos:relationKind = "authorization"`,
|
|
1205
|
+
...entityLabelOpinions(relation.entity, indent),
|
|
1206
|
+
`${pad}custom uniform string nomos:relation = ${quote(relation.relation)}`,
|
|
1207
|
+
`${pad}custom uniform int nomos:maxDepth = ${relation.maxDepth}`,
|
|
1208
|
+
`${pad}custom uniform int nomos:maxFanout = ${relation.maxFanout}`,
|
|
1209
|
+
`${pad}custom rel nomos:objectType = <${objectType}>`,
|
|
1210
|
+
`${pad}custom uniform token nomos:objectNamespace = ${quote(relation.objectType)}`,
|
|
1211
|
+
];
|
|
1212
|
+
if (relation.allowed.length > 0) {
|
|
1213
|
+
body.push(`${pad}custom uniform token[] nomos:subjectTypes = [${relation.allowed
|
|
1214
|
+
.map((subject) => quote(subject.kind === "wildcard"
|
|
1215
|
+
? "user:*"
|
|
1216
|
+
: subject.kind === "direct"
|
|
1217
|
+
? subject.type
|
|
1218
|
+
: `${subject.type}#${subject.relationName}`))
|
|
1219
|
+
.join(", ")}]`);
|
|
1220
|
+
}
|
|
1221
|
+
body.push(`${pad}custom rel nomos:rewrite = <${requiredPath(relation.rewrite.id, paths)}>`);
|
|
1222
|
+
const owned = predicates.filter((predicate) => predicate.owner.id === relation.entity.id);
|
|
1223
|
+
if (owned.length > 0) {
|
|
1224
|
+
body.push("", `${pad}def Scope "Predicates"`, `${pad}{`);
|
|
1225
|
+
for (const [index, predicate] of owned.entries()) {
|
|
1226
|
+
if (index > 0) body.push("");
|
|
1227
|
+
body.push(...emitRelationPredicate(predicate, paths, indent + 4));
|
|
1228
|
+
}
|
|
1229
|
+
body.push(`${pad}}`);
|
|
1230
|
+
}
|
|
1231
|
+
return body;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
/** Zanzibar has two semantically different named edges. A relation admits directly asserted
|
|
1235
|
+
* tuples (and may additionally compose them); a permission is computed only. Keep that distinction
|
|
1236
|
+
* in the nominal USDA type so generic USD inspection and the kernel gate do not have to infer
|
|
1237
|
+
* writability from an empty subject-type array. */
|
|
1238
|
+
function authorizationDefinitionType(relation: LawRelationNode): "NomosRelation" | "NomosPermission" {
|
|
1239
|
+
return relation.allowed.length === 0 ? "NomosPermission" : "NomosRelation";
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
function emitRelationPredicate(
|
|
1243
|
+
predicate: LawRelationPredicateNode,
|
|
1244
|
+
paths: ReadonlyMap<string, string>,
|
|
1245
|
+
indent: number,
|
|
1246
|
+
): string[] {
|
|
1247
|
+
const pad = " ".repeat(indent);
|
|
1248
|
+
const expression = predicate.expression;
|
|
1249
|
+
const primType = (() => {
|
|
1250
|
+
switch (expression.kind) {
|
|
1251
|
+
case "this": return "NomosDirectUserset";
|
|
1252
|
+
case "computed": return "NomosComputedUserset";
|
|
1253
|
+
case "union": return "NomosUnion";
|
|
1254
|
+
case "intersection": return "NomosIntersection";
|
|
1255
|
+
case "exclusion": return "NomosExclusion";
|
|
1256
|
+
case "ttu": return "NomosTupleToUserset";
|
|
1257
|
+
}
|
|
1258
|
+
})();
|
|
1259
|
+
const body = [
|
|
1260
|
+
`${pad}def ${primType} "p_${predicate.entity.sid}"`,
|
|
1261
|
+
`${pad}{`,
|
|
1262
|
+
];
|
|
1263
|
+
switch (expression.kind) {
|
|
1264
|
+
case "this":
|
|
1265
|
+
body.push(`${pad} custom uniform token nomos:operator = "this"`);
|
|
1266
|
+
break;
|
|
1267
|
+
case "computed":
|
|
1268
|
+
body.push(`${pad} custom uniform token nomos:operator = "computedUserset"`);
|
|
1269
|
+
body.push(`${pad} custom rel nomos:relation = <${requiredPath(expression.relation.id, paths)}>`);
|
|
1270
|
+
break;
|
|
1271
|
+
case "union":
|
|
1272
|
+
case "intersection":
|
|
1273
|
+
body.push(`${pad} custom uniform token nomos:operator = ${quote(expression.kind)}`);
|
|
1274
|
+
body.push(`${pad} custom rel nomos:terms = ${relList(expression.terms
|
|
1275
|
+
.map((term) => `<${requiredPath(term.id, paths)}>`))}`);
|
|
1276
|
+
break;
|
|
1277
|
+
case "exclusion":
|
|
1278
|
+
body.push(`${pad} custom uniform token nomos:operator = "difference"`);
|
|
1279
|
+
body.push(`${pad} custom rel nomos:terms = ${relList([expression.base, expression.subtract]
|
|
1280
|
+
.map((term) => `<${requiredPath(term.id, paths)}>`))}`);
|
|
1281
|
+
break;
|
|
1282
|
+
case "ttu":
|
|
1283
|
+
body.push(`${pad} custom uniform token nomos:operator = "tupleToUserset"`);
|
|
1284
|
+
body.push(`${pad} custom rel nomos:terms = ${relList([
|
|
1285
|
+
`<${requiredPath(expression.tupleset.id, paths)}>`,
|
|
1286
|
+
...expression.computed.map((relation) => `<${requiredPath(relation.id, paths)}>`),
|
|
1287
|
+
])}`);
|
|
1288
|
+
if (expression.all) body.push(`${pad} custom uniform bool nomos:all = true`);
|
|
1289
|
+
break;
|
|
1290
|
+
}
|
|
1291
|
+
body.push(`${pad}}`);
|
|
1292
|
+
return body;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
function emitContract(
|
|
1296
|
+
contract: LawContractNode,
|
|
1297
|
+
fields: readonly LawFieldNode[],
|
|
1298
|
+
paths: ReadonlyMap<string, string>,
|
|
1299
|
+
resolveDependency: ResolveDependency,
|
|
1300
|
+
indent: number,
|
|
1301
|
+
): string[] {
|
|
1302
|
+
const pad = " ".repeat(indent);
|
|
1303
|
+
const body = [
|
|
1304
|
+
...entityLabelOpinions(contract.entity, indent),
|
|
1305
|
+
`${pad}custom uniform bool nomos:public = ${contract.public}`,
|
|
1306
|
+
];
|
|
1307
|
+
if (contract.implementedBy.length > 0) {
|
|
1308
|
+
body.push(`${pad}custom rel nomos:implementedBy = ${relList(contract.implementedBy
|
|
1309
|
+
.map((target) => `<${requiredPath(target.id, paths)}>`))}`);
|
|
1310
|
+
}
|
|
1311
|
+
const owned = fields.filter((field) => field.owner.id === contract.entity.id);
|
|
1312
|
+
if (owned.length > 0) {
|
|
1313
|
+
body.push("", `${pad}def Scope "Fields"`, `${pad}{`);
|
|
1314
|
+
for (const [index, field] of owned.entries()) {
|
|
1315
|
+
if (index > 0) body.push("");
|
|
1316
|
+
body.push(...emitField(field, paths, resolveDependency, indent + 4));
|
|
1317
|
+
}
|
|
1318
|
+
body.push(`${pad}}`);
|
|
1319
|
+
}
|
|
1320
|
+
return body;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function emitPayloadContract(
|
|
1324
|
+
contract: LawPayloadContractNode,
|
|
1325
|
+
fields: readonly LawAnonymousValueFieldNode[],
|
|
1326
|
+
types: ReadonlyMap<string, LawAnonymousValueTypeNode>,
|
|
1327
|
+
paths: ReadonlyMap<string, string>,
|
|
1328
|
+
indent: number,
|
|
1329
|
+
): string[] {
|
|
1330
|
+
const pad = " ".repeat(indent);
|
|
1331
|
+
const body = [...entityLabelOpinions(contract.entity, indent)];
|
|
1332
|
+
const byId = new Map(fields.map((field) => [field.entity.id, field] as const));
|
|
1333
|
+
const owned = contract.fields.map((field) => {
|
|
1334
|
+
const node = byId.get(field.id);
|
|
1335
|
+
if (node === undefined) fail(`payload contract '${contract.entity.label}' is missing field '${field.id}'`);
|
|
1336
|
+
return node;
|
|
1337
|
+
});
|
|
1338
|
+
if (owned.length > 0) {
|
|
1339
|
+
body.push("", `${pad}def Scope "Fields"`, `${pad}{`);
|
|
1340
|
+
for (const [index, field] of owned.entries()) {
|
|
1341
|
+
if (index > 0) body.push("");
|
|
1342
|
+
body.push(...emitAnonymousField(field, types, paths, indent + 4));
|
|
1343
|
+
}
|
|
1344
|
+
body.push(`${pad}}`);
|
|
1345
|
+
}
|
|
1346
|
+
return body;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function emitType(
|
|
1350
|
+
type: LawTypeNode,
|
|
1351
|
+
fields: readonly LawFieldNode[],
|
|
1352
|
+
variants: readonly LawAnonymousValueVariantNode[],
|
|
1353
|
+
paths: ReadonlyMap<string, string>,
|
|
1354
|
+
resolveDependency: ResolveDependency,
|
|
1355
|
+
indent: number,
|
|
1356
|
+
): string[] {
|
|
1357
|
+
const pad = " ".repeat(indent);
|
|
1358
|
+
if (type.form === "geometry") {
|
|
1359
|
+
if (type.geometry === undefined) fail(`type '${type.entity.label}' is missing its geometry kind`);
|
|
1360
|
+
return [
|
|
1361
|
+
`${pad}custom uniform token nomos:typeKind = "geometry"`,
|
|
1362
|
+
...entityLabelOpinions(type.entity, indent),
|
|
1363
|
+
`${pad}custom uniform token nomos:geometry = ${quote(type.geometry)}`,
|
|
1364
|
+
`${pad}custom uniform token nomos:encoding = "rfc7946"`,
|
|
1365
|
+
];
|
|
1366
|
+
}
|
|
1367
|
+
const body = [
|
|
1368
|
+
`${pad}custom uniform token nomos:typeKind = ${quote(type.form === "sum" ? "union" : "record")}`,
|
|
1369
|
+
...entityLabelOpinions(type.entity, indent),
|
|
1370
|
+
];
|
|
1371
|
+
if (type.form === "sum") {
|
|
1372
|
+
const byId = new Map(variants.map((variant) => [variant.entity.id, variant] as const));
|
|
1373
|
+
const owned = type.variants.map((variant) => {
|
|
1374
|
+
const node = byId.get(variant.id);
|
|
1375
|
+
if (node === undefined) fail(`type '${type.entity.label}' is missing variant '${variant.id}'`);
|
|
1376
|
+
return node;
|
|
1377
|
+
});
|
|
1378
|
+
if (owned.length > 0) {
|
|
1379
|
+
body.push("", `${pad}def Scope "Variants"`, `${pad}{`);
|
|
1380
|
+
for (const [index, variant] of owned.entries()) {
|
|
1381
|
+
if (index > 0) body.push("");
|
|
1382
|
+
body.push(
|
|
1383
|
+
`${pad} def Scope "v_${variant.entity.sid}"`,
|
|
1384
|
+
`${pad} {`,
|
|
1385
|
+
`${pad} custom uniform string nomos:label = ${quote(variant.label ?? variant.entity.label)}`,
|
|
1386
|
+
`${pad} custom rel nomos:type = <${requiredPath(variant.value.id, paths)}>`,
|
|
1387
|
+
`${pad} }`,
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
body.push(`${pad}}`);
|
|
1391
|
+
}
|
|
1392
|
+
return body;
|
|
1393
|
+
}
|
|
1394
|
+
const owned = fields.filter((field) => field.owner.id === type.entity.id);
|
|
1395
|
+
if (owned.length > 0) {
|
|
1396
|
+
body.push("", `${pad}def Scope "Fields"`, `${pad}{`);
|
|
1397
|
+
for (const [index, field] of owned.entries()) {
|
|
1398
|
+
if (index > 0) body.push("");
|
|
1399
|
+
body.push(...emitField(field, paths, resolveDependency, indent + 4));
|
|
1400
|
+
}
|
|
1401
|
+
body.push(`${pad}}`);
|
|
1402
|
+
}
|
|
1403
|
+
return body;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
function emitAnonymousType(
|
|
1407
|
+
type: LawAnonymousValueTypeNode,
|
|
1408
|
+
fields: readonly LawAnonymousValueFieldNode[],
|
|
1409
|
+
variants: readonly LawAnonymousValueVariantNode[],
|
|
1410
|
+
types: ReadonlyMap<string, LawAnonymousValueTypeNode>,
|
|
1411
|
+
paths: ReadonlyMap<string, string>,
|
|
1412
|
+
indent: number,
|
|
1413
|
+
): string[] {
|
|
1414
|
+
const pad = " ".repeat(indent);
|
|
1415
|
+
const expression = type.expression;
|
|
1416
|
+
const body: string[] = [];
|
|
1417
|
+
switch (expression.kind) {
|
|
1418
|
+
case "string":
|
|
1419
|
+
case "number":
|
|
1420
|
+
case "integer":
|
|
1421
|
+
body.push(`${pad}custom uniform token nomos:typeKind = "scalar"`);
|
|
1422
|
+
body.push(`${pad}custom uniform token nomos:scalar = ${quote(
|
|
1423
|
+
expression.kind === "integer" ? "int" : expression.kind,
|
|
1424
|
+
)}`);
|
|
1425
|
+
{
|
|
1426
|
+
const formats = expression.constraints
|
|
1427
|
+
.filter((constraint) => constraint.kind === "format")
|
|
1428
|
+
.map((constraint) => quote(constraint.value));
|
|
1429
|
+
if (formats.length > 0) {
|
|
1430
|
+
body.push(`${pad}custom uniform string[] nomos:formats = [${formats.join(", ")}]`);
|
|
1431
|
+
}
|
|
1432
|
+
const multiples = expression.constraints
|
|
1433
|
+
.filter((constraint) => constraint.kind === "multiple-of")
|
|
1434
|
+
.map((constraint) => constraint.value);
|
|
1435
|
+
if (multiples.length > 0) {
|
|
1436
|
+
body.push(`${pad}custom uniform double[] nomos:multiplesOf = [${multiples.join(", ")}]`);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
for (const constraint of expression.constraints) {
|
|
1440
|
+
switch (constraint.kind) {
|
|
1441
|
+
case "min-length":
|
|
1442
|
+
body.push(`${pad}custom uniform int nomos:minLength = ${constraint.value}`);
|
|
1443
|
+
break;
|
|
1444
|
+
case "max-length":
|
|
1445
|
+
body.push(`${pad}custom uniform int nomos:maxLength = ${constraint.value}`);
|
|
1446
|
+
break;
|
|
1447
|
+
case "pattern":
|
|
1448
|
+
body.push(`${pad}custom uniform string nomos:pattern = ${quote(constraint.value)}`);
|
|
1449
|
+
break;
|
|
1450
|
+
case "minimum":
|
|
1451
|
+
case "maximum":
|
|
1452
|
+
body.push(
|
|
1453
|
+
`${pad}custom uniform double nomos:${constraint.inclusive ? constraint.kind : `exclusive${constraint.kind[0]!.toUpperCase()}${constraint.kind.slice(1)}`} = ${constraint.value}`,
|
|
1454
|
+
);
|
|
1455
|
+
break;
|
|
1456
|
+
case "format":
|
|
1457
|
+
case "multiple-of":
|
|
1458
|
+
break;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
break;
|
|
1462
|
+
case "boolean":
|
|
1463
|
+
body.push(`${pad}custom uniform token nomos:typeKind = "scalar"`);
|
|
1464
|
+
body.push(`${pad}custom uniform token nomos:scalar = "bool"`);
|
|
1465
|
+
break;
|
|
1466
|
+
case "null":
|
|
1467
|
+
body.push(`${pad}custom uniform token nomos:typeKind = "scalar"`);
|
|
1468
|
+
body.push(`${pad}custom uniform token nomos:scalar = "null"`);
|
|
1469
|
+
break;
|
|
1470
|
+
case "unknown":
|
|
1471
|
+
body.push(`${pad}custom uniform token nomos:typeKind = "scalar"`);
|
|
1472
|
+
body.push(`${pad}custom uniform token nomos:scalar = "json"`);
|
|
1473
|
+
break;
|
|
1474
|
+
case "enum":
|
|
1475
|
+
body.push(`${pad}custom uniform token nomos:typeKind = "scalar"`);
|
|
1476
|
+
body.push(`${pad}custom uniform token nomos:scalar = "string"`);
|
|
1477
|
+
body.push(`${pad}custom uniform token[] nomos:enum = [${expression.values.map(quote).join(", ")}]`);
|
|
1478
|
+
break;
|
|
1479
|
+
case "literal":
|
|
1480
|
+
body.push(`${pad}custom uniform token nomos:typeKind = "scalar"`);
|
|
1481
|
+
if (typeof expression.value === "string") {
|
|
1482
|
+
body.push(`${pad}custom uniform token nomos:scalar = "string"`);
|
|
1483
|
+
body.push(`${pad}custom uniform token[] nomos:enum = [${quote(expression.value)}]`);
|
|
1484
|
+
} else if (typeof expression.value === "boolean") {
|
|
1485
|
+
body.push(`${pad}custom uniform token nomos:scalar = "bool"`);
|
|
1486
|
+
body.push(`${pad}custom uniform bool[] nomos:enum = [${expression.value}]`);
|
|
1487
|
+
} else if (typeof expression.value === "number") {
|
|
1488
|
+
const integer = Number.isInteger(expression.value);
|
|
1489
|
+
body.push(`${pad}custom uniform token nomos:scalar = "${integer ? "int" : "number"}"`);
|
|
1490
|
+
body.push(`${pad}custom uniform ${integer ? "int" : "double"}[] nomos:enum = [${expression.value}]`);
|
|
1491
|
+
} else {
|
|
1492
|
+
// `null` is already a singleton type: unlike strings/numbers/booleans it needs no
|
|
1493
|
+
// additional enum opinion to distinguish the literal from its scalar family.
|
|
1494
|
+
body.push(`${pad}custom uniform token nomos:scalar = "null"`);
|
|
1495
|
+
}
|
|
1496
|
+
break;
|
|
1497
|
+
case "list":
|
|
1498
|
+
body.push(`${pad}custom uniform token nomos:typeKind = "list"`);
|
|
1499
|
+
body.push(`${pad}custom rel nomos:elementType = <${requiredPath(expression.element.id, paths)}>`);
|
|
1500
|
+
break;
|
|
1501
|
+
case "map":
|
|
1502
|
+
body.push(`${pad}custom uniform token nomos:typeKind = "map"`);
|
|
1503
|
+
body.push(`${pad}custom rel nomos:valueType = <${requiredPath(expression.value.id, paths)}>`);
|
|
1504
|
+
break;
|
|
1505
|
+
case "record": {
|
|
1506
|
+
body.push(`${pad}custom uniform token nomos:typeKind = "record"`);
|
|
1507
|
+
const owned = fields.filter((field) => field.owner.id === type.entity.id);
|
|
1508
|
+
if (owned.length > 0) {
|
|
1509
|
+
body.push("", `${pad}def Scope "Fields"`, `${pad}{`);
|
|
1510
|
+
for (const [index, field] of owned.entries()) {
|
|
1511
|
+
if (index > 0) body.push("");
|
|
1512
|
+
body.push(...emitAnonymousField(field, types, paths, indent + 4));
|
|
1513
|
+
}
|
|
1514
|
+
body.push(`${pad}}`);
|
|
1515
|
+
}
|
|
1516
|
+
break;
|
|
1517
|
+
}
|
|
1518
|
+
case "sum": {
|
|
1519
|
+
body.push(`${pad}custom uniform token nomos:typeKind = "union"`);
|
|
1520
|
+
const owned = variants.filter((variant) => variant.owner.id === type.entity.id);
|
|
1521
|
+
if (owned.length > 0) {
|
|
1522
|
+
body.push("", `${pad}def Scope "Variants"`, `${pad}{`);
|
|
1523
|
+
for (const [index, variant] of owned.entries()) {
|
|
1524
|
+
if (index > 0) body.push("");
|
|
1525
|
+
body.push(
|
|
1526
|
+
`${pad} def Scope "v_${variant.entity.sid}"`,
|
|
1527
|
+
`${pad} {`,
|
|
1528
|
+
`${pad} custom rel nomos:type = <${requiredPath(variant.value.id, paths)}>`,
|
|
1529
|
+
`${pad} }`,
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1532
|
+
body.push(`${pad}}`);
|
|
1533
|
+
}
|
|
1534
|
+
break;
|
|
1535
|
+
}
|
|
1536
|
+
case "nullable":
|
|
1537
|
+
case "default":
|
|
1538
|
+
fail(`anonymous type '${type.entity.label}' wrapper '${expression.kind}' cannot own a W1 type prim`);
|
|
1539
|
+
case "recursive":
|
|
1540
|
+
body.push(`${pad}custom uniform token nomos:typeKind = "recursive"`);
|
|
1541
|
+
body.push(`${pad}custom rel nomos:body = <${requiredPath(expression.value.id, paths)}>`);
|
|
1542
|
+
break;
|
|
1543
|
+
}
|
|
1544
|
+
return body;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
function emitAnonymousField(
|
|
1548
|
+
field: LawAnonymousValueFieldNode,
|
|
1549
|
+
types: ReadonlyMap<string, LawAnonymousValueTypeNode>,
|
|
1550
|
+
paths: ReadonlyMap<string, string>,
|
|
1551
|
+
indent: number,
|
|
1552
|
+
): string[] {
|
|
1553
|
+
const pad = " ".repeat(indent);
|
|
1554
|
+
const body = [
|
|
1555
|
+
`${pad}def Scope "f_${field.entity.sid}"`,
|
|
1556
|
+
`${pad}{`,
|
|
1557
|
+
`${pad} custom uniform string nomos:label = ${quote(field.name)}`,
|
|
1558
|
+
`${pad} custom uniform bool nomos:required = ${field.required}`,
|
|
1559
|
+
`${pad} custom rel nomos:type = <${requiredPath(field.value.id, paths)}>`,
|
|
1560
|
+
];
|
|
1561
|
+
let current = types.get(field.value.id);
|
|
1562
|
+
const seen = new Set<string>();
|
|
1563
|
+
while (current !== undefined && (current.expression.kind === "nullable" || current.expression.kind === "default")) {
|
|
1564
|
+
if (seen.has(current.entity.id)) fail(`field '${field.name}' contains a value-wrapper cycle`);
|
|
1565
|
+
seen.add(current.entity.id);
|
|
1566
|
+
if (current.expression.kind === "nullable") {
|
|
1567
|
+
body.push(`${pad} custom uniform bool nomos:nullable = true`);
|
|
1568
|
+
} else {
|
|
1569
|
+
body.push(...defaultOpinions(current.expression.defaultValue, field.name, indent + 4));
|
|
1570
|
+
}
|
|
1571
|
+
current = types.get(current.expression.value.id);
|
|
1572
|
+
}
|
|
1573
|
+
body.push(`${pad}}`);
|
|
1574
|
+
return body;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
function defaultOpinions(
|
|
1578
|
+
value: import("./law_value_type.js").LawConstant,
|
|
1579
|
+
owner: string,
|
|
1580
|
+
indent: number,
|
|
1581
|
+
): string[] {
|
|
1582
|
+
const pad = " ".repeat(indent);
|
|
1583
|
+
if (typeof value === "string") return [`${pad}custom uniform string nomos:default = ${quote(value)}`];
|
|
1584
|
+
if (typeof value === "boolean") return [`${pad}custom uniform bool nomos:default = ${value}`];
|
|
1585
|
+
if (typeof value === "number") {
|
|
1586
|
+
return [Number.isInteger(value)
|
|
1587
|
+
? `${pad}custom uniform int64 nomos:default = ${value}`
|
|
1588
|
+
: `${pad}custom uniform double nomos:default = ${value}`];
|
|
1589
|
+
}
|
|
1590
|
+
if (value === null) return [`${pad}custom uniform token nomos:defaultKind = "null"`];
|
|
1591
|
+
|
|
1592
|
+
// USDA dictionaries cannot faithfully represent heterogeneous/nested arrays. Preserve the
|
|
1593
|
+
// complete constant instead as a typed prim tree: generic USD tools can traverse it, humans can
|
|
1594
|
+
// inspect it, and the kernel parser retains every opinion without an embedded JSON sub-language.
|
|
1595
|
+
const kind = Array.isArray(value) ? "array" : "record";
|
|
1596
|
+
return [
|
|
1597
|
+
`${pad}custom uniform token nomos:defaultKind = ${quote(kind)}`,
|
|
1598
|
+
`${pad}def Scope "Default"`,
|
|
1599
|
+
`${pad}{`,
|
|
1600
|
+
...emitDefaultValue(value, owner, indent + 4),
|
|
1601
|
+
`${pad}}`,
|
|
1602
|
+
];
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
function emitDefaultValue(
|
|
1606
|
+
value: import("./law_value_type.js").LawConstant,
|
|
1607
|
+
owner: string,
|
|
1608
|
+
indent: number,
|
|
1609
|
+
): string[] {
|
|
1610
|
+
const pad = " ".repeat(indent);
|
|
1611
|
+
if (typeof value === "string") {
|
|
1612
|
+
return [
|
|
1613
|
+
`${pad}custom uniform token nomos:valueKind = "string"`,
|
|
1614
|
+
`${pad}custom uniform string nomos:value = ${quote(value)}`,
|
|
1615
|
+
];
|
|
1616
|
+
}
|
|
1617
|
+
if (typeof value === "boolean") {
|
|
1618
|
+
return [
|
|
1619
|
+
`${pad}custom uniform token nomos:valueKind = "bool"`,
|
|
1620
|
+
`${pad}custom uniform bool nomos:value = ${value}`,
|
|
1621
|
+
];
|
|
1622
|
+
}
|
|
1623
|
+
if (typeof value === "number") {
|
|
1624
|
+
if (!Number.isFinite(value)) fail(`field '${owner}' has a non-finite numeric default`);
|
|
1625
|
+
return [
|
|
1626
|
+
`${pad}custom uniform token nomos:valueKind = ${quote(Number.isInteger(value) ? "int" : "number")}`,
|
|
1627
|
+
Number.isInteger(value)
|
|
1628
|
+
? `${pad}custom uniform int64 nomos:value = ${value}`
|
|
1629
|
+
: `${pad}custom uniform double nomos:value = ${value}`,
|
|
1630
|
+
];
|
|
1631
|
+
}
|
|
1632
|
+
if (value === null) return [`${pad}custom uniform token nomos:valueKind = "null"`];
|
|
1633
|
+
|
|
1634
|
+
const entries: readonly [string, import("./law_value_type.js").LawConstant][] = Array.isArray(value)
|
|
1635
|
+
? value.map((item, index) => [String(index), item] as const)
|
|
1636
|
+
: Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
|
|
1637
|
+
const kind = Array.isArray(value) ? "array" : "record";
|
|
1638
|
+
const lines = [`${pad}custom uniform token nomos:valueKind = ${quote(kind)}`];
|
|
1639
|
+
for (const [index, [key, item]] of entries.entries()) {
|
|
1640
|
+
lines.push(
|
|
1641
|
+
"",
|
|
1642
|
+
`${pad}def Scope "v_${String(index).padStart(8, "0")}"`,
|
|
1643
|
+
`${pad}{`,
|
|
1644
|
+
`${pad} custom uniform string nomos:key = ${quote(key)}`,
|
|
1645
|
+
...emitDefaultValue(item, owner, indent + 4),
|
|
1646
|
+
`${pad}}`,
|
|
1647
|
+
);
|
|
1648
|
+
}
|
|
1649
|
+
return lines;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
function emitField(
|
|
1653
|
+
field: LawFieldNode,
|
|
1654
|
+
paths: ReadonlyMap<string, string>,
|
|
1655
|
+
resolveDependency: ResolveDependency,
|
|
1656
|
+
indent: number,
|
|
1657
|
+
): string[] {
|
|
1658
|
+
const pad = " ".repeat(indent);
|
|
1659
|
+
const body = [
|
|
1660
|
+
`${pad}def Scope "${field.entity.id}"`,
|
|
1661
|
+
`${pad}{`,
|
|
1662
|
+
...entityLabelOpinions(field.entity, indent + 4),
|
|
1663
|
+
];
|
|
1664
|
+
if (field.storageKind !== undefined) {
|
|
1665
|
+
body.push(`${pad} custom uniform token nomos:storageKind = ${quote(field.storageKind)}`);
|
|
1666
|
+
}
|
|
1667
|
+
if (field.mapValueKind !== undefined) {
|
|
1668
|
+
body.push(`${pad} custom uniform token nomos:mapValueKind = ${quote(field.mapValueKind)}`);
|
|
1669
|
+
}
|
|
1670
|
+
if (field.schemaIdentity !== undefined) {
|
|
1671
|
+
body.push(`${pad} custom uniform string nomos:schemaIdentity = ${quote(field.schemaIdentity)}`);
|
|
1672
|
+
}
|
|
1673
|
+
const evolutions = [...(field.evolutions ?? [])].sort((left, right) => left.from.localeCompare(right.from));
|
|
1674
|
+
if (evolutions.length > 0) {
|
|
1675
|
+
body.push(`${pad} def Scope "Evolutions"`, `${pad} {`);
|
|
1676
|
+
for (const [evolutionIndex, evolution] of evolutions.entries()) {
|
|
1677
|
+
const evolutionName = `e_${String(evolutionIndex).padStart(8, "0")}`;
|
|
1678
|
+
body.push(
|
|
1679
|
+
`${pad} def NomosFieldEvolution "${evolutionName}"`,
|
|
1680
|
+
`${pad} {`,
|
|
1681
|
+
`${pad} custom uniform string nomos:fromSchema = ${quote(evolution.from)}`,
|
|
1682
|
+
`${pad} custom uniform string nomos:toSchema = ${quote(evolution.to)}`,
|
|
1683
|
+
`${pad} custom uniform token nomos:sourceKind = ${quote(evolution.source.kind)}`,
|
|
1684
|
+
);
|
|
1685
|
+
if (evolution.source.mapValueKind !== undefined) {
|
|
1686
|
+
body.push(`${pad} custom uniform token nomos:sourceMapValueKind = ${quote(evolution.source.mapValueKind)}`);
|
|
1687
|
+
}
|
|
1688
|
+
body.push(`${pad} custom uniform token nomos:mode = ${quote(evolution.mode)}`);
|
|
1689
|
+
const cases = [...(evolution.cases ?? [])];
|
|
1690
|
+
if (cases.length > 0) {
|
|
1691
|
+
body.push(`${pad} def Scope "Cases"`, `${pad} {`);
|
|
1692
|
+
for (const [caseIndex, evolutionCase] of cases.entries()) {
|
|
1693
|
+
body.push(
|
|
1694
|
+
`${pad} def NomosFieldEvolutionCase "c_${String(caseIndex).padStart(8, "0")}"`,
|
|
1695
|
+
`${pad} {`,
|
|
1696
|
+
`${pad} def Scope "From"`,
|
|
1697
|
+
`${pad} {`,
|
|
1698
|
+
...emitDefaultValue(evolutionCase.from as import("./law_value_type.js").LawConstant, field.entity.label, indent + 24),
|
|
1699
|
+
`${pad} }`,
|
|
1700
|
+
`${pad} def Scope "To"`,
|
|
1701
|
+
`${pad} {`,
|
|
1702
|
+
...emitDefaultValue(evolutionCase.to as import("./law_value_type.js").LawConstant, field.entity.label, indent + 24),
|
|
1703
|
+
`${pad} }`,
|
|
1704
|
+
`${pad} }`,
|
|
1705
|
+
);
|
|
1706
|
+
}
|
|
1707
|
+
body.push(`${pad} }`);
|
|
1708
|
+
}
|
|
1709
|
+
body.push(`${pad} }`);
|
|
1710
|
+
}
|
|
1711
|
+
body.push(`${pad} }`);
|
|
1712
|
+
}
|
|
1713
|
+
if (field.mergeDrivers.length > 0) {
|
|
1714
|
+
if (field.mergeDrivers.some((driver) => driver.principal !== undefined)) {
|
|
1715
|
+
fail(`field '${field.entity.label}' uses a parameterised merge driver not yet ratified in W1 USDA`);
|
|
1716
|
+
}
|
|
1717
|
+
body.push(`${pad} custom uniform token[] nomos:mergeDriver = [${field.mergeDrivers
|
|
1718
|
+
.map((driver) => quote(driver.kind)).join(", ")}]`);
|
|
1719
|
+
}
|
|
1720
|
+
body.push(`${pad} custom uniform bool nomos:required = ${field.required}`);
|
|
1721
|
+
if (field.nullable) body.push(`${pad} custom uniform bool nomos:nullable = true`);
|
|
1722
|
+
if (field.encrypted) body.push(`${pad} custom uniform bool nomos:encrypted = true`);
|
|
1723
|
+
if (field.blindIndexed) body.push(`${pad} custom uniform bool nomos:blindIndexed = true`);
|
|
1724
|
+
if (field.refTarget === undefined) {
|
|
1725
|
+
body.push(`${pad} custom rel nomos:type = <${requiredPath(field.value.id, paths)}>`);
|
|
1726
|
+
}
|
|
1727
|
+
if (field.refTarget !== undefined) {
|
|
1728
|
+
body.push(`${pad} custom rel nomos:refTarget = ${targets([field.refTarget], paths, resolveDependency)}`);
|
|
1729
|
+
body.push(`${pad} custom uniform token nomos:referenceMode = ${quote(field.referenceMode ?? "current")}`);
|
|
1730
|
+
}
|
|
1731
|
+
if (field.workspaceField !== undefined) {
|
|
1732
|
+
body.push(`${pad} custom rel nomos:workspaceField = <${requiredPath(field.workspaceField.id, paths)}>`);
|
|
1733
|
+
}
|
|
1734
|
+
body.push(`${pad}}`);
|
|
1735
|
+
return body;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
function emitContainer<T extends { readonly entity: { readonly id: string } }>(
|
|
1739
|
+
lines: string[],
|
|
1740
|
+
name: string,
|
|
1741
|
+
nodes: readonly T[],
|
|
1742
|
+
indent: number,
|
|
1743
|
+
body: (node: T, indent: number) => string[],
|
|
1744
|
+
primName: (node: T) => string = (node) => node.entity.id,
|
|
1745
|
+
primType: (node: T) => string = () => "Scope",
|
|
1746
|
+
): void {
|
|
1747
|
+
const pad = " ".repeat(indent);
|
|
1748
|
+
lines.push(`${pad}def Scope "${name}"`, `${pad}{`);
|
|
1749
|
+
for (const [index, node] of nodes.entries()) {
|
|
1750
|
+
if (index > 0) lines.push("");
|
|
1751
|
+
lines.push(`${pad} def ${primType(node)} "${primName(node)}"`, `${pad} {`);
|
|
1752
|
+
lines.push(...body(node, indent + 8));
|
|
1753
|
+
lines.push(`${pad} }`);
|
|
1754
|
+
}
|
|
1755
|
+
lines.push(`${pad}}`);
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
function targets(
|
|
1759
|
+
refs: readonly LawTargetRef<"aggregate" | "contract" | "relation" | "policy" | "read">[],
|
|
1760
|
+
paths: ReadonlyMap<string, string>,
|
|
1761
|
+
resolveDependency: ResolveDependency,
|
|
1762
|
+
): string {
|
|
1763
|
+
return relList(refs.map((target) => {
|
|
1764
|
+
if ("kind" in target) {
|
|
1765
|
+
return `<${resolveDependency(target)}>`;
|
|
1766
|
+
}
|
|
1767
|
+
return `<${requiredPath(target.id, paths)}>`;
|
|
1768
|
+
}));
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
function target(
|
|
1772
|
+
value: LawTargetRef<"aggregate" | "contract" | "relation" | "policy">,
|
|
1773
|
+
paths: ReadonlyMap<string, string>,
|
|
1774
|
+
resolveDependency: ResolveDependency,
|
|
1775
|
+
): string {
|
|
1776
|
+
return targets([value], paths, resolveDependency);
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
function relList(paths: readonly string[]): string {
|
|
1780
|
+
return paths.length === 1 ? paths[0]! : `[${paths.join(", ")}]`;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
function stringList(values: readonly string[]): string {
|
|
1784
|
+
return `[${values.map(quote).join(", ")}]`;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
function requiredPath(id: string, paths: ReadonlyMap<string, string>): string {
|
|
1788
|
+
const path = paths.get(id);
|
|
1789
|
+
if (path === undefined) fail(`no USDA path exists for linked identity '${id}'`);
|
|
1790
|
+
return path;
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
function quote(value: string): string {
|
|
1794
|
+
return JSON.stringify(value);
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
function fail(message: string): never {
|
|
1798
|
+
throw new Error(`NMS-USDA-001 ${message}`);
|
|
1799
|
+
}
|