@githolon/dsl 0.102.5 → 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.
Files changed (131) hide show
  1. package/compile_package.mjs +43 -2
  2. package/compile_proof.d.mts +47 -0
  3. package/compile_proof.mjs +343 -0
  4. package/dart/CHANGELOG.md +7 -0
  5. package/dart/lib/src/dev_cloud.dart +8 -8
  6. package/dart/lib/src/nms_generated_runtime.dart +2 -0
  7. package/dart/lib/src/runner/node-entry.mjs +5504 -1236
  8. package/dart/lib/src/runner/offline-install.mjs +4578 -528
  9. package/dart/lib/src/test_harness.dart +206 -152
  10. package/dart/lib/src/webview_bridge.dart +196 -89
  11. package/dart/pubspec.lock +1 -1
  12. package/dart/pubspec.yaml +2 -2
  13. package/dart/test/acquire_session_credentials_test.dart +4 -3
  14. package/dart/test/adv_realm_interference_test.dart +7 -6
  15. package/dart/test/cloud_bound_topology_test.dart +3 -3
  16. package/dart/test/cluster_births_test.dart +4 -3
  17. package/dart/test/cluster_device_keyed_acceptance_test.dart +10 -16
  18. package/dart/test/cluster_local_autosync_test.dart +3 -2
  19. package/dart/test/cluster_scale_test.dart +3 -2
  20. package/dart/test/dlq_session_surface_test.dart +7 -6
  21. package/dart/test/first_app_gauntlet_test.dart +14 -21
  22. package/dart/test/fixtures/wedge/fake-install.mjs +1 -1
  23. package/dart/test/four_layer_nested_topology_test.dart +4 -4
  24. package/dart/test/harness_phase_timeout_test.dart +2 -2
  25. package/dart/test/local_sync_inert_test.dart +1 -1
  26. package/dart/test/parent_signer_recovery_test.dart +2 -2
  27. package/dart/test/sealing_client_test.dart +4 -3
  28. package/dart/test/session_bound_test.dart +8 -7
  29. package/dart/test/test_harness_test.dart +13 -12
  30. package/dart/test/vo_read_typing_test.dart +4 -3
  31. package/dart/test/watch_after_offer_stall_test.dart +4 -3
  32. package/dart/test/webview_bridge_test.dart +102 -5
  33. package/dart_types/CHANGELOG.md +5 -0
  34. package/dart_types/lib/nomos_types.dart +9 -0
  35. package/dart_types/lib/src/nomos_ref.dart +20 -0
  36. package/dart_types/lib/src/optional_ref.dart +85 -0
  37. package/dart_types/lib/src/sha256_digest.dart +41 -0
  38. package/dart_types/lib/src/versioned_nomos_ref.dart +81 -0
  39. package/dart_types/pubspec.yaml +1 -1
  40. package/dart_types/test/optional_ref_test.dart +44 -0
  41. package/dart_types/test/sha256_digest_test.dart +18 -0
  42. package/package.json +9 -5
  43. package/runtime/generated-application-host.js +5365 -1105
  44. package/src/aggregate.ts +18 -0
  45. package/src/application_composition.ts +6 -32
  46. package/src/attested_core.ts +6 -3
  47. package/src/birth_meter.ts +1 -0
  48. package/src/birth_meter_ensure.ts +28 -2
  49. package/src/births.ts +1 -1
  50. package/src/build_package.ts +162 -538
  51. package/src/business_birth.ts +19 -3
  52. package/src/canonical_json.ts +21 -0
  53. package/src/check_package_main.ts +73 -33
  54. package/src/clear_directive.ts +19 -8
  55. package/src/codegen_dart.ts +305 -22
  56. package/src/codegen_dart_application.ts +844 -292
  57. package/src/codegen_dart_client.ts +1084 -407
  58. package/src/codegen_dart_scenario.ts +12 -12
  59. package/src/codegen_proof.ts +169 -39
  60. package/src/codegen_provider_dart.ts +17 -1
  61. package/src/codegen_ts.ts +298 -106
  62. package/src/compat_manifest.ts +2 -2
  63. package/src/compat_package_main.ts +8 -12
  64. package/src/compile_package_main.ts +1858 -590
  65. package/src/compose.ts +1 -1
  66. package/src/directive.ts +208 -15
  67. package/src/engine_entry.ts +165 -111
  68. package/src/evolve_preview.ts +29 -69
  69. package/src/extremum.ts +1 -1
  70. package/src/fields.ts +183 -4
  71. package/src/flow_gate.ts +75 -2
  72. package/src/framework/application_home.ts +2 -0
  73. package/src/framework/auth_provider_law.ts +2 -2
  74. package/src/framework/birth_meter.ts +7 -19
  75. package/src/framework/bootstrap.ts +0 -2
  76. package/src/framework/business_corrections.ts +9 -5
  77. package/src/framework/domain_lifecycle.ts +15 -29
  78. package/src/framework/encryption_scope.ts +2 -1
  79. package/src/framework/framework_aggregates.ts +2 -0
  80. package/src/framework/genesis.ts +16 -14
  81. package/src/framework/home.ts +20 -14
  82. package/src/framework/owned_custody.ts +9 -3
  83. package/src/framework/platform_admin.ts +3 -3
  84. package/src/framework/relations.ts +21 -13
  85. package/src/framework/signing_identity.ts +45 -40
  86. package/src/framework/workspace_invariant.ts +245 -0
  87. package/src/framework/workspaces.ts +23 -29
  88. package/src/index.ts +36 -16
  89. package/src/intent.ts +178 -6
  90. package/src/law_check.ts +32 -11
  91. package/src/law_graph.ts +2902 -0
  92. package/src/law_graph_usda.ts +1799 -0
  93. package/src/law_identity.ts +435 -0
  94. package/src/law_value_type.ts +230 -0
  95. package/src/lsp_server.ts +58 -29
  96. package/src/ops.ts +7 -0
  97. package/src/package_config.ts +141 -0
  98. package/src/package_dependencies.ts +108 -0
  99. package/src/package_dependency_usage.ts +150 -0
  100. package/src/package_source_imports.ts +71 -0
  101. package/src/plan_source_slice.ts +335 -0
  102. package/src/point_guard.ts +192 -0
  103. package/src/precondition.ts +44 -9
  104. package/src/read_projection_namespace.ts +1 -1
  105. package/src/runtime_zod.ts +44 -0
  106. package/src/secondary_ensure.ts +67 -14
  107. package/src/shape_compat.ts +1 -1
  108. package/src/stable_id_identity_bridge.ts +149 -0
  109. package/src/stable_id_lockfile.ts +140 -6
  110. package/src/stable_ids.ts +1 -1
  111. package/src/stable_ids_for_module.ts +348 -0
  112. package/src/stable_ids_types.ts +1 -1
  113. package/src/sum.ts +1 -1
  114. package/src/usd_state.ts +1 -1
  115. package/src/usda_relationship_integrity.ts +66 -0
  116. package/src/usda_text.ts +18 -0
  117. package/src/usdz.ts +78 -0
  118. package/src/value_object.ts +95 -0
  119. package/src/wire.ts +19 -0
  120. package/src/wire_encode.ts +22 -0
  121. package/src/workspace_sharding.ts +52 -36
  122. package/src/workspace_sharing.ts +28 -4
  123. package/src/zod_introspection.ts +1 -1
  124. package/dart/test/cluster_attested_test.dart +0 -201
  125. package/src/compile_engine.ts +0 -595
  126. package/src/manifest.ts +0 -1476
  127. package/src/read_projection_composition.ts +0 -117
  128. package/src/read_projection_manifest.ts +0 -115
  129. package/src/relation_composition.ts +0 -286
  130. package/src/usd.ts +0 -1967
  131. package/src/usd_layers.ts +0 -1776
