@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,24 @@
1
+ // NOMOS — The wire identity of an operation field under OpenUSD composition.
2
+ //
3
+ // Node-free on purpose: the compatibility graph reaches this from the law bundle (the release law's
4
+ // plans bundle for a neutral platform), where `codegen_call_proto.ts` cannot live (it mints stable
5
+ // ids through `node:crypto`). Both the generated codecs and the compatibility requirement derive a
6
+ // nested member's allocation here, so the two can never disagree (CAP-681).
7
+
8
+ /**
9
+ * OpenUSD composition may share structurally identical anonymous records between
10
+ * operation contracts. `compileSparseLawGraphUsda` records that sharing by
11
+ * redirecting each duplicate field path to the canonical field prim. The wire
12
+ * identity must follow that composed path too; using the duplicate graph-local
13
+ * SID makes generated clients speak a different Protobuf schema from the law the
14
+ * kernel actually installs.
15
+ */
16
+ export function composedWireIdentity(
17
+ paths: ReadonlyMap<string, string>,
18
+ entity: { readonly id: string; readonly sid: string; readonly wireKey?: string },
19
+ ): string {
20
+ if (entity.wireKey !== undefined) return entity.wireKey;
21
+ const path = paths.get(entity.id);
22
+ const canonical = path?.match(/\/(?:Fields\/f_|Variants\/v_)([0-9a-f]{16})$/i)?.[1];
23
+ return canonical ?? entity.sid;
24
+ }
package/src/directive.ts CHANGED
@@ -536,6 +536,12 @@ export interface Directive<P = unknown> extends AuthenticatedMember {
536
536
  * `attestationAuthorizedGate`/`.attestationAuthorized()` naming convention; the declaration is the noun.)
537
537
  */
538
538
  readonly birthsChild?: boolean;
539
+ /** THE TYPED BIRTH KEY of a business birth (`birth(Aggregate, spec)`): the key's typed-id family, brand and
540
+ * the name's namespace. Lowered to the `nomos:birthKeyType` / `nomos:birthKeyBrand` /
541
+ * `nomos:birthKeyNamespace` era keys on the birthing intent; the kernel then requires every carried recipe
542
+ * of this intent to carry `nomos:birthKey`, checks its shape, and re-derives the child's name from it.
543
+ * Absent on every other directive (and on all historical law), so those chains replay unchanged. */
544
+ readonly birthKey?: import("./births.js").BirthKeyDeclaration;
539
545
  /** Additional ordinary offers which the born workspace's typed law requires in genesis.
540
546
  * This is compiler/runtime metadata attached only to a birthing directive. It contains no
541
547
  * payload or fallback value: the kernel checks that the re-derived recipe invokes each named
@@ -0,0 +1,75 @@
1
+ /**
2
+ * PER-DOMAIN UNIT REQUIREMENTS (CAP-666): what a generated client pins beside the unit set.
3
+ *
4
+ * An application is compiled against one law, named by its unit set (CAP-773). That one identity is
5
+ * what a workspace serves as a whole, so an app pinned to it alone goes stale the moment a rollout moves
6
+ * a domain the app never touches. Each application domain has its own unit in `build/<name>.units`
7
+ * (`domain:<key>`) and its own slice of the generated compatibility requirement
8
+ * (`<name>.compatibility.usda`, the `NomosCompatibilityDomain` the kernel's Compatibility Desk judges).
9
+ * This module names both per domain key, from the same in-memory units and interfaces the compiler
10
+ * just emitted, so the generated Dart/TS clients, `NomosApprovedModel`, the model-adoption desk and the
11
+ * client inventory all carry one contract per domain and never infer it from installed law.
12
+ *
13
+ * Era rule: the requirement is derived from existing artifacts and written into no law, unit or
14
+ * package. Unchanged law keeps every identity it had; only the generated client gains the table.
15
+ */
16
+ import { emitApplicationCompatibilityUsda } from "./compatibility_usda.js";
17
+ import type { DomainInterface } from "./compatibility_graph.js";
18
+ import type { LawUnit } from "./law_units.js";
19
+
20
+ export interface DomainUnitRequirement {
21
+ /** The application domain key. */
22
+ readonly domainKey: string;
23
+ /** The hash of its law unit (`domain:<key>` in the unit set), 64 lowercase hex. */
24
+ readonly unitSha256: string;
25
+ /** sha256 of the domain's own compatibility requirement USDA (the one-domain `emitApplicationCompatibilityUsda`). */
26
+ readonly requirementSha256: string;
27
+ }
28
+
29
+ const HEX64 = /^[0-9a-f]{64}$/;
30
+
31
+ /** The USDA requirement for exactly one domain: the slice a client carries and the kernel judges. */
32
+ export function domainRequirementUsda(domainKey: string, domainInterface: DomainInterface): string {
33
+ return emitApplicationCompatibilityUsda({ [domainKey]: domainInterface });
34
+ }
35
+
36
+ /**
37
+ * One requirement per application domain the package exposes an interface for, sorted by domain key. A
38
+ * domain unit without an interface (a private or framework domain) names no requirement and is omitted;
39
+ * an interface whose domain has no unit is a compiler defect and refuses.
40
+ */
41
+ export function domainRequirementsOf(
42
+ units: readonly Pick<LawUnit, "key" | "kind" | "domain" | "hash">[],
43
+ interfaces: Readonly<Record<string, DomainInterface>>,
44
+ sha256Hex: (utf8: string) => string,
45
+ ): DomainUnitRequirement[] {
46
+ const unitByDomain = new Map<string, string>();
47
+ for (const unit of units) {
48
+ if (unit.kind === "domain" && unit.domain !== undefined) unitByDomain.set(unit.domain, unit.hash);
49
+ }
50
+ return Object.keys(interfaces).sort().map((domainKey) => {
51
+ const unitSha256 = unitByDomain.get(domainKey);
52
+ if (unitSha256 === undefined) {
53
+ throw new Error(`domain requirements: '${domainKey}' has a compatibility interface but no law unit; the unit split and the interface disagree`);
54
+ }
55
+ const requirementSha256 = sha256Hex(domainRequirementUsda(domainKey, interfaces[domainKey]!));
56
+ for (const [name, value] of [["unit", unitSha256], ["requirement", requirementSha256]] as const) {
57
+ if (!HEX64.test(value)) throw new Error(`domain requirements: '${domainKey}' ${name} identity must be 64 lowercase hex, not '${value}'`);
58
+ }
59
+ return { domainKey, unitSha256, requirementSha256 };
60
+ });
61
+ }
62
+
63
+ /** Refuse a hand-supplied table that is not what the compiler would emit (generators take it as an input). */
64
+ export function assertDomainRequirements(requirements: readonly DomainUnitRequirement[]): void {
65
+ const seen = new Set<string>();
66
+ for (const requirement of requirements) {
67
+ if (!requirement.domainKey || seen.has(requirement.domainKey)) {
68
+ throw new Error(`domain requirements: '${requirement.domainKey}' is empty or named twice`);
69
+ }
70
+ seen.add(requirement.domainKey);
71
+ for (const [name, value] of [["unit", requirement.unitSha256], ["requirement", requirement.requirementSha256]] as const) {
72
+ if (!HEX64.test(value)) throw new Error(`domain requirements: '${requirement.domainKey}' ${name} identity must be 64 lowercase hex, not '${value}'`);
73
+ }
74
+ }
75
+ }
@@ -73,7 +73,6 @@ import {
73
73
  nomosSourceFrontierEnsure,
74
74
  withAttestedReadExports,
75
75
  } from "./attested.js";
