@githolon/dsl 0.100.6 → 0.101.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dart/CHANGELOG.md +6 -0
  2. package/dart/lib/src/dev_cloud.dart +46 -15
  3. package/dart/lib/src/test_harness.dart +78 -16
  4. package/dart/lib/src/webview_bridge.dart +640 -126
  5. package/dart/pubspec.lock +1 -1
  6. package/dart/pubspec.yaml +2 -2
  7. package/dart/test/bundle_version_pairing_test.dart +11 -22
  8. package/dart/test/co2_product_gauntlet_test.dart +228 -0
  9. package/dart/test/errors_test.dart +63 -12
  10. package/dart/test/first_app_gauntlet_test.dart +29 -3
  11. package/dart/test/webview_bridge_test.dart +274 -0
  12. package/dart_types/CHANGELOG.md +6 -0
  13. package/dart_types/lib/src/errors.dart +136 -30
  14. package/dart_types/lib/src/surfaced.dart +48 -2
  15. package/dart_types/lib/src/wire.dart +3 -1
  16. package/dart_types/pubspec.yaml +1 -1
  17. package/dart_types/test/surfaced_test.dart +19 -0
  18. package/package.json +1 -1
  19. package/src/activity.ts +43 -4
  20. package/src/aggregate.ts +132 -22
  21. package/src/attested_core.ts +1 -1
  22. package/src/authoring.ts +119 -6
  23. package/src/birth_meter.ts +4 -4
  24. package/src/birth_meter_ensure.ts +9 -1
  25. package/src/births.ts +53 -22
  26. package/src/build_package.ts +55 -14
  27. package/src/business_birth.ts +48 -7
  28. package/src/codegen_dart.ts +267 -37
  29. package/src/codegen_dart_client.ts +921 -82
  30. package/src/codegen_openapi.ts +28 -5
  31. package/src/codegen_proof.ts +107 -11
  32. package/src/codegen_ts.ts +961 -135
  33. package/src/compile_package_main.ts +155 -45
  34. package/src/conflicts.ts +252 -0
  35. package/src/describe_model.ts +448 -0
  36. package/src/directive.ts +5 -38
  37. package/src/drivers.ts +12 -4
  38. package/src/engine_entry.ts +4 -0
  39. package/src/evolve_preview.ts +72 -11
  40. package/src/fields.ts +201 -55
  41. package/src/framework/application_home.ts +103 -0
  42. package/src/framework/application_home_domain.ts +2 -0
  43. package/src/framework/attested_receipts.ts +2 -2
  44. package/src/framework/auth_provider_law.ts +51 -24
  45. package/src/framework/business_corrections.ts +170 -0
  46. package/src/framework/disclosure.ts +1 -1
  47. package/src/framework/domain_lifecycle.ts +250 -16
  48. package/src/framework/encryption_identity.ts +1 -1
  49. package/src/framework/encryption_scope.ts +21 -13
  50. package/src/framework/era_certification.ts +1 -1
  51. package/src/framework/framework_aggregates.ts +2 -0
  52. package/src/framework/genesis.ts +1 -1
  53. package/src/framework/home.ts +22 -9
  54. package/src/framework/identity.ts +5 -5
  55. package/src/framework/impure_capability.ts +2 -2
  56. package/src/framework/owned_custody.ts +561 -0
  57. package/src/framework/platform_admin.ts +1 -1
  58. package/src/framework/relations.ts +9 -1
  59. package/src/framework/repair.ts +1 -1
  60. package/src/framework/signing_identity.ts +2 -2
  61. package/src/framework/source_roster.ts +1 -1
  62. package/src/framework/workspaces.ts +17 -10
  63. package/src/generated_client_names.ts +39 -0
  64. package/src/index.ts +20 -15
  65. package/src/kernel_capabilities.ts +25 -1
  66. package/src/law_check.ts +4 -1
  67. package/src/manifest.ts +356 -63
  68. package/src/read_projection_manifest.ts +8 -0
  69. package/src/read_projection_namespace.ts +4 -0
  70. package/src/relation.ts +1 -1
  71. package/src/shape_compat.ts +47 -2
  72. package/src/stable_ids.ts +38 -6
  73. package/src/stable_ids_types.ts +30 -0
  74. package/src/surfaced_lint.ts +0 -0
  75. package/src/usd.ts +62 -11
  76. package/src/usd_layers.ts +54 -3
  77. package/src/value_object.ts +23 -19
  78. package/src/wire.ts +1 -0
  79. package/src/wire_encode.ts +48 -4
  80. package/src/workspace_routing.ts +57 -0
  81. package/src/workspace_sharding.ts +9 -9
  82. package/src/certified_read.ts +0 -313
@@ -28,7 +28,13 @@
28
28
  */
29
29
  import type { z } from "zod";
30
30
 
31
- import type { AggregateHandle } from "./aggregate.js";
31
+ import {
32
+ BUSINESS_BIRTH_SEED_PAYLOAD_FIELD,
33
+ OWNED_CUSTODY_PAYLOAD_FIELD,
34
+ OWNED_KEY_PAYLOAD_FIELD,
35
+ OWNED_PREMINTED_RECORDS_PAYLOAD_FIELD,
36
+ type AggregateHandle,
37
+ } from "./aggregate.js";
32
38
  import type { Field } from "./fields.js";
33
39
  import type { Directive } from "./directive.js";
34
40
  import type { QueryDecl } from "./query.js";
