@githolon/dsl 0.100.6 → 0.101.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 (82) hide show
  1. package/dart/CHANGELOG.md +6 -0
  2. package/dart/lib/src/dev_cloud.dart +46 -15
  3. package/dart/lib/src/test_harness.dart +78 -16
  4. package/dart/lib/src/webview_bridge.dart +640 -126
  5. package/dart/pubspec.lock +1 -1
  6. package/dart/pubspec.yaml +2 -2
  7. package/dart/test/bundle_version_pairing_test.dart +11 -22
  8. package/dart/test/co2_product_gauntlet_test.dart +228 -0
  9. package/dart/test/errors_test.dart +63 -12
  10. package/dart/test/first_app_gauntlet_test.dart +29 -3
  11. package/dart/test/webview_bridge_test.dart +274 -0
  12. package/dart_types/CHANGELOG.md +6 -0
  13. package/dart_types/lib/src/errors.dart +136 -30
  14. package/dart_types/lib/src/surfaced.dart +48 -2
  15. package/dart_types/lib/src/wire.dart +3 -1
  16. package/dart_types/pubspec.yaml +1 -1
  17. package/dart_types/test/surfaced_test.dart +19 -0
  18. package/package.json +1 -1
  19. package/src/activity.ts +43 -4
  20. package/src/aggregate.ts +132 -22
  21. package/src/attested_core.ts +1 -1
  22. package/src/authoring.ts +119 -6
  23. package/src/birth_meter.ts +4 -4
  24. package/src/birth_meter_ensure.ts +9 -1
  25. package/src/births.ts +53 -22
  26. package/src/build_package.ts +55 -14
  27. package/src/business_birth.ts +48 -7
  28. package/src/codegen_dart.ts +267 -37
  29. package/src/codegen_dart_client.ts +921 -82
  30. package/src/codegen_openapi.ts +28 -5
  31. package/src/codegen_proof.ts +107 -11
  32. package/src/codegen_ts.ts +961 -135
  33. package/src/compile_package_main.ts +155 -45
  34. package/src/conflicts.ts +252 -0
  35. package/src/describe_model.ts +448 -0
  36. package/src/directive.ts +5 -38
  37. package/src/drivers.ts +12 -4
  38. package/src/engine_entry.ts +4 -0
  39. package/src/evolve_preview.ts +72 -11
  40. package/src/fields.ts +201 -55
  41. package/src/framework/application_home.ts +103 -0
  42. package/src/framework/application_home_domain.ts +2 -0
  43. package/src/framework/attested_receipts.ts +2 -2
  44. package/src/framework/auth_provider_law.ts +51 -24
  45. package/src/framework/business_corrections.ts +170 -0
  46. package/src/framework/disclosure.ts +1 -1
  47. package/src/framework/domain_lifecycle.ts +250 -16
  48. package/src/framework/encryption_identity.ts +1 -1
  49. package/src/framework/encryption_scope.ts +21 -13
  50. package/src/framework/era_certification.ts +1 -1
  51. package/src/framework/framework_aggregates.ts +2 -0
  52. package/src/framework/genesis.ts +1 -1
  53. package/src/framework/home.ts +22 -9
  54. package/src/framework/identity.ts +5 -5
  55. package/src/framework/impure_capability.ts +2 -2
  56. package/src/framework/owned_custody.ts +561 -0
  57. package/src/framework/platform_admin.ts +1 -1
  58. package/src/framework/relations.ts +9 -1
  59. package/src/framework/repair.ts +1 -1
  60. package/src/framework/signing_identity.ts +2 -2
  61. package/src/framework/source_roster.ts +1 -1
  62. package/src/framework/workspaces.ts +17 -10
  63. package/src/generated_client_names.ts +39 -0
  64. package/src/index.ts +20 -15
  65. package/src/kernel_capabilities.ts +25 -1
  66. package/src/law_check.ts +4 -1
  67. package/src/manifest.ts +356 -63
  68. package/src/read_projection_manifest.ts +8 -0
  69. package/src/read_projection_namespace.ts +4 -0
  70. package/src/relation.ts +1 -1
  71. package/src/shape_compat.ts +47 -2
  72. package/src/stable_ids.ts +38 -6
  73. package/src/stable_ids_types.ts +30 -0
  74. package/src/surfaced_lint.ts +0 -0
  75. package/src/usd.ts +62 -11
  76. package/src/usd_layers.ts +54 -3
  77. package/src/value_object.ts +23 -19
  78. package/src/wire.ts +1 -0
  79. package/src/wire_encode.ts +48 -4
  80. package/src/workspace_routing.ts +57 -0
  81. package/src/workspace_sharding.ts +9 -9
  82. package/src/certified_read.ts +0 -313
package/src/fields.ts CHANGED
@@ -15,7 +15,7 @@
15
15
  * the value type (via Zod) is preserved.
16
16
  */
17
17
  import { z } from "zod";
18
- import { Conflict, Counter, LastPosition, Lww, type Driver } from "./drivers.js";
18
+ import { Conflict, Counter, FirstWriterWins, LastPosition, type Driver } from "./drivers.js";
19
19
  import type { AggregateHandle } from "./aggregate.js";
20
20
  import type { ContractHandle } from "./contract.js";
21
21
  import type { FieldFacet } from "./principal_evidence.js";
@@ -47,8 +47,9 @@ export type JsonValue =
47
47
  * `nullReason` records why (rides docs/openapi).
48
48
  * The EXPLICIT-CLEAR semantic is ORTHOGONAL, carried by the separate `isClearable` flag (`t.clearable(X)`
49
49
  * + `.clear(field)` / the generated `clearX(...)` lane, lowered to a Set of json `null`) — a field can be