76
- import type { OpenApiDoc } from "./codegen_openapi.js";
77
76
  import type { Directive } from "./directive.js";
78
77
  import type { AggregateHandle, AggregateInvariantFn, AggregateInvariantVerdict } from "./aggregate.js";
79
78
  import type { Ports } from "./ctx.js";
@@ -178,18 +177,6 @@ export interface EngineEntryConfig {
178
177
  * `sideEffects: false` (a bare side-effect import would be tree-shaken away).
179
178
  */
180
179
  readonly seeds?: readonly unknown[];
181
- /**
182
- * Compiler-derived self-description. It is a presentation projection held inside the separately
183
- * routed `describe` plan, never executable law IR. Ordinary offer/read plans omit it completely,
184
- * so OpenAPI/JSON-Schema generation cannot sit on their parse or readiness flame path.
185
- */
186
- readonly openapiDocument?: OpenApiDoc;
187
- /**
188
- * The package's OpenAPI document title (the compiler's `"<name> (nomos package)"`). Baked into
189
- * the lump so `describe` reproduces the compiler's public description byte-for-byte without
190
- * the kernel knowing the package name. The kernel MAY override it (the workspace-level title).
191
- */
192
- readonly packageTitle?: string;
193
180
  /** Compiler-generated nominal encoders keyed by the exact target Directive singleton. Source law
194
181
  * can name a target by type, while the runtime emits only canonical USDA. */
195
182
  readonly workspaceEffectTargets?: readonly {
@@ -297,7 +284,7 @@ function mergeModules(mods: readonly DomainModuleExports[], domain: string): Dom
297
284
  // without per-piece re-exports (capability_exports.ts).
298
285
  // METERED BIRTH: a `.births()`-declaring domain gains the birth-meter structural exports
299
286
  // (the sanctioning `nomosBirthRecordEnsure` + the meter aggregates) at this same seam, so
300
- // the registry + `describe()` see the exact surface the composed manifest carries.
287
+ // the registry sees the exact surface the composed manifest carries.
301
288
  // ROLE SUGAR: lift every `role(...).emit()` into its grant/revoke/seed directives at the SAME
302
289
  // engine merge point so the registry dispatches them (the compiler saw them via the same expander).
303
290
  return expandRoleExports(
@@ -650,8 +637,6 @@ export interface RegisteredEngine {
650
637
  prepareDomain(domain: string): { complete: boolean; initialized: number; total: number };
651
638
  /** Compiler/tooling probe: force one domain and prove an exact routed offer is registered. */
652
639
  hasDirective(domain: string, directiveId: string): boolean;
653
- /** The G1 self-describe verb: the lump's own merged OpenAPI, derived from its modules. */
654
- describe(opts?: { lawHash?: string; title?: string }): OpenApiDoc;
655
640
  }
656
641
 
657
642
  /**
@@ -1155,42 +1140,13 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
1155
1140
  };
1156
1141
  }
1157
1142
 
1158
- /**
1159
- * THE SELF-DESCRIBE VERB (G1) — the lump's OWN OpenAPI, derived from the SAME modules it
1160
- * dispatches. The kernel selects the content-addressed describe plan from the native USD
1161
- * package in custody, so the workspace's API surface is a pure,
1162
- * deterministic function of installed law — NOT a host-staged blob. Its public `paths` reproduce
1163
- * the public description the compiler derived; describe also carries a private, law-derived
1164
- * framework routing manifest which the host consumes internally and never publishes.
1165
- * `opts.lawHash`/`opts.title` are injected by the kernel at describe-time (the version stamp +
1166
- * package title), matching the compiler's `domainHash`/`"<name> (nomos package)"`.
1167
- */
1168
- function describe(opts?: { lawHash?: string; title?: string }): OpenApiDoc {
1169
- const document = config.openapiDocument;
1170
- if (document === undefined) {
1171
- throw new Error("engine describe: selected plan has no compiled OpenAPI projection");
1172
- }
1173
- const lawHash = opts?.lawHash;
1174
- const title = opts?.title ?? config.packageTitle ?? document.info["title"];
1175
- const priorInfo = document.info;
1176
- const info: Record<string, unknown> = {
1177
- ...priorInfo,
1178
- ...(title === undefined ? {} : { title }),
1179
- version: lawHash === undefined ? (priorInfo["version"] ?? "0") : lawHash.slice(0, 12),
1180
- };
1181
- if (lawHash === undefined) delete info["x-nomos-law-hash"];
1182
- else info["x-nomos-law-hash"] = lawHash;
1183
- return { ...document, info };
1184
- }
1185
-
1186
1143
  // The engine evals the bundle as a classic script and FREEZES globalThis before
1187
- // dispatch — assign all entries NOW, at top-level eval. `describe` is the G1 self-
1188
- // describe verb (the kernel `describe` run mode evals `globalThis.describe(opts)`).
1144
+ // dispatch — assign all entries NOW, at top-level eval. There is no `describe` entry: the
1145
+ // kernel derives a workspace's description from its installed law (CAP-658).
1189
1146
  (globalThis as { plan?: unknown }).plan = plan;
1190
1147
  (globalThis as { planReport?: unknown }).planReport = planReport;
1191
1148
  (globalThis as { __nomosPrepareDomain?: unknown }).__nomosPrepareDomain = prepareDomain;
1192
1149
  (globalThis as { __nomosHasDirective?: unknown }).__nomosHasDirective = hasDirective;
1193
- (globalThis as { describe?: unknown }).describe = describe;
1194
1150
 
1195
- return { plan, planReport, prepareDomain, hasDirective, describe } as RegisteredEngine;
1151
+ return { plan, planReport, prepareDomain, hasDirective } as RegisteredEngine;
1196
1152
  }
@@ -2,21 +2,27 @@
2
2
  * Canonicalise esbuild's non-semantic install-topology noise before the executable law is hashed.
3
3
  *
4
4
  * A workspace dependency is labelled `../../dsl/src/...`; the same published dependency is labelled
5
- * `node_modules/@githolon/dsl/src/...`. Esbuild can also leave either one or two newlines after its
5
+ * `node_modules/@githolon/dsl/src/...`. A dependency reached through a symlinked `node_modules` (a git
6
+ * worktree links the main checkout's) is labelled by its real path, climbing out and back down through
7
+ * another directory: `../../../../platform/nomos2/node_modules/zod/...`. Everything up to the last
8
+ * `node_modules/` in an esbuild label describes the install, never the law. Esbuild can also leave either one or two newlines after its
6
9
  * `"use strict"` prologue. None of those bytes describe business law, so they must not give the same
7
10
  * law two identities.
8
11
  *
9
12
  * Replacements are deliberately restricted to esbuild's generated comment/function labels. Tenant
10
13
  * strings and executable imports remain byte-significant.
11
14
  */
15
+ /** An esbuild label's install prefix: an optional climb (`../`), any directories, then `node_modules/`. */
16
+ const INSTALLED = String.raw`(?:(?:\.\.\/)+(?:[^\s"/]+\/)*?)?node_modules\/`;
17
+
12
18
  export function canonicalizeEngineJavascript(source: string): string {
13
19
  const body = source
14
20
  .replace(/\r\n?/g, "\n")
15
21
  .replace(/^[ \t]*"use strict";[ \t]*(?:\n|$)/gm, "")
16
- .replace(/^(\s*\/\/ )(?:(?:\.\.\/)+dsl\/src\/|node_modules\/@githolon\/dsl\/src\/)/gm, "$1@githolon/dsl/src/")
17
- .replace(/"(?:(?:\.\.\/)+dsl\/src\/|node_modules\/@githolon\/dsl\/src\/)([^"]+)"\(\) \{/g, '"@githolon/dsl/src/$1"() {')
18
- .replace(/^(\s*\/\/ )(?:(?:\.\.\/)+node_modules\/zod\/|node_modules\/zod\/)/gm, "$1zod/")
19
- .replace(/"(?:(?:\.\.\/)+node_modules\/zod\/|node_modules\/zod\/)([^"]+)"\(\) \{/g, '"zod/$1"() {')
22
+ .replace(new RegExp(`^(\\s*\\/\\/ )(?:(?:\\.\\.\\/)+dsl\\/src\\/|${INSTALLED}@githolon\\/dsl\\/src\\/)`, "gm"), "$1@githolon/dsl/src/")
23
+ .replace(new RegExp(`"(?:(?:\\.\\.\\/)+dsl\\/src\\/|${INSTALLED}@githolon\\/dsl\\/src\\/)([^"]+)"\\(\\) \\{`, "g"), '"@githolon/dsl/src/$1"() {')
24
+ .replace(new RegExp(`^(\\s*\\/\\/ )${INSTALLED}zod\\/`, "gm"), "$1zod/")
25
+ .replace(new RegExp(`"${INSTALLED}zod\\/([^"]+)"\\(\\) \\{`, "g"), '"zod/$1"() {')
20
26
  .replace(/\n[ \t]*\n/g, "\n")
21
27
  .trim();
22
28
  return `"use strict";\n${body}\n`;
@@ -25,7 +31,7 @@ export function canonicalizeEngineJavascript(source: string): string {
25
31
  /** Remove the same install-topology label from the sealed generated application bundle. */
26
32
  export function canonicalizeGeneratedClientJavascript(source: string): string {
27
33
  return source.replace(
28
- /^(\s*\/\/ )(?:(?:\.\.\/)+dsl\/runtime\/|node_modules\/@githolon\/dsl\/runtime\/)/gm,
34
+ new RegExp(`^(\\s*\\/\\/ )(?:(?:\\.\\.\\/)+dsl\\/runtime\\/|${INSTALLED}@githolon\\/dsl\\/runtime\\/)`, "gm"),
29
35
  "$1@githolon/dsl/runtime/",
30
36
  );
31
37
  }
package/src/fields.ts CHANGED
@@ -243,6 +243,97 @@ export function isGitObjectIdSchema(schema: z.ZodTypeAny): boolean {
243
243
  return false;
244
244
  }
245
245
 
246
+ // ── TYPED IDS (aggregate fields) ─────────────────────────────────────────────────────────────────────
247
+ // An identifier is never a plain string. `t.brandedId(Name)` and `t.uuid()` are the aggregate-field forms of
248
+ // the payload-side `brandedId(...)` schema: wire-identical to a string, nominal at compile time (a plain
249
+ // `string` is not assignable), and recognised by codegen, which emits the branded type in both generated
250
+ // clients. `t.ref(Aggregate)` is the aggregate-field form of `idOf(Aggregate)`. A birth key MUST be one of
251
+ // these (see `births.ts`).
252
+
253
+ declare const NOMOS_TYPED_ID: unique symbol;
254
+ /** A nominal id: a string at run time and on the wire, a distinct type per `Brand` at compile time. */
255
+ export type TypedId<Brand extends string> = string & { readonly [NOMOS_TYPED_ID]: Brand };
256
+ /** The brand a `t.uuid()` field carries. */
257
+ export type Uuid = TypedId<"Uuid">;
258
+
259
+ /** The same global symbol `value_object.ts` stamps on `brandedId(...)` payload schemas (kept as `Symbol.for`
260
+ * here because value_object.ts imports this module). */
261
+ const BRANDED_ID_SYMBOL: unique symbol = Symbol.for("nomos.brandedId") as never;
262
+ /** Marks a `t.uuid()` schema, so a birth key can be classified as a uuid without reading names. */
263
+ export const NOMOS_UUID_BRAND: unique symbol = Symbol.for("nomos.uuid");
264
+
265
+ const TYPED_ID_BRAND_RE = /^[A-Z][A-Za-z0-9_]*$/;
266
+ /** Canonical lowercase RFC 9562 text form (any version) — the one spelling a `t.uuid()` admits. */
267
+ export const CANONICAL_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
268
+ /** The longest id (UTF-8 bytes) a typed id admits — mirrored by the kernel's birth-key shape check. */
269
+ export const TYPED_ID_MAX_BYTES = 256;
270
+
271
+ /** Why `value` is not a well-formed typed id value (`undefined` when it is). One rule set, mirrored by
272
+ * the kernel (`kernel/src/birth_stage.rs` `typed_birth_key_refusal`): a non-empty string of at most
273
+ * {@link TYPED_ID_MAX_BYTES} UTF-8 bytes with no control character (U+0000–U+001F, U+007F — which also keeps
274
+ * the unit separator that delimits `(namespace, key)` out of every key). */
275
+ export function typedIdShapeRefusal(value: unknown): string | undefined {
276
+ if (typeof value !== "string") return `an id is a string; got ${value === null ? "null" : typeof value}`;
277
+ if (value.length === 0) return "an id is non-empty";
278
+ if (utf8ByteLength(value) > TYPED_ID_MAX_BYTES) {
279
+ return `an id is at most ${TYPED_ID_MAX_BYTES} UTF-8 bytes`;
280
+ }
281
+ // eslint-disable-next-line no-control-regex
282
+ if (/[\u0000-\u001f\u007f]/.test(value)) return "an id carries no control character";
283
+ return undefined;
284
+ }
285
+
286
+ /** UTF-8 length of a JS string, counted by code point. The sealed engine (QuickJS) has no TextEncoder, and
287
+ * this rule runs inside law plans and payload validation there. A lone surrogate counts 3 bytes (U+FFFD). */
288
+ function utf8ByteLength(value: string): number {
289
+ let bytes = 0;
290
+ for (const char of value) {
291
+ const code = char.codePointAt(0)!;
292
+ bytes += code < 0x80 ? 1 : code < 0x800 ? 2 : code < 0x10000 ? 3 : 4;
293
+ }
294
+ return bytes;
295
+ }
296
+
297
+ function typedIdSchema(): z.ZodString {
298
+ return z.string().superRefine((value, ctx) => {
299
+ const refusal = typedIdShapeRefusal(value);
300
+ if (refusal !== undefined) ctx.addIssue({ code: "custom", message: refusal });
301
+ });
302
+ }
303
+
304
+ /**
305
+ * Make a typed id VALUE of `brand` from its wire text, checking the shared shape. This is the one
306
+ * sanctioned door from text to an id (a constant the law owns, a value read off another system); a
307
+ * `"Uuid"` brand also requires the canonical uuid spelling. Never cast a string to {@link TypedId}.
308
+ */
309
+ export function typedId<const Brand extends string>(brand: Brand, value: string): TypedId<Brand> {
310
+ if (!TYPED_ID_BRAND_RE.test(brand)) {
311
+ throw new Error(`typedId: '${brand}' must be a capitalized identifier (the brand's type name)`);
312
+ }
313
+ const refusal = typedIdShapeRefusal(value);
314
+ if (refusal !== undefined) throw new Error(`typedId(${brand}): ${refusal}`);
315
+ if (brand === "Uuid" && !CANONICAL_UUID_RE.test(value)) {
316
+ throw new Error("typedId(Uuid): expected a canonical lowercase uuid");
317
+ }
318
+ return value as TypedId<Brand>;
319
+ }
320
+
321
+ /** Whether a schema is a `t.uuid()` id (through omission/null wrappers). */
322
+ export function isUuidIdSchema(schema: z.ZodTypeAny): boolean {
323
+ let current: z.ZodTypeAny | undefined = schema;
324
+ for (let hops = 0; current !== undefined && hops < 16; hops++) {
325
+ if ((current as unknown as Record<symbol, unknown>)[NOMOS_UUID_BRAND] === true) return true;
326
+ const raw = current as unknown as {
327
+ _def?: { type?: unknown; innerType?: z.ZodTypeAny };
328
+ def?: { type?: unknown; innerType?: z.ZodTypeAny };
329
+ };
330
+ const def = raw._def ?? raw.def;
331
+ if (def?.type !== "optional" && def?.type !== "default" && def?.type !== "nullable") return false;
332
+ current = def.innerType;
333
+ }
334
+ return false;
335
+ }
336
+
246
337
  export const nomosRefValueSchema: z.ZodType<NomosRefValue> = z.object({
247
338
  workspace: z.string().min(1),
248
339
  type: z.string().min(1),
@@ -1107,6 +1198,24 @@ export const t = {
1107
1198
  gitObjectId(): Field<GitObjectId, "string"> {
1108
1199
  return makeField<GitObjectId, "string">("string", gitObjectId(), FirstWriterWins);
1109
1200
  },
1201
+ /** A NOMINAL id field: `estateId: t.brandedId("EstateId")`. Wire-identical to a string; the DSL value
1202
+ * type is {@link TypedId}`<Name>` (a plain string is a compile error) and both generated clients emit
1203
+ * the branded `Name` type. Values come from a payload of the same brand or {@link typedId}. */
1204
+ brandedId<const Name extends string>(name: Name): Field<TypedId<Name>, "string"> {
1205
+ if (!TYPED_ID_BRAND_RE.test(name)) {
1206
+ throw new Error(`t.brandedId: '${name}' must be a capitalized identifier — it becomes the generated type's name.`);
1207
+ }
1208
+ const schema = typedIdSchema();
1209
+ Object.defineProperty(schema, BRANDED_ID_SYMBOL, { value: name, enumerable: false });
1210
+ return makeField<TypedId<Name>, "string">("string", schema as unknown as z.ZodType<TypedId<Name>>, FirstWriterWins);
1211
+ },
1212
+ /** A uuid id field in its canonical lowercase spelling. Generated clients emit the branded `Uuid`. */
1213
+ uuid(): Field<Uuid, "string"> {
1214
+ const schema = z.string().regex(CANONICAL_UUID_RE, "expected a canonical lowercase uuid");
1215
+ Object.defineProperty(schema, BRANDED_ID_SYMBOL, { value: "Uuid", enumerable: false });
1216
+ Object.defineProperty(schema, NOMOS_UUID_BRAND, { value: true, enumerable: false });
1217
+ return makeField<Uuid, "string">("string", schema as unknown as z.ZodType<Uuid>, FirstWriterWins);
1218
+ },
1110
1219
  int(rules: IntRules = {}): Field<number, "int"> {
1111
1220
  for (const [name, value] of [["minimum", rules.minimum], ["maximum", rules.maximum]] as const) {
1112
1221
  if (value !== undefined && !Number.isSafeInteger(value)) {
@@ -180,11 +180,13 @@ function emitValue(value: PlainValue, primName: string, indent: number, key?: st
180
180
  // general USD parser: it reads `def Scope "<name>" { … }` nesting and `custom uniform <type>
181
181
  // nomos:<attr> = <literal>` attributes, which is all this layer ever contains.
182
182
 
183
- interface Prim {
183
+ /** One prim of the exact typed subset this module writes (also the baseline artifact's host format). */
184
+ export interface IdentityPrim {
184
185
  readonly name: string;
185
186
  readonly attrs: Record<string, unknown>;
186
- readonly children: Prim[];
187
+ readonly children: IdentityPrim[];
187
188
  }
189
+ type Prim = IdentityPrim;
188
190
 
189
191
  // Attributes are written bare when the `NomosEntity` schema declares their type, and with
190
192
  // `custom uniform` only where a prim carries something the schema does not describe.
@@ -437,9 +439,14 @@ function readEntityForest(
437
439
  }
438
440
  }
439
441
 
440
- /** Render the committed identity layer. Deterministic: every map is emitted in sorted key order. */
441
- export function serializeIdentityUsda(lock: CompilerIdentityModel): string {
442
+ /**
443
+ * Render the identity scopes (`Entities`, `ValueContracts`, `Domains`) at `indent`. The committed
444
+ * identity layer hosts them under `NomosIdentity`; a released baseline (baseline_usda.ts) hosts the
445
+ * same scopes under its own root so continuity has exactly one encoding.
446
+ */
447
+ export function renderIdentityScopes(lock: CompilerIdentityModel, indent: number): string[] {
442
448
  if (lock.identity !== undefined) validateLawIdentityRegistry(lock.identity);
449
+ const p = pad(indent);
443
450
  const entities = Object.entries(lock.identity?.entities ?? {}).sort(([l], [r]) => (l < r ? -1 : l > r ? 1 : 0));
444
451
  const domains = Object.entries(lock.domains).sort(([l], [r]) => (l < r ? -1 : l > r ? 1 : 0));
445
452
  const contracts = new Map<string, unknown>();
@@ -456,7 +463,45 @@ export function serializeIdentityUsda(lock: CompilerIdentityModel): string {
456
463
  renderedContracts.set(field.contract, rendered);
457
464
  }
458
465
  }
466
+ return [
467
+ `${p}def Scope "Entities"`,
468
+ `${p}{`,
469
+ ...emitEntityForest(Object.fromEntries(entities), indent + 4),
470
+ `${p}}`,
471
+ `${p}def Scope "ValueContracts"`,
472
+ `${p}{`,
473
+ ...[...contracts.entries()].sort(([l], [r]) => (l < r ? -1 : l > r ? 1 : 0)).flatMap(([identity, shape]) => [
474
+ `${p} def Scope ${quote(`c_${identity}`)}`,
475
+ `${p} {`,
476
+ ...emitValue(shape, "Shape", indent + 8),
477
+ `${p} }`,
478
+ ]),
479
+ `${p}}`,
480
+ `${p}def Scope "Domains"`,
481
+ `${p}{`,
482
+ ...domains.flatMap(([name, domain]) => {
483
+ const aggregates = Object.entries(domain.stableIds.aggregates).sort(([l], [r]) => (l < r ? -1 : l > r ? 1 : 0));
484
+ return [
485
+ `${p} def Scope ${quote(`d_${domain.stableIds.domain}`)}`,
486
+ `${p} {`,
487
+ `${p} custom uniform string nomos:name = ${quote(name)}`,
488
+ `${p} custom uniform string nomos:sid = ${quote(domain.stableIds.domain)}`,
489
+ ...(domain.schemas === undefined ? [] : emitValue(domain.schemas, "Schemas", indent + 8)),
490
+ ...(aggregates.length === 0 ? [] : [
491
+ `${p} def Scope "Aggregates"`,
492
+ `${p} {`,
493
+ ...aggregates.flatMap(([label, aggregate]) => emitAggregate(label, aggregate, indent + 12)),
494
+ `${p} }`,
495
+ ]),
496
+ `${p} }`,
497
+ ];
498
+ }),
499
+ `${p}}`,
500
+ ];
501
+ }
459
502
 
503
+ /** Render the committed identity layer. Deterministic: every map is emitted in sorted key order. */
504
+ export function serializeIdentityUsda(lock: CompilerIdentityModel): string {
460
505
  return [
461
506
  "#usda 1.0",
462
507
  "(",
@@ -470,65 +515,30 @@ export function serializeIdentityUsda(lock: CompilerIdentityModel): string {
470
515
  "",
471
516
  'def Scope "NomosIdentity"',
472
517
  "{",
473
- ' def Scope "Entities"',
474
- " {",
475
- ...emitEntityForest(Object.fromEntries(entities), 8),
476
- " }",
477
- ' def Scope "ValueContracts"',
478
- " {",
479
- ...[...contracts.entries()].sort(([l], [r]) => (l < r ? -1 : l > r ? 1 : 0)).flatMap(([identity, shape]) => [
480
- ` def Scope ${quote(`c_${identity}`)}`,
481
- " {",
482
- ...emitValue(shape, "Shape", 12),
483
- " }",
484
- ]),
485
- " }",
486
- ' def Scope "Domains"',
487
- " {",
488
- ...domains.flatMap(([name, domain]) => {
489
- const aggregates = Object.entries(domain.stableIds.aggregates).sort(([l], [r]) => (l < r ? -1 : l > r ? 1 : 0));
490
- return [
491
- ` def Scope ${quote(`d_${domain.stableIds.domain}`)}`,
492
- " {",
493
- ` custom uniform string nomos:name = ${quote(name)}`,
494
- ` custom uniform string nomos:sid = ${quote(domain.stableIds.domain)}`,
495
- ...(domain.schemas === undefined ? [] : emitValue(domain.schemas, "Schemas", 12)),
496
- ...(aggregates.length === 0 ? [] : [
497
- ' def Scope "Aggregates"',
498
- " {",
499
- ...aggregates.flatMap(([label, aggregate]) => emitAggregate(label, aggregate, 16)),
500
- " }",
501
- ]),
502
- " }",
503
- ];
504
- }),
505
- " }",
518
+ ...renderIdentityScopes(lock, 4),
506
519
  "}",
507
520
  "",
508
521
  ].join("\n");
509
522
  }
510
523
 
511
- /** Read the committed identity layer back into the compiler's in-memory lock model. */
512
- export function parseIdentityUsda(text: string): CompilerIdentityModel {
513
- if (!text.startsWith("#usda")) {
514
- throw new Error(`${NOMOS_IDENTITY_FILENAME} is not a USDA layer`);
515
- }
516
- if (!text.includes(NOMOS_IDENTITY_LAYER_FORMAT)) {
517
- throw new Error(
518
- `${NOMOS_IDENTITY_FILENAME} is not ${NOMOS_IDENTITY_LAYER_FORMAT} — refusing to guess an identity encoding`,
519
- );
520
- }
524
+ /** The `NomosEntity` schema block a layer hosting identity scopes declares once at its top. */
525
+ export const NOMOS_IDENTITY_ENTITY_SCHEMA = NOMOS_ENTITY_SCHEMA;
526
+
527
+ /** Read the root prims of a layer written in this module's typed subset. */
528
+ export function parseIdentityPrims(text: string, filename: string = NOMOS_IDENTITY_FILENAME): IdentityPrim[] {
521
529
  const roots: Prim[] = [];
522
530
  // Skip the layer header before scanning. `customLayerData = { … }` closes with a braced line of
523
531
  // its own, which the prim reader would otherwise treat as the end of the enclosing scope and stop
524
532
  // before reaching the first prim. Root prims sit at column 0, so that is where parsing starts.
525
533
  const lines = text.split(/\r?\n/);
526
534
  const firstPrim = lines.findIndex((line) => /^def\s+Scope\s/.test(line));
527
- if (firstPrim === -1) throw new Error(`${NOMOS_IDENTITY_FILENAME} has no NomosIdentity prim`);
535
+ if (firstPrim === -1) throw new Error(`${filename} has no root prim`);
528
536
  parsePrims(lines.slice(firstPrim), 0, roots);
529
- const root = roots.find((prim) => prim.name === "NomosIdentity");
530
- if (root === undefined) throw new Error(`${NOMOS_IDENTITY_FILENAME} has no NomosIdentity prim`);
537
+ return roots;
538
+ }
531
539
 
540
+ /** Read identity scopes (`Entities`, `ValueContracts`, `Domains`) hosted under `root`. */
541
+ export function readIdentityScopes(root: IdentityPrim, filename: string = NOMOS_IDENTITY_FILENAME): CompilerIdentityModel {
532
542
  const entities: Record<string, LawIdentityEntity> = {};
533
543
  readEntityForest(childNamed(root, "Entities")?.children ?? [], undefined, entities);
534
544
  const identity: LawIdentityRegistry = { v: 1, entities };
@@ -538,7 +548,7 @@ export function parseIdentityUsda(text: string): CompilerIdentityModel {
538
548
  for (const prim of childNamed(root, "ValueContracts")?.children ?? []) {
539
549
  const identity = prim.name.replace(/^c_/u, "");
540
550
  const shape = childNamed(prim, "Shape");
541
- if (!identity || shape === undefined) throw new Error(`${NOMOS_IDENTITY_FILENAME} has an invalid value-contract prim ${prim.name}`);
551
+ if (!identity || shape === undefined) throw new Error(`${filename} has an invalid value-contract prim ${prim.name}`);
542
552
  contracts.set(identity, readValue(shape));
543
553
  }
544
554
 
@@ -569,6 +579,22 @@ export function parseIdentityUsda(text: string): CompilerIdentityModel {
569
579
  return { v: 1, domains, ...(Object.keys(entities).length === 0 ? {} : { identity }) };
570
580
  }
571
581
 
582
+ /** Read the committed identity layer back into the compiler's in-memory lock model. */
583
+ export function parseIdentityUsda(text: string): CompilerIdentityModel {
584
+ if (!text.startsWith("#usda")) {
585
+ throw new Error(`${NOMOS_IDENTITY_FILENAME} is not a USDA layer`);
586
+ }
587
+ if (!text.includes(NOMOS_IDENTITY_LAYER_FORMAT)) {
588
+ throw new Error(
589
+ `${NOMOS_IDENTITY_FILENAME} is not ${NOMOS_IDENTITY_LAYER_FORMAT} — refusing to guess an identity encoding`,
590
+ );
591
+ }
592
+ const roots = parseIdentityPrims(text);
593
+ const root = roots.find((prim) => prim.name === "NomosIdentity");
594
+ if (root === undefined) throw new Error(`${NOMOS_IDENTITY_FILENAME} has no NomosIdentity prim`);
595
+ return readIdentityScopes(root);
596
+ }
597
+
572
598
  /** An empty identity layer — the first compile of a package that has never been compiled. */
573
599
  export function emptyIdentityLock(): CompilerIdentityModel {
574
600
  return { v: 1, domains: {}, identity: emptyLawIdentityRegistry() };
package/src/index.ts CHANGED
@@ -49,6 +49,9 @@ export {
49
49
  forwardRef,
50
50
  requires,
51
51
  OWNER_FIELD,
52
+ BIRTH_KEY_WRITE_ONCE_CODE,
53
+ isBirthKeyWriteOnceRefusal,
54
+ birthKeyWriteOnceFields,
52
55
  type AggregateHandle,
53
56
  type AudienceMethods,
54
57
  type Audience,
@@ -89,6 +92,14 @@ export {
89
92
  isGitObjectIdSchema,
90
93
  NOMOS_SHA256_DIGEST_BRAND,
91
94
  NOMOS_GIT_OBJECT_ID_BRAND,
95
+ // TYPED IDS: `t.brandedId(Name)` / `t.uuid()` / `t.idOf(Agg)` fields carry `TypedId<Brand>` values;
96
+ // `typedId(brand, text)` is the one sanctioned door from text to an id.
97
+ type TypedId,
98
+ type Uuid,
99
+ typedId,
100
+ typedIdShapeRefusal,
101
+ isUuidIdSchema,
102
+ NOMOS_UUID_BRAND,
92
103
  // FIELD FACETS (the endorse model — security_model.md): authority/standing/attribution gate
93
104
  // set() acceptance of PrincipalEvidence; secret/public are the confidentiality lattice.
94
105
  authorityField,
@@ -391,9 +402,14 @@ export {
391
402
  type GenesisChainOrBuilder,
392
403
  type BusinessBirthDecl,
393
404
  type BusinessBirthSpec,
405
+ CHOOSING_A_BIRTH_KEY_DOC,
394
406
  type BusinessBirthPayload,
395
407
  type BirthFactKeys,
396
- type BirthStringFactKeys,
408
+ type BirthKeyFactKeys,
409
+ type BirthKeyType,
410
+ type BirthKeyDeclaration,
411
+ birthKeyTypeOfField,
412
+ birthKeyShapeRefusal,
397
413
  } from "./births.js";
398
414
  // THE PEER-SOVEREIGN ROOT-OF-TRUST (the BirthAttestation cert chain): a home/child law exports
399
415
  // `requireParentAttestedBirth` to declare it requires a parent-attested-birth cert chain (the
@@ -827,6 +843,14 @@ export {
827
843
  seedGenesis,
828
844
  type GenesisWarrantHandle,
829
845
  } from "./framework/genesis.js";
846
+ // TOKEN-FREE FOUNDING (identity target model D22) — the founding record and the kernel-appended birth
847
+ // origin a founded child carries instead of a BirthAttestation: operator tooling reads its lineage by them.
848
+ export {
849
+ FoundingPrincipal,
850
+ FOUNDING_PRINCIPAL_ID,
851
+ BirthOrigin,
852
+ BIRTH_ORIGIN_ID,
853
+ } from "./framework/founding_genesis.js";
830
854
  // E2E ENCRYPTION (slice 4) — the group-key events (RFC 9420 sender-keys on the ledger).
831
855
  export {
832
856
  KeyScope,
package/src/intent.ts CHANGED
@@ -1136,6 +1136,11 @@ class IntentBuilder<I> {
1136
1136
  ? {}
1137
1137
  : {
1138
1138
  birthsChild: true,
1139
+ // The composed intent births through the declared birth's lowered plan, so it carries the same
1140
+ // typed-key declaration (the kernel's `nomos:birthKeyType` era key) as that birth directive.
1141
+ ...(birthSteps[0]?.directive.birthKey === undefined
1142
+ ? {}
1143
+ : { birthKey: birthSteps[0]!.directive.birthKey }),
1139
1144
  ...(bindBusinessBirthPolicy === undefined
1140
1145
  ? {}
1141
1146
  : { [BUSINESS_BIRTH_POLICY_BINDING]: bindBusinessBirthPolicy }),