@@ -45,6 +51,13 @@ import {
45
51
  import { finishCount } from "./count.js";
46
52
  import { finishSum } from "./sum.js";
47
53
  import { autoStampFields, domainEncryptionMap, type EncDomainMap, type InterfaceAggShape } from "./codegen_ts.js";
54
+ import { isNamespacedAggregate, qualifyReadProjectionType } from "./read_projection_namespace.js";
55
+ import { publicClientInterface, type DomainInterface } from "./shape_compat.js";
56
+ import { BUSINESS_CORRECTION_DIRECTIVE } from "./framework/business_corrections.js";
57
+ import { deriveDirectiveRecordTarget } from "./workspace_routing.js";
58
+ import { buildNomosDescribeModel, type NomosDescribeModel } from "./describe_model.js";
59
+ import { assertModelClientMemberAvailable } from "./generated_client_names.js";
60
+ import { ownedRecordActionDirectiveId } from "./framework/owned_custody.js";
48
61
 
49
62
  const camel = (s: string) => s.replace(/[_\s-]+(\w)/g, (_m, c: string) => c.toUpperCase());
50
63
  const lcFirst = (s: string) => (s.length ? s[0]!.toLowerCase() + s.slice(1) : s);
@@ -98,8 +111,8 @@ function dartKeyParam(key: string): string {
98
111
  /** Parse ONE bridge row (`{type,id,data}`) into a typed read model, composing the canonical `.ref` from the
99
112
  * session workspace (`_ws`, a getter on the client) + the row's type/id. Works for a `Map` (one-shot
100
113
  * `query`/`queryById` rows) or a `dynamic` (`watch` stream rows) — indexing resolves on both. */
101
- function rowToModel(model: string, rowVar: string): string {
102
- return `${model}.fromProjected((${rowVar}['data'] as Map).cast<String, dynamic>(), (${rowVar}['id'] ?? '').toString(), _ws, (${rowVar}['type'] ?? '').toString())`;
114
+ function rowToModel(model: string, rowVar: string, workspace = "_ws"): string {
115
+ return `${model}.fromProjected((${rowVar}['data'] as Map).cast<String, dynamic>(), (${rowVar}['id'] ?? '').toString(), ${workspace}, (${rowVar}['type'] ?? '').toString())`;
103
116
  }
104
117
 
105
118
  export interface DartBridgeClientOptions {
@@ -131,9 +144,9 @@ export interface DartBridgeClientOptions {
131
144
  */
132
145
  readonly typesImportUri?: string;
133
146
  /**
134
- * ACTIVITY FEED (architecture/activity_descriptors.md): true iff the PACKAGE declares ≥1 directive
135
- * `.activity(...)`. When set the client emits `watchActivity()` / `activityLog()` (folding the bridge's
136
- * raw committed-intent rows through the shared pure `renderActivity`). Off ⇒ byte-identical to before.
147
+ * ACTIVITY + SELF-DESCRIPTION (architecture/activity_descriptors.md). The package compiler always sets
148
+ * this, including for an action-free package (empty history is still explainable). Direct codegen callers
149
+ * may leave it off when they explicitly need the legacy narrow surface.
137
150
  */
138
151
  readonly emitActivity?: boolean;
139
152
  /**
@@ -143,6 +156,8 @@ export interface DartBridgeClientOptions {
143
156
  * omitted. Only meaningful when {@link emitActivity} is set.
144
157
  */
145
158
  readonly activityImportUri?: string;
159
+ /** Compiler-known half of the one-root-plus-six `describe()` surface. */
160
+ readonly describeModel?: NomosDescribeModel;
146
161
  /**
147
162
  * CROSS-DOMAIN REF TRAVERSAL (dart_codegen_parity.md §2a follow-on): every aggregate id in the
148
163
  * PACKAGE → the domain module that owns it (`domain` for the client class name, `name` for the
@@ -186,6 +201,14 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
186
201
  };
187
202
  registerDartNames(mod.aggregates); // `.dartName` overrides for read-model class names in the client.
188
203
  const domain = mod.domain ?? mod.name;
204
+ assertModelClientMemberAvailable(mod);
205
+ const describeModel = opts.describeModel ?? buildNomosDescribeModel([mod], domain);
206
+ const clientInterface = opts.interface === undefined
207
+ ? undefined
208
+ : publicClientInterface(
209
+ [mod],
210
+ { [domain]: opts.interface as unknown as DomainInterface },
211
+ )[domain];
189
212
  const className = dartClientClassName(domain);
190
213
  const hashConst = dartHashConstName(mod.name);
191
214
  const requiresKernelFeats = requiredKernelFeatures([mod]);
@@ -195,6 +218,15 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
195
218
  : "nomos_client.dart";
196
219
  const aggById = new Map<string, AggregateHandle>();
197
220
  for (const a of mod.aggregates) aggById.set(a.id, a);
221
+ const conflictAggregateType = (aggregateId: string): string => {
222
+ const aggregate = aggById.get(aggregateId);
223
+ if (aggregate === undefined) {
224
+ throw new Error(`generated conflict policy refers to unknown aggregate '${aggregateId}' in domain '${domain}'`);
225
+ }
226
+ return isNamespacedAggregate(aggregate)
227
+ ? qualifyReadProjectionType(domain, aggregateId)
228
+ : aggregateId;
229
+ };
198
230
 
199
231
  // ROLE SUGAR — the `canEdit<Agg>`/`canView<Agg>` UI-hint helpers live HERE (the client package, which may
200
232
  // import runtime), NOT in the pure types file: they `jsonDecode` the folded RelationTuple leaf, and the
@@ -205,30 +237,89 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
205
237
  // below emits (the generated Dart client is byte-identical to a pre-sealing compile). Same map + sids as
206
238
  // the TS client (one source of truth: `domainEncryptionMap`); requires the baked interface for the sids.
207
239
  const encMap: EncDomainMap | null =
208
- opts.interface !== undefined
209
- ? domainEncryptionMap(mod, opts.interface as { aggregates?: Record<string, InterfaceAggShape> })
240
+ clientInterface !== undefined
241
+ ? domainEncryptionMap(mod, clientInterface as { aggregates?: Record<string, InterfaceAggShape> })
210
242
  : null;
211
243
  const encPresent = encMap !== null;
244
+ const manualEncryptionPresent = encMap !== null &&
245
+ [...Object.values(encMap.directives), ...Object.values(encMap.reads)].some((fields) =>
246
+ Object.values(fields).some((field) => field.owned !== true),
247
+ );
212
248
  /** The distinct scopes a read of `typeId` must inject keys for (empty ⇒ nothing encrypted on it). */
213
- const readScopesFor = (typeId: string): string[] => {
249
+ const readScopesFor = (typeId: string): { scope: string; owned?: true }[] => {
214
250
  const r = encMap?.reads?.[typeId];
215
- return r === undefined ? [] : [...new Set(Object.values(r).map((x) => x.scope))].sort();
251
+ if (r === undefined) return [];
252
+ const byKey = new Map<string, { scope: string; owned?: true }>();
253
+ for (const x of Object.values(r)) byKey.set(`${x.scope}:${x.owned === true}`, x);
254
+ return [...byKey.values()].sort((a, b) => a.scope.localeCompare(b.scope));
216
255
  };
256
+ const scopesExpr = (scopes: readonly { scope: string; owned?: true }[]): string =>
257
+ `[${scopes.map((s) => `{${dartStr("scope")}: ${dartStr(s.scope)}${s.owned === true ? `, ${dartStr("owned")}: true` : ""}}`).join(", ")}]`;
217
258
  const keysArg = (typeId: string): string => {
218
259
  const scopes = readScopesFor(typeId);
219
- return scopes.length ? `, await _readKeys([${scopes.map((s) => dartStr(s)).join(", ")}])` : "";
260
+ return scopes.length
261
+ ? `, await _readKeys(${scopesExpr(scopes)})`
262
+ : "";
263
+ };
264
+ const pastKeysArg = (typeId: string): string => {
265
+ const scopes = readScopesFor(typeId);
266
+ return scopes.length
267
+ ? `, await _client._readKeys(${scopesExpr(scopes)})`
268
+ : "";
220
269
  };
221
270
  // The DISTINCT encryption scopes this domain defines (across encrypted reads + sealing directives) — the
222
271
  // set the owner ceremony rotates. Empty ⇒ no `encryption` getter is emitted for this domain.
223
272
  const domScopes: string[] = (() => {
224
273
  if (encMap === null) return [];
225
274
  const s = new Set<string>();
226
- for (const r of Object.values(encMap.reads)) for (const f of Object.values(r)) s.add(f.scope);
227
- for (const dir of Object.values(encMap.directives)) for (const f of Object.values(dir)) s.add(f.scope);
275
+ for (const r of Object.values(encMap.reads)) for (const f of Object.values(r)) if (f.owned !== true) s.add(f.scope);
276
+ for (const dir of Object.values(encMap.directives)) for (const f of Object.values(dir)) if (f.owned !== true) s.add(f.scope);
228
277
  return [...s].sort();
229
278
  })();
279
+ const ownedScopes: string[] = (() => {
280
+ if (encMap === null) return [];
281
+ const scopes = new Set<string>();
282
+ for (const r of Object.values(encMap.reads)) for (const f of Object.values(r)) if (f.owned === true) scopes.add(f.scope);
283
+ for (const dir of Object.values(encMap.directives)) for (const f of Object.values(dir)) if (f.owned === true) scopes.add(f.scope);
284
+ return [...scopes].sort();
285
+ })();
286
+ if (ownedScopes.length > 1) {
287
+ throw new Error(`dart codegen: owned business state in domain '${domain}' unexpectedly uses more than one framework scope`);
288
+ }
289
+ const ownedScope = ownedScopes[0];
230
290
 
231
291
  const methods: string[] = [];
292
+ const pastMethods: string[] = [];
293
+ const pastDeclarations: string[] = [];
294
+ const conflictDeclarations: string[] = [];
295
+ const ownedActions = Object.fromEntries(
296
+ (mod.aggregates as AggregateHandle[])
297
+ .filter((aggregate) => aggregate.audience?.kind === "owned")
298
+ .map((aggregate) => {
299
+ const type = isNamespacedAggregate(aggregate) ? qualifyReadProjectionType(domain, aggregate.id) : aggregate.id;
300
+ return [type, {
301
+ share: ownedRecordActionDirectiveId("share", aggregate.id),
302
+ unshare: ownedRecordActionDirectiveId("unshare", aggregate.id),
303
+ transfer: ownedRecordActionDirectiveId("transfer", aggregate.id),
304
+ }];
305
+ }),
306
+ );
307
+ const ownedActionsDart = `{${Object.entries(ownedActions).map(([type, actions]) =>
308
+ `${dartStr(type)}: {${Object.entries(actions).map(([action, directive]) => `${dartStr(action)}: ${dartStr(directive)}`).join(", ")}}`,
309
+ ).join(", ")}}`;
310
+ if (ownedScope !== undefined && Object.keys(ownedActions).length > 0) {
311
+ methods.push(
312
+ ` /// Share one record. The peer can read its history and future changes; Nomos owns the keys and policy.`,
313
+ ` Future<NomosOfferOutcome> share(NomosRef record, String peer) => _ownedRecordAction('share', record, peer);`,
314
+ ``,
315
+ ` /// Stop sharing one record. Earlier knowledge cannot be erased; future changes use a fresh private key.`,
316
+ ` Future<NomosOfferOutcome> unshare(NomosRef record, String peer) => _ownedRecordAction('unshare', record, peer);`,
317
+ ``,
318
+ ` /// Hand ownership, readable history and future authority for one record to another person.`,
319
+ ` Future<NomosOfferOutcome> transfer(NomosRef record, String peer) => _ownedRecordAction('transfer', record, peer);`,
320
+ ``,
321
+ );
322
+ }
232
323
 
233
324
  // THE STATIC INTERFACE DESCRIPTOR (`XClient.interface`, session_bound_clients.md asks 5/9) —
234
325
  // what this client EXPECTS of a workspace: the domain key, the law hash it compiled under, and
@@ -246,6 +337,49 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
246
337
  );
247
338
  const birthDirs = allDirs.filter((d) => (d as { birthsChild?: boolean }).birthsChild === true);
248
339
  const dirs = allDirs.filter((d) => (d as { birthsChild?: boolean }).birthsChild !== true);
340
+ const correctionActions = dirs.filter(
341
+ (d) => (d as { businessBirth?: unknown }).businessBirth === undefined,
342
+ );
343
+ if (correctionActions.length > 0) {
344
+ const correctionsClass = `${className}Corrections`;
345
+ const outcomeClass = `${className}CorrectionOutcome`;
346
+ methods.push(
347
+ ` /// Correct this principal's latest business change. The framework owns target selection and replay.`,
348
+ ` ${correctionsClass} get corrections => ${correctionsClass}(this);`,
349
+ ``,
350
+ );
351
+ pastDeclarations.push(
352
+ `/// Result of a reasoned business correction. Framework intent and testimony ids remain private.`,
353
+ `class ${outcomeClass} {`,
354
+ ` final bool ok;`,
355
+ ` final String kind;`,
356
+ ` final String? reason;`,
357
+ ` final String? message;`,
358
+ ` final String? head;`,
359
+ ` const ${outcomeClass}({required this.ok, required this.kind, this.reason, this.message, this.head});`,
360
+ ` factory ${outcomeClass}.fromJson(Map<String, Object?> json) => ${outcomeClass}(`,
361
+ ` ok: json['ok'] == true, kind: (json['kind'] ?? '') as String,`,
362
+ ` reason: json['reason'] as String?, message: json['message'] as String?, head: json['head'] as String?);`,
363
+ `}`,
364
+ ``,
365
+ `/// One business correction lane. Application code provides only its reason.`,
366
+ `class ${correctionsClass} {`,
367
+ ` final ${className} _client;`,
368
+ ` const ${correctionsClass}(this._client);`,
369
+ ` Future<${outcomeClass}> undo(String reason) => _apply('undo', reason);`,
370
+ ` Future<${outcomeClass}> redo(String reason) => _apply('redo', reason);`,
371
+ ` Future<${outcomeClass}> _apply(String kind, String reason) async {`,
372
+ ` if (reason.trim().isEmpty) throw ArgumentError('corrections.\$kind() needs a reason');`,
373
+ ` final result = await _client.bridge.correctMostRecent(`,
374
+ ` kind: kind, domain: ${dartStr(domain)},`,
375
+ ` actions: const [${correctionActions.map((d) => dartStr(d.id)).join(", ")}],`,
376
+ ` reason: reason, domainHash: _client.domainHash, actor: _client.actor);`,
377
+ ` return ${outcomeClass}.fromJson(result);`,
378
+ ` }`,
379
+ `}`,
380
+ ``,
381
+ );
382
+ }
249
383
  for (const d of dirs) {
250
384
  const payloadClass = `${cap(d.id)}Payload`;
251
385
  const stamps = autoStampFields((d as unknown as { payloadSchema: z.ZodTypeAny }).payloadSchema);
@@ -278,12 +412,46 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
278
412
  ? ` The LAW mints the ${(d as { aggregateId?: string }).aggregateId ?? "aggregate"} id inside the sealed intent — it rides back on \`outcome.created\` (no id in the payload, no query-after).`
279
413
  : "";
280
414
  const stampsDoc = stamps.length > 0 ? `\n /// Omitted ${stamps.map((s) => `\`${s}\``).join(" / ")} auto-stamp(s) ISO now() at offer.` : "";
281
- if (encMap?.directives?.[d.id] !== undefined) {
415
+ const directiveEncryption = encMap?.directives?.[d.id];
416
+ if (directiveEncryption !== undefined) {
417
+ const ownedSeal = Object.values(directiveEncryption).every((field) => field.owned === true);
418
+ const targetAggregate = ownedSeal ? aggById.get((d as { aggregateId?: string }).aggregateId ?? "") : undefined;
419
+ const recordTarget = targetAggregate === undefined ? undefined : deriveDirectiveRecordTarget(d, targetAggregate);
420
+ if (ownedSeal && recordTarget === undefined) {
421
+ throw new Error(
422
+ `dart codegen: cannot derive the record targeted by encrypted owned directive '${domain}/${d.id}'. ` +
423
+ `Address it from one string payload field, one deterministic instance, or create it with create(Aggregate).`,
424
+ );
425
+ }
426
+ const runtimeType = targetAggregate === undefined
427
+ ? undefined
428
+ : isNamespacedAggregate(targetAggregate)
429
+ ? qualifyReadProjectionType(domain, targetAggregate.id)
430
+ : targetAggregate.id;
431
+ const recordPrelude = recordTarget === undefined || runtimeType === undefined
432
+ ? []
433
+ : recordTarget.kind === "premint"
434
+ ? [
435
+ ` final ownedRecordType = ${dartStr(runtimeType)};`,
436
+ ` final ownedRecordId = await bridge.mintId(ownedRecordType);`,
437
+ ` const ownedRecordPreMinted = true;`,
438
+ ]
439
+ : recordTarget.kind === "payload"
440
+ ? [
441
+ ` final ownedRecordType = ${dartStr(runtimeType)};`,
442
+ ` final ownedRecordId = (data[${dartStr(recordTarget.field)}] ?? '').toString();`,
443
+ ` const ownedRecordPreMinted = false;`,
444
+ ]
445
+ : [
446
+ ` final ownedRecordType = ${dartStr(runtimeType)};`,
447
+ ` final ownedRecordId = ${dartStr(recordTarget.id)};`,
448
+ ` const ownedRecordPreMinted = false;`,
449
+ ];
282
450
  methods.push(
283
451
  ` /// Author \`${domain}/${d.id}\` locally under the installed law; returns the typed [NomosOfferOutcome].${createsDoc}`,
284
- ` /// Encrypted field(s) are SEALED via the bound [keyStore] before dispatch (pass \`seal: false\` to skip).${stampsDoc}`,
452
+ ` /// ${ownedSeal ? "Owned content is encrypted automatically by Nomos." : "Encrypted field(s) are SEALED via the bound [keyStore] before dispatch (pass `seal: false` to skip)."}${stampsDoc}`,
285
453
  ...(gateDoc ? [gateDoc] : []),
286
- ` Future<NomosOfferOutcome> ${method}(${payloadClass} payload, {bool seal = true${wantAttest ? ", " + attNamed : ""}}) async {`,
454
+ ` Future<NomosOfferOutcome> ${method}(${payloadClass} payload${ownedSeal && !wantAttest ? "" : `, {${ownedSeal ? "" : "bool seal = true"}${wantAttest ? `${ownedSeal ? "" : ", "}${attNamed}` : ""}}`}) async {`,
287
455
  ...(gateGuard ? [gateGuard] : []),
288
456
  ` final data = payload.toPayloadJson();`,
289
457
  ...(stamps.length > 0
@@ -292,7 +460,8 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
292
460
  ...stamps.map((s) => ` data.putIfAbsent(${dartStr(s)}, () => now);`),
293
461
  ]
294
462
  : []),
295
- ` await _sealPayload(data, ${dartStr(d.id)}, seal);`,
463
+ ...recordPrelude,
464
+ ` await _sealPayload(data, ${dartStr(d.id)}, ${ownedSeal ? "true" : "seal"}${ownedSeal ? ", null, ownedRecordType, ownedRecordId, ownedRecordPreMinted" : ""});`,
296
465
  ` return _offer(${dartStr(d.id)}, data${attArg});`,
297
466
  ` }`,
298
467
  ``,
@@ -344,9 +513,13 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
344
513
  const method = safeMethod(d.id);
345
514
  const gate = (d as { requiresRelationGate?: { relation: string } }).requiresRelationGate;
346
515
  const businessBirth = (d as unknown as {
347
- businessBirth?: { aggregateId: string; rootId: string };
516
+ businessBirth?: { aggregateId: string; rootId: string; namespace: string; keyField: string; publicPayloadSchema: z.ZodTypeAny };
348
517
  }).businessBirth;
349
518
  if (businessBirth !== undefined) {
519
+ const birthEncryption = encMap?.directives?.[d.id];
520
+ const ownedBirthDescriptor = birthEncryption === undefined
521
+ ? undefined
522
+ : Object.values(birthEncryption).find((field) => field.owned === true);
350
523
  methods.push(
351
524
  ` /// Give one ${businessBirth.aggregateId} its own durable life. Nomos owns identity, authorship,`,
352
525
  ` /// provenance, persistence and recovery; the app supplies only business facts.`,
@@ -356,12 +529,35 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
356
529
  ` throw MissingOfferAuthority(domain: ${dartStr(domain)}, directiveId: ${dartStr(d.id)}, requires: 'a signed-in Nomos session');`,
357
530
  ` }`,
358
531
  ` final data = payload.toPayloadJson();`,
532
+ ...(ownedBirthDescriptor === undefined
533
+ ? []
534
+ : [
535
+ ` final ownedBirthKey = await bridge.nomosOwnedBirthScopeKey(`,
536
+ ` ${dartStr(ownedBirthDescriptor.scope)}, ${dartStr(businessBirth.namespace)}, data[${dartStr(businessBirth.keyField)}]);`,
537
+ ]),
538
+ ...(birthEncryption === undefined
539
+ ? []
540
+ : [
541
+ ` final sealedGenesis = Map<String, dynamic>.from(data);`,
542
+ ` await _sealPayload(sealedGenesis, ${dartStr(d.id)}, true${ownedBirthDescriptor === undefined ? "" : ", ownedBirthKey"});`,
543
+ ` data[${dartStr(BUSINESS_BIRTH_SEED_PAYLOAD_FIELD)}] = jsonEncode(sealedGenesis);`,
544
+ ...(ownedBirthDescriptor === undefined
545
+ ? []
546
+ : [` data[${dartStr(OWNED_KEY_PAYLOAD_FIELD)}] = ownedBirthKey.ownedKeySubject;`]),
547
+ ]),
359
548
  ` data['principal'] = principal;`,
360
549
  ` data['bornAt'] = DateTime.now().toUtc().toIso8601String();`,
361
550
  ` final outcome = await _offer(${dartStr(d.id)}, data);`,
362
551
  ` if (outcome.born.length != 1) {`,
363
552
  ` throw StateError('birth() committed without returning exactly one born workspace');`,
364
553
  ` }`,
554
+ ...(ownedBirthDescriptor === undefined
555
+ ? []
556
+ : [
557
+ ` if (ownedBirthKey.workspace == null || outcome.born.single != ownedBirthKey.workspace) {`,
558
+ ` throw StateError('birth() returned a workspace different from the deterministic owned-custody address');`,
559
+ ` }`,
560
+ ]),
365
561
  ` await bridge.sync();`,
366
562
  ` return NomosRef(workspace: outcome.born.single, type: ${dartStr(businessBirth.aggregateId)}, id: ${dartStr(businessBirth.rootId)});`,
367
563
  ` }`,
@@ -440,14 +636,29 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
440
636
  const params = q.key.map((k) => `required ${dartKeyType(agg, k)} ${dartKeyParam(k)}`).join(", ");
441
637
  const argMap = `{${q.key.map((k) => `${dartStr(k)}: ${dartKeyParam(k)}`).join(", ")}}`;
442
638
  const m = safeMethod(lcFirst(camel(q.id)));
639
+ const qScopes = readScopesFor(q.returns);
640
+ const qOwned = qScopes.some((scope) => scope.owned === true);
641
+ const qScopesExpr = scopesExpr(qScopes);
443
642
  methods.push(
444
643
  ` /// Declared query \`${q.id}\` (indexed probe — routed by the read manifest).${readScopesFor(q.returns).length ? " Encrypted field(s) decrypt when the bound [keyStore] holds the scope key; else they stay opaque envelopes." : ""}`,
445
644
  ` Future<List<${model}>> ${m}({${params}}) async =>`,
446
- ` (await bridge.query(${dartStr(q.id)}, ${argMap}${keysArg(q.returns)})).map((r) => ${rowToModel(model, "r")}).toList();`,
645
+ qOwned
646
+ ? ` (await _readWithOwnedKeys(${qScopesExpr}, (keys) => bridge.query(${dartStr(q.id)}, ${argMap}, keys))).map((r) => ${rowToModel(model, "r")}).toList();`
647
+ : ` (await bridge.query(${dartStr(q.id)}, ${argMap}${keysArg(q.returns)})).map((r) => ${rowToModel(model, "r")}).toList();`,
447
648
  ``,
448
649
  ` /// Reactive \`${q.id}\` — re-emits on every local change (cancel the subscription to stop).`,
449
650
  ` Stream<List<${model}>> watch${cap(camel(q.id))}({${params}}) =>`,
450
- ` bridge.watch(${dartStr(q.id)}, ${argMap}).map((rows) => rows.map((r) => ${rowToModel(model, "r")}).toList());`,
651
+ qOwned
652
+ ? ` bridge.watch(${dartStr(q.id)}, ${argMap}).asyncMap((_) => _readWithOwnedKeys(${qScopesExpr}, (keys) => bridge.query(${dartStr(q.id)}, ${argMap}, keys))).map((rows) => rows.map((r) => ${rowToModel(model, "r")}).toList());`
653
+ : ` bridge.watch(${dartStr(q.id)}, ${argMap}).map((rows) => rows.map((r) => ${rowToModel(model, "r")}).toList());`,
654
+ ``,
655
+ );
656
+ pastMethods.push(
657
+ ` /// Declared query \`${q.id}\` in the verified world at [moment].`,
658
+ ` Future<List<${model}>> ${m}({${params}}) async =>`,
659
+ qOwned
660
+ ? ` (await _client._readWithOwnedKeys(${qScopesExpr}, (keys) => _client.bridge.queryAt(moment, ${dartStr(q.id)}, ${argMap}, keys))).map((r) => ${rowToModel(model, "r", "_client._ws")}).toList();`
661
+ : ` (await _client.bridge.queryAt(moment, ${dartStr(q.id)}, ${argMap}${pastKeysArg(q.returns)})).map((r) => ${rowToModel(model, "r", "_client._ws")}).toList();`,
451
662
  ``,
452
663
  );
453
664
  }
@@ -478,6 +689,12 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
478
689
  : ` /// Declared count \`${c.id}\` — the maintained O(1) grand total.\n Future<int> ${m}() => bridge.count(${dartStr(c.id)});`,
479
690
  ``,
480
691
  );
692
+ pastMethods.push(
693
+ c.by != null
694
+ ? ` /// Declared count \`${c.id}\` at [moment].\n Future<int> ${m}(String ${dartKeyParam(c.by)}) => _client.bridge.countAt(moment, ${dartStr(c.id)}, ${dartKeyParam(c.by)});`
695
+ : ` /// Declared count \`${c.id}\` at [moment].\n Future<int> ${m}() => _client.bridge.countAt(moment, ${dartStr(c.id)});`,
696
+ ``,
697
+ );
481
698
  }
482
699
  for (const s of (mod.sums ?? []).map((raw) => finishSum(raw))) {
483
700
  const m = safeMethod(lcFirst(camel(s.id)));
@@ -487,42 +704,57 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
487
704
  : ` /// Declared sum \`${s.id}\` — the maintained O(1) grand total.\n Future<int> ${m}() => bridge.sum(${dartStr(s.id)});`,
488
705
  ``,
489
706
  );
707
+ pastMethods.push(
708
+ s.by != null
709
+ ? ` /// Declared sum \`${s.id}\` at [moment].\n Future<int> ${m}(String ${dartKeyParam(s.by)}) => _client.bridge.sumAt(moment, ${dartStr(s.id)}, ${dartKeyParam(s.by)});`
710
+ : ` /// Declared sum \`${s.id}\` at [moment].\n Future<int> ${m}() => _client.bridge.sumAt(moment, ${dartStr(s.id)});`,
711
+ ``,
712
+ );
490
713
  }
491
714
 
492
- // ── SURFACED CONFLICT DX (surfaced_merge_conflict.md): the workspace-wide pendingConflicts
493
- // read/watch + one typed `resolve…` helper per designated `.resolves()` directive. Emitted iff
494
- // this domain declares a Surfaced field (Surfaced-free packages emit nothing — the era rule).
715
+ // ── BUSINESS MERGE CONFLICT DX: keep the heterogeneous Nomos index behind an aggregate-specific,
716
+ // chained API. A pending conflict is a typed reference to its own `merge(...)` capability; callers
717
+ // inspect all versions and produce a merge result rather than choosing a winner.
495
718
  const resolverBindings = (mod as { surfacedResolvers?: import("./surfaced_lint.js").SurfacedResolverBinding[] }).surfacedResolvers ?? [];
496
719
  if (resolverBindings.length > 0) {
720
+ const conflictsClass = `${cap(camel(domain))}Conflicts`;
721
+ const summaryClass = `${cap(camel(domain))}ConflictSummary`;
722
+ const rowClass = `_${cap(camel(domain))}PendingConflictRow`;
723
+ const aggregateConflictsClass = (aggregateId: string) => {
724
+ const aggregateName = cap(camel(concreteClassName(aggregateId)));
725
+ return aggregateName === cap(camel(domain))
726
+ ? `${aggregateName}AggregateConflicts`
727
+ : `${aggregateName}Conflicts`;
728
+ };
729
+ const byAggregate = new Map<string, typeof resolverBindings>();
730
+ for (const binding of resolverBindings) {
731
+ byAggregate.set(binding.aggregateId, [...(byAggregate.get(binding.aggregateId) ?? []), binding]);
732
+ }
497
733
  methods.push(
498
- ` /// PENDING SURFACED CONFLICTS across the workspace — the maintained "decisions needed" read`,
499
- ` /// (reserved id \`nomosPendingConflicts\`; DERIVED, NOT DRAINED: a resolution write folding in`,
500
- ` /// removes its row by derivation). Pair each row with its typed \`resolve…\` helper below.`,
501
- ` Future<List<PendingConflictRow>> pendingConflicts() async =>`,
502
- ` (await bridge.pendingConflicts()).map(PendingConflictRow.fromJson).toList();`,
734
+ ` /// Pending business merges: summary → aggregate → typed conflict → merge().`,
735
+ ` ${conflictsClass} get conflicts => ${conflictsClass}(this);`,
503
736
  ``,
504
- ` /// Reactive pending-conflicts read — re-emits whenever the maintained index changes.`,
505
- ` Stream<List<PendingConflictRow>> watchPendingConflicts() =>`,
506
- ` bridge.watchPendingConflicts().map((rows) => rows`,
507
- ` .map((r) => PendingConflictRow.fromJson((r as Map).cast<String, dynamic>()))`,
508
- ` .toList());`,
737
+ ` /// Nomos-internal aggregate-filtered index used only by generated conflict references.`,
738
+ ` Future<List<${rowClass}>> _pendingConflictRows(String aggregateType) async =>`,
739
+ ` (await bridge.pendingConflictsFor(aggregateType)).map(${rowClass}.fromJson).toList();`,
509
740
  ``,
510
741
  );
511
742
  for (const b of resolverBindings) {
512
743
  if (b.valuePayloadField === undefined || b.idPayloadField === undefined) continue; // probe couldn't bind — dispatch the directive method directly.
513
744
  const agg = aggById.get(b.aggregateId);
514
745
  const fieldKind = (agg?.fields[b.field] as Field | undefined)?.kind;
515
- const chosenType = fieldKind === "string" ? "String" : fieldKind === "int" ? "int" : "Object?";
516
- const helper = safeMethod(`resolve${cap(camel(concreteClassName(b.aggregateId)))}${cap(camel(b.field))}`);
746
+ const mergedType = fieldKind === "string" || fieldKind === "ref" || fieldKind === "enum"
747
+ ? "String"
748
+ : fieldKind === "int"
749
+ ? "int"
750
+ : fieldKind === "bool"
751
+ ? "bool"
752
+ : "Object?";
753
+ const helper = `_merge${cap(camel(concreteClassName(b.aggregateId)))}${cap(camel(b.field))}`;
517
754
  const dir = allDirs.find((d) => d.id === b.directiveId);
518
755
  const gate = (dir as { requiresRelationGate?: { relation: string } } | undefined)?.requiresRelationGate;
519
756
  methods.push(
520
- ` /// RESOLVE a pending conflict on \`${b.aggregateId}.${b.field}\` by dispatching the DESIGNATED`,
521
- ` /// resolver \`${b.directiveId}\` with [chosen] (the conflict row names the instance; extra required`,
522
- ` /// payload fields ride [rest]). ESCAPE HATCH: any gated directive that sets the field still`,
523
- ` /// resolves the conflict in the kernel sense; \`.resolves()\` only designates which one THIS`,
524
- ` /// typed helper dispatches.`,
525
- ` Future<NomosOfferOutcome> ${helper}(PendingConflictRow conflict, ${chosenType} chosen,`,
757
+ ` Future<NomosOfferOutcome> ${helper}(${rowClass} conflict, ${mergedType} merged,`,
526
758
  ` {Map<String, Object?> rest = const {}}) {`,
527
759
  ...(gate !== undefined
528
760
  ? [
@@ -531,12 +763,177 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
531
763
  ` }`,
532
764
  ]
533
765
  : []),
534
- ` final data = <String, Object?>{...rest, ${dartStr(b.idPayloadField)}: conflict.aggregateId, ${dartStr(b.valuePayloadField)}: chosen};`,
535
- ` return _offer(${dartStr(b.directiveId)}, data);`,
766
+ ` final data = <String, Object?>{...rest, ${dartStr(b.idPayloadField)}: conflict.aggregateId, ${dartStr(b.valuePayloadField)}: merged};`,
767
+ ` return _offer(${dartStr(b.directiveId)}, data, conflictSnapshots: [<String, Object?>{`,
768
+ ` 'aggregateType': conflict.aggregateType, 'aggregateId': conflict.aggregateId,`,
769
+ ` 'field': conflict.field, 'targetSchema': conflict.targetSchema, 'token': conflict.snapshot,`,
770
+ ` }]);`,
536
771
  ` }`,
537
772
  ``,
538
773
  );
539
774
  }
775
+
776
+ conflictDeclarations.push(
777
+ `/// Framework conflict row decoded by this generated client. It is local to the`,
778
+ `/// generated release so schema-aware conflict DX never waits on a core package cast.`,
779
+ `class ${rowClass} {`,
780
+ ` final String aggregateType;`,
781
+ ` final String aggregateId;`,
782
+ ` final String field;`,
783
+ ` final String status;`,
784
+ ` final String targetSchema;`,
785
+ ` final List<SurfacedObservation<Object?>> observations;`,
786
+ ` final Map<String, dynamic>? evolution;`,
787
+ ` final String snapshot;`,
788
+ ` const ${rowClass}({required this.aggregateType, required this.aggregateId, required this.field, required this.status, required this.targetSchema, required this.observations, required this.evolution, required this.snapshot});`,
789
+ ` factory ${rowClass}.fromJson(Map<String, dynamic> json) {`,
790
+ ` final snapshot = json['snapshot'];`,
791
+ ` final targetSchema = json['targetSchema'];`,
792
+ ` if (snapshot is! String || snapshot.isEmpty || targetSchema is! String || targetSchema.isEmpty) {`,
793
+ ` throw StateError('conflicts need a Nomos runtime with schema-pinned immutable snapshots');`,
794
+ ` }`,
795
+ ` return ${rowClass}(`,
796
+ ` aggregateType: json['aggregateType'] as String, aggregateId: json['aggregateId'] as String,`,
797
+ ` field: json['field'] as String, status: json['status'] as String? ?? 'evolutionRequired',`,
798
+ ` targetSchema: targetSchema, snapshot: snapshot,`,
799
+ ` evolution: (json['evolution'] as Map?)?.cast<String, dynamic>(),`,
800
+ ` observations: List.unmodifiable((json['observations'] as List? ?? const []).map((o) => SurfacedObservation<Object?>.fromJson((o as Map).cast<String, dynamic>(), (v) => v))),`,
801
+ ` );`,
802
+ ` }`,
803
+ `}`,
804
+ ``,
805
+ `/// Workspace-wide pending-merge totals, derived from Nomos's maintained conflict index.`,
806
+ `class ${summaryClass} {`,
807
+ ` final int total;`,
808
+ ` final int ready;`,
809
+ ` final int evolutionRequired;`,
810
+ ` final Map<String, int> byAggregate;`,
811
+ ` const ${summaryClass}({required this.total, required this.ready, required this.evolutionRequired, required this.byAggregate});`,
812
+ `}`,
813
+ ``,
814
+ `/// Chained conflict API for the \`${domain}\` domain.`,
815
+ `class ${conflictsClass} {`,
816
+ ` final ${className} _client;`,
817
+ ` const ${conflictsClass}(this._client);`,
818
+ ``,
819
+ ` Future<${summaryClass}> summary() async {`,
820
+ ` final summary = await _client.bridge.pendingConflictSummary();`,
821
+ ` final byAggregate = (summary['byAggregate'] as Map? ?? const {}).map(`,
822
+ ` (key, value) => MapEntry(key.toString(), (value as num).toInt()));`,
823
+ ` return ${summaryClass}(`,
824
+ ` total: (summary['total'] as num? ?? 0).toInt(),`,
825
+ ` ready: (summary['ready'] as num? ?? 0).toInt(),`,
826
+ ` evolutionRequired: (summary['evolutionRequired'] as num? ?? 0).toInt(),`,
827
+ ` byAggregate: Map.unmodifiable(byAggregate),`,
828
+ ` );`,
829
+ ` }`,
830
+ ...[...byAggregate.keys()].map((aggregateId) =>
831
+ ` ${aggregateConflictsClass(aggregateId)} get ${lcFirst(camel(concreteClassName(aggregateId)))} => ${aggregateConflictsClass(aggregateId)}(_client);`,
832
+ ),
833
+ `}`,
834
+ ``,
835
+ );
836
+
837
+ for (const [aggregateId, bindings] of byAggregate) {
838
+ const runtimeAggregateType = conflictAggregateType(aggregateId);
839
+ const aggregateName = cap(camel(concreteClassName(aggregateId)));
840
+ const aggregateConflict = `${aggregateName}PendingConflict`;
841
+ conflictDeclarations.push(
842
+ `/// One pending merge on \`${aggregateId}\`; narrow by subtype to call its typed merge.`,
843
+ `sealed class ${aggregateConflict} {`,
844
+ ` const ${aggregateConflict}();`,
845
+ ` String get aggregateId;`,
846
+ ` String get field;`,
847
+ `}`,
848
+ ``,
849
+ );
850
+ for (const b of bindings) {
851
+ if (b.valuePayloadField === undefined || b.idPayloadField === undefined) continue;
852
+ const agg = aggById.get(b.aggregateId);
853
+ const fieldKind = (agg?.fields[b.field] as Field | undefined)?.kind;
854
+ const mergedType = fieldKind === "string" || fieldKind === "ref" || fieldKind === "enum"
855
+ ? "String"
856
+ : fieldKind === "int"
857
+ ? "int"
858
+ : fieldKind === "bool"
859
+ ? "bool"
860
+ : "Object?";
861
+ const typeName = `${aggregateName}${cap(camel(b.field))}Conflict`;
862
+ const evolutionTypeName = `${aggregateName}${cap(camel(b.field))}ConflictEvolutionRequired`;
863
+ const helper = `_merge${aggregateName}${cap(camel(b.field))}`;
864
+ conflictDeclarations.push(
865
+ `/// Pending versions of \`${aggregateId}.${b.field}\`. [merge] commits a result that may`,
866
+ `/// equal either version or be a new reconciled business value.`,
867
+ `final class ${typeName} extends ${aggregateConflict} {`,
868
+ ` final ${rowClass} _details;`,
869
+ ` final ${className} _client;`,
870
+ ` const ${typeName}._(this._details, this._client);`,
871
+ ` @override`,
872
+ ` String get aggregateId => _details.aggregateId;`,
873
+ ` @override`,
874
+ ` String get field => _details.field;`,
875
+ ``,
876
+ ` List<SurfacedObservation<${mergedType}>> get versions => List.unmodifiable(`,
877
+ ` _details.observations.map((o) => SurfacedObservation<${mergedType}>(`,
878
+ ` value: o.value as ${mergedType}, occurredAt: o.occurredAt, author: o.author)));`,
879
+ ``,
880
+ ` Future<NomosOfferOutcome> merge(`,
881
+ ` ${mergedType} Function(List<SurfacedObservation<${mergedType}>> versions) reconcile,`,
882
+ ` {Map<String, Object?> rest = const {}}) =>`,
883
+ ` _client.${helper}(_details, reconcile(versions), rest: rest);`,
884
+ `}`,
885
+ ``,
886
+ `/// The pending values span field schemas. No merge method exists until Nomos can`,
887
+ `/// deterministically evolve every version to the current business type.`,
888
+ `final class ${evolutionTypeName} extends ${aggregateConflict} {`,
889
+ ` final ${rowClass} _details;`,
890
+ ` const ${evolutionTypeName}._(this._details);`,
891
+ ` @override`,
892
+ ` String get aggregateId => _details.aggregateId;`,
893
+ ` @override`,
894
+ ` String get field => _details.field;`,
895
+ ` Map<String, dynamic> get evolution => _details.evolution ?? const {};`,
896
+ `}`,
897
+ ``,
898
+ );
899
+ }
900
+ conflictDeclarations.push(
901
+ `/// Aggregate-specific pending merges for \`${aggregateId}\`.`,
902
+ `class ${aggregateConflictsClass(aggregateId)} {`,
903
+ ` final ${className} _client;`,
904
+ ` const ${aggregateConflictsClass(aggregateId)}(this._client);`,
905
+ ``,
906
+ ` Future<List<${aggregateConflict}>> pending() async {`,
907
+ ` final rows = await _client._pendingConflictRows(${dartStr(runtimeAggregateType)});`,
908
+ ` final out = <${aggregateConflict}>[];`,
909
+ ` for (final row in rows) {`,
910
+ ` if (row.aggregateType != ${dartStr(runtimeAggregateType)}) {`,
911
+ ` throw StateError('Nomos returned a conflict outside the generated aggregate filter');`,
912
+ ` }`,
913
+ ` switch (row.field) {`,
914
+ ...bindings.flatMap((b) => {
915
+ if (b.valuePayloadField === undefined || b.idPayloadField === undefined) return [];
916
+ const typeName = `${aggregateName}${cap(camel(b.field))}Conflict`;
917
+ const evolutionTypeName = `${aggregateName}${cap(camel(b.field))}ConflictEvolutionRequired`;
918
+ return [
919
+ ` case ${dartStr(b.field)}:`,
920
+ ` if (row.status == 'ready') {`,
921
+ ` out.add(${typeName}._(row, _client));`,
922
+ ` } else if (row.status == 'evolutionRequired') {`,
923
+ ` out.add(${evolutionTypeName}._(row));`,
924
+ ` } else {`,
925
+ ` throw StateError('Nomos returned an unknown conflict status: \${row.status}');`,
926
+ ` }`,
927
+ ];
928
+ }),
929
+ ` }`,
930
+ ` }`,
931
+ ` return List.unmodifiable(out);`,
932
+ ` }`,
933
+ `}`,
934
+ ``,
935
+ );
936
+ }
540
937
  }
541
938
 
542
939
  // ── per-aggregate by-id read/watch + type-index list ──
@@ -547,22 +944,194 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
547
944
  const name = concreteClassName(agg.id);
548
945
  const lc = lcFirst(camel(name));
549
946
  const Cap = cap(camel(name));
947
+ const aScopes = readScopesFor(agg.id);
948
+ const aOwned = aScopes.some((scope) => scope.owned === true);
949
+ const aScopesExpr = scopesExpr(aScopes);
950
+ const runtimeType = isNamespacedAggregate(agg) ? qualifyReadProjectionType(domain, agg.id) : agg.id;
951
+ const byIdKeys = aScopes.length === 0
952
+ ? ""
953
+ : aOwned
954
+ ? `, await _readKeys(${aScopesExpr}, [{${dartStr("type")}: ${dartStr(runtimeType)}, ${dartStr("id")}: id}])`
955
+ : keysArg(agg.id);
956
+ const pastByIdKeys = aScopes.length === 0
957
+ ? ""
958
+ : aOwned
959
+ ? `, await _client._readKeys(${aScopesExpr}, [{${dartStr("type")}: ${dartStr(runtimeType)}, ${dartStr("id")}: id}])`
960
+ : pastKeysArg(agg.id);
550
961
  methods.push(
551
962
  ` /// Read one ${agg.id} by aggregate id.${readScopesFor(agg.id).length ? " Encrypted field(s) decrypt when the bound [keyStore] holds the scope key." : ""}`,
552
963
  ` Future<List<${model}>> ${safeMethod(`${lc}ById`)}(String id) async =>`,
553
- ` (await bridge.queryById(id${keysArg(agg.id)})).map((r) => ${rowToModel(model, "r")}).toList();`,
964
+ ` (await bridge.queryById(id${byIdKeys})).map((r) => ${rowToModel(model, "r")}).toList();`,
554
965
  ``,
555
966
  ` /// Watch one ${agg.id} by aggregate id (local reactive read).`,
556
967
  ` Stream<List<${model}>> watch${Cap}ById(String id) =>`,
557
- ` bridge.watchById(id).map((rows) => rows.map((r) => ${rowToModel(model, "r")}).toList());`,
968
+ aOwned
969
+ ? ` bridge.watchById(id).asyncMap((_) async => bridge.queryById(id, await _readKeys(${aScopesExpr}, [{${dartStr("type")}: ${dartStr(runtimeType)}, ${dartStr("id")}: id}]))).map((rows) => rows.map((r) => ${rowToModel(model, "r")}).toList());`
970
+ : ` bridge.watchById(id).map((rows) => rows.map((r) => ${rowToModel(model, "r")}).toList());`,
558
971
  ``,
559
972
  ` /// Every ${agg.id} (the type-index probe — O(result), not a scan).`,
560
973
  ` Future<List<${model}>> ${safeMethod(`list${Cap}s`)}() async =>`,
561
- ` (await bridge.query('aggregatesByType', {'__type': ${dartStr(agg.id)}}${keysArg(agg.id)})).map((r) => ${rowToModel(model, "r")}).toList();`,
974
+ aOwned
975
+ ? ` (await _readWithOwnedKeys(${aScopesExpr}, (keys) => bridge.query('aggregatesByType', {'__type': ${dartStr(agg.id)}}, keys))).map((r) => ${rowToModel(model, "r")}).toList();`
976
+ : ` (await bridge.query('aggregatesByType', {'__type': ${dartStr(agg.id)}}${keysArg(agg.id)})).map((r) => ${rowToModel(model, "r")}).toList();`,
977
+ ``,
978
+ );
979
+ pastMethods.push(
980
+ ` /// Read one ${agg.id} by aggregate id in the verified world at [moment].`,
981
+ ` Future<List<${model}>> ${safeMethod(`${lc}ById`)}(String id) async =>`,
982
+ ` (await _client.bridge.queryByIdAt(moment, id${pastByIdKeys})).map((r) => ${rowToModel(model, "r", "_client._ws")}).toList();`,
983
+ ``,
984
+ ` /// Every ${agg.id} in the verified world at [moment].`,
985
+ ` Future<List<${model}>> ${safeMethod(`list${Cap}s`)}() async =>`,
986
+ aOwned
987
+ ? ` (await _client._readWithOwnedKeys(${aScopesExpr}, (keys) => _client.bridge.queryAt(moment, 'aggregatesByType', {'__type': ${dartStr(agg.id)}}, keys))).map((r) => ${rowToModel(model, "r", "_client._ws")}).toList();`
988
+ : ` (await _client.bridge.queryAt(moment, 'aggregatesByType', {'__type': ${dartStr(agg.id)}}${pastKeysArg(agg.id)})).map((r) => ${rowToModel(model, "r", "_client._ws")}).toList();`,
562
989
  ``,
563
990
  );
564
991
  }
