@githolon/dsl 0.40.0 → 0.42.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.
package/dart/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## 0.31.0
2
+
3
+ - `NomosFieldMeta.fromBirth` — the registry surfaces BIRTH-PRESENCE LAW (`.fromBirth()` fields, tooling ≥0.42): a gate invariant guarantees the field present on every row, and the generated read models type it non-nullable.
4
+
5
+ ## 0.30.0
6
+
7
+ - RUNTIME AGGREGATE METADATA: `NomosAggregateMeta` / `NomosFieldMeta` / `mergeAggregateMeta` (`src/aggregate_meta.dart`) — the support types for the generated per-domain `const Map<String, NomosAggregateMeta> <domain>AggregateMeta` registry (`nomos-compile` ≥0.41 tooling). Generic surfaces (history/audit UIs, op renderers, in-app agents) resolve `NomosAggregateMeta.typeOfId(op.aggregate)` → field kinds, enum wire values, VO/variant names, and the typed `fromJson` decoder — the compiler's knowledge emitted as data, derived from the law's own declarations.
8
+
1
9
  ## 0.29.0
2
10
 
3
11
  - LAW-MINTED IDS RETURN TO THE CALLER: `NomosBridge.offerCreates(...)` → `NomosOfferOutcome{head, created}` — a `.creates` plan mints its aggregate id inside the sealed intent (payloads never carry one); the offer now hands the minted ids back, so generated create methods return the typed id with no query-after round-trip. New types `NomosOfferOutcome` / `NomosCreated`.
@@ -30,6 +30,9 @@ export 'src/offer.dart';
30
30
  export 'src/home.dart';
31
31
  export 'src/subscriptions.dart';
32
32
  export 'src/schema_validation.dart';
33
+ // RUNTIME AGGREGATE METADATA — the generated per-domain registry's support types
34
+ // (lookup by aggregateType for generic surfaces: history UIs, op renderers, agents).
35
+ export 'src/aggregate_meta.dart';
33
36
  // THE WEBVIEW BRIDGE (app-binding boundary) — the async Dart runtime that drives the REAL githolon running
34
37
  // in a WebView, so the generated typed client never needs a `MemoryPersistence` fake.
35
38
  export 'src/webview_bridge.dart';
