@githolon/dsl 0.113.4 → 0.115.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/assets/operation-contract.wasm +0 -0
- package/compile_package.mjs +23 -3
- package/compile_proof.d.mts +7 -0
- package/compile_proof.mjs +38 -3
- package/dart/CHANGELOG.md +60 -0
- package/dart/lib/src/application_framework_calls.dart +16 -6
- package/dart/lib/src/console_enrolment.dart +7 -0
- package/dart/lib/src/console_http_io.dart +22 -15
- package/dart/lib/src/dev_cloud.dart +62 -0
- package/dart/lib/src/generated_call.dart +14 -3
- package/dart/lib/src/nms_generated_runtime.dart +7 -0
- package/dart/lib/src/node_runner_transport.dart +77 -8
- package/dart/lib/src/nomos_trace.dart +107 -0
- package/dart/lib/src/operation_receipts.dart +74 -7
- package/dart/lib/src/reception/dart_reception.dart +174 -17
- package/dart/lib/src/rows_watch.dart +390 -0
- package/dart/lib/src/runner/node-entry.mjs +7678 -2860
- package/dart/lib/src/runtime_v2_transport.dart +3 -0
- package/dart/lib/src/sync_status_projection.dart +94 -1
- package/dart/lib/src/sync_status_reducer.dart +5 -2
- package/dart/lib/src/test_harness.dart +107 -41
- package/dart/lib/src/testing/test_peer.dart +107 -99
- package/dart/lib/src/testing/test_peer_host.dart +305 -0
- package/dart/lib/src/typed_watch_registry.dart +26 -3
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart/test/background_model_adoption_test.dart +1 -0
- package/dart/test/console_enrolment_test.dart +47 -0
- package/dart/test/custodian_warming_test.dart +105 -0
- package/dart/test/fixtures/offer_watch_order_peer.dart +74 -0
- package/dart/test/kernel_author_after_read_test.dart +6 -3
- package/dart/test/kernel_fence_test.dart +4 -1
- package/dart/test/model_adoption_test.dart +66 -0
- package/dart/test/nomos_trace_test.dart +91 -0
- package/dart/test/own_offer_watch_order_test.dart +90 -0
- package/dart/test/receiving_group_open_test.dart +4 -8
- package/dart/test/replica_readiness_test.dart +14 -4
- package/dart/test/rows_watch_test.dart +331 -0
- package/dart/test/runtime_projections_test.dart +41 -0
- package/dart/test/test_peer_host_selection_test.dart +49 -0
- package/dart_types/CHANGELOG.md +39 -0
- package/dart_types/lib/src/approved_model.dart +100 -6
- package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pb.dart +1063 -3
- package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbenum.dart +89 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/application_desk.pb.dart +14 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/correspondence_desk.pb.dart +2272 -130
- package/dart_types/lib/src/proto/nomos/runtime/v2/correspondence_desk.pbenum.dart +65 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/model_adoption_desk.pb.dart +118 -2
- package/dart_types/lib/src/proto/nomos/runtime/v2/observability_desk.pb.dart +278 -9
- package/dart_types/lib/src/proto/nomos/runtime/v2/receiving_group_desk.pb.dart +7 -85
- package/dart_types/lib/src/proto/nomos/runtime/v2/records_desk.pb.dart +16 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/residency_desk.pb.dart +35 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pb.dart +43 -2
- package/dart_types/pubspec.yaml +1 -1
- package/package.json +6 -2
- package/src/aggregate.ts +93 -0
- package/src/anonymous_value_interning.ts +205 -0
- package/src/baseline.ts +27 -0
- package/src/baseline_export.ts +432 -0
- package/src/baseline_import.ts +312 -0
- package/src/baseline_usda.ts +186 -0
- package/src/birth_key_rule.ts +472 -0
- package/src/births.ts +207 -24
- package/src/build_provenance.ts +17 -2
- package/src/business_birth.ts +50 -4
- package/src/check_package_main.ts +50 -6
- package/src/codegen_call_proto.ts +5 -18
- package/src/codegen_dart.ts +95 -3
- package/src/codegen_dart_client.ts +73 -15
- package/src/codegen_dart_facade.ts +58 -35
- package/src/codegen_dart_framework_calls.ts +32 -10
- package/src/codegen_dart_scenario.ts +7 -0
- package/src/codegen_proof.ts +5 -2
- package/src/codegen_result_proto.ts +8 -6
- package/src/codegen_result_usda.ts +4 -0
- package/src/codegen_ts.ts +135 -34
- package/src/compatibility_graph.ts +41 -4
- package/src/compatibility_usda.ts +11 -1
- package/src/compile_package_main.ts +272 -105
- package/src/composed_wire_identity.ts +24 -0
- package/src/directive.ts +6 -0
- package/src/domain_requirements.ts +75 -0
- package/src/engine_entry.ts +4 -48
- package/src/engine_javascript.ts +12 -6
- package/src/fields.ts +109 -0
- package/src/identity_usda.ts +77 -51
- package/src/index.ts +25 -1
- package/src/intent.ts +5 -0
- package/src/law_graph.ts +142 -1
- package/src/law_graph_usda.ts +25 -141
- package/src/law_layer_prims.ts +199 -0
- package/src/law_unit_selection.ts +45 -0
- package/src/law_unit_set_identity.ts +44 -0
- package/src/law_version.ts +107 -0
- package/src/package_config.ts +44 -0
- package/src/package_dependencies.ts +8 -0
- package/src/query/compile.ts +0 -0
- package/src/release_inspection.ts +24 -0
- package/src/release_publication.ts +298 -0
- package/src/value_object.ts +6 -3
- package/src/wire.ts +2 -0
- package/src/wire_paths.ts +17 -0
- package/src/workspace_routing.ts +1 -1
package/src/law_graph.ts
CHANGED
|
@@ -32,6 +32,8 @@ import {
|
|
|
32
32
|
} from "./relation_schema.js";
|
|
33
33
|
import type { StableFieldEvolution, StableIds } from "./stable_ids_types.js";
|
|
34
34
|
import { isBusinessIntent, publicOfferRoute } from "./intent.js";
|
|
35
|
+
import { listElementPath, mapValuePath, nestedMemberPath, unionVariantPath } from "./wire_paths.js";
|
|
36
|
+
import { anonymousTypeOwnsPrim, internAnonymousValues } from "./anonymous_value_interning.js";
|
|
35
37
|
import {
|
|
36
38
|
lowerLawValueType,
|
|
37
39
|
type LawConstant,
|
|
@@ -634,6 +636,8 @@ export interface LawIntentNode {
|
|
|
634
636
|
readonly requiresHostCapabilities: readonly string[];
|
|
635
637
|
readonly resolves?: LawRef<"field">;
|
|
636
638
|
readonly births: boolean;
|
|
639
|
+
/** The typed birth key declaration (business births only); see `Directive.birthKey`. */
|
|
640
|
+
readonly birthKey?: { readonly type: string; readonly brand: string; readonly namespace: string };
|
|
637
641
|
readonly birthRecord?: LawRef<"aggregate">;
|
|
638
642
|
readonly birthPrefix?: string;
|
|
639
643
|
readonly birthDelegations?: LawRef<"field">;
|
|
@@ -1269,6 +1273,8 @@ export function discoverLawGraph(
|
|
|
1269
1273
|
}
|
|
1270
1274
|
fail(`intent '${intent.id}' in domain '${module.domain ?? module.name}' targets unknown aggregate '${aggregateId}'`);
|
|
1271
1275
|
};
|
|
1276
|
+
/** Nested payload paths by intent key → field node id, for phase 2 of nested wire continuity. */
|
|
1277
|
+
const nestedPathsByIntent = new Map<string, Map<string, string>>();
|
|
1272
1278
|
const intentNodes: LawIntentNode[] = intents.map((intent) => {
|
|
1273
1279
|
const intentEntity = intentEntities.get(intent.id)!;
|
|
1274
1280
|
const liveOperationFieldNames = new Set<string>();
|
|
@@ -1289,7 +1295,64 @@ export function discoverLawGraph(
|
|
|
1289
1295
|
}
|
|
1290
1296
|
return field;
|
|
1291
1297
|
});
|
|
1292
|
-
|
|
1298
|
+
// NESTED CONTINUITY (CAP-660). A field inside a structural value — a record member, a map
|
|
1299
|
+
// value, a list element, a union variant — used to take its structural sid as its wire key, so
|
|
1300
|
+
// a change anywhere in the enclosing structure moved every sibling's key (the 1 October 2026
|
|
1301
|
+
// `canvasRequirements.isRequired` incident). Nested fields are now ledgered under their path
|
|
1302
|
+
// (wire_paths.ts) in the intent's operation-field scope: the first allocation is still the
|
|
1303
|
+
// structural sid (unchanged law bytes for every existing package), and from then on the
|
|
1304
|
+
// ledger keeps it through named and structural generations.
|
|
1305
|
+
const nestedByPath = new Map<string, LawAnonymousValueFieldNode>();
|
|
1306
|
+
const pathsByNode = new Map<string, string[]>();
|
|
1307
|
+
const walkNested = (valueRef: LawValueTypeRef, path: string, visiting: ReadonlySet<string>): void => {
|
|
1308
|
+
if (visiting.has(valueRef.id)) return;
|
|
1309
|
+
const valueNode = provisionalById.get(valueRef.id as string as LawStructuralSid<"value-type">);
|
|
1310
|
+
if (valueNode?.kind !== "structural" || valueNode.structuralKind !== "value-type") return;
|
|
1311
|
+
const next = new Set(visiting).add(valueRef.id);
|
|
1312
|
+
switch (valueNode.expression.kind) {
|
|
1313
|
+
case "record":
|
|
1314
|
+
for (const memberRef of valueNode.expression.fields) {
|
|
1315
|
+
const member = provisionalById.get(memberRef.id);
|
|
1316
|
+
if (member?.kind !== "structural" || member.structuralKind !== "value-field") continue;
|
|
1317
|
+
const memberPath = nestedMemberPath(path, member.name);
|
|
1318
|
+
if (nestedByPath.has(memberPath)) continue;
|
|
1319
|
+
nestedByPath.set(memberPath, member);
|
|
1320
|
+
pathsByNode.set(member.entity.id, [...(pathsByNode.get(member.entity.id) ?? []), memberPath]);
|
|
1321
|
+
walkNested(member.value, memberPath, next);
|
|
1322
|
+
}
|
|
1323
|
+
break;
|
|
1324
|
+
case "list":
|
|
1325
|
+
walkNested(valueNode.expression.element, listElementPath(path), next);
|
|
1326
|
+
break;
|
|
1327
|
+
case "map":
|
|
1328
|
+
walkNested(valueNode.expression.value, mapValuePath(path), next);
|
|
1329
|
+
break;
|
|
1330
|
+
case "sum":
|
|
1331
|
+
valueNode.expression.variants.forEach((variantRef, index) => {
|
|
1332
|
+
const variant = provisionalById.get(variantRef.id);
|
|
1333
|
+
if (variant?.kind !== "structural" || variant.structuralKind !== "value-variant") return;
|
|
1334
|
+
walkNested(variant.value, unionVariantPath(path, variant.label ?? String(index)), next);
|
|
1335
|
+
});
|
|
1336
|
+
break;
|
|
1337
|
+
case "nullable":
|
|
1338
|
+
case "default":
|
|
1339
|
+
case "recursive":
|
|
1340
|
+
walkNested(valueNode.expression.value, path, next);
|
|
1341
|
+
break;
|
|
1342
|
+
default:
|
|
1343
|
+
break;
|
|
1344
|
+
}
|
|
1345
|
+
};
|
|
1346
|
+
for (const field of fields) walkNested(field.value, field.name, new Set());
|
|
1347
|
+
// Phase 1 of nested continuity: ledger every nested path NOW so it is live for this intent's
|
|
1348
|
+
// retirement pass and so an earlier generation's allocation is found, but allocate no wire yet.
|
|
1349
|
+
// The wire a nested field is served on is the canonical interned member's (law_graph_usda.ts
|
|
1350
|
+
// shares one prim between structurally identical records), which is only known once every
|
|
1351
|
+
// intent, read and contract of the domain is lowered: phase 2, below the intent loop.
|
|
1352
|
+
const intentPaths = nestedPathsByIntent.get(intentEntity.id) ?? new Map<string, string>();
|
|
1353
|
+
for (const [path, node] of nestedByPath) if (!intentPaths.has(path)) intentPaths.set(path, node.entity.id);
|
|
1354
|
+
nestedPathsByIntent.set(intentEntity.id, intentPaths);
|
|
1355
|
+
const labels = [...fields.map((field) => field.name), ...nestedByPath.keys()];
|
|
1293
1356
|
for (const label of labels) liveOperationFieldNames.add(label);
|
|
1294
1357
|
const preferredOperationFieldWire = Object.fromEntries(
|
|
1295
1358
|
fields.map((field) => [
|
|
@@ -1636,6 +1699,13 @@ export function discoverLawGraph(
|
|
|
1636
1699
|
requiresHostCapabilities: [...new Set(intent.declaredHostCapabilities ?? [])].sort(compareText),
|
|
1637
1700
|
...(resolvedField === undefined ? {} : { resolves: ref(resolvedField.entity) }),
|
|
1638
1701
|
births: intent.birthsChild === true,
|
|
1702
|
+
...(intent.birthsChild !== true || intent.birthKey === undefined ? {} : {
|
|
1703
|
+
birthKey: {
|
|
1704
|
+
type: intent.birthKey.type,
|
|
1705
|
+
brand: intent.birthKey.brand,
|
|
1706
|
+
namespace: intent.birthKey.namespace,
|
|
1707
|
+
},
|
|
1708
|
+
}),
|
|
1639
1709
|
...(intent.birthsChild !== true || aggregateByLabel.get("NomosBirthRecord") === undefined ? {} : {
|
|
1640
1710
|
birthRecord: ref(aggregateByLabel.get("NomosBirthRecord")!.node.entity),
|
|
1641
1711
|
birthPrefix: "nomos-birth-record:",
|
|
@@ -2554,6 +2624,77 @@ export function discoverLawGraph(
|
|
|
2554
2624
|
},
|
|
2555
2625
|
}),
|
|
2556
2626
|
};
|
|
2627
|
+
// NESTED WIRE CONTINUITY, PHASE 2 (CAP-660). Every nested payload field is served on the wire key of
|
|
2628
|
+
// the canonical interned member (the prim law_graph_usda.ts emits; the key codegen_call_proto.ts
|
|
2629
|
+
// encodes). Its first allocation is exactly that member's structural sid — the key every existing
|
|
2630
|
+
// package already serves, so law bytes do not move — and from then on the ledger keeps it under
|
|
2631
|
+
// the field's path through named and structural generations. Paths that end up sharing one
|
|
2632
|
+
// canonical member must agree on one allocation: two released keys for one served prim is an
|
|
2633
|
+
// explicit evolution decision, never a compile's silent pick.
|
|
2634
|
+
{
|
|
2635
|
+
const valueNodes = [...anonymousValueNodes.values()];
|
|
2636
|
+
const interning = internAnonymousValues({
|
|
2637
|
+
types: valueNodes.filter((node): node is LawAnonymousValueTypeNode => node.structuralKind === "value-type"),
|
|
2638
|
+
fields: valueNodes.filter((node): node is LawAnonymousValueFieldNode => node.structuralKind === "value-field"),
|
|
2639
|
+
variants: valueNodes.filter((node): node is LawAnonymousValueVariantNode => node.structuralKind === "value-variant"),
|
|
2640
|
+
ownsPrim: anonymousTypeOwnsPrim,
|
|
2641
|
+
});
|
|
2642
|
+
const canonicalFieldOf = (fieldId: string): string => interning.canonicalFieldIdById.get(fieldId) ?? fieldId;
|
|
2643
|
+
const allocationByCanonical = new Map<string, { canonical: string; aliases: Set<string>; owners: string[] }>();
|
|
2644
|
+
for (const [intentKey, paths] of nestedPathsByIntent) {
|
|
2645
|
+
if (paths.size === 0) continue;
|
|
2646
|
+
const intentLabel = registry.entities[intentKey]?.label ?? intentKey;
|
|
2647
|
+
const preferredOperationFieldWire: Record<string, LawOperationFieldWire> = {};
|
|
2648
|
+
for (const [path, fieldId] of paths) {
|
|
2649
|
+
const canonical = anonymousValueNodes.get(canonicalFieldOf(fieldId));
|
|
2650
|
+
if (canonical === undefined) fail(`directive '${intentLabel}': nested field '${path}' has no canonical value node`);
|
|
2651
|
+
preferredOperationFieldWire[path] = { protobuf: { canonical: canonical.entity.sid } };
|
|
2652
|
+
}
|
|
2653
|
+
const ledger = identities.reconcile({
|
|
2654
|
+
kind: "operation-field",
|
|
2655
|
+
parent: intentKey,
|
|
2656
|
+
labels: [...paths.keys()],
|
|
2657
|
+
preferredOperationFieldWire,
|
|
2658
|
+
retireAbsent: false,
|
|
2659
|
+
});
|
|
2660
|
+
registry = ledger.registry;
|
|
2661
|
+
for (const [path, fieldId] of paths) {
|
|
2662
|
+
const entity = registry.entities[ledger.resolved[path] ?? ""];
|
|
2663
|
+
if (entity?.kind !== "operation-field" || entity.status !== "active" || entity.operationFieldWire === undefined) {
|
|
2664
|
+
fail(`directive '${intentLabel}' has no active wire allocation for nested field '${path}'`);
|
|
2665
|
+
}
|
|
2666
|
+
const wire = entity.operationFieldWire.protobuf;
|
|
2667
|
+
const canonicalId = canonicalFieldOf(fieldId);
|
|
2668
|
+
const owner = `${intentLabel}.${path}`;
|
|
2669
|
+
const prior = allocationByCanonical.get(canonicalId);
|
|
2670
|
+
if (prior === undefined) {
|
|
2671
|
+
allocationByCanonical.set(canonicalId, { canonical: wire.canonical, aliases: new Set(wire.aliases ?? []), owners: [owner] });
|
|
2672
|
+
continue;
|
|
2673
|
+
}
|
|
2674
|
+
if (prior.canonical !== wire.canonical) {
|
|
2675
|
+
fail(
|
|
2676
|
+
`NMS-WIRE-001 nested fields ${[...prior.owners, owner].map((name) => `'${name}'`).join(" and ")} share one structural value ` +
|
|
2677
|
+
`but hold different wire allocations (${prior.canonical}, ${wire.canonical}); ` +
|
|
2678
|
+
"declare an evolution plan for the reallocation instead of letting a compile choose",
|
|
2679
|
+
);
|
|
2680
|
+
}
|
|
2681
|
+
for (const alias of wire.aliases ?? []) prior.aliases.add(alias);
|
|
2682
|
+
prior.owners.push(owner);
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
// Every node of an allocated equivalence class — the canonical member the law emits and each
|
|
2686
|
+
// duplicate a generated client reads — carries the one allocation.
|
|
2687
|
+
for (const node of valueNodes) {
|
|
2688
|
+
if (node.structuralKind !== "value-field") continue;
|
|
2689
|
+
const allocation = allocationByCanonical.get(canonicalFieldOf(node.entity.id));
|
|
2690
|
+
if (allocation === undefined) continue;
|
|
2691
|
+
const aliases = [...allocation.aliases].sort(compareText);
|
|
2692
|
+
anonymousValueNodes.set(node.entity.id, {
|
|
2693
|
+
...node,
|
|
2694
|
+
entity: { ...node.entity, wireKey: allocation.canonical, ...(aliases.length === 0 ? {} : { wireAliases: aliases }) },
|
|
2695
|
+
});
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2557
2698
|
const nodes: DeclaredLawNode[] = [
|
|
2558
2699
|
domainNode,
|
|
2559
2700
|
...aggregateNodes,
|
package/src/law_graph_usda.ts
CHANGED
|
@@ -28,6 +28,7 @@ import type {
|
|
|
28
28
|
LawWorkspaceRelationNode,
|
|
29
29
|
} from "./law_graph.js";
|
|
30
30
|
import { MemberAuthenticationKind, type AuthenticationProviderRef } from "./member_authentication.js";
|
|
31
|
+
import { internAnonymousValues } from "./anonymous_value_interning.js";
|
|
31
32
|
|
|
32
33
|
const FRAMEWORK_SCALAR: Readonly<Record<string, string>> = {
|
|
33
34
|
string: "/Nomos/Framework/Types/t_0000000000000001",
|
|
@@ -420,112 +421,18 @@ export function compileSparseLawGraphUsda(
|
|
|
420
421
|
};
|
|
421
422
|
for (const type of anonymousTypes) resolveAnonymousTypePath(type.entity.id);
|
|
422
423
|
|
|
423
|
-
// Anonymous value types are structural values, not independently lived declarations
|
|
424
|
-
//
|
|
425
|
-
//
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
return undefined;
|
|
436
|
-
}
|
|
437
|
-
const node = anonymousTypeById.get(id);
|
|
438
|
-
if (node === undefined) return undefined;
|
|
439
|
-
active.add(id);
|
|
440
|
-
const childKey = (childId: string): string | undefined => semanticTypeKey(childId, active);
|
|
441
|
-
let key: string | undefined;
|
|
442
|
-
switch (node.expression.kind) {
|
|
443
|
-
case "string":
|
|
444
|
-
case "number":
|
|
445
|
-
case "integer":
|
|
446
|
-
case "boolean":
|
|
447
|
-
case "null":
|
|
448
|
-
case "unknown":
|
|
449
|
-
case "enum":
|
|
450
|
-
case "literal":
|
|
451
|
-
key = JSON.stringify(node.expression);
|
|
452
|
-
break;
|
|
453
|
-
case "list": {
|
|
454
|
-
const child = childKey(node.expression.element.id);
|
|
455
|
-
if (child !== undefined) key = JSON.stringify(["list", child]);
|
|
456
|
-
break;
|
|
457
|
-
}
|
|
458
|
-
case "map": {
|
|
459
|
-
const child = childKey(node.expression.value.id);
|
|
460
|
-
if (child !== undefined) key = JSON.stringify(["map", child]);
|
|
461
|
-
break;
|
|
462
|
-
}
|
|
463
|
-
case "nullable":
|
|
464
|
-
case "default": {
|
|
465
|
-
// Wrappers do not own prims, but their meaning must survive structural
|
|
466
|
-
// interning. A nullable field is not the same contract as a non-null field.
|
|
467
|
-
const child = childKey(node.expression.value.id);
|
|
468
|
-
if (child !== undefined) key = JSON.stringify(node.expression.kind === "nullable"
|
|
469
|
-
? ["nullable", child]
|
|
470
|
-
: ["default", child, node.expression.defaultValue]);
|
|
471
|
-
break;
|
|
472
|
-
}
|
|
473
|
-
case "record": {
|
|
474
|
-
const members: Array<readonly [string, boolean, string]> = [];
|
|
475
|
-
for (const field of anonymousFieldsByOwner.get(id) ?? []) {
|
|
476
|
-
const child = childKey(field.value.id);
|
|
477
|
-
if (child === undefined) break;
|
|
478
|
-
members.push([field.name, field.required, child]);
|
|
479
|
-
}
|
|
480
|
-
if (members.length === (anonymousFieldsByOwner.get(id)?.length ?? 0)) {
|
|
481
|
-
members.sort(([left], [right]) => left.localeCompare(right));
|
|
482
|
-
key = JSON.stringify(["record", members,
|
|
483
|
-
node.expression.semantic?.kind === "value-object" ? node.expression.semantic.geometry : undefined]);
|
|
484
|
-
}
|
|
485
|
-
break;
|
|
486
|
-
}
|
|
487
|
-
case "sum": {
|
|
488
|
-
const members: Array<readonly [string, string]> = [];
|
|
489
|
-
for (const variant of anonymousVariantsByOwner.get(id) ?? []) {
|
|
490
|
-
const child = childKey(variant.value.id);
|
|
491
|
-
if (child === undefined) break;
|
|
492
|
-
// A union arm's discriminator is part of its wire contract. Two sums
|
|
493
|
-
// with the same carried value shapes but different arm labels cannot
|
|
494
|
-
// share one USDA prim: authoring reconstructs the nominal oneof from
|
|
495
|
-
// these labels before it decodes the generated Protobuf call.
|
|
496
|
-
members.push([variant.label ?? variant.entity.label, child]);
|
|
497
|
-
}
|
|
498
|
-
if (members.length === (anonymousVariantsByOwner.get(id)?.length ?? 0)) {
|
|
499
|
-
members.sort(([leftLabel, leftChild], [rightLabel, rightChild]) =>
|
|
500
|
-
leftLabel.localeCompare(rightLabel) || leftChild.localeCompare(rightChild));
|
|
501
|
-
key = JSON.stringify(["sum", members]);
|
|
502
|
-
}
|
|
503
|
-
break;
|
|
504
|
-
}
|
|
505
|
-
case "recursive":
|
|
506
|
-
key = undefined;
|
|
507
|
-
break;
|
|
508
|
-
}
|
|
509
|
-
active.delete(id);
|
|
510
|
-
semanticTypeKeyMemo.set(id, key ?? null);
|
|
511
|
-
return key;
|
|
512
|
-
};
|
|
513
|
-
|
|
514
|
-
const canonicalTypeBySemanticKey = new Map<string, LawAnonymousValueTypeNode>();
|
|
515
|
-
const canonicalTypeIdById = new Map<string, string>();
|
|
516
|
-
for (const type of [...anonymousTypes].sort((left, right) => left.entity.sid.localeCompare(right.entity.sid))) {
|
|
517
|
-
const ownPath = pathById.get(type.entity.id);
|
|
518
|
-
if (ownPath?.endsWith(`/Types/t_${type.entity.sid}`) !== true) continue;
|
|
519
|
-
const key = semanticTypeKey(type.entity.id);
|
|
520
|
-
if (key === undefined) continue;
|
|
521
|
-
const canonical = canonicalTypeBySemanticKey.get(key);
|
|
522
|
-
if (canonical === undefined) {
|
|
523
|
-
canonicalTypeBySemanticKey.set(key, type);
|
|
524
|
-
canonicalTypeIdById.set(type.entity.id, type.entity.id);
|
|
525
|
-
continue;
|
|
526
|
-
}
|
|
527
|
-
canonicalTypeIdById.set(type.entity.id, canonical.entity.id);
|
|
528
|
-
pathById.set(type.entity.id, requiredPath(canonical.entity.id, pathById));
|
|
424
|
+
// Anonymous value types are structural values, not independently lived declarations: one semantic
|
|
425
|
+
// shape, one canonical prim (anonymous_value_interning.ts — the same interning the graph discovery
|
|
426
|
+
// ledgers nested wire keys against). Declared types remain untouched.
|
|
427
|
+
const interning = internAnonymousValues({
|
|
428
|
+
types: anonymousTypes,
|
|
429
|
+
fields: anonymousFields,
|
|
430
|
+
variants: anonymousVariants,
|
|
431
|
+
ownsPrim: (type) => pathById.get(type.entity.id)?.endsWith(`/Types/t_${type.entity.sid}`) === true,
|
|
432
|
+
});
|
|
433
|
+
const canonicalTypeIdById = interning.canonicalTypeIdById;
|
|
434
|
+
for (const [typeId, canonicalId] of canonicalTypeIdById) {
|
|
435
|
+
if (typeId !== canonicalId) pathById.set(typeId, requiredPath(canonicalId, pathById));
|
|
529
436
|
}
|
|
530
437
|
|
|
531
438
|
// Wrapper types do not own a USDA prim: their identity is the final path of the carried type.
|
|
@@ -561,41 +468,11 @@ export function compileSparseLawGraphUsda(
|
|
|
561
468
|
// When two record/sum types share one canonical prim, their nested structural references must
|
|
562
469
|
// share the canonical child paths too. This preserves effects which target a nested payload field;
|
|
563
470
|
// no dangling path is allowed merely because its source owner was the non-canonical occurrence.
|
|
564
|
-
for (const [
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
if (child !== undefined) canonicalFields.set(JSON.stringify([field.name, field.required, child]), field);
|
|
570
|
-
}
|
|
571
|
-
for (const field of anonymousFieldsByOwner.get(duplicateId) ?? []) {
|
|
572
|
-
const child = semanticTypeKey(field.value.id);
|
|
573
|
-
const canonical = child === undefined
|
|
574
|
-
? undefined
|
|
575
|
-
: canonicalFields.get(JSON.stringify([field.name, field.required, child]));
|
|
576
|
-
if (canonical !== undefined) {
|
|
577
|
-
pathById.set(field.entity.id, requiredPath(canonical.entity.id, pathById));
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
const canonicalVariants = new Map<string, LawAnonymousValueVariantNode[]>();
|
|
582
|
-
for (const variant of anonymousVariantsByOwner.get(canonicalId) ?? []) {
|
|
583
|
-
const child = semanticTypeKey(variant.value.id);
|
|
584
|
-
if (child === undefined) continue;
|
|
585
|
-
const key = JSON.stringify([variant.label ?? variant.entity.label, child]);
|
|
586
|
-
const matches = canonicalVariants.get(key) ?? [];
|
|
587
|
-
matches.push(variant);
|
|
588
|
-
canonicalVariants.set(key, matches);
|
|
589
|
-
}
|
|
590
|
-
for (const variant of anonymousVariantsByOwner.get(duplicateId) ?? []) {
|
|
591
|
-
const child = semanticTypeKey(variant.value.id);
|
|
592
|
-
if (child === undefined) continue;
|
|
593
|
-
const key = JSON.stringify([variant.label ?? variant.entity.label, child]);
|
|
594
|
-
const canonical = canonicalVariants.get(key)?.shift();
|
|
595
|
-
if (canonical !== undefined) {
|
|
596
|
-
pathById.set(variant.entity.id, requiredPath(canonical.entity.id, pathById));
|
|
597
|
-
}
|
|
598
|
-
}
|
|
471
|
+
for (const [fieldId, canonicalId] of interning.canonicalFieldIdById) {
|
|
472
|
+
pathById.set(fieldId, requiredPath(canonicalId, pathById));
|
|
473
|
+
}
|
|
474
|
+
for (const [variantId, canonicalId] of interning.canonicalVariantIdById) {
|
|
475
|
+
pathById.set(variantId, requiredPath(canonicalId, pathById));
|
|
599
476
|
}
|
|
600
477
|
for (const field of fields) {
|
|
601
478
|
const owner = pathById.get(field.owner.id);
|
|
@@ -1101,6 +978,13 @@ function emitIntent(
|
|
|
1101
978
|
body.push(`${pad}custom rel nomos:resolves = <${requiredPath(intent.resolves.id, paths)}>`);
|
|
1102
979
|
}
|
|
1103
980
|
if (intent.births) body.push(`${pad}custom uniform bool nomos:births = true`);
|
|
981
|
+
if (intent.birthKey !== undefined) {
|
|
982
|
+
body.push(
|
|
983
|
+
`${pad}custom uniform token nomos:birthKeyType = ${quote(intent.birthKey.type)}`,
|
|
984
|
+
`${pad}custom uniform string nomos:birthKeyBrand = ${quote(intent.birthKey.brand)}`,
|
|
985
|
+
`${pad}custom uniform string nomos:birthKeyNamespace = ${quote(intent.birthKey.namespace)}`,
|
|
986
|
+
);
|
|
987
|
+
}
|
|
1104
988
|
if (intent.birthRecord !== undefined) {
|
|
1105
989
|
body.push(
|
|
1106
990
|
`${pad}custom rel nomos:birthRecord = <${requiredPath(intent.birthRecord.id, paths)}>`,
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// NOMOS — A tolerant structural reader for compiler-authored USDA layers.
|
|
2
|
+
//
|
|
3
|
+
// The compiler writes one opinion per line with every prim's braces on their own lines
|
|
4
|
+
// (law_graph_usda.ts, identity_usda.ts). This reader recovers that prim tree and the raw text of
|
|
5
|
+
// each `nomos:*` opinion without interpreting USD composition. It is NOT a USD parser: an authored
|
|
6
|
+
// stage with arbitrary layout is out of scope, and a layer the compiler did not write is refused by
|
|
7
|
+
// the callers that know what they expect to find.
|
|
8
|
+
//
|
|
9
|
+
// Why a second reader beside identity_usda.ts: that one is exact for the identity layer's typed
|
|
10
|
+
// subset and throws on anything else. Law layers carry relationships, dictionaries and typed
|
|
11
|
+
// literals that layer never contains, so the baseline export needs a reader that keeps going past
|
|
12
|
+
// opinions it does not understand instead of refusing a whole released layer over one of them.
|
|
13
|
+
|
|
14
|
+
export interface UsdaPrim {
|
|
15
|
+
/** The prim's name, unquoted. */
|
|
16
|
+
readonly name: string;
|
|
17
|
+
/** The schema/type word between `def` and the name, or "" for an untyped `over`. */
|
|
18
|
+
readonly type: string;
|
|
19
|
+
/** Absolute prim path, `/A/B/C`. */
|
|
20
|
+
readonly path: string;
|
|
21
|
+
/** `nomos:<attr>` opinions by attribute name, raw literal text (quoted strings keep their quotes). */
|
|
22
|
+
readonly attrs: ReadonlyMap<string, string>;
|
|
23
|
+
/** `rel nomos:<name>` targets by relationship name (one or several prim paths). */
|
|
24
|
+
readonly rels: ReadonlyMap<string, readonly string[]>;
|
|
25
|
+
readonly children: readonly UsdaPrim[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const PRIM_OPEN = /^\s*(?:def|over|class)\b(?:\s+([A-Za-z_][A-Za-z0-9_]*))?\s+"((?:[^"\\]|\\.)*)"\s*(?:\(\s*)?$/;
|
|
29
|
+
const REL = /^\s*(?:custom\s+)?(?:uniform\s+)?rel\s+nomos:([A-Za-z0-9_]+)\s*=\s*(.+?)\s*$/;
|
|
30
|
+
const ATTR = /^\s*(?:custom\s+)?(?:uniform\s+)?[A-Za-z0-9_]+(?:\[\])?\s+nomos:([A-Za-z0-9_]+)\s*=\s*(.+?)\s*$/;
|
|
31
|
+
|
|
32
|
+
/** Decode a USDA quoted string literal (the escapes the compiler writes). */
|
|
33
|
+
export function unquoteUsda(raw: string): string {
|
|
34
|
+
const value = raw.trim();
|
|
35
|
+
if (!value.startsWith('"') || !value.endsWith('"') || value.length < 2) {
|
|
36
|
+
throw new Error(`expected a quoted USDA string, got ${value}`);
|
|
37
|
+
}
|
|
38
|
+
let out = "";
|
|
39
|
+
for (let index = 1; index < value.length - 1; index += 1) {
|
|
40
|
+
const character = value[index]!;
|
|
41
|
+
if (character !== "\\") {
|
|
42
|
+
out += character;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
index += 1;
|
|
46
|
+
const escaped = value[index];
|
|
47
|
+
if (escaped === undefined) throw new Error("unterminated USDA escape");
|
|
48
|
+
if (escaped === "u") {
|
|
49
|
+
const digits = value.slice(index + 1, index + 5);
|
|
50
|
+
if (!/^[0-9a-fA-F]{4}$/u.test(digits)) throw new Error(`invalid USDA unicode escape \\u${digits}`);
|
|
51
|
+
out += String.fromCharCode(Number.parseInt(digits, 16));
|
|
52
|
+
index += 4;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const decoded: Record<string, string> = { "\\": "\\", '"': '"', n: "\n", r: "\r", t: "\t", b: "\b", f: "\f" };
|
|
56
|
+
const replacement = decoded[escaped];
|
|
57
|
+
if (replacement === undefined) throw new Error(`unsupported USDA escape \\${escaped}`);
|
|
58
|
+
out += replacement;
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Decode a one-line USDA string array literal `["a", "b"]`. */
|
|
64
|
+
export function unquoteUsdaList(raw: string): string[] {
|
|
65
|
+
const value = raw.trim();
|
|
66
|
+
if (!value.startsWith("[") || !value.endsWith("]")) throw new Error(`expected a USDA array, got ${value}`);
|
|
67
|
+
const body = value.slice(1, -1).trim();
|
|
68
|
+
if (body === "") return [];
|
|
69
|
+
const items: string[] = [];
|
|
70
|
+
let start = 0;
|
|
71
|
+
let quoted = false;
|
|
72
|
+
let escaped = false;
|
|
73
|
+
for (let index = 0; index < body.length; index += 1) {
|
|
74
|
+
const character = body[index]!;
|
|
75
|
+
if (escaped) { escaped = false; continue; }
|
|
76
|
+
if (quoted && character === "\\") { escaped = true; continue; }
|
|
77
|
+
if (character === '"') quoted = !quoted;
|
|
78
|
+
if (!quoted && character === ",") {
|
|
79
|
+
items.push(unquoteUsda(body.slice(start, index)));
|
|
80
|
+
start = index + 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (quoted) throw new Error("unterminated USDA string array");
|
|
84
|
+
items.push(unquoteUsda(body.slice(start)));
|
|
85
|
+
return items;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function relTargets(raw: string): string[] {
|
|
89
|
+
const value = raw.trim();
|
|
90
|
+
const body = value.startsWith("[") && value.endsWith("]") ? value.slice(1, -1) : value;
|
|
91
|
+
return body
|
|
92
|
+
.split(",")
|
|
93
|
+
.map((item) => item.trim())
|
|
94
|
+
.filter((item) => item.length > 0)
|
|
95
|
+
.map((item) => {
|
|
96
|
+
if (!item.startsWith("<") || !item.endsWith(">")) throw new Error(`expected a USDA relationship target, got ${item}`);
|
|
97
|
+
return item.slice(1, -1);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
interface MutablePrim {
|
|
102
|
+
name: string;
|
|
103
|
+
type: string;
|
|
104
|
+
path: string;
|
|
105
|
+
attrs: Map<string, string>;
|
|
106
|
+
rels: Map<string, string[]>;
|
|
107
|
+
children: MutablePrim[];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Read every root prim of a compiler-authored layer. The layer header `( … )` and any
|
|
111
|
+
* dictionary-valued opinion (`… = {` … `}`) are skipped structurally. */
|
|
112
|
+
export function readUsdaPrims(text: string): UsdaPrim[] {
|
|
113
|
+
const roots: MutablePrim[] = [];
|
|
114
|
+
const stack: MutablePrim[] = [];
|
|
115
|
+
const lines = text.split(/\r?\n/);
|
|
116
|
+
let index = 0;
|
|
117
|
+
// Skip the layer header: everything before the first prim line.
|
|
118
|
+
while (index < lines.length && PRIM_OPEN.exec(lines[index]!) === null) index += 1;
|
|
119
|
+
let pending: MutablePrim | undefined;
|
|
120
|
+
let dictionaryDepth = 0;
|
|
121
|
+
for (; index < lines.length; index += 1) {
|
|
122
|
+
const line = lines[index]!;
|
|
123
|
+
const trimmed = line.trim();
|
|
124
|
+
if (dictionaryDepth > 0) {
|
|
125
|
+
if (trimmed.endsWith("{")) dictionaryDepth += 1;
|
|
126
|
+
else if (trimmed === "}") dictionaryDepth -= 1;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (pending !== undefined) {
|
|
130
|
+
// Between a prim's opening line and its `{`: prim metadata we do not read.
|
|
131
|
+
if (trimmed === "{") {
|
|
132
|
+
stack.push(pending);
|
|
133
|
+
pending = undefined;
|
|
134
|
+
}
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const open = PRIM_OPEN.exec(line);
|
|
138
|
+
if (open !== null) {
|
|
139
|
+
const name = unquoteUsda(`"${open[2] ?? ""}"`);
|
|
140
|
+
const parent = stack[stack.length - 1];
|
|
141
|
+
const prim: MutablePrim = {
|
|
142
|
+
name,
|
|
143
|
+
type: open[1] ?? "",
|
|
144
|
+
path: `${parent?.path ?? ""}/${name}`,
|
|
145
|
+
attrs: new Map(),
|
|
146
|
+
rels: new Map(),
|
|
147
|
+
children: [],
|
|
148
|
+
};
|
|
149
|
+
(parent?.children ?? roots).push(prim);
|
|
150
|
+
pending = prim;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (trimmed === "}") {
|
|
154
|
+
stack.pop();
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (trimmed.endsWith("= {")) {
|
|
158
|
+
dictionaryDepth = 1;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const current = stack[stack.length - 1];
|
|
162
|
+
if (current === undefined) continue;
|
|
163
|
+
const rel = REL.exec(line);
|
|
164
|
+
if (rel !== null) {
|
|
165
|
+
current.rels.set(rel[1]!, relTargets(rel[2]!));
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const attr = ATTR.exec(line);
|
|
169
|
+
if (attr !== null) current.attrs.set(attr[1]!, attr[2]!);
|
|
170
|
+
}
|
|
171
|
+
return roots;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Index every prim of a forest by absolute path. */
|
|
175
|
+
export function indexUsdaPrims(roots: readonly UsdaPrim[]): Map<string, UsdaPrim> {
|
|
176
|
+
const index = new Map<string, UsdaPrim>();
|
|
177
|
+
const visit = (prim: UsdaPrim): void => {
|
|
178
|
+
index.set(prim.path, prim);
|
|
179
|
+
for (const child of prim.children) visit(child);
|
|
180
|
+
};
|
|
181
|
+
for (const root of roots) visit(root);
|
|
182
|
+
return index;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** A `nomos:<attr>` string opinion, decoded, or undefined when the prim carries none. */
|
|
186
|
+
export function usdaString(prim: UsdaPrim, attr: string): string | undefined {
|
|
187
|
+
const raw = prim.attrs.get(attr);
|
|
188
|
+
return raw === undefined ? undefined : unquoteUsda(raw);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** A `nomos:<attr>` string-array opinion, decoded, or undefined when the prim carries none. */
|
|
192
|
+
export function usdaStringList(prim: UsdaPrim, attr: string): string[] | undefined {
|
|
193
|
+
const raw = prim.attrs.get(attr);
|
|
194
|
+
return raw === undefined ? undefined : unquoteUsdaList(raw);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function usdaChild(prim: UsdaPrim | undefined, name: string): UsdaPrim | undefined {
|
|
198
|
+
return prim?.children.find((child) => child.name === name);
|
|
199
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// PER-KIND LAW (CAP-809, architecture/frontier_law_storage.md slice 6).
|
|
2
|
+
//
|
|
3
|
+
// A workspace kind installs only the domains it serves. Its law is a unit set holding the framework unit, the
|
|
4
|
+
// composition unit and the domain units of exactly those domains; the kernel admits such a set and its composed stage
|
|
5
|
+
// exports only the domains it holds. The subset is a version of the same law identity (the package name), so
|
|
6
|
+
// installing it over the complete set (or the reverse) replaces that version at the frontier like any other.
|
|
7
|
+
|
|
8
|
+
import { COMPOSITION_UNIT, FRAMEWORK_UNIT, domainUnitKey, type LawUnit } from "./law_units.js";
|
|
9
|
+
|
|
10
|
+
/** A domain's key in a package (`nomos:domains`). Typed: a bare string never selects law. */
|
|
11
|
+
export type LawDomainKey = string & { readonly __nomosLawDomainKey: unique symbol };
|
|
12
|
+
|
|
13
|
+
export function lawDomainKey(name: string): LawDomainKey {
|
|
14
|
+
if (name.length === 0 || name.trim() !== name || name.includes("\0")) {
|
|
15
|
+
throw new TypeError(`${JSON.stringify(name)} is not a domain key`);
|
|
16
|
+
}
|
|
17
|
+
return name as LawDomainKey;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The units a workspace kind installs: framework, composition, and the domain unit of each domain it serves.
|
|
22
|
+
* Refuses an empty selection, a repeated domain and a domain the compiled package does not hold — a selection
|
|
23
|
+
* names law exactly, it never silently drops a domain.
|
|
24
|
+
*/
|
|
25
|
+
export function unitsForDomains(units: readonly LawUnit[], domains: readonly LawDomainKey[]): LawUnit[] {
|
|
26
|
+
if (domains.length === 0) throw new Error("a workspace kind must serve at least one domain");
|
|
27
|
+
const wanted = new Set<string>();
|
|
28
|
+
for (const domain of domains) {
|
|
29
|
+
if (wanted.has(domain)) throw new Error(`domain ${domain} is selected twice`);
|
|
30
|
+
wanted.add(domain);
|
|
31
|
+
}
|
|
32
|
+
const byKey = new Map(units.map((unit) => [unit.key, unit] as const));
|
|
33
|
+
const selected: LawUnit[] = [];
|
|
34
|
+
for (const key of [FRAMEWORK_UNIT, COMPOSITION_UNIT]) {
|
|
35
|
+
const unit = byKey.get(key);
|
|
36
|
+
if (unit === undefined) throw new Error(`the compiled units have no ${key} unit`);
|
|
37
|
+
selected.push(unit);
|
|
38
|
+
}
|
|
39
|
+
for (const domain of wanted) {
|
|
40
|
+
const unit = byKey.get(domainUnitKey(domain));
|
|
41
|
+
if (unit === undefined) throw new Error(`the compiled units hold no domain ${domain}`);
|
|
42
|
+
selected.push(unit);
|
|
43
|
+
}
|
|
44
|
+
return selected.sort((left, right) => (left.key < right.key ? -1 : left.key > right.key ? 1 : 0));
|
|
45
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// THE LAW IDENTITY (CAP-773, architecture/law_units_format.md "Identities").
|
|
2
|
+
//
|
|
3
|
+
// A law is a set of content-addressed units (framework, composition, one per domain). Its identity is the unit set
|
|
4
|
+
// identity: sha256("nomos-law-unit-set-identity-v1\0" { key "\0" unitHash "\0" }*), units sorted by key in byte
|
|
5
|
+
// order. It names exactly which units the set holds and nothing about how a kernel composes them, so publishers,
|
|
6
|
+
// generated clients, the console and qualification all name law by it, and every kernel agrees on it whatever its
|
|
7
|
+
// composer version. The kernel's definition is `law_unit_set_identity` (kernel/src/law_units.rs); the two are bound
|
|
8
|
+
// by one vector (law_unit_set_identity.test.ts, law_units.rs tests) and by a compiled fixture the real kernel composes
|
|
9
|
+
// (kernel/tests/compiled_law_units.rs).
|
|
10
|
+
//
|
|
11
|
+
// Not the law identity: the realised closure a target's kernel composes from the set (it also depends on the
|
|
12
|
+
// kernel's OpenUSD pin and composer version), the compiler's package closure (`build/<name>.law.sha256`), and the
|
|
13
|
+
// kernel's composition identity (the set record's `setIdentity`, which binds the set to one composer).
|
|
14
|
+
//
|
|
15
|
+
// Dependency-free and synchronous over the bytes: each runtime hashes the preimage with its own sha256 (node:crypto
|
|
16
|
+
// in the compiler, WebCrypto in the console and CLI, the Peer Office's own digest on a device).
|
|
17
|
+
|
|
18
|
+
export const LAW_UNIT_SET_IDENTITY_TAG = "nomos-law-unit-set-identity-v1";
|
|
19
|
+
|
|
20
|
+
export interface LawUnitSetMember {
|
|
21
|
+
readonly key: string;
|
|
22
|
+
readonly unitHash: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const HEX64 = /^[0-9a-f]{64}$/;
|
|
26
|
+
const byteOrder = (left: string, right: string): number => (left < right ? -1 : left > right ? 1 : 0);
|
|
27
|
+
|
|
28
|
+
/** The exact bytes the unit set identity hashes. Refuses a duplicate key or a unit hash that is not a sha256. */
|
|
29
|
+
export function lawUnitSetIdentityPreimage(units: readonly LawUnitSetMember[]): Uint8Array {
|
|
30
|
+
const sorted = [...units].sort((left, right) => byteOrder(left.key, right.key));
|
|
31
|
+
const parts = [`${LAW_UNIT_SET_IDENTITY_TAG}\0`];
|
|
32
|
+
sorted.forEach((unit, index) => {
|
|
33
|
+
if (unit.key.length === 0 || unit.key.includes("\0")) throw new Error(`law unit key ${JSON.stringify(unit.key)} is not a unit key`);
|
|
34
|
+
if (index > 0 && sorted[index - 1]!.key === unit.key) throw new Error(`law unit ${unit.key} appears twice in one set`);
|
|
35
|
+
if (!HEX64.test(unit.unitHash)) throw new Error(`law unit ${unit.key} names ${JSON.stringify(unit.unitHash)}, not a unit hash`);
|
|
36
|
+
parts.push(unit.key, "\0", unit.unitHash, "\0");
|
|
37
|
+
});
|
|
38
|
+
return new TextEncoder().encode(parts.join(""));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The unit set identity, hashed with the caller's synchronous sha256 (lowercase hex out). */
|
|
42
|
+
export function lawUnitSetIdentityWith(sha256Hex: (bytes: Uint8Array) => string, units: readonly LawUnitSetMember[]): string {
|
|
43
|
+
return sha256Hex(lawUnitSetIdentityPreimage(units));
|
|
44
|
+
}
|