50
- * BOTH clearable and nullable. FRONTEND/IR-ONLY metadata: neither ever enters the kernel wire `Schema`
51
- * (driver-only — see `encodeKernelSchema`), so a field's domainHash is UNCHANGED. Codegen reads
50
+ * BOTH clearable and nullable. Neither enters the driver-only kernel wire `Schema`, but the compiler
51
+ * includes value-shaping semantics in the stable field contract. Changing them therefore changes the
52
+ * field/law identity used to judge whether observations can be merged. Codegen reads
52
53
  * `nullSemantic`/`nullReason`/`isClearable` to shape the tri-state client surface; the runtime enforcement
53
54
  * is the payload zod (no-null refine) + the encode-path null guard (`wire_encode.ts`).
54
55
  */
@@ -212,11 +213,11 @@ export interface ValueObjectDescriptor {
212
213
 
213
214
  /**
214
215
  * The value-object metadata a VO-typed `Field` carries (`ValueObject.field()` sets it).
215
- * FRONTEND/IR-ONLY: it never enters the kernel wire `Schema` (driver-only — see
216
- * `encodeKernelSchema`) nor the canonical manifest, so a VO field's domainHash is
217
- * IDENTICAL to a hand-rolled `t.jsonObject()` field. The additive `nomosValueObjects`
218
- * USD key (built by `composeDomainModule`/`usd.ts` FROM this metadata) is what makes
219
- * the structure visible to tooling.
216
+ * It never enters the driver-only kernel wire `Schema`, but its canonical descriptor is
217
+ * part of the compiler-derived stable field value contract. A VO shape change therefore
218
+ * changes the field/law identity used by schema-aware conflicts. The additive
219
+ * `nomosValueObjects` USD key (built by `composeDomainModule`/`usd.ts` FROM this metadata)
220
+ * also makes the structure visible to tooling.
220
221
  */
221
222
  export interface ValueObjectFieldMeta {
222
223
  /** The VO's declared name (the generated TS interface / Dart class name). */
@@ -232,6 +233,15 @@ export interface ValueObjectFieldMeta {
232
233
  readonly example?: unknown;
233
234
  }
234
235
 
236
+ /** One business-value evolution declared on a field. This is author-time metadata only:
237
+ * the compiler replaces the source field with stable contract identities and a deterministic
238
+ * identity/case table in the law manifest. No function source reaches the runtime. */
239
+ export interface FieldEvolutionDeclaration {
240
+ readonly source: Field;
241
+ readonly mode: "identity" | "cases";
242
+ readonly cases?: readonly { readonly from: unknown; readonly to: unknown }[];
243
+ }
244
+
235
245
  export interface Field<T = unknown, K extends FieldKind = FieldKind> {
236
246
  /** The kind tag. `K` is the LITERAL kind so the query DSL can constrain `.ref`
237
247
  * to ONLY `kind:'ref'` fields at compile time (`t.ref` → `Field<string,'ref'>`).
@@ -249,8 +259,9 @@ export interface Field<T = unknown, K extends FieldKind = FieldKind> {
249
259
  * NULL SEMANTICS (`architecture/null_semantics.md`): what `null` MEANS on this field. Set by the
250
260
  * `t.json(...)` null option and the `t.optional`/`t.clearable`/`t.nullable` wrappers. `undefined`
251
261
  * ⇒ the field carries no null declaration (a scalar/collection whose null-ness never arises); a
252
- * `json` field with no wrapper defaults to `"forbid"` at construction. FRONTEND/IR-ONLY — never in
253
- * the kernel wire `Schema` (driver-only), so it changes NO hash. See {@link NullSemantic}.
262
+ * `json` field with no wrapper defaults to `"forbid"` at construction. It does not change the
263
+ * driver-only kernel wire `Schema`, but it is part of the stable field value contract. See
264
+ * {@link NullSemantic}.
254
265
  */
255
266
  readonly nullSemantic?: NullSemantic;
256
267
  /** The REQUIRED reason string for `nullable`/`explicit-empty` (law documentation — rides docs/openapi).
@@ -260,7 +271,7 @@ export interface Field<T = unknown, K extends FieldKind = FieldKind> {
260
271
  * explicit-clear write (`clear(handle, "field")` / `.clear("field")` / the generated `clearX(...)`
261
272
  * lane), lowered to a Set of json `null`. ORTHOGONAL to `nullSemantic` (a field may be both). A
262
273
  * cleared field reads back as json null, so codegen surfaces it tri-state (see {@link admitsNull}).
263
- * FRONTEND/IR-ONLY — no hash change. */
274
+ * It leaves the kernel driver unchanged but participates in the stable field value contract. */
264
275
  readonly isClearable?: true;
265
276
  /** The AUTHZ relation the GENERATED clear directive requires (`t.clearable(field, { requires })`). When
266
277
  * set, the compose-seam-injected `clear<Agg><Field>` directive carries `.requires(relation)`; when
@@ -296,7 +307,7 @@ export interface Field<T = unknown, K extends FieldKind = FieldKind> {
296
307
  * For a `t.map(inner)` field: the inner VALUE field's kind (e.g. `"string"` for
297
308
  * `t.map(t.string())`, `"json"` for `t.map(t.json())`). The wire DRIVER is
298
309
  * identical for both (`MapOf(Lww)` — a json leaf encodes to a `Str` value just like
299
- * a string), so this changes NO hash; it only tells codegen how to SURFACE each
310
+ * a string), but the inner value kind is part of the stable field value contract. It also tells codegen how to SURFACE each
300
311
  * entry value: a `string`-valued map reads `Map<String,String>` (flat strings),
301
312
  * whereas a `json`-valued map reads `Map<String,Object?>` (each entry JSON-decoded
302
313
  * back to its object) so a value-object map (e.g. a site's `customFields`,
@@ -315,26 +326,26 @@ export interface Field<T = unknown, K extends FieldKind = FieldKind> {
315
326
  * a `scaleX` double or an `estimatedCost` authored as a JSON leaf). `"object"` is the
316
327
  * EXPLICIT assertion "this json leaf is always a JSON OBJECT" (set by `t.jsonObject()`);
317
328
  * `undefined` (a plain `t.json()`) means the shape is unknown/non-object → the
318
- * projection defaults it to the permissive `Object?`. This flag affects ONLY the
319
- * generated frontend projection TYPE — NOT the kernel wire `Schema` (driver-only, see
320
- * `encodeKernelSchema`) — so it changes NO hash (identical to `mapValueKind`/`isOptional`).
329
+ * projection defaults it to the permissive `Object?`. This flag does not change the
330
+ * driver-only kernel wire `Schema`, but it is part of the stable field value contract.
321
331
  */
322
332
  readonly jsonShape?: "object";
323
333
  /**
324
334
  * For a `json`-kind field lowered from a NAMED VALUE OBJECT (`ValueObject.field()`):
325
335
  * the VO metadata (name + canonical descriptor + nested-VO dependencies). Drives the
326
336
  * generated TS interface / Dart class and the additive `nomosValueObjects` USD key.
327
- * FRONTEND/IR-ONLY — never in the kernel wire `Schema` (driver-only), so the field's
328
- * canonical-manifest bytes are identical to a plain `t.jsonObject()` (see
329
- * {@link ValueObjectFieldMeta}). `undefined` for every non-VO field.
337
+ * It never changes the driver-only kernel wire `Schema`; its canonical descriptor does
338
+ * participate in the stable field value contract (see {@link ValueObjectFieldMeta}).
339
+ * `undefined` for every non-VO field.
330
340
  */
331
341
  readonly valueObject?: ValueObjectFieldMeta;
332
342
  /**
333
343
  * For a `t.map(inner)` whose INNER value is a NAMED VALUE OBJECT (`t.map(VO.field())`):
334
344
  * the inner VO's metadata, so codegen surfaces the map as `Map<String, VoClass>` /
335
345
  * `Record<string, VoClass>` (each entry decoded via `VoClass.fromProjected`) instead of
336
- * the raw `Map<String, Object?>` / `Record<string, unknown>`. FRONTEND/IR-ONLY (the wire
337
- * driver is `MapOf(Lww)` exactly as for a plain json-valued map — NO hash change).
346
+ * the raw `Map<String, Object?>` / `Record<string, unknown>`. The wire driver remains
347
+ * `MapOf(Lww)` exactly as for a plain json-valued map, while the VO descriptor participates
348
+ * in the stable field value contract.
338
349
  * `undefined` for a non-map field or a map of scalars.
339
350
  */
340
351
  readonly mapValueVo?: ValueObjectFieldMeta;
@@ -345,8 +356,8 @@ export interface Field<T = unknown, K extends FieldKind = FieldKind> {
345
356
  * `.optional()` so the generated Dart projection type makes it NULLABLE and decodes
346
357
  * a minimal freshly-folded aggregate (DSL codegen GAP 1). COLLECTION fields
347
358
  * (`set`/`map`) are ALWAYS empty-defaultable and never need this. This flag affects
348
- * ONLY the generated frontend type's required-vs-optional — NOT the kernel wire
349
- * `Schema` (which is driver-only, see `encodeKernelSchema`), so it changes no hash.
359
+ * the generated frontend type's required-vs-optional and the stable field value contract;
360
+ * it does not change the driver-only kernel wire `Schema`.
350
361
  */
351
362
  readonly isOptional: boolean;
352
363
  /**
@@ -390,8 +401,11 @@ export interface Field<T = unknown, K extends FieldKind = FieldKind> {
390
401
  * unchanged — the facet governs WRITES, not what's stored.
391
402
  */
392
403
  readonly facet?: FieldFacet;
393
- /** Returns a copy with the chosen driver (kind literal preserved). */
394
- merge(driver: Driver): Field<T, K>;
404
+ /** Business-declared source contracts this field can understand during a gradual rollout.
405
+ * Compiler-owned metadata; generated clients and tenant runtime code never see contract ids. */
406
+ readonly evolutions?: readonly FieldEvolutionDeclaration[];
407
+ /** Returns a copy with the chosen driver while preserving fluent law markers already applied. */
408
+ merge(driver: Driver): this;
395
409
  /** Tag this field's facet, threaded through merge/optional (the canonical form is the
396
410
  * `authorityField(...)`/`attributionField(...)`/… wrappers, which call this). */
397
411
  withFacet<Fa extends FieldFacet>(facet: Fa): Field<T, K> & { readonly facet: Fa };
@@ -411,7 +425,7 @@ export interface Field<T = unknown, K extends FieldKind = FieldKind> {
411
425
  optional(): Field<T | undefined, K>;
412
426
  /** Declare BIRTH-PRESENCE LAW: aggregate() synthesizes a gate invariant requiring this field
413
427
  * present on every event touching the type; read models type it NON-NULLABLE. */
414
- fromBirth(): Field<T, K>;
428
+ fromBirth(): this & { readonly isFromBirth: true };
415
429
  /**
416
430
  * INTERNAL (the `t.optional`/`t.clearable`/`t.nullable` wrappers call this): tag this field's null
417
431
  * semantic (+ optional reason). Threaded through merge/optional like every other flag. Not for
@@ -421,6 +435,11 @@ export interface Field<T = unknown, K extends FieldKind = FieldKind> {
421
435
  /** INTERNAL (`t.clearable` calls this): mark this field EXPLICIT-CLEARABLE (sets {@link Field.isClearable}
422
436
  * + optional {@link Field.clearRequires}). Threaded through merge/optional like every other flag. */
423
437
  clearable(requires?: string): Field<T, K> & { readonly isClearable: true };
438
+ /** Declare how values authored by an earlier field contract become this field's current
439
+ * business value. With no function, the compiler proves identity widening. With a function,
440
+ * the source must be finite (enum/boolean/literal union), so Nomos can exhaustively compile
441
+ * and validate the complete case table rather than executing tenant code during replay. */
442
+ evolvesFrom<From>(source: Field<From>, project?: (value: From) => T): Field<T, K>;
424
443
  /** Phantom to keep T load-bearing for inference. */
425
444
  readonly _t?: T;
426
445
  }
@@ -454,6 +473,7 @@ function makeField<T, K extends FieldKind = FieldKind>(
454
473
  nullReason?: string;
455
474
  isClearable?: true;
456
475
  clearRequires?: string;
476
+ evolutions?: readonly FieldEvolutionDeclaration[];
457
477
  } = {},
458
478
  ): Field<T, K> {
459
479
  const isOptional = extra.isOptional ?? false;
@@ -479,8 +499,11 @@ function makeField<T, K extends FieldKind = FieldKind>(
479
499
  ...(extra.nullReason !== undefined ? { nullReason: extra.nullReason } : {}),
480
500
  ...(extra.isClearable ? { isClearable: true as const } : {}),
481
501
  ...(extra.clearRequires !== undefined ? { clearRequires: extra.clearRequires } : {}),
482
- merge(d: Driver) {
483
- return makeField<T, K>(kind, zod, d, extra);
502
+ ...(extra.evolutions !== undefined && extra.evolutions.length > 0
503
+ ? { evolutions: extra.evolutions }
504
+ : {}),
505
+ merge(d: Driver): typeof f {
506
+ return makeField<T, K>(kind, zod, d, extra) as typeof f;
484
507
  },
485
508
  optional(): Field<T | undefined, K> {
486
509
  if (extra.fromBirth) {
@@ -494,7 +517,7 @@ function makeField<T, K extends FieldKind = FieldKind>(
494
517
  isOptional: true,
495
518
  });
496
519
  },
497
- fromBirth(): Field<T, K> {
520
+ fromBirth(): typeof f & { readonly isFromBirth: true } {
498
521
  // BIRTH-PRESENCE LAW: only scalar Lww-class kinds — a set/map/counter/position reads as
499
522
  // an empty/zero default (presence is meaningless), a hasMany is virtual. Presence is
500
523
  // MONOTONE for the allowed kinds (no op unsets a value leaf), which is exactly what makes
@@ -506,7 +529,9 @@ function makeField<T, K extends FieldKind = FieldKind>(
506
529
  if (isOptional) {
507
530
  throw new Error("a .optional() field cannot be .fromBirth() — birth-presence and optionality contradict.");
508
531
  }
509
- return makeField<T, K>(kind, zod, driver, { ...extra, fromBirth: true });
532
+ return makeField<T, K>(kind, zod, driver, { ...extra, fromBirth: true }) as typeof f & {
533
+ readonly isFromBirth: true;
534
+ };
510
535
  },
511
536
  withNull(semantic: NullSemantic, reason?: string): Field<T, K> {
512
537
  // Threaded through merge/optional like every other flag (it rides `extra`). Reason is carried
@@ -524,6 +549,44 @@ function makeField<T, K extends FieldKind = FieldKind>(
524
549
  ...(requires !== undefined ? { clearRequires: requires } : {}),
525
550
  }) as Field<T, K> & { readonly isClearable: true };
526
551
  },
552
+ evolvesFrom<From>(source: Field<From>, project?: (value: From) => T): Field<T, K> {
553
+ if (source === null || typeof source !== "object" || source.zod === undefined) {
554
+ throw new Error(".evolvesFrom(source, project) needs an earlier Nomos field declaration as its source");
555
+ }
556
+ let declaration: FieldEvolutionDeclaration;
557
+ if (project === undefined) {
558
+ declaration = { source, mode: "identity" };
559
+ } else {
560
+ const values = finiteEvolutionValues(source.zod);
561
+ if (values === undefined) {
562
+ throw new Error(
563
+ `.evolvesFrom(source, project) cannot prove project is total because the source value space is infinite. ` +
564
+ `Use the no-function identity form for a compatible widening, or model the transition as a finite ` +
565
+ `business enum/union so Nomos can compile every case.`,
566
+ );
567
+ }
568
+ const cases = values.map((from) => {
569
+ const projected = project(from as From);
570
+ const parsed = zod.safeParse(projected);
571
+ if (!parsed.success) {
572
+ throw new Error(
573
+ `.evolvesFrom() maps ${JSON.stringify(from)} to a value the target field refuses: ` +
574
+ parsed.error.issues.map((issue) => issue.message).join("; "),
575
+ );
576
+ }
577
+ return { from, to: parsed.data } as const;
578
+ }).sort((a, b) => {
579
+ const left = JSON.stringify(a.from);
580
+ const right = JSON.stringify(b.from);
581
+ return left < right ? -1 : left > right ? 1 : 0;
582
+ });
583
+ declaration = { source, mode: "cases", cases };
584
+ }
585
+ return makeField<T, K>(kind, zod, driver, {
586
+ ...extra,
587
+ evolutions: [...(extra.evolutions ?? []), declaration],
588
+ });
589
+ },
527
590
  encrypted(): Field<T, K> {
528
591
  // Carry the flag through merge/optional (extra is threaded), so `.merge(Lww).encrypted()`
529
592
  // and `.encrypted().merge(Lww)` agree. The wire schema is UNCHANGED (driver-only) — the
@@ -565,6 +628,36 @@ function makeField<T, K extends FieldKind = FieldKind>(
565
628
  return f;
566
629
  }
567
630
 
631
+ /** Enumerate a finite JSON-schema value language. Deliberately conservative: failure to
632
+ * prove finiteness refuses the function form, so a partial transform can never masquerade
633
+ * as rollout coverage. */
634
+ function finiteEvolutionValues(schema: z.ZodTypeAny): unknown[] | undefined {
635
+ let json: unknown;
636
+ try {
637
+ json = z.toJSONSchema(schema);
638
+ } catch {
639
+ return undefined;
640
+ }
641
+ const walk = (node: unknown): unknown[] | undefined => {
642
+ if (node === null || typeof node !== "object") return undefined;
643
+ const o = node as Record<string, unknown>;
644
+ if (Array.isArray(o.enum)) return [...o.enum];
645
+ if (Object.prototype.hasOwnProperty.call(o, "const")) return [o.const];
646
+ if (o.type === "boolean") return [false, true];
647
+ for (const key of ["anyOf", "oneOf"] as const) {
648
+ if (!Array.isArray(o[key])) continue;
649
+ const all = o[key]!.flatMap((arm) => walk(arm) ?? [FINITE_EVOLUTION_UNKNOWN]);
650
+ if (all.includes(FINITE_EVOLUTION_UNKNOWN)) return undefined;
651
+ const byJson = new Map(all.map((value) => [JSON.stringify(value), value]));
652
+ return [...byJson.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)).map(([, value]) => value);
653
+ }
654
+ return undefined;
655
+ };
656
+ return walk(json);
657
+ }
658
+
659
+ const FINITE_EVOLUTION_UNKNOWN = Symbol("nomos-finite-evolution-unknown");
660
+
568
661
  // ── FIELD FACETS (security_model.md — the endorse model) ─────────────────────────────────────
569
662
  // Canonical wrappers that tag a field's facet; `set()` then gates which PrincipalEvidence
570
663
  // conversions it accepts. The field's read/wire type is unchanged — only WRITES are governed.
@@ -615,10 +708,10 @@ function assertNullEligible(field: Field<unknown, FieldKind>, wrapper: "clearabl
615
708
  );
616
709
  }
617
710
  const dk = (field.driver as { kind?: string }).kind;
618
- if (dk !== "Lww" && dk !== "LwwCausal" && dk !== "LwwBy") {
711
+ if (dk !== "FirstWriterWins" && dk !== "Lww" && dk !== "LwwCausal" && dk !== "LwwBy") {
619
712
  throw new Error(
620
- `t.${wrapper}(...) needs an Lww-class field (Lww / LwwCausal / LwwBy) — a null/clear replaces the ` +
621
- `value WHOLESALE, which a '${dk}' merge cannot express. Declare the field Lww-class first.`,
713
+ `t.${wrapper}(...) needs a wholesale scalar field (FirstWriterWins / Lww / LwwCausal / LwwBy) — ` +
714
+ `a null/clear replaces the value WHOLESALE, which a '${dk}' merge cannot express.`,
622
715
  );
623
716
  }
624
717
  if (field.isFromBirth === true) {
@@ -628,18 +721,70 @@ function assertNullEligible(field: Field<unknown, FieldKind>, wrapper: "clearabl
628
721
  }
629
722
  }
630
723
 
724
+ /** Common business validation carried by a string field itself. This is deliberately small: a field
725
+ * declaration is the one source for stored shape, birth input, generated clients and admission. */
726
+ export interface StringRules {
727
+ readonly nonempty?: true;
728
+ readonly length?: number;
729
+ readonly minLength?: number;
730
+ readonly maxLength?: number;
731
+ }
732
+
733
+ /** Common integer validation carried by the field itself. */
734
+ export interface IntRules {
735
+ readonly nonnegative?: true;
736
+ readonly minimum?: number;
737
+ readonly maximum?: number;
738
+ }
739
+
740
+ function naturalRule(value: number | undefined, name: string): void {
741
+ if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) {
742
+ throw new Error(`${name} must be a non-negative safe integer; got ${JSON.stringify(value)}`);
743
+ }
744
+ }
745
+
631
746
  export const t = {
632
- string(): Field<string, "string"> {
633
- return makeField("string", z.string(), Lww);
747
+ string(rules: StringRules = {}): Field<string, "string"> {
748
+ naturalRule(rules.length, "t.string() length");
749
+ naturalRule(rules.minLength, "t.string() minLength");
750
+ naturalRule(rules.maxLength, "t.string() maxLength");
751
+ const minimum = Math.max(rules.nonempty === true ? 1 : 0, rules.minLength ?? 0);
752
+ const maximum = rules.maxLength;
753
+ if (rules.length !== undefined && (rules.length < minimum || (maximum !== undefined && rules.length > maximum))) {
754
+ throw new Error("t.string() length contradicts its minLength/maxLength/nonempty rule");
755
+ }
756
+ if (maximum !== undefined && minimum > maximum) {
757
+ throw new Error("t.string() minLength/nonempty rule exceeds maxLength");
758
+ }
759
+ let schema = z.string();
760
+ if (rules.length !== undefined) schema = schema.length(rules.length);
761
+ else {
762
+ if (minimum > 0) schema = schema.min(minimum);
763
+ if (maximum !== undefined) schema = schema.max(maximum);
764
+ }
765
+ return makeField("string", schema, FirstWriterWins);
634
766
  },
635
- int(): Field<number, "int"> {
636
- return makeField("int", z.number().int(), Lww);
767
+ int(rules: IntRules = {}): Field<number, "int"> {
768
+ for (const [name, value] of [["minimum", rules.minimum], ["maximum", rules.maximum]] as const) {
769
+ if (value !== undefined && !Number.isSafeInteger(value)) {
770
+ throw new Error(`t.int() ${name} must be a safe integer; got ${JSON.stringify(value)}`);
771
+ }
772
+ }
773
+ const minimum = Math.max(rules.nonnegative === true ? 0 : Number.NEGATIVE_INFINITY, rules.minimum ?? Number.NEGATIVE_INFINITY);
774
+ const maximum = rules.maximum;
775
+ if (maximum !== undefined && minimum > maximum) {
776
+ throw new Error("t.int() minimum/nonnegative rule exceeds maximum");
777
+ }
778
+ let schema = z.number().int();
779
+ if (Number.isFinite(minimum)) schema = schema.min(minimum);
780
+ if (maximum !== undefined) schema = schema.max(maximum);
781
+ return makeField("int", schema, FirstWriterWins);
637
782
  },
638
783
  /** A boolean flag (Lww). Stored as the kernel scalar `"true"`/`"false"` and decoded back to a real
639
784
  * JSON boolean on read — so a flag is `t.bool()`, not a 2-value `t.enum`. Counts/filters key on the
640
785
  * same string form. (Not encryptable — only Lww-class scalars are, and bool already is.) */
641
786
  bool(): Field<boolean, "bool"> {
642
- return makeField("bool", z.boolean(), Lww);
787
+ return makeField("bool", z.boolean(), FirstWriterWins);
643
788
  },
644
789
  /** A PN-COUNTER (`Driver::Counter`) — a number that CONVERGES under concurrent edits: two replicas
645
790
  * that each `increment` while offline both land (the merge is the per-replica sum), so nobody's count
@@ -673,7 +818,7 @@ export const t = {
673
818
  return makeField<E[number], "enum">(
674
819
  "enum",
675
820
  z.enum(values) as unknown as z.ZodType<E[number]>,
676
- Lww,
821
+ FirstWriterWins,
677
822
  );
678
823
  },
679
824
  /** A set of items. Defaults to AddWins is NOT assumed — caller picks via .merge; default Lww-on-set is wrong, so we default to Conflict-free AddWins-friendly Lww only when overridden. */
@@ -688,7 +833,7 @@ export const t = {
688
833
  * `json`-valued map (`t.map(t.json())`) as `Map<String,Object?>` (each entry decoded
689
834
  * back to its object — loss-less value-object round-trip) while a `string`-valued map
690
835
  * stays `Map<String,String>`. The wire driver is identical (`MapOf(Lww)` — both encode
691
- * a leaf to `Str`), so this changes no hash. */
836
+ * a leaf to `Str`), while the inner value contract deliberately changes. */
692
837
  map<T>(inner: Field<T>): Field<Record<string, T>, "map"> {
693
838
  return makeField<Record<string, T>, "map">(
694
839
  "map",
@@ -697,8 +842,8 @@ export const t = {
697
842
  {
698
843
  mapValueKind: inner.kind,
699
844
  // A map of a NAMED value object (`t.map(VO.field())`) carries the inner VO's
700
- // metadata so codegen types each entry as the VO class (frontend-only; the wire
701
- // driver is `MapOf(Lww)` either way — no hash change).
845
+ // metadata so codegen types each entry as the VO class. The wire driver is
846
+ // `MapOf(Lww)` either way; the stable field value contract includes the VO shape.
702
847
  ...(inner.valueObject !== undefined ? { mapValueVo: inner.valueObject } : {}),
703
848
  },
704
849
  );
@@ -715,8 +860,8 @@ export const t = {
715
860
  // guard (`wire_encode.ts`) refuses a plan-injected null in the lump on every peer, and a payload
716
861
  // declared with the exported {@link jsonNoNullSchema} refuses a null at `.parse()`. `{ null:
717
862
  // "explicit-empty", reason }` admits nulls as a documented domain value (reason REQUIRED — a
718
- // meaning-free null is the whole footgun); the `nullSemantic` metadata is FRONTEND/IR-ONLY (codegen
719
- // shapes the tri-state client surface from it) and never enters the hashed IR.
863
+ // meaning-free null is the whole footgun); codegen shapes the tri-state client surface from the
864
+ // metadata and the stable field value contract includes it, even though the kernel driver does not.
720
865
  const mode = opts?.null ?? "forbid";
721
866
  if (mode === "explicit-empty") {
722
867
  if (typeof opts?.reason !== "string" || opts.reason.trim() === "") {
@@ -726,12 +871,12 @@ export const t = {
726
871
  `Give a reason, or use the default t.json() which forbids null.`,
727
872
  );
728
873
  }
729
- return makeField<string, "json">("json", z.string(), Lww, {
874
+ return makeField<string, "json">("json", z.string(), FirstWriterWins, {
730
875
  nullSemantic: "explicit-empty",
731
876
  nullReason: opts.reason,
732
877
  }) as unknown as Field<JsonValue, "json">;
733
878
  }
734
- return makeField<string, "json">("json", z.string(), Lww, {
879
+ return makeField<string, "json">("json", z.string(), FirstWriterWins, {
735
880
  nullSemantic: "forbid",
736
881
  }) as unknown as Field<JsonValue, "json">;
737
882
  },
@@ -780,16 +925,17 @@ export const t = {
780
925
  };
781
926
  },
782
927
  /** A JSON-OBJECT value-object leaf: an arbitrary JSON-string leaf (identical wire +
783
- * driver to `t.json()` — a `Str` at op time, so NO hash change) whose decoded value is
928
+ * driver to `t.json()` — a `Str` at op time) whose decoded value is
784
929
  * asserted to ALWAYS be a JSON OBJECT (e.g. `location`, `address`, `blobRef`,
785
930
  * `structuralAssignment`). The v1-FREE projection surfaces it as a STRUCTURED
786
931
  * `Map<String,Object?>` with a STRICT reader (a present-but-non-object value throws —
787
932
  * the strict-decode contract), as opposed to a plain `t.json()` (decoded shape unknown
788
933
  * → permissive `Object?`). Marks `jsonShape:"object"`; the kind stays `"json"`. */
789
934
  jsonObject(): Field<JsonValue, "json"> {
790
- // Same JsonValue authoring widening as `t.json()` (structured value in, `Str` wire out — no hash
791
- // change); the `jsonShape:"object"` marker only steers the read-model to a strict object reader.
792
- return makeField<string, "json">("json", z.string(), Lww, { jsonShape: "object" }) as unknown as Field<JsonValue, "json">;
935
+ // Same JsonValue authoring widening as `t.json()` (structured value in, `Str` wire out).
936
+ // `jsonShape:"object"` also enters the stable field value contract: the generated strict
937
+ // object reader must never be applied to an observation authored as arbitrary JSON.
938
+ return makeField<string, "json">("json", z.string(), FirstWriterWins, { jsonShape: "object" }) as unknown as Field<JsonValue, "json">;
793
939
  },
794
940
  /**
795
941
  * Reference to another aggregate (stored as its id string).
@@ -814,7 +960,7 @@ export const t = {
814
960
  domain?: string;
815
961
  }): Field<string, "ref"> {
816
962
  const isContract = (opts.target as { __isContract?: unknown }).__isContract === true;
817
- return makeField<string, "ref">("ref", z.string(), Conflict, {
963
+ return makeField<string, "ref">("ref", z.string(), FirstWriterWins, {
818
964
  refAggregateId: opts.target.id,
819
965
  foreignRef: {
820
966
  workspaceField: opts.workspace,
@@ -865,7 +1011,7 @@ export const t = {
865
1011
  * value, like a string), overridable via `.merge(...)`.
866
1012
  */
867
1013
  evidence(): Field<EvidenceRefValue, "evidence"> {
868
- return makeField<EvidenceRefValue, "evidence">("evidence", evidenceRefSchema, Lww);
1014
+ return makeField<EvidenceRefValue, "evidence">("evidence", evidenceRefSchema, FirstWriterWins);
869
1015
  },
870
1016
  };
871
1017
 
@@ -890,16 +1036,16 @@ function refImpl(
890
1036
  ): Field<string, "ref"> {
891
1037
  if (typeof a === "string") {
892
1038
  const target = b!;
893
- return makeField<string, "ref">("ref", z.string(), Conflict, {
1039
+ return makeField<string, "ref">("ref", z.string(), FirstWriterWins, {
894
1040
  refWorkspace: a,
895
1041
  refAggregateId: target.id,
896
1042
  });
897
1043
  }
898
1044
  // THUNK form (`t.ref(() => Agg)`) — resolve lazily to break a bidirectional declaration cycle.
899
1045
  if (typeof a === "function") {
900
- return makeField<string, "ref">("ref", z.string(), Conflict, { refAggregateIdThunk: () => a().id });
1046
+ return makeField<string, "ref">("ref", z.string(), FirstWriterWins, { refAggregateIdThunk: () => a().id });
901
1047
  }
902
- return makeField<string, "ref">("ref", z.string(), Conflict, { refAggregateId: a.id });
1048
+ return makeField<string, "ref">("ref", z.string(), FirstWriterWins, { refAggregateId: a.id });
903
1049
  }
904
1050
 
905
1051
  /**
@@ -916,5 +1062,5 @@ export function valueObjectField<T>(
916
1062
  zod: z.ZodType<T>,
917
1063
  valueObject: ValueObjectFieldMeta,
918
1064
  ): Field<T, "json"> {
919
- return makeField<T, "json">("json", zod, Lww, { jsonShape: "object", valueObject });
1065
+ return makeField<T, "json">("json", zod, FirstWriterWins, { jsonShape: "object", valueObject });
920
1066
  }
@@ -0,0 +1,103 @@
1
+ // NOMOS — compiler-owned application/home law for one-business-birth packages.
2
+ //
3
+ // A tenant declaring `birth(Estate, ...)` is declaring a complete product entrypoint,
4
+ // not merely a child recipe. The compiler privately composes this module into the same
5
+ // package so NomosScope.home(application:) can create/recover the signed-in person's home,
6
+ // and that home can immediately call the tenant's generated `birth(...)`.
7
+ //
8
+ // Every declaration is presentation-internal: it remains certified, hash-bearing and
9
+ // routable through the runtime OpenAPI, while TS/Dart/OpenAPI business surfaces omit it.
10
+
11
+ import type { AggregateHandle } from "../aggregate.js";
12
+ import type { Directive } from "../directive.js";
13
+ import type { QueryDecl } from "../query.js";
14
+ import { APPLICATION_HOME_DOMAIN } from "./application_home_domain.js";
15
+ import {
16
+ AuthProvider,
17
+ authProvider,
18
+ authProviderSeedBeforeWarrant,
19
+ seedAuthProvider,
20
+ setAuthProvider,
21
+ } from "./auth_provider_law.js";
22
+ import { GenesisWarrant, seedGenesis } from "./genesis.js";
23
+ import {
24
+ BirthAttestation,
25
+ HomeOwner,
26
+ SharedWithMe,
27
+ birthAttestation,
28
+ birthHomeForDomain,
29
+ homeNameBindsOwner,
30
+ homeRelations,
31
+ parentAttestedBirth,
32
+ pruneShare,
33
+ recordShare,
34
+ seedBirthAttestation,
35
+ seedHomeOwner,
36
+ sharedByWorkspace,
37
+ } from "./home.js";
38
+ import { RelationTuple, relationTuplesByObject } from "./relations.js";
39
+ import {
40
+ SignerDelegation,
41
+ SignerEpoch,
42
+ advanceSignerEpoch,
43
+ enrollDelegation,
44
+ enrollFirstDevice,
45
+ enrollSigner,
46
+ enrollSignerSelf,
47
+ enrollSignerSelfRequiresVerified,
48
+ identityRelations,
49
+ signerDelegationByUser,
50
+ signerEpochCurrent,
51
+ signerEpochStrictlyIncreases,
52
+ } from "./signing_identity.js";
53
+
54
+ export { APPLICATION_HOME_DOMAIN };
55
+
56
+ type Internal<T extends object> = T & { readonly businessBirthInternal: true };
57
+ const internal = <T extends object>(value: T): Internal<T> => ({
58
+ ...value,
59
+ businessBirthInternal: true,
60
+ });
61
+ const aggregate = (value: AggregateHandle): Internal<AggregateHandle> => internal(value);
62
+ const directive = <P>(value: Directive<P>): Internal<Directive<P>> => internal(value);
63
+ const query = (value: QueryDecl): Internal<QueryDecl> => internal(value);
64
+
65
+ // Parent primitive + the law its born home uses.
66
+ export const __nomos_birthHome = directive(birthHomeForDomain(APPLICATION_HOME_DOMAIN));
67
+ export const __nomos_HomeOwner = aggregate(HomeOwner);
68
+ export const __nomos_seedHomeOwner = directive(seedHomeOwner);
69
+ export const __nomos_homeNameBindsOwner = internal(homeNameBindsOwner);
70
+ export const __nomos_homeRelations = internal(homeRelations);
71
+
72
+ // Subject-side discovery/share custody.
73
+ export const __nomos_SharedWithMe = aggregate(SharedWithMe);
74
+ export const __nomos_recordShare = directive(recordShare);
75
+ export const __nomos_pruneShare = directive(pruneShare);
76
+ export const __nomos_sharedByWorkspace = query(sharedByWorkspace);
77
+
78
+ // Warrant, provenance, signer seating, recovery and verifier state.
79
+ export const __nomos_GenesisWarrant = aggregate(GenesisWarrant);
80
+ export const __nomos_seedGenesis = directive(seedGenesis);
81
+ export const __nomos_BirthAttestation = aggregate(BirthAttestation);
82
+ export const __nomos_seedBirthAttestation = directive(seedBirthAttestation);
83
+ export const __nomos_birthAttestation = query(birthAttestation);
84
+ export const __nomos_parentAttestedBirth = internal(parentAttestedBirth);
85
+ export const __nomos_RelationTuple = aggregate(RelationTuple);
86
+ export const __nomos_relationTuplesByObject = query(relationTuplesByObject);
87
+ export const __nomos_identityRelations = internal(identityRelations);
88
+ export const __nomos_enrollSigner = directive(enrollSigner);
89
+ export const __nomos_enrollFirstDevice = directive(enrollFirstDevice);
90
+ export const __nomos_enrollSignerSelf = directive(enrollSignerSelf);
91
+ export const __nomos_enrollSignerSelfRequiresVerified = internal(enrollSignerSelfRequiresVerified);
92
+ export const __nomos_SignerEpoch = aggregate(SignerEpoch);
93
+ export const __nomos_signerEpochCurrent = query(signerEpochCurrent);
94
+ export const __nomos_advanceSignerEpoch = directive(advanceSignerEpoch);
95
+ export const __nomos_signerEpochStrictlyIncreases = internal(signerEpochStrictlyIncreases);
96
+ export const __nomos_SignerDelegation = aggregate(SignerDelegation);
97
+ export const __nomos_enrollDelegation = directive(enrollDelegation);
98
+ export const __nomos_signerDelegationByUser = query(signerDelegationByUser);
99
+ export const __nomos_AuthProvider = aggregate(AuthProvider);
100
+ export const __nomos_seedAuthProvider = directive(seedAuthProvider);
101
+ export const __nomos_authProviderSeedBeforeWarrant = internal(authProviderSeedBeforeWarrant);
102
+ export const __nomos_setAuthProvider = directive(setAuthProvider);
103
+ export const __nomos_authProvider = query(authProvider);
@@ -0,0 +1,2 @@
1
+ /** Compiler-owned dispatch key for the private application/home law. */
2
+ export const APPLICATION_HOME_DOMAIN = "__nomos_application_home_v1";
@@ -75,7 +75,7 @@ export const NomosAttestationReceipt = aggregate("NomosAttestationReceipt", {
75
75
  foldRootAtSeal: t.string().merge(Lww),
76
76
  /** Which read-attestor key signed every member of this batch (`sha256hex(pubkey)`). */
77
77
  signerKeyHash: t.string().merge(Lww),
78
- });
78
+ }).public();
79
79
 
80
80
  /** Audit read: the source's sealed receipt stream, by batch id. */
81
81
  export const attestationReceiptByBatch = query("attestationReceiptByBatch")
@@ -138,7 +138,7 @@ export const NomosSourceDishonor = aggregate("NomosSourceDishonor", {
138
138
  foldRoot: t.string().merge(Lww),
139
139
  /** When the reporter proved it (ms — the reporter's clock, informational). */
140
140
  provenAtMs: t.int().merge(Lww),
141
- });
141
+ }).public();
142
142
 
143
143
  /** Audit read: the folded dishonor facts, by source genesis. */
144
144
  export const sourceDishonorByGenesis = query("sourceDishonorByGenesis")