@@ -0,0 +1,85 @@
1
+ // RUNTIME AGGREGATE METADATA (architecture/dart_codegen_parity.md — the registry slice).
2
+ //
3
+ // Dart is nominal and has no runtime type derivation, so everything the compiler knows about an
4
+ // aggregate's shape is invisible at runtime unless it is EMITTED AS DATA. `nomos-compile`
5
+ // generates one `const Map<String, NomosAggregateMeta>` per domain (keyed by the projection's
6
+ // wire type, e.g. `drawing.Site`), so generic surfaces — history/audit UIs over `historyOf`,
7
+ // undo previews, op renderers, an in-app agent introspecting state — can resolve
8
+ // `(aggregateType, field, rawValue)` to typed knowledge with NO hand-maintained switch.
9
+ //
10
+ // THE LINE THIS HOLDS: this is client-side reflection over the LAW'S DECLARED SHAPE — derived
11
+ // from the same descriptors the package IR carries, never hand-authored, never a second source
12
+ // of truth. If it disagrees with the law, the generator is broken, not the registry.
13
+
14
+ /// Runtime metadata for ONE field of an aggregate — the compiler's knowledge, emitted as data.
15
+ class NomosFieldMeta {
16
+ /// The DSL field kind (`string` | `int` | `double` | `bool` | `enum` | `set` | `map` |
17
+ /// `json` | `ref` | `counter` | `position` | `evidence` | ...).
18
+ final String kind;
19
+
20
+ /// For `enum` kinds: the exact wire values the law's zod validates.
21
+ final List<String>? enumValues;
22
+
23
+ /// For `json` leaves that are DECLARED value objects: the VO / variant name
24
+ /// (the generated Dart class / sealed base of the same name decodes it).
25
+ final String? vo;
26
+
27
+ /// True when [vo] names a `variant(...)` sum type (decode lands on a sealed hierarchy
28
+ /// whose `Unknown` arm carries arms this client's law version doesn't know).
29
+ final bool isVariant;
30
+
31
+ /// For `ref` kinds: the referenced aggregate's wire type, when the law declares it.
32
+ final String? refType;
33
+
34
+ /// True when the field carries BIRTH-PRESENCE LAW (`.fromBirth()`): a gate invariant
35
+ /// guarantees it present on every row, and the generated read model types it non-nullable.
36
+ final bool fromBirth;
37
+
38
+ const NomosFieldMeta({
39
+ required this.kind,
40
+ this.enumValues,
41
+ this.vo,
42
+ this.isVariant = false,
43
+ this.refType,
44
+ this.fromBirth = false,
45
+ });
46
+ }
47
+
48
+ /// Runtime metadata for ONE aggregate, keyed by its projection wire type. [fromRow] is the
49
+ /// generated typed decoder (`<Agg>.fromJson`) — the same one every generated accessor uses,
50
+ /// surfaced here so a runtime-dispatched row still decodes to the REAL typed class.
51
+ class NomosAggregateMeta {
52
+ /// The projection wire type (`<domain>.<Aggregate>`).
53
+ final String type;
54
+
55
+ /// The aggregate's declared label (`Site`).
56
+ final String label;
57
+
58
+ /// Declared field name → its metadata.
59
+ final Map<String, NomosFieldMeta> fields;
60
+
61
+ /// The generated typed decoder (`Site.fromJson`) — returns the generated aggregate class
62
+ /// (typed as [Object] here; cast at the call site, or drive it generically via [fields]).
63
+ final Object Function(Map<String, dynamic> data, [String rowId]) fromRow;
64
+
65
+ const NomosAggregateMeta({
66
+ required this.type,
67
+ required this.label,
68
+ required this.fields,
69
+ required this.fromRow,
70
+ });
71
+
72
+ /// The AGGREGATE WIRE TYPE of a kernel aggregate id: minted ids are
73
+ /// `<domain>.<Type>_<uuidv7>`, so the type is the prefix before the trailing
74
+ /// `_<uuid>` (the same rule `@githolon/client` applies to Create events).
75
+ /// Returns the input unchanged when no uuid suffix is present (already a type).
76
+ static String typeOfId(String aggregateId) =>
77
+ aggregateId.replaceFirst(RegExp(r'_[0-9a-fA-F-]+$'), '');
78
+ }
79
+
80
+ /// Merge per-domain registries into one lookup (a package with several domains). Later maps
81
+ /// win on key collisions — collisions only occur when two domains declare the SAME wire type,
82
+ /// which the compiler already refuses.
83
+ Map<String, NomosAggregateMeta> mergeAggregateMeta(
84
+ Iterable<Map<String, NomosAggregateMeta>> registries) =>
85
+ {for (final r in registries) ...r};
package/dart/pubspec.yaml CHANGED
@@ -11,7 +11,7 @@ description: >-
11
11
  Nomos 2 Dart frontend types — the hand-written kernel wire types plus the
12
12
  code-generated per-domain aggregate + directive-payload types. (De)serializes
13
13
  the EXACT serde JSON the Rust kernel + projection produce/consume.
14
- version: 0.29.0
14
+ version: 0.31.0
15
15
 
16
16
  environment:
17
17
  sdk: ^3.5.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.40.0",
3
+ "version": "0.42.0",
4
4
  "type": "module",
5
5
  "description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
package/src/aggregate.ts CHANGED
@@ -277,6 +277,31 @@ export function aggregate<const Id extends string, F extends Record<string, Fiel
277
277
  .map(([name]) => name)
278
278
  .sort();
279
279
 