@@ -70,8 +70,8 @@ export const GLOBAL_READ_PROJECTION_AGGREGATE_IDS: ReadonlySet<string> = new Set
70
70
  // disclosure
71
71
  "Disclosure",
72
72
  // domain lifecycle (the law records themselves)
73
- "PolicyBundle",
74
73
  "DomainInstallation",
74
+ "nomos_internal_CorrectionRecord_a91e73",
75
75
  "nomos_internal_ReleaseReceipt_6f0d3a",
76
76
  // encryption identity + scope (the operator-blind key machinery)
77
77
  "EncKey",
@@ -0,0 +1,44 @@
1
+ // Runtime-only Zod surface for compiled intent plans.
2
+ //
3
+ // The public DSL deliberately exposes Zod's full `z` namespace for authoring and type inference.
4
+ // Bundling that namespace, however, marks every Zod export live: all locales plus JSON-schema
5
+ // import/export machinery enter every exact plan. The plan slicer rewrites only the runtime `z`
6
+ // binding to this curated value; tenant source and TypeScript types continue to use the public API.
7
+
8
+ import {
9
+ ZodIssueCode,
10
+ array,
11
+ boolean,
12
+ discriminatedUnion,
13
+ enum as zEnum,
14
+ lazy,
15
+ literal,
16
+ null as zNull,
17
+ number,
18
+ object,
19
+ record,
20
+ string,
21
+ toJSONSchema,
22
+ union,
23
+ unknown,
24
+ url,
25
+ } from "zod/v4";
26
+
27
+ export const z = Object.freeze({
28
+ ZodIssueCode,
29
+ array,
30
+ boolean,
31
+ discriminatedUnion,
32
+ enum: zEnum,
33
+ lazy,
34
+ literal,
35
+ null: zNull,
36
+ number,
37
+ object,
38
+ record,
39
+ string,
40
+ toJSONSchema,
41
+ union,
42
+ unknown,
43
+ url,
44
+ });
@@ -4,7 +4,7 @@
4
4
  // QuickJS engine}. If a file isn't this / hosting this / authoring for this / proving this — it's gone.
