@githolon/dsl 0.102.4 → 0.103.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/compile_package.mjs +43 -2
- package/compile_proof.d.mts +47 -0
- package/compile_proof.mjs +343 -0
- package/dart/CHANGELOG.md +7 -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 +5504 -1236
- 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 +196 -89
- 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/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 +76 -35
- package/src/clear_directive.ts +19 -8
- package/src/codegen_dart.ts +305 -22
- package/src/codegen_dart_application.ts +844 -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 +1866 -597
- package/src/compose.ts +1 -1
- package/src/directive.ts +208 -15
- package/src/engine_entry.ts +165 -111
- package/src/evolve_preview.ts +43 -81
- 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 +61 -31
- 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 +278 -0
- package/src/stable_ids.ts +7 -2
- 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,435 @@
|
|
|
1
|
+
// NOMOS — Compiler-custodied identity for independently-lived law declarations.
|
|
2
|
+
//
|
|
3
|
+
// This registry is authoring custody, not law. It remembers only identity continuity;
|
|
4
|
+
// every semantic opinion remains in USDA. Retired entries are deliberately retained so
|
|
5
|
+
// deleting and later recreating the same label cannot resurrect the deleted entity.
|
|
6
|
+
import { mintStableId } from "./stable_ids.js";
|
|
7
|
+
|
|
8
|
+
export const LAW_ENTITY_KINDS = [
|
|
9
|
+
"domain",
|
|
10
|
+
"aggregate",
|
|
11
|
+
"field",
|
|
12
|
+
"intent",
|
|
13
|
+
"read",
|
|
14
|
+
"relation",
|
|
15
|
+
"policy",
|
|
16
|
+
"invariant",
|
|
17
|
+
"capability",
|
|
18
|
+
"flow",
|
|
19
|
+
"workspace-type",
|
|
20
|
+
"contract",
|
|
21
|
+
"type",
|
|
22
|
+
"object-type",
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
export type LawEntityKind = (typeof LAW_ENTITY_KINDS)[number];
|
|
26
|
+
export type LawIdentityStatus = "active" | "retired";
|
|
27
|
+
|
|
28
|
+
export interface LawIdentityEntity {
|
|
29
|
+
readonly kind: LawEntityKind;
|
|
30
|
+
readonly sid: string;
|
|
31
|
+
/** Registry key of the stable owning entity, or a reserved compiler scope. */
|
|
32
|
+
readonly parent?: string;
|
|
33
|
+
readonly label: string;
|
|
34
|
+
readonly priorLabels?: readonly string[];
|
|
35
|
+
readonly generation: number;
|
|
36
|
+
readonly status: LawIdentityStatus;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface LawIdentityRegistry {
|
|
40
|
+
readonly v: 1;
|
|
41
|
+
readonly entities: Readonly<Record<string, LawIdentityEntity>>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface LawIdentityCarry {
|
|
45
|
+
readonly from: string;
|
|
46
|
+
readonly to: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ReconcileLawIdentityScopeInput {
|
|
50
|
+
readonly kind: LawEntityKind;
|
|
51
|
+
readonly parent?: string;
|
|
52
|
+
readonly labels: readonly string[];
|
|
53
|
+
/** Explicit, already-validated identity carries. Never inferred in this layer. */
|
|
54
|
+
readonly carries?: readonly LawIdentityCarry[];
|
|
55
|
+
/** Used only while importing an existing compiler-minted identity byte-for-byte. */
|
|
56
|
+
readonly preferredSids?: Readonly<Record<string, string>>;
|
|
57
|
+
/**
|
|
58
|
+
* Defaults to true. Set false only when this compile sees a non-exhaustive slice of
|
|
59
|
+
* a shared scope (currently project-root domains and framework declarations).
|
|
60
|
+
*/
|
|
61
|
+
readonly retireAbsent?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ReconciledLawIdentityScope {
|
|
65
|
+
readonly registry: LawIdentityRegistry;
|
|
66
|
+
/** Current label to registry key. */
|
|
67
|
+
readonly resolved: Readonly<Record<string, string>>;
|
|
68
|
+
readonly carried: readonly { from: string; to: string; key: string }[];
|
|
69
|
+
readonly allocated: readonly { label: string; key: string; generation: number }[];
|
|
70
|
+
readonly retired: readonly string[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A compile-scoped identity transaction. Large laws reconcile hundreds of sibling
|
|
75
|
+
* scopes; cloning, sorting and validating the complete registry after every field
|
|
76
|
+
* group made that linear work accidentally quadratic. The transaction owns one
|
|
77
|
+
* private mutable copy, while the public one-shot API below retains its immutable
|
|
78
|
+
* behaviour.
|
|
79
|
+
*/
|
|
80
|
+
export interface LawIdentityReconciler {
|
|
81
|
+
reconcile(input: ReconcileLawIdentityScopeInput): ReconciledLawIdentityScope;
|
|
82
|
+
finish(): LawIdentityRegistry;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const KIND_PREFIX: Readonly<Record<LawEntityKind, string>> = {
|
|
86
|
+
domain: "d",
|
|
87
|
+
aggregate: "a",
|
|
88
|
+
field: "f",
|
|
89
|
+
intent: "i",
|
|
90
|
+
read: "q",
|
|
91
|
+
relation: "r",
|
|
92
|
+
policy: "p",
|
|
93
|
+
invariant: "n",
|
|
94
|
+
capability: "k",
|
|
95
|
+
flow: "w",
|
|
96
|
+
"workspace-type": "x",
|
|
97
|
+
contract: "c",
|
|
98
|
+
type: "t",
|
|
99
|
+
"object-type": "o",
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const SID_PATTERN = /^[0-9a-f]{16}$/u;
|
|
103
|
+
|
|
104
|
+
/** Reserved owner for declarations supplied by the kernel/framework, not one tenant domain. */
|
|
105
|
+
export const LAW_FRAMEWORK_PARENT = "@framework";
|
|
106
|
+
|
|
107
|
+
export function emptyLawIdentityRegistry(): LawIdentityRegistry {
|
|
108
|
+
return { v: 1, entities: {} };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function lawEntityKey(kind: LawEntityKind, sid: string): string {
|
|
112
|
+
if (!SID_PATTERN.test(sid)) {
|
|
113
|
+
throw new Error(`NMS-ID-001 invalid ${kind} SID '${sid}'`);
|
|
114
|
+
}
|
|
115
|
+
return `${KIND_PREFIX[kind]}_${sid}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Identity for a compiler-owned slot whose lifecycle is exactly its stable owner's. */
|
|
119
|
+
export function mintOwnedLawSid(ownerSid: string, slotKind: string, slot: string): string {
|
|
120
|
+
assertSid(ownerSid, "owner");
|
|
121
|
+
assertToken(slotKind, "owned slot kind");
|
|
122
|
+
assertToken(slot, "owned slot");
|
|
123
|
+
return mintStableId(`law-owned:${ownerSid}:${slotKind}:${slot}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Identity for an anonymous structural node; any semantic change creates a new node. */
|
|
127
|
+
export function mintStructuralLawSid(
|
|
128
|
+
ownerSid: string,
|
|
129
|
+
nodeKind: string,
|
|
130
|
+
canonicalValue: unknown,
|
|
131
|
+
): string {
|
|
132
|
+
assertSid(ownerSid, "owner");
|
|
133
|
+
assertToken(nodeKind, "structural node kind");
|
|
134
|
+
return mintStableId(`law-structural:${ownerSid}:${nodeKind}:${canonicalStructuralValue(canonicalValue)}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Reconcile every currently declared label in one (kind, parent) namespace.
|
|
139
|
+
*
|
|
140
|
+
* Exact labels retain identity. Explicit carries retain identity and append label
|
|
141
|
+
* lineage. All other additions allocate a generation-safe SID; unmatched active
|
|
142
|
+
* declarations become durable tombstones, together with their owned descendants.
|
|
143
|
+
*/
|
|
144
|
+
export function reconcileLawIdentityScope(
|
|
145
|
+
source: LawIdentityRegistry,
|
|
146
|
+
input: ReconcileLawIdentityScopeInput,
|
|
147
|
+
): ReconciledLawIdentityScope {
|
|
148
|
+
const reconciler = createLawIdentityReconciler(source);
|
|
149
|
+
const result = reconciler.reconcile(input);
|
|
150
|
+
return { ...result, registry: reconciler.finish() };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function createLawIdentityReconciler(
|
|
154
|
+
source: LawIdentityRegistry,
|
|
155
|
+
): LawIdentityReconciler {
|
|
156
|
+
validateLawIdentityRegistry(source);
|
|
157
|
+
const entities: Record<string, LawIdentityEntity> = cloneEntities(source.entities);
|
|
158
|
+
const registryView: LawIdentityRegistry = { v: 1, entities };
|
|
159
|
+
let finished: LawIdentityRegistry | undefined;
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
reconcile(input): ReconciledLawIdentityScope {
|
|
163
|
+
if (finished !== undefined) throw new Error("NMS-ID-001 identity reconciler is already finished");
|
|
164
|
+
assertUsableParent(registryView, input.parent);
|
|
165
|
+
|
|
166
|
+
const labels = [...input.labels].sort(compareText);
|
|
167
|
+
assertUniqueNonEmpty(labels, `${input.kind} labels`);
|
|
168
|
+
const scopeEntries = Object.entries(entities).filter(
|
|
169
|
+
([, entity]) =>
|
|
170
|
+
entity.kind === input.kind && entity.parent === input.parent && entity.status === "active",
|
|
171
|
+
);
|
|
172
|
+
const activeByLabel = new Map(scopeEntries.map(([key, entity]) => [entity.label, key] as const));
|
|
173
|
+
const requested = new Set(labels);
|
|
174
|
+
const carried: { from: string; to: string; key: string }[] = [];
|
|
175
|
+
const carriedFrom = new Set<string>();
|
|
176
|
+
const carriedTo = new Set<string>();
|
|
177
|
+
|
|
178
|
+
for (const carry of [...(input.carries ?? [])].sort((a, b) => compareText(a.to, b.to))) {
|
|
179
|
+
if (carry.from === carry.to) {
|
|
180
|
+
throw new Error(`NMS-ID-004 identity carry '${carry.from}' -> '${carry.to}' is not a rename`);
|
|
181
|
+
}
|
|
182
|
+
if (carriedFrom.has(carry.from) || carriedTo.has(carry.to)) {
|
|
183
|
+
throw new Error(`NMS-ID-004 duplicate identity carry involving '${carry.from}' or '${carry.to}'`);
|
|
184
|
+
}
|
|
185
|
+
const key = activeByLabel.get(carry.from);
|
|
186
|
+
if (key === undefined) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`NMS-ID-004 cannot carry missing active ${input.kind} '${carry.from}' in ${scopeName(input.parent)}`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
if (!requested.has(carry.to)) {
|
|
192
|
+
throw new Error(`NMS-ID-004 carry target '${carry.to}' is not a current ${input.kind} declaration`);
|
|
193
|
+
}
|
|
194
|
+
const occupied = activeByLabel.get(carry.to);
|
|
195
|
+
if (occupied !== undefined && occupied !== key) {
|
|
196
|
+
throw new Error(`NMS-ID-003 active ${input.kind} label '${carry.to}' already owns another SID`);
|
|
197
|
+
}
|
|
198
|
+
const entity = entities[key]!;
|
|
199
|
+
entities[key] = {
|
|
200
|
+
...entity,
|
|
201
|
+
label: carry.to,
|
|
202
|
+
priorLabels: appendUnique(entity.priorLabels, carry.from),
|
|
203
|
+
};
|
|
204
|
+
activeByLabel.delete(carry.from);
|
|
205
|
+
activeByLabel.set(carry.to, key);
|
|
206
|
+
carriedFrom.add(carry.from);
|
|
207
|
+
carriedTo.add(carry.to);
|
|
208
|
+
carried.push({ ...carry, key });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const resolved: Record<string, string> = {};
|
|
212
|
+
const allocated: { label: string; key: string; generation: number }[] = [];
|
|
213
|
+
for (const label of labels) {
|
|
214
|
+
const existing = activeByLabel.get(label);
|
|
215
|
+
if (existing !== undefined) {
|
|
216
|
+
resolved[label] = existing;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const historic = Object.values(entities).filter(
|
|
221
|
+
(entity) =>
|
|
222
|
+
entity.kind === input.kind &&
|
|
223
|
+
entity.parent === input.parent &&
|
|
224
|
+
(entity.label === label || entity.priorLabels?.includes(label) === true),
|
|
225
|
+
);
|
|
226
|
+
const generation = historic.reduce((max, entity) => Math.max(max, entity.generation), -1) + 1;
|
|
227
|
+
const preferredSid = generation === 0 ? input.preferredSids?.[label] : undefined;
|
|
228
|
+
// A compatibility proposal may repeat a deterministic legacy SID whose former
|
|
229
|
+
// owner is now a tombstone (most visibly a field beneath a recreated aggregate).
|
|
230
|
+
// That is evidence to allocate afresh, never authority to reuse the old identity.
|
|
231
|
+
const preferredKey = preferredSid === undefined ? undefined : lawEntityKey(input.kind, preferredSid);
|
|
232
|
+
const preferredAvailable =
|
|
233
|
+
preferredSid !== undefined && preferredKey !== undefined && entities[preferredKey] === undefined;
|
|
234
|
+
const sid =
|
|
235
|
+
preferredAvailable
|
|
236
|
+
? preferredSid
|
|
237
|
+
: mintStableId(
|
|
238
|
+
`law-entity:${input.kind}:${input.parent ?? "@root"}:${label}:generation:${generation}`,
|
|
239
|
+
);
|
|
240
|
+
const key = lawEntityKey(input.kind, sid);
|
|
241
|
+
if (entities[key] !== undefined) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
`NMS-ID-002 SID collision: ${key} is already ${describeEntity(entities[key]!)}`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
entities[key] = {
|
|
247
|
+
kind: input.kind,
|
|
248
|
+
sid,
|
|
249
|
+
...(input.parent === undefined ? {} : { parent: input.parent }),
|
|
250
|
+
label,
|
|
251
|
+
generation,
|
|
252
|
+
status: "active",
|
|
253
|
+
};
|
|
254
|
+
activeByLabel.set(label, key);
|
|
255
|
+
resolved[label] = key;
|
|
256
|
+
allocated.push({ label, key, generation });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const retired: string[] = [];
|
|
260
|
+
if (input.retireAbsent !== false) {
|
|
261
|
+
for (const [key, entity] of scopeEntries) {
|
|
262
|
+
if (carriedFrom.has(entity.label)) continue;
|
|
263
|
+
if (requested.has(entity.label)) continue;
|
|
264
|
+
if (entities[key]?.status === "active") {
|
|
265
|
+
entities[key] = { ...entities[key]!, status: "retired" };
|
|
266
|
+
retired.push(key);
|
|
267
|
+
retireDescendants(entities, key, retired);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
registry: registryView,
|
|
274
|
+
resolved: canonicalRecord(resolved),
|
|
275
|
+
carried,
|
|
276
|
+
allocated,
|
|
277
|
+
retired: [...retired].sort(compareText),
|
|
278
|
+
};
|
|
279
|
+
},
|
|
280
|
+
finish(): LawIdentityRegistry {
|
|
281
|
+
if (finished !== undefined) return finished;
|
|
282
|
+
finished = canonicalRegistry(entities);
|
|
283
|
+
validateLawIdentityRegistry(finished);
|
|
284
|
+
return finished;
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function validateLawIdentityRegistry(registry: LawIdentityRegistry): void {
|
|
290
|
+
if (registry.v !== 1 || registry.entities === null || typeof registry.entities !== "object") {
|
|
291
|
+
throw new Error("NMS-ID-001 invalid identity registry envelope");
|
|
292
|
+
}
|
|
293
|
+
const activeCoordinates = new Map<string, string>();
|
|
294
|
+
for (const [key, entity] of Object.entries(registry.entities)) {
|
|
295
|
+
if (!LAW_ENTITY_KINDS.includes(entity.kind)) {
|
|
296
|
+
throw new Error(`NMS-ID-001 ${key} has unknown entity kind`);
|
|
297
|
+
}
|
|
298
|
+
if (lawEntityKey(entity.kind, entity.sid) !== key) {
|
|
299
|
+
throw new Error(`NMS-ID-001 registry key '${key}' disagrees with ${entity.kind} SID '${entity.sid}'`);
|
|
300
|
+
}
|
|
301
|
+
if (entity.label.length === 0) throw new Error(`NMS-ID-001 ${key} has an empty label`);
|
|
302
|
+
if (!Number.isSafeInteger(entity.generation) || entity.generation < 0) {
|
|
303
|
+
throw new Error(`NMS-ID-001 ${key} has invalid generation '${entity.generation}'`);
|
|
304
|
+
}
|
|
305
|
+
if (entity.status !== "active" && entity.status !== "retired") {
|
|
306
|
+
throw new Error(`NMS-ID-001 ${key} has invalid status`);
|
|
307
|
+
}
|
|
308
|
+
if (entity.priorLabels !== undefined) {
|
|
309
|
+
assertUniqueNonEmpty([...entity.priorLabels], `${key} prior labels`);
|
|
310
|
+
if (entity.priorLabels.includes(entity.label)) {
|
|
311
|
+
throw new Error(`NMS-ID-001 ${key} repeats its current label in priorLabels`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (entity.parent !== undefined && entity.parent !== LAW_FRAMEWORK_PARENT) {
|
|
315
|
+
const parent = registry.entities[entity.parent];
|
|
316
|
+
if (parent === undefined) {
|
|
317
|
+
throw new Error(`NMS-ID-005 ${key} names missing parent '${entity.parent}'`);
|
|
318
|
+
}
|
|
319
|
+
if (entity.status === "active" && parent.status !== "active") {
|
|
320
|
+
throw new Error(`NMS-ID-005 active ${key} has retired parent '${entity.parent}'`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (entity.status === "active") {
|
|
324
|
+
const coordinate = `${entity.kind}\u0000${entity.parent ?? "@root"}\u0000${entity.label}`;
|
|
325
|
+
const prior = activeCoordinates.get(coordinate);
|
|
326
|
+
if (prior !== undefined) {
|
|
327
|
+
throw new Error(`NMS-ID-003 active label '${entity.label}' aliases ${prior} and ${key}`);
|
|
328
|
+
}
|
|
329
|
+
activeCoordinates.set(coordinate, key);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function assertUsableParent(registry: LawIdentityRegistry, parent: string | undefined): void {
|
|
335
|
+
if (parent === undefined || parent === LAW_FRAMEWORK_PARENT) return;
|
|
336
|
+
const entity = registry.entities[parent];
|
|
337
|
+
if (entity === undefined) throw new Error(`NMS-ID-005 missing identity parent '${parent}'`);
|
|
338
|
+
if (entity.status !== "active") throw new Error(`NMS-ID-005 identity parent '${parent}' is retired`);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function retireDescendants(
|
|
342
|
+
entities: Record<string, LawIdentityEntity>,
|
|
343
|
+
parent: string,
|
|
344
|
+
retired: string[],
|
|
345
|
+
): void {
|
|
346
|
+
for (const [key, entity] of Object.entries(entities)) {
|
|
347
|
+
if (entity.parent !== parent || entity.status !== "active") continue;
|
|
348
|
+
entities[key] = { ...entity, status: "retired" };
|
|
349
|
+
retired.push(key);
|
|
350
|
+
retireDescendants(entities, key, retired);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function canonicalRegistry(entities: Record<string, LawIdentityEntity>): LawIdentityRegistry {
|
|
355
|
+
return { v: 1, entities: canonicalRecord(entities) };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function canonicalRecord<T>(record: Record<string, T>): Record<string, T> {
|
|
359
|
+
return Object.fromEntries(Object.entries(record).sort(([a], [b]) => compareText(a, b)));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function cloneEntities(
|
|
363
|
+
entities: Readonly<Record<string, LawIdentityEntity>>,
|
|
364
|
+
): Record<string, LawIdentityEntity> {
|
|
365
|
+
return Object.fromEntries(
|
|
366
|
+
Object.entries(entities).map(([key, entity]) => [
|
|
367
|
+
key,
|
|
368
|
+
{
|
|
369
|
+
...entity,
|
|
370
|
+
...(entity.priorLabels === undefined ? {} : { priorLabels: [...entity.priorLabels] }),
|
|
371
|
+
},
|
|
372
|
+
]),
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function appendUnique(values: readonly string[] | undefined, value: string): readonly string[] {
|
|
377
|
+
return values?.includes(value) === true ? [...values] : [...(values ?? []), value];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function assertUniqueNonEmpty(values: readonly string[], description: string): void {
|
|
381
|
+
const seen = new Set<string>();
|
|
382
|
+
for (const value of values) {
|
|
383
|
+
if (value.length === 0) throw new Error(`NMS-ID-001 ${description} contain an empty label`);
|
|
384
|
+
if (seen.has(value)) throw new Error(`NMS-ID-003 ${description} contain duplicate '${value}'`);
|
|
385
|
+
seen.add(value);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function describeEntity(entity: LawIdentityEntity): string {
|
|
390
|
+
return `${entity.kind} '${entity.label}' under ${scopeName(entity.parent)}`;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function scopeName(parent: string | undefined): string {
|
|
394
|
+
return parent ?? "the project root";
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function compareText(a: string, b: string): number {
|
|
398
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function assertSid(sid: string, description: string): void {
|
|
402
|
+
if (!SID_PATTERN.test(sid)) throw new Error(`NMS-ID-001 invalid ${description} SID '${sid}'`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function assertToken(value: string, description: string): void {
|
|
406
|
+
if (value.length === 0 || /[\u0000\r\n]/u.test(value)) {
|
|
407
|
+
throw new Error(`NMS-ID-001 invalid ${description} '${value}'`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function canonicalStructuralValue(value: unknown, seen = new Set<object>()): string {
|
|
412
|
+
if (value === null) return "null";
|
|
413
|
+
if (typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
|
|
414
|
+
if (typeof value === "number") {
|
|
415
|
+
if (!Number.isFinite(value)) throw new Error("NMS-ID-001 structural identity contains a non-finite number");
|
|
416
|
+
return JSON.stringify(value);
|
|
417
|
+
}
|
|
418
|
+
if (typeof value !== "object") {
|
|
419
|
+
throw new Error(`NMS-ID-001 structural identity cannot contain '${typeof value}'`);
|
|
420
|
+
}
|
|
421
|
+
if (seen.has(value)) throw new Error("NMS-ID-001 structural identity contains a cycle");
|
|
422
|
+
seen.add(value);
|
|
423
|
+
if (Array.isArray(value)) {
|
|
424
|
+
const encoded = `[${value.map((item) => canonicalStructuralValue(item, seen)).join(",")}]`;
|
|
425
|
+
seen.delete(value);
|
|
426
|
+
return encoded;
|
|
427
|
+
}
|
|
428
|
+
const record = value as Record<string, unknown>;
|
|
429
|
+
const encoded = `{${Object.keys(record)
|
|
430
|
+
.sort(compareText)
|
|
431
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalStructuralValue(record[key], seen)}`)
|
|
432
|
+
.join(",")}}`;
|
|
433
|
+
seen.delete(value);
|
|
434
|
+
return encoded;
|
|
435
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
zodArrayElement,
|
|
4
|
+
zodDef,
|
|
5
|
+
zodEnumValues,
|
|
6
|
+
zodKind,
|
|
7
|
+
zodObjectShape,
|
|
8
|
+
} from "./zod_introspection.js";
|
|
9
|
+
|
|
10
|
+
export type LawLiteral = string | number | boolean | null;
|
|
11
|
+
export type LawConstant = LawLiteral | readonly LawConstant[] | { readonly [key: string]: LawConstant };
|
|
12
|
+
|
|
13
|
+
export type LawValueConstraint =
|
|
14
|
+
| { readonly kind: "min-length" | "max-length"; readonly value: number }
|
|
15
|
+
| { readonly kind: "pattern"; readonly value: string }
|
|
16
|
+
| { readonly kind: "format"; readonly value: string }
|
|
17
|
+
| { readonly kind: "minimum" | "maximum"; readonly value: number; readonly inclusive: boolean }
|
|
18
|
+
| { readonly kind: "multiple-of"; readonly value: number };
|
|
19
|
+
|
|
20
|
+
export interface LawValueMember {
|
|
21
|
+
readonly name: string;
|
|
22
|
+
readonly required: boolean;
|
|
23
|
+
readonly value: LawValueType;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type LawValueType =
|
|
27
|
+
| { readonly kind: "string" | "number" | "integer"; readonly constraints: readonly LawValueConstraint[] }
|
|
28
|
+
| { readonly kind: "boolean" | "null" | "unknown" }
|
|
29
|
+
| { readonly kind: "enum"; readonly values: readonly string[] }
|
|
30
|
+
| { readonly kind: "literal"; readonly value: LawLiteral }
|
|
31
|
+
| { readonly kind: "list"; readonly element: LawValueType }
|
|
32
|
+
| { readonly kind: "map"; readonly value: LawValueType }
|
|
33
|
+
| { readonly kind: "record"; readonly fields: readonly LawValueMember[] }
|
|
34
|
+
| { readonly kind: "sum"; readonly options: readonly LawValueType[] }
|
|
35
|
+
| { readonly kind: "nullable"; readonly value: LawValueType }
|
|
36
|
+
| { readonly kind: "default"; readonly value: LawValueType; readonly defaultValue: LawConstant }
|
|
37
|
+
| { readonly kind: "recursive"; readonly anchor: string; readonly value: LawValueType }
|
|
38
|
+
| { readonly kind: "recursive-ref"; readonly anchor: string };
|
|
39
|
+
|
|
40
|
+
interface ZodCheck {
|
|
41
|
+
readonly check?: string;
|
|
42
|
+
readonly minimum?: number;
|
|
43
|
+
readonly maximum?: number;
|
|
44
|
+
readonly value?: number;
|
|
45
|
+
readonly inclusive?: boolean;
|
|
46
|
+
readonly format?: string;
|
|
47
|
+
readonly pattern?: RegExp;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface ZodCheckCarrier {
|
|
51
|
+
readonly _zod?: { readonly def?: ZodCheck };
|
|
52
|
+
readonly def?: ZodCheck;
|
|
53
|
+
readonly _def?: ZodCheck;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface ZodExtendedDef {
|
|
57
|
+
readonly innerType?: z.ZodTypeAny;
|
|
58
|
+
readonly options?: readonly z.ZodTypeAny[];
|
|
59
|
+
readonly valueType?: z.ZodTypeAny;
|
|
60
|
+
readonly keyType?: z.ZodTypeAny;
|
|
61
|
+
readonly getter?: () => z.ZodTypeAny;
|
|
62
|
+
readonly defaultValue?: unknown;
|
|
63
|
+
readonly checks?: readonly ZodCheckCarrier[];
|
|
64
|
+
readonly value?: unknown;
|
|
65
|
+
readonly values?: readonly unknown[] | Set<unknown>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function lowerLawValueType(schema: z.ZodTypeAny): LawValueType {
|
|
69
|
+
return lower(schema, "$", new Map());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function lower(
|
|
73
|
+
schema: z.ZodTypeAny,
|
|
74
|
+
path: string,
|
|
75
|
+
activeLazy: Map<z.ZodTypeAny, string>,
|
|
76
|
+
): LawValueType {
|
|
77
|
+
const kind = zodKind(schema);
|
|
78
|
+
const def = zodDef(schema) as ZodExtendedDef;
|
|
79
|
+
switch (kind) {
|
|
80
|
+
case "string":
|
|
81
|
+
return { kind: "string", constraints: constraintsOf(def) };
|
|
82
|
+
case "number": {
|
|
83
|
+
const constraints = constraintsOf(def);
|
|
84
|
+
const integer = def.checks?.some((check) => checkDef(check).format === "safeint") === true;
|
|
85
|
+
return { kind: integer ? "integer" : "number", constraints };
|
|
86
|
+
}
|
|
87
|
+
case "boolean":
|
|
88
|
+
return { kind: "boolean" };
|
|
89
|
+
case "null":
|
|
90
|
+
return { kind: "null" };
|
|
91
|
+
case "unknown":
|
|
92
|
+
case "any":
|
|
93
|
+
return { kind: "unknown" };
|
|
94
|
+
case "enum":
|
|
95
|
+
return { kind: "enum", values: zodEnumValues(schema).sort(compareText) };
|
|
96
|
+
case "literal":
|
|
97
|
+
return { kind: "literal", value: literalOf(def, path) };
|
|
98
|
+
case "array":
|
|
99
|
+
return { kind: "list", element: lower(zodArrayElement(schema), `${path}[]`, activeLazy) };
|
|
100
|
+
case "record": {
|
|
101
|
+
if (def.valueType === undefined) fail(path, "record has no value type");
|
|
102
|
+
return { kind: "map", value: lower(def.valueType, `${path}.*`, activeLazy) };
|
|
103
|
+
}
|
|
104
|
+
case "object":
|
|
105
|
+
return {
|
|
106
|
+
kind: "record",
|
|
107
|
+
fields: Object.entries(zodObjectShape(schema))
|
|
108
|
+
.sort(([a], [b]) => compareText(a, b))
|
|
109
|
+
.map(([name, field]) => lowerMember(name, field, path, activeLazy)),
|
|
110
|
+
};
|
|
111
|
+
case "union":
|
|
112
|
+
return {
|
|
113
|
+
kind: "sum",
|
|
114
|
+
options: (def.options ?? []).map((option, index) =>
|
|
115
|
+
lower(option, `${path}|${index}`, activeLazy)),
|
|
116
|
+
};
|
|
117
|
+
case "optional": {
|
|
118
|
+
if (def.innerType === undefined) fail(path, "optional has no inner type");
|
|
119
|
+
return lower(def.innerType, path, activeLazy);
|
|
120
|
+
}
|
|
121
|
+
case "nullable": {
|
|
122
|
+
if (def.innerType === undefined) fail(path, "nullable has no inner type");
|
|
123
|
+
return { kind: "nullable", value: lower(def.innerType, path, activeLazy) };
|
|
124
|
+
}
|
|
125
|
+
case "default": {
|
|
126
|
+
if (def.innerType === undefined) fail(path, "default has no inner type");
|
|
127
|
+
return {
|
|
128
|
+
kind: "default",
|
|
129
|
+
value: lower(def.innerType, path, activeLazy),
|
|
130
|
+
defaultValue: constant(def.defaultValue, path, "default"),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
case "lazy": {
|
|
134
|
+
const existing = activeLazy.get(schema);
|
|
135
|
+
if (existing !== undefined) return { kind: "recursive-ref", anchor: existing };
|
|
136
|
+
if (def.getter === undefined) fail(path, "lazy has no getter");
|
|
137
|
+
activeLazy.set(schema, path);
|
|
138
|
+
const value = lower(def.getter(), path, activeLazy);
|
|
139
|
+
activeLazy.delete(schema);
|
|
140
|
+
return { kind: "recursive", anchor: path, value };
|
|
141
|
+
}
|
|
142
|
+
default:
|
|
143
|
+
fail(path, `unsupported Zod type '${kind}'`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function lowerMember(
|
|
148
|
+
name: string,
|
|
149
|
+
schema: z.ZodTypeAny,
|
|
150
|
+
parentPath: string,
|
|
151
|
+
activeLazy: Map<z.ZodTypeAny, string>,
|
|
152
|
+
): LawValueMember {
|
|
153
|
+
let value = schema;
|
|
154
|
+
let required = true;
|
|
155
|
+
while (zodKind(value) === "optional") {
|
|
156
|
+
required = false;
|
|
157
|
+
const inner = zodDef(value).innerType;
|
|
158
|
+
if (inner === undefined) fail(`${parentPath}.${name}`, "optional has no inner type");
|
|
159
|
+
value = inner;
|
|
160
|
+
}
|
|
161
|
+
if (zodKind(value) === "default") required = false;
|
|
162
|
+
return {
|
|
163
|
+
name,
|
|
164
|
+
required,
|
|
165
|
+
value: lower(value, `${parentPath}.${name}`, activeLazy),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function constraintsOf(def: ZodExtendedDef): LawValueConstraint[] {
|
|
170
|
+
const constraints: LawValueConstraint[] = [];
|
|
171
|
+
for (const carrier of def.checks ?? []) {
|
|
172
|
+
const check = checkDef(carrier);
|
|
173
|
+
if (check.check === "min_length" && check.minimum !== undefined) {
|
|
174
|
+
constraints.push({ kind: "min-length", value: check.minimum });
|
|
175
|
+
} else if (check.check === "max_length" && check.maximum !== undefined) {
|
|
176
|
+
constraints.push({ kind: "max-length", value: check.maximum });
|
|
177
|
+
} else if (check.check === "string_format" && check.format !== undefined) {
|
|
178
|
+
if (check.format === "regex" && check.pattern !== undefined) {
|
|
179
|
+
constraints.push({ kind: "pattern", value: check.pattern.source });
|
|
180
|
+
} else {
|
|
181
|
+
constraints.push({ kind: "format", value: check.format });
|
|
182
|
+
}
|
|
183
|
+
} else if (check.check === "greater_than" && check.value !== undefined) {
|
|
184
|
+
constraints.push({ kind: "minimum", value: check.value, inclusive: check.inclusive === true });
|
|
185
|
+
} else if (check.check === "less_than" && check.value !== undefined) {
|
|
186
|
+
constraints.push({ kind: "maximum", value: check.value, inclusive: check.inclusive === true });
|
|
187
|
+
} else if (check.check === "multiple_of" && check.value !== undefined) {
|
|
188
|
+
constraints.push({ kind: "multiple-of", value: check.value });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return constraints.sort((a, b) => compareText(JSON.stringify(a), JSON.stringify(b)));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function checkDef(value: ZodCheckCarrier): ZodCheck {
|
|
195
|
+
return value._zod?.def ?? value.def ?? value._def ?? {};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function literalOf(def: ZodExtendedDef, path: string): LawLiteral {
|
|
199
|
+
const values = def.values instanceof Set ? [...def.values] : def.values;
|
|
200
|
+
return literal(def.value ?? values?.[0], path, "literal");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function literal(value: unknown, path: string, owner: string): LawLiteral {
|
|
204
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
205
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
206
|
+
fail(path, `${owner} is not a finite JSON scalar`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function constant(value: unknown, path: string, owner: string): LawConstant {
|
|
210
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
211
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
212
|
+
if (Array.isArray(value)) return value.map((child, index) =>
|
|
213
|
+
constant(child, `${path}[${index}]`, owner));
|
|
214
|
+
if (typeof value === "object" && value !== null) {
|
|
215
|
+
const result: Record<string, LawConstant> = {};
|
|
216
|
+
for (const [key, child] of Object.entries(value).sort(([a], [b]) => compareText(a, b))) {
|
|
217
|
+
result[key] = constant(child, `${path}.${key}`, owner);
|
|
218
|
+
}
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
fail(path, `${owner} is not canonical data`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function compareText(a: string, b: string): number {
|
|
225
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function fail(path: string, reason: string): never {
|
|
229
|
+
throw new Error(`NMS-GRAPH-002 payload type '${path}' ${reason}`);
|
|
230
|
+
}
|