280
+ // BIRTH-PRESENCE LAW (`.fromBirth()`, dart_codegen_parity.md): the declared fields become an
281
+ // AUTO-GENERATED aggregate invariant — the SAME #41 gate machinery a hand-written invariant
282
+ // uses, executing on EVERY event touching the type (author, edge admission, verify_chain), so
283
+ // a plan that leaves one absent is a TYPED refusal and the generated read models may type
284
+ // these fields NON-NULLABLE (absence is unrepresentable BY LAW, not by convention). Presence
285
+ // is monotone for the allowed kinds (no op unsets a value leaf), which keeps the claim sound
286
+ // on every later event. Composed BEFORE any user invariant (presence first, then domain law).
287
+ const fromBirthFields = Object.entries(stored)
288
+ .filter(([, f]) => (f as Field).isFromBirth === true)
289
+ .map(([name]) => name)
290
+ .sort();
291
+ const userInvariant = opts?.invariant;
292
+ const composedInvariant: AggregateInvariantFn | undefined =
293
+ fromBirthFields.length > 0
294
+ ? (snapshot) => {
295
+ for (const name of fromBirthFields) {
296
+ const v = snapshot[name];
297
+ if (v === undefined || v === null) {
298
+ return { reject: `${id}.${name} is fromBirth() — every ${id} must carry it from creation (the plan left it unset)` };
299
+ }
300
+ }
301
+ return userInvariant !== undefined ? userInvariant(snapshot) : { accept: true };
302
+ }
303
+ : userInvariant;
304
+
280
305
  // The audience-free DATA shape; the audience is attached below (from the legacy `{visibility}` opt or a
281
306
  // fluent `.public()`/`.readableBy()`/`.owned()` call).
