@githolon/dsl 0.71.0 → 0.72.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 +28 -0
- package/dart/lib/nomos_client.dart +4 -0
- package/dart/lib/src/session_bound.dart +309 -0
- package/dart/lib/src/test_harness.dart +136 -4
- package/dart/lib/src/webview_bridge.dart +164 -25
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart/test/cluster_attested_test.dart +0 -1
- package/dart/test/session_bound_test.dart +289 -0
- package/dart_types/CHANGELOG.md +9 -0
- package/dart_types/lib/nomos_types.dart +3 -0
- package/dart_types/lib/src/surfaced.dart +208 -0
- package/dart_types/pubspec.yaml +1 -1
- package/dart_types/test/surfaced_test.dart +69 -0
- package/package.json +1 -1
- package/src/aggregate.ts +64 -7
- package/src/build_package.ts +22 -0
- package/src/codegen_dart.ts +155 -31
- package/src/codegen_dart_client.ts +245 -28
- package/src/codegen_openapi.ts +20 -0
- package/src/codegen_proof.ts +5 -1
- package/src/codegen_ts.ts +113 -1
- package/src/compile_package_main.ts +130 -14
- package/src/directive.ts +40 -0
- package/src/drivers.ts +63 -2
- package/src/idof_lint.ts +144 -0
- package/src/index.ts +1 -0
- package/src/surfaced_lint.ts +0 -0
- package/src/usd_layers.ts +5 -0
- package/src/wire.ts +4 -1
|
@@ -33,7 +33,14 @@ import type { Field } from "./fields.js";
|
|
|
33
33
|
import type { Directive } from "./directive.js";
|
|
34
34
|
import type { QueryDecl } from "./query.js";
|
|
35
35
|
import type { DomainModule } from "./codegen_dart.js";
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
cap,
|
|
38
|
+
readModelClassName,
|
|
39
|
+
dartRoleHelpers,
|
|
40
|
+
registerDartNames,
|
|
41
|
+
concreteClassName,
|
|
42
|
+
dartMemberName,
|
|
43
|
+
} from "./codegen_dart.js";
|
|
37
44
|
import { finishCount } from "./count.js";
|
|
38
45
|
import { finishSum } from "./sum.js";
|
|
39
46
|
import { autoStampFields, domainEncryptionMap, type EncDomainMap, type InterfaceAggShape } from "./codegen_ts.js";
|
|
@@ -139,6 +146,7 @@ export interface DartBridgeClientOptions {
|
|
|
139
146
|
|
|
140
147
|
/** Generate the `<name>.client.dart` async bridge client for one domain module. */
|
|
141
148
|
export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClientOptions): string {
|
|
149
|
+
registerDartNames(mod.aggregates); // `.dartName` overrides for read-model class names in the client.
|
|
142
150
|
const domain = mod.domain ?? mod.name;
|
|
143
151
|
const className = dartClientClassName(domain);
|
|
144
152
|
const hashConst = dartHashConstName(mod.name);
|
|
@@ -183,6 +191,11 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
183
191
|
|
|
184
192
|
const methods: string[] = [];
|
|
185
193
|
|
|
194
|
+
// THE STATIC INTERFACE DESCRIPTOR (`XClient.interface`, session_bound_clients.md asks 5/9) —
|
|
195
|
+
// what this client EXPECTS of a workspace: the domain key, the law hash it compiled under, and
|
|
196
|
+
// the structural interface (#72). `bind(session)` / `session.assertCompatible(...)` consume it.
|
|
197
|
+
const compatInterfaceHash = (opts.compat as { interfaceHash?: string } | undefined)?.interfaceHash;
|
|
198
|
+
|
|
186
199
|
// ── directives → typed offer ──
|
|
187
200
|
// A `.births()` directive (birthsChild === true) is NOT a same-workspace write — it is a signed
|
|
188
201
|
// cross-workspace governance offer to a PARENT. It gets a typed `bridge.createWorkspace(...)` wrapper
|
|
@@ -232,7 +245,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
232
245
|
]
|
|
233
246
|
: []),
|
|
234
247
|
` await _sealPayload(data, ${dartStr(d.id)}, seal);`,
|
|
235
|
-
` return
|
|
248
|
+
` return _offer(${dartStr(d.id)}, data);`,
|
|
236
249
|
` }`,
|
|
237
250
|
``,
|
|
238
251
|
);
|
|
@@ -247,7 +260,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
247
260
|
` final data = payload.toPayloadJson();`,
|
|
248
261
|
` final now = DateTime.now().toUtc().toIso8601String();`,
|
|
249
262
|
...stamps.map((s) => ` data.putIfAbsent(${dartStr(s)}, () => now);`),
|
|
250
|
-
` return
|
|
263
|
+
` return _offer(${dartStr(d.id)}, data);`,
|
|
251
264
|
` }`,
|
|
252
265
|
``,
|
|
253
266
|
);
|
|
@@ -258,7 +271,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
258
271
|
gateDoc,
|
|
259
272
|
` Future<NomosOfferOutcome> ${method}(${payloadClass} payload) {`,
|
|
260
273
|
gateGuard,
|
|
261
|
-
` return
|
|
274
|
+
` return _offer(${dartStr(d.id)}, payload.toPayloadJson());`,
|
|
262
275
|
` }`,
|
|
263
276
|
``,
|
|
264
277
|
);
|
|
@@ -267,7 +280,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
267
280
|
` /// Author \`${domain}/${d.id}\` locally under the installed law; returns the typed [NomosOfferOutcome].${createsDoc}`,
|
|
268
281
|
...(gateDoc ? [gateDoc] : []),
|
|
269
282
|
` Future<NomosOfferOutcome> ${method}(${payloadClass} payload) =>`,
|
|
270
|
-
`
|
|
283
|
+
` _offer(${dartStr(d.id)}, payload.toPayloadJson());`,
|
|
271
284
|
``,
|
|
272
285
|
);
|
|
273
286
|
}
|
|
@@ -281,31 +294,67 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
281
294
|
for (const d of birthDirs) {
|
|
282
295
|
const payloadClass = `${cap(d.id)}Payload`;
|
|
283
296
|
const method = safeMethod(d.id);
|
|
297
|
+
const gate = (d as { requiresRelationGate?: { relation: string } }).requiresRelationGate;
|
|
284
298
|
methods.push(
|
|
285
|
-
` /// Births a CHILD WORKSPACE named by \`${payloadClass}\`
|
|
286
|
-
` ///
|
|
287
|
-
` ///
|
|
288
|
-
` /// \`
|
|
289
|
-
` ///
|
|
290
|
-
` ///
|
|
291
|
-
`
|
|
292
|
-
`
|
|
293
|
-
`
|
|
294
|
-
`
|
|
295
|
-
`
|
|
299
|
+
` /// Births a CHILD WORKSPACE named by \`${payloadClass}\` — a \`.births()\` offer on THIS client's`,
|
|
300
|
+
` /// session, whose workspace IS the parent (session_bound_clients.md ask 4: no loose parent string).`,
|
|
301
|
+
` /// Returns the typed [NomosOfferOutcome]: \`outcome.born\` names the child, \`outcome.bornRefs\` are`,
|
|
302
|
+
` /// followable — \`final child = await engine.follow(outcome.bornRefs.single);\` mounts it, typed`,
|
|
303
|
+
` /// client construction is then \`await ChildClient.bind(child)\`. [actor] (a [NomosPrincipal])`,
|
|
304
|
+
` /// overrides this client's own \`actor\` for the offer.`,
|
|
305
|
+
` ///`,
|
|
306
|
+
` /// COMPATIBILITY SHIM (one minor version): passing the deprecated [parent] with a workspace that is`,
|
|
307
|
+
` /// NOT this session's routes the old SIGNED cross-workspace gateway relay`,
|
|
308
|
+
` /// (POST /v2/workspaces/<parent>/${d.id}); its verdict rides \`outcome.gatewayVerdict\`. [authorSecret]/`,
|
|
309
|
+
` /// [authToken] only apply on that legacy lane.`,
|
|
310
|
+
` Future<NomosOfferOutcome> ${method}({`,
|
|
296
311
|
` required ${payloadClass} payload,`,
|
|
312
|
+
` NomosPrincipal? actor,`,
|
|
313
|
+
` @Deprecated('the birth parent IS the session workspace — bind this client to the PARENT session '`,
|
|
314
|
+
` '(session_bound_clients.md ask 4); an explicit different parent rides the legacy gateway relay '`,
|
|
315
|
+
` 'one more minor version') String? parent,`,
|
|
316
|
+
` NomosDeviceSecret? authorSecret,`,
|
|
297
317
|
` String? authToken,`,
|
|
298
|
-
` })
|
|
299
|
-
`
|
|
318
|
+
` }) async {`,
|
|
319
|
+
` final ws = _ws;`,
|
|
320
|
+
` final explicitParent = parent;`,
|
|
321
|
+
` if (explicitParent != null && explicitParent.isNotEmpty && explicitParent != ws) {`,
|
|
322
|
+
` // LEGACY GATEWAY SHIM — the pre-session-bound cross-workspace relay, delegated unchanged.`,
|
|
323
|
+
` if (actor == null) {`,
|
|
324
|
+
` throw MissingOfferAuthority(domain: ${dartStr(domain)}, directiveId: ${dartStr(d.id)}, requires: 'a requesting principal (actor) on the legacy parent-relay lane');`,
|
|
325
|
+
` }`,
|
|
326
|
+
` final verdict = await bridge.createWorkspace(`,
|
|
300
327
|
` domain: ${dartStr(domain)},`,
|
|
301
328
|
` directiveId: ${dartStr(d.id)},`,
|
|
302
329
|
` domainHash: domainHash,`,
|
|
303
|
-
` parent:
|
|
330
|
+
` parent: explicitParent,`,
|
|
304
331
|
` actor: actor.wire,`,
|
|
305
332
|
` authorSecret: authorSecret?.wire,`,
|
|
306
333
|
` payload: payload.toPayloadJson(),`,
|
|
307
334
|
` authToken: authToken,`,
|
|
308
335
|
` );`,
|
|
336
|
+
` return NomosOfferOutcome(`,
|
|
337
|
+
` head: (verdict['head'] ?? '').toString(),`,
|
|
338
|
+
` created: const [],`,
|
|
339
|
+
` workspace: explicitParent,`,
|
|
340
|
+
` gatewayVerdict: verdict);`,
|
|
341
|
+
` }`,
|
|
342
|
+
` if (ws.isEmpty) {`,
|
|
343
|
+
` throw StateError(`,
|
|
344
|
+
` '${method}: bind this client to the PARENT NomosSession (${className}.bind(session)) — '`,
|
|
345
|
+
` 'the birth parent IS the session workspace, never a loose string');`,
|
|
346
|
+
` }`,
|
|
347
|
+
` final effActor = actor?.wire ?? this.actor;`,
|
|
348
|
+
...(gate !== undefined
|
|
349
|
+
? [
|
|
350
|
+
` if (effActor == null || effActor.trim().isEmpty) {`,
|
|
351
|
+
` throw MissingOfferAuthority(domain: ${dartStr(domain)}, directiveId: ${dartStr(d.id)}, requires: ${dartStr(gate.relation)});`,
|
|
352
|
+
` }`,
|
|
353
|
+
]
|
|
354
|
+
: []),
|
|
355
|
+
` await _preflightOnce();`,
|
|
356
|
+
` return bridge.offerOutcome(domain: ${dartStr(domain)}, directiveId: ${dartStr(d.id)}, payload: payload.toPayloadJson(), actor: effActor, domainHash: domainHash);`,
|
|
357
|
+
` }`,
|
|
309
358
|
``,
|
|
310
359
|
);
|
|
311
360
|
}
|
|
@@ -366,11 +415,64 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
366
415
|
);
|
|
367
416
|
}
|
|
368
417
|
|
|
418
|
+
// ── SURFACED CONFLICT DX (surfaced_merge_conflict.md): the workspace-wide pendingConflicts
|
|
419
|
+
// read/watch + one typed `resolve…` helper per designated `.resolves()` directive. Emitted iff
|
|
420
|
+
// this domain declares a Surfaced field (Surfaced-free packages emit nothing — the era rule).
|
|
421
|
+
const resolverBindings = (mod as { surfacedResolvers?: import("./surfaced_lint.js").SurfacedResolverBinding[] }).surfacedResolvers ?? [];
|
|
422
|
+
if (resolverBindings.length > 0) {
|
|
423
|
+
methods.push(
|
|
424
|
+
` /// PENDING SURFACED CONFLICTS across the workspace — the maintained "decisions needed" read`,
|
|
425
|
+
` /// (reserved id \`nomosPendingConflicts\`; DERIVED, NOT DRAINED: a resolution write folding in`,
|
|
426
|
+
` /// removes its row by derivation). Pair each row with its typed \`resolve…\` helper below.`,
|
|
427
|
+
` Future<List<PendingConflictRow>> pendingConflicts() async =>`,
|
|
428
|
+
` (await bridge.pendingConflicts()).map(PendingConflictRow.fromJson).toList();`,
|
|
429
|
+
``,
|
|
430
|
+
` /// Reactive pending-conflicts read — re-emits whenever the maintained index changes.`,
|
|
431
|
+
` Stream<List<PendingConflictRow>> watchPendingConflicts() =>`,
|
|
432
|
+
` bridge.watchPendingConflicts().map((rows) => rows`,
|
|
433
|
+
` .map((r) => PendingConflictRow.fromJson((r as Map).cast<String, dynamic>()))`,
|
|
434
|
+
` .toList());`,
|
|
435
|
+
``,
|
|
436
|
+
);
|
|
437
|
+
for (const b of resolverBindings) {
|
|
438
|
+
if (b.valuePayloadField === undefined || b.idPayloadField === undefined) continue; // probe couldn't bind — dispatch the directive method directly.
|
|
439
|
+
const agg = aggById.get(b.aggregateId);
|
|
440
|
+
const fieldKind = (agg?.fields[b.field] as Field | undefined)?.kind;
|
|
441
|
+
const chosenType = fieldKind === "string" ? "String" : fieldKind === "int" ? "int" : "Object?";
|
|
442
|
+
const helper = safeMethod(`resolve${cap(camel(concreteClassName(b.aggregateId)))}${cap(camel(b.field))}`);
|
|
443
|
+
const dir = allDirs.find((d) => d.id === b.directiveId);
|
|
444
|
+
const gate = (dir as { requiresRelationGate?: { relation: string } } | undefined)?.requiresRelationGate;
|
|
445
|
+
methods.push(
|
|
446
|
+
` /// RESOLVE a pending conflict on \`${b.aggregateId}.${b.field}\` by dispatching the DESIGNATED`,
|
|
447
|
+
` /// resolver \`${b.directiveId}\` with [chosen] (the conflict row names the instance; extra required`,
|
|
448
|
+
` /// payload fields ride [rest]). ESCAPE HATCH: any gated directive that sets the field still`,
|
|
449
|
+
` /// resolves the conflict in the kernel sense; \`.resolves()\` only designates which one THIS`,
|
|
450
|
+
` /// typed helper dispatches.`,
|
|
451
|
+
` Future<NomosOfferOutcome> ${helper}(PendingConflictRow conflict, ${chosenType} chosen,`,
|
|
452
|
+
` {Map<String, Object?> rest = const {}}) {`,
|
|
453
|
+
...(gate !== undefined
|
|
454
|
+
? [
|
|
455
|
+
` if (actor == null || actor!.trim().isEmpty) {`,
|
|
456
|
+
` throw MissingOfferAuthority(domain: ${dartStr(domain)}, directiveId: ${dartStr(b.directiveId)}, requires: ${dartStr(gate.relation)});`,
|
|
457
|
+
` }`,
|
|
458
|
+
]
|
|
459
|
+
: []),
|
|
460
|
+
` final data = <String, Object?>{...rest, ${dartStr(b.idPayloadField)}: conflict.aggregateId, ${dartStr(b.valuePayloadField)}: chosen};`,
|
|
461
|
+
` return _offer(${dartStr(b.directiveId)}, data);`,
|
|
462
|
+
` }`,
|
|
463
|
+
``,
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
369
468
|
// ── per-aggregate by-id read/watch + type-index list ──
|
|
370
469
|
for (const agg of mod.aggregates) {
|
|
371
470
|
const model = readModelClassName(agg.id);
|
|
372
|
-
|
|
373
|
-
|
|
471
|
+
// Accessor method-name stem honours `.dartName` (e.g. `EstateSite` + .dartName("Site") → `siteById`,
|
|
472
|
+
// `watchSiteById`, `listSites`) so the whole Dart surface renames consistently.
|
|
473
|
+
const name = concreteClassName(agg.id);
|
|
474
|
+
const lc = lcFirst(camel(name));
|
|
475
|
+
const Cap = cap(camel(name));
|
|
374
476
|
methods.push(
|
|
375
477
|
` /// Read one ${agg.id} by aggregate id.${readScopesFor(agg.id).length ? " Encrypted field(s) decrypt when the bound [keyStore] holds the scope key." : ""}`,
|
|
376
478
|
` Future<List<${model}>> ${safeMethod(`${lc}ById`)}(String id) async =>`,
|
|
@@ -387,6 +489,67 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
387
489
|
);
|
|
388
490
|
}
|
|
389
491
|
|
|
492
|
+
// ── REF-TRAVERSAL ACCESSORS (dart_codegen_parity.md P2 — ref ergonomics) ──
|
|
493
|
+
// For every SAME-WORKSPACE `t.ref(Target)` field whose target aggregate is ALSO in THIS domain module
|
|
494
|
+
// (so its by-id accessor is right here on this SAME client class), emit a companion extension method
|
|
495
|
+
// that resolves the stored ref id to the actual target row — reusing the EXISTING `<target>ById`
|
|
496
|
+
// by-id accessor above (no second read path, no new bridge op). SCOPE (deliberately narrow):
|
|
497
|
+
// - `t.foreignRef` fields are NOT covered — a foreign ref crosses workspaces (its own locator +
|
|
498
|
+
// attested-read trust plumbing, a different lane entirely; `ForeignRef<T>` stays as-is).
|
|
499
|
+
// - a same-workspace ref field whose target aggregate is a CROSS-DOMAIN reference (not present in
|
|
500
|
+
// `mod.aggregates`) is skipped too — its by-id accessor lives on a DIFFERENT domain's generated
|
|
501
|
+
// client, unreachable from an extension scoped to this file.
|
|
502
|
+
// A domain with no resolvable ref field emits none of this (byte-identical to before this existed).
|
|
503
|
+
const refExtensions: string[] = [];
|
|
504
|
+
for (const agg of mod.aggregates) {
|
|
505
|
+
const model = readModelClassName(agg.id);
|
|
506
|
+
const refFields = Object.entries(agg.fields).filter(([, f]) => {
|
|
507
|
+
const field = f as Field;
|
|
508
|
+
return field.kind === "ref" && field.foreignRef === undefined && field.refAggregateId !== undefined;
|
|
509
|
+
}) as [string, Field][];
|
|
510
|
+
const targetableFields = refFields.filter(([, f]) => aggById.has(f.refAggregateId!));
|
|
511
|
+
if (targetableFields.length === 0) continue;
|
|
512
|
+
const lines: string[] = [
|
|
513
|
+
`/// Ref-traversal accessors for [${model}] (dart_codegen_parity.md P2) — resolve a stored ref id`,
|
|
514
|
+
`/// to the actual target row via the SAME by-id read lane already exposed above.`,
|
|
515
|
+
`extension ${model}Refs on ${model} {`,
|
|
516
|
+
];
|
|
517
|
+
for (const [fieldName, f] of targetableFields) {
|
|
518
|
+
const targetType = f.refAggregateId!;
|
|
519
|
+
const targetModel = readModelClassName(targetType);
|
|
520
|
+
const targetConcrete = concreteClassName(targetType);
|
|
521
|
+
const targetLc = lcFirst(camel(targetConcrete));
|
|
522
|
+
const byIdMethod = safeMethod(`${targetLc}ById`);
|
|
523
|
+
const member = dartMemberName(fieldName);
|
|
524
|
+
// `siteId` → `site` (the common `<name>Id` ref-field convention); anything else gets a `Ref`
|
|
525
|
+
// suffix so the resolver method never collides with the stored field's own getter.
|
|
526
|
+
const methodStem = member.length > 2 && member.endsWith("Id") ? member.slice(0, -2) : `${member}Ref`;
|
|
527
|
+
const method = safeMethod(methodStem);
|
|
528
|
+
// NULLABILITY: the read-model member's Dart type follows the PROJECTION's own nullability, not
|
|
529
|
+
// the DSL schema's `.optional()` — a partial fold decodes every scalar `null` when absent
|
|
530
|
+
// (`emitReadModel`'s own doc: "PARTIAL folds"), so the member is nullable UNLESS the field is
|
|
531
|
+
// `.fromBirth()` (birth-presence law — the ONE case the projection types non-null).
|
|
532
|
+
const memberNullable = f.isFromBirth !== true;
|
|
533
|
+
lines.push(
|
|
534
|
+
` /// Resolve \`${fieldName}\` (${agg.id}.${fieldName} -> ${targetType}) by dispatching to`,
|
|
535
|
+
` /// [${className}.${byIdMethod}] — the existing by-id read lane, never a second one.`,
|
|
536
|
+
` Future<${targetModel}?> ${method}(${className} client) async {`,
|
|
537
|
+
...(memberNullable
|
|
538
|
+
? [
|
|
539
|
+
` final id = ${member};`,
|
|
540
|
+
` if (id == null) return null;`,
|
|
541
|
+
` final rows = await client.${byIdMethod}(id.value);`,
|
|
542
|
+
]
|
|
543
|
+
: [` final rows = await client.${byIdMethod}(${member}.value);`]),
|
|
544
|
+
` return rows.isEmpty ? null : rows.first;`,
|
|
545
|
+
` }`,
|
|
546
|
+
``,
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
lines.push(`}`);
|
|
550
|
+
refExtensions.push(lines.join("\n"));
|
|
551
|
+
}
|
|
552
|
+
|
|
390
553
|
// ── activity feed → typed reactive + one-shot (declared `.activity(...)`) ──
|
|
391
554
|
if (opts.emitActivity === true) {
|
|
392
555
|
methods.push(
|
|
@@ -439,17 +602,20 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
439
602
|
return [
|
|
440
603
|
`// AUTO-GENERATED by nomos-compile — typed ASYNC Dart client for domain "${domain}".`,
|
|
441
604
|
`// Drives the REAL githolon over a NomosBridge (the WebView runner). Regenerated every compile; do`,
|
|
442
|
-
`// not edit.
|
|
605
|
+
`// not edit. SESSION-BOUND (architecture/session_bound_clients.md): construct with a NomosSession —`,
|
|
606
|
+
`// one session, one workspace, one domain interface, one actor context:`,
|
|
443
607
|
`//`,
|
|
444
608
|
// GENERATED-CODE lints: the bridge read/watch surface returns dynamic (JSON rows), so typed accessors
|
|
445
609
|
// do dynamic member access + casts by construction. Silence the lints that only make sense for
|
|
446
610
|
// hand-written code, so this file never adds noise to a tenant's analyzer output (co2 feedback).
|
|
447
|
-
|
|
611
|
+
// (deprecated_member_use*: the compatibility shims intentionally delegate to the deprecated
|
|
612
|
+
// engine-level lanes for one minor version.)
|
|
613
|
+
`// ignore_for_file: avoid_dynamic_calls, unnecessary_this, prefer_const_constructors, unused_import, type_annotate_public_apis, always_specify_types, deprecated_member_use, deprecated_member_use_from_same_package`,
|
|
448
614
|
`//`,
|
|
449
|
-
`// final
|
|
450
|
-
`// await
|
|
451
|
-
`// await
|
|
452
|
-
`// final app = ${className}(
|
|
615
|
+
`// final engine = NomosBridge(transport); // the device's local plane (NomosEngine)`,
|
|
616
|
+
`// await engine.ready;`,
|
|
617
|
+
`// final session = await engine.open(cloud: cloud, workspace: ws, clientId: deviceId);`,
|
|
618
|
+
`// final app = await ${className}.bind(session); // preflights law compatibility (ask 5)`,
|
|
453
619
|
`// await app.${dirs[0] ? safeMethod(dirs[0].id) : "someDirective"}(/* typed payload */);`,
|
|
454
620
|
``,
|
|
455
621
|
// `dart:convert` is needed ONLY for the interface block's `jsonDecode` — the offer lane no longer encodes
|
|
@@ -469,7 +635,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
469
635
|
`// client file gives an app the whole typed surface.`,
|
|
470
636
|
...(opts.typesImportUri !== undefined ? [`export '${opts.typesImportUri}';`] : [`export '${mod.name}.dart';`]),
|
|
471
637
|
...(opts.typesImportUri === undefined && opts.frameworkImportUri !== undefined ? [`export '${opts.frameworkImportUri}';`] : []),
|
|
472
|
-
`export '${supportImport}' show NomosBridge, NomosSession, BridgeTransport, NomosBridgeError, MissingOfferAuthority, kWebviewBridgeVersion, NomosCompat, NomosCompatMember, NomosOfferOutcome, NomosCreated, NomosRef${encPresent ? ", NomosScopeKey, NomosKeyStore, EncryptionKeyUnavailable, WrappedKeyStore, NomosEncryption, NomosEpochResult, EncryptionMemberKeyMissing, EncryptionRosterMismatch" : ""};`,
|
|
638
|
+
`export '${supportImport}' show NomosBridge, NomosEngine, NomosSession, NomosSessionCompat, BridgeTransport, NomosBridgeError, MissingOfferAuthority, kWebviewBridgeVersion, NomosCompat, NomosCompatMember, NomosOfferOutcome, NomosCreated, NomosRef, NomosClientInterface, DomainClientSessionMismatch, DomainNotInstalledForSession, SessionMismatchCause${encPresent ? ", NomosScopeKey, NomosKeyStore, EncryptionKeyUnavailable, WrappedKeyStore, NomosEncryption, NomosEpochResult, EncryptionMemberKeyMissing, EncryptionRosterMismatch" : ""};`,
|
|
473
639
|
``,
|
|
474
640
|
`/// sha256 of the deployed \`.package.usda\` — the installed law this client offers under.`,
|
|
475
641
|
`const String ${hashConst} = ${dartStr(opts.domainHash)};`,
|
|
@@ -519,6 +685,29 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
519
685
|
: []),
|
|
520
686
|
` ${className}(this.bridge, {this.domainHash = ${hashConst}, this.actor${encPresent ? ", this.keyStore" : ""}});`,
|
|
521
687
|
``,
|
|
688
|
+
` /// THE STATIC INTERFACE DESCRIPTOR (session_bound_clients.md asks 5/9): what this client EXPECTS`,
|
|
689
|
+
` /// of the workspace it binds to — the domain key, the law hash it was generated against, and the`,
|
|
690
|
+
` /// structural interface (#72). Consumed by [bind] and by \`session.assertCompatible(${className}.interface)\`.`,
|
|
691
|
+
` static const NomosClientInterface interface = NomosClientInterface(`,
|
|
692
|
+
` domain: ${dartStr(domain)},`,
|
|
693
|
+
` expectedDomainHash: ${hashConst},`,
|
|
694
|
+
...(compatInterfaceHash !== undefined ? [` expectedInterfaceHash: ${dartStr(compatInterfaceHash)},`] : []),
|
|
695
|
+
...(opts.interface !== undefined ? [` interfaceJson: ${interfaceConstNameDart(mod.name)},`] : []),
|
|
696
|
+
...(opts.compat !== undefined ? [` compatJson: ${compatConstNameDart(mod.name)},`] : []),
|
|
697
|
+
` );`,
|
|
698
|
+
``,
|
|
699
|
+
` /// THE SESSION-BOUND CONSTRUCTION PATH (session_bound_clients.md asks 2/5): bind this client to`,
|
|
700
|
+
` /// [session] with an ASYNC PREFLIGHT — the session's workspace must be reachable, hold the`,
|
|
701
|
+
` /// \`${domain}\` domain, and serve a structurally compatible interface (#72 shape-compat, the ONE`,
|
|
702
|
+
` /// compatibility notion). An incompatible session throws [DomainClientSessionMismatch]`,
|
|
703
|
+
` /// IMMEDIATELY — at provider construction, not two clicks into the app. The synchronous`,
|
|
704
|
+
` /// constructor remains for codepaths that can't await; it lazily runs the SAME preflight on the`,
|
|
705
|
+
` /// first dispatch.`,
|
|
706
|
+
` static Future<${className}> bind(NomosSession session, {String? actor${encPresent ? ", NomosKeyStore? keyStore" : ""}}) async {`,
|
|
707
|
+
` await session.assertCompatible(interface);`,
|
|
708
|
+
` return ${className}(session, actor: actor${encPresent ? ", keyStore: keyStore" : ""}).._preflighted = true;`,
|
|
709
|
+
` }`,
|
|
710
|
+
``,
|
|
522
711
|
` /// The session workspace this client is bound to (from the [NomosSession] handle, when constructed with`,
|
|
523
712
|
` /// one) — the \`workspace\` of every read row's canonical [NomosRef]. '' on a bare (non-session) bridge.`,
|
|
524
713
|
` String get _ws {`,
|
|
@@ -526,6 +715,31 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
526
715
|
` return b is NomosSession ? b.workspace : '';`,
|
|
527
716
|
` }`,
|
|
528
717
|
``,
|
|
718
|
+
` /// The [NomosSession] this client dispatches through, when session-bound (the blessed shape);`,
|
|
719
|
+
` /// null on a bare (non-session) bridge. Inspectable context: \`client.session?.workspace\`.`,
|
|
720
|
+
` NomosSession? get session {`,
|
|
721
|
+
` final b = bridge;`,
|
|
722
|
+
` return b is NomosSession ? b : null;`,
|
|
723
|
+
` }`,
|
|
724
|
+
``,
|
|
725
|
+
` // LAZY PREFLIGHT (the can't-await constructor path): the first dispatch runs the same`,
|
|
726
|
+
` // assertCompatible check bind() runs eagerly. Only when session-bound; a bare bridge keeps the`,
|
|
727
|
+
` // pre-session behavior (the kernel gate remains the fail-closed floor either way).`,
|
|
728
|
+
` bool _preflighted = false;`,
|
|
729
|
+
` Future<void> _preflightOnce() async {`,
|
|
730
|
+
` if (_preflighted) return;`,
|
|
731
|
+
` _preflighted = true;`,
|
|
732
|
+
` final b = bridge;`,
|
|
733
|
+
` if (b is NomosSession) await b.assertCompatible(interface);`,
|
|
734
|
+
` }`,
|
|
735
|
+
``,
|
|
736
|
+
` /// THE ONE DISPATCH LANE every generated directive method routes through: lazy preflight, then`,
|
|
737
|
+
` /// the session's offer (the generated-internal use of the engine-level lane).`,
|
|
738
|
+
` Future<NomosOfferOutcome> _offer(String directiveId, Object? payload) async {`,
|
|
739
|
+
` await _preflightOnce();`,
|
|
740
|
+
` return bridge.offerOutcome(domain: ${dartStr(domain)}, directiveId: directiveId, payload: payload, actor: actor, domainHash: domainHash);`,
|
|
741
|
+
` }`,
|
|
742
|
+
``,
|
|
529
743
|
...(encPresent ? dartEncClassMembers(encMap!) : []),
|
|
530
744
|
// THE OWNER CEREMONY (slice 2): grantAndWrap / revokeAndRotate / rotate — the operator-blind key-epoch
|
|
531
745
|
// handshake as one call. Bind `actor` (the owner) to author the fan-out. Only for a domain with a scope.
|
|
@@ -578,6 +792,9 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
578
792
|
...methods,
|
|
579
793
|
`}`,
|
|
580
794
|
``,
|
|
795
|
+
...(refExtensions.length > 0
|
|
796
|
+
? [`// ---- ref-traversal accessors (dart_codegen_parity.md P2) ----`, refExtensions.join("\n\n"), ``]
|
|
797
|
+
: []),
|
|
581
798
|
...(roleHelpers !== ""
|
|
582
799
|
? [
|
|
583
800
|
`// ---- role policy (UI hints — the KERNEL GATE is the authority) ----`,
|
package/src/codegen_openapi.ts
CHANGED
|
@@ -192,6 +192,26 @@ export function generateOpenApiFragment(
|
|
|
192
192
|
{ name: "bbox", in: "query", required: true, schema: { type: "string" }, description: "minX,minY,maxX,maxY" },
|
|
193
193
|
]);
|
|
194
194
|
}
|
|
195
|
+
// THE PENDING-CONFLICTS READ (surfaced_merge_conflict.md — the DX layer): a domain declaring
|
|
196
|
+
// ≥1 `.merge(Surfaced(...))` field emits the reserved, workspace-wide `nomosPendingConflicts`
|
|
197
|
+
// read — the maintained "decisions needed" index (rows: {aggregateType, aggregateId, field,
|
|
198
|
+
// observations}). DECLARED BY THE LAW ITSELF (the era-key idiom, like nomosInvariantGate):
|
|
199
|
+
// emitted iff a Surfaced field is declared, so a Surfaced-free domain's fragment — and thus
|
|
200
|
+
// its openapi/package bytes — is byte-identical to before this existed. No params: the read
|
|
201
|
+
// is workspace-wide (the reserved id IS the selector).
|
|
202
|
+
const declaresSurfaced = module.aggregates.some((a) =>
|
|
203
|
+
Object.values((a.fields ?? {}) as Record<string, Field>).some(
|
|
204
|
+
(f) => (f.driver as { kind?: string } | undefined)?.kind === "Surfaced",
|
|
205
|
+
),
|
|
206
|
+
);
|
|
207
|
+
if (declaresSurfaced) {
|
|
208
|
+
getRead(
|
|
209
|
+
"nomosPendingConflicts",
|
|
210
|
+
"pending Surfaced merge conflicts across the workspace (maintained; derived, not drained)",
|
|
211
|
+
{ kind: "pendingConflicts" },
|
|
212
|
+
[],
|
|
213
|
+
);
|
|
214
|
+
}
|
|
195
215
|
|
|
196
216
|
return {
|
|
197
217
|
openapi: "3.1.0",
|
package/src/codegen_proof.ts
CHANGED
|
@@ -778,7 +778,11 @@ function probePlanOpsWithPayload(
|
|
|
778
778
|
return ops;
|
|
779
779
|
}
|
|
780
780
|
|
|
781
|
-
|
|
781
|
+
/** PUBLIC (dart_codegen_parity.md P2 / idof_lint.ts): the same plan-probe primitive the proof generator's
|
|
782
|
+
* own discovery legs use — run a directive's plan under a synthesized sample payload + deterministic probe
|
|
783
|
+
* ports and return the emitted field ops alongside the sample that produced them. Reused (not reinvented)
|
|
784
|
+
* by the payload-ref idOf lint to match an op's value back to the payload field that fed it. */
|
|
785
|
+
export function probePlanOps(
|
|
782
786
|
dir: Directive<unknown>,
|
|
783
787
|
where: string,
|
|
784
788
|
): { ops: FieldOp[]; synth: SynthesizedPayload; probePayload: Record<string, unknown> } | undefined {
|
package/src/codegen_ts.ts
CHANGED
|
@@ -470,7 +470,16 @@ function tsTypeOfPayload(
|
|
|
470
470
|
// ── DSL Field → read-model TS type (the projection's decode surface) ──────────────
|
|
471
471
|
|
|
472
472
|
function tsTypeOfField(field: Field): string {
|
|
473
|
-
|
|
473
|
+
let base = tsBaseTypeOfField(field);
|
|
474
|
+
// SURFACED MERGE CONFLICTS (architecture/surfaced_merge_conflict.md): the FIRST driver that
|
|
475
|
+
// changes a field's generated type. A `.merge(Surfaced(...))` field reads as the plain scalar
|
|
476
|
+
// once resolved, or the `{__surfaced:{pending:[...]}}` envelope while genuinely concurrent
|
|
477
|
+
// writes await a human pick — typed as the discriminated union `SurfacedValue<T>` (emitted
|
|
478
|
+
// once per file that needs it). The client does NO transformation; this types the exact
|
|
479
|
+
// read-collapse JSON.
|
|
480
|
+
if ((field.driver as { kind?: string } | undefined)?.kind === "Surfaced") {
|
|
481
|
+
base = `SurfacedValue<${base}>`;
|
|
482
|
+
}
|
|
474
483
|
// NULL SEMANTICS (null_semantics.md): a `nullable`/`explicit-empty` field ADMITS a domain `null`,
|
|
475
484
|
// so its read-model type carries the `| null` arm (distinct from the `?` partial-fold absence a
|
|
476
485
|
// non-null-admitting field gets). A `forbid` json/VO field NEVER gets the null arm.
|
|
@@ -792,6 +801,15 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
792
801
|
const hasBirths = modules.some((m) =>
|
|
793
802
|
(m.directives as Directive<unknown>[]).some((d) => (d as { birthsChild?: boolean })?.birthsChild === true),
|
|
794
803
|
);
|
|
804
|
+
// SURFACED MERGE CONFLICTS: a package declaring ≥1 `.merge(Surfaced(...))` field emits the
|
|
805
|
+
// `SurfacedValue<T>` union + guard (below). Surfaced-free packages emit nothing (byte-identical).
|
|
806
|
+
const hasSurfaced = modules.some((m) =>
|
|
807
|
+
m.aggregates.some((a) =>
|
|
808
|
+
Object.values(a.fields as Record<string, Field>).some(
|
|
809
|
+
(f) => (f.driver as { kind?: string } | undefined)?.kind === "Surfaced",
|
|
810
|
+
),
|
|
811
|
+
),
|
|
812
|
+
);
|
|
795
813
|
// ACTIVITY FEED (architecture/activity_descriptors.md): the package's declared `.activity(...)` descriptors.
|
|
796
814
|
// Empty ⇒ NOTHING activity-related emits (byte-identical to a pre-activity compile).
|
|
797
815
|
const activityEntries = collectActivityDescriptors(modules);
|
|
@@ -950,6 +968,45 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
950
968
|
` * at runtime and the recursive edge is typed here). Named recursion TS resolves fine. */`,
|
|
951
969
|
`export type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };`,
|
|
952
970
|
``,
|
|
971
|
+
// SURFACED MERGE CONFLICTS (architecture/surfaced_merge_conflict.md): the read-collapse union for a
|
|
972
|
+
// `.merge(Surfaced(...))` field — the plain scalar once resolved, or the pending envelope while
|
|
973
|
+
// genuinely concurrent writes await a human pick. Emitted ONLY when the package declares one.
|
|
974
|
+
...(hasSurfaced
|
|
975
|
+
? [
|
|
976
|
+
`/** One competing observation on a PENDING Surfaced field. \`author\` is the writer principal —`,
|
|
977
|
+
` * nullable (an observation folded where no intent context existed carries none). The author`,
|
|
978
|
+
` * JSON key is pinned in ONE place: this property name (kernel b215430d — exactly "author"). */`,
|
|
979
|
+
`export interface SurfacedObservation<T> {`,
|
|
980
|
+
` value: T;`,
|
|
981
|
+
` occurredAt: { physical: number; logical: number; replica: number };`,
|
|
982
|
+
` author?: string | null;`,
|
|
983
|
+
`}`,
|
|
984
|
+
`/** The PENDING read-collapse of a \`.merge(Surfaced(...))\` field: the kernel retained BOTH sides`,
|
|
985
|
+
` * of a genuinely concurrent write (ALWAYS 2+ observations — a single-observation set collapses`,
|
|
986
|
+
` * to the plain scalar; bounded, cap 8) instead of silently picking a winner. Resolve`,
|
|
987
|
+
` * it with an ORDINARY next write (a \`.mutates()\` directive that set()s the field) — the field`,
|
|
988
|
+
` * then reads as the plain scalar again, indistinguishable from any other field. */`,
|
|
989
|
+
`export interface SurfacedPending<T> { __surfaced: { pending: SurfacedObservation<T>[] } }`,
|
|
990
|
+
`/** A Surfaced field's read value: the resolved scalar, or the pending envelope under conflict. */`,
|
|
991
|
+
`export type SurfacedValue<T> = T | SurfacedPending<T>;`,
|
|
992
|
+
`/** True iff \`v\` is the pending-conflict envelope (narrows to [SurfacedPending]). */`,
|
|
993
|
+
`export function isSurfacedPending<T>(v: SurfacedValue<T> | null | undefined): v is SurfacedPending<T> {`,
|
|
994
|
+
` return typeof v === "object" && v !== null && "__surfaced" in (v as Record<string, unknown>);`,
|
|
995
|
+
`}`,
|
|
996
|
+
`/** ONE row of the workspace-wide \`pendingConflicts()\` read (reserved id \`nomosPendingConflicts\`):`,
|
|
997
|
+
` * a (aggregate, field) whose folded state awaits a human pick. MAINTAINED like counts/sums`,
|
|
998
|
+
` * (fold-time, O(pending) to read) and DERIVED, NOT DRAINED — a resolution write folding in removes`,
|
|
999
|
+
` * the row by derivation. The workspace-wide surface is HETEROGENEOUS, so each observation's`,
|
|
1000
|
+
` * \`value\` is \`unknown\` here — the per-field typed seam stays on the read models (\`SurfacedValue<T>\`). */`,
|
|
1001
|
+
`export interface PendingConflictRow {`,
|
|
1002
|
+
` aggregateType: string;`,
|
|
1003
|
+
` aggregateId: string;`,
|
|
1004
|
+
` field: string;`,
|
|
1005
|
+
` observations: SurfacedObservation<unknown>[];`,
|
|
1006
|
+
`}`,
|
|
1007
|
+
``,
|
|
1008
|
+
]
|
|
1009
|
+
: []),
|
|
953
1010
|
`/** The holon surface this client binds to (structurally satisfied by @githolon/client). */`,
|
|
954
1011
|
`export interface NomosHolon {`,
|
|
955
1012
|
` /** THE ONE ENCODER offer (the single v2-envelope codec lives in @githolon/client \`encodeIntentOfferBytes\`):`,
|
|
@@ -990,6 +1047,13 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
990
1047
|
` queryById(aggregateId: string): Promise<unknown>;`,
|
|
991
1048
|
]),
|
|
992
1049
|
` watchById(aggregateId: string, cb: (rows: unknown) => void, opts?: { intervalMs?: number }): () => void;`,
|
|
1050
|
+
...(hasSurfaced
|
|
1051
|
+
? [
|
|
1052
|
+
` /** The workspace-wide pending Surfaced-conflict read (reserved id \`nomosPendingConflicts\`).`,
|
|
1053
|
+
` * OPTIONAL member: structural binding tolerates a pre-Surfaced holon (the accessor throws at call). */`,
|
|
1054
|
+
` pendingConflicts?(): Promise<PendingConflictRow[]>;`,
|
|
1055
|
+
]
|
|
1056
|
+
: []),
|
|
993
1057
|
` /** Declared-count / declared-sum O(1) reads (maintained by the read engine). */`,
|
|
994
1058
|
` count(countId: string, groupKey?: string): Promise<number>;`,
|
|
995
1059
|
` /** THE SPATIAL READ (R*Tree probe): rows intersecting the bbox — same row shape as query().`,
|
|
@@ -1718,10 +1782,13 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1718
1782
|
for (const dir of Object.values(dm.directives)) for (const f of Object.values(dir)) s.add(f.scope);
|
|
1719
1783
|
return [...s].sort();
|
|
1720
1784
|
})();
|
|
1785
|
+
// directiveId → its emitted payload TYPE name (the surfaced `resolve…` helpers reference it).
|
|
1786
|
+
const dirPayloadNames = new Map<string, string>();
|
|
1721
1787
|
for (const d of dirs) {
|
|
1722
1788
|
let payloadName = `${cap(d.id)}Payload`;
|
|
1723
1789
|
if (usedPayloadNames.has(payloadName)) payloadName = `${cap(camel(domain))}${cap(d.id)}Payload`;
|
|
1724
1790
|
usedPayloadNames.add(payloadName);
|
|
1791
|
+
dirPayloadNames.set(d.id, payloadName);
|
|
1725
1792
|
const schema = (d as unknown as { payloadSchema: z.ZodTypeAny }).payloadSchema;
|
|
1726
1793
|
const stamps = autoStampFields(schema);
|
|
1727
1794
|
const mint = mintPlan?.get(d.id);
|
|
@@ -1875,6 +1942,51 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1875
1942
|
: ` ${lcFirst(camel(s.id))}: () => holon.sum(${JSON.stringify(s.id)}),`,
|
|
1876
1943
|
);
|
|
1877
1944
|
}
|
|
1945
|
+
|
|
1946
|
+
// ── SURFACED CONFLICT DX (surfaced_merge_conflict.md): the workspace-wide pendingConflicts
|
|
1947
|
+
// accessor + one typed `resolve…` helper per designated `.resolves()` directive. Emitted iff
|
|
1948
|
+
// this domain declares a Surfaced field (byte-identical otherwise — the era rule).
|
|
1949
|
+
const resolverBindings = (mod as { surfacedResolvers?: import("./surfaced_lint.js").SurfacedResolverBinding[] }).surfacedResolvers ?? [];
|
|
1950
|
+
if (resolverBindings.length > 0) {
|
|
1951
|
+
methods.push(
|
|
1952
|
+
` /** PENDING SURFACED CONFLICTS across the workspace — the maintained "decisions needed" read`,
|
|
1953
|
+
` * (reserved id \`nomosPendingConflicts\`; derived, not drained: a resolution write removes its`,
|
|
1954
|
+
` * row by derivation). Pair each row with its typed \`resolve…\` helper below. */`,
|
|
1955
|
+
` pendingConflicts: () => {`,
|
|
1956
|
+
` if (!holon.pendingConflicts) throw new Error("this holon predates pendingConflicts reads — upgrade @githolon/client");`,
|
|
1957
|
+
` return holon.pendingConflicts();`,
|
|
1958
|
+
` },`,
|
|
1959
|
+
` /** Watch the pending Surfaced conflicts (local reactive read; returns stop()). */`,
|
|
1960
|
+
` watchPendingConflicts: (cb: (rows: PendingConflictRow[]) => void, opts?: { intervalMs?: number }) => {`,
|
|
1961
|
+
` if (!holon.pendingConflicts) throw new Error("this holon predates pendingConflicts reads — upgrade @githolon/client");`,
|
|
1962
|
+
` let stopped = false;`,
|
|
1963
|
+
` let last = "";`,
|
|
1964
|
+
` const tick = async () => { try { const rows = await holon.pendingConflicts!(); const j = JSON.stringify(rows); if (!stopped && j !== last) { last = j; cb(rows); } } catch { /* projection catching up */ } };`,
|
|
1965
|
+
` void tick();`,
|
|
1966
|
+
` const t = setInterval(tick, opts?.intervalMs ?? 1000);`,
|
|
1967
|
+
` return () => { stopped = true; clearInterval(t); };`,
|
|
1968
|
+
` },`,
|
|
1969
|
+
);
|
|
1970
|
+
for (const b of resolverBindings) {
|
|
1971
|
+
if (b.valuePayloadField === undefined) continue; // probe couldn't bind — dispatch the directive directly.
|
|
1972
|
+
const payloadName = dirPayloadNames.get(b.directiveId);
|
|
1973
|
+
if (payloadName === undefined) continue;
|
|
1974
|
+
const helper = `resolve${cap(camel(b.aggregateId))}${cap(camel(b.field))}`;
|
|
1975
|
+
const idField = b.idPayloadField;
|
|
1976
|
+
const bound = [b.valuePayloadField, ...(idField !== undefined ? [idField] : [])];
|
|
1977
|
+
const restType = `Omit<${payloadName}, ${bound.map((f) => JSON.stringify(f)).join(" | ")}>`;
|
|
1978
|
+
const conflictParam = idField !== undefined ? `conflict: string | PendingConflictRow, ` : ``;
|
|
1979
|
+
const payloadExpr = `{ ...(rest ?? {}), ${idField !== undefined ? `${JSON.stringify(idField)}: typeof conflict === "string" ? conflict : conflict.aggregateId, ` : ``}${JSON.stringify(b.valuePayloadField)}: chosen } as ${payloadName}`;
|
|
1980
|
+
methods.push(
|
|
1981
|
+
` /** RESOLVE a pending conflict on \`${b.aggregateId}.${b.field}\` by dispatching the DESIGNATED`,
|
|
1982
|
+
` * resolver \`${b.directiveId}\` with the chosen value${idField !== undefined ? ` (the conflict row — or the bare aggregate id — names the instance)` : ""}.`,
|
|
1983
|
+
` * ESCAPE HATCH: any gated directive that sets the field still resolves the conflict in the`,
|
|
1984
|
+
` * kernel sense; \`.resolves()\` only designates which one THIS typed helper dispatches. */`,
|
|
1985
|
+
` ${helper}: (${conflictParam}chosen: ${payloadName}[${JSON.stringify(b.valuePayloadField)}], rest?: ${restType}): Promise<NomosOutcome> =>`,
|
|
1986
|
+
` _outcome(holon.workspace, holon.offerDirective(${JSON.stringify(domain)}, ${JSON.stringify(b.directiveId)}, ${payloadExpr}, { domainHash, actor, returnCreated: true }) as Promise<string | { head: string; created?: { type: string; id: string }[] }>),`,
|
|
1987
|
+
);
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1878
1990
|
for (const agg of mod.aggregates as AggregateHandle[]) {
|
|
1879
1991
|
const aScopes = readScopesFor(agg.id);
|
|
1880
1992
|
methods.push(
|