@githolon/dsl 0.1.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 (53) hide show
  1. package/LICENSE.md +36 -0
  2. package/compile_package.mjs +50 -0
  3. package/package.json +59 -0
  4. package/src/aggregate.ts +167 -0
  5. package/src/authoring.ts +119 -0
  6. package/src/build_package.ts +636 -0
  7. package/src/certified_read.ts +313 -0
  8. package/src/codegen_dart.ts +2732 -0
  9. package/src/codegen_dot.ts +466 -0
  10. package/src/codegen_provider_dart.ts +358 -0
  11. package/src/codegen_ts.ts +365 -0
  12. package/src/codegen_usda.ts +388 -0
  13. package/src/combined.ts +195 -0
  14. package/src/compile_engine.ts +567 -0
  15. package/src/compile_package_main.ts +496 -0
  16. package/src/compose.ts +317 -0
  17. package/src/count.ts +218 -0
  18. package/src/ctx.ts +57 -0
  19. package/src/derived.ts +138 -0
  20. package/src/directive.ts +306 -0
  21. package/src/drivers.ts +95 -0
  22. package/src/emits_guard.ts +123 -0
  23. package/src/engine_entry.ts +449 -0
  24. package/src/exists.ts +170 -0
  25. package/src/extremum.ts +227 -0
  26. package/src/fields.ts +291 -0
  27. package/src/framework/bootstrap.ts +22 -0
  28. package/src/framework/disclosure.ts +108 -0
  29. package/src/framework/domain_lifecycle.ts +108 -0
  30. package/src/framework/identity.ts +537 -0
  31. package/src/framework/impure_capability.ts +643 -0
  32. package/src/framework/rbac.ts +418 -0
  33. package/src/framework/repair.ts +150 -0
  34. package/src/framework/sync_lifecycle.ts +125 -0
  35. package/src/framework/workspace_invariant.ts +128 -0
  36. package/src/framework/workspaces.ts +817 -0
  37. package/src/index.ts +317 -0
  38. package/src/manifest.ts +947 -0
  39. package/src/ops.ts +145 -0
  40. package/src/ordered_read.ts +228 -0
  41. package/src/predicate.ts +203 -0
  42. package/src/query/compile.ts +0 -0
  43. package/src/query/relations.ts +144 -0
  44. package/src/query.ts +151 -0
  45. package/src/read.ts +54 -0
  46. package/src/relation.ts +189 -0
  47. package/src/report/csv.ts +54 -0
  48. package/src/report.ts +401 -0
  49. package/src/spatial.ts +115 -0
  50. package/src/sum.ts +194 -0
  51. package/src/usd.ts +563 -0
  52. package/src/wire.ts +149 -0
  53. package/src/wire_encode.ts +250 -0