282
307
  const base = {
@@ -284,9 +309,10 @@ export function aggregate<const Id extends string, F extends Record<string, Fiel
284
309
  fields: stored as StoredFields<F>,
285
310
  hasMany: hasMany as HasManyFields<F>,
286
311
  __isAggregateHandle: true as const,
287
- ...(opts?.invariant !== undefined
288
- ? { hasInvariant: true as const, invariant: opts.invariant }
312
+ ...(composedInvariant !== undefined
313
+ ? { hasInvariant: true as const, invariant: composedInvariant }
289
314
  : {}),
315
+ ...(fromBirthFields.length > 0 ? { fromBirthFields } : {}),
290
316
  ...(opts?.invariantDeclassify !== undefined ? { invariantDeclassify: opts.invariantDeclassify } : {}),
291
317
  ...(opts?.cap !== undefined ? { cap: opts.cap } : {}),
292
318
  ...(opts?.global === true ? { global: true } : {}),
@@ -743,9 +743,14 @@ function aggregateFieldSpecs(
743
743
  // decode would crash mid-fold). `.optional()` on the DSL field no longer
744
744
  // changes the read surface; it was nullable in practice — this makes the type
745
745
  // honest. Collections are unchanged (they already empty-default when absent).
746
+ // BIRTH-PRESENCE LAW (`.fromBirth()`): the gate refuses any event that leaves the field
747
+ // absent, so the read model may type it NON-NULLABLE — the one lawful exception to the
748
+ // partial-fold nullability doctrine (absence is unrepresentable by law, not convention).
746
749
  const spec: AggFieldSpec = isCollectionKind(base.kind)
747
750
  ? base
748
- : { ...base, optional: true };
751
+ : f.isFromBirth === true
752
+ ? { ...base, optional: false }
753
+ : { ...base, optional: true };
749
754
  if (used.has(spec.name)) {
750
755
  throw new Error(
751
756
  `aggregate '${agg.id}' field '${spec.wireName}' maps to duplicate Dart field '${spec.name}'`,
@@ -1536,6 +1541,61 @@ function emitUnionClasses(d: ValueObjectDescriptor): string {
1536
1541
  return [base, ...armClasses, unknown].join("\n\n");
1537
1542
  }
1538
1543
 
1544
+ /** THE RUNTIME AGGREGATE-META REGISTRY (dart_codegen_parity.md): one generated
1545
+ * `const Map<String, NomosAggregateMeta>` per domain, keyed by the projection wire type
1546
+ * (`<domain>.<Aggregate>`). Generic surfaces (history/audit UIs, op renderers, in-app agents)
1547
+ * resolve `op.aggregateType` → field kinds / enum wire values / VO+variant names / the typed
1548
+ * `fromJson` decoder — the compiler's knowledge, emitted as data, derived from the SAME
1549
+ * declarations the law carries (never hand-authored; never a second source of truth). */
1550
+ function emitAggregateMetaRegistry(
1551
+ domainKey: string,
1552
+ aggregates: readonly AggregateHandle[],
1553
+ ): string {
1554
+ if (aggregates.length === 0) return "";
1555
+ const entries = aggregates.map((agg) => {
1556
+ const wireType = `${domainKey}.${agg.id}`;
1557
+ const fieldLines = Object.entries(agg.fields as Record<string, Field>).map(([name, f]) => {
1558
+ const parts: string[] = [`kind: '${f.kind}'`];
1559
+ if (f.kind === "enum") {
1560
+ try {
1561
+ const values = zodEnumValues(f.zod as z.ZodTypeAny);
1562
+ parts.push(`enumValues: [${values.map((v) => `'${v}'`).join(", ")}]`);
1563
+ } catch { /* enum without introspectable values — kind alone still serves */ }
1564
+ }
1565
+ const vo = (f as { valueObject?: { name: string; descriptor?: { union?: unknown } } }).valueObject;
1566
+ if (vo !== undefined) {
1567
+ parts.push(`vo: '${vo.name}'`);
1568
+ if (vo.descriptor && (vo.descriptor as { union?: unknown }).union !== undefined) parts.push(`isVariant: true`);
1569
+ }
1570
+ const refTarget = (f as { refAggregateId?: string }).refAggregateId;
1571
+ if (f.kind === "ref" && refTarget !== undefined) {
1572
+ parts.push(`refType: '${domainKey}.${refTarget}'`);
1573
+ }
1574
+ if ((f as { isFromBirth?: true }).isFromBirth === true) parts.push(`fromBirth: true`);
1575
+ return ` '${name}': NomosFieldMeta(${parts.join(", ")}),`;
1576
+ });
1577
+ return [
1578
+ ` '${wireType}': NomosAggregateMeta(`,
1579
+ ` type: '${wireType}',`,
1580
+ ` label: '${agg.id}',`,
1581
+ ` fields: {`,
1582
+ ...fieldLines,
1583
+ ` },`,
1584
+ ` fromRow: ${agg.id}.fromJson,`,
1585
+ ` ),`,
1586
+ ].join("\n");
1587
+ });
1588
+ const mapName = `${dartMemberName(domainKey)}AggregateMeta`;
1589
+ return [
1590
+ `/// RUNTIME lookup of this domain's aggregate shapes by projection wire type`,
1591
+ `/// (\`NomosAggregateMeta.typeOfId(op.aggregate)\` resolves an instance id to its key).`,
1592
+ `/// Derived from the law's own declarations — the compiler's knowledge as const data.`,
1593
+ `const Map<String, NomosAggregateMeta> ${mapName} = {`,
1594
+ ...entries,
1595
+ `};`,
1596
+ ].join("\n");
1597
+ }
1598
+
1539
1599
  function emitValueObjectClass(d: ValueObjectDescriptor): string {
1540
1600
  if (d.union !== undefined) return emitUnionClasses(d);
1541
1601
  if (d.geometry !== undefined) {
@@ -3212,6 +3272,8 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
3212
3272
  ),
3213
3273
  )
3214
3274
  .join("\n\n");
3275
+ // the runtime aggregate-meta registry (lookup by op.aggregateType — generic surfaces).
3276
+ const aggregateMetaBlock = emitAggregateMetaRegistry(domainKey, localAggregates);
3215
3277
  const dirBlock = localDirectives
3216
3278
  .map((d) => emitDirective(d, domainKey, dartTypes))
3217
3279
  .join("\n\n");
@@ -3426,6 +3488,13 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
3426
3488
  `// ---- aggregates (projection types) ----`,
3427
3489
  aggBlock,
3428
3490
  ``,
3491
+ ...(aggregateMetaBlock !== ""
3492
+ ? [
3493
+ `// ---- runtime aggregate metadata (lookup by op.aggregateType) ----`,
3494
+ aggregateMetaBlock,
3495
+ ``,
3496
+ ]
3497
+ : []),
3429
3498
  ...(readModelBlock !== ""
3430
3499
  ? [
3431
3500
  `// ---- generated read models (from aggregate DSL fields) ----`,
package/src/codegen_ts.ts CHANGED
@@ -527,11 +527,12 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
527
527
  }
528
528
  }
529
529
 
530
- out.push(`// ── read models (projection \`data\` shapes; every field is partial-fold optional) ──`, ``);
530
+ out.push(`// ── read models (projection \`data\` shapes; partial-fold optional, EXCEPT .fromBirth()`, `// fields — their presence is gate law, so the type is non-optional) ──`, ``);
531
531
  for (const [typeId, agg] of [...aggByType].sort(([a], [b]) => a.localeCompare(b))) {
532
532
  out.push(`export interface ${typeId}Data {`);
533
533
  for (const [name, field] of Object.entries(agg.fields)) {
534
- out.push(` ${name}?: ${tsTypeOfField(field as Field)};`);
534
+ const required = (field as Field).isFromBirth === true;
535
+ out.push(` ${name}${required ? "" : "?"}: ${tsTypeOfField(field as Field)};`);
535
536
  }
536
537
  for (const extra of extrasByType.get(typeId) ?? []) out.push(extra);
537
538
  out.push(`}`, ``);
@@ -353,6 +353,9 @@ const DART_SUPPORT_SRC = [
353
353
  "offer.dart",
354
354
  "subscriptions.dart",
355
355
  "provider.dart",
356
+ // Runtime aggregate-meta support types (NomosAggregateMeta/NomosFieldMeta) — the generated
357
+ // per-domain registry consts reference them through the barrel.
358
+ "aggregate_meta.dart",
356
359
  // The async WebView bridge runtime (NomosBridge / BridgeTransport) the generated
357
360
  // `<name>.client.dart` drives — the barrel re-exports it, so it must be vendored too.
358
361
  "webview_bridge.dart",
@@ -1091,6 +1094,35 @@ async function main(): Promise<void> {
1091
1094
  }
1092
1095
  }
1093
1096
  if (inferred.length > 0) inferredRenames.push({ domain: identityName, renames: inferred });
1097
+ // ── THE fromBirth EVOLVE GUARD (dart_codegen_parity.md): birth-presence may be declared at a
1098
+ // field's FIRST appearance (fresh sid — every row of the type is born under the invariant) or
1099
+ // carried forward, but NEVER newly added to a sid that existed without it: rows born under the
1100
+ // earlier law may lack the field, and the non-nullable read model would LIE about them. The
1101
+ // lawful adoption path for an existing field is a data migration (backfill every row, then
1102
+ // re-birth/dispositions) — named here so the refusal teaches it. ──
1103
+ if (prior !== undefined) {
1104
+ const priorFromBirthBySid = new Map<string, boolean>();
1105
+ for (const a of Object.values(prior.aggregates)) {
1106
+ for (const f of Object.values(a.fields)) priorFromBirthBySid.set(f.sid, f.fromBirth === true);
1107
+ }
1108
+ for (const agg of mod.aggregates) {
1109
+ const fields = (agg.fields ?? {}) as Record<string, { isFromBirth?: true }>;
1110
+ for (const [fieldName, f] of Object.entries(fields)) {
1111
+ if (f?.isFromBirth !== true) continue;
1112
+ const sid = continuity.aggregates[agg.id]?.fields[fieldName]?.sid;
1113
+ if (sid === undefined) continue; // first appearance — lawful
1114
+ const priorHad = priorFromBirthBySid.get(sid);
1115
+ if (priorHad === false) {
1116
+ throw new Error(
1117
+ `${identityName}.${agg.id}.${fieldName}: .fromBirth() cannot be NEWLY declared on an existing field — ` +
1118
+ `rows born under the earlier law may lack it, and the non-nullable read model would lie about them. ` +
1119
+ `Either declare fromBirth at the field's first appearance, or migrate: backfill every ${agg.id} row, ` +
1120
+ `then adopt via a re-birth (the #58 dispositions lane).`,
1121
+ );
1122
+ }
1123
+ }
1124
+ }
1125
+ }
1094
1126
  // The lockfile records the EMITTED surface (sids + lineage + kinds — the next
1095
1127
  // compile's pair rule reads the kinds), not the bare continuity.
1096
1128
  nextLock.domains[identityName] = {
package/src/fields.ts CHANGED
@@ -154,6 +154,12 @@ export interface Field<T = unknown, K extends FieldKind = FieldKind> {
154
154
  readonly kind: K;
155
155
  readonly zod: z.ZodType<T>;
156
156
  readonly driver: Driver;
157
+ /** BIRTH-PRESENCE LAW (`.fromBirth()`): the aggregate() declaration SYNTHESIZES a real
158
+ * #41 gate invariant requiring this field present on EVERY event touching the type — so
159
+ * the generated read models type it NON-NULLABLE (absence is law-refused, not convention).
160
+ * Scalar Lww-class kinds only; mutually exclusive with `.optional()`. Flag named
161
+ * `isFromBirth` — the `fromBirth()` METHOD sets it (the isOptional/isEncrypted convention). */
162
+ readonly isFromBirth?: true;
157
163
  /** For `t.ref` cross-workspace edges: the foreign workspace id, if any. */
158
164
  readonly refWorkspace?: string;
159
165
  /** For `t.ref` (the target) AND `t.hasMany` (the child type): the related aggregate's wire id. */
@@ -253,6 +259,9 @@ export interface Field<T = unknown, K extends FieldKind = FieldKind> {
253
259
  * schema IS the single source of truth codegen derives required-vs-optional from.
254
260
  */
255
261
  optional(): Field<T | undefined, K>;
262
+ /** Declare BIRTH-PRESENCE LAW: aggregate() synthesizes a gate invariant requiring this field
263
+ * present on every event touching the type; read models type it NON-NULLABLE. */
264
+ fromBirth(): Field<T, K>;
256
265
  /** Phantom to keep T load-bearing for inference. */
257
266
  readonly _t?: T;
258
267
  }
@@ -266,6 +275,7 @@ function makeField<T, K extends FieldKind = FieldKind>(
266
275
  refAggregateId?: string;
267
276
  viaField?: string;
268
277
  isOptional?: boolean;
278
+ fromBirth?: true;
269
279
  mapValueKind?: FieldKind;
270
280
  jsonShape?: "object";
271
281
  valueObject?: ValueObjectFieldMeta;
@@ -289,10 +299,14 @@ function makeField<T, K extends FieldKind = FieldKind>(
289
299
  ...(extra.valueObject !== undefined ? { valueObject: extra.valueObject } : {}),
290
300
  ...(extra.encrypted ? { isEncrypted: true as const } : {}),
291
301
  ...(extra.meta ? { isMeta: true as const } : {}),
302
+ ...(extra.fromBirth ? { isFromBirth: true as const } : {}),
292
303
  merge(d: Driver) {
293
304
  return makeField<T, K>(kind, zod, d, extra);
294
305
  },
295
306
  optional(): Field<T | undefined, K> {
307
+ if (extra.fromBirth) {
308
+ throw new Error("a .fromBirth() field cannot be .optional() — birth-presence and optionality contradict.");
309
+ }
296
310
  // Wrap the Zod once (`.optional()` is idempotent enough — re-wrapping an
297
311
  // already-optional schema stays optional). Carry the flag so codegen reads it
298
312
  // directly without re-introspecting Zod.
@@ -301,6 +315,20 @@ function makeField<T, K extends FieldKind = FieldKind>(
301
315
  isOptional: true,
302
316
  });
303
317
  },
318
+ fromBirth(): Field<T, K> {
319
+ // BIRTH-PRESENCE LAW: only scalar Lww-class kinds — a set/map/counter/position reads as
320
+ // an empty/zero default (presence is meaningless), a hasMany is virtual. Presence is
321
+ // MONOTONE for the allowed kinds (no op unsets a value leaf), which is exactly what makes
322
+ // the non-nullable read type sound.
323
+ const allowed: readonly FieldKind[] = ["string", "int", "bool", "enum", "json", "ref", "evidence"];
324
+ if (!allowed.includes(kind)) {
325
+ throw new Error(`.fromBirth() is not available on a '${kind}' field — collections/CRDT scalars read as defaults; presence law applies to value leaves (${allowed.join("/")}).`);
326
+ }
327
+ if (isOptional) {
328
+ throw new Error("a .optional() field cannot be .fromBirth() — birth-presence and optionality contradict.");
329
+ }
330
+ return makeField<T, K>(kind, zod, driver, { ...extra, fromBirth: true });
331
+ },
304
332
  encrypted(): Field<T, K> {
305
333
  // Carry the flag through merge/optional (extra is threaded), so `.merge(Lww).encrypted()`
306
334
  // and `.encrypted().merge(Lww)` agree. The wire schema is UNCHANGED (driver-only) — the
package/src/manifest.ts CHANGED
@@ -978,7 +978,7 @@ function stableIdsForModule(
978
978
  const aggSid = prior?.sid ?? mintAggregateSid(domainSid, agg.id);
979
979
  const handleFields = (handlesById.get(agg.id)?.fields ?? {}) as Record<
980
980
  string,
981
- { kind?: string } | undefined
981
+ { kind?: string; isFromBirth?: true } | undefined
982
982
  >;
983
983
  const fields: Record<string, StableFieldId> = {};
984
984
  for (const fieldName of Object.keys(agg.schema).sort()) {
@@ -989,6 +989,9 @@ function stableIdsForModule(
989
989
  ...(pf?.was !== undefined && pf.was.length > 0 ? { was: pf.was } : {}),
990
990
  // The CURRENT kind, always from the live module (continuity never supplies it).
991
991
  ...(kind !== undefined ? { kind } : {}),
992
+ // Birth-presence law rides identity: the evolve guard reads it (fromBirth may not be
993
+ // NEWLY declared on a sid that existed without it).
994
+ ...(handleFields[fieldName]?.isFromBirth === true ? { fromBirth: true as const } : {}),
992
995
  };
993
996
  }
994
997
  out[agg.id] = {
@@ -21,6 +21,10 @@ export interface StableFieldId {
21
21
  * Always set at emission; continuity input may omit it (the current module is the
22
22
  * source of the CURRENT kind). */
23
23
  readonly kind?: string;
24
+ /** BIRTH-PRESENCE LAW (`.fromBirth()`): recorded at the field's first fromBirth emission.
25
+ * The compile guard refuses NEWLY declaring fromBirth on a sid that existed without it
26
+ * (old rows may lack the field — the non-nullable read type would lie). */
27
+ readonly fromBirth?: true;
24
28
  }
25
29
 
26
30
  /** One aggregate's stable identity: sid + label lineage + per-field sids (keyed by CURRENT field name). */