@githolon/dsl 0.100.6 → 0.101.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dart/CHANGELOG.md +6 -0
- package/dart/lib/src/dev_cloud.dart +46 -15
- package/dart/lib/src/test_harness.dart +78 -16
- package/dart/lib/src/webview_bridge.dart +640 -126
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart/test/bundle_version_pairing_test.dart +11 -22
- package/dart/test/co2_product_gauntlet_test.dart +228 -0
- package/dart/test/errors_test.dart +63 -12
- package/dart/test/first_app_gauntlet_test.dart +29 -3
- package/dart/test/webview_bridge_test.dart +274 -0
- package/dart_types/CHANGELOG.md +6 -0
- package/dart_types/lib/src/errors.dart +136 -30
- package/dart_types/lib/src/surfaced.dart +48 -2
- package/dart_types/lib/src/wire.dart +3 -1
- package/dart_types/pubspec.yaml +1 -1
- package/dart_types/test/surfaced_test.dart +19 -0
- package/package.json +1 -1
- package/src/activity.ts +43 -4
- package/src/aggregate.ts +132 -22
- package/src/attested_core.ts +1 -1
- package/src/authoring.ts +119 -6
- package/src/birth_meter.ts +4 -4
- package/src/birth_meter_ensure.ts +9 -1
- package/src/births.ts +53 -22
- package/src/build_package.ts +55 -14
- package/src/business_birth.ts +48 -7
- package/src/codegen_dart.ts +267 -37
- package/src/codegen_dart_client.ts +921 -82
- package/src/codegen_openapi.ts +28 -5
- package/src/codegen_proof.ts +107 -11
- package/src/codegen_ts.ts +961 -135
- package/src/compile_package_main.ts +155 -45
- package/src/conflicts.ts +252 -0
- package/src/describe_model.ts +448 -0
- package/src/directive.ts +5 -38
- package/src/drivers.ts +12 -4
- package/src/engine_entry.ts +4 -0
- package/src/evolve_preview.ts +72 -11
- package/src/fields.ts +201 -55
- package/src/framework/application_home.ts +103 -0
- package/src/framework/application_home_domain.ts +2 -0
- package/src/framework/attested_receipts.ts +2 -2
- package/src/framework/auth_provider_law.ts +51 -24
- package/src/framework/business_corrections.ts +170 -0
- package/src/framework/disclosure.ts +1 -1
- package/src/framework/domain_lifecycle.ts +250 -16
- package/src/framework/encryption_identity.ts +1 -1
- package/src/framework/encryption_scope.ts +21 -13
- package/src/framework/era_certification.ts +1 -1
- package/src/framework/framework_aggregates.ts +2 -0
- package/src/framework/genesis.ts +1 -1
- package/src/framework/home.ts +22 -9
- package/src/framework/identity.ts +5 -5
- package/src/framework/impure_capability.ts +2 -2
- package/src/framework/owned_custody.ts +561 -0
- package/src/framework/platform_admin.ts +1 -1
- package/src/framework/relations.ts +9 -1
- package/src/framework/repair.ts +1 -1
- package/src/framework/signing_identity.ts +2 -2
- package/src/framework/source_roster.ts +1 -1
- package/src/framework/workspaces.ts +17 -10
- package/src/generated_client_names.ts +39 -0
- package/src/index.ts +20 -15
- package/src/kernel_capabilities.ts +25 -1
- package/src/law_check.ts +4 -1
- package/src/manifest.ts +356 -63
- package/src/read_projection_manifest.ts +8 -0
- package/src/read_projection_namespace.ts +4 -0
- package/src/relation.ts +1 -1
- package/src/shape_compat.ts +47 -2
- package/src/stable_ids.ts +38 -6
- package/src/stable_ids_types.ts +30 -0
- package/src/surfaced_lint.ts +0 -0
- package/src/usd.ts +62 -11
- package/src/usd_layers.ts +54 -3
- package/src/value_object.ts +23 -19
- package/src/wire.ts +1 -0
- package/src/wire_encode.ts +48 -4
- package/src/workspace_routing.ts +57 -0
- package/src/workspace_sharding.ts +9 -9
- package/src/certified_read.ts +0 -313
|
@@ -28,7 +28,13 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import type { z } from "zod";
|
|
30
30
|
|
|
31
|
-
import
|
|
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(),
|
|
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
|
|
135
|
-
*
|
|
136
|
-
*
|
|
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
|
-
|
|
209
|
-
? domainEncryptionMap(mod,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
// ──
|
|
493
|
-
//
|
|
494
|
-
//
|
|
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
|
-
` ///
|
|
499
|
-
`
|
|
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
|
-
` ///
|
|
505
|
-
`
|
|
506
|
-
` bridge.
|
|
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
|
|
516
|
-
|
|
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
|
-
`
|
|
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)}:
|
|
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${
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
728
|
-
` ///
|
|
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
|
|
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
|
-
` ///
|
|
743
|
-
` ///
|
|
744
|
-
`
|
|
745
|
-
`
|
|
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
|
-
`
|
|
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
|
-
...(
|
|
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${
|
|
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.
|
|
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(
|
|
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
|
-
...(
|
|
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${
|
|
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
|
-
...(
|
|
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${
|
|
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
|
-
|
|
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${
|
|
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${
|
|
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
|
-
...(
|
|
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.
|
|
1026
|
-
` ///
|
|
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
|
-
`
|
|
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
|
-
` ///
|
|
1047
|
-
`
|
|
1048
|
-
`
|
|
1049
|
-
|
|
1050
|
-
|
|
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
|
}
|