@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,107 @@
1
+ // THE CLAIMED VERSION OF A LAW (CAP-811).
2
+ //
3
+ // A law's identity is its package name (unique per provider, enforced by nomos.cafe). A version's identity is its
4
+ // content hash (the unit set identity). The claimed version orders the versions of one identity: an author declares
5
+ // it in `nomos.project.ts` (`version: "1.4.0"`), the compiler writes it into the package contract
6
+ // (`/Nomos/Package` `nomos:version` in `package.usda`), and so it travels inside the composition unit and is covered
7
+ // by that unit's hash, the unit set identity and the closure identity. Nobody can restate a version without minting
8
+ // a different law.
9
+ //
10
+ // The claim is a claim, not an authority: the provider's release lane (qualify -> publish -> rollout) refuses to
11
+ // publish a claim that does not move its series forward, and a kernel WARNS when an install serves an equal or lower
12
+ // claim for an identity it already serves. Absent claim = no ordering = no warning; a package that declares no
13
+ // version is byte-identical to one compiled before this key.
14
+ //
15
+ // Grammar: semver 2.0 MAJOR.MINOR.PATCH with an optional pre-release, no build metadata (build metadata does not
16
+ // order versions, so it could only make two different laws claim the same place). Dependency-free so the compiler,
17
+ // CLI, console and Peer Office share one comparison; the kernel's twin is `law_version` (kernel/src/law_version.rs).
18
+
19
+ /** A validated claim, as authored: typed, never a bare string. Only `lawVersionClaim` and the readers here mint one. */
20
+ export type LawVersionClaim = string & { readonly __nomosLawVersionClaim: unique symbol };
21
+
22
+ export interface LawVersion {
23
+ readonly major: number;
24
+ readonly minor: number;
25
+ readonly patch: number;
26
+ /** Dot-separated pre-release identifiers, empty for a release. */
27
+ readonly prerelease: readonly string[];
28
+ }
29
+
30
+ const NUMERIC = "0|[1-9]\\d*";
31
+ const IDENT = "(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)";
32
+ const LAW_VERSION = new RegExp(`^(${NUMERIC})\\.(${NUMERIC})\\.(${NUMERIC})(?:-(${IDENT}(?:\\.${IDENT})*))?$`);
33
+
34
+ /** Parse a claimed version, or explain why it is not one. */
35
+ export function parseLawVersion(text: string): LawVersion {
36
+ if (typeof text !== "string") throw new Error(`a law version must be a string; got ${JSON.stringify(text)}`);
37
+ if (text.includes("+")) {
38
+ throw new Error(`law version '${text}' carries build metadata; a claim must order law, so drop the '+…' suffix`);
39
+ }
40
+ const match = LAW_VERSION.exec(text);
41
+ if (match === null) {
42
+ throw new Error(`law version '${text}' is not semver MAJOR.MINOR.PATCH[-prerelease] (e.g. "1.4.0", "2.0.0-rc.1")`);
43
+ }
44
+ const part = (value: string): number => {
45
+ const number = Number(value);
46
+ if (!Number.isSafeInteger(number)) throw new Error(`law version '${text}' has a component too large to order`);
47
+ return number;
48
+ };
49
+ return {
50
+ major: part(match[1]!),
51
+ minor: part(match[2]!),
52
+ patch: part(match[3]!),
53
+ prerelease: match[4] === undefined ? [] : match[4].split("."),
54
+ };
55
+ }
56
+
57
+ /** Validate authored text as a claim, or throw why it is not one. */
58
+ export function lawVersionClaim(text: string): LawVersionClaim {
59
+ parseLawVersion(text);
60
+ return text as LawVersionClaim;
61
+ }
62
+
63
+ export function isLawVersion(text: unknown): text is LawVersionClaim {
64
+ if (typeof text !== "string") return false;
65
+ try {
66
+ parseLawVersion(text);
67
+ return true;
68
+ } catch {
69
+ return false;
70
+ }
71
+ }
72
+
73
+ const compareIdentifier = (left: string, right: string): number => {
74
+ const leftNumeric = /^\d+$/.test(left);
75
+ const rightNumeric = /^\d+$/.test(right);
76
+ if (leftNumeric && rightNumeric) return Math.sign(Number(left) - Number(right));
77
+ if (leftNumeric) return -1;
78
+ if (rightNumeric) return 1;
79
+ return left < right ? -1 : left > right ? 1 : 0;
80
+ };
81
+
82
+ /** Semver precedence: negative when `left` orders before `right`, zero when they claim the same place. */
83
+ export function compareLawVersions(left: LawVersionClaim, right: LawVersionClaim): number {
84
+ const a = parseLawVersion(left);
85
+ const b = parseLawVersion(right);
86
+ for (const key of ["major", "minor", "patch"] as const) {
87
+ if (a[key] !== b[key]) return Math.sign(a[key] - b[key]);
88
+ }
89
+ if (a.prerelease.length === 0 || b.prerelease.length === 0) {
90
+ return a.prerelease.length === b.prerelease.length ? 0 : a.prerelease.length === 0 ? 1 : -1;
91
+ }
92
+ for (let index = 0; index < Math.min(a.prerelease.length, b.prerelease.length); index += 1) {
93
+ const order = compareIdentifier(a.prerelease[index]!, b.prerelease[index]!);
94
+ if (order !== 0) return order;
95
+ }
96
+ return Math.sign(a.prerelease.length - b.prerelease.length);
97
+ }
98
+
99
+ /**
100
+ * Read the claimed version from a compiled package contract (`package.usda`, the composition unit's asset), or
101
+ * `undefined` when the law claims none. Throws when the contract carries a claim that is not a law version.
102
+ */
103
+ export function claimedVersionOfPackageContract(packageUsda: string): LawVersionClaim | undefined {
104
+ const match = /^\s*custom uniform string nomos:version = ("(?:[^"\\]|\\.)*")\s*$/mu.exec(packageUsda);
105
+ if (match === null) return undefined;
106
+ return lawVersionClaim(JSON.parse(match[1]!) as string);
107
+ }
@@ -1,5 +1,6 @@
1
1
  // NOMOS — one package contract for authoring, checking, compilation and editor diagnostics.