5
5
 
6
6
  /**
7
- * SECONDARY-CREATE AUTO-DECLARE (the co2-dev id-mint-gate DX).
7
+ * SECONDARY-OUTPUT EFFECT CHECK + ENSURE SANCTION (the co2-dev id-mint-gate DX).
8
8
  *
9
9
  * A directive carries ONE referential marker for its PRIMARY aggregate (`.creates(A)` /
10
10
  * `.ensures(A)` / …). But a plan may ALSO introduce a SECOND aggregate at a caller-chosen
@@ -16,29 +16,77 @@
16
16
  * (an unsanctioned dev-chosen id can't be forged) but an opaque footgun for the author.
17
17
  *
18
18
  * This module closes the footgun WITHOUT weakening the gate: `composeDomainModule` PLAN-PROBES each
19
- * directive (the same probe machinery `codegen_proof.ts` uses), discovers the secondary types its
20
- * plan ensures, and — for any type NO directive in the domain already declares `.ensures()` for —
21
- * injects a NO-OP sanctioning `.ensures(Type)` directive (a {@link sanctioningEnsureDirective},
22
- * modelled on `nomosBirthRecordEnsure`). The manifest/USD then carries the `Ensure → Type`
23
- * declaration the id-mint gate reads, so the deterministic id is admitted. The synthesized directive
24
- * is DELIBERATELY USELESS TO DISPATCH (its plan writes nothing); its only load-bearing property is
25
- * its manifest presence.
19
+ * directive (the same probe machinery `codegen_proof.ts` uses) and first refuses any observed marked
20
+ * output absent from that directive's typed effect signature. An author must explicitly declare a
21
+ * secondary output with `.alsoEnsures(Type)` (or its marker-specific sibling). For a DECLARED secondary
22
+ * Ensure whose type has no primary Ensure declaration, it then injects a NO-OP sanctioning
23
+ * `.ensures(Type)` directive (a {@link sanctioningEnsureDirective}, modelled on
24
+ * `nomosBirthRecordEnsure`). The manifest/USD carries both the honest per-plan effect and the legacy
25
+ * id-mint sanction. The synthesized directive is deliberately useless to dispatch.
26
26
  *
27
27
  * ERA-SAFE: a domain whose plans do NO secondary ensures is returned untouched (byte-identical). A
28
28
  * domain that DOES gains the sanction — and such a domain could not have deployed before this without
29
29
  * the author hand-writing the declaration, so no already-shippable manifest hash silently drifts.
30
30
  *
31
- * BEST-EFFORT (graceful degradation): the probe runs each plan ONCE under a synthesized payload. A
32
- * plan that ensures CONDITIONALLY on a payload shape the synthesis didn't hit (or that can't be
33
- * probed) simply gets no auto-sanction — and the runtime id-mint gate refuses the ensure exactly as
34
- * before (no regression, no forged-id hole). Auto-declare only ever ADDS a sanction the author's own
35
- * plan already proves it intends; it never blocks a valid deploy.
31
+ * PROBE BOUNDARY: the probe runs each plan once under a synthesized lawful payload. Every observed
32
+ * mismatch is a compile error. A conditional branch not exercised by that sample still requires a
33
+ * future static effect analysis to prove exhaustively; the runtime id-mint gate remains fail-closed.
36
34
  */
