@githolon/dsl 0.114.0 → 0.116.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/assets/operation-contract.wasm +0 -0
  2. package/dart/CHANGELOG.md +55 -0
  3. package/dart/lib/src/application_framework_calls.dart +7 -1
  4. package/dart/lib/src/console_enrolment.dart +7 -0
  5. package/dart/lib/src/console_http_io.dart +22 -15
  6. package/dart/lib/src/dev_cloud.dart +62 -0
  7. package/dart/lib/src/nms_generated_runtime.dart +1 -0
  8. package/dart/lib/src/operation_receipts.dart +74 -7
  9. package/dart/lib/src/reception/dart_reception.dart +26 -6
  10. package/dart/lib/src/result_usda.dart +64 -27
  11. package/dart/lib/src/runner/node-entry.mjs +4733 -1534
  12. package/dart/lib/src/runtime_v2_transport.dart +3 -0
  13. package/dart/lib/src/sync_status_projection.dart +11 -0
  14. package/dart/pubspec.lock +1 -1
  15. package/dart/pubspec.yaml +2 -2
  16. package/dart/test/console_enrolment_test.dart +47 -0
  17. package/dart/test/kernel_refused_follow_test.dart +36 -0
  18. package/dart/test/model_adoption_test.dart +49 -0
  19. package/dart/test/result_usda_test.dart +67 -0
  20. package/dart/test/runtime_projections_test.dart +41 -0
  21. package/dart_types/CHANGELOG.md +30 -0
  22. package/dart_types/lib/src/approved_model.dart +78 -1
  23. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pb.dart +528 -2
  24. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbenum.dart +24 -0
  25. package/dart_types/lib/src/proto/nomos/runtime/v2/correspondence_desk.pb.dart +1897 -112
  26. package/dart_types/lib/src/proto/nomos/runtime/v2/model_adoption_desk.pb.dart +98 -0
  27. package/dart_types/lib/src/proto/nomos/runtime/v2/observability_desk.pbenum.dart +11 -1
  28. package/dart_types/lib/src/proto/nomos/runtime/v2/records_desk.pb.dart +16 -0
  29. package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pb.dart +43 -2
  30. package/dart_types/lib/src/vo_canonical.dart +185 -0
  31. package/dart_types/pubspec.lock +1 -1
  32. package/dart_types/pubspec.yaml +3 -1
  33. package/dart_types/test/vo_canonical_test.dart +91 -0
  34. package/package.json +4 -2
  35. package/src/aggregate.ts +95 -0
  36. package/src/anonymous_value_interning.ts +205 -0
  37. package/src/baseline.ts +27 -0
  38. package/src/baseline_export.ts +432 -0
  39. package/src/baseline_import.ts +312 -0
  40. package/src/baseline_usda.ts +185 -0
  41. package/src/birth_key_rule.ts +472 -0
  42. package/src/births.ts +207 -24
  43. package/src/build_package.ts +47 -7
  44. package/src/build_provenance.ts +10 -0
  45. package/src/business_birth.ts +50 -4
  46. package/src/check_package_main.ts +50 -6
  47. package/src/codegen_call_proto.ts +5 -18
  48. package/src/codegen_call_usda.ts +6 -3
  49. package/src/codegen_dart.ts +114 -60
  50. package/src/codegen_dart_client.ts +52 -6
  51. package/src/codegen_dart_facade.ts +57 -35
  52. package/src/codegen_dart_framework_calls.ts +20 -1
  53. package/src/codegen_proof.ts +5 -2
  54. package/src/codegen_result_proto.ts +8 -6
  55. package/src/codegen_result_usda.ts +12 -4
  56. package/src/codegen_ts.ts +109 -10
  57. package/src/compatibility_graph.ts +41 -4
  58. package/src/compatibility_usda.ts +11 -1
  59. package/src/compile_package_main.ts +222 -52
  60. package/src/composed_wire_identity.ts +24 -0
  61. package/src/directive.ts +6 -0
  62. package/src/domain_requirements.ts +75 -0
  63. package/src/engine_entry.ts +12 -4
  64. package/src/engine_javascript.ts +12 -6
  65. package/src/fields.ts +109 -0
  66. package/src/identity_usda.ts +80 -103
  67. package/src/index.ts +35 -1
  68. package/src/intent.ts +33 -3
  69. package/src/law_check.ts +34 -0
  70. package/src/law_graph.ts +142 -1
  71. package/src/law_graph_usda.ts +27 -144
  72. package/src/law_layer_prims.ts +174 -0
  73. package/src/law_unit_selection.ts +45 -0
  74. package/src/law_units.ts +7 -6
  75. package/src/law_version.ts +108 -0
  76. package/src/package_config.ts +44 -0
  77. package/src/package_dependencies.ts +8 -0
  78. package/src/point_guard.ts +5 -1
  79. package/src/release_inspection.ts +6 -4
  80. package/src/release_publication.ts +6 -5
  81. package/src/usda_text.ts +78 -3
  82. package/src/value_identity.ts +355 -0
  83. package/src/value_object.ts +35 -4
  84. package/src/wire.ts +2 -0
  85. package/src/wire_paths.ts +17 -0
  86. package/src/workspace_routing.ts +1 -1