2
2
 
3
+ import { parseLawVersion } from "./law_version.js";
3
4
  import {
4
5
  normalizeRuntimePackageDependencies,
5
6
  type RuntimePackageDependency,
@@ -72,6 +73,15 @@ export interface NormalizedNomosPackageConfig extends Omit<NomosPackageConfig, "
72
73
  */
73
74
  export interface NomosPackageConfig {
74
75
  readonly name: string;
76
+ /**
77
+ * THE CLAIMED VERSION of this law (CAP-811): semver `MAJOR.MINOR.PATCH[-prerelease]`, e.g. `"1.4.0"`. The
78
+ * package name is the law's identity, the content hash is the version's identity, and this claim orders the
79
+ * versions of one identity. It is compiled into the package contract, so it is covered by the law's hash. The
80
+ * release lane refuses to publish a claim that does not move its series forward; a kernel warns when an install
81
+ * serves an equal or lower claim. Era rule: a package that declares none is byte-identical to one compiled
82
+ * before this key.
83
+ */
84
+ readonly version?: string;
75
85
  readonly domains: readonly NomosPackageDomainConfig[];
76
86
  /** Application-root policy compiled into the private receiving-group law.
77
87
  * Required for a published application: Nomos never invents a child-workspace allowance. */
@@ -91,6 +101,17 @@ export interface NomosPackageConfig {
91
101
  };
92
102
  /** Separately installed law required at runtime; USDA composition arcs do not belong here. */
93
103
  readonly dependencies?: readonly RuntimePackageDependency[];
104
+ /**
105
+ * THE RELEASED BASELINE this package's compiles extend (architecture/domain_release_lifecycle.md,
106
+ * step 1): a `nomos.baseline.usda` exported from released custody by `githolon baseline export`,
107
+ * pinned by content address. A publishable compile (`githolon compile --publishable`) refuses
108
+ * without one; every compile that names one verifies the ledger against it and imports the
109
+ * released identities and wire allocations it lacks. `artifact` is relative to this file.
110
+ */
111
+ readonly baseline?: {
112
+ readonly artifact: string;
113
+ readonly sha256: string;
114
+ };
94
115
  }
95
116
 
96
117
  type PackageDomainKey<Config extends NomosPackageConfig> =
@@ -143,6 +164,13 @@ export function validateNomosPackageConfig(value: unknown): {
143
164
  if (!Array.isArray(config.domains) || config.domains.length === 0) {
144
165
  throw new Error("config needs domains: [{ key, features }, ...]");
145
166
  }
167
+ if (config.version !== undefined) {
168
+ try {
169
+ parseLawVersion(config.version);
170
+ } catch (error) {
171
+ throw new Error(`package '${config.name}' version: ${(error as Error).message}`);
172
+ }
173
+ }
146
174
  const domainKeys = new Set<string>();
147
175
  const normalizedDomains: NormalizedNomosPackageDomainConfig[] = [];
148
176
  for (const candidate of config.domains) {
@@ -219,6 +247,22 @@ export function validateNomosPackageConfig(value: unknown): {
219
247
  if (config.dependencies !== undefined && !Array.isArray(config.dependencies)) {
220
248
  throw new Error("package dependencies must be an array");
221
249
  }
250
+ if (config.baseline !== undefined) {
251
+ const baseline = config.baseline as Partial<NonNullable<NomosPackageConfig["baseline"]>> | null;
252
+ if (
253
+ baseline === null
254
+ || typeof baseline !== "object"
255
+ || typeof baseline.artifact !== "string"
256
+ || baseline.artifact.length === 0
257
+ || typeof baseline.sha256 !== "string"
258
+ || !/^[0-9a-f]{64}$/u.test(baseline.sha256)
259
+ ) {
260
+ throw new Error(
261
+ "baseline must be { artifact: '<path to nomos.baseline.usda>', sha256: '<its 64-hex content address>' }; " +
262
+ "export it with githolon baseline export and pin the sha256 it prints",
263
+ );
264
+ }
265
+ }
222
266
  if (config.application !== undefined) {
223
267
  const max = config.application.maxChildWorkspacesPerUser;
224
268
  if (!Number.isInteger(max) || max < 0) {
@@ -1,5 +1,6 @@
1
1
  // NOMOS — runtime package requirements are canonical typed USDA law, not ambient host setup.
2
2
 
3
+ import { parseLawVersion, type LawVersionClaim } from "./law_version.js";
3
4
  import { usdaString, usdaStringArray } from "./usda_text.js";
4
5
 
5
6
  /**
@@ -59,9 +60,13 @@ export function normalizeRuntimePackageDependencies(
59
60
  /** Emit the package contract into the same composed stage the kernel executes. */
60
61
  export function packageContractUsda(input: {
61
62
  readonly packageName: string;
63
+ /** The law's claimed version (CAP-811, law_version.ts). Absent: no claim, and the contract is byte-identical
64
+ * to one compiled before claims existed. Present: carried in the composition unit, so covered by its hash. */
65
+ readonly claimedVersion?: LawVersionClaim;
62
66
  readonly exportedDomains: readonly string[];
63
67
  readonly runtimeDependencies: readonly RuntimePackageDependency[];
64
68
  }): string {
69
+ if (input.claimedVersion !== undefined) parseLawVersion(input.claimedVersion);
65
70
  const dependencies = [...input.runtimeDependencies];
66
71
  const dependencyPrimName = (index: number): string => `d_${String(index).padStart(6, "0")}`;
67
72
  const targets = dependencies.map(
@@ -81,6 +86,9 @@ export function packageContractUsda(input: {
81
86
  ' def NomosPackage "Package"',
82
87
  " {",
83
88
  ` custom uniform string nomos:name = ${usdaString(input.packageName)}`,
89
+ ...(input.claimedVersion === undefined
90
+ ? []
91
+ : [` custom uniform string nomos:version = ${usdaString(input.claimedVersion)}`]),
84
92
  ' custom uniform token nomos:operationContract = "nomos.protobuf.v1"',
85
93
  ` custom uniform string[] nomos:domains = ${usdaStringArray([...input.exportedDomains].sort())}`,
86
94
  ...(targets.length > 0
Binary file
@@ -145,3 +145,27 @@ export function inspectReleaseBaseline(input: { identity: string; units: string;
145
145
  'Unobserved declarations may be out of package scope or structurally interned; absence is not a compatibility refusal.',
146
146
  'Consumer compatibility, state evolution and publication provenance are not certified by this report.'] };
147
147
  }
148
+
149
+ /**
150
+ * A compiled `build/<name>.units` directory as a release candidate and the bytes to upload: every unit
151
+ * verified by `readInspectionUnits` (digests, canonical manifest, unit hash), every file named by digest.
152
+ * The console verifies the same bytes again on publication (release_publication.ts `verifyRelease`).
153
+ */
154
+ export function releaseCandidateFromUnits(directory: string, series: string, predecessor: string): {
155
+ candidate: { series: string; predecessor: string; units: { key: string; unitHash: string; files: { path: string; sha256: string; size: number }[] }[] };
156
+ blobs: Map<string, Uint8Array>;
157
+ } {
158
+ const blobs = new Map<string, Uint8Array>();
159
+ const units = readInspectionUnits(directory).map((unit) => {
160
+ const files = [...unit.assets.map((asset) => ({ path: asset.logicalPath, bytes: asset.bytes })),
161
+ { path: 'unit.usda', bytes: unit.manifest }]
162
+ .sort((l, r) => (l.path < r.path ? -1 : l.path > r.path ? 1 : 0))
163
+ .map(({ path: filePath, bytes }) => {
164
+ const sha256 = sha(bytes);
165
+ blobs.set(sha256, new Uint8Array(bytes));
166
+ return { path: filePath, sha256, size: bytes.byteLength };
167
+ });
168
+ return { key: unit.key, unitHash: unit.hash, files };
169
+ }).sort((l, r) => (l.key < r.key ? -1 : l.key > r.key ? 1 : 0));
170
+ return { candidate: { series, predecessor, units }, blobs };
171
+ }
@@ -0,0 +1,298 @@
1
+ // A published domain release, as bytes: its identity and the verification every publisher and the console
2
+ // apply to the uploaded units (architecture/domain_release_lifecycle.md, "Publish candidate").
3
+ //
4
+ // A release is a complete law unit set (architecture/law_units_format.md): framework, composition and the
5
+ // domain units the compiler emitted, each named by its unit hash. Publishing it verifies every byte against
6
+ // the unit hashes the kernel also derives, and names the release by an identity over its series, its
7
+ // predecessor and its exact (key, unit hash) set. Publication installs nothing: each target's kernel
8
+ // admits the same units again, from their content, when it is asked to stage them.
9
+ //
10
+ // Node-free (WebCrypto only), so the console Worker, the CLI and CI verify with this one module. The unit
11
+ // hash and canonical manifest follow `law_units.ts` (`lawUnitHash`, `unitManifestUsda`) and the kernel's
12
+ // `DomainLawUnit`; `release_publication.test.ts` proves agreement on compiled units.
13
+
14
+ import { lawUnitSetIdentityPreimage } from "./law_unit_set_identity.ts";
15
+
16
+ export const RELEASE_IDENTITY_TAG = "nomos-domain-release-v1";
17
+ const LAW_UNIT_HASH_TAG = "nomos-law-unit-v1";
18
+ const LAW_UNIT_FORMAT = "nomos.law-unit.v1";
19
+ export const LAW_UNIT_MANIFEST = "unit.usda";
20
+
21
+ /** The first release of a series names this as its predecessor. Explicit, never an empty default. */
22
+ export const NO_PREDECESSOR = "none";
23
+
24
+ const HEX64 = /^[0-9a-f]{64}$/;
25
+ const SERIES = /^[A-Za-z_][A-Za-z0-9_.-]{0,127}$/;
26
+ const UNIT_KEY = /^(framework|composition|domain:[A-Za-z_][A-Za-z0-9_.-]{0,127})$/;
27
+ /** Largest single file a release may carry (one upload request; the console's content store holds it in parts). */
28
+ export const MAX_RELEASE_FILE_BYTES = 32_000_000;
29
+ export const MAX_RELEASE_FILES = 4096;
30
+
31
+ export interface ReleaseFile {
32
+ /** The file's logical path inside its unit (`unit.usda` is the manifest). */
33
+ readonly path: string;
34
+ readonly sha256: string;
35
+ readonly size: number;
36
+ }
37
+
38
+ export interface ReleaseUnit {
39
+ readonly key: string;
40
+ readonly unitHash: string;
41
+ /** Every file of the unit, the manifest included, sorted by path. */
42
+ readonly files: readonly ReleaseFile[];
43
+ }
44
+
45
+ export interface ReleaseCandidate {
46
+ /** The published domain identity this release continues: one of its domain keys. */
47
+ readonly series: string;
48
+ /** The release identity this one follows, or {@link NO_PREDECESSOR}. */
49
+ readonly predecessor: string;
50
+ readonly units: readonly ReleaseUnit[];
51
+ }
52
+
53
+ /** A refusal with a stable code; a client never parses the message. */
54
+ export class ReleaseVerificationError extends Error {
55
+ readonly code: string;
56
+ constructor(code: string, message: string) {
57
+ super(message);
58
+ this.code = code;
59
+ }
60
+ }
61
+
62
+ const refuse = (code: string, message: string): never => {
63
+ throw new ReleaseVerificationError(code, message);
64
+ };
65
+
66
+ const utf8 = (text: string): Uint8Array => new TextEncoder().encode(text);
67
+ const byteOrder = (left: string, right: string): number => (left < right ? -1 : left > right ? 1 : 0);
68
+
69
+ const hex = (bytes: ArrayBuffer): string =>
70
+ [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
71
+
72
+ export async function sha256Hex(input: Uint8Array | string): Promise<string> {
73
+ const bytes = typeof input === "string" ? utf8(input) : input;
74
+ return hex(await crypto.subtle.digest("SHA-256", bytes as Uint8Array<ArrayBuffer>));
75
+ }
76
+
77
+ /** One concatenation of tagged, NUL-separated fields, hashed once. */
78
+ async function taggedHash(parts: readonly string[]): Promise<string> {
79
+ return sha256Hex(parts.join(""));
80
+ }
81
+
82
+ /**
83
+ * The release identity: sha256("nomos-domain-release-v1\0" series "\0" predecessor "\0"
84
+ * { key "\0" unitHash "\0" }*), units sorted by key in byte order. Two clients that publish the same
85
+ * set over the same predecessor publish the same release; nothing else (who, when, evidence) moves it.
86
+ */
87
+ export async function releaseIdentity(candidate: {
88
+ readonly series: string;
89
+ readonly predecessor: string;
90
+ readonly units: readonly { readonly key: string; readonly unitHash: string }[];
91
+ }): Promise<string> {
92
+ const parts = [`${RELEASE_IDENTITY_TAG}\0`, candidate.series, "\0", candidate.predecessor, "\0"];
93
+ for (const unit of [...candidate.units].sort((l, r) => byteOrder(l.key, r.key))) {
94
+ parts.push(unit.key, "\0", unit.unitHash, "\0");
95
+ }
96
+ return taggedHash(parts);
97
+ }
98
+
99
+ /**
100
+ * THE LAW IDENTITY of a unit set (law_unit_set_identity.ts; the kernel's `law_unit_set_identity`): its sorted
101
+ * (key, unit hash) pairs and nothing else. A release names it through its units; every target realises it as a
102
+ * closure that also depends on the target kernel's composer, which is why a receipt records both.
103
+ */
104
+ export async function lawUnitSetIdentity(units: readonly { readonly key: string; readonly unitHash: string }[]): Promise<string> {
105
+ return sha256Hex(lawUnitSetIdentityPreimage(units));
106
+ }
107
+
108
+ /** The unit hash (law_units.ts `lawUnitHash`, the kernel's `DomainLawUnit`), over WebCrypto. */
109
+ export async function lawUnitHashOf(
110
+ key: string,
111
+ inventory: readonly { readonly logicalPath: string; readonly sha256: string }[],
112
+ routes: readonly { readonly selector: string; readonly planSha256: string }[],
113
+ ): Promise<string> {
114
+ const parts = [`${LAW_UNIT_HASH_TAG}\0`, key, "\0", "assets\0"];
115
+ for (const asset of [...inventory].sort((l, r) => byteOrder(l.logicalPath, r.logicalPath))) {
116
+ parts.push(asset.logicalPath, "\0", asset.sha256, "\0");
117
+ }
118
+ parts.push("routes\0");
119
+ for (const route of [...routes].sort((l, r) => byteOrder(l.selector, r.selector))) {
120
+ parts.push(route.selector, "\0", route.planSha256, "\0");
121
+ }
122
+ return taggedHash(parts);
123
+ }
124
+
125
+ interface ParsedManifest {
126
+ readonly key: string;
127
+ readonly kind: "domain" | "framework" | "composition";
128
+ readonly domain?: string;
129
+ readonly hash: string;
130
+ readonly assets: readonly { readonly logicalPath: string; readonly sha256: string }[];
131
+ readonly routes: readonly { readonly selector: string; readonly planSha256: string }[];
132
+ }
133
+
134
+ /** The compiler's canonical manifest (law_units.ts `unitManifestUsda`), over WebCrypto. */
135
+ async function canonicalManifest(unit: ParsedManifest): Promise<string> {
136
+ const assetPrims = await Promise.all(unit.assets.map(async (asset) => [
137
+ ` def NomosLawAsset "a_${await sha256Hex(asset.logicalPath)}"`,
138
+ " {",
139
+ ` custom string nomos:logicalPath = ${JSON.stringify(asset.logicalPath)}`,
140
+ ` custom string nomos:sha256 = "${asset.sha256}"`,
141
+ " }",
142
+ ]));
143
+ const routePrims = await Promise.all(unit.routes.map(async (route) => [
144
+ ` def NomosLawRoute "s_${await sha256Hex(route.selector)}"`,
145
+ " {",
146
+ ` custom string nomos:selector = ${JSON.stringify(route.selector)}`,
147
+ ` custom string nomos:plan = "${route.planSha256}"`,
148
+ " }",
149
+ ]));
150
+ return [
151
+ "#usda 1.0",
152
+ "(",
153
+ ' defaultPrim = "__NomosLawUnit_7e9c"',
154
+ " customLayerData = {",
155
+ ` string "nomos:format" = "${LAW_UNIT_FORMAT}"`,
156
+ " }",
157
+ ")",
158
+ "",
159
+ 'def NomosLawUnit "__NomosLawUnit_7e9c"',
160
+ "{",
161
+ ` custom string nomos:unit = ${JSON.stringify(unit.key)}`,
162
+ ` custom token nomos:unitKind = "${unit.kind}"`,
163
+ ...(unit.domain === undefined ? [] : [` custom string nomos:domain = ${JSON.stringify(unit.domain)}`]),
164
+ ` custom string nomos:unitHash = "${unit.hash}"`,
165
+ "",
166
+ ' def Scope "Assets"',
167
+ " {",
168
+ ...assetPrims.flat(),
169
+ " }",
170
+ "",
171
+ ' def Scope "Routes"',
172
+ " {",
173
+ ...routePrims.flat(),
174
+ " }",
175
+ "}",
176
+ "",
177
+ ].join("\n");
178
+ }
179
+
180
+ const LITERAL = '("(?:[^"\\\\]|\\\\.)*")';
181
+ function attribute(source: string, name: string, where: string): string {
182
+ const values = [...source.matchAll(new RegExp(`nomos:${name} = ${LITERAL}`, "g"))];
183
+ if (values.length !== 1) refuse("manifest-invalid", `${where}: expected one nomos:${name}`);
184
+ return JSON.parse(values[0]![1]!) as string;
185
+ }
186
+
187
+ /**
188
+ * Read a unit manifest in the compiler's canonical dialect only (release_inspection.ts
189
+ * `readInspectionUnits`): the attributes it names, then a byte-for-byte round trip through the canonical
190
+ * emitter. Any other spelling is refused rather than interpreted.
191
+ */
192
+ export async function parseUnitManifest(source: string, where: string): Promise<ParsedManifest> {
193
+ const key = attribute(source, "unit", where);
194
+ const kind = attribute(source, "unitKind", where) as ParsedManifest["kind"];
195
+ if (!["domain", "framework", "composition"].includes(kind)) refuse("manifest-invalid", `${where}: unknown unit kind '${kind}'`);
196
+ const hash = attribute(source, "unitHash", where);
197
+ const domain = kind === "domain" ? attribute(source, "domain", where) : undefined;
198
+ if (key !== (domain === undefined ? kind : `domain:${domain}`)) refuse("manifest-invalid", `${where}: unit key and kind disagree`);
199
+ const assets = [...source.matchAll(/def NomosLawAsset "[^"]+"\s*\{([^}]+)\}/g)].map((match) => ({
200
+ logicalPath: attribute(match[1]!, "logicalPath", where), sha256: attribute(match[1]!, "sha256", where),
201
+ }));
202
+ const routes = [...source.matchAll(/def NomosLawRoute "[^"]+"\s*\{([^}]+)\}/g)].map((match) => ({
203
+ selector: attribute(match[1]!, "selector", where), planSha256: attribute(match[1]!, "plan", where),
204
+ }));
205
+ const parsed: ParsedManifest = { key, kind, ...(domain === undefined ? {} : { domain }), hash, assets, routes };
206
+ if (await canonicalManifest(parsed) !== source) refuse("manifest-noncanonical", `${where}: the manifest is not in the compiler's canonical form`);
207
+ return parsed;
208
+ }
209
+
210
+ const safePath = (path: string): boolean =>
211
+ path.length > 0 && path.length <= 512 && !path.startsWith("/") && !path.includes("\\")
212
+ && path.split("/").every((part) => part !== "" && part !== "." && part !== "..");
213
+
214
+ /** Shape checks that need no bytes: sorted, unique, complete, well-formed. */
215
+ export function checkReleaseShape(candidate: ReleaseCandidate): void {
216
+ if (!SERIES.test(candidate.series)) refuse("release-invalid", "series must be a domain key");
217
+ if (candidate.predecessor !== NO_PREDECESSOR && !HEX64.test(candidate.predecessor)) {
218
+ refuse("release-invalid", `predecessor must be a release identity or '${NO_PREDECESSOR}'`);
219
+ }
220
+ const keys = candidate.units.map((unit) => unit.key);
221
+ if (!keys.every((key) => UNIT_KEY.test(key))) refuse("release-invalid", "every unit key is framework, composition or domain:<key>");
222
+ if (!keys.every((key, index) => index === 0 || byteOrder(keys[index - 1]!, key) < 0)) {
223
+ refuse("release-invalid", "units must be sorted by key, without repeats");
224
+ }
225
+ for (const required of ["framework", "composition", `domain:${candidate.series}`]) {
226
+ if (!keys.includes(required)) {
227
+ refuse("release-incomplete", `a release is a complete unit set; it lacks '${required}' (publish the compiler's whole build/<name>.units)`);
228
+ }
229
+ }
230
+ let files = 0;
231
+ for (const unit of candidate.units) {
232
+ if (!HEX64.test(unit.unitHash)) refuse("release-invalid", `${unit.key}: unitHash must be sha256 hex`);
233
+ const paths = unit.files.map((file) => file.path);
234
+ if (!paths.every((path, index) => index === 0 || byteOrder(paths[index - 1]!, path) < 0)) {
235
+ refuse("release-invalid", `${unit.key}: files must be sorted by path, without repeats`);
236
+ }
237
+ if (!paths.includes(LAW_UNIT_MANIFEST)) refuse("release-invalid", `${unit.key}: the unit carries no ${LAW_UNIT_MANIFEST}`);
238
+ for (const file of unit.files) {
239
+ if (!safePath(file.path)) refuse("release-invalid", `${unit.key}: unsafe file path '${file.path}'`);
240
+ if (!HEX64.test(file.sha256)) refuse("release-invalid", `${unit.key}/${file.path}: sha256 must be hex`);
241
+ if (!Number.isSafeInteger(file.size) || file.size < 0 || file.size > MAX_RELEASE_FILE_BYTES) {
242
+ refuse("release-invalid", `${unit.key}/${file.path}: size must be 0..${MAX_RELEASE_FILE_BYTES} bytes`);
243
+ }
244
+ }
245
+ files += unit.files.length;
246
+ }
247
+ if (files > MAX_RELEASE_FILES) refuse("release-invalid", `a release carries at most ${MAX_RELEASE_FILES} files`);
248
+ }
249
+
250
+ export interface VerifiedRelease {
251
+ readonly identity: string;
252
+ /** The law identity of its units ({@link lawUnitSetIdentity}). */
253
+ readonly unitSet: string;
254
+ readonly candidate: ReleaseCandidate;
255
+ /** The domain keys of its domain units, sorted. */
256
+ readonly serves: readonly string[];
257
+ }
258
+
259
+ /**
260
+ * Verify a release candidate against its bytes: every file is present and is exactly the bytes its digest
261
+ * names, every manifest is canonical and lists exactly the unit's files, and every unit hash re-derives.
262
+ * `read` answers a digest's bytes from a content store, or undefined when the store lacks it.
263
+ */
264
+ export async function verifyRelease(
265
+ candidate: ReleaseCandidate,
266
+ read: (sha256: string) => Promise<Uint8Array | undefined>,
267
+ ): Promise<VerifiedRelease> {
268
+ checkReleaseShape(candidate);
269
+ for (const unit of candidate.units) {
270
+ const where = unit.key;
271
+ const bytesOf = new Map<string, Uint8Array>();
272
+ for (const file of unit.files) {
273
+ const bytes = await read(file.sha256);
274
+ if (!bytes) refuse("artifact-missing", `${where}/${file.path}: ${file.sha256} has not been uploaded`);
275
+ if (await sha256Hex(bytes!) !== file.sha256) refuse("artifact-digest-mismatch", `${where}/${file.path}: the stored bytes are not ${file.sha256}`);
276
+ if (bytes!.byteLength !== file.size) refuse("artifact-size-mismatch", `${where}/${file.path}: ${bytes!.byteLength} bytes, not ${file.size}`);
277
+ bytesOf.set(file.path, bytes!);
278
+ }
279
+ const manifest = await parseUnitManifest(new TextDecoder().decode(bytesOf.get(LAW_UNIT_MANIFEST)!), where);
280
+ if (manifest.key !== unit.key) refuse("unit-key-mismatch", `${where}: the manifest names unit '${manifest.key}'`);
281
+ const listed = [...manifest.assets.map((asset) => asset.logicalPath), LAW_UNIT_MANIFEST].sort(byteOrder);
282
+ const carried = unit.files.map((file) => file.path);
283
+ if (listed.length !== carried.length || listed.some((path, index) => path !== carried[index])) {
284
+ refuse("unit-inventory-mismatch", `${where}: the files carried are not exactly the files its manifest lists`);
285
+ }
286
+ for (const asset of manifest.assets) {
287
+ if (unit.files.find((file) => file.path === asset.logicalPath)!.sha256 !== asset.sha256) {
288
+ refuse("artifact-digest-mismatch", `${where}/${asset.logicalPath}: the manifest names another digest`);
289
+ }
290
+ }
291
+ const derived = await lawUnitHashOf(manifest.key, manifest.assets, manifest.routes);
292
+ if (derived !== manifest.hash || derived !== unit.unitHash) {
293
+ refuse("unit-hash-mismatch", `${where}: its content hashes to ${derived}, not ${unit.unitHash}`);
294
+ }
295
+ }
296
+ const serves = candidate.units.filter((unit) => unit.key.startsWith("domain:")).map((unit) => unit.key.slice("domain:".length)).sort(byteOrder);
297
+ return { identity: await releaseIdentity(candidate), unitSet: await lawUnitSetIdentity(candidate.units), candidate, serves };
298
+ }
@@ -66,6 +66,7 @@ import {
66
66
  type ValueObjectDescriptor,
67
67
  type ValueObjectFieldDescriptor,
68
68
  type ValueObjectFieldMeta,
69
+ type TypedId,
69
70
  } from "./fields.js";
70
71
  // LOCAL zod introspection (the same `_def ?? def` convention as codegen_ts.ts) —
71
72
  // deliberately NOT imported from codegen_ts.js: this module rides the RUNTIME barrel
@@ -434,14 +435,16 @@ export const NOMOS_BRANDED_ID_BRAND: unique symbol = Symbol.for("nomos.brandedId
434
435
 
435
436
  const BRAND_NAME_RE = /^[A-Z][A-Za-z0-9_]*$/;
436
437
 
437
- /** Declare a NOMINAL string id: `siteId: brandedId("SiteId")`. */
438
- export function brandedId(name: string): z.ZodType<string> {
438
+ /** Declare a NOMINAL string id: `siteId: brandedId("SiteId")`. The parsed value is typed
439
+ * {@link TypedId}`<"SiteId">` in law code too (the same type a `t.brandedId("SiteId")` field holds), so a
440
+ * plain string is a compile error on both sides of the wire. */
441
+ export function brandedId<const Name extends string>(name: Name): z.ZodType<TypedId<Name>> {
439
442
  if (!BRAND_NAME_RE.test(name)) {
440
443
  throw new Error(`brandedId: '${name}' must be a capitalized identifier — it becomes the generated branded type's name.`);
441
444
  }
442
445
  const schema = z.string();
443
446
  Object.defineProperty(schema, NOMOS_BRANDED_ID_BRAND, { value: name, enumerable: false });
444
- return schema;
447
+ return schema as unknown as z.ZodType<TypedId<Name>>;
445
448
  }
446
449
 
447
450
  /** Resolve the `brandedId(...)` brand name of a schema (through optional/default/nullable). */
package/src/wire.ts CHANGED
@@ -211,6 +211,8 @@ export interface WireIntent {
211
211
  workspace: string;
212
212
  genesisChain: unknown;
213
213
  parentWorkspaceRoles?: readonly import("./births.js").WorkspaceRole[];
214
+ /** The typed key the workspace name hashes (business births under typed-key law → `nomos:birthKey`). */
215
+ birthKey?: string;
214
216
  }[];
215
217
  /** Exact target calls authored by source law. The OpenUSD bytes come from a compiler-generated
216
218
  * encoder for the target Directive object; no public operation string or generic payload crosses. */
@@ -0,0 +1,17 @@
1
+ // NOMOS — The one spelling of a nested operation field's ledger label.
2
+ //
3
+ // A top-level payload field is ledgered under its own name. A field INSIDE a structural value
4
+ // (a record member, a map value, a list element, a union variant) is ledgered under a path built
5
+ // from these segments, so its transport allocation survives a change elsewhere in the structure
6
+ // (the 1 October 2026 `canvasRequirements` incident: nested `isRequired` keys moved because their
7
+ // structural sids moved). law_graph.ts allocates by these paths; baseline_export.ts recovers the
8
+ // same paths from released law so the two agree without a sidecar.
9
+
10
+ /** A record member beneath `parent`. */
11
+ export const nestedMemberPath = (parent: string, name: string): string => `${parent}.${name}`;
12
+ /** The element of a list beneath `parent`. */
13
+ export const listElementPath = (parent: string): string => `${parent}[]`;
14
+ /** The value of a map beneath `parent`. */
15
+ export const mapValuePath = (parent: string): string => `${parent}{}`;
16
+ /** One variant of a union beneath `parent`, by its discriminator label or ordinal. */
17
+ export const unionVariantPath = (parent: string, variant: string): string => `${parent}|${variant}|`;
@@ -164,7 +164,7 @@ export function routeTagHexOfMintedId(id: string): string | undefined {
164
164
  // ── the plan trace (the mint-front-door family, aimed at HOMES) ──────────────────────
165
165
 
166
166
  /** Build a schema-valid probe payload: every top-level string field gets its sentinel. */
167
- function buildProbePayload(
167
+ export function buildProbePayload(
168
168
  schema: z.ZodTypeAny,
169
169
  sentinelOf: Map<string, string>,
170
170
  jsonSafe = false,