37
35
  import { z } from "zod";
38
36
  import { directive } from "./directive.js";
39
37
  import type { Directive } from "./directive.js";
40
38
  import type { AggregateHandle } from "./aggregate.js";
41
- import { probeEnsuredTypes } from "./codegen_proof.js";
39
+ import { probeEnsuredTypes, probePlanOps } from "./codegen_proof.js";
40
+
41
+ const effectMethod = (marker: string): string => {
42
+ switch (marker) {
43
+ case "creates": return "alsoCreates";
44
+ case "ensures": return "alsoEnsures";
45
+ case "mutates": return "alsoMutates";
46
+ case "archives": return "alsoArchives";
47
+ default: return "alsoWrites";
48
+ }
49
+ };
50
+
51
+ /**
52
+ * Refuse a plan whose observable marked effects exceed its certified directive signature.
53
+ * TypeScript can prove that each individual field operation is lawful for its aggregate, but
54
+ * it cannot currently express "this function may write exactly these aggregate types". The
55
+ * compile probe closes that effect-system gap for every path it can execute: a secondary output
56
+ * must be named with `.alsoCreates/.alsoEnsures/.alsoMutates/.alsoArchives` on the directive that
57
+ * emits it. A domain-wide sanction is not a substitute for an honest per-plan signature.
58
+ */
59
+ export function assertPlanEffectsDeclared(
60
+ directive: Directive<unknown>,
61
+ domain: string,
62
+ ): void {
63
+ if (directive.manifestOnly === true || directive.birthsChild === true) return;
64
+ const probed = probePlanOps(directive, `${domain}/${directive.id}`);
65
+ if (probed === undefined) return;
66
+
67
+ const declared = new Set<string>([`${directive.marker}:${directive.aggregateId}`]);
68
+ for (const output of directive.secondaryOutputs ?? []) {
69
+ declared.add(`${output.marker}:${output.aggregateId}`);
70
+ }
71
+
72
+ for (const op of probed.ops) {
73
+ const marker = (op as { marker?: unknown }).marker;
74
+ const aggregateType = op.aggregateType;
75
+ if (typeof marker !== "string" || typeof aggregateType !== "string") continue;
76
+ if (declared.has(`${marker}:${aggregateType}`)) continue;
77
+ const method = effectMethod(marker);
78
+ const markerLabel = marker === "creates" ? "Create"
79
+ : marker === "ensures" ? "Ensure"
80
+ : marker === "mutates" ? "Mutate"
81
+ : marker === "archives" ? "Archive"
82
+ : marker;
83
+ throw new Error(
84
+ `[effect signature] ${domain}/${directive.id}: the plan emits ${markerLabel} -> ${aggregateType}, ` +
85
+ `but that output is absent from the directive's typed law. Declare ` +
86
+ `.${method}(${aggregateType}) on this directive.`,
87
+ );
88
+ }
89
+ }
42
90
 
43
91
  /** The synthesized directive id sanctioning `Ensure → <aggId>`. Derived purely from the aggregate id
44
92
  * so recompiles are stable; non-id chars are flattened so the id stays route-safe. */
@@ -61,6 +109,10 @@ export function sanctioningEnsureDirective(agg: AggregateHandle): Directive<unkn
61
109
  // tenant action and must not appear in generated clients, applications, or
62
110
  // OpenAPI, irrespective of whether the aggregate itself is framework-owned.
63
111
  (dir as { businessBirthInternal?: true }).businessBirthInternal = true;
112
+ // This no-op is a certified manifest statement, not an action. Keeping that
113
+ // distinction explicit prevents the plan router from advertising a runtime
114
+ // selector that no source module can register.
115
+ (dir as { manifestOnly?: true }).manifestOnly = true;
64
116
  // FRAMEWORK-SHARED DEDUP (Dart codegen): when the sanctioned aggregate is a FRAMEWORK singleton
65
117
  // (e.g. RelationTuple — auto-ensured into EVERY relation-using domain), this directive is injected
66
118
  // per-domain but its payload class (`NomosEnsure_<Agg>Payload`, zero-field) is byte-identical across