565
992
 
993
+ if (opts.emitActivity === true) methods.push(
994
+ ` /// Adopt THIS generated model. Nomos proves it against current history; a deliberate`,
995
+ ` /// difference returns a decision whose accept(reason) re-proves and records the reason.`,
996
+ ` NomosModel get model => NomosModel(_adoptGeneratedModel, _restorePreviousGeneratedModel);`,
997
+ ``,
998
+ ` NomosModelReview _nomosModelReview(Map<String, Object?>? report) {`,
999
+ ` final differences = <NomosModelDifference>[];`,
1000
+ ` for (final raw in ((report?['divergences'] as List?) ?? const [])) {`,
1001
+ ` final difference = (raw as Map).cast<String, Object?>();`,
1002
+ ` final rawArea = difference['domain'];`,
1003
+ ` final area = rawArea is String && !rawArea.contains('nomos_internal') ? rawArea : null;`,
1004
+ ` final rawAction = difference['directiveId'];`,
1005
+ ` final action = rawAction is String && !rawAction.contains('nomos_internal') ? rawAction : null;`,
1006
+ ` final rawKind = difference['kind']?.toString();`,
1007
+ ` final kind = rawKind == 'plan-divergence'`,
1008
+ ` ? NomosModelDifferenceKind.outcome`,
1009
+ ` : rawKind == 'unsupported-premise' || rawKind == 'read-conflict'`,
1010
+ ` ? NomosModelDifferenceKind.evidence`,
1011
+ ` : NomosModelDifferenceKind.rules;`,
1012
+ ` final subject = action ?? (kind == NomosModelDifferenceKind.outcome ? 'A business action' : 'A historical action');`,
1013
+ ` final summary = kind == NomosModelDifferenceKind.outcome`,
1014
+ ` ? '\$subject would produce a different result under this model.'`,
1015
+ ` : kind == NomosModelDifferenceKind.evidence`,
1016
+ ` ? 'Nomos could not reproduce all evidence needed by \$subject.'`,
1017
+ ` : "\$subject is not accepted by this model's rules.";`,
1018
+ ` differences.add(NomosModelDifference(kind: kind, area: area, action: action, summary: summary));`,
1019
+ ` }`,
1020
+ ` final evolve = (report?['evolve'] as Map?)?.cast<String, Object?>();`,
1021
+ ` if (report != null && evolve?['ok'] != true && !differences.any((d) => d.kind == NomosModelDifferenceKind.rules)) {`,
1022
+ ` differences.add(const NomosModelDifference(kind: NomosModelDifferenceKind.rules, summary: 'This model does not yet explain how to interpret every existing value.'));`,
1023
+ ` }`,
1024
+ ` if (report != null && (report['sourceVerified'] != true || report['complete'] != true)`,
1025
+ ` && !differences.any((d) => d.kind == NomosModelDifferenceKind.evidence)) {`,
1026
+ ` differences.add(const NomosModelDifference(kind: NomosModelDifferenceKind.evidence, summary: 'Nomos could not re-prove every premise in the current business history.'));`,
1027
+ ` }`,
1028
+ ` final projection = (report?['projection'] as Map?)?.cast<String, Object?>();`,
1029
+ ` int number(String name) => (report?[name] as num?)?.toInt() ?? 0;`,
1030
+ ` return NomosModelReview(`,
1031
+ ` checkedActions: number('replayedIntents'), candidateActions: number('candidateIntents'),`,
1032
+ ` sameOutcome: projection?['equal'] as bool?, differences: differences);`,
1033
+ ` }`,
1034
+ ``,
1035
+ ` Future<NomosModelAdoption> _adoptGeneratedModel([String? because]) async {`,
1036
+ ` final raw = await bridge.nomosFrameworkAdoptGeneratedModel(`,
1037
+ ` domain: ${dartStr(domain)}, candidateHash: domainHash, because: because);`,
1038
+ ` final reportRaw = raw['report'];`,
1039
+ ` final report = reportRaw is Map ? reportRaw.cast<String, Object?>() : null;`,
1040
+ ` final review = _nomosModelReview(report);`,
1041
+ ` final stage = raw['stage']?.toString();`,
1042
+ ` if (stage == 'promoted' && raw['ok'] == true) {`,
1043
+ ` return NomosModelAdoption.adopted(`,
1044
+ ` summary: 'Nomos proved this model against current history and made it active.', review: review);`,
1045
+ ` }`,
1046
+ ` if (stage == 'acceptance') {`,
1047
+ ` return NomosModelAdoption.needsDecision(`,
1048
+ ` summary: 'This model would change prior business outcomes. Review the differences and record why that is intended.',`,
1049
+ ` review: review, accept: (reason) => _adoptGeneratedModel(reason));`,
1050
+ ` }`,
1051
+ ` final evolve = (report?['evolve'] as Map?)?.cast<String, Object?>();`,
1052
+ ` final summary = stage == 'identity'`,
1053
+ ` ? 'Sign in before adopting this model so the decision is accountable.'`,
1054
+ ` : report != null && report['sourceVerified'] != true`,
1055
+ ` ? 'The current history could not be verified. Nothing changed and the current model remains active.'`,
1056
+ ` : report != null && report['complete'] != true`,
1057
+ ` ? 'Nomos could not re-prove every historical dependency. Nothing changed and the current model remains active.'`,
1058
+ ` : report != null && evolve?['ok'] != true`,
1059
+ ` ? 'This model cannot yet interpret every existing value. Nothing changed and the current model remains active.'`,
1060
+ ` : 'The workspace changed or refused this adoption. Nothing changed; ask Nomos to try again.';`,
1061
+ ` return NomosModelAdoption.blocked(summary: summary, review: review);`,
1062
+ ` }`,
1063
+ ``,
1064
+ ` Future<NomosModelRestoration> _restorePreviousGeneratedModel([String? because]) async {`,
1065
+ ` final raw = await bridge.nomosFrameworkRestorePreviousGeneratedModel(`,
1066
+ ` domain: ${dartStr(domain)}, currentHash: domainHash, because: because);`,
1067
+ ` final reportRaw = raw['report'];`,
1068
+ ` final report = reportRaw is Map ? reportRaw.cast<String, Object?>() : null;`,
1069
+ ` final review = _nomosModelReview(report);`,
1070
+ ` final stage = raw['stage']?.toString();`,
1071
+ ` if (stage == 'restored' && raw['ok'] == true) {`,
1072
+ ` return NomosModelRestoration.restored(`,
1073
+ ` summary: 'Nomos re-proved current history and restored the previous model.', review: review);`,
1074
+ ` }`,
1075
+ ` if (stage == 'acceptance') {`,
1076
+ ` return NomosModelRestoration.needsDecision(`,
1077
+ ` summary: 'Restoring the previous model changes which rules govern future work. Review the effects and record why restoration is intended.',`,
1078
+ ` review: review, accept: (reason) => _restorePreviousGeneratedModel(reason));`,
1079
+ ` }`,
1080
+ ` final evolve = (report?['evolve'] as Map?)?.cast<String, Object?>();`,
1081
+ ` final error = raw['error']?.toString();`,
1082
+ ` final summary = stage == 'identity'`,
1083
+ ` ? 'Sign in before restoring a model so the decision is accountable.'`,
1084
+ ` : error?.contains('restore-no-previous') == true`,
1085
+ ` ? 'This is the first model for this business; there is no previous model to restore.'`,
1086
+ ` : error?.contains('restore-current-changed') == true`,
1087
+ ` ? 'The current model changed before restoration could finish. Nothing changed; ask Nomos to review again.'`,
1088
+ ` : report != null && report['sourceVerified'] != true`,
1089
+ ` ? 'The current history could not be verified. Nothing changed and the current model remains active.'`,
1090
+ ` : report != null && report['complete'] != true`,
1091
+ ` ? 'Nomos could not re-prove every historical dependency. Nothing changed and the current model remains active.'`,
1092
+ ` : report != null && evolve?['ok'] != true`,
1093
+ ` ? 'The previous model cannot safely interpret every existing value. Nothing changed and the current model remains active.'`,
1094
+ ` : 'The workspace refused restoration. Nothing changed and the current model remains active.';`,
1095
+ ` return NomosModelRestoration.blocked(summary: summary, review: review);`,
1096
+ ` }`,
1097
+ ``,
1098
+ );
1099
+
1100
+ const pastClass = `${className}Past`;
1101
+ const atClass = `${className}At`;
1102
+ methods.push(
1103
+ ` /// Read the same typed business world at a verified point in its history.`,
1104
+ ` ${pastClass} get past => ${pastClass}(this);`,
1105
+ ``,
1106
+ );
1107
+ pastDeclarations.push(
1108
+ `/// The accountable timeline for a [${className}]. Commit identities remain opaque inside [NomosMoment].`,
1109
+ `class ${pastClass} {`,
1110
+ ` final ${className} _client;`,
1111
+ ` const ${pastClass}(this._client);`,
1112
+ ``,
1113
+ ` Future<List<NomosMoment>> moments({int limit = 100}) async {`,
1114
+ ` final moments = await _client.bridge.moments(limit: limit);`,
1115
+ ` const actions = <String>{${[...allDirs.map((directive) => directive.id), ...(correctionActions.length > 0 ? [BUSINESS_CORRECTION_DIRECTIVE] : [])].map(dartStr).join(", ")}};`,
1116
+ ` return moments.where((moment) =>`,
1117
+ ` moment.domain == ${dartStr(domain)} && moment.action != null && actions.contains(moment.action)).map((moment) =>`,
1118
+ ` moment.action == ${dartStr(BUSINESS_CORRECTION_DIRECTIVE)} ? NomosMoment(`,
1119
+ ` id: moment.id, occurredAt: moment.occurredAt, actorSubject: moment.actorSubject, domain: moment.domain, action: 'correction') : moment).toList();`,
1120
+ ` }`,
1121
+ ` ${atClass} at(NomosMoment moment) => ${atClass}(_client, moment);`,
1122
+ `}`,
1123
+ ``,
1124
+ `/// A read-only, business-shaped view of [${className}] at one verified [moment].`,
1125
+ `class ${atClass} {`,
1126
+ ` final ${className} _client;`,
1127
+ ` final NomosMoment moment;`,
1128
+ ` const ${atClass}(this._client, this.moment);`,
1129
+ ``,
1130
+ ...pastMethods,
1131
+ `}`,
1132
+ ``,
1133
+ );
1134
+
566
1135
  // ── REF-TRAVERSAL ACCESSORS (dart_codegen_parity.md P2 + §2a follow-on — ref ergonomics) ──
