@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.
Files changed (103) hide show
  1. package/assets/operation-contract.wasm +0 -0
  2. package/compile_package.mjs +23 -3
  3. package/compile_proof.d.mts +7 -0
  4. package/compile_proof.mjs +38 -3
  5. package/dart/CHANGELOG.md +60 -0
  6. package/dart/lib/src/application_framework_calls.dart +16 -6
  7. package/dart/lib/src/console_enrolment.dart +7 -0
  8. package/dart/lib/src/console_http_io.dart +22 -15
  9. package/dart/lib/src/dev_cloud.dart +62 -0
  10. package/dart/lib/src/generated_call.dart +14 -3
  11. package/dart/lib/src/nms_generated_runtime.dart +7 -0
  12. package/dart/lib/src/node_runner_transport.dart +77 -8
  13. package/dart/lib/src/nomos_trace.dart +107 -0
  14. package/dart/lib/src/operation_receipts.dart +74 -7
  15. package/dart/lib/src/reception/dart_reception.dart +174 -17
  16. package/dart/lib/src/rows_watch.dart +390 -0
  17. package/dart/lib/src/runner/node-entry.mjs +7678 -2860
  18. package/dart/lib/src/runtime_v2_transport.dart +3 -0
  19. package/dart/lib/src/sync_status_projection.dart +94 -1
  20. package/dart/lib/src/sync_status_reducer.dart +5 -2
  21. package/dart/lib/src/test_harness.dart +107 -41
  22. package/dart/lib/src/testing/test_peer.dart +107 -99
  23. package/dart/lib/src/testing/test_peer_host.dart +305 -0
  24. package/dart/lib/src/typed_watch_registry.dart +26 -3
  25. package/dart/pubspec.lock +1 -1
  26. package/dart/pubspec.yaml +2 -2
  27. package/dart/test/background_model_adoption_test.dart +1 -0
  28. package/dart/test/console_enrolment_test.dart +47 -0
  29. package/dart/test/custodian_warming_test.dart +105 -0
  30. package/dart/test/fixtures/offer_watch_order_peer.dart +74 -0
  31. package/dart/test/kernel_author_after_read_test.dart +6 -3
  32. package/dart/test/kernel_fence_test.dart +4 -1
  33. package/dart/test/model_adoption_test.dart +66 -0
  34. package/dart/test/nomos_trace_test.dart +91 -0
  35. package/dart/test/own_offer_watch_order_test.dart +90 -0
  36. package/dart/test/receiving_group_open_test.dart +4 -8
  37. package/dart/test/replica_readiness_test.dart +14 -4
  38. package/dart/test/rows_watch_test.dart +331 -0
  39. package/dart/test/runtime_projections_test.dart +41 -0
  40. package/dart/test/test_peer_host_selection_test.dart +49 -0
  41. package/dart_types/CHANGELOG.md +39 -0
  42. package/dart_types/lib/src/approved_model.dart +100 -6
  43. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pb.dart +1063 -3
  44. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbenum.dart +89 -0
  45. package/dart_types/lib/src/proto/nomos/runtime/v2/application_desk.pb.dart +14 -0
  46. package/dart_types/lib/src/proto/nomos/runtime/v2/correspondence_desk.pb.dart +2272 -130
  47. package/dart_types/lib/src/proto/nomos/runtime/v2/correspondence_desk.pbenum.dart +65 -0
  48. package/dart_types/lib/src/proto/nomos/runtime/v2/model_adoption_desk.pb.dart +118 -2
  49. package/dart_types/lib/src/proto/nomos/runtime/v2/observability_desk.pb.dart +278 -9
  50. package/dart_types/lib/src/proto/nomos/runtime/v2/receiving_group_desk.pb.dart +7 -85
  51. package/dart_types/lib/src/proto/nomos/runtime/v2/records_desk.pb.dart +16 -0
  52. package/dart_types/lib/src/proto/nomos/runtime/v2/residency_desk.pb.dart +35 -0
  53. package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pb.dart +43 -2
  54. package/dart_types/pubspec.yaml +1 -1
  55. package/package.json +6 -2
  56. package/src/aggregate.ts +93 -0
  57. package/src/anonymous_value_interning.ts +205 -0
  58. package/src/baseline.ts +27 -0
  59. package/src/baseline_export.ts +432 -0
  60. package/src/baseline_import.ts +312 -0
  61. package/src/baseline_usda.ts +186 -0
  62. package/src/birth_key_rule.ts +472 -0
  63. package/src/births.ts +207 -24
  64. package/src/build_provenance.ts +17 -2
  65. package/src/business_birth.ts +50 -4
  66. package/src/check_package_main.ts +50 -6
  67. package/src/codegen_call_proto.ts +5 -18
  68. package/src/codegen_dart.ts +95 -3
  69. package/src/codegen_dart_client.ts +73 -15
  70. package/src/codegen_dart_facade.ts +58 -35
  71. package/src/codegen_dart_framework_calls.ts +32 -10
  72. package/src/codegen_dart_scenario.ts +7 -0
  73. package/src/codegen_proof.ts +5 -2
  74. package/src/codegen_result_proto.ts +8 -6
  75. package/src/codegen_result_usda.ts +4 -0
  76. package/src/codegen_ts.ts +135 -34
  77. package/src/compatibility_graph.ts +41 -4
  78. package/src/compatibility_usda.ts +11 -1
  79. package/src/compile_package_main.ts +272 -105
  80. package/src/composed_wire_identity.ts +24 -0
  81. package/src/directive.ts +6 -0
  82. package/src/domain_requirements.ts +75 -0
  83. package/src/engine_entry.ts +4 -48
  84. package/src/engine_javascript.ts +12 -6
  85. package/src/fields.ts +109 -0
  86. package/src/identity_usda.ts +77 -51
  87. package/src/index.ts +25 -1
  88. package/src/intent.ts +5 -0
  89. package/src/law_graph.ts +142 -1
  90. package/src/law_graph_usda.ts +25 -141
  91. package/src/law_layer_prims.ts +199 -0
  92. package/src/law_unit_selection.ts +45 -0
  93. package/src/law_unit_set_identity.ts +44 -0
  94. package/src/law_version.ts +107 -0
  95. package/src/package_config.ts +44 -0
  96. package/src/package_dependencies.ts +8 -0
  97. package/src/query/compile.ts +0 -0
  98. package/src/release_inspection.ts +24 -0
  99. package/src/release_publication.ts +298 -0
  100. package/src/value_object.ts +6 -3
  101. package/src/wire.ts +2 -0
  102. package/src/wire_paths.ts +17 -0
  103. package/src/workspace_routing.ts +1 -1
