@githolon/dsl 0.114.0 → 0.116.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/assets/operation-contract.wasm +0 -0
  2. package/dart/CHANGELOG.md +55 -0
  3. package/dart/lib/src/application_framework_calls.dart +7 -1
  4. package/dart/lib/src/console_enrolment.dart +7 -0
  5. package/dart/lib/src/console_http_io.dart +22 -15
  6. package/dart/lib/src/dev_cloud.dart +62 -0
  7. package/dart/lib/src/nms_generated_runtime.dart +1 -0
  8. package/dart/lib/src/operation_receipts.dart +74 -7
  9. package/dart/lib/src/reception/dart_reception.dart +26 -6
  10. package/dart/lib/src/result_usda.dart +64 -27
  11. package/dart/lib/src/runner/node-entry.mjs +4733 -1534
  12. package/dart/lib/src/runtime_v2_transport.dart +3 -0
  13. package/dart/lib/src/sync_status_projection.dart +11 -0
  14. package/dart/pubspec.lock +1 -1
  15. package/dart/pubspec.yaml +2 -2
  16. package/dart/test/console_enrolment_test.dart +47 -0
  17. package/dart/test/kernel_refused_follow_test.dart +36 -0
  18. package/dart/test/model_adoption_test.dart +49 -0
  19. package/dart/test/result_usda_test.dart +67 -0
  20. package/dart/test/runtime_projections_test.dart +41 -0
  21. package/dart_types/CHANGELOG.md +30 -0
  22. package/dart_types/lib/src/approved_model.dart +78 -1
  23. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pb.dart +528 -2
  24. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbenum.dart +24 -0
  25. package/dart_types/lib/src/proto/nomos/runtime/v2/correspondence_desk.pb.dart +1897 -112
  26. package/dart_types/lib/src/proto/nomos/runtime/v2/model_adoption_desk.pb.dart +98 -0
  27. package/dart_types/lib/src/proto/nomos/runtime/v2/observability_desk.pbenum.dart +11 -1
  28. package/dart_types/lib/src/proto/nomos/runtime/v2/records_desk.pb.dart +16 -0
  29. package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pb.dart +43 -2
  30. package/dart_types/lib/src/vo_canonical.dart +185 -0
  31. package/dart_types/pubspec.lock +1 -1
  32. package/dart_types/pubspec.yaml +3 -1
  33. package/dart_types/test/vo_canonical_test.dart +91 -0
  34. package/package.json +4 -2
  35. package/src/aggregate.ts +95 -0
  36. package/src/anonymous_value_interning.ts +205 -0
  37. package/src/baseline.ts +27 -0
  38. package/src/baseline_export.ts +432 -0
  39. package/src/baseline_import.ts +312 -0
  40. package/src/baseline_usda.ts +185 -0
  41. package/src/birth_key_rule.ts +472 -0
  42. package/src/births.ts +207 -24
  43. package/src/build_package.ts +47 -7
  44. package/src/build_provenance.ts +10 -0
  45. package/src/business_birth.ts +50 -4
  46. package/src/check_package_main.ts +50 -6
  47. package/src/codegen_call_proto.ts +5 -18
  48. package/src/codegen_call_usda.ts +6 -3
  49. package/src/codegen_dart.ts +114 -60
  50. package/src/codegen_dart_client.ts +52 -6
  51. package/src/codegen_dart_facade.ts +57 -35
  52. package/src/codegen_dart_framework_calls.ts +20 -1
  53. package/src/codegen_proof.ts +5 -2
  54. package/src/codegen_result_proto.ts +8 -6
  55. package/src/codegen_result_usda.ts +12 -4
  56. package/src/codegen_ts.ts +109 -10
  57. package/src/compatibility_graph.ts +41 -4
  58. package/src/compatibility_usda.ts +11 -1
  59. package/src/compile_package_main.ts +222 -52
  60. package/src/composed_wire_identity.ts +24 -0
  61. package/src/directive.ts +6 -0
  62. package/src/domain_requirements.ts +75 -0
  63. package/src/engine_entry.ts +12 -4
  64. package/src/engine_javascript.ts +12 -6
  65. package/src/fields.ts +109 -0
  66. package/src/identity_usda.ts +80 -103
  67. package/src/index.ts +35 -1
  68. package/src/intent.ts +33 -3
  69. package/src/law_check.ts +34 -0
  70. package/src/law_graph.ts +142 -1
  71. package/src/law_graph_usda.ts +27 -144
  72. package/src/law_layer_prims.ts +174 -0
  73. package/src/law_unit_selection.ts +45 -0
  74. package/src/law_units.ts +7 -6
  75. package/src/law_version.ts +108 -0
  76. package/src/package_config.ts +44 -0
  77. package/src/package_dependencies.ts +8 -0
  78. package/src/point_guard.ts +5 -1
  79. package/src/release_inspection.ts +6 -4
  80. package/src/release_publication.ts +6 -5
  81. package/src/usda_text.ts +78 -3
  82. package/src/value_identity.ts +355 -0
  83. package/src/value_object.ts +35 -4
  84. package/src/wire.ts +2 -0
  85. package/src/wire_paths.ts +17 -0
  86. package/src/workspace_routing.ts +1 -1