@@ -0,0 +1,2732 @@
1
+ // NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
2
+ // remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
3
+ // wasm32-wasip1 artifact {kernel · projection · embedded QuickJS engine},
4
+ // byte-identical everywhere. Host JS is only the bridge; domain JS runs inside
5
+ // GitHolon, never as a second execution path.
6
+ // If a file isn't this / hosting this / authoring for this / proving this — it's gone.
7
+
8
+ /**
9
+ * Dart frontend codegen (DSL decision-6: the schema is the single source of
10
+ * truth -> TS (done), proto (later), and Dart frontend types (here)).
11
+ *
12
+ * Emits, per domain:
13
+ * - a Dart class per aggregate with its typed fields, parsed (`fromJson`) from
14
+ * the FLAT, already-decoded projection `data` map that `query()`/`query_by_id()`
15
+ * emit (e.g. `{name:"HQ", createdAt:1000}`);
16
+ * - typed READ accessors per aggregate + per declared query — reactive
17
+ * `watch…`/one-shot `read…` over the GitHolon `query()`/`query_by_id()`
18
+ * surface (the read half of the dispatch(intent) boundary);
19
+ * - a Dart enum per `t.enum([...])` field;
20
+ * - a Dart intent class per domain action — a pure typed DTO. Each class exposes its
21
+ * `domain` / `intentType` (so the call site never hand-types a string) and a
22
+ * `toPayloadJson()` that serialises only its typed fields to data. It builds no
23
+ * `Event` and no `Hlc`: the GitHolon executes the installed domain plan.
24
+ * - a single generic write verb [`dispatch`] that ships {domain, intentType,
25
+ * toPayloadJson()} to the GitHolon `author(domain, intentType, payloadJson, actor)`.
26
+ *
27
+ * It does NOT hand-write the fixed wire types (`Value`/`Op`/`Event`/...): those
28
+ * live hand-written in `dart/lib/src/wire.dart`. Only the per-domain shapes are
29
+ * generated, so they stay reproducible from the domain definitions.
30
+ */
31
+ import { z } from "zod";
32
+ import type { AggregateHandle } from "./aggregate.js";
33
+ import type { WorkspaceInvariantDecl } from "./framework/workspace_invariant.js";
34
+ import type { Field, FieldKind } from "./fields.js";
35
+ import type { Directive } from "./directive.js";
36
+ import { executeDirectiveToIntent } from "./wire_encode.js";
37
+ import { deterministicPorts } from "./ctx.js";
38
+ import type { QueryDecl } from "./query.js";
39
+ import { finishCount, type AnyCount } from "./count.js";
40
+ import type { SpatialDecl } from "./spatial.js";
41
+ import { finishSum, type AnySum } from "./sum.js";
42
+ import { finishExtremum, type AnyExtremum } from "./extremum.js";
43
+ import { finishExists, type AnyExists } from "./exists.js";
44
+ import type { OrderedReadDecl } from "./ordered_read.js";
45
+ import type { DerivedDecl } from "./derived.js";
46
+ import type { CombinedDecl } from "./combined.js";
47
+ import { emitProviderSdk, type ImpureCapabilityDecl } from "./codegen_provider_dart.js";
48
+
49
+ /** A directive of any payload type. `Directive<P>` is invariant in `P`, so the
50
+ * generator (which only introspects shapes) takes the `any`-payload form. */
51
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
52
+ type AnyDirective = Directive<any>;
53
+
54
+ // ---- Zod payload introspection --------------------------------------------
55
+
56
+ /** How a typed payload field serialises into the payload-as-DATA JSON the engine reads. */
57
+ export type ToJsonWrap =
58
+ | "scalar" // String/int/bool — emit the field directly.
59
+ | "enum" // generated enum — emit `.wire`.
60
+ | "strList" // List<String> — emit directly.
61
+ | "json"; // nested object — emit the field's `Map<String, Object?>` directly.
62
+
63
+ export interface PayloadFieldSpec {
64
+ name: string;
65
+ dartType: string;
66
+ /** for enums: the Dart enum type name + raw values. */
67
+ enumValues?: string[];
68
+ /** how this field serialises into `toPayloadJson()`. */
69
+ toJson: ToJsonWrap;
70
+ /**
71
+ * Whether the payload field is OPTIONAL (Zod `.optional()`/`.default()`). Optional
72
+ * fields are nullable in Dart and OMITTED from `toPayloadJson()` when null — so the
73
+ * engine's `.plan()` sees exactly the present-field set its TS Zod schema would (it
74
+ * only emits ops for present fields). Required fields are always emitted.
75
+ */
76
+ optional: boolean;
77
+ }
78
+
79
+ type ZodInternalDef = {
80
+ type?: string | z.ZodTypeAny;
81
+ innerType?: z.ZodTypeAny;
82
+ element?: z.ZodTypeAny;
83
+ checks?: ReadonlyArray<unknown>;
84
+ shape?: Record<string, z.ZodTypeAny> | (() => Record<string, z.ZodTypeAny>);
85
+ entries?: Record<string, string | number>;
86
+ options?: z.ZodTypeAny[];
87
+ value?: unknown;
88
+ values?: readonly unknown[];
89
+ valueType?: z.ZodTypeAny;
90
+ };
91
+
92
+ function zodDef(zt: z.ZodTypeAny): ZodInternalDef {
93
+ const raw = zt as unknown as { _def?: ZodInternalDef; def?: ZodInternalDef };
94
+ return raw._def ?? raw.def ?? {};
95
+ }
96
+
97
+ function zodTypeName(zt: z.ZodTypeAny): string {
98
+ const def = zodDef(zt);
99
+ switch (def.type) {
100
+ case "string":
101
+ return "ZodString";
102
+ case "number":
103
+ return "ZodNumber";
104
+ case "boolean":
105
+ return "ZodBoolean";
106
+ case "enum":
107
+ return "ZodEnum";
108
+ case "array":
109
+ return "ZodArray";
110
+ case "object":
111
+ return "ZodObject";
112
+ case "record":
113
+ return "ZodRecord";
114
+ case "optional":
115
+ return "ZodOptional";
116
+ case "default":
117
+ return "ZodDefault";
118
+ case "unknown":
119
+ case "any":
120
+ return "ZodThing";
121
+ case "union":
122
+ return "ZodUnion";
123
+ case "literal":
124
+ return "ZodLiteral";
125
+ case "nullable":
126
+ return "ZodNullable";
127
+ default:
128
+ return typeof def.type === "string" ? `Zod${cap(def.type)}` : "ZodUnknown";
129
+ }
130
+ }
131
+
132
+ function zodInnerType(zt: z.ZodTypeAny): z.ZodTypeAny {
133
+ const inner = zodDef(zt).innerType;
134
+ if (inner === undefined) throw new Error(`Zod ${zodTypeName(zt)} has no innerType`);
135
+ return inner;
136
+ }
137
+
138
+ function zodArrayElement(zt: z.ZodTypeAny): z.ZodTypeAny {
139
+ const def = zodDef(zt);
140
+ if (def.element !== undefined) return def.element;
141
+ if (typeof def.type !== "string" && def.type !== undefined) return def.type;
142
+ throw new Error("ZodArray has no element type");
143
+ }
144
+
145
+ function zodObjectShape(zt: z.ZodTypeAny): Record<string, z.ZodTypeAny> {
146
+ const shape = zodDef(zt).shape;
147
+ if (typeof shape === "function") return shape();
148
+ if (shape !== undefined) return shape;
149
+ const raw = zt as unknown as { shape?: Record<string, z.ZodTypeAny> };
150
+ if (raw.shape !== undefined) return raw.shape;
151
+ throw new Error("ZodObject has no shape");
152
+ }
153
+
154
+ function zodEnumValues(zt: z.ZodTypeAny): string[] {
155
+ const raw = zt as unknown as { options?: readonly unknown[] };
156
+ if (raw.options !== undefined) return raw.options.map(String);
157
+ const entries = zodDef(zt).entries;
158
+ if (entries !== undefined) return Object.values(entries).map(String);
159
+ throw new Error("ZodEnum has no values");
160
+ }
161
+
162
+ function zodUnionOptions(zt: z.ZodTypeAny): z.ZodTypeAny[] {
163
+ return zodDef(zt).options ?? [];
164
+ }
165
+
166
+ function zodLiteralValue(zt: z.ZodTypeAny): unknown {
167
+ const def = zodDef(zt);
168
+ if (def.value !== undefined) return def.value;
169
+ if (def.values !== undefined) return def.values[0];
170
+ return undefined;
171
+ }
172
+
173
+ function zodRecordValueType(zt: z.ZodTypeAny): z.ZodTypeAny | undefined {
174
+ return zodDef(zt).valueType;
175
+ }
176
+
177
+ function isOptionalOrDefault(zt: z.ZodTypeAny): boolean {
178
+ const tn = zodTypeName(zt);
179
+ return tn === "ZodOptional" || tn === "ZodDefault";
180
+ }
181
+
182
+ /**
183
+ * True when a `ZodNumber` schema carries the `.int()` constraint. Zod things `.int()`
184
+ * as a `{ kind: "int" }` entry in the number's `_def.checks` array; an unconstrained
185
+ * `z.number()` has no such check and is a FRACTIONAL double. This is the ONE bit that
186
+ * decides whether the generated Dart field is `int` (whole number) or `double`
187
+ * (fractional, e.g. a 0.5 metre scale) — mirroring exactly the runtime constraint the
188
+ * engine's same Zod schema validates, so the call-site type can never truncate a
189
+ * fractional value the engine would accept.
190
+ */
191
+ function zodNumberIsInt(zt: z.ZodTypeAny): boolean {
192
+ const raw = zt as unknown as { isInt?: boolean };
193
+ if (raw.isInt === true) return true;
194
+ const checks = zodDef(zt).checks ?? [];
195
+ return checks.some((c) => {
196
+ const check = c as { kind?: string; def?: { check?: string; format?: string } };
197
+ return (
198
+ check.kind === "int" ||
199
+ (check.def?.check === "number_format" && check.def.format === "safeint")
200
+ );
201
+ });
202
+ }
203
+
204
+ export function introspectPayload(
205
+ schema: z.ZodTypeAny,
206
+ enumDartTypes: Map<string, string>,
207
+ ): PayloadFieldSpec[] {
208
+ if (zodTypeName(schema) !== "ZodObject") {
209
+ throw new Error("directive payload must be a z.object");
210
+ }
211
+ const shape = zodObjectShape(schema);
212
+ const out: PayloadFieldSpec[] = [];
213
+ for (const [name, ztRaw] of Object.entries(shape)) {
214
+ // Unwrap ZodOptional / ZodDefault to the inner type, REMEMBERING that the field
215
+ // is optional (it becomes a nullable Dart field, omitted from the payload JSON
216
+ // when null — mirroring the engine's Zod, which only sees present fields).
217
+ let zt = ztRaw as z.ZodTypeAny;
218
+ let optional = false;
219
+ while (isOptionalOrDefault(zt)) {
220
+ optional = true;
221
+ zt = zodInnerType(zt);
222
+ }
223
+ out.push({ ...introspectField(name, zt, enumDartTypes), optional });
224
+ }
225
+ return out;
226
+ }
227
+
228
+ /** Introspect ONE (already-unwrapped) Zod field into its Dart spec. Handles the
229
+ * four shapes a tenant domain uses: scalar (string/int/bool), enum, string[], and a
230
+ * NESTED object (e.g. attach/detach's `target:{targetType,targetId}`) — the engine
231
+ * receives nested objects as DATA, so we model them as `Map<String, Object?>`. */
232
+ function introspectField(
233
+ name: string,
234
+ zt: z.ZodTypeAny,
235
+ enumDartTypes: Map<string, string>,
236
+ ): Omit<PayloadFieldSpec, "optional"> {
237
+ const tn = zodTypeName(zt);
238
+ switch (tn) {
239
+ case "ZodString":
240
+ return { name, dartType: "String", toJson: "scalar" };
241
+ case "ZodNumber":
242
+ // A `z.number()` is a FRACTIONAL double in JS (e.g. catalogues `createListing`
243
+ // scaleX/scaleY = 0.5) UNLESS the schema adds the `.int()` check. Only an
244
+ // `.int()`-constrained number is a Dart `int`; an unconstrained `z.number()`
245
+ // emits `double` so a fractional value (0.5/0.75) is NOT truncated to 0 at the
246
+ // call site. Detect the `.int()` check on the Zod number's `_def.checks`
247
+ // (`{kind:"int"}`), exactly the runtime constraint the engine's Zod validates.
248
+ return {
249
+ name,
250
+ dartType: zodNumberIsInt(zt) ? "int" : "double",
251
+ toJson: "scalar",
252
+ };
253
+ case "ZodBoolean":
254
+ return { name, dartType: "bool", toJson: "scalar" };
255
+ case "ZodEnum": {
256
+ const values = zodEnumValues(zt);
257
+ const key = values.join("|");
258
+ const dartType = enumDartTypes.get(key) ?? `${cap(name)}Enum`;
259
+ return { name, dartType, toJson: "enum", enumValues: values };
260
+ }
261
+ case "ZodArray": {
262
+ const elem = zodArrayElement(zt);
263
+ // A `string[]` is a typed `List<String>`. ANY other element type (e.g.
264
+ // `z.array(z.object(...))` — proposals' replacement `edges`) is arbitrary
265
+ // JSON DATA to the engine, modelled as `List<Object?>` and shipped verbatim.
266
+ if (zodTypeName(elem) === "ZodString") {
267
+ return { name, dartType: "List<String>", toJson: "strList" };
268
+ }
269
+ return { name, dartType: "List<Object?>", toJson: "json" };
270
+ }
271
+ case "ZodObject":
272
+ // Nested object payload (e.g. `target: z.object({targetType, targetId})`). The
273
+ // engine receives it as DATA verbatim, so we model it as an arbitrary JSON map
274
+ // — the call site builds the literal, and `toPayloadJson()` ships it through.
275
+ return { name, dartType: "Map<String, Object?>", toJson: "json" };
276
+ case "ZodRecord": {
277
+ const valueType = zodRecordValueType(zt);
278
+ const dartType =
279
+ valueType !== undefined && zodTypeName(valueType) === "ZodString"
280
+ ? "Map<String, String>"
281
+ : "Map<String, Object?>";
282
+ return { name, dartType, toJson: "json" };
283
+ }
284
+ case "ZodThing":
285
+ // A `z.thing(...)` payload — also arbitrary JSON DATA to the engine.
286
+ return { name, dartType: "Map<String, Object?>", toJson: "json" };
287
+ default:
288
+ throw new Error(`unsupported payload field type '${tn}' for '${name}'`);
289
+ }
290
+ }
291
+
292
+ /** A `///`-doc fragment for a payload class: the directive's `.doc(...)` prose, or a
293
+ * sensible default derived from the directive id (so EVERY class is documented). */
294
+ function directiveDoc(d: AnyDirective): string {
295
+ if (d.description !== undefined && d.description.trim() !== "") {
296
+ return d.description.trim();
297
+ }
298
+ // Default: split the camelCase id into words → "create site", "set site custom field".
299
+ const words = d.id
300
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
301
+ .toLowerCase()
302
+ .trim();
303
+ return `Dispatch the \`${d.id}\` directive (${words}).`;
304
+ }
305
+
306
+ // ---- aggregate field -> Dart type -----------------------------------------
307
+
308
+ type MaterializedFieldKind = Exclude<FieldKind, "hasMany">;
309
+
310
+ interface AggFieldSpec {
311
+ /** Dart-safe field/property name exposed on generated classes. */
312
+ name: string;
313
+ /** Canonical wire/readmodel key declared by the DSL, e.g. `spec.domainHash`. */
314
+ wireName: string;
315
+ kind: MaterializedFieldKind;
316
+ dartType: string;
317
+ enumValues?: string[];
318
+ /** For an own-id string field represented as a typed aggregate id value class. */
319
+ valueClass?: string;
320
+ /**
321
+ * For a `ref`-kind field: the TYPED id class of the target aggregate (e.g. a
322
+ * `Building.siteId` ref → `SiteId`). The field's Dart type is this typed id (NOT a
323
+ * bare `String`), so a cross-aggregate ref carries its target's TYPE — passing a
324
+ * `RoomId` where a `SiteId` is expected is a compile error. `undefined` for a
325
+ * non-ref field (or a ref whose target type is unknown — falls back to `String`).
326
+ */
327
+ refIdClass?: string;
328
+ /**
329
+ * Whether the field is OPTIONAL on a folded aggregate — derived from the DSL field
330
+ * schema (GAP 1). A `set`/`map` is a COLLECTION: it is ALWAYS empty-defaultable
331
+ * (a freshly-created aggregate that never folded it decodes to an empty collection),
332
+ * so it is never `required` in the Dart ctor and its reader tolerates absence. A
333
+ * SCALAR (`string`/`int`/`enum`/`ref`/`json`) is optional ONLY when the DSL marked
334
+ * it `.optional()` (`field.isOptional`) — then the Dart field is NULLABLE and its
335
+ * reader returns null when absent; an unmarked scalar stays REQUIRED (a missing
336
+ * value still throws, preserving the strict-decode contract for must-be-folded
337
+ * fields like a create's id/name).
338
+ */
339
+ optional: boolean;
340
+ /**
341
+ * For a `map`-kind field: the inner VALUE field's kind (`field.mapValueKind`). A
342
+ * `"json"`-valued map (`t.map(t.json())`) is surfaced as `Map<String, Object?>` (each
343
+ * entry JSON-DECODED back to its object — a value-object map round-trips losslessly),
344
+ * whereas any other (default `"string"`) stays `Map<String, String>` (flat strings).
345
+ * `undefined` for a non-map field.
346
+ */
347
+ mapValueKind?: FieldKind;
348
+ /**
349
+ * For a `json`-kind field: the asserted decoded SHAPE (`field.jsonShape`). `"object"`
350
+ * (a `t.jsonObject()` leaf, e.g. `location`) means the value is ALWAYS a JSON object —
351
+ * the generated read model surfaces it as a strict `Map<String, Object?>`. `undefined`
352
+ * (a plain `t.json()`) means the shape is unknown/non-object (a double/bool/list
353
+ * authored as a JSON leaf — the kernel `Value` is Str|Int only), so the projection
354
+ * defaults it to the permissive `Object?` with a non-throwing reader. This affects
355
+ * only the generated read-model type. `undefined` for a non-json field.
356
+ */
357
+ jsonShape?: "object";
358
+ }
359
+
360
+ export interface DartImport {
361
+ uri: string;
362
+ prefix?: string;
363
+ }
364
+
365
+ export interface DartRefImport {
366
+ aggregateType: string;
367
+ uri: string;
368
+ prefix: string;
369
+ }
370
+
371
+ export interface PermissionVocabulary {
372
+ resourceTypes: readonly string[];
373
+ roleCatalogue: Readonly<Record<string, readonly string[]>>;
374
+ }
375
+
376
+ /** True for the COLLECTION kinds, which always carry a sensible empty default. */
377
+ function isCollectionKind(kind: FieldKind): boolean {
378
+ return kind === "set" || kind === "map";
379
+ }
380
+
381
+ /** True when a `map`-kind field's VALUE leaves are JSON objects (`t.map(t.json())`),
382
+ * surfaced as `Map<String, Object?>` rather than the flat-string `Map<String, String>`. */
383
+ function isJsonValuedMap(s: AggFieldSpec): boolean {
384
+ return s.kind === "map" && s.mapValueKind === "json";
385
+ }
386
+
387
+ /** True when a `json`-kind field is an asserted JSON-object value-object
388
+ * (`t.jsonObject()`, `jsonShape === "object"`) surfaced as a strict
389
+ * `Map<String, Object?>`. A plain `t.json()` (shape unknown/non-object) is false,
390
+ * surfaced as the permissive `Object?`. */
391
+ function isJsonObjectLeaf(s: AggFieldSpec): boolean {
392
+ return s.kind === "json" && s.jsonShape === "object";
393
+ }
394
+
395
+ /** True for a permissive `json` leaf on the generated read model: a plain `t.json()` whose
396
+ * decoded shape is unknown/non-object (a double/bool/list authored as a JSON leaf). Its
397
+ * projection type is the already-nullable `Object?` (not a strict `Map`) with a
398
+ * non-throwing reader, so it is treated like an always-nullable scalar (no `required`,
399
+ * no extra `?`, no ctor default). An OBJECT-asserted leaf (`t.jsonObject()`) is NOT
400
+ * permissive — it is the strict `Map<String, Object?>`. */
401
+ function isPermissiveJsonLeaf(s: AggFieldSpec): boolean {
402
+ return s.kind === "json" && s.jsonShape !== "object";
403
+ }
404
+
405
+ /** The `?` nullable suffix for a projection field DECL. A permissive `json` leaf's type
406
+ * is ALREADY the nullable `Object?`, so it takes NO extra `?` (a doubled `Object??` is
407
+ * invalid); every other field appends `?` only when `.optional()`. */
408
+ function readModelNullableSuffix(s: AggFieldSpec): string {
409
+ if (isPermissiveJsonLeaf(s)) return ""; // `Object?` is already nullable.
410
+ return s.optional ? "?" : "";
411
+ }
412
+
413
+ function dartMemberName(raw: string): string {
414
+ const parts = raw.split(/[^A-Za-z0-9]+/).filter((p) => p !== "");
415
+ const camel =
416
+ parts.length === 0
417
+ ? "field"
418
+ : parts[0] + parts.slice(1).map((p) => cap(p)).join("");
419
+ const cleaned = camel.replace(/[^A-Za-z0-9_]/g, "_");
420
+ const safe = /^[0-9]/.test(cleaned) ? `v${cleaned}` : cleaned;
421
+ return DART_RESERVED.has(safe) ? `${safe}_` : safe;
422
+ }
423
+
424
+ function dartTypeNameFromField(raw: string): string {
425
+ return cap(dartMemberName(raw));
426
+ }
427
+
428
+ function aggregateIdDartType(aggregateType: string): string {
429
+ return typedIdClassForAggregateType(aggregateType);
430
+ }
431
+
432
+ function aggregateIdFromRowExpr(aggregateType: string, rowIdExpr: string): string {
433
+ return `${aggregateIdDartType(aggregateType)}.fromRow(${rowIdExpr})`;
434
+ }
435
+
436
+ function aggregateFieldSpecForKey(
437
+ aggregatesById: Map<string, AggregateHandle>,
438
+ aggregateType: string,
439
+ keyField: string,
440
+ enumDartTypes: Map<string, string>,
441
+ refIdClassForAggregate: (aggregateType: string) => string = typedIdClassForAggregateType,
442
+ ownIdFieldsByAggregate: ReadonlyMap<string, ReadonlySet<string>> = new Map(),
443
+ ): AggFieldSpec | undefined {
444
+ const agg = aggregatesById.get(aggregateType);
445
+ const field = (agg?.fields as Record<string, Field> | undefined)?.[keyField];
446
+ if (field === undefined || field.kind === "hasMany") return undefined;
447
+ const valueClass =
448
+ field.kind === "string" && (ownIdFieldsByAggregate.get(aggregateType)?.has(keyField) ?? false)
449
+ ? aggregateIdDartType(aggregateType)
450
+ : undefined;
451
+ return aggFieldSpec(keyField, field, enumDartTypes, refIdClassForAggregate, valueClass);
452
+ }
453
+
454
+ function readKeyParamSpec(
455
+ paramName: string,
456
+ fieldSpec: AggFieldSpec | undefined,
457
+ ): { dartType: string; valueExpr: string } {
458
+ if (fieldSpec === undefined) return { dartType: "String", valueExpr: paramName };
459
+ if (fieldSpec.valueClass !== undefined) {
460
+ return { dartType: fieldSpec.valueClass, valueExpr: `${paramName}.value` };
461
+ }
462
+ if (fieldSpec.kind === "ref" && fieldSpec.refIdClass !== undefined) {
463
+ return { dartType: fieldSpec.refIdClass, valueExpr: `${paramName}.value` };
464
+ }
465
+ if (fieldSpec.kind === "enum") {
466
+ return { dartType: fieldSpec.dartType, valueExpr: `${paramName}.wire` };
467
+ }
468
+ if (fieldSpec.kind === "int") {
469
+ return { dartType: "int", valueExpr: paramName };
470
+ }
471
+ return { dartType: "String", valueExpr: paramName };
472
+ }
473
+
474
+ function dartImportLine(imp: DartImport): string {
475
+ const prefix = imp.prefix !== undefined && imp.prefix !== "" ? ` as ${imp.prefix}` : "";
476
+ return `import '${imp.uri}'${prefix};`;
477
+ }
478
+
479
+ function refIdClassResolver(mod: DomainModule): (aggregateType: string) => string {
480
+ const imports = new Map<string, DartRefImport>();
481
+ for (const imp of mod.dartRefImports ?? []) {
482
+ imports.set(imp.aggregateType, imp);
483
+ }
484
+ return (aggregateType: string): string => {
485
+ const imp = imports.get(aggregateType);
486
+ const cls = typedIdClassForAggregateType(aggregateType);
487
+ return imp === undefined ? cls : `${imp.prefix}.${cls}`;
488
+ };
489
+ }
490
+
491
+ function unwrapProjectionReturn(zt: z.ZodTypeAny): z.ZodTypeAny {
492
+ let cur = zt;
493
+ while (
494
+ zodTypeName(cur) === "ZodOptional" ||
495
+ zodTypeName(cur) === "ZodDefault" ||
496
+ zodTypeName(cur) === "ZodNullable"
497
+ ) {
498
+ cur = zodInnerType(cur);
499
+ }
500
+ return cur;
501
+ }
502
+
503
+ function projectionReturnDartBaseType(schema: z.ZodTypeAny): string {
504
+ const inner = unwrapProjectionReturn(schema);
505
+ switch (zodTypeName(inner)) {
506
+ case "ZodString":
507
+ case "ZodEnum":
508
+ return "String";
509
+ case "ZodNumber":
510
+ return zodNumberIsInt(inner) ? "int" : "double";
511
+ case "ZodBoolean":
512
+ return "bool";
513
+ case "ZodObject":
514
+ case "ZodRecord":
515
+ return "Map<String, Object?>";
516
+ case "ZodArray":
517
+ return "List<Object?>";
518
+ default:
519
+ return "Object?";
520
+ }
521
+ }
522
+
523
+ function projectionReturnDartType(schema: z.ZodTypeAny): string {
524
+ const base = projectionReturnDartBaseType(schema);
525
+ return base.endsWith("?") ? base : `${base}?`;
526
+ }
527
+
528
+ function parseProjectionReturnField(field: string, schema: z.ZodTypeAny): string {
529
+ const inner = unwrapProjectionReturn(schema);
530
+ switch (zodTypeName(inner)) {
531
+ case "ZodString":
532
+ case "ZodEnum":
533
+ return `data['${field}'] as String?`;
534
+ case "ZodBoolean":
535
+ return `data['${field}'] as bool?`;
536
+ case "ZodNumber":
537
+ return zodNumberIsInt(inner)
538
+ ? `(() { final v = data['${field}']; return v == null ? null : (v as num).toInt(); })()`
539
+ : `(() { final v = data['${field}']; return v == null ? null : (v as num).toDouble(); })()`;
540
+ case "ZodObject":
541
+ case "ZodRecord":
542
+ return `(data['${field}'] as Map?)?.cast<String, Object?>()`;
543
+ case "ZodArray":
544
+ return `(data['${field}'] as List?)?.cast<Object?>()`;
545
+ default:
546
+ return `data['${field}']`;
547
+ }
548
+ }
549
+
550
+ /** Unwrap a possibly-`.optional()`/`.default()` Zod schema to its inner type, so an
551
+ * optional enum field still yields its literal options. */
552
+ function unwrapZod(zt: z.ZodTypeAny): z.ZodTypeAny {
553
+ let inner = zt;
554
+ while (isOptionalOrDefault(inner)) {
555
+ inner = zodInnerType(inner);
556
+ }
557
+ return inner;
558
+ }
559
+
560
+ function aggFieldSpec(
561
+ wireName: string,
562
+ field: Field,
563
+ enumDartTypes: Map<string, string>,
564
+ refIdClassForAggregate: (aggregateType: string) => string = typedIdClassForAggregateType,
565
+ valueClass?: string,
566
+ ): AggFieldSpec {
567
+ const name = dartMemberName(wireName);
568
+ // Collections are ALWAYS empty-defaultable; scalars are optional only when the DSL
569
+ // field schema marked them `.optional()`.
570
+ const optional = isCollectionKind(field.kind) ? false : field.isOptional;
571
+ switch (field.kind) {
572
+ case "string":
573
+ return {
574
+ name,
575
+ wireName,
576
+ kind: field.kind,
577
+ dartType: valueClass ?? "String",
578
+ optional,
579
+ ...(valueClass !== undefined ? { valueClass } : {}),
580
+ };
581
+ case "json":
582
+ // `jsonShape` ("object" via `t.jsonObject()`) chooses strict-object vs
583
+ // permissive read-model type.
584
+ return {
585
+ name,
586
+ wireName,
587
+ kind: field.kind,
588
+ dartType: "String",
589
+ optional,
590
+ ...(field.jsonShape !== undefined ? { jsonShape: field.jsonShape } : {}),
591
+ };
592
+ case "ref": {
593
+ // A `ref` field carries ANOTHER aggregate's id — type it as that target's
594
+ // TYPED id class (cross-aggregate ref typing), so e.g. a `Building.siteId`
595
+ // ref is a `SiteId`, never a bare String. The target wire id is recorded on
596
+ // the field (`refAggregateId`); fall back to `String` only if it is absent.
597
+ const targetType = field.refAggregateId;
598
+ const refIdClass =
599
+ targetType !== undefined && targetType !== ""
600
+ ? refIdClassForAggregate(targetType)
601
+ : undefined;
602
+ return {
603
+ name,
604
+ wireName,
605
+ kind: field.kind,
606
+ dartType: refIdClass ?? "String",
607
+ optional,
608
+ ...(refIdClass !== undefined ? { refIdClass } : {}),
609
+ };
610
+ }
611
+ case "int":
612
+ return { name, wireName, kind: field.kind, dartType: "int", optional };
613
+ case "enum": {
614
+ // Pull the enum values out of the Zod schema (unwrapping `.optional()`).
615
+ const values = zodEnumValues(unwrapZod(field.zod));
616
+ const key = values.join("|");
617
+ const dartType = enumDartTypes.get(key) ?? `${dartTypeNameFromField(wireName)}Enum`;
618
+ return { name, wireName, kind: field.kind, dartType, enumValues: values, optional };
619
+ }
620
+ case "evidence":
621
+ // A content-addressed EvidenceRef → the shared typed `EvidenceRef` Dart class
622
+ // (emitted once per file). A scalar value, optional only when `.optional()`.
623
+ return { name, wireName, kind: field.kind, dartType: "EvidenceRef", optional };
624
+ case "set":
625
+ return { name, wireName, kind: field.kind, dartType: "Set<String>", optional: false };
626
+ case "map": {
627
+ // A `json`-valued map (`t.map(t.json())`) surfaces each entry as its DECODED
628
+ // object (`Map<String, Object?>`) so a value-object map round-trips losslessly;
629
+ // every other map keeps the flat-string `Map<String, String>`. The wire driver is
630
+ // identical for both, so this is a frontend-only type choice (no hash change).
631
+ const jsonValued = field.mapValueKind === "json";
632
+ return {
633
+ name,
634
+ wireName,
635
+ kind: field.kind,
636
+ dartType: jsonValued ? "Map<String, Object?>" : "Map<String, String>",
637
+ optional: false,
638
+ ...(field.mapValueKind !== undefined ? { mapValueKind: field.mapValueKind } : {}),
639
+ };
640
+ }
641
+ case "hasMany":
642
+ throw new Error(
643
+ `virtual hasMany field '${name}' is read-index only and cannot be emitted as folded Dart state`,
644
+ );
645
+ }
646
+ }
647
+
648
+ function aggregateFieldSpecs(
649
+ agg: AggregateHandle,
650
+ enumDartTypes: Map<string, string>,
651
+ refIdClassForAggregate: (aggregateType: string) => string = typedIdClassForAggregateType,
652
+ ownIdFields: ReadonlySet<string> = new Set(),
653
+ ): AggFieldSpec[] {
654
+ const used = new Set<string>(["aggregateId"]);
655
+ // `agg.fields` is STORED fields only — virtual `t.hasMany` inverses are in `agg.hasMany`
656
+ // and never reach Dart materialization (they are served by the inverse read index).
657
+ return Object.entries(agg.fields as Record<string, Field>)
658
+ .map(([name, f]) => {
659
+ const valueClass =
660
+ f.kind === "string" && ownIdFields.has(name)
661
+ ? aggregateIdDartType(agg.id)
662
+ : undefined;
663
+ const spec = aggFieldSpec(
664
+ name,
665
+ f,
666
+ enumDartTypes,
667
+ refIdClassForAggregate,
668
+ valueClass,
669
+ );
670
+ if (used.has(spec.name)) {
671
+ throw new Error(
672
+ `aggregate '${agg.id}' field '${spec.wireName}' maps to duplicate Dart field '${spec.name}'`,
673
+ );
674
+ }
675
+ used.add(spec.name);
676
+ return spec;
677
+ });
678
+ }
679
+
680
+ // ---- emit helpers ----------------------------------------------------------
681
+
682
+ export function cap(s: string): string {
683
+ return s.charAt(0).toUpperCase() + s.slice(1);
684
+ }
685
+
686
+ // ---- typed-id naming (the type-safety keystone, generalized) ----------------
687
+ //
688
+ // A DISTINCT typed id class is emitted PER AGGREGATE so passing one aggregate's
689
+ // id where another's is expected is a COMPILE ERROR. The class NAME must be a pure
690
+ // function of the aggregate's wire TYPE (e.g. `SiteRootAggregate`) so three sites
691
+ // agree on it everywhere it is referenced — the typed-id class declaration, every
692
+ // `createX` factory's return type, AND every cross-aggregate REF field that targets
693
+ // that type. Convention: strip a trailing `RootAggregate`/`Aggregate` suffix and
694
+ // append `Id` — `SiteRootAggregate → SiteId`, `ThingAggregate → ThingId`,
695
+ // `TrackableThing → TrackableThingId`, `BuildingAggregate → BuildingId`. The
696
+ // `RootAggregate` strip BEFORE `Aggregate` keeps the proven `SiteId` byte-identical.
697
+
698
+ /** The typed-id class name for an aggregate WIRE TYPE (e.g. `SiteRootAggregate` →
699
+ * `SiteId`). Pure + deterministic: the SAME wire type ALWAYS yields the SAME name,
700
+ * so the id-class decl, the `createX` factory return, and every typed ref field that
701
+ * points at this type all agree. */
702
+ function typedIdClassForAggregateType(aggregateType: string): string {
703
+ let stem = aggregateType;
704
+ if (stem.endsWith("RootAggregate")) {
705
+ stem = stem.slice(0, -"RootAggregate".length);
706
+ } else if (stem.endsWith("Aggregate")) {
707
+ stem = stem.slice(0, -"Aggregate".length);
708
+ }
709
+ // Empty stem (a type literally named `Aggregate`) is impossible for registered
710
+ // aggregates, but guard so the name is always a legal identifier.
711
+ return `${cap(stem === "" ? aggregateType : stem)}Id`;
712
+ }
713
+
714
+ /** Collect all enum value-sets across aggregates + directives, name each ONCE. */
715
+ function collectEnums(
716
+ aggregates: AggregateHandle[],
717
+ directives: AnyDirective[],
718
+ ): { dartTypes: Map<string, string>; decls: string[] } {
719
+ const dartTypes = new Map<string, string>(); // "a|b|c" -> DartEnumName
720
+ const usedNames = new Set<string>(); // every emitted enum name (collision guard)
721
+ const decls: string[] = [];
722
+ const register = (preferredName: string, values: readonly string[]) => {
723
+ const key = values.join("|");
724
+ if (dartTypes.has(key)) return;
725
+ // Two DIFFERENT value-sets can prefer the SAME name (e.g. several `status`
726
+ // fields with distinct literal lists all want `StatusEnum`). Suffix on collision
727
+ // so each distinct value-set gets a UNIQUE Dart enum (`StatusEnum`, `Status2Enum`).
728
+ const base = `${dartTypeNameFromField(preferredName)}Enum`;
729
+ let name = base;
730
+ let n = 1;
731
+ while (usedNames.has(name)) {
732
+ n += 1;
733
+ name = `${dartTypeNameFromField(preferredName)}${n}Enum`;
734
+ }
735
+ usedNames.add(name);
736
+ dartTypes.set(key, name);
737
+ const variants = values
738
+ .map((v) => ` ${dartIdent(v)}('${v}')`)
739
+ .join(",\n");
740
+ decls.push(
741
+ `/// Generated enum for field values: ${values.join(", ")}.\n` +
742
+ `enum ${name} {\n${variants};\n\n` +
743
+ ` final String wire;\n` +
744
+ ` const ${name}(this.wire);\n\n` +
745
+ ` static ${name} fromWire(String wire) =>\n` +
746
+ ` ${name}.values.firstWhere((e) => e.wire == wire,\n` +
747
+ ` orElse: () => throw FormatException('unknown ${name}: \$wire'));\n` +
748
+ `}`,
749
+ );
750
+ };
751
+ for (const agg of aggregates) {
752
+ for (const [name, field] of Object.entries(
753
+ agg.fields as Record<string, Field>,
754
+ )) {
755
+ if (field.kind === "enum") {
756
+ // Unwrap `.optional()` so an optional enum field still registers its options.
757
+ register(name, zodEnumValues(unwrapZod(field.zod)));
758
+ }
759
+ }
760
+ }
761
+ for (const d of directives) {
762
+ if (zodTypeName(d.payloadSchema as z.ZodTypeAny) !== "ZodObject") continue;
763
+ const shape = zodObjectShape(d.payloadSchema as z.ZodTypeAny);
764
+ for (const [name, zt] of Object.entries(shape)) {
765
+ const inner = unwrapZod(zt as z.ZodTypeAny);
766
+ if (zodTypeName(inner) === "ZodEnum") {
767
+ register(name, zodEnumValues(inner));
768
+ }
769
+ }
770
+ }
771
+ return { dartTypes, decls };
772
+ }
773
+
774
+ /** Dart reserved words that cannot be bare identifiers (enum members / fields). A
775
+ * value like `"new"` (feedback status) collides, so it is suffixed (`new_`). */
776
+ const DART_RESERVED = new Set([
777
+ "abstract", "as", "assert", "async", "await", "break", "case", "catch", "class",
778
+ "const", "continue", "covariant", "default", "deferred", "do", "dynamic", "else",
779
+ "enum", "export", "extends", "extension", "external", "factory", "false", "final",
780
+ "finally", "for", "function", "get", "hide", "if", "implements", "import", "in",
781
+ "interface", "is", "late", "library", "mixin", "new", "null", "on", "operator",
782
+ "part", "required", "rethrow", "return", "set", "show", "static", "super",
783
+ "switch", "this", "throw", "true", "try", "typedef", "var", "void", "while",
784
+ "with", "yield",
785
+ ]);
786
+
787
+ /** A Dart-safe enum identifier (handles values like "in_repair", and reserved words
788
+ * like "new"). */
789
+ function dartIdent(v: string): string {
790
+ const cleaned = v.replace(/[^A-Za-z0-9_]/g, "_");
791
+ const safe = /^[0-9]/.test(cleaned) ? `v$cleaned` : cleaned;
792
+ return DART_RESERVED.has(safe) ? `${safe}_` : safe;
793
+ }
794
+
795
+ function dartString(s: string): string {
796
+ return `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\$/g, "\\$")}'`;
797
+ }
798
+
799
+ function emitPermissionVocabulary(vocab: PermissionVocabulary | undefined): string {
800
+ if (vocab === undefined) return "";
801
+ const capabilities = [...new Set(Object.values(vocab.roleCatalogue).flat())].sort();
802
+ const roleNames = Object.keys(vocab.roleCatalogue).sort();
803
+ const roleDecls = roleNames.map((role) => {
804
+ const caps = [...new Set(vocab.roleCatalogue[role] ?? [])].sort();
805
+ const capSet =
806
+ caps.length === 0
807
+ ? `<String>{}`
808
+ : `<String>{${caps.map(dartString).join(", ")}}`;
809
+ return ` static const ${dartIdent(role)} = PermissionRole(${dartString(role)}, ${capSet});`;
810
+ });
811
+ const capabilityDecls = capabilities.map(
812
+ (capability) => ` static const ${dartIdent(capability)} = ${dartString(capability)};`,
813
+ );
814
+ const values = roleNames.map((role) => dartIdent(role)).join(", ");
815
+ const allCapabilities = capabilities.map(dartString).join(", ");
816
+
817
+ return [
818
+ `/// Generated permission vocabulary from the domain role catalogue.`,
819
+ `///`,
820
+ `/// This is frontend read/dispatch vocabulary only. Domain execution and`,
821
+ `/// admission still happen inside the installed GitHolon domain law.`,
822
+ `class PermissionRole {`,
823
+ ` const PermissionRole(this.value, this.capabilities);`,
824
+ ``,
825
+ ` final String value;`,
826
+ ` final Set<String> capabilities;`,
827
+ ``,
828
+ ` bool grants(String capability) => capabilities.contains(capability);`,
829
+ ``,
830
+ ` @override`,
831
+ ` bool operator ==(Object other) => other is PermissionRole && other.value == value;`,
832
+ ``,
833
+ ` @override`,
834
+ ` int get hashCode => value.hashCode;`,
835
+ `}`,
836
+ ``,
837
+ `class PermissionRoles {`,
838
+ ` const PermissionRoles._();`,
839
+ ...roleDecls,
840
+ ``,
841
+ ` static const values = <PermissionRole>[${values}];`,
842
+ ``,
843
+ ` static PermissionRole fromWire(String value) => values.firstWhere(`,
844
+ ` (role) => role.value == value,`,
845
+ ` orElse: () => PermissionRole(value, const <String>{}),`,
846
+ ` );`,
847
+ `}`,
848
+ ``,
849
+ `class PermissionCapabilities {`,
850
+ ` const PermissionCapabilities._();`,
851
+ ...capabilityDecls,
852
+ ``,
853
+ ` static const values = <String>[${allCapabilities}];`,
854
+ `}`,
855
+ ``,
856
+ `class PermissionBindingProjection {`,
857
+ ` const PermissionBindingProjection({`,
858
+ ` required this.permissionId,`,
859
+ ` required this.userId,`,
860
+ ` required this.resourceType,`,
861
+ ` required this.resourceId,`,
862
+ ` required this.role,`,
863
+ ` required this.status,`,
864
+ ` required this.grantedBy,`,
865
+ ` this.grantedAt,`,
866
+ ` this.displayName,`,
867
+ ` });`,
868
+ ``,
869
+ ` final String permissionId;`,
870
+ ` final String userId;`,
871
+ ` final ResourceTypeEnum resourceType;`,
872
+ ` final String resourceId;`,
873
+ ` final PermissionRole role;`,
874
+ ` final String status;`,
875
+ ` final String grantedBy;`,
876
+ ` final DateTime? grantedAt;`,
877
+ ` final String? displayName;`,
878
+ ``,
879
+ ` bool get isActive => status != 'revoked' && status != 'deleted';`,
880
+ ` bool grants(String capability) => isActive && role.grants(capability);`,
881
+ ``,
882
+ ` factory PermissionBindingProjection.fromProjected(`,
883
+ ` String permissionId,`,
884
+ ` Map<String, Object?> json,`,
885
+ ` ) {`,
886
+ ` final resourceTypeWire = (json['resourceType'] ?? '').toString();`,
887
+ ` return PermissionBindingProjection(`,
888
+ ` permissionId: permissionId,`,
889
+ ` userId: (json['userId'] ?? '').toString(),`,
890
+ ` resourceType: ResourceTypeEnum.fromWire(resourceTypeWire),`,
891
+ ` resourceId: (json['resourceId'] ?? json['estateId'] ?? '').toString(),`,
892
+ ` role: PermissionRoles.fromWire((json['role'] ?? '').toString()),`,
893
+ ` status: (json['status'] ?? 'active').toString(),`,
894
+ ` grantedBy: (json['grantedBy'] ?? '').toString(),`,
895
+ ` grantedAt: _permissionDate(json['grantedAt']),`,
896
+ ` displayName: json['displayName']?.toString(),`,
897
+ ` );`,
898
+ ` }`,
899
+ `}`,
900
+ ``,
901
+ `extension UserPermissionReadModelRbac on UserPermissionReadModel {`,
902
+ ` List<PermissionBindingProjection> get activePermissionBindings =>`,
903
+ ` permissionBindings.where((binding) => binding.isActive).toList(growable: false);`,
904
+ ``,
905
+ ` List<PermissionBindingProjection> get permissionBindings => permissions.entries`,
906
+ ` .where((entry) => entry.value is Map)`,
907
+ ` .map(`,
908
+ ` (entry) => PermissionBindingProjection.fromProjected(`,
909
+ ` entry.key,`,
910
+ ` (entry.value as Map).cast<String, Object?>(),`,
911
+ ` ),`,
912
+ ` )`,
913
+ ` .toList(growable: false);`,
914
+ ``,
915
+ ` bool hasCapability({`,
916
+ ` required ResourceTypeEnum resourceType,`,
917
+ ` required String resourceId,`,
918
+ ` required String capability,`,
919
+ ` }) => activePermissionBindings.any(`,
920
+ ` (binding) =>`,
921
+ ` binding.resourceType == resourceType &&`,
922
+ ` binding.resourceId == resourceId &&`,
923
+ ` binding.role.grants(capability),`,
924
+ ` );`,
925
+ ``,
926
+ ` bool hasCapabilityInAnyScope(String capability) => activePermissionBindings.any(`,
927
+ ` (binding) => binding.role.grants(capability),`,
928
+ ` );`,
929
+ ``,
930
+ ` bool hasCapabilityForAnyResource({`,
931
+ ` required ResourceTypeEnum resourceType,`,
932
+ ` required String capability,`,
933
+ ` }) => activePermissionBindings.any(`,
934
+ ` (binding) =>`,
935
+ ` binding.resourceType == resourceType && binding.role.grants(capability),`,
936
+ ` );`,
937
+ `}`,
938
+ ``,
939
+ `DateTime? _permissionDate(Object? raw) {`,
940
+ ` if (raw is DateTime) return raw;`,
941
+ ` if (raw is int && raw > 0) {`,
942
+ ` return DateTime.fromMillisecondsSinceEpoch(raw, isUtc: true);`,
943
+ ` }`,
944
+ ` if (raw is String && raw.isNotEmpty) return DateTime.tryParse(raw);`,
945
+ ` return null;`,
946
+ `}`,
947
+ ].join("\n");
948
+ }
949
+
950
+ // ---- aggregate class -------------------------------------------------------
951
+
952
+ function emitAggregate(
953
+ agg: AggregateHandle,
954
+ enumDartTypes: Map<string, string>,
955
+ refIdClassForAggregate: (aggregateType: string) => string = typedIdClassForAggregateType,
956
+ ownIdFields: ReadonlySet<string> = new Set(),
957
+ ): string {
958
+ const className = agg.id; // wire id is already PascalCase by convention.
959
+ const idType = aggregateIdDartType(agg.id);
960
+ const specs = aggregateFieldSpecs(
961
+ agg,
962
+ enumDartTypes,
963
+ refIdClassForAggregate,
964
+ ownIdFields,
965
+ );
966
+
967
+ // GAP 2 — the kernel aggregate id. The projection strips the reserved `__id`/
968
+ // `__type` from each row's flat `data`; the row-level `id` (== the kernel aggregate
969
+ // id, e.g. the siteId) is the ONLY carrier of it. We codegen a non-domain,
970
+ // TYPED `aggregateId` field threaded from the row id through `fromJson`'s optional
971
+ // second positional arg, so BOTH the indexed accessors (`read…`/`watch…`) AND the
972
+ // by-id accessors (`read…ById`) — all of which funnel through `NomosReads.decodeRows`,
973
+ // which now passes `row['id']` — surface the id on the typed aggregate. (No domain
974
+ // aggregate declares an `aggregateId` field, so there is no collision.)
975
+
976
+ // Field decls: the GAP-2 id first, then each domain field. A `.optional()` scalar is
977
+ // nullable (`String?`/`int?`/`Enum?`); collections (`set`/`map`) keep their non-null
978
+ // container type but default to EMPTY when absent.
979
+ const fieldDecls = [
980
+ ` /// The kernel aggregate id (the \`{type,id,data}\` row's \`id\`) — the id the`,
981
+ ` /// indexed/by-id accessor matched on (e.g. the siteId). Empty string only when`,
982
+ ` /// the aggregate is built from a bare \`data\` map with no row id (e.g. a unit test).`,
983
+ ` final ${idType} aggregateId;`,
984
+ ...specs.map(
985
+ (s) => ` final ${s.dartType}${s.optional ? "?" : ""} ${s.name};`,
986
+ ),
987
+ ].join("\n");
988
+ const ctorParams = [
989
+ ` this.aggregateId = const ${idType}.fromRow(''),`,
990
+ ...specs.map(
991
+ // A required scalar stays `required`; an optional scalar OR a collection is not
992
+ // `required` (an absent optional scalar is null; an absent collection is empty).
993
+ (s) =>
994
+ s.optional || isCollectionKind(s.kind)
995
+ ? ` ${collectionDefaultParam(s)}`
996
+ : ` required this.${s.name},`,
997
+ ),
998
+ ].join("\n");
999
+
1000
+ // fromJson: the FLAT, already-decoded projection `data` map (what `query()` /
1001
+ // `query_by_id()` emit per aggregate — e.g. `{"name":"HQ",
1002
+ // "createdAt":1000,"location":{…}}`), NOT the projection-internal
1003
+ // `{field:{value,stamp}}` shape. Each field reads via a tolerant
1004
+ // flat-projection reader from `subscriptions.dart`. The kernel aggregate [id] is
1005
+ // threaded from the row id (GAP 2) — defaulting to '' for a bare-map call.
1006
+ const parseLines = [
1007
+ ` aggregateId: ${aggregateIdFromRowExpr(agg.id, "id")},`,
1008
+ ...specs.map((s) => ` ${s.name}: ${parseField(s, className)},`),
1009
+ ].join("\n");
1010
+
1011
+ // `==`/`hashCode` include the id (two rows with the same data but different ids are
1012
+ // distinct aggregates). An optional scalar compares with `==` (null-safe); a
1013
+ // collection compares set/map-wise (always non-null after the empty default).
1014
+ const eqLines = [
1015
+ `other.aggregateId == aggregateId`,
1016
+ ...specs.map((s) =>
1017
+ s.kind === "set"
1018
+ ? `other.${s.name}.length == ${s.name}.length && other.${s.name}.containsAll(${s.name})`
1019
+ : s.kind === "map"
1020
+ ? `_mapEq(other.${s.name}, ${s.name})`
1021
+ : `other.${s.name} == ${s.name}`,
1022
+ ),
1023
+ ].join(" &&\n ");
1024
+
1025
+ const hashLines = [
1026
+ ` aggregateId,`,
1027
+ ...specs.map((s) =>
1028
+ s.kind === "set" || s.kind === "map"
1029
+ ? ` Object.hashAll(${s.name}${s.kind === "map" ? ".entries.map((e) => Object.hash(e.key, e.value))" : ""}),`
1030
+ : ` ${s.name},`,
1031
+ ),
1032
+ ].join("\n");
1033
+
1034
+ return [
1035
+ `/// Generated projection aggregate for '${agg.id}'.`,
1036
+ `///`,
1037
+ `/// Deserialized via [${className}.fromJson] from the FLAT, already-decoded`,
1038
+ `/// projection \`data\` map (the \`{type,id,data}\` row \`query()\`/\`query_by_id()\``,
1039
+ `/// emits) — e.g. \`{"name":"HQ","createdAt":1000}\`. The kernel aggregate`,
1040
+ `/// [aggregateId] is carried from the row \`id\`. Read it reactively through the`,
1041
+ `/// generated \`watch…\` accessors below. Collection fields (\`set\`/\`map\`) default to`,
1042
+ `/// EMPTY and \`.optional()\` scalars to \`null\` when a freshly-folded aggregate has`,
1043
+ `/// not yet folded them; must-be-folded scalars still throw when absent.`,
1044
+ `class ${className} {`,
1045
+ fieldDecls,
1046
+ ``,
1047
+ ` const ${className}({`,
1048
+ ctorParams,
1049
+ ` });`,
1050
+ ``,
1051
+ ` /// Parse from the FLAT projection \`data\` map (the \`{type,id,data}\` row's`,
1052
+ ` /// \`data\` that \`query()\`/\`query_by_id()\` emit — fields already`,
1053
+ ` /// decoded to bare JSON), carrying the kernel aggregate [id] from the row \`id\``,
1054
+ ` /// (defaults to '' for a bare-map call). Missing REQUIRED fields throw a`,
1055
+ ` /// [FormatException]; absent collections decode empty and absent \`.optional()\``,
1056
+ ` /// scalars decode \`null\`.`,
1057
+ ` factory ${className}.fromJson(Map<String, dynamic> data, [String id = '']) {`,
1058
+ ` return ${className}(`,
1059
+ parseLines,
1060
+ ` );`,
1061
+ ` }`,
1062
+ ``,
1063
+ ` @override`,
1064
+ ` bool operator ==(Object other) =>`,
1065
+ ` other is ${className} &&`,
1066
+ ` ${eqLines};`,
1067
+ ``,
1068
+ ` @override`,
1069
+ ` int get hashCode => Object.hashAll([`,
1070
+ hashLines,
1071
+ ` ]);`,
1072
+ `}`,
1073
+ ].join("\n");
1074
+ }
1075
+
1076
+ /** The shared, domain-agnostic `EvidenceRef` Dart class (`evidence.md` §9.2) — emitted
1077
+ * ONCE per generated file when any aggregate/projection field is `t.evidence()`. A typed
1078
+ * read of the read engine's clean decoded object (`{hash, mediaType, byteLength,
1079
+ * storageClass}`); it carries the content hash + metadata, NOT the bytes (the read side
1080
+ * fetches the Evidence Store by hash; a shredded ref materialises a typed "redacted").
1081
+ * Pure data + value-equality so it slots into the aggregate's `==`/`hashCode`. */
1082
+ function emitEvidenceRefClass(): string {
1083
+ return [
1084
+ `/// A content-addressed EVIDENCE reference (evidence.md §1/§9.2) — the framework`,
1085
+ `/// commits this (hash + metadata), never the bytes. Read it off an aggregate field`,
1086
+ `/// declared \`t.evidence()\`; fetch the bytes from the Evidence Store BY \`hash\` (a`,
1087
+ `/// shredded ref materialises a typed "redacted", never the bytes). \`hash\` is git's`,
1088
+ `/// blob oid — the SAME content hash for git + external storage (backend-portable).`,
1089
+ `class EvidenceRef {`,
1090
+ ` /// The content hash — git's blob oid (the identity).`,
1091
+ ` final String hash;`,
1092
+ ` /// The declared content type (e.g. \`application/pdf\`).`,
1093
+ ` final String mediaType;`,
1094
+ ` /// The byte length of the referenced content.`,
1095
+ ` final int byteLength;`,
1096
+ ` /// Where the bytes live: 'git' (inline-syncing) or 'external' (shreddable storage).`,
1097
+ ` final String storageClass;`,
1098
+ ``,
1099
+ ` const EvidenceRef({`,
1100
+ ` required this.hash,`,
1101
+ ` required this.mediaType,`,
1102
+ ` required this.byteLength,`,
1103
+ ` required this.storageClass,`,
1104
+ ` });`,
1105
+ ``,
1106
+ ` /// Read from the read engine's CLEAN decoded object`,
1107
+ ` /// (\`{hash, mediaType, byteLength, storageClass}\`).`,
1108
+ ` factory EvidenceRef.fromProjected(Map<String, Object?> m) {`,
1109
+ ` return EvidenceRef(`,
1110
+ ` hash: m['hash']! as String,`,
1111
+ ` mediaType: m['mediaType']! as String,`,
1112
+ ` byteLength: (m['byteLength']! as num).toInt(),`,
1113
+ ` storageClass: m['storageClass']! as String,`,
1114
+ ` );`,
1115
+ ` }`,
1116
+ ``,
1117
+ ` /// The authored payload shape (for dispatch): the typed value-object the DSL`,
1118
+ ` /// \`t.evidence()\` field lowers to a kernel \`Value::Evidence\`.`,
1119
+ ` Map<String, Object?> toJson() => <String, Object?>{`,
1120
+ ` 'hash': hash,`,
1121
+ ` 'mediaType': mediaType,`,
1122
+ ` 'byteLength': byteLength,`,
1123
+ ` 'storageClass': storageClass,`,
1124
+ ` };`,
1125
+ ``,
1126
+ ` @override`,
1127
+ ` bool operator ==(Object other) =>`,
1128
+ ` other is EvidenceRef &&`,
1129
+ ` other.hash == hash &&`,
1130
+ ` other.mediaType == mediaType &&`,
1131
+ ` other.byteLength == byteLength &&`,
1132
+ ` other.storageClass == storageClass;`,
1133
+ ``,
1134
+ ` @override`,
1135
+ ` int get hashCode => Object.hash(hash, mediaType, byteLength, storageClass);`,
1136
+ `}`,
1137
+ ].join("\n");
1138
+ }
1139
+
1140
+ /** True when ANY aggregate field in the module is `t.evidence()` — gates emitting the
1141
+ * shared [emitEvidenceRefClass] once. */
1142
+ function moduleUsesEvidence(mod: DomainModule): boolean {
1143
+ return mod.aggregates.some((a) =>
1144
+ Object.values(a.fields as Record<string, Field>).some((f) => f.kind === "evidence"),
1145
+ );
1146
+ }
1147
+
1148
+ /** The ctor param for a non-`required` field: an optional scalar takes no default
1149
+ * (null), a collection takes an empty-collection default so it is never null. */
1150
+ function collectionDefaultParam(s: AggFieldSpec): string {
1151
+ switch (s.kind) {
1152
+ case "set":
1153
+ return `this.${s.name} = const <String>{},`;
1154
+ case "map":
1155
+ return isJsonValuedMap(s)
1156
+ ? `this.${s.name} = const <String, Object?>{},`
1157
+ : `this.${s.name} = const <String, String>{},`;
1158
+ default:
1159
+ // An optional scalar — nullable, no default needed (defaults to null).
1160
+ return `this.${s.name},`;
1161
+ }
1162
+ }
1163
+
1164
+ /** Expression that extracts one field's typed value from the FLAT, already-decoded
1165
+ * projection `data` map, via a tolerant flat-projection reader (`subscriptions.dart`).
1166
+ *
1167
+ * The projection has ALREADY decoded each kernel `Value` to bare JSON: a
1168
+ * `string`/`ref` field is a bare JSON string; an `int` is a number; a `set` is a
1169
+ * string array; a `map` is a `{k:v}` object (or a structural `Value::Map` form a
1170
+ * spike path may emit); an `enum` is its bare wire string; a `json` field is the
1171
+ * stored JSON which may have re-parsed into a container (re-stringified to keep the
1172
+ * aggregate's `String` type). */
1173
+ function parseField(s: AggFieldSpec, aggregate: string): string {
1174
+ // A `.optional()` scalar reads through the `…OrNull` tolerant reader (returns null
1175
+ // when the flat `data` lacks it — a freshly-folded aggregate need not have folded
1176
+ // it); a required scalar keeps the strict reader (absence throws). Collections read
1177
+ // through their empty-defaulting reader (absence ⇒ an empty collection).
1178
+ const opt = s.optional;
1179
+ switch (s.kind) {
1180
+ case "ref": {
1181
+ // A ref reads its id STRING, then wraps it via `.fromRow` (NOT the public
1182
+ // `.fromMinted`, which asserts the type tag). A ref may
1183
+ // legitimately point at a not-yet-minted aggregate whose id carries no
1184
+ // type tag; enforcing the tag is the CREATE gate's job, not a read's. When the
1185
+ // target type is unknown the field stays a bare `String`.
1186
+ const reader = opt
1187
+ ? `readStrOrNull(data, '${s.wireName}')`
1188
+ : `readStr(data, '${aggregate}', '${s.wireName}')`;
1189
+ if (s.refIdClass === undefined) return reader;
1190
+ return opt
1191
+ ? `() { final v = ${reader}; return v == null ? null : ${s.refIdClass}.fromRow(v); }()`
1192
+ : `${s.refIdClass}.fromRow(${reader})`;
1193
+ }
1194
+ case "string":
1195
+ if (s.valueClass !== undefined) {
1196
+ const reader = opt
1197
+ ? `readStrOrNull(data, '${s.wireName}')`
1198
+ : `readStr(data, '${aggregate}', '${s.wireName}')`;
1199
+ return opt
1200
+ ? `() { final v = ${reader}; return v == null ? null : ${s.valueClass}.fromRow(v); }()`
1201
+ : `${s.valueClass}.fromRow(${reader})`;
1202
+ }
1203
+ return opt
1204
+ ? `readStrOrNull(data, '${s.wireName}')`
1205
+ : `readStr(data, '${aggregate}', '${s.wireName}')`;
1206
+ case "json":
1207
+ // A `json`-kind field is typed `String` on the aggregate; the projection may
1208
+ // have parsed its stored JSON into a container — re-stringify to keep `String`.
1209
+ return opt
1210
+ ? `readJsonStrOrNull(data, '${s.wireName}')`
1211
+ : `readJsonStr(data, '${aggregate}', '${s.wireName}')`;
1212
+ case "int":
1213
+ return opt
1214
+ ? `readIntOrNull(data, '${aggregate}', '${s.wireName}')`
1215
+ : `readInt(data, '${aggregate}', '${s.wireName}')`;
1216
+ case "enum":
1217
+ return opt
1218
+ ? `readEnumOrNull(data, '${aggregate}', '${s.wireName}', ${s.dartType}.fromWire)`
1219
+ : `readEnum(data, '${aggregate}', '${s.wireName}', ${s.dartType}.fromWire)`;
1220
+ case "evidence":
1221
+ // An EvidenceRef reads the read engine's CLEAN decoded object
1222
+ // (`{hash, mediaType, byteLength, storageClass}`) via the same `readJsonObject`
1223
+ // reader a `t.jsonObject()` leaf uses, then wraps it in the typed `EvidenceRef`.
1224
+ return opt
1225
+ ? `() { final m = readJsonObjectOrNull(data, '${aggregate}', '${s.wireName}'); return m == null ? null : EvidenceRef.fromProjected(m); }()`
1226
+ : `EvidenceRef.fromProjected(readJsonObject(data, '${aggregate}', '${s.wireName}'))`;
1227
+ case "set":
1228
+ // Collections are always empty-defaulting (absent ⇒ empty), never required.
1229
+ return `readSetOrEmpty(data, '${aggregate}', '${s.wireName}')`;
1230
+ case "map":
1231
+ // A `json`-valued map decodes each entry value back to its OBJECT (so a
1232
+ // value-object map — e.g. `customFields`, each entry the canonical
1233
+ // `CustomField.toJson()` — round-trips losslessly); a string-valued map stays flat.
1234
+ return isJsonValuedMap(s)
1235
+ ? `readJsonMapOrEmpty(data, '${aggregate}', '${s.wireName}')`
1236
+ : `readStrMapOrEmpty(data, '${aggregate}', '${s.wireName}')`;
1237
+ }
1238
+ }
1239
+
1240
+ // ---- generated read model class --------------------------------------------
1241
+ //
1242
+ // A typed read model class generated from aggregate DSL field declarations. It
1243
+ // decodes the flat `{type,id,data}` projection rows emitted by the read engine.
1244
+
1245
+ /** The projection class name for an aggregate wire type: strip the
1246
+ * trailing `RootAggregate`/`Aggregate` (the SAME stem the typed-id naming uses, so
1247
+ * `SiteRootAggregate` → `Site`) and append `ReadModel` (`SiteReadModel`,
1248
+ * `BuildingReadModel`, `ThingReadModel`, `TrackableThingReadModel`). Pure + deterministic
1249
+ * — the same wire type always yields the same name. */
1250
+ function readModelClassName(aggregateType: string): string {
1251
+ let stem = aggregateType;
1252
+ if (stem.endsWith("RootAggregate")) {
1253
+ stem = stem.slice(0, -"RootAggregate".length);
1254
+ } else if (stem.endsWith("Aggregate")) {
1255
+ stem = stem.slice(0, -"Aggregate".length);
1256
+ }
1257
+ return `${cap(stem === "" ? aggregateType : stem)}ReadModel`;
1258
+ }
1259
+
1260
+ /** The Dart type for one generated read-model field. */
1261
+ function readModelDartType(s: AggFieldSpec): string {
1262
+ switch (s.kind) {
1263
+ case "ref":
1264
+ return s.refIdClass ?? "String";
1265
+ case "json":
1266
+ // A `t.jsonObject()` leaf (`jsonShape:"object"`, e.g. `location`) is the DECODED
1267
+ // structured JSON OBJECT (`Map<String, Object?>`, strict). A plain `t.json()` leaf
1268
+ // has an UNKNOWN/non-object decoded shape (a double/bool/list authored as a JSON
1269
+ // leaf — the kernel `Value` is Str|Int only), so it is surfaced as the permissive
1270
+ // `Object?` (the read engine's decoded value as-is — never coerced/thrown).
1271
+ return isJsonObjectLeaf(s) ? "Map<String, Object?>" : "Object?";
1272
+ case "string":
1273
+ return s.valueClass ?? "String";
1274
+ case "int":
1275
+ return "int";
1276
+ case "enum":
1277
+ return s.dartType; // the generated enum type
1278
+ case "evidence":
1279
+ return "EvidenceRef"; // the shared typed content-addressed ref class
1280
+ case "set":
1281
+ return "Set<String>";
1282
+ case "map":
1283
+ return isJsonValuedMap(s) ? "Map<String, Object?>" : "Map<String, String>";
1284
+ }
1285
+ }
1286
+
1287
+ /** The ctor param for a non-`required` projection field: an optional scalar takes no
1288
+ * default (null); a collection takes its empty-collection default. Mirrors
1289
+ * [collectionDefaultParam] but a `json` leaf's container is `Map<String, Object?>`. */
1290
+ function readModelDefaultParam(s: AggFieldSpec): string {
1291
+ switch (s.kind) {
1292
+ case "set":
1293
+ return `this.${s.name} = const <String>{},`;
1294
+ case "map":
1295
+ return isJsonValuedMap(s)
1296
+ ? `this.${s.name} = const <String, Object?>{},`
1297
+ : `this.${s.name} = const <String, String>{},`;
1298
+ default:
1299
+ // An optional scalar, an optional OBJECT json leaf (nullable `Map`), OR a
1300
+ // permissive plain-json leaf (already-nullable `Object?`) — nullable, defaults to
1301
+ // null (no ctor default needed).
1302
+ return `this.${s.name},`;
1303
+ }
1304
+ }
1305
+
1306
+ /** Expression that extracts one field's typed value from the flat read-engine
1307
+ * `data` map via `subscriptions.dart` readers. */
1308
+ function parseReadModelField(s: AggFieldSpec, aggregate: string): string {
1309
+ const opt = s.optional;
1310
+ switch (s.kind) {
1311
+ case "ref": {
1312
+ const reader = opt
1313
+ ? `readStrOrNull(data, '${s.wireName}')`
1314
+ : `readStr(data, '${aggregate}', '${s.wireName}')`;
1315
+ if (s.refIdClass === undefined) return reader;
1316
+ return opt
1317
+ ? `() { final v = ${reader}; return v == null ? null : ${s.refIdClass}.fromRow(v); }()`
1318
+ : `${s.refIdClass}.fromRow(${reader})`;
1319
+ }
1320
+ case "string":
1321
+ if (s.valueClass !== undefined) {
1322
+ const reader = opt
1323
+ ? `readStrOrNull(data, '${s.wireName}')`
1324
+ : `readStr(data, '${aggregate}', '${s.wireName}')`;
1325
+ return opt
1326
+ ? `() { final v = ${reader}; return v == null ? null : ${s.valueClass}.fromRow(v); }()`
1327
+ : `${s.valueClass}.fromRow(${reader})`;
1328
+ }
1329
+ return opt
1330
+ ? `readStrOrNull(data, '${s.wireName}')`
1331
+ : `readStr(data, '${aggregate}', '${s.wireName}')`;
1332
+ case "json":
1333
+ // An OBJECT-asserted leaf (`t.jsonObject()`, e.g. `location`): the read engine has
1334
+ // decoded the stored JSON into a structured object — surface it as a STRICT
1335
+ // `Map<String, Object?>` (a present-but-non-object value throws; the strict-decode
1336
+ // contract). A PERMISSIVE plain `t.json()` leaf has an unknown/non-object decoded
1337
+ // shape (a double/bool/list authored as a JSON leaf), so surface the read engine's
1338
+ // decoded value AS-IS via the NON-THROWING `readJsonValueOrNull` (`Object?`, null
1339
+ // when absent) — NEVER a strict-object reader that would throw on a legit non-object.
1340
+ if (isPermissiveJsonLeaf(s)) {
1341
+ return `readJsonValueOrNull(data, '${s.wireName}')`;
1342
+ }
1343
+ return opt
1344
+ ? `readJsonObjectOrNull(data, '${aggregate}', '${s.wireName}')`
1345
+ : `readJsonObject(data, '${aggregate}', '${s.wireName}')`;
1346
+ case "int":
1347
+ return opt
1348
+ ? `readIntOrNull(data, '${aggregate}', '${s.wireName}')`
1349
+ : `readInt(data, '${aggregate}', '${s.wireName}')`;
1350
+ case "enum":
1351
+ return opt
1352
+ ? `readEnumOrNull(data, '${aggregate}', '${s.wireName}', ${s.dartType}.fromWire)`
1353
+ : `readEnum(data, '${aggregate}', '${s.wireName}', ${s.dartType}.fromWire)`;
1354
+ case "evidence":
1355
+ // Same clean-object read + typed wrap as the aggregate class.
1356
+ return opt
1357
+ ? `() { final m = readJsonObjectOrNull(data, '${aggregate}', '${s.wireName}'); return m == null ? null : EvidenceRef.fromProjected(m); }()`
1358
+ : `EvidenceRef.fromProjected(readJsonObject(data, '${aggregate}', '${s.wireName}'))`;
1359
+ case "set":
1360
+ return `readSetOrEmpty(data, '${aggregate}', '${s.wireName}')`;
1361
+ case "map":
1362
+ return isJsonValuedMap(s)
1363
+ ? `readJsonMapOrEmpty(data, '${aggregate}', '${s.wireName}')`
1364
+ : `readStrMapOrEmpty(data, '${aggregate}', '${s.wireName}')`;
1365
+ }
1366
+ }
1367
+
1368
+ /** Emit the generated typed read-model class for one aggregate. The class
1369
+ * (`SiteReadModel`, …) carries the kernel aggregate [aggregateId] (threaded from the
1370
+ * `{type,id,data}` row id like the aggregate class) plus a DSL-named typed field per DSL
1371
+ * field declaration, and a [fromProjected] factory that reads the read engine's
1372
+ * already-decoded projected `data` shape. */
1373
+ function emitReadModel(
1374
+ agg: AggregateHandle,
1375
+ enumDartTypes: Map<string, string>,
1376
+ deriveds: DerivedDecl[] = [],
1377
+ combineds: CombinedDecl[] = [],
1378
+ refIdClassForAggregate: (aggregateType: string) => string = typedIdClassForAggregateType,
1379
+ ownIdFields: ReadonlySet<string> = new Set(),
1380
+ ): string {
1381
+ const className = readModelClassName(agg.id);
1382
+ const idType = aggregateIdDartType(agg.id);
1383
+ const wireType = agg.id;
1384
+ const specs = aggregateFieldSpecs(
1385
+ agg,
1386
+ enumDartTypes,
1387
+ refIdClassForAggregate,
1388
+ ownIdFields,
1389
+ );
1390
+
1391
+ // DERIVED/COMBINED read fields are engine-projected JSON leaves. Their value schema is
1392
+ // declared in the DSL via `.returns(z...)`, so the Dart surface is typed instead of
1393
+ // `Object?`. The fields stay nullable because a freshly mounted projection can be
1394
+ // observed before the engine pass has materialised them.
1395
+
1396
+ // Field decls: the kernel aggregate id first (threaded from the row id, like the
1397
+ // aggregate class), then each DSL-named domain field. An optional scalar (incl. an
1398
+ // optional json-object leaf) is nullable; a PERMISSIVE json leaf is already the
1399
+ // nullable `Object?` (no extra `?`); a collection keeps its non-null container.
1400
+ const fieldDecls = [
1401
+ ` /// The kernel aggregate id (the \`{type,id,data}\` row's \`id\`, e.g. the siteId).`,
1402
+ ` /// Empty string only when built from a bare \`data\` map with no row id (a unit test).`,
1403
+ ` final ${idType} aggregateId;`,
1404
+ ...specs.map(
1405
+ (s) => ` final ${readModelDartType(s)}${readModelNullableSuffix(s)} ${s.name};`,
1406
+ ),
1407
+ ...deriveds.map(
1408
+ (d) =>
1409
+ ` /// Engine-projected DERIVED read field (pure fn of the folded fields; NOT kernel state).\n final ${projectionReturnDartType(d.returns)} ${d.id};`,
1410
+ ),
1411
+ ...combineds.map(
1412
+ (c) =>
1413
+ ` /// Engine-projected COMBINED read field (JOIN across aggregates; NOT kernel state).\n final ${projectionReturnDartType(c.returns)} ${c.id};`,
1414
+ ),
1415
+ ].join("\n");
1416
+ const ctorParams = [
1417
+ ` this.aggregateId = const ${idType}.fromRow(''),`,
1418
+ ...specs.map((s) =>
1419
+ // A required field is `required`; an optional scalar, a collection, OR a
1420
+ // permissive json leaf (already nullable `Object?`) takes its default/null.
1421
+ s.optional || isCollectionKind(s.kind) || isPermissiveJsonLeaf(s)
1422
+ ? ` ${readModelDefaultParam(s)}`
1423
+ : ` required this.${s.name},`,
1424
+ ),
1425
+ // Derived/combined fields are always nullable (null until the engine pass runs).
1426
+ ...deriveds.map((d) => ` this.${d.id},`),
1427
+ ...combineds.map((c) => ` this.${c.id},`),
1428
+ ].join("\n");
1429
+
1430
+ const parseLines = [
1431
+ ` aggregateId: ${aggregateIdFromRowExpr(agg.id, "id")},`,
1432
+ ...specs.map((s) => ` ${s.name}: ${parseReadModelField(s, wireType)},`),
1433
+ // Derived/combined fields decode from the engine-produced bare JSON leaf.
1434
+ ...deriveds.map((d) => ` ${d.id}: ${parseProjectionReturnField(d.id, d.returns)},`),
1435
+ ...combineds.map((c) => ` ${c.id}: ${parseProjectionReturnField(c.id, c.returns)},`),
1436
+ ].join("\n");
1437
+
1438
+ return [
1439
+ `/// Generated read model for '${agg.id}'.`,
1440
+ `///`,
1441
+ `/// Deserialized via [${className}.fromProjected] from the Rust read engine's already-`,
1442
+ `/// decoded projected \`data\` map (the \`{type,id,data}\` row's \`data\`). Fields are`,
1443
+ `/// DSL-named; a \`json\` value-object leaf (e.g. \`location\`)`,
1444
+ `/// is a STRUCTURED \`Map<String, Object?>\` — the decoded shape the engine emits, NOT a`,
1445
+ `/// JSON string. This class is fully DSL-derived.`,
1446
+ `/// Collection fields (\`set\`/\`map\`) default to EMPTY and \`.optional()\` scalars to`,
1447
+ `/// \`null\` when a freshly-folded aggregate has not yet folded them.`,
1448
+ `class ${className} {`,
1449
+ fieldDecls,
1450
+ ``,
1451
+ ` const ${className}({`,
1452
+ ctorParams,
1453
+ ` });`,
1454
+ ``,
1455
+ ` /// Parse from the Rust read engine's already-decoded projected \`data\` map (the`,
1456
+ ` /// \`{type,id,data}\` row's \`data\` that \`query()\`/\`query_by_id()\` emit — every field`,
1457
+ ` /// already decoded to bare/structured JSON), carrying the kernel aggregate [id] from`,
1458
+ ` /// the row \`id\` (defaults to '' for a bare-map call). Missing REQUIRED fields throw a`,
1459
+ ` /// [FormatException]; absent collections decode empty and absent \`.optional()\` scalars`,
1460
+ ` /// decode \`null\`.`,
1461
+ ` factory ${className}.fromProjected(Map<String, dynamic> data, [String id = '']) {`,
1462
+ ` return ${className}(`,
1463
+ parseLines,
1464
+ ` );`,
1465
+ ` }`,
1466
+ `}`,
1467
+ ].join("\n");
1468
+ }
1469
+
1470
+ // ---- public intent payload class -------------------------------------------
1471
+
1472
+ function emitDirective(
1473
+ d: AnyDirective,
1474
+ domainName: string,
1475
+ enumDartTypes: Map<string, string>,
1476
+ ): string {
1477
+ const className = `${cap(d.id)}Payload`;
1478
+ const specs = introspectPayload(d.payloadSchema as z.ZodTypeAny, enumDartTypes);
1479
+
1480
+ // Field decls: optional payload fields are nullable Dart fields (omitted from the
1481
+ // payload JSON when null); required fields are non-null + `required` in the ctor.
1482
+ const fieldDecls = specs
1483
+ .map((s) => ` final ${s.dartType}${s.optional ? "?" : ""} ${s.name};`)
1484
+ .join("\n");
1485
+ const ctorParams = specs
1486
+ .map((s) => ` ${s.optional ? "" : "required "}this.${s.name},`)
1487
+ .join("\n");
1488
+
1489
+ // toPayloadJson: serialise only typed fields to data. No Event, no Hlc; the
1490
+ // GitHolon executes the installed domain plan. Optional
1491
+ // fields are written only when present (mirrors the engine's Zod: ops are emitted
1492
+ // only for present fields).
1493
+ const requiredEntries = specs
1494
+ .filter((s) => !s.optional)
1495
+ .map((s) => ` '${s.name}': ${toJsonExpr(s, s.name)},`);
1496
+ const optionalLines = specs
1497
+ .filter((s) => s.optional)
1498
+ .map(
1499
+ (s) =>
1500
+ ` if (${s.name} != null) '${s.name}': ${toJsonExpr(s, `${s.name}!`)},`,
1501
+ );
1502
+
1503
+ const docFirst = directiveDoc(d);
1504
+
1505
+ return [
1506
+ `/// ${docFirst}`,
1507
+ `///`,
1508
+ `/// Generated typed intent DTO for '${d.id}' (marker: ${d.marker},`,
1509
+ `/// aggregate: ${d.aggregateId}). It carries its [domain] + [intentType] and a`,
1510
+ `/// [toPayloadJson] that ships only typed fields as data. It builds no`,
1511
+ `/// \`Event\` and no \`Hlc\`. Pass it to [dispatch]; the GitHolon executes`,
1512
+ `/// the installed domain plan.`,
1513
+ `class ${className} implements NomosIntent {`,
1514
+ ` /// The policy domain this intent belongs to (the kernel \`author\` \`domain\`).`,
1515
+ ` static const String domainName = '${domainName}';`,
1516
+ ` /// The public intent type within [domainName] — never hand-typed at the call site.`,
1517
+ ` static const String intentTypeName = '${d.id}';`,
1518
+ ` /// The aggregate this intent targets (provenance only; the engine binds the instance).`,
1519
+ ` static const String aggregateId = '${d.aggregateId}';`,
1520
+ ` /// The referential marker (creates/mutates/ensures/archives).`,
1521
+ ` static const String marker = '${d.marker}';`,
1522
+ ``,
1523
+ fieldDecls,
1524
+ ``,
1525
+ ` /// ${docFirst}`,
1526
+ ` const ${className}({`,
1527
+ ctorParams,
1528
+ ` });`,
1529
+ ``,
1530
+ ` @override`,
1531
+ ` String get domain => domainName;`,
1532
+ ``,
1533
+ ` @override`,
1534
+ ` String get intentType => intentTypeName;`,
1535
+ ``,
1536
+ ` /// Serialise this payload to the intent DATA JSON (the shape the`,
1537
+ ` /// domain Zod schema validates inside the engine). Optional fields are`,
1538
+ ` /// omitted when null.`,
1539
+ ` @override`,
1540
+ ` Map<String, Object?> toPayloadJson() => <String, Object?>{`,
1541
+ ...requiredEntries,
1542
+ ...optionalLines,
1543
+ ` };`,
1544
+ `}`,
1545
+ ].join("\n");
1546
+ }
1547
+
1548
+ /** Expression that serialises ONE typed payload field into its JSON value. */
1549
+ export function toJsonExpr(spec: PayloadFieldSpec, accessor: string): string {
1550
+ switch (spec.toJson) {
1551
+ case "scalar":
1552
+ case "strList":
1553
+ case "json":
1554
+ return accessor;
1555
+ case "enum":
1556
+ return `${accessor}.wire`;
1557
+ }
1558
+ }
1559
+
1560
+ // ---- read accessors (typed subscriptions) ----------------------------------
1561
+ //
1562
+ // The READ half of the peer dispatch surface (Jack 2026-06-01): the app reads ONLY via
1563
+ // these generated typed accessors over the GitHolon read surface
1564
+ // (`query(query_id, params_json)` + `query_by_id(aggregate_id)`, #140's indexed/PK
1565
+ // read-closure). It invents NO new Rust read primitive. Two emitters:
1566
+ // * per declared `QueryDecl` → a typed indexed `watch…`/`read…` pair (the
1567
+ // INDEXED, O(matches) reads — `watchSites`, `watchThings`, …);
1568
+ // * per aggregate → a typed by-id `watch…ById`/`read…ById` pair off the PK
1569
+ // `query_by_id` (the single-aggregate reads — `watchSite`, `watchThing`, …).
1570
+ // Every accessor hangs off the one [NomosReads] the app threads in, and carries a
1571
+ // `///` hover-doc (the same self-describing mechanism the directive payloads use).
1572
+
1573
+ /** A Dart-safe method-name fragment from a query id (e.g. `sitesByOwner`). The id
1574
+ * is already a lowerCamel identifier by convention; reserved-word-guarded for safety. */
1575
+ function readMethodName(queryId: string): string {
1576
+ return DART_RESERVED.has(queryId) ? `${queryId}_` : queryId;
1577
+ }
1578
+
1579
+ /** A Dart-safe PARAM identifier from an index-key field, which may be a NESTED
1580
+ * dotted path. Folds `a.b.c` → `aBC`-style lowerCamel (drops the dots, capitalising
1581
+ * each following segment) so it is a legal Dart parameter name; reserved-word-guarded. */
1582
+ function dartParamName(keyField: string): string {
1583
+ const parts = keyField.split(".").filter((p) => p !== "");
1584
+ const camel =
1585
+ parts.length === 0
1586
+ ? "key"
1587
+ : parts[0] + parts.slice(1).map((p) => cap(p)).join("");
1588
+ const cleaned = camel.replace(/[^A-Za-z0-9_]/g, "_");
1589
+ const safe = /^[0-9]/.test(cleaned) ? `v${cleaned}` : cleaned;
1590
+ return DART_RESERVED.has(safe) ? `${safe}_` : safe;
1591
+ }
1592
+
1593
+ /** Emit the typed accessor PAIR for ONE declared query: a reactive `watch<Id>` →
1594
+ * `Stream<List<Agg>>` and a one-shot `read<Id>` → `List<Agg>`. The query's single
1595
+ * index-key field becomes a `String` param (every registered query keys on an
1596
+ * id/ref string). Both call through the [NomosReads] `query`/`watchQuery` seam with
1597
+ * the query id + `{keyField: value}` params + the aggregate's flat `fromJson`. */
1598
+ function emitQueryReads(
1599
+ q: QueryDecl,
1600
+ aggregatesById: Map<string, AggregateHandle>,
1601
+ enumDartTypes: Map<string, string>,
1602
+ refIdClassForAggregate: (aggregateType: string) => string = typedIdClassForAggregateType,
1603
+ ownIdFieldsByAggregate: ReadonlyMap<string, ReadonlySet<string>> = new Map(),
1604
+ ): string {
1605
+ const agg = q.returns; // the aggregate TYPE id (== the generated class name).
1606
+ const method = readMethodName(q.id);
1607
+ const Method = cap(method);
1608
+ // The query registry keys every query on EXACTLY ONE string field; take it as
1609
+ // the param. (A multi-key index is a later step — see the gaps in the report.)
1610
+ const keyField = q.key[0] ?? "key";
1611
+ // The index-key field may be a NESTED DOTTED PATH (e.g.
1612
+ // `structuralAssignment.targetKey`) — that is the JSON key the index probe
1613
+ // wants, but NOT a legal Dart identifier. Sanitize it to a camelCase PARAM name
1614
+ // (`structuralAssignmentTargetKey`) while keeping the dotted path as the
1615
+ // JSON key the params map ships.
1616
+ const paramName = dartParamName(keyField);
1617
+ const keySpec = readKeyParamSpec(
1618
+ paramName,
1619
+ aggregateFieldSpecForKey(
1620
+ aggregatesById,
1621
+ agg,
1622
+ keyField,
1623
+ enumDartTypes,
1624
+ refIdClassForAggregate,
1625
+ ownIdFieldsByAggregate,
1626
+ ),
1627
+ );
1628
+ const params = `'${keyField}': ${keySpec.valueExpr}`;
1629
+ const keyParam = `${keySpec.dartType} ${paramName}`;
1630
+ const words = q.id.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase();
1631
+ return [
1632
+ `/// Reactive typed read: every \`${agg}\` matching the indexed \`${q.id}\` query`,
1633
+ `/// (${words}), keyed on \`${keyField}\`. Emits the current matches, then re-emits`,
1634
+ `/// on each projection tick. O(matches) via the projection index (#140) — never a`,
1635
+ `/// workspace scan.`,
1636
+ `Stream<List<${agg}>> watch${Method}(NomosReads reads, ${keyParam}) =>`,
1637
+ ` reads.watchQuery('${q.id}', {${params}}, ${agg}.fromJson);`,
1638
+ ``,
1639
+ `/// One-shot typed read of the \`${q.id}\` query (${words}): the CURRENT matching`,
1640
+ `/// \`${agg}\` list, keyed on \`${keyField}\`. O(matches).`,
1641
+ `List<${agg}> read${Method}(NomosReads reads, ${keyParam}) =>`,
1642
+ ` reads.readQuery('${q.id}', {${params}}, ${agg}.fromJson);`,
1643
+ ].join("\n");
1644
+ }
1645
+
1646
+ /** Emit the typed by-id accessor PAIR for ONE aggregate: a reactive
1647
+ * `watch<Agg>ById` → `Stream<Agg?>` and a one-shot `read<Agg>ById` → `Agg?`, both
1648
+ * off the PK `query_by_id` seam (resolves the unbound singleton by its type-key AND
1649
+ * an instance-bound aggregate by its id). */
1650
+ function emitAggregateByIdReads(agg: AggregateHandle): string {
1651
+ const cls = agg.id;
1652
+ const idType = aggregateIdDartType(agg.id);
1653
+ return [
1654
+ `/// Reactive typed by-id read: the \`${cls}\` with kernel aggregate id [id]`,
1655
+ `/// (\`null\` until/unless it has been folded). Emits the current value, then`,
1656
+ `/// re-emits on each projection tick. Primary-key lookup (#140) — O(1).`,
1657
+ `Stream<${cls}?> watch${cls}ById(NomosReads reads, ${idType} id) =>`,
1658
+ ` reads.watchById(id.value, ${cls}.fromJson);`,
1659
+ ``,
1660
+ `/// One-shot typed by-id read: the CURRENT \`${cls}\` with kernel aggregate id`,
1661
+ `/// [id], or \`null\` when no such aggregate has been folded. O(1).`,
1662
+ `${cls}? read${cls}ById(NomosReads reads, ${idType} id) =>`,
1663
+ ` reads.readById(id.value, ${cls}.fromJson);`,
1664
+ ].join("\n");
1665
+ }
1666
+
1667
+ // ---- maintained count/sum accessors (Slice 1: .where predicated count + sum) ----
1668
+ //
1669
+ // The read engine maintains an O(1)-lookup `counts` table and (Slice 1) a `sums` table
1670
+ // from the declarations shipped in the runtime manifest. These emitters produce TYPED
1671
+ // `watchCount`/`readCount` and `watchSum`/`readSum` Dart accessors — a `Stream<int>`
1672
+ // and an `int`, never a query, never SQL. The predicate is INVISIBLE to the accessor:
1673
+ // it is baked into the maintained counter. The dev reads `watchPublishedCount(reads,
1674
+ // catalogueId)` and never sees a `where`-clause (the worldview law: domain devs must
1675
+ // NOT understand Nomos internals).
1676
+ //
1677
+ // Note that the accessor is GROUPED (takes a `groupKey` String) when the count/sum
1678
+ // declares a `.by(...)` field, and GROUP-KEY-FREE (no param) for a grand total. The
1679
+ // grand-total variant passes the empty string `''` as the group key (the read engine
1680
+ // uses `''` as the synthetic grand-total group key, matching `count_group_key`'s
1681
+ // `None → Some(String::new())` arm in Rust).
1682
+
1683
+ /** Emit the typed accessor PAIR for ONE declared count (read-engine maintained counter):
1684
+ * a reactive `watch<Id>` → `Stream<int>` and a one-shot `read<Id>` → `int`. Both
1685
+ * call through the [NomosReads] `watchCount`/`readCount` seam with the count id and
1686
+ * (if grouped) the group key. The predicate is INVISIBLE (baked into the maintained
1687
+ * counter). O(1) lookup — never a scan. */
1688
+ function emitCountReads(
1689
+ c: ReturnType<typeof finishCount>,
1690
+ aggregatesById: Map<string, AggregateHandle>,
1691
+ enumDartTypes: Map<string, string>,
1692
+ refIdClassForAggregate: (aggregateType: string) => string = typedIdClassForAggregateType,
1693
+ ownIdFieldsByAggregate: ReadonlyMap<string, ReadonlySet<string>> = new Map(),
1694
+ ): string {
1695
+ const method = readMethodName(c.id);
1696
+ const Method = cap(method);
1697
+ const words = c.id.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase();
1698
+ const byDesc = c.by !== undefined ? `, grouped by \`${c.by}\`` : " (grand total)";
1699
+ if (c.by !== undefined) {
1700
+ const groupSpec = readKeyParamSpec(
1701
+ "groupKey",
1702
+ aggregateFieldSpecForKey(
1703
+ aggregatesById,
1704
+ c.of,
1705
+ c.by,
1706
+ enumDartTypes,
1707
+ refIdClassForAggregate,
1708
+ ownIdFieldsByAggregate,
1709
+ ),
1710
+ );
1711
+ return [
1712
+ `/// Reactive maintained count \`${c.id}\` (${words}${byDesc}). O(1) lookup`,
1713
+ `/// (read-engine maintained counter — never a scan). Emits the current count,`,
1714
+ `/// then re-emits on each projection tick. The predicate is baked in.`,
1715
+ `Stream<int> watch${Method}(NomosReads reads, ${groupSpec.dartType} groupKey) =>`,
1716
+ ` reads.watchCount('${c.id}', ${groupSpec.valueExpr});`,
1717
+ ``,
1718
+ `/// One-shot maintained count \`${c.id}\` (${words}), keyed on [groupKey]. O(1).`,
1719
+ `int read${Method}(NomosReads reads, ${groupSpec.dartType} groupKey) =>`,
1720
+ ` reads.readCount('${c.id}', ${groupSpec.valueExpr});`,
1721
+ ].join("\n");
1722
+ }
1723
+ // Grand total — no groupKey param; pass '' (the synthetic grand-total group key).
1724
+ return [
1725
+ `/// Reactive maintained count \`${c.id}\` (${words}${byDesc}). O(1) lookup`,
1726
+ `/// (read-engine maintained counter — never a scan). Emits the current count,`,
1727
+ `/// then re-emits on each projection tick. The predicate is baked in.`,
1728
+ `Stream<int> watch${Method}(NomosReads reads) =>`,
1729
+ ` reads.watchCount('${c.id}', '');`,
1730
+ ``,
1731
+ `/// One-shot maintained count \`${c.id}\` (${words}) — grand total. O(1).`,
1732
+ `int read${Method}(NomosReads reads) =>`,
1733
+ ` reads.readCount('${c.id}', '');`,
1734
+ ].join("\n");
1735
+ }
1736
+
1737
+ /** Emit the typed accessor PAIR for ONE declared sum (read-engine maintained total):
1738
+ * a reactive `watch<Id>` → `Stream<int>` and a one-shot `read<Id>` → `int`. Mirrors
1739
+ * `emitCountReads` exactly — only the seam name (`watchSum`/`readSum`) differs. */
1740
+ function emitSumReads(
1741
+ s: ReturnType<typeof finishSum>,
1742
+ aggregatesById: Map<string, AggregateHandle>,
1743
+ enumDartTypes: Map<string, string>,
1744
+ refIdClassForAggregate: (aggregateType: string) => string = typedIdClassForAggregateType,
1745
+ ownIdFieldsByAggregate: ReadonlyMap<string, ReadonlySet<string>> = new Map(),
1746
+ ): string {
1747
+ const method = readMethodName(s.id);
1748
+ const Method = cap(method);
1749
+ const words = s.id.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase();
1750
+ const byDesc = s.by !== undefined ? `, grouped by \`${s.by}\`` : " (grand total)";
1751
+ if (s.by !== undefined) {
1752
+ const groupSpec = readKeyParamSpec(
1753
+ "groupKey",
1754
+ aggregateFieldSpecForKey(
1755
+ aggregatesById,
1756
+ s.of,
1757
+ s.by,
1758
+ enumDartTypes,
1759
+ refIdClassForAggregate,
1760
+ ownIdFieldsByAggregate,
1761
+ ),
1762
+ );
1763
+ return [
1764
+ `/// Reactive maintained sum \`${s.id}\` (${words}${byDesc}). O(1) lookup`,
1765
+ `/// (read-engine maintained total — never a scan). Emits the current sum,`,
1766
+ `/// then re-emits on each projection tick. The predicate is baked in.`,
1767
+ `Stream<int> watch${Method}(NomosReads reads, ${groupSpec.dartType} groupKey) =>`,
1768
+ ` reads.watchSum('${s.id}', ${groupSpec.valueExpr});`,
1769
+ ``,
1770
+ `/// One-shot maintained sum \`${s.id}\` (${words}), keyed on [groupKey]. O(1).`,
1771
+ `int read${Method}(NomosReads reads, ${groupSpec.dartType} groupKey) =>`,
1772
+ ` reads.readSum('${s.id}', ${groupSpec.valueExpr});`,
1773
+ ].join("\n");
1774
+ }
1775
+ return [
1776
+ `/// Reactive maintained sum \`${s.id}\` (${words}${byDesc}). O(1) lookup`,
1777
+ `/// (read-engine maintained total — never a scan). Emits the current sum,`,
1778
+ `/// then re-emits on each projection tick. The predicate is baked in.`,
1779
+ `Stream<int> watch${Method}(NomosReads reads) =>`,
1780
+ ` reads.watchSum('${s.id}', '');`,
1781
+ ``,
1782
+ `/// One-shot maintained sum \`${s.id}\` (${words}) — grand total. O(1).`,
1783
+ `int read${Method}(NomosReads reads) =>`,
1784
+ ` reads.readSum('${s.id}', '');`,
1785
+ ].join("\n");
1786
+ }
1787
+
1788
+ /** Emit the typed accessor PAIR for ONE declared min (read-engine maintained minimum):
1789
+ * a reactive `watch<Id>` → `Stream<int?>` and a one-shot `read<Id>` → `int?`.
1790
+ * Returns `int?` (nullable): an empty group has NO minimum, and `0` is a legitimate
1791
+ * minimum value — collapsing empty-group to `0` would be the sum-of-0 ambiguity. Mirrors
1792
+ * `emitSumReads` exactly, differing only in seam name (`watchMin`/`readMin`) and return
1793
+ * type (`int?` vs `int`). The predicate is INVISIBLE (baked into the maintained minimum). */
1794
+ function emitMinReads(
1795
+ e: ReturnType<typeof finishExtremum>,
1796
+ aggregatesById: Map<string, AggregateHandle>,
1797
+ enumDartTypes: Map<string, string>,
1798
+ refIdClassForAggregate: (aggregateType: string) => string = typedIdClassForAggregateType,
1799
+ ownIdFieldsByAggregate: ReadonlyMap<string, ReadonlySet<string>> = new Map(),
1800
+ ): string {
1801
+ const method = readMethodName(e.id);
1802
+ const Method = cap(method);
1803
+ const words = e.id.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase();
1804
+ const byDesc = e.by !== undefined ? `, grouped by \`${e.by}\`` : " (grand total)";
1805
+ if (e.by !== undefined) {
1806
+ const groupSpec = readKeyParamSpec(
1807
+ "groupKey",
1808
+ aggregateFieldSpecForKey(
1809
+ aggregatesById,
1810
+ e.of,
1811
+ e.by,
1812
+ enumDartTypes,
1813
+ refIdClassForAggregate,
1814
+ ownIdFieldsByAggregate,
1815
+ ),
1816
+ );
1817
+ return [
1818
+ `/// Reactive maintained minimum \`${e.id}\` (${words}${byDesc}). O(1) lookup`,
1819
+ `/// (read-engine maintained minimum — never a scan). Emits the current minimum`,
1820
+ `/// (null when no contributing member), then re-emits on each projection tick.`,
1821
+ `/// The predicate is baked in. null = empty group (0 is a legitimate minimum).`,
1822
+ `Stream<int?> watch${Method}(NomosReads reads, ${groupSpec.dartType} groupKey) =>`,
1823
+ ` reads.watchMin('${e.id}', ${groupSpec.valueExpr});`,
1824
+ ``,
1825
+ `/// One-shot maintained minimum \`${e.id}\` (${words}), keyed on [groupKey]. O(1).`,
1826
+ `/// Returns null when no contributing member exists in the group.`,
1827
+ `int? read${Method}(NomosReads reads, ${groupSpec.dartType} groupKey) =>`,
1828
+ ` reads.readMin('${e.id}', ${groupSpec.valueExpr});`,
1829
+ ].join("\n");
1830
+ }
1831
+ return [
1832
+ `/// Reactive maintained minimum \`${e.id}\` (${words}${byDesc}). O(1) lookup.`,
1833
+ `/// Returns null when no contributing member exists.`,
1834
+ `Stream<int?> watch${Method}(NomosReads reads) =>`,
1835
+ ` reads.watchMin('${e.id}', '');`,
1836
+ ``,
1837
+ `/// One-shot maintained minimum \`${e.id}\` (${words}) — grand total. O(1).`,
1838
+ `int? read${Method}(NomosReads reads) =>`,
1839
+ ` reads.readMin('${e.id}', '');`,
1840
+ ].join("\n");
1841
+ }
1842
+
1843
+ /** Emit the typed accessor PAIR for ONE declared max (read-engine maintained maximum):
1844
+ * a reactive `watch<Id>` → `Stream<int?>` and a one-shot `read<Id>` → `int?`.
1845
+ * Returns `int?` (nullable): mirrors `emitMinReads`, differing only in seam name
1846
+ * (`watchMax`/`readMax`). */
1847
+ function emitMaxReads(
1848
+ e: ReturnType<typeof finishExtremum>,
1849
+ aggregatesById: Map<string, AggregateHandle>,
1850
+ enumDartTypes: Map<string, string>,
1851
+ refIdClassForAggregate: (aggregateType: string) => string = typedIdClassForAggregateType,
1852
+ ownIdFieldsByAggregate: ReadonlyMap<string, ReadonlySet<string>> = new Map(),
1853
+ ): string {
1854
+ const method = readMethodName(e.id);
1855
+ const Method = cap(method);
1856
+ const words = e.id.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase();
1857
+ const byDesc = e.by !== undefined ? `, grouped by \`${e.by}\`` : " (grand total)";
1858
+ if (e.by !== undefined) {
1859
+ const groupSpec = readKeyParamSpec(
1860
+ "groupKey",
1861
+ aggregateFieldSpecForKey(
1862
+ aggregatesById,
1863
+ e.of,
1864
+ e.by,
1865
+ enumDartTypes,
1866
+ refIdClassForAggregate,
1867
+ ownIdFieldsByAggregate,
1868
+ ),
1869
+ );
1870
+ return [
1871
+ `/// Reactive maintained maximum \`${e.id}\` (${words}${byDesc}). O(1) lookup`,
1872
+ `/// (read-engine maintained maximum — never a scan). Emits the current maximum`,
1873
+ `/// (null when no contributing member), then re-emits on each projection tick.`,
1874
+ `/// The predicate is baked in. null = empty group (0 is a legitimate maximum).`,
1875
+ `Stream<int?> watch${Method}(NomosReads reads, ${groupSpec.dartType} groupKey) =>`,
1876
+ ` reads.watchMax('${e.id}', ${groupSpec.valueExpr});`,
1877
+ ``,
1878
+ `/// One-shot maintained maximum \`${e.id}\` (${words}), keyed on [groupKey]. O(1).`,
1879
+ `/// Returns null when no contributing member exists in the group.`,
1880
+ `int? read${Method}(NomosReads reads, ${groupSpec.dartType} groupKey) =>`,
1881
+ ` reads.readMax('${e.id}', ${groupSpec.valueExpr});`,
1882
+ ].join("\n");
1883
+ }
1884
+ return [
1885
+ `/// Reactive maintained maximum \`${e.id}\` (${words}${byDesc}). O(1) lookup.`,
1886
+ `/// Returns null when no contributing member exists.`,
1887
+ `Stream<int?> watch${Method}(NomosReads reads) =>`,
1888
+ ` reads.watchMax('${e.id}', '');`,
1889
+ ``,
1890
+ `/// One-shot maintained maximum \`${e.id}\` (${words}) — grand total. O(1).`,
1891
+ `int? read${Method}(NomosReads reads) =>`,
1892
+ ` reads.readMax('${e.id}', '');`,
1893
+ ].join("\n");
1894
+ }
1895
+
1896
+ /** Emit the typed accessor PAIR for ONE declared exists (boolean membership over the
1897
+ * Slice-1 `counts` table): a reactive `watch<Id>` → `Stream<bool>` and a one-shot
1898
+ * `read<Id>` → `bool`. Returns `bool`, NOT `int` — the only shape difference from
1899
+ * `emitCountReads`. The predicate is INVISIBLE (baked into the maintained counter).
1900
+ * `exists` is backed by `readExists`/`watchExists` which internally call `count > 0`
1901
+ * on the `counts` table — zero new Rust state (LAW 4, one engine). */
1902
+ function emitExistsReads(
1903
+ e: ReturnType<typeof finishExists>,
1904
+ aggregatesById: Map<string, AggregateHandle>,
1905
+ enumDartTypes: Map<string, string>,
1906
+ refIdClassForAggregate: (aggregateType: string) => string = typedIdClassForAggregateType,
1907
+ ownIdFieldsByAggregate: ReadonlyMap<string, ReadonlySet<string>> = new Map(),
1908
+ ): string {
1909
+ const method = readMethodName(e.id);
1910
+ const Method = cap(method);
1911
+ const words = e.id.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase();
1912
+ const byDesc = e.by !== undefined ? `, grouped by \`${e.by}\`` : " (grand total)";
1913
+ if (e.by !== undefined) {
1914
+ const groupSpec = readKeyParamSpec(
1915
+ "groupKey",
1916
+ aggregateFieldSpecForKey(
1917
+ aggregatesById,
1918
+ e.of,
1919
+ e.by,
1920
+ enumDartTypes,
1921
+ refIdClassForAggregate,
1922
+ ownIdFieldsByAggregate,
1923
+ ),
1924
+ );
1925
+ return [
1926
+ `/// Reactive maintained existence check \`${e.id}\` (${words}${byDesc}). O(1) lookup`,
1927
+ `/// (backed by the read-engine counts table — count > 0). Emits the current boolean,`,
1928
+ `/// then re-emits on each projection tick. The predicate is baked in.`,
1929
+ `Stream<bool> watch${Method}(NomosReads reads, ${groupSpec.dartType} groupKey) =>`,
1930
+ ` reads.watchExists('${e.id}', ${groupSpec.valueExpr});`,
1931
+ ``,
1932
+ `/// One-shot existence check \`${e.id}\` (${words}), keyed on [groupKey]. O(1).`,
1933
+ `bool read${Method}(NomosReads reads, ${groupSpec.dartType} groupKey) =>`,
1934
+ ` reads.readExists('${e.id}', ${groupSpec.valueExpr});`,
1935
+ ].join("\n");
1936
+ }
1937
+ return [
1938
+ `/// Reactive maintained existence check \`${e.id}\` (${words}${byDesc}). O(1) lookup.`,
1939
+ `Stream<bool> watch${Method}(NomosReads reads) =>`,
1940
+ ` reads.watchExists('${e.id}', '');`,
1941
+ ``,
1942
+ `/// One-shot existence check \`${e.id}\` (${words}) — grand total. O(1).`,
1943
+ `bool read${Method}(NomosReads reads) =>`,
1944
+ ` reads.readExists('${e.id}', '');`,
1945
+ ].join("\n");
1946
+ }
1947
+
1948
+ /** Emit the typed accessor PAIR for ONE declared `first` or `take(n)` ordered-row read:
1949
+ * a reactive `watch<Id>` → `Stream<List<Agg>>` (returns 0-or-1 for first, 0-to-n for
1950
+ * take) and a one-shot `read<Id>` → `List<Agg>`. The `orderKey` is baked into the spec
1951
+ * and INVISIBLE to the accessor — the dev declared it once; they consume an ordered list.
1952
+ * Returns projected typed aggregates (via `project_by_ids`, `lib.rs:1686`) — not raw ids.
1953
+ * Archived aggregates are auto-excluded (the `type_index` row is dropped on archive). */
1954
+ function emitOrderedReads(d: OrderedReadDecl): string {
1955
+ const agg = d.of;
1956
+ const method = readMethodName(d.id);
1957
+ const Method = cap(method);
1958
+ const words = d.id.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase();
1959
+ const orderDesc = `${d.orderDesc ? "descending " : "ascending "}by \`${d.orderKey}\``;
1960
+ if (d.limit === 1) {
1961
+ // first — returns a single projected aggregate or null.
1962
+ return [
1963
+ `/// Reactive ordered-row read \`${d.id}\` (${words}): the FIRST \`${agg}\` ${orderDesc},`,
1964
+ `/// or null when none exist. LIMIT 1 applied after the full ORDER BY (order-sensitive;`,
1965
+ `/// the total order bakes in \`${d.orderKey}\` + aggregate_id tiebreak for determinism).`,
1966
+ `Stream<${agg}?> watch${Method}(NomosReads reads) =>`,
1967
+ ` reads.watchFirst('${d.id}', ${agg}.fromJson);`,
1968
+ ``,
1969
+ `/// One-shot ordered-row read \`${d.id}\` (${words}): the CURRENT first \`${agg}\`, or null.`,
1970
+ `${agg}? read${Method}(NomosReads reads) =>`,
1971
+ ` reads.readFirst('${d.id}', ${agg}.fromJson);`,
1972
+ ].join("\n");
1973
+ }
1974
+ // take(n) — returns a list, n applied after the total ORDER BY.
1975
+ return [
1976
+ `/// Reactive ordered-row read \`${d.id}\` (${words}): the TOP ${d.limit} \`${agg}\` ${orderDesc}.`,
1977
+ `/// LIMIT ${d.limit} applied after the full ORDER BY (order-sensitive; the total order bakes in`,
1978
+ `/// \`${d.orderKey}\` + aggregate_id tiebreak for determinism). Returns an empty list when none exist.`,
1979
+ `Stream<List<${agg}>> watch${Method}(NomosReads reads) =>`,
1980
+ ` reads.watchTake('${d.id}', ${d.limit}, ${agg}.fromJson);`,
1981
+ ``,
1982
+ `/// One-shot ordered-row read \`${d.id}\` (${words}): CURRENT top ${d.limit} \`${agg}\`.`,
1983
+ `List<${agg}> read${Method}(NomosReads reads) =>`,
1984
+ ` reads.readTake('${d.id}', ${d.limit}, ${agg}.fromJson);`,
1985
+ ].join("\n");
1986
+ }
1987
+
1988
+ // ---- minted typed-id creates (the kernel id-mint keystone) -----------------
1989
+ //
1990
+ // A `.creates` directive whose payload carries a target-id field (the id bound to the
1991
+ // NEW aggregate) is a KERNEL-MINTED create: the frontend must NOT pass a raw id. For
1992
+ // such a directive we emit, ALONGSIDE the plain payload DTO:
1993
+ // * a typed id value class (e.g. `SiteId`) wrapping the kernel-minted string, so a
1994
+ // raw `String` is never accepted where a minted id is required, and
1995
+ // * a top-level `create…({Minter mint, …})` factory that calls the KERNEL mint (no
1996
+ // raw id param, NO seed) to RESERVE the typed id and builds the payload from it — so
1997
+ // the only way to construct a minted create at the call site is THROUGH the kernel
1998
+ // mint, and the dev never sees a uuid/seed/nonce.
1999
+ //
2000
+ // THE MODEL: the KERNEL mints `<Type>_<uuidv7>` from the HOST-INJECTED rng; the id is
2001
+ // CAPTURED in the create event and READ on replay (never re-minted). The UUID body is
2002
+ // opaque uniqueness only — NOT time/order; intent HLCs carry ordering. There is NO
2003
+ // `mintSeed` payload field any more — the id itself is the proof. The factory passes
2004
+ // NOTHING to the mint but the aggregate type.
2005
+
2006
+ /**
2007
+ * The target-id field of a minted create: the payload field bound to the aggregate the
2008
+ * directive `.creates`. MARKER-DRIVEN — the kernel mints the `.creates` target's id, so we
2009
+ * identify that field by running the directive's REAL `.plan()` and seeing which payload
2010
+ * value becomes the created aggregate's `__id` (the ground truth), NOT by guessing from the
2011
+ * field name. This is correct for a COMPOSED create that also mutates a sibling: e.g.
2012
+ * `correctThingLabel` CREATES `RepairRecord` (keyed by its own id) AND MUTATES an existing
2013
+ * `TrackableThing` (keyed by `thingId`) — the name heuristic would mis-pick `thingId`; the
2014
+ * plan-trace picks the field that is the RepairRecord create's `__id`.
2015
+ *
2016
+ * Mechanism: probe each top-level STRING field with a unique sentinel, run `.plan()`, find
2017
+ * the `Create` event whose `__type` == the directive's `.creates` aggregate, and map its
2018
+ * `__id` sentinel back to the payload field. Returns `undefined` when the created aggregate's
2019
+ * id is not a verbatim top-level payload string (then no minting factory is emitted).
2020
+ *
2021
+ * `agg` is the `.creates` target handle (`directive.aggregateId`), required by `executeDirectiveToIntent`.
2022
+ * If the plan cannot be traced, emit no factory. Guessing by field name makes the
2023
+ * public Dart surface lie about aggregate identity.
2024
+ */
2025
+ function mintedIdField(
2026
+ d: AnyDirective,
2027
+ agg: AggregateHandle | undefined,
2028
+ specs: PayloadFieldSpec[],
2029
+ ): PayloadFieldSpec | undefined {
2030
+ return agg !== undefined ? mintedIdFieldByPlan(d, agg, specs) : undefined;
2031
+ }
2032
+
2033
+ /** Marker-driven identification: run the REAL `.plan()` with sentinel string values and
2034
+ * find which payload field becomes the `.creates`-target aggregate's `__id`. Returns
2035
+ * `undefined` if the plan throws or the created id is not a verbatim top-level string. */
2036
+ function mintedIdFieldByPlan(
2037
+ d: AnyDirective,
2038
+ agg: AggregateHandle,
2039
+ specs: PayloadFieldSpec[],
2040
+ ): PayloadFieldSpec | undefined {
2041
+ const stringFields = specs.filter((s) => s.dartType === "String");
2042
+ if (stringFields.length === 0) return undefined;
2043
+ const sentinelToField = new Map<string, string>();
2044
+ for (const s of stringFields) sentinelToField.set(`__MINT_PROBE_${s.name}__`, s.name);
2045
+ let payload: Record<string, unknown>;
2046
+ try {
2047
+ payload = buildProbePayload(d.payloadSchema as z.ZodTypeAny, sentinelToField);
2048
+ } catch {
2049
+ return undefined; // could not build a schema-valid probe
2050
+ }
2051
+ let intent: ReturnType<typeof executeDirectiveToIntent>;
2052
+ try {
2053
+ intent = executeDirectiveToIntent(d, agg, payload as never, deterministicPorts({ physical: 1, replica: 1 }));
2054
+ } catch {
2055
+ return undefined; // plan/Zod threw on the probe
2056
+ }
2057
+ // The created aggregate's event: marker Create + the `__type` stamp == the .creates target.
2058
+ for (const ev of intent.events) {
2059
+ if (ev.marker !== "Create") continue;
2060
+ const typeOp = ev.ops.find((o) => o.field === "__type");
2061
+ const typeVal = typeOp && "Set" in typeOp.op ? (typeOp.op.Set as { Str?: string }).Str : undefined;
2062
+ if (typeVal !== agg.id) continue;
2063
+ const idOp = ev.ops.find((o) => o.field === "__id");
2064
+ const idVal = idOp && "Set" in idOp.op ? (idOp.op.Set as { Str?: string }).Str : undefined;
2065
+ const fieldName = idVal !== undefined ? sentinelToField.get(idVal) : undefined;
2066
+ if (fieldName !== undefined) return specs.find((s) => s.name === fieldName);
2067
+ }
2068
+ return undefined;
2069
+ }
2070
+
2071
+ /** Build a minimal, schema-VALID probe payload for a directive: every top-level STRING field
2072
+ * gets its unique sentinel (so the `.plan()` trace can map the create's `__id` back to a
2073
+ * field); every other field gets a minimal valid sample. Throws if a field type cannot be
2074
+ * sampled (the caller falls back to the name heuristic). */
2075
+ function buildProbePayload(
2076
+ schema: z.ZodTypeAny,
2077
+ sentinelToField: Map<string, string>,
2078
+ ): Record<string, unknown> {
2079
+ if (zodTypeName(schema) !== "ZodObject") throw new Error("payload is not a z.object");
2080
+ const fieldBySentinel = new Map<string, string>();
2081
+ for (const [sent, name] of sentinelToField) fieldBySentinel.set(name, sent);
2082
+ const shape = zodObjectShape(schema);
2083
+ const out: Record<string, unknown> = {};
2084
+ for (const [name, ztRaw] of Object.entries(shape)) {
2085
+ let zt = ztRaw as z.ZodTypeAny;
2086
+ while (isOptionalOrDefault(zt)) {
2087
+ zt = zodInnerType(zt);
2088
+ }
2089
+ const sentinel = fieldBySentinel.get(name);
2090
+ out[name] = sentinel !== undefined && zodTypeName(zt) === "ZodString"
2091
+ ? sentinel
2092
+ : sampleFromZod(zt);
2093
+ }
2094
+ return out;
2095
+ }
2096
+
2097
+ /** A minimal valid sample value for a Zod type — enough for a directive `.plan()` probe. */
2098
+ function sampleFromZod(zt: z.ZodTypeAny): unknown {
2099
+ let t = zt;
2100
+ while (isOptionalOrDefault(t)) {
2101
+ t = zodInnerType(t);
2102
+ }
2103
+ switch (zodTypeName(t)) {
2104
+ case "ZodString":
2105
+ return "x";
2106
+ case "ZodNumber":
2107
+ return 1;
2108
+ case "ZodBoolean":
2109
+ return false;
2110
+ case "ZodEnum":
2111
+ return zodEnumValues(t)[0];
2112
+ case "ZodArray":
2113
+ return [];
2114
+ case "ZodObject": {
2115
+ const shape = zodObjectShape(t);
2116
+ const o: Record<string, unknown> = {};
2117
+ for (const [k, v] of Object.entries(shape)) o[k] = sampleFromZod(v as z.ZodTypeAny);
2118
+ return o;
2119
+ }
2120
+ case "ZodRecord":
2121
+ return {};
2122
+ case "ZodThing":
2123
+ return {};
2124
+ case "ZodUnion": {
2125
+ const opt = zodUnionOptions(t)[0];
2126
+ if (opt === undefined) throw new Error("ZodUnion has no options to sample");
2127
+ return sampleFromZod(opt);
2128
+ }
2129
+ case "ZodLiteral":
2130
+ return zodLiteralValue(t);
2131
+ case "ZodNullable":
2132
+ return null;
2133
+ default:
2134
+ throw new Error(`cannot sample Zod type '${zodTypeName(t)}'`);
2135
+ }
2136
+ }
2137
+
2138
+ /** The Dart typed-id class name for a minted create's id field. Unified on the
2139
+ * aggregate WIRE TYPE (so `createSite`'s `siteId` → `SiteId`, the same name a ref
2140
+ * targeting `SiteRootAggregate` uses), NOT on the payload field name.
2141
+ * The minting factory uses `FooId._(mintedId)` (the truly private ctor, same file). */
2142
+ function typedIdClassName(aggregateType: string): string {
2143
+ return typedIdClassForAggregateType(aggregateType);
2144
+ }
2145
+
2146
+ /** Emit a typed id value class for an aggregate TYPE. Type-safe: a raw `String` — or
2147
+ * another aggregate's typed id — is a COMPILE ERROR where a [${cls}] is required (the
2148
+ * value classes are distinct nominal types). Public constructors:
2149
+ * * [${cls}] / [${cls}.fromString] — non-strict wrappers for app route params,
2150
+ * existing refs, and test fixtures.
2151
+ * * [${cls}.fromMinted] — STRICT: asserts the kernel type tag matches.
2152
+ * One row-id constructor:
2153
+ * * [${cls}.fromRow] — wraps a kernel-stored row/ref id when deserializing a
2154
+ * projection row. This intentionally does NOT enforce the minted type tag because
2155
+ * reads may include not-yet-minted ids; write admission remains the mint gate.
2156
+ */
2157
+ function emitTypedIdClass(aggregateType: string): string {
2158
+ const cls = typedIdClassName(aggregateType);
2159
+ return [
2160
+ `/// Typed id for \`${aggregateType}\`. A DISTINCT nominal type so a raw String — or`,
2161
+ `/// ANOTHER aggregate's typed id — is a COMPILE ERROR where a [${cls}] is required.`,
2162
+ `/// [fromMinted] is the strict constructor for ids reserved by the kernel mint.`,
2163
+ `/// [${cls}] / [fromString] / [fromRow] are non-strict wrappers for route params,`,
2164
+ `/// existing refs, and generated projection decoding.`,
2165
+ `class ${cls} {`,
2166
+ ` /// The kernel-minted id string (\`${aggregateType}_<uuidv7>\`).`,
2167
+ ` /// The UUID body is opaque uniqueness only — NOT time/order; intent HLCs carry ordering.`,
2168
+ ` final String value;`,
2169
+ ` const ${cls}(this.value);`,
2170
+ ` const ${cls}._(this.value);`,
2171
+ ` factory ${cls}.fromString(String value) => ${cls}.fromRow(value);`,
2172
+ ` /// Wrap a kernel-stored row/ref id while decoding a projection row. This is`,
2173
+ ` /// intentionally non-strict: old ledgers and cross-domain refs may not carry the`,
2174
+ ` /// minted type tag, so admission/minting is enforced on writes, not reads.`,
2175
+ ` const ${cls}.fromRow(this.value);`,
2176
+ ` /// Wrap an ALREADY-kernel-minted id string (e.g. one read back from a query).`,
2177
+ ` /// STRICT: asserts the type tag matches \`${aggregateType}\` — a wrong-typed id throws.`,
2178
+ ` /// A minted id is \`<TypeTag>_<uuidv7>\`, so the tag is the prefix before the FIRST \`_\`.`,
2179
+ ` /// Do not read time or ordering from the UUID body; it is an opaque uniqueness token.`,
2180
+ ` factory ${cls}.fromMinted(String minted) {`,
2181
+ ` final i = minted.indexOf('_');`,
2182
+ ` final tag = i < 0 ? minted : minted.substring(0, i);`,
2183
+ ` if (tag != '${aggregateType}') {`,
2184
+ ` throw ArgumentError('id "$minted" is not a ${aggregateType} id (tag "$tag")');`,
2185
+ ` }`,
2186
+ ` return ${cls}._(minted);`,
2187
+ ` }`,
2188
+ ` @override`,
2189
+ ` bool operator ==(Object other) => other is ${cls} && other.value == value;`,
2190
+ ` @override`,
2191
+ ` int get hashCode => value.hashCode;`,
2192
+ ` @override`,
2193
+ ` String toString() => '${cls}($value)';`,
2194
+ `}`,
2195
+ ].join("\n");
2196
+ }
2197
+
2198
+ /** Emit the top-level minted `create…(...)` factory function: it calls the KERNEL mint
2199
+ * (NO raw id param, NO seed) to RESERVE a typed id and builds the payload from it,
2200
+ * returning `(payload, typedId)`. This is the ONLY way to construct a minted create at the
2201
+ * call site — the dev supplies NOTHING about the id. The factory itself contains NO
2202
+ * id/uuid/seed construction — only the runtime kernel-mint call via [mint]. */
2203
+ function emitMintedFactory(
2204
+ d: AnyDirective,
2205
+ _domainName: string,
2206
+ idField: PayloadFieldSpec,
2207
+ specs: PayloadFieldSpec[],
2208
+ aggregateType: string,
2209
+ ): string {
2210
+ const className = `${cap(d.id)}Payload`;
2211
+ const idCls = typedIdClassName(aggregateType);
2212
+ const fnName = d.id; // e.g. `createSite` — the typed minting factory.
2213
+ // The caller supplies every field EXCEPT the minted id (the kernel mint produces it).
2214
+ // Optional fields become optional named params. There is NO seed/author param: the
2215
+ // kernel mints the UUIDv7 from the host-injected rng — the dev touches none of it.
2216
+ const callerFields = specs.filter((s) => s.name !== idField.name);
2217
+ const ctorParams = callerFields
2218
+ .map((s) => ` ${s.optional ? "" : "required "}${s.dartType}${s.optional ? "?" : ""} ${s.name},`)
2219
+ .join("\n");
2220
+ const forwarded = callerFields.map((s) => ` ${s.name}: ${s.name},`).join("\n");
2221
+ return [
2222
+ `/// KERNEL-MINTED \`${d.id}\` factory: calls [mint] (the kernel id-mint) to RESERVE a`,
2223
+ `/// fresh typed [${idCls}] — NO raw id param, NO seed — and ships the minted id in the`,
2224
+ `/// payload, so the kernel GATE verifies its shape + type tag on author. The kernel`,
2225
+ `/// mints the \`${aggregateType}_<uuidv7>\` from the HOST-INJECTED rng (native OsRng / web`,
2226
+ `/// crypto). The UUID body is opaque uniqueness only — NOT time/order; intent HLCs carry`,
2227
+ `/// ordering. This factory builds NO id/uuid/seed itself — only the runtime mint call.`,
2228
+ `/// Returns the [${className}] AND the minted [${idCls}] (so the caller can reference the`,
2229
+ `/// new aggregate). This is the type-safe create path: the frontend cannot supply a raw id.`,
2230
+ `(${className}, ${idCls}) ${fnName}({`,
2231
+ ` required Minter mint,`,
2232
+ ctorParams,
2233
+ `}) {`,
2234
+ ` final mintedId = mint(aggregateType: '${aggregateType}');`,
2235
+ ` final payload = ${className}(`,
2236
+ ` ${idField.name}: mintedId,`,
2237
+ forwarded,
2238
+ ` );`,
2239
+ ` return (payload, ${idCls}._(mintedId));`,
2240
+ `}`,
2241
+ ].join("\n");
2242
+ }
2243
+
2244
+ /** Emit the typed-id keystone for a domain (GENERALIZED across ALL aggregates):
2245
+ * 1. a DISTINCT typed id value class PER AGGREGATE (so cross-aggregate type
2246
+ * confusion is a compile error + ref fields have a target type to name); and
2247
+ * 2. a kernel-minting `create…` factory for every `.creates` directive whose real
2248
+ * plan trace proves a payload field is the created aggregate id.
2249
+ * Every aggregate gets a typed id even if it has no minting factory (a singleton /
2250
+ * kernel-minted create), because OTHER aggregates' ref fields may target it.
2251
+ * Returns `""` when the domain has no aggregates (nothing to emit). */
2252
+ function emitMintedCreates(
2253
+ aggregates: AggregateHandle[],
2254
+ directives: AnyDirective[],
2255
+ domainName: string,
2256
+ enumDartTypes: Map<string, string>,
2257
+ ): string {
2258
+ const blocks: string[] = [];
2259
+
2260
+ // (1) A typed id class per aggregate. Dedup by the typed-id class NAME: two wire
2261
+ // types never collide on a name (the name is a pure function of the type), and a
2262
+ // domain that re-exports a framework aggregate must not emit its id class twice.
2263
+ const emittedIdClasses = new Set<string>();
2264
+ for (const agg of aggregates) {
2265
+ const cls = typedIdClassForAggregateType(agg.id);
2266
+ if (emittedIdClasses.has(cls)) continue;
2267
+ emittedIdClasses.add(cls);
2268
+ blocks.push(emitTypedIdClass(agg.id));
2269
+ }
2270
+
2271
+ // (2) A minting factory per qualifying create — the kernel mints the `.creates` target's
2272
+ // id, identified MARKER-DRIVEN by running the real `.plan()` (see `mintedIdField`).
2273
+ const aggById = new Map<string, AggregateHandle>();
2274
+ for (const a of aggregates) aggById.set(a.id, a);
2275
+ for (const d of directives) {
2276
+ if (d.marker !== "creates") continue;
2277
+ const specs = introspectPayload(d.payloadSchema as z.ZodTypeAny, enumDartTypes);
2278
+ const idField = mintedIdField(d, aggById.get(d.aggregateId), specs);
2279
+ if (idField === undefined) continue;
2280
+ const aggregateType = d.aggregateId;
2281
+ blocks.push(emitMintedFactory(d, domainName, idField, specs, aggregateType));
2282
+ }
2283
+ return blocks.join("\n\n");
2284
+ }
2285
+
2286
+ function collectOwnIdFieldsByAggregate(
2287
+ aggregates: AggregateHandle[],
2288
+ directives: AnyDirective[],
2289
+ enumDartTypes: Map<string, string>,
2290
+ ): Map<string, Set<string>> {
2291
+ const out = new Map<string, Set<string>>();
2292
+ const aggById = new Map<string, AggregateHandle>();
2293
+ for (const a of aggregates) aggById.set(a.id, a);
2294
+ for (const d of directives) {
2295
+ if (d.marker !== "creates") continue;
2296
+ const agg = aggById.get(d.aggregateId);
2297
+ if (agg === undefined) continue;
2298
+ const specs = introspectPayload(d.payloadSchema as z.ZodTypeAny, enumDartTypes);
2299
+ const idField = mintedIdField(d, agg, specs);
2300
+ if (idField === undefined) continue;
2301
+ const field = (agg.fields as Record<string, Field>)[idField.name];
2302
+ if (field?.kind !== "string") continue;
2303
+ const fields = out.get(agg.id) ?? new Set<string>();
2304
+ fields.add(idField.name);
2305
+ out.set(agg.id, fields);
2306
+ }
2307
+ return out;
2308
+ }
2309
+
2310
+ // ---- top-level generator ---------------------------------------------------
2311
+
2312
+ export interface DomainModule {
2313
+ /** Dart file basename (no extension), e.g. "sample_thing". */
2314
+ name: string;
2315
+ /**
2316
+ * The policy domain key the GitHolon `author` resolves the intent against (the engine
2317
+ * registry key, e.g. `thing_structures` / `identity`). Defaults to [name] when
2318
+ * omitted — they coincide for most domains, but the `identity` registry key differs
2319
+ * from any single file basename, so it is INJECTABLE.
2320
+ */
2321
+ domain?: string;
2322
+ aggregates: AggregateHandle[];
2323
+ directives: AnyDirective[];
2324
+ /**
2325
+ * The domain's NAMED, INDEXED read declarations (read-side closure step 1).
2326
+ * ADDITIVE + OPTIONAL: a domain that declares no query omits this entirely and is
2327
+ * byte-identical in the canonical manifest to before `queries` existed. Wired the
2328
+ * same way `aggregates`/`directives` are; carried into the manifest/USD IR by
2329
+ * `manifest.ts`/`usd.ts` with omit-when-empty identity semantics.
2330
+ */
2331
+ queries?: QueryDecl[];
2332
+ /**
2333
+ * The domain's NAMED, MAINTAINED count declarations (read-side closure step 3 — the
2334
+ * aggregation analogue of {@link queries}). ADDITIVE + OPTIONAL: a domain that
2335
+ * declares no count omits this entirely and is byte-identical in the canonical
2336
+ * manifest to before `counts` existed. Wired the same way `queries` is; carried into
2337
+ * the read manifest by `emit_manifests.ts` and the identity manifest by `manifest.ts`
2338
+ * with omit-when-empty identity semantics. The read engine (`nomos_readmodel`)
2339
+ * maintains an O(1)-lookup counter table from these declarations. Accepts either a
2340
+ * grouped `count(id).of(T).by(field)` declaration or a bare grand-total
2341
+ * `count(id).of(T)` builder (both are `AnyCount`; `finishCount` normalizes them).
2342
+ */
2343
+ counts?: AnyCount[];
2344
+ /**
2345
+ * The domain's NAMED, MAINTAINED SPATIAL index declarations (read-side closure — the
2346
+ * GEOSPATIAL analogue of {@link counts}; the keystone of `docs/placement_and_spatial_reads.md`).
2347
+ * Each is a `spatial(id).of(T).on(geometryField)` declaration backed by SQLite R*Tree: the
2348
+ * read engine maintains a bounding-box index over the aggregate's GeoJSON geometry field so a
2349
+ * "within bbox" / "nearest" lookup is an R*Tree probe, NOT an in-VM scan. ADDITIVE + OPTIONAL:
2350
+ * a domain that declares no spatial index omits this entirely and is byte-identical in the
2351
+ * canonical manifest to before `spatials` existed (omit-when-empty, like {@link counts}/
2352
+ * {@link deriveds}). Carried into the read manifest + identity manifest by `manifest.ts` with
2353
+ * omit-when-empty identity semantics.
2354
+ */
2355
+ spatials?: SpatialDecl[];
2356
+ /**
2357
+ * The domain's NAMED, MAINTAINED sum declarations (read-side closure — the numeric-
2358
+ * accumulation analogue of {@link counts}). ADDITIVE + OPTIONAL: a domain that
2359
+ * declares no sum omits this entirely and is byte-identical in the canonical manifest
2360
+ * to before `sums` existed. Each entry is an `AnySum` (either a grand-total builder or
2361
+ * a grouped `SumDecl`); `finishSum` normalizes both. The read engine maintains an
2362
+ * O(1)-lookup `sums` table from these declarations.
2363
+ */
2364
+ sums?: AnySum[];
2365
+ /**
2366
+ * The domain's NAMED, MAINTAINED min declarations (read-side closure — the
2367
+ * minimum analogue of {@link sums}). ADDITIVE + OPTIONAL. The read engine maintains
2368
+ * an `extrema` table per (spec_id, group_key) recomputed via a bounded O(log n)
2369
+ * `SELECT MIN` over the `extremum_members` B-tree index. Generates `int?`-typed
2370
+ * (nullable) accessors: an empty group has NO minimum (0 is a legitimate minimum
2371
+ * value — `null` = no contributing member, per LAW 3). No `.orderBy` exposed (min
2372
+ * is order-INDEPENDENT — LAW 3). `finishExtremum` normalizes both builder + decl.
2373
+ */
2374
+ mins?: AnyExtremum[];
2375
+ /**
2376
+ * The domain's NAMED, MAINTAINED max declarations (read-side closure — the
2377
+ * maximum analogue of {@link mins}). ADDITIVE + OPTIONAL. Same maintenance strategy
2378
+ * as mins. Generates `int?`-typed (nullable) accessors. No `.orderBy` exposed.
2379
+ */
2380
+ maxes?: AnyExtremum[];
2381
+ /**
2382
+ * The domain's BOOLEAN EXISTENCE checks (read-side closure — the membership-test
2383
+ * analogue of {@link counts}). ADDITIVE + OPTIONAL. ZERO new IR: each `exists` entry
2384
+ * is serialized into the `counts` array in the manifest (the Rust engine sees an
2385
+ * ordinary count). The ONLY difference is in the generated accessor: it returns
2386
+ * `bool` = `count > 0`, not `int` (the `_existsMarker` lives only in the DSL/codegen
2387
+ * layer). No `.orderBy` exposed (membership is order-INDEPENDENT — LAW 3).
2388
+ */
2389
+ exists_?: AnyExists[];
2390
+ /**
2391
+ * The domain's ORDERED ROW READS (first/take.orderBy — order-SENSITIVE reads).
2392
+ * ADDITIVE + OPTIONAL. Each entry is a FINISHED `OrderedReadDecl` (the type-level
2393
+ * guardrail ensures it is UN-CONSTRUCTIBLE without `.orderBy(key)` — a
2394
+ * `first("x").of(T)` without `.orderBy` is not assignable here → COMPILE ERROR).
2395
+ * The generated accessors return projected typed aggregates (not raw ids), with
2396
+ * `LIMIT n` applied strictly after the full `ORDER BY <key>, aggregate_id` (the
2397
+ * mandatory tiebreak that makes the order TOTAL even on key-ties — LAW 1).
2398
+ */
2399
+ orderedReads?: OrderedReadDecl[];
2400
+ /**
2401
+ * The domain's WORKSPACE INVARIANTS (#266 — consistency level 2): HARD, synchronous,
2402
+ * admission-time predicates over a BOUNDED, DECLARED multi-aggregate/multi-domain
2403
+ * post-apply read-set. ADDITIVE + OPTIONAL: a domain that declares none omits this
2404
+ * entirely and is byte-identical in the canonical manifest to before this key existed.
2405
+ * PRESENCE ONLY (id + the directive it is `on`) is hashed by `manifest.ts`; the executable
2406
+ * `reads`/`assert` bodies ship in the engine bundle, NEVER the ledger (mirrors the
2407
+ * aggregate `hasInvariant` / a directive `plan`). See `docs/workspace_invariant.md`.
2408
+ */
2409
+ workspaceInvariants?: WorkspaceInvariantDecl[];
2410
+ /**
2411
+ * The domain's ENGINE-PROJECTED DERIVED read fields (read-engine: derived read fields).
2412
+ * Each is a PURE fn of ONE aggregate's folded fields, computed BY THE ENGINE during the
2413
+ * projection projection and stored ONLY in the read model (NEVER stamped into the
2414
+ * ledger). ADDITIVE + OPTIONAL: a domain that declares no derived field omits this
2415
+ * entirely. Carried into the read manifest by `emit_manifests.ts` (the `{id, of}` decl —
2416
+ * the fn SOURCE is NOT in the manifest; it ships in the engine bundle's `planReport`
2417
+ * derive-dispatch) and the identity manifest by `manifest.ts` with omit-when-empty
2418
+ * semantics (the fn body is NOT hashed, exactly like a directive's `.plan` body). The
2419
+ * read engine (`nomos_readmodel`) stores each derived value as an ordinary projected
2420
+ * field, so a reader decodes it like any other field.
2421
+ */
2422
+ deriveds?: DerivedDecl[];
2423
+ /**
2424
+ * The domain's ENGINE-PROJECTED COMBINED read fields (read-engine: combined read
2425
+ * fields) — the JOIN analogue of {@link deriveds}. Each is a PURE fn of ONE owner
2426
+ * aggregate's folded fields AND a RELATED aggregate's folded fields (reached by a typed
2427
+ * ref edge), computed BY THE ENGINE during the projection projection and stored ONLY in
2428
+ * the read model (NEVER stamped into the ledger). ADDITIVE + OPTIONAL: a domain that
2429
+ * declares no combined field omits this entirely. Carried into the read manifest by
2430
+ * `emit_manifests.ts` (the `{id, of, refField, reads}` decl — the fn SOURCE ships in the
2431
+ * engine bundle's `planReport` derive/combine-dispatch, NOT the manifest) and the
2432
+ * identity manifest by `manifest.ts` with omit-when-empty semantics (the fn body is NOT
2433
+ * hashed). The `reads` type is the CROSS-AGGREGATE INVALIDATION key: the read engine
2434
+ * recomputes every owner's combined field when a projection delta touches that type.
2435
+ */
2436
+ combineds?: CombinedDecl[];
2437
+ /**
2438
+ * Wire types (e.g. `["SiteRootAggregate"]`) for which to emit generated read
2439
+ * model classes (`SiteReadModel`). A domain that omits this emits no read-model
2440
+ * classes. Generalizing = widening this allowlist (or, once
2441
+ * proven across the field-kind matrix, defaulting it to every aggregate). A type not in
2442
+ * `aggregates` is ignored (nothing to emit). NOT carried into any manifest/hash — it is
2443
+ * a pure FRONTEND codegen choice, so no domain identity changes.
2444
+ */
2445
+ readModelAggregates?: string[];
2446
+ /**
2447
+ * The domain's `impureCapability` capabilities (framework Order→Receipt; `impure_capability.ts`)
2448
+ * for which to ALSO emit the typed PROVIDER SDK — the symmetric twin of the peer
2449
+ * payload classes. For each declared task the codegen emits a typed `XOrder` (+
2450
+ * `fromJson`), `XResult`/`XFailure`, a `XHandler` typedef and a `XProvider` dispatcher
2451
+ * that decodes the incoming Order JSON and encodes the Receipt (reusing the
2452
+ * peer-codegen `CompleteXPayload`/`FailXPayload`, so the Receipt is an ordinary
2453
+ * intent through the ONE write path). ADDITIVE + OPTIONAL: a domain that omits this is
2454
+ * byte-identical to before. NOT carried into any manifest/hash — a pure provider-side
2455
+ * codegen choice, like {@link readModelAggregates}. Each entry is a {@link ImpureCapabilityDecl}
2456
+ * (the `impureCapability(...)` factory return value satisfies it structurally).
2457
+ */
2458
+ impureCapabilities?: ImpureCapabilityDecl[];
2459
+ /**
2460
+ * Optional permission/RBAC vocabulary declared by a tenant domain. This is not
2461
+ * executable law; it is generated frontend vocabulary over the domain-declared
2462
+ * resource types and role catalogue so app code never invents role strings in
2463
+ * Dart.
2464
+ */
2465
+ permissionVocabulary?: PermissionVocabulary;
2466
+ /**
2467
+ * Extra Dart imports needed by this generated file. Used when a domain has typed
2468
+ * refs to aggregate id classes emitted by another generated domain file.
2469
+ */
2470
+ dartImports?: DartImport[];
2471
+ /**
2472
+ * Mapping from referenced aggregate wire type to the Dart import prefix that exposes
2473
+ * that aggregate's typed id class. This preserves cross-domain ref typing without
2474
+ * forcing every domain into one giant generated library.
2475
+ */
2476
+ dartRefImports?: DartRefImport[];
2477
+ }
2478
+
2479
+ /** Generate the full Dart source for one domain module. */
2480
+ export function generateDartDomain(mod: DomainModule): string {
2481
+ const domainKey = mod.domain ?? mod.name;
2482
+ const { dartTypes, decls } = collectEnums(mod.aggregates, mod.directives);
2483
+ const refIdClassForAggregate = refIdClassResolver(mod);
2484
+ const aggregatesById = new Map<string, AggregateHandle>();
2485
+ for (const a of mod.aggregates) aggregatesById.set(a.id, a);
2486
+ const ownIdFieldsByAggregate = collectOwnIdFieldsByAggregate(
2487
+ mod.aggregates,
2488
+ mod.directives,
2489
+ dartTypes,
2490
+ );
2491
+ const enumBlock = decls.join("\n\n");
2492
+ const permissionVocabularyBlock = emitPermissionVocabulary(mod.permissionVocabulary);
2493
+ const aggBlock = mod.aggregates
2494
+ .map((a) =>
2495
+ emitAggregate(
2496
+ a,
2497
+ dartTypes,
2498
+ refIdClassForAggregate,
2499
+ ownIdFieldsByAggregate.get(a.id) ?? new Set(),
2500
+ ),
2501
+ )
2502
+ .join("\n\n");
2503
+ const dirBlock = mod.directives
2504
+ .map((d) => emitDirective(d, domainKey, dartTypes))
2505
+ .join("\n\n");
2506
+
2507
+ // TYPED IDS + MINTED CREATES (the id-mint keystone, GENERALIZED): a DISTINCT typed
2508
+ // id class for EVERY aggregate (cross-aggregate type-confusion is a compile error;
2509
+ // ref fields name their target's typed id), plus a top-level `create…` factory that
2510
+ // mints THROUGH the kernel (no raw id) for every `.creates` directive carrying a
2511
+ // `mintSeed` + an own-id field.
2512
+ const mintedBlock = emitMintedCreates(mod.aggregates, mod.directives, domainKey, dartTypes);
2513
+
2514
+ // PROVIDER SDK (the symmetric twin of the peer payload classes): for each declared
2515
+ // `impureCapability` capability, a typed Order/Result/Failure + handler typedef + dispatcher.
2516
+ // ADDITIVE + omit-when-empty: a domain with no `impureCapabilities` emits nothing (and the
2517
+ // `provider.dart` import is omitted), so it is byte-identical to before. The enum map is
2518
+ // SHARED with the directive/aggregate codegen so an enum-typed order param references the
2519
+ // already-emitted domain enum (no duplicate decl).
2520
+ const providerBlock = emitProviderSdk(mod.impureCapabilities ?? [], dartTypes);
2521
+
2522
+ // Read-model classes emitted for aggregate wire types selected by
2523
+ // `readModelAggregates`.
2524
+ // additive frontend codegen — no manifest/hash impact.
2525
+ const readModelTypes = new Set(mod.readModelAggregates ?? []);
2526
+ // DERIVED read fields by aggregate type (read-engine: derived read fields): each
2527
+ // `…ReadModel` ALSO carries the engine-projected derived fields its type declares (e.g.
2528
+ // `SupportSessionReadModel.isTerminal`), decoded like a normal projected field from the
2529
+ // row's `data` (the Rust read engine stores it there as a json-leaf).
2530
+ const derivedsByType = new Map<string, DerivedDecl[]>();
2531
+ for (const d of mod.deriveds ?? []) {
2532
+ const list = derivedsByType.get(d.of) ?? [];
2533
+ list.push(d);
2534
+ derivedsByType.set(d.of, list);
2535
+ }
2536
+ // COMBINED read fields by OWNER aggregate type (read-engine: combined read fields):
2537
+ // the JOIN analogue — each `…ReadModel` ALSO carries the engine-projected combined
2538
+ // fields its type owns (e.g. `BuildingReadModel.siteName`), decoded like derived.
2539
+ const combinedsByType = new Map<string, CombinedDecl[]>();
2540
+ for (const c of mod.combineds ?? []) {
2541
+ const list = combinedsByType.get(c.of) ?? [];
2542
+ list.push(c);
2543
+ combinedsByType.set(c.of, list);
2544
+ }
2545
+ const readModelBlock = mod.aggregates
2546
+ .filter((a) => readModelTypes.has(a.id))
2547
+ .map((a) =>
2548
+ emitReadModel(
2549
+ a,
2550
+ dartTypes,
2551
+ derivedsByType.get(a.id) ?? [],
2552
+ combinedsByType.get(a.id) ?? [],
2553
+ refIdClassForAggregate,
2554
+ ownIdFieldsByAggregate.get(a.id) ?? new Set(),
2555
+ ),
2556
+ )
2557
+ .join("\n\n");
2558
+
2559
+ // READ accessors: per declared query (indexed reads) + per aggregate (by-id PK
2560
+ // reads). A domain that declares no `queries` still gets the by-id reads for its
2561
+ // aggregates (they map onto `query_by_id`, which needs no registration).
2562
+ // SLICE 1: also emit per declared count (maintained counter O(1) read) and per
2563
+ // declared sum (maintained total O(1) read). These are NEW in Slice 1 — the spec
2564
+ // §5 claim that "codegen already emits count reads" was wrong; this is the actual
2565
+ // first emission. The predicate is baked into the maintained counter and is
2566
+ // INVISIBLE to the generated accessor (worldview law).
2567
+ const queryReads = (mod.queries ?? [])
2568
+ .map((q) =>
2569
+ emitQueryReads(
2570
+ q,
2571
+ aggregatesById,
2572
+ dartTypes,
2573
+ refIdClassForAggregate,
2574
+ ownIdFieldsByAggregate,
2575
+ ),
2576
+ )
2577
+ .join("\n\n");
2578
+ const countReads = (mod.counts ?? [])
2579
+ .map(finishCount)
2580
+ .map((c) =>
2581
+ emitCountReads(
2582
+ c,
2583
+ aggregatesById,
2584
+ dartTypes,
2585
+ refIdClassForAggregate,
2586
+ ownIdFieldsByAggregate,
2587
+ ),
2588
+ )
2589
+ .join("\n\n");
2590
+ const sumReads = (mod.sums ?? [])
2591
+ .map(finishSum)
2592
+ .map((s) =>
2593
+ emitSumReads(
2594
+ s,
2595
+ aggregatesById,
2596
+ dartTypes,
2597
+ refIdClassForAggregate,
2598
+ ownIdFieldsByAggregate,
2599
+ ),
2600
+ )
2601
+ .join("\n\n");
2602
+ // SLICE 2a: min/max (order-independent extrema, int?-typed), exists (bool, count>0),
2603
+ // first/take (order-sensitive row reads, ordered by declared key + aggregate_id tiebreak).
2604
+ const minReads = (mod.mins ?? [])
2605
+ .map(finishExtremum)
2606
+ .map((e) =>
2607
+ emitMinReads(
2608
+ e,
2609
+ aggregatesById,
2610
+ dartTypes,
2611
+ refIdClassForAggregate,
2612
+ ownIdFieldsByAggregate,
2613
+ ),
2614
+ )
2615
+ .join("\n\n");
2616
+ const maxReads = (mod.maxes ?? [])
2617
+ .map(finishExtremum)
2618
+ .map((e) =>
2619
+ emitMaxReads(
2620
+ e,
2621
+ aggregatesById,
2622
+ dartTypes,
2623
+ refIdClassForAggregate,
2624
+ ownIdFieldsByAggregate,
2625
+ ),
2626
+ )
2627
+ .join("\n\n");
2628
+ const existsReads = (mod.exists_ ?? [])
2629
+ .map(finishExists)
2630
+ .map((e) =>
2631
+ emitExistsReads(
2632
+ e,
2633
+ aggregatesById,
2634
+ dartTypes,
2635
+ refIdClassForAggregate,
2636
+ ownIdFieldsByAggregate,
2637
+ ),
2638
+ )
2639
+ .join("\n\n");
2640
+ const orderedReadBlock = (mod.orderedReads ?? []).map(emitOrderedReads).join("\n\n");
2641
+ const byIdReads = mod.aggregates.map(emitAggregateByIdReads).join("\n\n");
2642
+ const readBlock = [queryReads, countReads, sumReads, minReads, maxReads, existsReads, orderedReadBlock, byIdReads]
2643
+ .filter((s) => s !== "")
2644
+ .join("\n\n");
2645
+
2646
+ // `_mapEq` is only referenced by aggregate `==`/`hashCode` for MAP fields. Emit it
2647
+ // ONLY when some aggregate has a map field, else a map-less domain trips an
2648
+ // unused-element lint.
2649
+ const hasMapField = mod.aggregates.some((a) =>
2650
+ Object.values(a.fields as Record<string, Field>).some((f) => f.kind === "map"),
2651
+ );
2652
+ const helperBlock = hasMapField
2653
+ ? [
2654
+ `// ---- helpers ----`,
2655
+ `bool _mapEq<K, V>(Map<K, V> a, Map<K, V> b) {`,
2656
+ ` if (a.length != b.length) return false;`,
2657
+ ` for (final e in a.entries) {`,
2658
+ ` if (!b.containsKey(e.key) || b[e.key] != e.value) return false;`,
2659
+ ` }`,
2660
+ ` return true;`,
2661
+ `}`,
2662
+ ]
2663
+ : [];
2664
+
2665
+ return [
2666
+ `// GENERATED by nomos2/dsl/src/codegen_dart.ts — DO NOT EDIT BY HAND.`,
2667
+ `//`,
2668
+ `// Source of truth: the '${mod.name}' domain (ts_packages/co2-nomos-domains/src/domains/`,
2669
+ `// for tenant domains; nomos2/dsl framework domains for the re-exported framework ones).`,
2670
+ `// Regenerate: \`npx tsx src/emit_dart.ts\` from ts_packages/co2-nomos-domains.`,
2671
+ `//`,
2672
+ `// ignore_for_file: unnecessary_this, prefer_const_constructors, constant_identifier_names`,
2673
+ `// ignore_for_file: unused_import`,
2674
+ ``,
2675
+ // The generated tenant domains live in the `co2_nomos_domains` package and depend
2676
+ // on the framework `nomos_dsl` package by its PUBLIC package: URI — NOT a relative
2677
+ // `../wire.dart` path into the framework (which broke when the generated dir moved
2678
+ // out of nomos_dsl into the co-located tenant package). The framework barrel
2679
+ // re-exports wire/dispatch/subscriptions/provider, so one import covers them all.
2680
+ `import 'package:nomos_dsl/nomos_dsl.dart';`,
2681
+ ...(mod.dartImports ?? []).map(dartImportLine),
2682
+ ``,
2683
+ `// ---- enums ----`,
2684
+ enumBlock,
2685
+ ``,
2686
+ ...(permissionVocabularyBlock !== ""
2687
+ ? [
2688
+ `// ---- permission vocabulary (generated from domain RBAC declarations) ----`,
2689
+ permissionVocabularyBlock,
2690
+ ``,
2691
+ ]
2692
+ : []),
2693
+ ...(moduleUsesEvidence(mod)
2694
+ ? [
2695
+ `// ---- evidence (content-addressed EvidenceRef; evidence.md §9.2) ----`,
2696
+ emitEvidenceRefClass(),
2697
+ ``,
2698
+ ]
2699
+ : []),
2700
+ `// ---- aggregates (projection types) ----`,
2701
+ aggBlock,
2702
+ ``,
2703
+ ...(readModelBlock !== ""
2704
+ ? [
2705
+ `// ---- generated read models (from aggregate DSL fields) ----`,
2706
+ readModelBlock,
2707
+ ``,
2708
+ ]
2709
+ : []),
2710
+ `// ---- public intents (typed DTOs; ship via the generic dispatch() verb) ----`,
2711
+ dirBlock,
2712
+ ``,
2713
+ ...(mintedBlock !== ""
2714
+ ? [
2715
+ `// ---- typed ids + minted creates (kernel id-mint: per-aggregate typed id classes + create… factories) ----`,
2716
+ mintedBlock,
2717
+ ``,
2718
+ ]
2719
+ : []),
2720
+ `// ---- read accessors (typed subscriptions over the GitHolon query()/query_by_id() surface) ----`,
2721
+ readBlock,
2722
+ ``,
2723
+ ...(providerBlock !== ""
2724
+ ? [
2725
+ `// ---- provider SDK (capability providers; the symmetric twin of the peer payload classes) ----`,
2726
+ providerBlock,
2727
+ ``,
2728
+ ]
2729
+ : []),
2730
+ ...helperBlock,
2731
+ ].join("\n");
2732
+ }