@@ -86,6 +138,7 @@ export function discoverUnsanctionedEnsures(
86
138
  directives: readonly Directive<unknown>[],
87
139
  domain: string,
88
140
  ): string[] {
141
+ for (const directive of directives) assertPlanEffectsDeclared(directive, domain);
89
142
  // Types some directive already DECLARES as its primary `.ensures(...)` target — already sanctioned.
90
143
  const declared = new Set<string>();
91
144
  for (const d of directives) {
@@ -126,7 +126,7 @@ function payloadFieldKind(spec: PayloadFieldSpec): string {
126
126
  return "json";
127
127
  case "wrapped":
128
128
  // typed-id (string wire) or unit (number wire) — the interface sees the WIRE kind.
129
- return spec.dartType.endsWith("Id") ? "string" : "double";
129
+ return spec.wrappedScalarKind === "string" ? "string" : "double";
130
130
  case "ref":
131
131
  // NomosRef<T> is aggregate-typed in the client and remains a string id on the wire.
132
132
  return "string";
@@ -0,0 +1,149 @@
1
+ import {
2
+ LAW_FRAMEWORK_PARENT,
3
+ createLawIdentityReconciler,
4
+ type LawIdentityRegistry,
5
+ } from "./law_identity.js";
6
+ import type { InferredRename } from "./stable_ids.js";
7
+ import type { StableAggregateId, StableFieldId, StableIds } from "./stable_ids_types.js";
8
+
9
+ export interface StableIdentityAggregateInput {
10
+ readonly id: string;
11
+ readonly fields: readonly string[];
12
+ readonly global?: boolean;
13
+ }
14
+
15
+ /**
16
+ * Compatibility bridge for W4.1. The existing shape-aware continuity pass proposes
17
+ * carries; the generalized registry owns the final SIDs and durable tombstones. Later
18
+ * W4 work replaces those proposals with typed-graph/previous-USDA evidence.
19
+ */
20
+ export function reconcileStableIdsWithIdentity(
21
+ source: LawIdentityRegistry,
22
+ domainName: string,
23
+ currentAggregates: readonly StableIdentityAggregateInput[],
24
+ proposed: StableIds,
25
+ inferred: readonly InferredRename[],
26
+ ): { registry: LawIdentityRegistry; continuity: StableIds } {
27
+ const identities = createLawIdentityReconciler(source);
28
+ let result = identities.reconcile({
29
+ kind: "domain",
30
+ labels: [domainName],
31
+ preferredSids: { [domainName]: proposed.domain },
32
+ // One config is only a slice of the project-wide domain namespace.
33
+ retireAbsent: false,
34
+ });
35
+ let registry = result.registry;
36
+ const domainKey = result.resolved[domainName]!;
37
+ const aggregateKeys: Record<string, string> = {};
38
+
39
+ const local = currentAggregates.filter((aggregate) => aggregate.global !== true);
40
+ const localResult = identities.reconcile({
41
+ kind: "aggregate",
42
+ parent: domainKey,
43
+ labels: local.map((aggregate) => aggregate.id),
44
+ carries: aggregateCarries(inferred, new Set(local.map((aggregate) => aggregate.id))),
45
+ preferredSids: preferredAggregateSids(local, proposed),
46
+ });
47
+ registry = localResult.registry;
48
+ Object.assign(aggregateKeys, localResult.resolved);
49
+
50
+ const framework = currentAggregates.filter((aggregate) => aggregate.global === true);
51
+ if (framework.length > 0) {
52
+ const frameworkResult = identities.reconcile({
53
+ kind: "aggregate",
54
+ parent: LAW_FRAMEWORK_PARENT,
55
+ labels: framework.map((aggregate) => aggregate.id),
56
+ preferredSids: preferredAggregateSids(framework, proposed),
57
+ // Framework declarations are shared by independently compiled packages.
58
+ retireAbsent: false,
59
+ });
60
+ registry = frameworkResult.registry;
61
+ Object.assign(aggregateKeys, frameworkResult.resolved);
62
+ }
63
+
64
+ const aggregateInputs = new Map(currentAggregates.map((aggregate) => [aggregate.id, aggregate]));
65
+ const aggregates: Record<string, StableAggregateId> = {};
66
+ for (const aggregateName of Object.keys(proposed.aggregates).sort(compareText)) {
67
+ const proposedAggregate = proposed.aggregates[aggregateName]!;
68
+ const aggregateInput = aggregateInputs.get(aggregateName);
69
+ if (aggregateInput === undefined) {
70
+ throw new Error(`NMS-ID-006 proposed aggregate '${aggregateName}' was not discovered`);
71
+ }
72
+ const aggregateKey = aggregateKeys[aggregateName];
73
+ if (aggregateKey === undefined) {
74
+ throw new Error(`NMS-ID-006 aggregate '${aggregateName}' has no reconciled identity`);
75
+ }
76
+ const fieldResult = identities.reconcile({
77
+ kind: "field",
78
+ parent: aggregateKey,
79
+ labels: aggregateInput.fields,
80
+ carries: fieldCarries(inferred, aggregateName),
81
+ preferredSids: Object.fromEntries(
82
+ Object.entries(proposedAggregate.fields).map(([label, field]) => [label, field.sid]),
83
+ ),
84
+ });
85
+ registry = fieldResult.registry;
86
+
87
+ const fields: Record<string, StableFieldId> = {};
88
+ for (const [fieldName, proposedField] of Object.entries(proposedAggregate.fields).sort(
89
+ ([a], [b]) => compareText(a, b),
90
+ )) {
91
+ const key = fieldResult.resolved[fieldName];
92
+ if (key === undefined) throw new Error(`NMS-ID-006 field '${aggregateName}.${fieldName}' has no identity`);
93
+ const identity = registry.entities[key]!;
94
+ fields[fieldName] = {
95
+ ...proposedField,
96
+ sid: identity.sid,
97
+ ...(identity.priorLabels === undefined ? {} : { was: [...identity.priorLabels] }),
98
+ };
99
+ }
100
+ const identity = registry.entities[aggregateKey]!;
101
+ aggregates[aggregateName] = {
102
+ sid: identity.sid,
103
+ ...(identity.priorLabels === undefined ? {} : { was: [...identity.priorLabels] }),
104
+ fields,
105
+ };
106
+ }
107
+
108
+ const finalRegistry = identities.finish();
109
+ return {
110
+ registry: finalRegistry,
111
+ continuity: {
112
+ v: 1,
113
+ domain: finalRegistry.entities[domainKey]!.sid,
114
+ aggregates,
115
+ },
116
+ };
117
+ }
118
+
119
+ function aggregateCarries(
120
+ inferred: readonly InferredRename[],
121
+ currentLocalLabels: ReadonlySet<string>,
122
+ ): { from: string; to: string }[] {
123
+ return inferred
124
+ .filter((rename) => !rename.what.includes(".") && currentLocalLabels.has(rename.to))
125
+ .map(({ from, to }) => ({ from, to }));
126
+ }
127
+
128
+ function fieldCarries(
129
+ inferred: readonly InferredRename[],
130
+ aggregateName: string,
131
+ ): { from: string; to: string }[] {
132
+ const prefix = `${aggregateName}.`;
133
+ return inferred
134
+ .filter((rename) => rename.what.startsWith(prefix))
135
+ .map(({ from, to }) => ({ from, to }));
136
+ }
137
+
138
+ function preferredAggregateSids(
139
+ aggregates: readonly StableIdentityAggregateInput[],
140
+ proposed: StableIds,
141
+ ): Record<string, string> {
142
+ return Object.fromEntries(
143
+ aggregates.map((aggregate) => [aggregate.id, proposed.aggregates[aggregate.id]!.sid]),
144
+ );
145
+ }
146
+
147
+ function compareText(a: string, b: string): number {
148
+ return a < b ? -1 : a > b ? 1 : 0;
149
+ }
@@ -1,5 +1,14 @@
1
1
  import type { StableAggregateId, StableFieldId, StableIds } from "./stable_ids_types.js";
2
2
  import type { WireSchema } from "./wire.js";
3
+ import { mintGlobalAggregateSid } from "./stable_ids.js";
4
+ import {
5
+ LAW_FRAMEWORK_PARENT,
6
+ emptyLawIdentityRegistry,
7
+ lawEntityKey,
8
+ validateLawIdentityRegistry,
9
+ type LawIdentityEntity,
10
+ type LawIdentityRegistry,
11
+ } from "./law_identity.js";
3
12
 
4
13
  export interface StableIdLockDomain {
5
14
  stableIds: StableIds;
@@ -10,6 +19,8 @@ export interface StableIdLockDomain {
10
19
  export interface StableIdLockfile {
11
20
  v: 1;
12
21
  domains: Record<string, StableIdLockDomain>;
22
+ /** W4 compiler identity custody. Semantics remain exclusively in USDA. */
23
+ identity?: LawIdentityRegistry;
13
24
  }
14
25
 
15
26
  type CompactStableFieldId = Omit<StableFieldId, "contract" | "shape"> & {
@@ -31,6 +42,15 @@ interface CompactStableIdLockfile {
31
42
  domains: Record<string, { stableIds: CompactStableIds }>;
32
43
  }
33
44
 
45
+ interface IdentityStableIdLockfile {
46
+ v: 3;
47
+ identity: LawIdentityRegistry;
48
+ /** Temporary W4 compatibility evidence; deleted once evolution traverses prior USDA. */
49
+ contracts: readonly (readonly [string, unknown])[];
50
+ /** Temporary aggregate/field compatibility view consumed by the v2 compiler path. */
51
+ domains: Record<string, { stableIds: CompactStableIds }>;
52
+ }
53
+
34
54
  function isRecord(value: unknown): value is Record<string, unknown> {
35
55
  return typeof value === "object" && value !== null && !Array.isArray(value);
36
56
  }
@@ -53,11 +73,22 @@ function sortedJson(value: unknown): unknown {
53
73
  export function parseStableIdLockfile(value: unknown): StableIdLockfile {
54
74
  if (!isRecord(value)) throw new Error("stable-id lock must be a JSON object");
55
75
  if (value.v === 1) return value as unknown as StableIdLockfile;
56
- if (value.v !== 2 || !Array.isArray(value.contracts) || !isRecord(value.domains)) {
76
+ if (
77
+ (value.v !== 2 && value.v !== 3) ||
78
+ !Array.isArray(value.contracts) ||
79
+ !isRecord(value.domains)
80
+ ) {
57
81
  throw new Error("unsupported stable-id lock encoding");
58
82
  }
59
83
 
60
- const compact = value as unknown as CompactStableIdLockfile;
84
+ let identity: LawIdentityRegistry | undefined;
85
+ if (value.v === 3) {
86
+ if (!isRecord(value.identity)) throw new Error("stable-id lock v3 identity is malformed");
87
+ identity = value.identity as unknown as LawIdentityRegistry;
88
+ validateLawIdentityRegistry(identity);
89
+ }
90
+
91
+ const compact = value as unknown as CompactStableIdLockfile | IdentityStableIdLockfile;
61
92
  const contracts = compact.contracts.map((entry, index) => {
62
93
  if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== "string") {
63
94
  throw new Error(`stable-id lock contract ${index} is malformed`);
@@ -98,7 +129,7 @@ export function parseStableIdLockfile(value: unknown): StableIdLockfile {
98
129
  };
99
130
  }
100
131
 
101
- return { v: 1, domains };
132
+ return { v: 1, domains, ...(identity === undefined ? {} : { identity }) };
102
133
  }
103
134
 
104
135
  /**
@@ -107,6 +138,7 @@ export function parseStableIdLockfile(value: unknown): StableIdLockfile {
107
138
  * the contract table retains the full descriptor needed to prove safe widening.
108
139
  */
109
140
  export function serializeStableIdLockfile(lock: StableIdLockfile): string {
141
+ if (lock.identity !== undefined) validateLawIdentityRegistry(lock.identity);
110
142
  const contracts: [string, unknown][] = [];
111
143
  const contractIndexes = new Map<string, number>();
112
144
  const domains: CompactStableIdLockfile["domains"] = {};
@@ -138,7 +170,109 @@ export function serializeStableIdLockfile(lock: StableIdLockfile): string {
138
170
 
139
171
  // This is a machine-owned lockfile. Compact JSON is intentional: unlike generated
140
172
  // law and clients, it must be committed so identity survives a clean clone.
141
- return JSON.stringify(
142
- sortedJson({ v: 2, contracts, domains } satisfies CompactStableIdLockfile),
143
- ) + "\n";
173
+ const encoded =
174
+ lock.identity === undefined
175
+ ? ({ v: 2, contracts, domains } satisfies CompactStableIdLockfile)
176
+ : ({ v: 3, identity: lock.identity, contracts, domains } satisfies IdentityStableIdLockfile);
177
+ return JSON.stringify(sortedJson(encoded)) + "\n";
178
+ }
179
+
180
+ /**
181
+ * One-time W4 import of the existing domain/aggregate/field SIDs. The legacy view has
182
+ * already forgotten removals, so only currently present entities can be imported;
183
+ * from this point onward the returned registry retains every retirement.
184
+ */
185
+ export function identityRegistryFromStableIdLockfile(
186
+ lock: StableIdLockfile | undefined,
187
+ ): LawIdentityRegistry {
188
+ if (lock?.identity !== undefined) {
189
+ validateLawIdentityRegistry(lock.identity);
190
+ return lock.identity;
191
+ }
192
+ if (lock === undefined) return emptyLawIdentityRegistry();
193
+
194
+ const entities: Record<string, LawIdentityEntity> = {};
195
+ const aggregateOccurrences = new Map<string, number>();
196
+ for (const domain of Object.values(lock.domains)) {
197
+ for (const aggregate of Object.values(domain.stableIds.aggregates)) {
198
+ aggregateOccurrences.set(aggregate.sid, (aggregateOccurrences.get(aggregate.sid) ?? 0) + 1);
199
+ }
200
+ }
201
+
202
+ for (const [domainName, domain] of Object.entries(lock.domains).sort(([a], [b]) =>
203
+ compareText(a, b),
204
+ )) {
205
+ const domainKey = importIdentityEntity(entities, {
206
+ kind: "domain",
207
+ sid: domain.stableIds.domain,
208
+ label: domainName,
209
+ generation: 0,
210
+ status: "active",
211
+ });
212
+ for (const [aggregateName, aggregate] of Object.entries(domain.stableIds.aggregates).sort(
213
+ ([a], [b]) => compareText(a, b),
214
+ )) {
215
+ const parent =
216
+ (aggregateOccurrences.get(aggregate.sid) ?? 0) > 1 ||
217
+ aggregate.sid === mintGlobalAggregateSid(aggregateName)
218
+ ? LAW_FRAMEWORK_PARENT
219
+ : domainKey;
220
+ const aggregateKey = importIdentityEntity(entities, {
221
+ kind: "aggregate",
222
+ sid: aggregate.sid,
223
+ parent,
224
+ label: aggregateName,
225
+ ...(aggregate.was === undefined ? {} : { priorLabels: aggregate.was }),
226
+ generation: 0,
227
+ status: "active",
228
+ });
229
+ for (const [fieldName, field] of Object.entries(aggregate.fields).sort(([a], [b]) =>
230
+ compareText(a, b),
231
+ )) {
232
+ importIdentityEntity(entities, {
233
+ kind: "field",
234
+ sid: field.sid,
235
+ parent: aggregateKey,
236
+ label: fieldName,
237
+ ...(field.was === undefined ? {} : { priorLabels: field.was }),
238
+ generation: 0,
239
+ status: "active",
240
+ });
241
+ }
242
+ }
243
+ }
244
+
245
+ const registry: LawIdentityRegistry = {
246
+ v: 1,
247
+ entities: Object.fromEntries(Object.entries(entities).sort(([a], [b]) => compareText(a, b))),
248
+ };
249
+ validateLawIdentityRegistry(registry);
250
+ return registry;
251
+ }
252
+
253
+ function importIdentityEntity(
254
+ entities: Record<string, LawIdentityEntity>,
255
+ entity: LawIdentityEntity,
256
+ ): string {
257
+ const key = lawEntityKey(entity.kind, entity.sid);
258
+ const prior = entities[key];
259
+ if (prior === undefined) {
260
+ entities[key] = entity;
261
+ return key;
262
+ }
263
+ if (
264
+ prior.kind !== entity.kind ||
265
+ prior.parent !== entity.parent ||
266
+ prior.label !== entity.label ||
267
+ JSON.stringify(prior.priorLabels ?? []) !== JSON.stringify(entity.priorLabels ?? [])
268
+ ) {
269
+ throw new Error(
270
+ `NMS-ID-002 legacy SID ${key} aliases incompatible ${prior.kind} '${prior.label}' and ${entity.kind} '${entity.label}'`,
271
+ );
272
+ }
273
+ return key;
274
+ }
275
+
276
+ function compareText(a: string, b: string): number {
277
+ return a < b ? -1 : a > b ? 1 : 0;
144
278
  }
package/src/stable_ids.ts CHANGED
@@ -46,7 +46,7 @@
46
46
  * projection reads it to fold old-era field/type labels into the current ones.
47
47
  *
48
48
  * BUILD-TIME ONLY (imports `node:crypto`): reached via `@githolon/dsl/stable-ids` /
49
- * `@githolon/dsl/manifest`, never the runtime barrel. The pure TYPE surface lives in
49
+ * `stable_ids_for_module.ts`, never the runtime barrel. The pure TYPE surface lives in
50
50
  * `stable_ids_types.ts` so the runtime type graph stays node-free.
51
51
  */
52
52
  import { createHash } from "node:crypto";