@@ -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
+ }
@@ -93,7 +93,8 @@ import {
93
93
  assertApplicationLanguageCoversAuthoredDirectives,
94
94
  executableDirectivesForLanguage,
95
95
  publicOfferRoute,
96
- remapWorkspaceInvariantsForLanguage,
96
+ carryWorkspaceInvariantsToPerformingIntents,
97
+ isBusinessIntent,
97
98
  } from "./intent.js";
98
99
 
99
100
  /** One bundled domain module: a bag of named exports the entry scans by SHAPE. */
@@ -511,9 +512,16 @@ function buildDomainSlice(mod: DomainModuleExports, domain: string): DomainSlice
511
512
  declaredWorkspaceInvariants.push(w);
512
513
  }
513
514
  }
514
- for (const w of applicationLanguage === undefined
515
- ? declaredWorkspaceInvariants
516
- : remapWorkspaceInvariantsForLanguage(declaredWorkspaceInvariants, applicationLanguage)) {
515
+ // CAP-863: the SAME carry rule package composition applies, with the same keep-the-original predicate —
516
+ // an effect's invariant is registered for every routable intent that performs it, every original is kept
517
+ // without a language, and a language keeps an original only while its target is still dispatchable. The
518
+ // registered set feeds the routing table, so it must equal the manifest's set: an extra body would emit a
519
+ // dead invariant route into language law, a missing one would fail closed at dispatch.
520
+ for (const w of carryWorkspaceInvariantsToPerformingIntents(
521
+ declaredWorkspaceInvariants,
522
+ [...dirs.values()].filter(isBusinessIntent),
523
+ (directiveId) => applicationLanguage === undefined || dirs.has(directiveId),
524
+ )) {
517
525
  wsInvariants.set(w.id, w);
518
526
  }
519
527
  // ── DESUGARED write-path preconditions (`.requiresExisting`/`.creates(Agg,idFn)`/`.where`) ──
@@ -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)) {
@@ -12,6 +12,7 @@
12
12
  // NO JSON, INCLUDING FOR OPAQUE DESCRIPTORS. A field's canonical value `shape` and merge `driver`
13
13
  // are compiler-derived structured values. They are written here through the generic typed-USDA value
14
14
  // codec below rather than stringified, so this file has no JSON in it at any depth.
15
+ import { usdaString, usdaUnquote } from "./usda_text.js";
15
16
  import {
16
17
  emptyLawIdentityRegistry,
17
18
  lawEntityKeyParts,
@@ -28,58 +29,8 @@ export const NOMOS_IDENTITY_LAYER_FORMAT = "nomos.identity.v1";
28
29
  /** The committed identity layer's filename, beside the package config. */
29
30
  export const NOMOS_IDENTITY_FILENAME = "nomos.identity.usda";
30
31
 
31
- function quote(value: string): string {
32
- return `"${value.replace(/[\\"\u0000-\u001f]/gu, (character) => {
33
- switch (character) {
34
- case "\\": return "\\\\";
35
- case '"': return '\\"';
36
- case "\n": return "\\n";
37
- case "\r": return "\\r";
38
- case "\t": return "\\t";
39
- case "\b": return "\\b";
40
- case "\f": return "\\f";
41
- default: return `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`;
42
- }
43
- })}"`;
44
- }
45
-
46
- function parseQuoted(raw: string): string {
47
- const value = raw.trim();
48
- if (!value.startsWith('"') || !value.endsWith('"')) {
49
- throw new Error(`expected a quoted USDA string, got ${value}`);
50
- }
51
- let out = "";
52
- for (let index = 1; index < value.length - 1; index += 1) {
53
- const character = value[index]!;
54
- if (character !== "\\") {
55
- out += character;
56
- continue;
57
- }
58
- index += 1;
59
- const escaped = value[index];
60
- if (escaped === undefined || index >= value.length - 1) throw new Error("unterminated USDA escape");
61
- if (escaped === "u") {
62
- const digits = value.slice(index + 1, index + 5);
63
- if (!/^[0-9a-fA-F]{4}$/u.test(digits)) throw new Error(`invalid USDA unicode escape \\u${digits}`);
64
- out += String.fromCharCode(Number.parseInt(digits, 16));
65
- index += 4;
66
- continue;
67
- }
68
- const decoded: Record<string, string> = {
69
- "\\": "\\",
70
- '"': '"',
71
- n: "\n",
72
- r: "\r",
73
- t: "\t",
74
- b: "\b",
75
- f: "\f",
76
- };
77
- const replacement = decoded[escaped];
78
- if (replacement === undefined) throw new Error(`unsupported USDA escape \\${escaped}`);
79
- out += replacement;
80
- }
81
- return out;
82
- }
32
+ const quote = usdaString;
33
+ const parseQuoted = usdaUnquote;
83
34
 
84
35
  function parseStringList(raw: string): string[] {
85
36
  const value = raw.trim();
@@ -180,11 +131,13 @@ function emitValue(value: PlainValue, primName: string, indent: number, key?: st
180
131
  // general USD parser: it reads `def Scope "<name>" { … }` nesting and `custom uniform <type>
181
132
  // nomos:<attr> = <literal>` attributes, which is all this layer ever contains.
182
133
 
183
- interface Prim {
134
+ /** One prim of the exact typed subset this module writes (also the baseline artifact's host format). */
135
+ export interface IdentityPrim {
184
136
  readonly name: string;
185
137
  readonly attrs: Record<string, unknown>;
186
- readonly children: Prim[];
138
+ readonly children: IdentityPrim[];
187
139
  }
140
+ type Prim = IdentityPrim;
188
141
 
189
142
  // Attributes are written bare when the `NomosEntity` schema declares their type, and with
190
143
  // `custom uniform` only where a prim carries something the schema does not describe.
@@ -437,9 +390,14 @@ function readEntityForest(
437
390
  }
438
391
  }
439
392
 
440
- /** Render the committed identity layer. Deterministic: every map is emitted in sorted key order. */
441
- export function serializeIdentityUsda(lock: CompilerIdentityModel): string {
393
+ /**
394
+ * Render the identity scopes (`Entities`, `ValueContracts`, `Domains`) at `indent`. The committed
395
+ * identity layer hosts them under `NomosIdentity`; a released baseline (baseline_usda.ts) hosts the
396
+ * same scopes under its own root so continuity has exactly one encoding.
397
+ */
398
+ export function renderIdentityScopes(lock: CompilerIdentityModel, indent: number): string[] {
442
399
  if (lock.identity !== undefined) validateLawIdentityRegistry(lock.identity);
400
+ const p = pad(indent);
443
401
  const entities = Object.entries(lock.identity?.entities ?? {}).sort(([l], [r]) => (l < r ? -1 : l > r ? 1 : 0));
444
402
  const domains = Object.entries(lock.domains).sort(([l], [r]) => (l < r ? -1 : l > r ? 1 : 0));
445
403
  const contracts = new Map<string, unknown>();
@@ -456,7 +414,45 @@ export function serializeIdentityUsda(lock: CompilerIdentityModel): string {
456
414
  renderedContracts.set(field.contract, rendered);
457
415
  }
458
416
  }
417
+ return [
418
+ `${p}def Scope "Entities"`,
419
+ `${p}{`,
420
+ ...emitEntityForest(Object.fromEntries(entities), indent + 4),
421
+ `${p}}`,
422
+ `${p}def Scope "ValueContracts"`,
423
+ `${p}{`,
424
+ ...[...contracts.entries()].sort(([l], [r]) => (l < r ? -1 : l > r ? 1 : 0)).flatMap(([identity, shape]) => [
425
+ `${p} def Scope ${quote(`c_${identity}`)}`,
426
+ `${p} {`,
427
+ ...emitValue(shape, "Shape", indent + 8),
428
+ `${p} }`,
429
+ ]),
430
+ `${p}}`,
431
+ `${p}def Scope "Domains"`,
432
+ `${p}{`,
433
+ ...domains.flatMap(([name, domain]) => {
434
+ const aggregates = Object.entries(domain.stableIds.aggregates).sort(([l], [r]) => (l < r ? -1 : l > r ? 1 : 0));
435
+ return [
436
+ `${p} def Scope ${quote(`d_${domain.stableIds.domain}`)}`,
437
+ `${p} {`,
438
+ `${p} custom uniform string nomos:name = ${quote(name)}`,
439
+ `${p} custom uniform string nomos:sid = ${quote(domain.stableIds.domain)}`,
440
+ ...(domain.schemas === undefined ? [] : emitValue(domain.schemas, "Schemas", indent + 8)),
441
+ ...(aggregates.length === 0 ? [] : [
442
+ `${p} def Scope "Aggregates"`,
443
+ `${p} {`,
444
+ ...aggregates.flatMap(([label, aggregate]) => emitAggregate(label, aggregate, indent + 12)),
445
+ `${p} }`,
446
+ ]),
447
+ `${p} }`,
448
+ ];
449
+ }),
450
+ `${p}}`,
451
+ ];
452
+ }
459
453
 
454
+ /** Render the committed identity layer. Deterministic: every map is emitted in sorted key order. */
455
+ export function serializeIdentityUsda(lock: CompilerIdentityModel): string {
460
456
  return [
461
457
  "#usda 1.0",
462
458
  "(",
@@ -470,65 +466,30 @@ export function serializeIdentityUsda(lock: CompilerIdentityModel): string {
470
466
  "",
471
467
  'def Scope "NomosIdentity"',
472
468
  "{",
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
- " }",
469
+ ...renderIdentityScopes(lock, 4),
506
470
  "}",
507
471
  "",
508
472
  ].join("\n");
509
473
  }
510
474
 
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
- }
475
+ /** The `NomosEntity` schema block a layer hosting identity scopes declares once at its top. */
476
+ export const NOMOS_IDENTITY_ENTITY_SCHEMA = NOMOS_ENTITY_SCHEMA;
477
+
478
+ /** Read the root prims of a layer written in this module's typed subset. */
479
+ export function parseIdentityPrims(text: string, filename: string = NOMOS_IDENTITY_FILENAME): IdentityPrim[] {
521
480
  const roots: Prim[] = [];
522
481
  // Skip the layer header before scanning. `customLayerData = { … }` closes with a braced line of
523
482
  // its own, which the prim reader would otherwise treat as the end of the enclosing scope and stop
524
483
  // before reaching the first prim. Root prims sit at column 0, so that is where parsing starts.
525
484
  const lines = text.split(/\r?\n/);
526
485
  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`);
486
+ if (firstPrim === -1) throw new Error(`${filename} has no root prim`);
528
487
  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`);
488
+ return roots;
489
+ }
531
490
 
491
+ /** Read identity scopes (`Entities`, `ValueContracts`, `Domains`) hosted under `root`. */
492
+ export function readIdentityScopes(root: IdentityPrim, filename: string = NOMOS_IDENTITY_FILENAME): CompilerIdentityModel {
532
493
  const entities: Record<string, LawIdentityEntity> = {};
533
494
  readEntityForest(childNamed(root, "Entities")?.children ?? [], undefined, entities);
534
495
  const identity: LawIdentityRegistry = { v: 1, entities };
@@ -538,7 +499,7 @@ export function parseIdentityUsda(text: string): CompilerIdentityModel {
538
499
  for (const prim of childNamed(root, "ValueContracts")?.children ?? []) {
539
500
  const identity = prim.name.replace(/^c_/u, "");
540
501
  const shape = childNamed(prim, "Shape");
541
- if (!identity || shape === undefined) throw new Error(`${NOMOS_IDENTITY_FILENAME} has an invalid value-contract prim ${prim.name}`);
502
+ if (!identity || shape === undefined) throw new Error(`${filename} has an invalid value-contract prim ${prim.name}`);
542
503
  contracts.set(identity, readValue(shape));
543
504
  }
544
505
 
@@ -569,6 +530,22 @@ export function parseIdentityUsda(text: string): CompilerIdentityModel {
569
530
  return { v: 1, domains, ...(Object.keys(entities).length === 0 ? {} : { identity }) };
570
531
  }
571
532
 
533
+ /** Read the committed identity layer back into the compiler's in-memory lock model. */
534
+ export function parseIdentityUsda(text: string): CompilerIdentityModel {
535
+ if (!text.startsWith("#usda")) {
536
+ throw new Error(`${NOMOS_IDENTITY_FILENAME} is not a USDA layer`);
537
+ }
538
+ if (!text.includes(NOMOS_IDENTITY_LAYER_FORMAT)) {
539
+ throw new Error(
540
+ `${NOMOS_IDENTITY_FILENAME} is not ${NOMOS_IDENTITY_LAYER_FORMAT} — refusing to guess an identity encoding`,
541
+ );
542
+ }
543
+ const roots = parseIdentityPrims(text);
544
+ const root = roots.find((prim) => prim.name === "NomosIdentity");
545
+ if (root === undefined) throw new Error(`${NOMOS_IDENTITY_FILENAME} has no NomosIdentity prim`);
546
+ return readIdentityScopes(root);
547
+ }
548
+
572
549
  /** An empty identity layer — the first compile of a package that has never been compiled. */
573
550
  export function emptyIdentityLock(): CompilerIdentityModel {
574
551
  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,
@@ -136,7 +147,17 @@ export {
136
147
  type GeoPolygonValue,
137
148
  type GeoMultiPointValue,
138
149
  type ValueObjectUsage,
150
+ valueIdentity,
139
151
  } from "./value_object.js";
152
+ // VALUE IDENTITY (`value_identity.ts`, CAP-853): every value object's `canonical` / `equals` /
153
+ // `digest` implement the versioned `nomos.vo-canonical.v1` encoding (RFC 8785 over the
154
+ // descriptor-judged value); `valueIdentity(schema)` derives them for lists/maps of VOs.
155
+ export {
156
+ VO_CANONICAL_ENCODING,
157
+ VoCanonicalError,
158
+ type ValueIdentity,
159
+ type VoCanonicalEncoding,
160
+ } from "./value_identity.js";
140
161
  export {
141
162
  enumType,
142
163
  enumTypeNameOf,
@@ -391,9 +412,14 @@ export {
391
412
  type GenesisChainOrBuilder,
392
413
  type BusinessBirthDecl,
393
414
  type BusinessBirthSpec,
415
+ CHOOSING_A_BIRTH_KEY_DOC,
394
416
  type BusinessBirthPayload,
395
417
  type BirthFactKeys,
396
- type BirthStringFactKeys,
418
+ type BirthKeyFactKeys,
419
+ type BirthKeyType,
420
+ type BirthKeyDeclaration,
421
+ birthKeyTypeOfField,
422
+ birthKeyShapeRefusal,
397
423
  } from "./births.js";
398
424
  // THE PEER-SOVEREIGN ROOT-OF-TRUST (the BirthAttestation cert chain): a home/child law exports
399
425
  // `requireParentAttestedBirth` to declare it requires a parent-attested-birth cert chain (the
@@ -827,6 +853,14 @@ export {
827
853
  seedGenesis,
828
854
  type GenesisWarrantHandle,
829
855
  } from "./framework/genesis.js";
856
+ // TOKEN-FREE FOUNDING (identity target model D22) — the founding record and the kernel-appended birth
857
+ // origin a founded child carries instead of a BirthAttestation: operator tooling reads its lineage by them.
858
+ export {
859
+ FoundingPrincipal,
860
+ FOUNDING_PRINCIPAL_ID,
861
+ BirthOrigin,
862
+ BIRTH_ORIGIN_ID,
863
+ } from "./framework/founding_genesis.js";
830
864
  // E2E ENCRYPTION (slice 4) — the group-key events (RFC 9420 sender-keys on the ledger).
831
865
  export {
832
866
  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 }),
@@ -1444,17 +1449,42 @@ export function assertApplicationLanguageCoversAuthoredDirectives(
1444
1449
  export function remapWorkspaceInvariantsForLanguage(
1445
1450
  invariants: readonly WorkspaceInvariantDecl[],
1446
1451
  language: ApplicationLanguage
1452
+ ): WorkspaceInvariantDecl[] {
1453
+ return carryWorkspaceInvariantsToPerformingIntents(
1454
+ invariants,
1455
+ language.intents,
1456
+ () => false
1457
+ );
1458
+ }
1459
+
1460
+ /**
1461
+ * THE ONE CARRY RULE for effect invariants (CAP-863). The kernel selects a workspace invariant by
1462
+ * the DISPATCHED directive id (`invariant.on == directive_id`), and a business intent dispatches
1463
+ * under its own id while planning its effects inline. An invariant declared `.on(effect)` therefore
1464
+ * judges an intent only if it is carried onto that intent. Carry it onto EVERY routable business
1465
+ * intent that performs the effect, whether or not the domain declares `language(...)`.
1466
+ *
1467
+ * The original declaration stays on the effect when nothing performs it, or when the caller says the
1468
+ * effect id may still be dispatched (`dispatchable(effect.id)`): a public directive dispatched directly,
1469
+ * or (without a language) a bare id another installed law may dispatch, must still be judged. A
1470
+ * language-closed effect is private, so its original is dropped exactly as before. Package composition
1471
+ * and the sealed engine both call this function with the same predicate, so the manifest and the
1472
+ * executable invariant registry (and its routing table) name exactly the same invariants.
1473
+ */
1474
+ export function carryWorkspaceInvariantsToPerformingIntents(
1475
+ invariants: readonly WorkspaceInvariantDecl[],
1476
+ intents: readonly BusinessIntent<any>[],
1477
+ dispatchable: (directiveId: string) => boolean
1447
1478
  ): WorkspaceInvariantDecl[] {
1448
1479
  const out: WorkspaceInvariantDecl[] = [];
1449
1480
  for (const invariant of invariants) {
1450
- const uses = language.intents.flatMap((candidate) =>
1481
+ const uses = intents.flatMap((candidate) =>
1451
1482
  candidate.steps
1452
1483
  .filter((step) => step.directive.id === invariant.on)
1453
1484
  .map((step) => ({ intent: candidate, step }))
1454
1485
  );
1455
- if (uses.length === 0) {
1486
+ if (uses.length === 0 || dispatchable(invariant.on)) {
1456
1487
  out.push(invariant);
1457
- continue;
1458
1488
  }
1459
1489
  for (const use of uses) {
1460
1490
  const mapFacts = (