567
1136
  // For every reference field on a read model, emit a companion extension method that resolves the
568
1137
  // stored locator to the actual target row, composed ONLY from surface that already exists (no new
@@ -720,29 +1289,32 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
720
1289
  refExtensions.push(lines.join("\n"));
721
1290
  }
722
1291
 
723
- // ── activity feed → typed reactive + one-shot (declared `.activity(...)`) ──
1292
+ // ── activity feed → typed reactive + one-shot (automatic baseline; `.activity(...)` enriches) ──
724
1293
  if (opts.emitActivity === true) {
725
1294
  methods.push(
726
1295
  ` /// THE ACTIVITY FEED (architecture/activity_descriptors.md): fold the committed intent log into typed`,
727
- ` /// [ActivityEvent]s (declared \`.activity(...)\`). Re-emits on every new committed intent; a directive`,
728
- ` /// with no declared activity is dropped. Cancel the subscription to stop.`,
1296
+ ` /// [ActivityEvent]s (automatic baseline; \`.activity(...)\` enriches it). Re-emits on every new`,
1297
+ ` /// committed intent; a directive`,
1298
+ ` /// outside this generated business package is dropped. Cancel the subscription to stop.`,
729
1299
  ` Stream<List<ActivityEvent>> watchActivity() =>`,
730
1300
  ` bridge.watchActivity().map((rows) => rows`,
731
1301
  ` .map((r) => renderActivity((r as Map).cast<String, dynamic>()))`,
732
1302
  ` .whereType<ActivityEvent>()`,
733
1303
  ` .toList());`,
734
1304
  ``,
735
- ` /// One-shot: the current committed activity log, folded to typed [ActivityEvent]s (declared \`.activity(...)\`).`,
1305
+ ` /// One-shot: the current committed activity log, folded to typed [ActivityEvent]s.`,
736
1306
  ` Future<List<ActivityEvent>> activityLog({String? base, int? limit}) async =>`,
737
1307
  ` (await bridge.activityLog(base: base, limit: limit))`,
738
1308
  ` .map((r) => renderActivity(r.cast<String, dynamic>()))`,
739
1309
  ` .whereType<ActivityEvent>()`,
740
1310
  ` .toList();`,
741
1311
  ``,
742
- ` /// NARRATIVE (for an in-app LLM): the workspace story — the activity log folded to typed events + a`,
743
- ` /// plain-English [ActivityNarrative.transcript] grounded in the VERIFIED log. Filter by [actor] /`,
744
- ` /// [category] / [since] (ISO occurredAt lower bound); [limit] caps the rows pulled. Newest-first.`,
745
- ` Future<ActivityNarrative> describe({String? since, String? actor, String? category, int? limit}) async {`,
1312
+ ` /// THE SELF-DESCRIPTION: root links to exactly six grounded pages — model, current state, rules,`,
1313
+ ` /// history, decisions and evidence. The old events/transcript fields remain on [NomosDescription].`,
1314
+ ` Future<NomosDescription> describe({`,
1315
+ ` NomosDescribeSection section = NomosDescribeSection.root,`,
1316
+ ` String? since, String? actor, String? category, int? limit,`,
1317
+ ` }) async {`,
746
1318
  ` final raw = await bridge.activityLog(limit: limit);`,
747
1319
  ` var events = raw`,
748
1320
  ` .map((r) => renderActivity(r.cast<String, dynamic>()))`,
@@ -751,7 +1323,170 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
751
1323
  ` if (actor != null) events = events.where((e) => e.actorSubject == actor).toList();`,
752
1324
  ` if (category != null) events = events.where((e) => e.category == category).toList();`,
753
1325
  ` if (since != null) events = events.where((e) => e.occurredAt.compareTo(since) >= 0).toList();`,
754
- ` return activityNarrative(events);`,
1326
+ ` final narrative = activityNarrative(events);`,
1327
+ ` var organisation = const NomosOrganisationDescription(complete: false, children: []);`,
1328
+ ` try {`,
1329
+ ` final children = (await bridge.nomosFrameworkChildWorkspaces()).map((child) {`,
1330
+ ` final workspace = child['workspace']?.toString() ?? '';`,
1331
+ ` final status = child['status']?.toString() ?? 'unknown';`,
1332
+ ` final summary = status == 'active'`,
1333
+ ` ? 'This part of the organisation is active. Follow its generated description to continue.'`,
1334
+ ` : status == 'retired'`,
1335
+ ` ? 'This part of the organisation is retired. Its recorded history remains explainable.'`,
1336
+ ` : status == 'reclaimed'`,
1337
+ ` ? "This part of the organisation was reclaimed. Its birth remains part of this workspace's history."`,
1338
+ ` : 'This part of the organisation has a newer lifecycle this client does not understand. Its link remains visible.';`,
1339
+ ` return NomosDescribeChild(`,
1340
+ ` workspace: workspace, status: status, bornAt: child['bornAt']?.toString(), label: workspace,`,
1341
+ ` question: 'What does this part of the organisation do, and what has happened there?', summary: summary);`,
1342
+ ` }).where((child) => child.workspace.isNotEmpty).toList();`,
1343
+ ` organisation = NomosOrganisationDescription(complete: children.every((child) => child.status != 'unknown'), children: children);`,
1344
+ ` } catch (_) {`,
1345
+ ` // An old/unavailable runtime cannot prove a leaf. Keep incompleteness explicit.`,
1346
+ ` }`,
1347
+ ` final model = (jsonDecode(_nomosDescribeModelJson) as Map).cast<String, dynamic>();`,
1348
+ ` final links = ((model['links'] as List?) ?? const [])`,
1349
+ ` .map((raw) {`,
1350
+ ` final link = (raw as Map).cast<String, dynamic>();`,
1351
+ ` return NomosDescribeLink(`,
1352
+ ` section: NomosDescribeSection.values.byName(link['section'] as String),`,
1353
+ ` label: link['label'] as String, question: link['question'] as String, summary: link['summary'] as String);`,
1354
+ ` }).toList();`,
1355
+ ` NomosDescribeItem staticItem(dynamic raw) {`,
1356
+ ` final item = (raw as Map).cast<String, dynamic>();`,
1357
+ ` return NomosDescribeItem(`,
1358
+ ` kind: item['kind'] as String, id: item['id'] as String, label: item['label'] as String,`,
1359
+ ` summary: item['summary'] as String,`,
1360
+ ` details: item['details'] == null ? null : (item['details'] as Map).cast<String, dynamic>());`,
1361
+ ` }`,
1362
+ ` List<NomosRef> eventRefs(ActivityEvent event) => event.refs.values`,
1363
+ ` .map((ref) => NomosRef(workspace: ref.workspace, type: ref.type, id: ref.id)).toList();`,
1364
+ ` String refusalReason(Object? raw) {`,
1365
+ ` final text = (raw ?? '').toString().toLowerCase();`,
1366
+ ` bool has(List<String> parts) => parts.any(text.contains);`,
1367
+ ` if (has(const ['evolve', 'schema', 'projection', 'retype', 'removed field', 'law drift', 'law-drift', 'cross-version', 'older model', 'authored under'])) return 'model-change';`,
1368
+ ` if (has(const ['attest', 'evidence', 'source session', 'source unreachable', 'read unreachable'])) return 'evidence';`,
1369
+ ` if (has(const ['permission', 'unauthor', 'forbidden', 'requires relation', 'relation gate', 'signer', 'principal', 'access denied', 'not enrolled'])) return 'permission';`,
1370
+ ` if (has(const ['precondition', 'invariant', 'constraint', 'validation', 'invalid payload', 'business rule', 'required field', 'must be'])) return 'business-rule';`,
1371
+ ` return 'review';`,
1372
+ ` }`,
1373
+ ` String refusalSummary(String reason, String action) {`,
1374
+ ` switch (reason) {`,
1375
+ ` case 'permission': return '$action was retained because its author did not have the required business permission.';`,
1376
+ ` case 'business-rule': return '$action was retained because it did not satisfy the business rules declared for that action.';`,
1377
+ ` case 'model-change': return '$action was created under a model the workspace cannot currently interpret. It remains available while other work continues.';`,
1378
+ ` case 'evidence': return '$action was retained because required verified business evidence was unavailable.';`,
1379
+ ` default: return '$action was retained for review and has not changed the business state.';`,
1380
+ ` }`,
1381
+ ` }`,
1382
+ ``,
1383
+ ` var title = model['title'] as String;`,
1384
+ ` var summary = model['summary'] as String;`,
1385
+ ` var items = <NomosDescribeItem>[];`,
1386
+ ` if (section == NomosDescribeSection.model || section == NomosDescribeSection.rules) {`,
1387
+ ` final pages = (model['pages'] as Map).cast<String, dynamic>();`,
1388
+ ` final page = (pages[section.name] as Map).cast<String, dynamic>();`,
1389
+ ` title = page['title'] as String; summary = page['summary'] as String;`,
1390
+ ` items = ((page['items'] as List?) ?? const []).map(staticItem).toList();`,
1391
+ ` } else if (section == NomosDescribeSection.current) {`,
1392
+ ` title = 'Current state';`,
1393
+ ` summary = 'The business records this session is allowed to read now. An unavailable type stays named without leaking its contents.';`,
1394
+ ` for (final rawAggregate in ((model['aggregateTypes'] as List?) ?? const [])) {`,
1395
+ ` final aggregate = (rawAggregate as Map).cast<String, dynamic>();`,
1396
+ ` final type = aggregate['type'] as String;`,
1397
+ ` final queryType = aggregate['queryType'] as String;`,
1398
+ ` final label = aggregate['label'] as String;`,
1399
+ ` try {`,
1400
+ ` final rows = await bridge.query('aggregatesByType', {'__type': queryType});`,
1401
+ ` final refs = rows.map((row) => NomosRef(`,
1402
+ ` workspace: _ws, type: (row['type'] ?? type).toString(), id: (row['id'] ?? '').toString())).toList();`,
1403
+ ` final exact = aggregate['audience'] == 'public' || rows.isNotEmpty;`,
1404
+ ` items.add(NomosDescribeItem(`,
1405
+ ` kind: 'current', id: '\${aggregate['domain']}.\$type', label: label,`,
1406
+ ` summary: exact ? '\${rows.length} \$label record\${rows.length == 1 ? '' : 's'} visible now.' : 'No \$label records are visible to this session.',`,
1407
+ ` details: {'domain': aggregate['domain'], 'type': type, 'audience': aggregate['audience'], if (exact) 'count': rows.length else 'status': 'none-visible'}, refs: refs));`,
1408
+ ` } catch (_) {`,
1409
+ ` items.add(NomosDescribeItem(`,
1410
+ ` kind: 'current', id: '\${aggregate['domain']}.\$type', label: label,`,
1411
+ ` summary: '\$label exists in the model, but this session cannot read its current records.',`,
1412
+ ` details: {'domain': aggregate['domain'], 'type': type, 'audience': aggregate['audience'], 'status': 'unavailable'}));`,
1413
+ ` }`,
1414
+ ` }`,
1415
+ ` } else if (section == NomosDescribeSection.history) {`,
1416
+ ` title = 'Verified business history';`,
1417
+ ` summary = 'What changed, who changed it and when, read from verified committed testimony.';`,
1418
+ ` items = events.map((event) => NomosDescribeItem(`,
1419
+ ` kind: 'change', id: event.eventId, label: event.title, summary: event.message,`,
1420
+ ` details: {'actor': event.actorSubject, 'occurredAt': event.occurredAt, 'domain': event.domain, 'kind': event.kind},`,
1421
+ ` refs: eventRefs(event))).toList();`,
1422
+ ` } else if (section == NomosDescribeSection.decisions) {`,
1423
+ ` title = 'Decisions and work needing attention';`,
1424
+ ` summary = 'Recorded business reasons, pending merges and retained rejected work, plus declared conflict policy from the model.';`,
1425
+ ` items = events.where((event) => event.kind.endsWith('.correction')).map((event) => NomosDescribeItem(`,
1426
+ ` kind: 'decision', id: event.eventId, label: event.title, summary: event.message,`,
1427
+ ` details: {'actor': event.actorSubject, 'occurredAt': event.occurredAt, 'domain': event.domain},`,
1428
+ ` refs: eventRefs(event))).toList();`,
1429
+ ` final aggregateTypes = ((model['aggregateTypes'] as List?) ?? const [])`,
1430
+ ` .map((raw) => (raw as Map).cast<String, dynamic>()).toList();`,
1431
+ ` for (final conflict in await bridge.pendingConflicts()) {`,
1432
+ ` Map<String, dynamic>? aggregate;`,
1433
+ ` for (final candidate in aggregateTypes) {`,
1434
+ ` if (candidate['queryType'] == conflict['aggregateType']) { aggregate = candidate; break; }`,
1435
+ ` }`,
1436
+ ` if (aggregate == null) continue;`,
1437
+ ` final field = (conflict['field'] ?? '').toString();`,
1438
+ ` final pages = (model['pages'] as Map).cast<String, dynamic>();`,
1439
+ ` final ruleItems = (((pages['rules'] as Map)['items'] as List?) ?? const [])`,
1440
+ ` .map((raw) => (raw as Map).cast<String, dynamic>());`,
1441
+ ` Map<String, dynamic>? rule;`,
1442
+ ` final ruleId = '\${aggregate['domain']}.\${aggregate['type']}.$field.merge';`,
1443
+ ` for (final candidate in ruleItems) { if (candidate['id'] == ruleId) { rule = candidate; break; } }`,
1444
+ ` final subject = rule?['label']?.toString() ?? '\${aggregate['label']} $field';`,
1445
+ ` final status = conflict['status']?.toString() ?? 'ready';`,
1446
+ ` final versions = status == 'ready'`,
1447
+ ` ? ((conflict['observations'] as List?)?.length ?? 0)`,
1448
+ ` : ((((conflict['evolution'] as Map?)?['sourceSchemas']) as List?)?.length ?? 0);`,
1449
+ ` final conflictSummary = status == 'ready'`,
1450
+ ` ? '$subject has $versions competing versions waiting for one business merge.'`,
1451
+ ` : '$subject cannot yet be compared because the model does not explain every earlier value. The disagreement remains pending while unrelated work continues.';`,
1452
+ ` items.add(NomosDescribeItem(`,
1453
+ ` kind: status == 'ready' ? 'pending-merge' : 'evolution-required',`,
1454
+ ` id: 'decision:\${aggregate['domain']}.\${aggregate['type']}:\${conflict['aggregateId']}:$field',`,
1455
+ ` label: 'Review $subject', summary: conflictSummary,`,
1456
+ ` details: {'domain': aggregate['domain'], 'type': aggregate['type'], 'field': field, 'status': status, 'versions': versions, 'next': status == 'ready' ? 'merge' : 'update-model'},`,
1457
+ ` refs: [NomosRef(workspace: _ws, type: aggregate['type'].toString(), id: conflict['aggregateId'].toString())]));`,
1458
+ ` }`,
1459
+ ` final modelPageItems = ((((model['pages'] as Map)['model'] as Map)['items'] as List?) ?? const [])`,
1460
+ ` .map((raw) => (raw as Map).cast<String, dynamic>()).where((item) => item['kind'] == 'verb').toList();`,
1461
+ ` for (final entry in await bridge.deadLetters()) {`,
1462
+ ` final domain = entry['domain']; final directive = entry['directiveId'];`,
1463
+ ` if (domain is! String || directive is! String) continue;`,
1464
+ ` Map<String, dynamic>? action;`,
1465
+ ` for (final candidate in modelPageItems) { if (candidate['id'] == '$domain.$directive') { action = candidate; break; } }`,
1466
+ ` if (action == null) continue;`,
1467
+ ` final reason = refusalReason(entry['error']); final actionLabel = action['label'].toString();`,
1468
+ ` items.add(NomosDescribeItem(kind: 'rejected-action', id: 'rejected:\${entry['id'] ?? entry['oid']}',`,
1469
+ ` label: 'Review $actionLabel', summary: refusalSummary(reason, actionLabel),`,
1470
+ ` details: {'domain': domain, 'action': directive, 'status': 'retained', 'reason': reason, 'refusedAt': entry['at'], 'canRetry': true}));`,
1471
+ ` }`,
1472
+ ` final pages = (model['pages'] as Map).cast<String, dynamic>();`,
1473
+ ` final rules = ((pages['rules'] as Map)['items'] as List?) ?? const [];`,
1474
+ ` items.addAll(rules.where((raw) => (raw as Map)['kind'] == 'conflict-rule' || raw['kind'] == 'merge-rule').map(staticItem));`,
1475
+ ` } else if (section == NomosDescribeSection.evidence) {`,
1476
+ ` title = 'Evidence';`,
1477
+ ` summary = 'Opaque identities tying this answer to one workspace, one generated law and its verified business history.';`,
1478
+ ` final latest = events.isEmpty ? null : events.first;`,
1479
+ ` items = [`,
1480
+ ` NomosDescribeItem(kind: 'evidence', id: 'workspace', label: 'Workspace', summary: _ws, details: {'workspace': _ws}),`,
1481
+ ` NomosDescribeItem(kind: 'evidence', id: 'law', label: 'Generated law', summary: domainHash, details: {'domainHash': domainHash}),`,
1482
+ ` NomosDescribeItem(kind: 'evidence', id: 'reader', label: 'Verified reader', summary: this.actor ?? bridge.principal ?? 'anonymous', details: {'actor': this.actor ?? bridge.principal}),`,
1483
+ ` if (latest != null) NomosDescribeItem(kind: 'evidence', id: latest.eventId, label: 'Latest verified moment', summary: latest.eventId,`,
1484
+ ` details: {'occurredAt': latest.occurredAt, 'actor': latest.actorSubject, 'domain': latest.domain, 'kind': latest.kind}),`,
1485
+ ` ];`,
1486
+ ` }`,
1487
+ ` return NomosDescription(`,
1488
+ ` section: section, title: title, summary: summary, items: items, links: links, organisation: organisation,`,
1489
+ ` events: events, transcript: narrative.transcript);`,
755
1490
  ` }`,
756
1491
  ``,
757
1492
  ` /// NARRATIVE for ONE thing: the story of the aggregate [aggregateId] — events whose declared refs`,
@@ -790,7 +1525,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
790
1525
  ``,
791
1526
  // `dart:convert` is needed ONLY for the interface block's `jsonDecode` — the offer lane no longer encodes
792
1527
  // any envelope (it routes through `bridge.offerDirective` → THE ONE ENCODER). Omit it when no interface.
793
- ...(opts.interface !== undefined || roleHelpers !== "" ? [`import 'dart:convert';`, ``] : []),
1528
+ ...(clientInterface !== undefined || roleHelpers !== "" || opts.emitActivity === true ? [`import 'dart:convert';`, ``] : []),
794
1529
  `import '${supportImport}';`,
795
1530
  // Layered-packages mode: the sibling types live in their OWN package — one barrel import
796
1531
  // replaces both the relative types import and the relative framework import.
@@ -818,18 +1553,25 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
818
1553
  `// client file gives an app the whole typed surface.`,
819
1554
  ...(opts.typesImportUri !== undefined ? [`export '${opts.typesImportUri}';`] : [`export '${mod.name}.dart';`]),
820
1555
  ...(opts.typesImportUri === undefined && opts.frameworkImportUri !== undefined ? [`export '${opts.frameworkImportUri}';`] : []),
821
- `export '${supportImport}' show NomosBridge, NomosEngine, NomosSession, NomosSessionCompat, BridgeTransport, NomosBridgeError, MissingOfferAuthority, kWebviewBridgeVersion, NomosCompat, NomosCompatMember, NomosOfferOutcome, NomosCreated, NomosRef, NomosClientInterface, DomainClientSessionMismatch, DomainNotInstalledForSession, SessionMismatchCause${encPresent ? ", NomosScopeKey, NomosKeyStore, EncryptionKeyUnavailable, WrappedKeyStore, NomosEncryption, NomosEpochResult, EncryptionMemberKeyMissing, EncryptionRosterMismatch" : ""};`,
1556
+ `export '${supportImport}' show NomosBridge, NomosEngine, NomosSession, NomosSessionCompat, BridgeTransport, NomosBridgeError, MissingOfferAuthority, kWebviewBridgeVersion, NomosCompat, NomosCompatMember, NomosOfferOutcome, NomosCreated, NomosRef, NomosMoment, NomosClientInterface, DomainClientSessionMismatch, DomainNotInstalledForSession, SessionMismatchCause${manualEncryptionPresent ? ", NomosScopeKey, NomosKeyStore, EncryptionKeyUnavailable, WrappedKeyStore, NomosEncryption, NomosEpochResult, EncryptionMemberKeyMissing, EncryptionRosterMismatch" : ""};`,
822
1557
  ``,
823
1558
  `/// sha256 of the deployed \`.package.usda\` — the installed law this client offers under.`,
824
1559
  `const String ${hashConst} = ${dartStr(opts.domainHash)};`,
825
1560
  ``,
826
- ...(opts.interface !== undefined
1561
+ ...(opts.emitActivity === true
1562
+ ? [
1563
+ `/// Compiler-known business language and policy; verified runtime facts join it behind describe().`,
1564
+ `const String _nomosDescribeModelJson = ${dartRawJson(describeModel)};`,
1565
+ ``,
1566
+ ]
1567
+ : []),
1568
+ ...(clientInterface !== undefined
827
1569
  ? [
828
1570
  `/// This client's STRUCTURAL INTERFACE REQUIREMENT (#72) as JSON — the directives (payload`,
829
1571
  `/// shapes) + aggregates (read shapes, sid-keyed) it depends on. \`connect()\` negotiates this`,
830
1572
  `/// against the workspace's installed offer by SHAPE (stable-id subtyping), not by law hash,`,
831
1573
  `/// so the domain can evolve without republishing this client.`,
832
- `const String ${interfaceConstNameDart(mod.name)} = ${dartRawJson(opts.interface)};`,
1574
+ `const String ${interfaceConstNameDart(mod.name)} = ${dartRawJson(clientInterface)};`,
833
1575
  ``,
834
1576
  ]
835
1577
  : []),
@@ -842,6 +1584,8 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
842
1584
  ``,
843
1585
  ]
844
1586
  : []),
1587
+ ...pastDeclarations,
1588
+ ...conflictDeclarations,
845
1589
  `/// Typed async client for the \`${domain}\` domain, bound to a connected [NomosBridge].`,
846
1590
  `///`,
847
1591
  `/// Pass \`actor\` (the verified principal uid) to author AS that user — the bridge's \`offerDirective\` op`,
@@ -857,7 +1601,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
857
1601
  ` /// The verified principal uid every offer is authored as (sealed as \`security.actor\` by the one encoder).`,
858
1602
  ` /// Null ⇒ open (un-gated) authoring; a gated directive then throws [MissingOfferAuthority].`,
859
1603
  ` final String? actor;`,
860
- ...(encPresent
1604
+ ...(manualEncryptionPresent
861
1605
  ? [
862
1606
  ``,
863
1607
  ` /// THE KEY CUSTODY surface (E2E sealing). When set, encrypted directive fields SEAL automatically`,
@@ -866,7 +1610,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
866
1610
  ` final NomosKeyStore? keyStore;`,
867
1611
  ]
868
1612
  : []),
869
- ` ${className}(this.bridge, {this.domainHash = ${hashConst}, this.actor${encPresent ? ", this.keyStore" : ""}});`,
1613
+ ` ${className}(this.bridge, {this.domainHash = ${hashConst}, this.actor${manualEncryptionPresent ? ", this.keyStore" : ""}});`,
870
1614
  ``,
871
1615
  ` /// THE STATIC INTERFACE DESCRIPTOR (session_bound_clients.md asks 5/9): what this client EXPECTS`,
872
1616
  ` /// of the workspace it binds to — the domain key, the law hash it was generated against, and the`,
@@ -879,7 +1623,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
879
1623
  ` domain: ${dartStr(domain)},`,
880
1624
  ` expectedDomainHash: ${hashConst},`,
881
1625
  ...(compatInterfaceHash !== undefined ? [` expectedInterfaceHash: ${dartStr(compatInterfaceHash)},`] : []),
882
- ...(opts.interface !== undefined ? [` interfaceJson: ${interfaceConstNameDart(mod.name)},`] : []),
1626
+ ...(clientInterface !== undefined ? [` interfaceJson: ${interfaceConstNameDart(mod.name)},`] : []),
883
1627
  ...(opts.compat !== undefined ? [` compatJson: ${compatConstNameDart(mod.name)},`] : []),
884
1628
  ` );`,
885
1629
  ``,
@@ -890,13 +1634,16 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
890
1634
  ` /// IMMEDIATELY — at provider construction, not two clicks into the app. The synchronous`,
891
1635
  ` /// constructor remains for codepaths that can't await; it lazily runs the SAME preflight on the`,
892
1636
  ` /// first dispatch.`,
893
- ` static Future<${className}> bind(NomosSession session, {String? actor${encPresent ? ", NomosKeyStore? keyStore" : ""}}) async {`,
1637
+ ` static Future<${className}> bind(NomosSession session, {String? actor${manualEncryptionPresent ? ", NomosKeyStore? keyStore" : ""}}) async {`,
894
1638
  ` await session.assertCompatible(interface);`,
895
1639
  ` // RELEASE COHERENCE: the cloud's kernel must support what this law needs (e.g. a compound-key query`,
896
1640
  ` // needs 'compoundKeys'). Throws a typed KernelCoherenceError on a stale/mismatched cloud kernel,`,
897
1641
  ` // up front — not an opaque deep-fold refusal. Best-effort (no-op if the cloud advertises no caps).`,
898
1642
  ` await session.assertKernelCoherence(requiresKernel, lawHash: ${hashConst});`,
899
- ` return ${className}(session, actor: actor${encPresent ? ", keyStore: keyStore" : ""}).._preflighted = true;`,
1643
+ ...(ownedScope === undefined
1644
+ ? []
1645
+ : [` await session.nomosEnsureOwnedIdentity(scope: ${dartStr(ownedScope)}, domain: ${dartStr(domain)}, domainHash: ${hashConst});`]),
1646
+ ` return ${className}(session, actor: actor${manualEncryptionPresent ? ", keyStore: keyStore" : ""}).._preflighted = true${ownedScope === undefined ? "" : ".._ownedIdentityReady = true"};`,
900
1647
  ` }`,
901
1648
  ``,
902
1649
  ` /// OPEN-THEN-BIND (session_bound_clients.md ask 3) — the ONE-CALL path for a born/resident workspace:`,
@@ -905,13 +1652,13 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
905
1652
  ` /// resident/parked/just-born one opens local-only in place) and THEN binds — so a not-open workspace`,
906
1653
  ` /// fails HERE, at open, never later on the client's first query. For a genuinely-absent cloud workspace,`,
907
1654
  ` /// \`acquireSession(ws, ifAbsentCredentials: ...)\` it yourself then [bind].`,
908
- ` static Future<${className}> open(NomosBridge bridge, {NomosRef? ref, String? workspace, String? actor${encPresent ? ", NomosKeyStore? keyStore" : ""}}) async {`,
1655
+ ` static Future<${className}> open(NomosBridge bridge, {NomosRef? ref, String? workspace, String? actor${manualEncryptionPresent ? ", NomosKeyStore? keyStore" : ""}}) async {`,
909
1656
  ` final ws = ref?.workspace ?? workspace;`,
910
1657
  ` if (ws == null || ws.isEmpty) {`,
911
1658
  ` throw ArgumentError('${className}.open: pass a workspace: or ref: — the workspace to open and bind');`,
912
1659
  ` }`,
913
1660
  ` final session = await bridge.acquireSession(ws);`,
914
- ` return bind(session, actor: actor${encPresent ? ", keyStore: keyStore" : ""});`,
1661
+ ` return bind(session, actor: actor${manualEncryptionPresent ? ", keyStore: keyStore" : ""});`,
915
1662
  ` }`,
916
1663
  ``,
917
1664
  ` /// The session workspace this client is bound to (from the [NomosSession] handle, when constructed with`,
@@ -939,18 +1686,53 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
939
1686
  ` if (b is NomosSession) await b.assertCompatible(interface);`,
940
1687
  ` }`,
941
1688
  ``,
1689
+ ...(ownedScope === undefined || Object.keys(ownedActions).length === 0
1690
+ ? []
1691
+ : [
1692
+ ` static const Map<String, Map<String, String>> _ownedRecordActions = ${ownedActionsDart};`,
1693
+ ` bool _ownedIdentityReady = false;`,
1694
+ ` Future<void> _ensureOwnedIdentity() async {`,
1695
+ ` if (_ownedIdentityReady) return;`,
1696
+ ` await bridge.nomosEnsureOwnedIdentity(scope: ${dartStr(ownedScope)}, domain: ${dartStr(domain)}, domainHash: domainHash);`,
1697
+ ` _ownedIdentityReady = true;`,
1698
+ ` }`,
1699
+ ``,
1700
+ ` Future<NomosOfferOutcome> _ownedRecordAction(String action, NomosRef record, String peer) async {`,
1701
+ ` await _preflightOnce();`,
1702
+ ` await _ensureOwnedIdentity();`,
1703
+ ` if (record.workspace != _ws) throw ArgumentError('$action() cannot use a record from another workspace');`,
1704
+ ` final directiveId = _ownedRecordActions[record.type]?[action];`,
1705
+ ` if (directiveId == null) throw ArgumentError('$action() needs a generated owned record reference');`,
1706
+ ` if (peer.trim().isEmpty) throw ArgumentError('$action() needs the person to act with');`,
1707
+ ` final payload = await bridge.nomosPrepareOwnedRecordAction(action: action, scope: ${dartStr(ownedScope)}, record: record, peer: peer);`,
1708
+ ` return bridge.offerOutcome(domain: ${dartStr(domain)}, directiveId: directiveId, payload: payload, actor: actor, domainHash: domainHash);`,
1709
+ ` }`,
1710
+ ``,
1711
+ ]),
942
1712
  ...(dirs.length > 0 || birthDirs.some((d) => (d as { businessBirth?: unknown }).businessBirth !== undefined)
943
1713
  ? [
944
1714
  ` /// THE ONE DISPATCH LANE every generated directive method routes through: lazy preflight, then`,
945
1715
  ` /// the session's offer (the generated-internal use of the engine-level lane).`,
946
- ` Future<NomosOfferOutcome> _offer(String directiveId, Object? payload, {List<Map<String, Object?>>? attestations}) async {`,
1716
+ ` Future<NomosOfferOutcome> _offer(String directiveId, Object? payload, {List<Map<String, Object?>>? attestations, List<Map<String, Object?>>? conflictSnapshots}) async {`,
947
1717
  ` await _preflightOnce();`,
948
- ` return bridge.offerOutcome(domain: ${dartStr(domain)}, directiveId: directiveId, payload: payload, actor: actor, domainHash: domainHash, attestations: attestations);`,
1718
+ ` return bridge.offerOutcome(domain: ${dartStr(domain)}, directiveId: directiveId, payload: payload, actor: actor, domainHash: domainHash, attestations: attestations, nomosConflictSnapshots: conflictSnapshots);`,
949
1719
  ` }`,
950
1720
  ``,
951
1721
  ]
952
1722
  : []),
953
- ...(encPresent ? dartEncClassMembers(encMap!) : []),
1723
+ ...(encPresent ? dartEncClassMembers(encMap!, manualEncryptionPresent) : []),
1724
+ ...(ownedScope === undefined
1725
+ ? []
1726
+ : [
1727
+ ` /// Opt into all-device-loss recovery with one explicitly designated person.`,
1728
+ ` NomosRecovery get recovery => NomosRecovery(`,
1729
+ ` bridge: bridge, domain: ${dartStr(domain)}, domainHash: domainHash, scope: ${dartStr(ownedScope)});`,
1730
+ ``,
1731
+ ` /// Add another of this person's devices. Requests are opaque; Nomos owns the handoff.`,
1732
+ ` NomosDevices get devices => NomosDevices(`,
1733
+ ` bridge: bridge, domain: ${dartStr(domain)}, domainHash: domainHash, actor: actor, scope: ${dartStr(ownedScope)});`,
1734
+ ``,
1735
+ ]),
954
1736
  // THE OWNER CEREMONY (slice 2): grantAndWrap / revokeAndRotate / rotate — the operator-blind key-epoch
955
1737
  // handshake as one call. Bind `actor` (the owner) to author the fan-out. Only for a domain with a scope.
956
1738
  ...(domScopes.length > 0
@@ -963,7 +1745,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
963
1745
  ``,
964
1746
  ]
965
1747
  : []),
966
- ...(opts.interface !== undefined
1748
+ ...(clientInterface !== undefined
967
1749
  ? [
968
1750
  ` /// This client's structural interface requirement (#72) as JSON — hand it to the`,
969
1751
  ` /// bridge's compatibility check to negotiate by SHAPE against the workspace's offer.`,
@@ -1016,40 +1798,97 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
1016
1798
  }
1017
1799
 
1018
1800
  /** The in-class E2E-sealing members: the baked map + the seal/read private helpers. */
1019
- function dartEncClassMembers(encMap: EncDomainMap): string[] {
1801
+ function dartEncClassMembers(encMap: EncDomainMap, manualEncryptionPresent: boolean): string[] {
1020
1802
  return [
1021
1803
  ` /// The baked E2E-sealing map (compile-time, from the SAME sids the law was compiled with): which`,
1022
1804
  ` /// directive payload fields seal → { scope, aggSid, fieldSid }, and which read-model fields decrypt.`,
1023
1805
  ` static final Map<String, dynamic> encryptionMap = (jsonDecode(${dartRawJson(encMap)}) as Map).cast<String, dynamic>();`,
1024
1806
  ``,
1025
- ` /// Seal the mapped encrypted fields of \`data\` in place before dispatch. No keyStore / \`seal == false\``,
1026
- ` /// ⇒ passthrough (today's behavior); a held key seals; a missing key throws [EncryptionKeyUnavailable].`,
1027
- ` Future<void> _sealPayload(Map<String, dynamic> data, String directiveId, bool seal) async {`,
1028
- ` if (!seal || keyStore == null) return;`,
1807
+ ` /// Seal the mapped encrypted fields of \`data\` in place before dispatch. Ordinary encryption may`,
1808
+ ` /// use the bound keyStore; .owned() keys are held internally by the connected Nomos client.`,
1809
+ ` Future<void> _sealPayload(Map<String, dynamic> data, String directiveId, bool seal, [NomosScopeKey? ownedKeyOverride, String? ownedRecordType, String? ownedRecordId, bool ownedRecordPreMinted = false]) async {`,
1029
1810
  ` final fields = (encryptionMap['directives'] as Map)[directiveId] as Map?;`,
1030
1811
  ` if (fields == null) return;`,
1812
+ ` if (!seal) {`,
1813
+ ` if (fields.values.any((raw) => (raw as Map)['owned'] == true)) {`,
1814
+ ` throw StateError('.owned() encryption cannot be disabled');`,
1815
+ ` }`,
1816
+ ` return;`,
1817
+ ` }`,
1818
+ ` NomosScopeKey? ownedKey = ownedKeyOverride;`,
1819
+ ` Map? owned;`,
1820
+ ` for (final raw in fields.values) {`,
1821
+ ` final candidate = raw as Map;`,
1822
+ ` if (candidate['owned'] == true) { owned = candidate; break; }`,
1823
+ ` }`,
1824
+ ` if (owned != null) {`,
1825
+ ` if (ownedKey == null && (ownedRecordType == null || ownedRecordType.isEmpty || ownedRecordId == null || ownedRecordId.isEmpty)) {`,
1826
+ ` throw StateError('Nomos could not derive the owned record identity before sealing; recompile the client and law together');`,
1827
+ ` }`,
1828
+ ` ownedKey ??= await bridge.nomosOwnedScopeKey(owned['scope'] as String, recordType: ownedRecordType, recordId: ownedRecordId);`,
1829
+ ` final subject = ownedKey.ownedKeySubject;`,
1830
+ ` if (subject == null || subject.isEmpty) {`,
1831
+ ` throw StateError('Nomos did not return the framework-owned device identity for .owned() custody');`,
1832
+ ` }`,
1833
+ ` data[${JSON.stringify(OWNED_KEY_PAYLOAD_FIELD)}] = subject;`,
1834
+ ` if (ownedKey.ownedCustody != null && ownedKey.ownedCustody!.isNotEmpty) {`,
1835
+ ` data[${JSON.stringify(OWNED_CUSTODY_PAYLOAD_FIELD)}] = ownedKey.ownedCustody;`,
1836
+ ` }`,
1837
+ ` if (ownedRecordPreMinted) {`,
1838
+ ` data[${JSON.stringify(OWNED_PREMINTED_RECORDS_PAYLOAD_FIELD)}] = [{'type': ownedRecordType, 'id': ownedRecordId}];`,
1839
+ ` }`,
1840
+ ` }`,
1031
1841
  ` for (final entry in fields.entries) {`,
1032
1842
  ` final field = entry.key as String;`,
1033
1843
  ` final m = entry.value as Map;`,
1034
1844
  ` if (data[field] == null) continue;`,
1035
1845
  ` final scope = m['scope'] as String;`,
1036
- ` final sk = await keyStore!.scopeKey(scope);`,
1846
+ ` NomosScopeKey? sk = m['owned'] == true ? ownedKey : ${manualEncryptionPresent ? "keyStore == null ? null : await keyStore!.scopeKey(scope)" : "null"};`,
1037
1847
  ` if (sk == null) throw EncryptionKeyUnavailable(scope, field);`,
1038
1848
  ` data[field] = await bridge.cryptoSealField(`,
1039
- ` scopeKey: sk.key, scope: scope, epoch: sk.epoch,`,
1849
+ ` scopeKey: sk.key, scope: m['owned'] == true ? sk.scope : scope, epoch: sk.epoch,`,
1040
1850
  ` aggSid: m['aggSid'] as String, fieldSid: m['fieldSid'] as String,`,
1041
1851
  ` plaintext: data[field].toString(),`,
1042
1852
  ` );`,
1043
1853
  ` }`,
1044
1854
  ` }`,
1045
1855
  ``,
1046
- ` /// The read keys to inject for \`scopes\`, or null when there is nothing to inject (no keyStore / no`,
1047
- ` /// held keys) — the read then behaves exactly as today (encrypted fields stay opaque envelopes).`,
1048
- ` Future<List<NomosScopeKey>?> _readKeys(List<String> scopes) async {`,
1049
- ` if (keyStore == null || scopes.isEmpty) return null;`,
1050
- ` final keys = await keyStore!.readKeys(scopes);`,
1856
+ ` /// Read keys for ordinary app-custodied scopes plus framework-custodied .owned() scopes.`,
1857
+ ` Future<List<NomosScopeKey>?> _readKeys(List<Map<String, Object?>> scopes, [List<Map<String, String>> records = const []]) async {`,
1858
+ ` if (scopes.isEmpty) return null;`,
1859
+ ...(manualEncryptionPresent
1860
+ ? [` final ordinary = scopes.where((s) => s['owned'] != true).map((s) => s['scope']! as String).toSet().toList();`]
1861
+ : []),
1862
+ ` final keys = ${manualEncryptionPresent ? "keyStore == null || ordinary.isEmpty ? <NomosScopeKey>[] : await keyStore!.readKeys(ordinary)" : "<NomosScopeKey>[]"};`,
1863
+ ` for (final descriptor in scopes) {`,
1864
+ ` if (descriptor['owned'] != true) continue;`,
1865
+ ` final scope = descriptor['scope']! as String;`,
1866
+ ` for (final record in records) {`,
1867
+ ` final type = record['type']!;`,
1868
+ ` final id = record['id']!;`,
1869
+ ` final held = await bridge.nomosOwnedScopeKeys(scope, recordType: type, recordId: id);`,
1870
+ ` for (final key in held) {`,
1871
+ ` if (!keys.any((candidate) => candidate.scope == key.scope && candidate.epoch == key.epoch)) keys.add(key);`,
1872
+ ` }`,
1873
+ ` }`,
1874
+ ` }`,
1051
1875
  ` return keys.isEmpty ? null : keys;`,
1052
1876
  ` }`,
1053
1877
  ``,
1878
+ ` /// Multi-row owned reads are two local indexed reads: discover only authorized ciphertext rows,`,
1879
+ ` /// derive those record keys, then repeat with decryption. No key or crypto concept reaches the app.`,
1880
+ ` Future<List<Map<String, dynamic>>> _readWithOwnedKeys(`,
1881
+ ` List<Map<String, Object?>> scopes,`,
1882
+ ` Future<List<Map<String, dynamic>>> Function(List<NomosScopeKey>? keys) read,`,
1883
+ ` ) async {`,
1884
+ ` final first = await read(await _readKeys(scopes));`,
1885
+ ` if (!scopes.any((scope) => scope['owned'] == true) || first.isEmpty) return first;`,
1886
+ ` final records = first.map((row) => {`,
1887
+ ` 'type': (row['type'] ?? row['__type'] ?? '').toString(),`,
1888
+ ` 'id': (row['id'] ?? row['__id'] ?? '').toString(),`,
1889
+ ` }).where((record) => record['type']!.isNotEmpty && record['id']!.isNotEmpty).toList();`,
1890
+ ` return read(await _readKeys(scopes, records));`,
1891
+ ` }`,
1892
+ ``,
1054
1893
  ];
1055
1894
  }