package/src/law_check.ts CHANGED
@@ -1103,6 +1103,39 @@ function invariantFindings(mod: DomainModule, aggById: ReadonlyMap<string, Aggre
1103
1103
  return out;
1104
1104
  }
1105
1105
 
1106
+ // ── VALUE IDENTITY (CAP-853) — `JSON.stringify` COMPARISON WARN ──────────────────────────
1107
+ // `JSON.stringify(a) === JSON.stringify(b)` compares TEXT, and json text depends on member insertion
1108
+ // order: a value object read back from storage arrives in storage order, so the comparison calls a
1109
+ // value unequal to itself (CO2's field-publication invariant refused every round-tripped snapshot).
1110
+ // Law compares typed values with `<Vo>.equals(a, b)` (`nomos.vo-canonical.v1`). This is a source-text
1111
+ // heuristic over the law bodies Nomos runs (plans, aggregate invariants, workspace invariants), so it
1112
+ // WARNS — it cannot prove the operands are structured values.
1113
+ const JSON_STRINGIFY_COMPARISON =
1114
+ /JSON\.stringify\s*\((?:[^()]|\((?:[^()]|\([^()]*\))*\))*\)\s*[!=]==?|[!=]==?\s*JSON\.stringify\s*\(/;
1115
+
1116
+ function jsonStringifyComparisonFindings(mod: DomainModule): Finding[] {
1117
+ const domain = mod.domain ?? mod.name;
1118
+ const out: Finding[] = [];
1119
+ const judge = (body: unknown, subject: string, directiveId?: string): void => {
1120
+ if (typeof body !== "function") return;
1121
+ if (!JSON_STRINGIFY_COMPARISON.test(Function.prototype.toString.call(body))) return;
1122
+ out.push({
1123
+ domain,
1124
+ ...(directiveId === undefined ? {} : { directiveId }),
1125
+ level: "warn",
1126
+ what: `${subject} compares JSON.stringify(...) text — member order is not part of a value, so a value read back from storage can compare unequal to itself`,
1127
+ fix: "compare typed values with <ValueObject>.equals(a, b) (or valueIdentity(schema).equals for lists/maps of them); for plain json leaves compare the fields you mean",
1128
+ });
1129
+ };
1130
+ for (const dir of mod.directives as Directive<unknown>[]) judge(dir.plan, `directive '${dir.id}' plan`, dir.id);
1131
+ for (const agg of mod.aggregates) judge(agg.invariant, `aggregate '${agg.id}' invariant`);
1132
+ for (const wi of mod.workspaceInvariants ?? []) {
1133
+ judge(wi.assert, `workspace invariant '${wi.id}' assert`, wi.on);
1134
+ judge(wi.reads, `workspace invariant '${wi.id}' reads`, wi.on);
1135
+ }
1136
+ return out;
1137
+ }
1138
+
1106
1139
  // ── TWIN 2 (slice 2) — TOP-LEVEL PORT-READ pool-eligibility WARN ───────────────────
1107
1140
  // A tenant MODULE whose TOP LEVEL reads the captured ports (`Date.now()`/`new Date()`/`Math.random()`
1108
1141
  // at module scope, not inside a plan) makes the built engine lump PRISTINE-POOL-INELIGIBLE
@@ -1359,6 +1392,7 @@ export function checkDomainModules(
1359
1392
  findings.push(...nullBanFindings(mod, aggById));
1360
1393
  findings.push(...nullClearAuthorityFindings(mod, aggById));
1361
1394
  findings.push(...attestedReadFindings(mod));
1395
+ findings.push(...jsonStringifyComparisonFindings(mod));
1362
1396
  }
1363
1397
 
1364
1398
  // The BIRTH-RECIPE twin (unify): fold the scattered `.births()` recipe diagnostics into the
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
- const labels = fields.map((field) => field.name);
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,
@@ -1,3 +1,4 @@
1
+ import { usdaString } from "./usda_text.js";
1
2
  import type {
2
3
  LawAggregateNode,
3
4
  LawAnonymousValueFieldNode,
@@ -28,6 +29,7 @@ import type {
28
29
  LawWorkspaceRelationNode,
29
30
  } from "./law_graph.js";
30
31
  import { MemberAuthenticationKind, type AuthenticationProviderRef } from "./member_authentication.js";
32
+ import { internAnonymousValues } from "./anonymous_value_interning.js";
31
33
 
32
34
  const FRAMEWORK_SCALAR: Readonly<Record<string, string>> = {
33
35
  string: "/Nomos/Framework/Types/t_0000000000000001",
@@ -420,112 +422,18 @@ export function compileSparseLawGraphUsda(
420
422
  };
421
423
  for (const type of anonymousTypes) resolveAnonymousTypePath(type.entity.id);
422
424
 
423
- // Anonymous value types are structural values, not independently lived declarations. The graph
424
- // deliberately retains their source owner for diagnostics, but USDA must not turn that provenance
425
- // into thousands of duplicate runtime prims. Compute a semantic key which excludes owner identity,
426
- // then point every equivalent type graph at one canonical prim. Declared types remain untouched.
427
- // Recursive graphs keep their owner-scoped identity for now: aliasing a cycle needs an explicit
428
- // recursion-isomorphism proof, whereas every acyclic case below is exact by construction.
429
- const semanticTypeKeyMemo = new Map<string, string | null>();
430
- const semanticTypeKey = (id: string, active = new Set<string>()): string | undefined => {
431
- const memoized = semanticTypeKeyMemo.get(id);
432
- if (memoized !== undefined) return memoized ?? undefined;
433
- if (active.has(id)) {
434
- semanticTypeKeyMemo.set(id, null);
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));
425
+ // Anonymous value types are structural values, not independently lived declarations: one semantic
426
+ // shape, one canonical prim (anonymous_value_interning.ts — the same interning the graph discovery
427
+ // ledgers nested wire keys against). Declared types remain untouched.
428
+ const interning = internAnonymousValues({
429
+ types: anonymousTypes,
430
+ fields: anonymousFields,
431
+ variants: anonymousVariants,
432
+ ownsPrim: (type) => pathById.get(type.entity.id)?.endsWith(`/Types/t_${type.entity.sid}`) === true,
433
+ });
434
+ const canonicalTypeIdById = interning.canonicalTypeIdById;
435
+ for (const [typeId, canonicalId] of canonicalTypeIdById) {
436
+ if (typeId !== canonicalId) pathById.set(typeId, requiredPath(canonicalId, pathById));
529
437
  }
530
438
 
531
439
  // Wrapper types do not own a USDA prim: their identity is the final path of the carried type.
@@ -561,41 +469,11 @@ export function compileSparseLawGraphUsda(
561
469
  // When two record/sum types share one canonical prim, their nested structural references must
562
470
  // share the canonical child paths too. This preserves effects which target a nested payload field;
563
471
  // no dangling path is allowed merely because its source owner was the non-canonical occurrence.
564
- for (const [duplicateId, canonicalId] of canonicalTypeIdById) {
565
- if (duplicateId === canonicalId) continue;
566
- const canonicalFields = new Map<string, LawAnonymousValueFieldNode>();
567
- for (const field of anonymousFieldsByOwner.get(canonicalId) ?? []) {
568
- const child = semanticTypeKey(field.value.id);
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
- }
472
+ for (const [fieldId, canonicalId] of interning.canonicalFieldIdById) {
473
+ pathById.set(fieldId, requiredPath(canonicalId, pathById));
474
+ }
475
+ for (const [variantId, canonicalId] of interning.canonicalVariantIdById) {
476
+ pathById.set(variantId, requiredPath(canonicalId, pathById));
599
477
  }
600
478
  for (const field of fields) {
601
479
  const owner = pathById.get(field.owner.id);
@@ -1101,6 +979,13 @@ function emitIntent(
1101
979
  body.push(`${pad}custom rel nomos:resolves = <${requiredPath(intent.resolves.id, paths)}>`);
1102
980
  }
1103
981
  if (intent.births) body.push(`${pad}custom uniform bool nomos:births = true`);
982
+ if (intent.birthKey !== undefined) {
983
+ body.push(
984
+ `${pad}custom uniform token nomos:birthKeyType = ${quote(intent.birthKey.type)}`,
985
+ `${pad}custom uniform string nomos:birthKeyBrand = ${quote(intent.birthKey.brand)}`,
986
+ `${pad}custom uniform string nomos:birthKeyNamespace = ${quote(intent.birthKey.namespace)}`,
987
+ );
988
+ }
1104
989
  if (intent.birthRecord !== undefined) {
1105
990
  body.push(
1106
991
  `${pad}custom rel nomos:birthRecord = <${requiredPath(intent.birthRecord.id, paths)}>`,
@@ -2062,9 +1947,7 @@ function requiredPath(id: string, paths: ReadonlyMap<string, string>): string {
2062
1947
  return path;
2063
1948
  }
2064
1949
 
2065
- function quote(value: string): string {
2066
- return JSON.stringify(value);
2067
- }
1950
+ const quote = usdaString;
2068
1951
 
2069
1952
  function fail(message: string): never {
2070
1953
  throw new Error(`NMS-USDA-001 ${message}`);
@@ -0,0 +1,174 @@
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
+ import { usdaUnquote } from "./usda_text.js";
14
+
15
+ export interface UsdaPrim {
16
+ /** The prim's name, unquoted. */
17
+ readonly name: string;
18
+ /** The schema/type word between `def` and the name, or "" for an untyped `over`. */
19
+ readonly type: string;
20
+ /** Absolute prim path, `/A/B/C`. */
21
+ readonly path: string;
22
+ /** `nomos:<attr>` opinions by attribute name, raw literal text (quoted strings keep their quotes). */
23
+ readonly attrs: ReadonlyMap<string, string>;
24
+ /** `rel nomos:<name>` targets by relationship name (one or several prim paths). */
25
+ readonly rels: ReadonlyMap<string, readonly string[]>;
26
+ readonly children: readonly UsdaPrim[];
27
+ }
28
+
29
+ const PRIM_OPEN = /^\s*(?:def|over|class)\b(?:\s+([A-Za-z_][A-Za-z0-9_]*))?\s+"((?:[^"\\]|\\.)*)"\s*(?:\(\s*)?$/;
30
+ const REL = /^\s*(?:custom\s+)?(?:uniform\s+)?rel\s+nomos:([A-Za-z0-9_]+)\s*=\s*(.+?)\s*$/;
31
+ const ATTR = /^\s*(?:custom\s+)?(?:uniform\s+)?[A-Za-z0-9_]+(?:\[\])?\s+nomos:([A-Za-z0-9_]+)\s*=\s*(.+?)\s*$/;
32
+
33
+ /** Decode a USDA quoted string literal with OpenUSD's escape rules (and legacy `\uXXXX`). */
34
+ export function unquoteUsda(raw: string): string {
35
+ return usdaUnquote(raw);
36
+ }
37
+
38
+ /** Decode a one-line USDA string array literal `["a", "b"]`. */
39
+ export function unquoteUsdaList(raw: string): string[] {
40
+ const value = raw.trim();
41
+ if (!value.startsWith("[") || !value.endsWith("]")) throw new Error(`expected a USDA array, got ${value}`);
42
+ const body = value.slice(1, -1).trim();
43
+ if (body === "") return [];
44
+ const items: string[] = [];
45
+ let start = 0;
46
+ let quoted = false;
47
+ let escaped = false;
48
+ for (let index = 0; index < body.length; index += 1) {
49
+ const character = body[index]!;
50
+ if (escaped) { escaped = false; continue; }
51
+ if (quoted && character === "\\") { escaped = true; continue; }
52
+ if (character === '"') quoted = !quoted;
53
+ if (!quoted && character === ",") {
54
+ items.push(unquoteUsda(body.slice(start, index)));
55
+ start = index + 1;
56
+ }
57
+ }
58
+ if (quoted) throw new Error("unterminated USDA string array");
59
+ items.push(unquoteUsda(body.slice(start)));
60
+ return items;
61
+ }
62
+
63
+ function relTargets(raw: string): string[] {
64
+ const value = raw.trim();
65
+ const body = value.startsWith("[") && value.endsWith("]") ? value.slice(1, -1) : value;
66
+ return body
67
+ .split(",")
68
+ .map((item) => item.trim())
69
+ .filter((item) => item.length > 0)
70
+ .map((item) => {
71
+ if (!item.startsWith("<") || !item.endsWith(">")) throw new Error(`expected a USDA relationship target, got ${item}`);
72
+ return item.slice(1, -1);
73
+ });
74
+ }
75
+
76
+ interface MutablePrim {
77
+ name: string;
78
+ type: string;
79
+ path: string;
80
+ attrs: Map<string, string>;
81
+ rels: Map<string, string[]>;
82
+ children: MutablePrim[];
83
+ }
84
+
85
+ /** Read every root prim of a compiler-authored layer. The layer header `( … )` and any
86
+ * dictionary-valued opinion (`… = {` … `}`) are skipped structurally. */
87
+ export function readUsdaPrims(text: string): UsdaPrim[] {
88
+ const roots: MutablePrim[] = [];
89
+ const stack: MutablePrim[] = [];
90
+ const lines = text.split(/\r?\n/);
91
+ let index = 0;
92
+ // Skip the layer header: everything before the first prim line.
93
+ while (index < lines.length && PRIM_OPEN.exec(lines[index]!) === null) index += 1;
94
+ let pending: MutablePrim | undefined;
95
+ let dictionaryDepth = 0;
96
+ for (; index < lines.length; index += 1) {
97
+ const line = lines[index]!;
98
+ const trimmed = line.trim();
99
+ if (dictionaryDepth > 0) {
100
+ if (trimmed.endsWith("{")) dictionaryDepth += 1;
101
+ else if (trimmed === "}") dictionaryDepth -= 1;
102
+ continue;
103
+ }
104
+ if (pending !== undefined) {
105
+ // Between a prim's opening line and its `{`: prim metadata we do not read.
106
+ if (trimmed === "{") {
107
+ stack.push(pending);
108
+ pending = undefined;
109
+ }
110
+ continue;
111
+ }
112
+ const open = PRIM_OPEN.exec(line);
113
+ if (open !== null) {
114
+ const name = unquoteUsda(`"${open[2] ?? ""}"`);
115
+ const parent = stack[stack.length - 1];
116
+ const prim: MutablePrim = {
117
+ name,
118
+ type: open[1] ?? "",
119
+ path: `${parent?.path ?? ""}/${name}`,
120
+ attrs: new Map(),
121
+ rels: new Map(),
122
+ children: [],
123
+ };
124
+ (parent?.children ?? roots).push(prim);
125
+ pending = prim;
126
+ continue;
127
+ }
128
+ if (trimmed === "}") {
129
+ stack.pop();
130
+ continue;
131
+ }
132
+ if (trimmed.endsWith("= {")) {
133
+ dictionaryDepth = 1;
134
+ continue;
135
+ }
136
+ const current = stack[stack.length - 1];
137
+ if (current === undefined) continue;
138
+ const rel = REL.exec(line);
139
+ if (rel !== null) {
140
+ current.rels.set(rel[1]!, relTargets(rel[2]!));
141
+ continue;
142
+ }
143
+ const attr = ATTR.exec(line);
144
+ if (attr !== null) current.attrs.set(attr[1]!, attr[2]!);
145
+ }
146
+ return roots;
147
+ }
148
+
149
+ /** Index every prim of a forest by absolute path. */
150
+ export function indexUsdaPrims(roots: readonly UsdaPrim[]): Map<string, UsdaPrim> {
151
+ const index = new Map<string, UsdaPrim>();
152
+ const visit = (prim: UsdaPrim): void => {
153
+ index.set(prim.path, prim);
154
+ for (const child of prim.children) visit(child);
155
+ };
156
+ for (const root of roots) visit(root);
157
+ return index;
158
+ }
159
+
160
+ /** A `nomos:<attr>` string opinion, decoded, or undefined when the prim carries none. */
161
+ export function usdaString(prim: UsdaPrim, attr: string): string | undefined {
162
+ const raw = prim.attrs.get(attr);
163
+ return raw === undefined ? undefined : unquoteUsda(raw);
164
+ }
165
+
166
+ /** A `nomos:<attr>` string-array opinion, decoded, or undefined when the prim carries none. */
167
+ export function usdaStringList(prim: UsdaPrim, attr: string): string[] | undefined {
168
+ const raw = prim.attrs.get(attr);
169
+ return raw === undefined ? undefined : unquoteUsdaList(raw);
170
+ }
171
+
172
+ export function usdaChild(prim: UsdaPrim | undefined, name: string): UsdaPrim | undefined {
173
+ return prim?.children.find((child) => child.name === name);
174
+ }
@@ -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
+ }