@@ -0,0 +1,312 @@
1
+ // NOMOS — IMPORT A PINNED BASELINE INTO A COMPILE (architecture/domain_release_lifecycle.md, step 1).
2
+ //
3
+ // The ledger (`nomos.identity.usda`) is the compiler's continuity; the baseline (baseline_usda.ts) is
4
+ // what released custody proves. A publishable compile starts from their agreement:
5
+ //
6
+ // * an identity the release carries that the ledger lacks is IMPORTED — a clean checkout, or a ledger
7
+ // that never saw the release, still allocates the released sid and wire key, never a fresh one;
8
+ // * an identity the ledger carries under the same key keeps its released label lineage and status,
9
+ // or the compile REFUSES: a branch that allocated its own sid for a label the release allocated
10
+ // differently, or retired what the release serves, cannot publish over it by accident;
11
+ // * an operation field's released wire key must be the ledger's canonical key, with every released
12
+ // alias retained, or the compile REFUSES: generated clients would otherwise encode keys installed
13
+ // law does not decode (the 1 October 2026 incident).
14
+ //
15
+ // Refusals are typed (`BaselineRefusal.code`) so `githolon check` and `githolon compile` name the
16
+ // same reason, and a release lane can gate on the code rather than the prose.
17
+ import {
18
+ LAW_FRAMEWORK_PARENT,
19
+ createLawIdentityReconciler,
20
+ validateLawIdentityRegistry,
21
+ type LawIdentityEntity,
22
+ type LawIdentityRegistry,
23
+ } from "./law_identity.js";
24
+ import { NOMOS_BASELINE_FILENAME, baselineSha256, type ReleasedBaseline } from "./baseline_usda.js";
25
+
26
+ export type BaselineRefusalCode =
27
+ /** A publishable compile names no baseline. */
28
+ | "baseline-required"
29
+ /** The pinned artifact's bytes are not the pinned content address. */
30
+ | "baseline-pin-mismatch"
31
+ /** The artifact is not a readable baseline. */
32
+ | "baseline-unreadable"
33
+ /** The baseline carries none of this package's domains: it was exported from another package. */
34
+ | "baseline-wrong-package"
35
+ /** The ledger and the release disagree about which sid a declaration owns, or what it is called. */
36
+ | "baseline-identity-divergence"
37
+ /** The release retired a declaration the ledger still serves. */
38
+ | "baseline-retirement-divergence"
39
+ /** The ledger's canonical wire key or aliases for a released operation field differ. */
40
+ | "baseline-wire-divergence"
41
+ /** The candidate re-allocated a released identity or wire key after import. */
42
+ | "baseline-reallocation";
43
+
44
+ export class BaselineRefusal extends Error {
45
+ readonly code: BaselineRefusalCode;
46
+ /** One line per disagreement, each naming the coordinate, the released value and the local value. */
47
+ readonly details: readonly string[];
48
+ constructor(code: BaselineRefusalCode, message: string, details: readonly string[] = []) {
49
+ super(details.length === 0 ? message : `${message}\n${details.map((detail) => ` - ${detail}`).join("\n")}`);
50
+ this.code = code;
51
+ this.details = details;
52
+ }
53
+ }
54
+
55
+ export interface BaselinePin {
56
+ /** Path of the artifact, relative to `nomos.project.ts`. */
57
+ readonly artifact: string;
58
+ /** Its content address (baseline_usda.ts `baselineSha256`). */
59
+ readonly sha256: string;
60
+ }
61
+
62
+ /** The pin must name the exact bytes: a baseline edited by hand is not the one that was exported. */
63
+ export function verifyBaselinePin(text: string, pin: BaselinePin): void {
64
+ const actual = baselineSha256(text);
65
+ if (actual !== pin.sha256) {
66
+ throw new BaselineRefusal(
67
+ "baseline-pin-mismatch",
68
+ `${pin.artifact} is ${actual}, not the pinned ${pin.sha256}: re-export the baseline from the release you intend to extend and pin its sha256`,
69
+ );
70
+ }
71
+ }
72
+
73
+ export interface BaselineImport {
74
+ readonly registry: LawIdentityRegistry;
75
+ /** Registry keys the baseline added (absent from the ledger). */
76
+ readonly imported: readonly string[];
77
+ /** Registry keys whose released label lineage the ledger was behind on and now carries. */
78
+ readonly relabelled: readonly string[];
79
+ /** Operation-field coordinates whose released allocation the ledger now carries. */
80
+ readonly allocated: readonly string[];
81
+ /** Entities and allocations the ledger already agreed with. */
82
+ readonly verified: number;
83
+ }
84
+
85
+ const compareText = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0);
86
+ const coordinateOf = (entity: Pick<LawIdentityEntity, "kind" | "parent" | "label">): string =>
87
+ `${entity.kind} '${entity.label}' in ${entity.parent ?? "@root"}`;
88
+
89
+ function activeAt(
90
+ entities: Readonly<Record<string, LawIdentityEntity>>,
91
+ kind: LawIdentityEntity["kind"],
92
+ parent: string | undefined,
93
+ labels: readonly string[],
94
+ ): [string, LawIdentityEntity] | undefined {
95
+ return Object.entries(entities).find(([, entity]) =>
96
+ entity.kind === kind && entity.parent === parent && entity.status === "active" &&
97
+ (labels.includes(entity.label) || entity.priorLabels?.some((prior) => labels.includes(prior)) === true));
98
+ }
99
+
100
+ /** Parents first, so every imported child finds its parent already in the registry. */
101
+ function orderByParent(entities: Readonly<Record<string, LawIdentityEntity>>): [string, LawIdentityEntity][] {
102
+ const ordered: [string, LawIdentityEntity][] = [];
103
+ const placed = new Set<string>();
104
+ const place = (key: string, trail: ReadonlySet<string>): void => {
105
+ if (placed.has(key)) return;
106
+ const entity = entities[key];
107
+ if (entity === undefined) return;
108
+ const parent = entity.parent;
109
+ if (parent !== undefined && parent !== LAW_FRAMEWORK_PARENT && !trail.has(key)) place(parent, new Set(trail).add(key));
110
+ placed.add(key);
111
+ ordered.push([key, entity]);
112
+ };
113
+ for (const key of Object.keys(entities).sort(compareText)) place(key, new Set());
114
+ return ordered;
115
+ }
116
+
117
+ /**
118
+ * Verify the ledger against the baseline and return the registry a compile starts from. Throws a
119
+ * typed {@link BaselineRefusal} on the first class of disagreement found, listing every instance.
120
+ * `packageDomains` are the domain keys this compile declares (`nomos.project.ts`).
121
+ */
122
+ export function importBaseline(
123
+ ledger: LawIdentityRegistry,
124
+ baseline: ReleasedBaseline,
125
+ packageDomains: readonly string[],
126
+ ): BaselineImport {
127
+ validateLawIdentityRegistry(ledger);
128
+ const released = baseline.continuity.identity?.entities ?? {};
129
+
130
+ const releasedDomains = Object.values(released).filter((entity) => entity.kind === "domain");
131
+ const servesPackage = releasedDomains.some((domain) =>
132
+ packageDomains.includes(domain.label) || domain.priorLabels?.some((prior) => packageDomains.includes(prior)) === true);
133
+ if (!servesPackage) {
134
+ throw new BaselineRefusal(
135
+ "baseline-wrong-package",
136
+ `${NOMOS_BASELINE_FILENAME} (release ${baseline.release}, law ${baseline.unitSet}) carries domain(s) ` +
137
+ `${releasedDomains.map((domain) => `'${domain.label}'`).join(", ") || "none"}, not this package's ` +
138
+ `${packageDomains.map((domain) => `'${domain}'`).join(", ")}: pin the baseline exported from this package's release`,
139
+ );
140
+ }
141
+
142
+ const entities: Record<string, LawIdentityEntity> = Object.fromEntries(
143
+ Object.entries(ledger.entities).map(([key, entity]) => [key, { ...entity }]),
144
+ );
145
+ const imported: string[] = [];
146
+ const relabelled: string[] = [];
147
+ const identityDivergence: string[] = [];
148
+ const retirementDivergence: string[] = [];
149
+ let verified = 0;
150
+
151
+ for (const [key, releasedEntity] of orderByParent(released)) {
152
+ const lineage = [releasedEntity.label, ...(releasedEntity.priorLabels ?? [])];
153
+ const local = entities[key];
154
+ if (local !== undefined) {
155
+ if (local.kind !== releasedEntity.kind) {
156
+ identityDivergence.push(`${key}: released as ${releasedEntity.kind}, ledgered as ${local.kind}`);
157
+ continue;
158
+ }
159
+ if (releasedEntity.status === "retired" && local.status === "active") {
160
+ retirementDivergence.push(`${key} (${coordinateOf(releasedEntity)}): retired by the release, active in the ledger`);
161
+ continue;
162
+ }
163
+ if (local.label === releasedEntity.label || local.priorLabels?.includes(releasedEntity.label) === true) {
164
+ verified += 1;
165
+ continue;
166
+ }
167
+ if (releasedEntity.priorLabels?.includes(local.label) === true) {
168
+ // The ledger predates a released rename: carry the released lineage so the compile resolves
169
+ // the current label to this sid instead of minting a successor.
170
+ const priorLabels = [...new Set([...(local.priorLabels ?? []), ...(releasedEntity.priorLabels ?? [])])]
171
+ .filter((prior) => prior !== releasedEntity.label);
172
+ entities[key] = { ...local, label: releasedEntity.label, ...(priorLabels.length > 0 ? { priorLabels } : {}) };
173
+ relabelled.push(key);
174
+ continue;
175
+ }
176
+ identityDivergence.push(
177
+ `${key}: released as '${releasedEntity.label}'${releasedEntity.priorLabels ? ` (was ${releasedEntity.priorLabels.join(", ")})` : ""}, ` +
178
+ `ledgered as '${local.label}'${local.priorLabels ? ` (was ${local.priorLabels.join(", ")})` : ""}`,
179
+ );
180
+ continue;
181
+ }
182
+ // Absent from the ledger. The same coordinate owned by another sid locally is the branch-divergence
183
+ // case: this checkout allocated its own identity for a label the release allocated differently.
184
+ const occupied = activeAt(entities, releasedEntity.kind, releasedEntity.parent, lineage);
185
+ if (occupied !== undefined && releasedEntity.status === "active") {
186
+ identityDivergence.push(
187
+ `${coordinateOf(releasedEntity)}: the release carries ${key}, this ledger allocated ${occupied[0]}`,
188
+ );
189
+ continue;
190
+ }
191
+ const parent = releasedEntity.parent;
192
+ if (parent !== undefined && parent !== LAW_FRAMEWORK_PARENT && entities[parent] === undefined) {
193
+ // Its parent diverged above; that refusal names the cause.
194
+ continue;
195
+ }
196
+ if (parent !== undefined && parent !== LAW_FRAMEWORK_PARENT && entities[parent]!.status !== "active" && releasedEntity.status === "active") {
197
+ // The ledger retired the parent after the release: the candidate is ahead, not divergent. The
198
+ // evolution gate judges that retirement; nothing to import beneath a tombstone.
199
+ continue;
200
+ }
201
+ entities[key] = { ...releasedEntity };
202
+ imported.push(key);
203
+ }
204
+
205
+ if (identityDivergence.length > 0) {
206
+ throw new BaselineRefusal(
207
+ "baseline-identity-divergence",
208
+ `the identity ledger disagrees with release ${baseline.release} (law ${baseline.unitSet}) about ${identityDivergence.length} ` +
209
+ "declaration(s); reconcile the ledger with the branch that released them, or pin the baseline of the release this branch extends",
210
+ identityDivergence,
211
+ );
212
+ }
213
+ if (retirementDivergence.length > 0) {
214
+ throw new BaselineRefusal(
215
+ "baseline-retirement-divergence",
216
+ `release ${baseline.release} retired ${retirementDivergence.length} declaration(s) this ledger still serves; ` +
217
+ "a retired identity cannot be revived — declare the retirement in the ledger's branch or pin the baseline this branch extends",
218
+ retirementDivergence,
219
+ );
220
+ }
221
+
222
+ // Wire allocations: by ledger coordinate (intent, field label), never by the `u_` key — the ledger's
223
+ // generation for an operation field is not recoverable from released law.
224
+ const wireDivergence: string[] = [];
225
+ const allocated: string[] = [];
226
+ const reconciler = createLawIdentityReconciler({ v: 1, entities });
227
+ let registry: LawIdentityRegistry = { v: 1, entities };
228
+ for (const allocation of [...baseline.wireAllocations].sort((l, r) => compareText(l.intent, r.intent) || compareText(l.field, r.field))) {
229
+ const intent = registry.entities[allocation.intent];
230
+ if (intent === undefined || intent.status !== "active") continue; // the intent diverged or was retired: judged above / by evolution
231
+ const local = activeAt(registry.entities, "operation-field", allocation.intent, [allocation.field]);
232
+ if (local !== undefined) {
233
+ const wire = local[1].operationFieldWire?.protobuf;
234
+ if (wire === undefined) {
235
+ wireDivergence.push(`${intent.label}.${allocation.field}: released on ${allocation.protobuf.canonical}, ledgered without a wire allocation`);
236
+ continue;
237
+ }
238
+ if (wire.canonical !== allocation.protobuf.canonical) {
239
+ wireDivergence.push(`${intent.label}.${allocation.field}: released on ${allocation.protobuf.canonical}, ledgered on ${wire.canonical}`);
240
+ continue;
241
+ }
242
+ const missingAliases = (allocation.protobuf.aliases ?? []).filter((alias) => !(wire.aliases ?? []).includes(alias));
243
+ if (missingAliases.length > 0) {
244
+ wireDivergence.push(`${intent.label}.${allocation.field}: released alias(es) ${missingAliases.join(", ")} are not retained by the ledger`);
245
+ continue;
246
+ }
247
+ verified += 1;
248
+ continue;
249
+ }
250
+ const retired = Object.values(registry.entities).some((entity) =>
251
+ entity.kind === "operation-field" && entity.parent === allocation.intent && entity.status === "retired" &&
252
+ (entity.label === allocation.field || entity.priorLabels?.includes(allocation.field) === true));
253
+ if (retired) continue; // the candidate dropped the field after the release; the evolution lane judges it
254
+ const result = reconciler.reconcile({
255
+ kind: "operation-field",
256
+ parent: allocation.intent,
257
+ labels: [allocation.field],
258
+ preferredOperationFieldWire: { [allocation.field]: { protobuf: allocation.protobuf } },
259
+ retireAbsent: false,
260
+ });
261
+ registry = result.registry;
262
+ allocated.push(`${intent.label}.${allocation.field}`);
263
+ }
264
+ if (wireDivergence.length > 0) {
265
+ throw new BaselineRefusal(
266
+ "baseline-wire-divergence",
267
+ `${wireDivergence.length} operation field(s) would encode wire keys release ${baseline.release} (law ${baseline.unitSet}) does not serve; ` +
268
+ "generated clients and installed law would disagree — restore the released allocation, or evolve it with a declared alias window",
269
+ wireDivergence,
270
+ );
271
+ }
272
+ const finished = reconciler.finish();
273
+ return { registry: finished, imported: imported.sort(compareText), relabelled: relabelled.sort(compareText), allocated, verified };
274
+ }
275
+
276
+ /**
277
+ * After reconciliation: every released identity still resolves to its released sid and every released
278
+ * wire allocation is still the canonical key. Reconciliation never rewrites an existing allocation, so
279
+ * this is the compile's own proof that nothing it did re-allocated released continuity.
280
+ */
281
+ export function assertBaselineRetained(final: LawIdentityRegistry, baseline: ReleasedBaseline): void {
282
+ const details: string[] = [];
283
+ for (const [key, releasedEntity] of Object.entries(baseline.continuity.identity?.entities ?? {})) {
284
+ if (releasedEntity.status !== "active") continue;
285
+ const local = final.entities[key];
286
+ if (local === undefined) {
287
+ details.push(`${coordinateOf(releasedEntity)}: released ${key} is absent after reconciliation`);
288
+ continue;
289
+ }
290
+ if (local.status !== "active") continue; // retired by a declared candidate-law retirement; the evolution gate judged it
291
+ const lineage = [releasedEntity.label, ...(releasedEntity.priorLabels ?? [])];
292
+ const occupant = activeAt(final.entities, releasedEntity.kind, local.parent, lineage);
293
+ if (occupant !== undefined && occupant[0] !== key) {
294
+ details.push(`${coordinateOf(releasedEntity)}: released ${key}, the candidate resolved ${occupant[0]}`);
295
+ }
296
+ }
297
+ for (const allocation of baseline.wireAllocations) {
298
+ const local = activeAt(final.entities, "operation-field", allocation.intent, [allocation.field]);
299
+ if (local === undefined) continue;
300
+ const canonical = local[1].operationFieldWire?.protobuf.canonical;
301
+ if (canonical !== allocation.protobuf.canonical) {
302
+ details.push(`${final.entities[allocation.intent]?.label ?? allocation.intent}.${allocation.field}: released on ${allocation.protobuf.canonical}, candidate on ${canonical ?? "nothing"}`);
303
+ }
304
+ }
305
+ if (details.length > 0) {
306
+ throw new BaselineRefusal(
307
+ "baseline-reallocation",
308
+ `the candidate re-allocated ${details.length} released identity/wire allocation(s)`,
309
+ details,
310
+ );
311
+ }
312
+ }
@@ -0,0 +1,186 @@
1
+ // NOMOS — THE RELEASED BASELINE ARTIFACT (architecture/domain_release_lifecycle.md, build step 1).
2
+ //
3
+ // A baseline is the continuity a publishable compile must start from: the identities, label lineage,
4
+ // declared retirements and operation wire allocations that released custody actually carries, named
5
+ // by the release / unit set they were exported from. It is content-addressed (its sha256 is the pin
6
+ // `nomos.project.ts` names), deterministic (two exports of the same units are byte-identical) and
7
+ // verifiable (export re-derives every unit hash from the bytes before it reads a single opinion).
8
+ //
9
+ // It is NOT the identity ledger. `nomos.identity.usda` keeps value contracts, merge drivers and every
10
+ // historical tombstone the compiler ever minted; released law carries none of that. The baseline
11
+ // anchors the ledger to custody: a compile that pins one refuses when its ledger would allocate an
12
+ // identity or wire key that disagrees with what was released (baseline_import.ts).
13
+ //
14
+ // Typed USDA, like everything Nomos remembers: the identity scopes are the identity layer's own
15
+ // encoding (identity_usda.ts `renderIdentityScopes`), hosted under `NomosBaseline` beside the
16
+ // provenance and wire-allocation scopes this artifact adds.
17
+ import { createHash } from "node:crypto";
18
+ import type { CompilerIdentityModel } from "./identity_model.js";
19
+ import {
20
+ NOMOS_IDENTITY_ENTITY_SCHEMA,
21
+ parseIdentityPrims,
22
+ readIdentityScopes,
23
+ renderIdentityScopes,
24
+ type IdentityPrim,
25
+ } from "./identity_usda.js";
26
+ import type { LawOperationFieldWire } from "./law_identity.js";
27
+
28
+ export const NOMOS_BASELINE_FORMAT = "nomos.baseline.v1";
29
+ /** The pinned baseline's conventional filename, beside `nomos.project.ts`. */
30
+ export const NOMOS_BASELINE_FILENAME = "nomos.baseline.usda";
31
+ /** The baseline's own "no release" marker, matching release_publication.ts `NO_PREDECESSOR`. */
32
+ export const BASELINE_NO_RELEASE = "none";
33
+
34
+ export type BaselineSource = "release" | "installed" | "units";
35
+
36
+ /** One operation field's released transport allocation, by its ledger coordinate. */
37
+ export interface BaselineWireAllocation {
38
+ /** The owning intent's registry key (`i_<sid>`). */
39
+ readonly intent: string;
40
+ /** The field's ledger label: a top-level payload name or a nested path (law_graph.ts `nestedWirePath`). */
41
+ readonly field: string;
42
+ readonly protobuf: LawOperationFieldWire["protobuf"];
43
+ }
44
+
45
+ export interface ReleasedBaseline {
46
+ readonly format: typeof NOMOS_BASELINE_FORMAT;
47
+ readonly source: BaselineSource;
48
+ /** The release identity (release_publication.ts) or {@link BASELINE_NO_RELEASE}. */
49
+ readonly release: string;
50
+ readonly series: string;
51
+ readonly predecessor: string;
52
+ /** THE LAW IDENTITY the baseline was read from (law_unit_set_identity.ts). */
53
+ readonly unitSet: string;
54
+ readonly units: readonly { readonly key: string; readonly unitHash: string }[];
55
+ /** For `installed`: the workspace whose installed law named these units, and the head observed. */
56
+ readonly workspace: string;
57
+ readonly workspaceHead: string;
58
+ /** Identities, lineage and declared retirements, in the identity layer's own model. */
59
+ readonly continuity: CompilerIdentityModel;
60
+ readonly wireAllocations: readonly BaselineWireAllocation[];
61
+ }
62
+
63
+ function quote(value: string): string {
64
+ return JSON.stringify(value);
65
+ }
66
+ const stringList = (items: readonly string[]): string => `[${items.map(quote).join(", ")}]`;
67
+ const byteOrder = (left: string, right: string): number => (left < right ? -1 : left > right ? 1 : 0);
68
+
69
+ /** The baseline's content address: sha256 of its exact bytes. */
70
+ export function baselineSha256(text: string): string {
71
+ return createHash("sha256").update(text, "utf8").digest("hex");
72
+ }
73
+
74
+ export function isBaselineUsda(text: string): boolean {
75
+ return text.startsWith("#usda") && text.includes(`"nomos:format" = ${quote(NOMOS_BASELINE_FORMAT)}`);
76
+ }
77
+
78
+ /** Render the baseline. Deterministic: units, allocations and every identity map are sorted. */
79
+ export function serializeBaselineUsda(baseline: Omit<ReleasedBaseline, "format">): string {
80
+ const units = [...baseline.units].sort((l, r) => byteOrder(l.key, r.key));
81
+ const allocations = [...baseline.wireAllocations].sort(
82
+ (l, r) => byteOrder(l.intent, r.intent) || byteOrder(l.field, r.field),
83
+ );
84
+ return [
85
+ "#usda 1.0",
86
+ "(",
87
+ ' defaultPrim = "NomosBaseline"',
88
+ " customLayerData = {",
89
+ ` string "nomos:format" = ${quote(NOMOS_BASELINE_FORMAT)}`,
90
+ " }",
91
+ ")",
92
+ "",
93
+ NOMOS_IDENTITY_ENTITY_SCHEMA,
94
+ "",
95
+ 'def Scope "NomosBaseline"',
96
+ "{",
97
+ ` custom uniform string nomos:source = ${quote(baseline.source)}`,
98
+ ` custom uniform string nomos:release = ${quote(baseline.release)}`,
99
+ ` custom uniform string nomos:series = ${quote(baseline.series)}`,
100
+ ` custom uniform string nomos:predecessor = ${quote(baseline.predecessor)}`,
101
+ ` custom uniform string nomos:unitSet = ${quote(baseline.unitSet)}`,
102
+ ` custom uniform string nomos:workspace = ${quote(baseline.workspace)}`,
103
+ ` custom uniform string nomos:workspaceHead = ${quote(baseline.workspaceHead)}`,
104
+ ' def Scope "Units"',
105
+ " {",
106
+ ...units.flatMap((unit, index) => [
107
+ ` def Scope ${quote(`u_${String(index).padStart(4, "0")}`)}`,
108
+ " {",
109
+ ` custom uniform string nomos:key = ${quote(unit.key)}`,
110
+ ` custom uniform string nomos:unitHash = ${quote(unit.unitHash)}`,
111
+ " }",
112
+ ]),
113
+ " }",
114
+ ' def Scope "WireAllocations"',
115
+ " {",
116
+ ...allocations.flatMap((allocation, index) => [
117
+ ` def Scope ${quote(`w_${String(index).padStart(6, "0")}`)}`,
118
+ " {",
119
+ ` custom uniform string nomos:intent = ${quote(allocation.intent)}`,
120
+ ` custom uniform string nomos:field = ${quote(allocation.field)}`,
121
+ ` custom uniform string nomos:protobufCanonicalWireKey = ${quote(allocation.protobuf.canonical)}`,
122
+ ...(allocation.protobuf.aliases === undefined || allocation.protobuf.aliases.length === 0
123
+ ? []
124
+ : [` custom uniform string[] nomos:protobufWireAliases = ${stringList(allocation.protobuf.aliases)}`]),
125
+ " }",
126
+ ]),
127
+ " }",
128
+ ' def Scope "Continuity"',
129
+ " {",
130
+ ...renderIdentityScopes(baseline.continuity, 8),
131
+ " }",
132
+ "}",
133
+ "",
134
+ ].join("\n");
135
+ }
136
+
137
+ function attr(prim: IdentityPrim, name: string): string {
138
+ const value = prim.attrs[name];
139
+ if (typeof value !== "string") throw new Error(`${NOMOS_BASELINE_FILENAME} prim ${prim.name} lacks nomos:${name}`);
140
+ return value;
141
+ }
142
+
143
+ /** Read a baseline back. Refuses anything that is not this exact format. */
144
+ export function parseBaselineUsda(text: string): ReleasedBaseline {
145
+ if (!isBaselineUsda(text)) {
146
+ throw new Error(`${NOMOS_BASELINE_FILENAME} is not ${NOMOS_BASELINE_FORMAT} — refusing to guess a baseline encoding`);
147
+ }
148
+ const roots = parseIdentityPrims(text, NOMOS_BASELINE_FILENAME);
149
+ const root = roots.find((prim) => prim.name === "NomosBaseline");
150
+ if (root === undefined) throw new Error(`${NOMOS_BASELINE_FILENAME} has no NomosBaseline prim`);
151
+ const child = (name: string): IdentityPrim | undefined => root.children.find((prim) => prim.name === name);
152
+ const source = attr(root, "source");
153
+ if (source !== "release" && source !== "installed" && source !== "units") {
154
+ throw new Error(`${NOMOS_BASELINE_FILENAME} names unknown source '${source}'`);
155
+ }
156
+ const units = (child("Units")?.children ?? []).map((prim) => ({
157
+ key: attr(prim, "key"),
158
+ unitHash: attr(prim, "unitHash"),
159
+ }));
160
+ const wireAllocations = (child("WireAllocations")?.children ?? []).map((prim) => {
161
+ const aliases = prim.attrs.protobufWireAliases;
162
+ return {
163
+ intent: attr(prim, "intent"),
164
+ field: attr(prim, "field"),
165
+ protobuf: {
166
+ canonical: attr(prim, "protobufCanonicalWireKey"),
167
+ ...(Array.isArray(aliases) && aliases.length > 0 ? { aliases: aliases as string[] } : {}),
168
+ },
169
+ };
170
+ });
171
+ const continuityPrim = child("Continuity");
172
+ if (continuityPrim === undefined) throw new Error(`${NOMOS_BASELINE_FILENAME} has no Continuity scope`);
173
+ return {
174
+ format: NOMOS_BASELINE_FORMAT,
175
+ source,
176
+ release: attr(root, "release"),
177
+ series: attr(root, "series"),
178
+ predecessor: attr(root, "predecessor"),
179
+ unitSet: attr(root, "unitSet"),
180
+ units,
181
+ workspace: attr(root, "workspace"),
182
+ workspaceHead: attr(root, "workspaceHead"),
183
+ continuity: readIdentityScopes(continuityPrim, NOMOS_BASELINE_FILENAME),
184
+ wireAllocations,
185
+ };
186
+ }