@githolon/dsl 0.70.2 → 0.72.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dart/CHANGELOG.md +13 -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 +173 -27
- package/dart/pubspec.yaml +1 -1
- package/dart/test/cluster_attested_test.dart +0 -1
- package/dart/test/session_bound_test.dart +255 -0
- package/dart_types/CHANGELOG.md +8 -0
- package/dart_types/lib/nomos_types.dart +6 -0
- package/dart_types/lib/src/principal.dart +56 -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 +274 -15
- package/src/codegen_openapi.ts +20 -0
- package/src/codegen_proof.ts +5 -1
- package/src/codegen_ts.ts +166 -1
- package/src/compile_package_main.ts +126 -9
- 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,10 +191,20 @@ 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
|
|
201
|
+
// cross-workspace governance offer to a PARENT. It gets a typed `bridge.createWorkspace(...)` wrapper
|
|
202
|
+
// (below), NOT an `offerOutcome`; excluded here so a frontend dev can never call it as a local write.
|
|
203
|
+
const allDirs = (mod.directives as AnyDirective[]).filter(
|
|
188
204
|
(d) => d && typeof d.id === "string" && typeof (d as { payloadSchema?: unknown }).payloadSchema === "object",
|
|
189
205
|
);
|
|
206
|
+
const birthDirs = allDirs.filter((d) => (d as { birthsChild?: boolean }).birthsChild === true);
|
|
207
|
+
const dirs = allDirs.filter((d) => (d as { birthsChild?: boolean }).birthsChild !== true);
|
|
190
208
|
for (const d of dirs) {
|
|
191
209
|
const payloadClass = `${cap(d.id)}Payload`;
|
|
192
210
|
const stamps = autoStampFields((d as unknown as { payloadSchema: z.ZodTypeAny }).payloadSchema);
|
|
@@ -227,7 +245,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
227
245
|
]
|
|
228
246
|
: []),
|
|
229
247
|
` await _sealPayload(data, ${dartStr(d.id)}, seal);`,
|
|
230
|
-
` return
|
|
248
|
+
` return _offer(${dartStr(d.id)}, data);`,
|
|
231
249
|
` }`,
|
|
232
250
|
``,
|
|
233
251
|
);
|
|
@@ -242,7 +260,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
242
260
|
` final data = payload.toPayloadJson();`,
|
|
243
261
|
` final now = DateTime.now().toUtc().toIso8601String();`,
|
|
244
262
|
...stamps.map((s) => ` data.putIfAbsent(${dartStr(s)}, () => now);`),
|
|
245
|
-
` return
|
|
263
|
+
` return _offer(${dartStr(d.id)}, data);`,
|
|
246
264
|
` }`,
|
|
247
265
|
``,
|
|
248
266
|
);
|
|
@@ -253,7 +271,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
253
271
|
gateDoc,
|
|
254
272
|
` Future<NomosOfferOutcome> ${method}(${payloadClass} payload) {`,
|
|
255
273
|
gateGuard,
|
|
256
|
-
` return
|
|
274
|
+
` return _offer(${dartStr(d.id)}, payload.toPayloadJson());`,
|
|
257
275
|
` }`,
|
|
258
276
|
``,
|
|
259
277
|
);
|
|
@@ -262,12 +280,85 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
262
280
|
` /// Author \`${domain}/${d.id}\` locally under the installed law; returns the typed [NomosOfferOutcome].${createsDoc}`,
|
|
263
281
|
...(gateDoc ? [gateDoc] : []),
|
|
264
282
|
` Future<NomosOfferOutcome> ${method}(${payloadClass} payload) =>`,
|
|
265
|
-
`
|
|
283
|
+
` _offer(${dartStr(d.id)}, payload.toPayloadJson());`,
|
|
266
284
|
``,
|
|
267
285
|
);
|
|
268
286
|
}
|
|
269
287
|
}
|
|
270
288
|
|
|
289
|
+
// ── `.births()` directives → typed CROSS-WORKSPACE birth (bridge.createWorkspace) ──
|
|
290
|
+
// Each births a CHILD WORKSPACE under `parent` via a signed governance offer routed by the PARENT'S
|
|
291
|
+
// law (POST /v2/workspaces/<parent>/<directiveId>) — never a same-workspace write. The declaring
|
|
292
|
+
// domain's key + directiveId + baked domainHash are all bound at codegen time, so a frontend dev
|
|
293
|
+
// never hand-writes the four raw strings this replaces (the co2 production bug).
|
|
294
|
+
for (const d of birthDirs) {
|
|
295
|
+
const payloadClass = `${cap(d.id)}Payload`;
|
|
296
|
+
const method = safeMethod(d.id);
|
|
297
|
+
const gate = (d as { requiresRelationGate?: { relation: string } }).requiresRelationGate;
|
|
298
|
+
methods.push(
|
|
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}({`,
|
|
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,`,
|
|
317
|
+
` String? authToken,`,
|
|
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(`,
|
|
327
|
+
` domain: ${dartStr(domain)},`,
|
|
328
|
+
` directiveId: ${dartStr(d.id)},`,
|
|
329
|
+
` domainHash: domainHash,`,
|
|
330
|
+
` parent: explicitParent,`,
|
|
331
|
+
` actor: actor.wire,`,
|
|
332
|
+
` authorSecret: authorSecret?.wire,`,
|
|
333
|
+
` payload: payload.toPayloadJson(),`,
|
|
334
|
+
` authToken: authToken,`,
|
|
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
|
+
` }`,
|
|
358
|
+
``,
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
271
362
|
// ── declared queries → one-shot + reactive ──
|
|
272
363
|
for (const q of (mod.queries ?? []) as QueryDecl[]) {
|
|
273
364
|
const model = readModelClassName(q.returns);
|
|
@@ -324,11 +415,64 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
324
415
|
);
|
|
325
416
|
}
|
|
326
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
|
+
|
|
327
468
|
// ── per-aggregate by-id read/watch + type-index list ──
|
|
328
469
|
for (const agg of mod.aggregates) {
|
|
329
470
|
const model = readModelClassName(agg.id);
|
|
330
|
-
|
|
331
|
-
|
|
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));
|
|
332
476
|
methods.push(
|
|
333
477
|
` /// Read one ${agg.id} by aggregate id.${readScopesFor(agg.id).length ? " Encrypted field(s) decrypt when the bound [keyStore] holds the scope key." : ""}`,
|
|
334
478
|
` Future<List<${model}>> ${safeMethod(`${lc}ById`)}(String id) async =>`,
|
|
@@ -345,6 +489,67 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
345
489
|
);
|
|
346
490
|
}
|
|
347
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
|
+
|
|
348
553
|
// ── activity feed → typed reactive + one-shot (declared `.activity(...)`) ──
|
|
349
554
|
if (opts.emitActivity === true) {
|
|
350
555
|
methods.push(
|
|
@@ -397,17 +602,20 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
397
602
|
return [
|
|
398
603
|
`// AUTO-GENERATED by nomos-compile — typed ASYNC Dart client for domain "${domain}".`,
|
|
399
604
|
`// Drives the REAL githolon over a NomosBridge (the WebView runner). Regenerated every compile; do`,
|
|
400
|
-
`// 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:`,
|
|
401
607
|
`//`,
|
|
402
608
|
// GENERATED-CODE lints: the bridge read/watch surface returns dynamic (JSON rows), so typed accessors
|
|
403
609
|
// do dynamic member access + casts by construction. Silence the lints that only make sense for
|
|
404
610
|
// hand-written code, so this file never adds noise to a tenant's analyzer output (co2 feedback).
|
|
405
|
-
|
|
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`,
|
|
406
614
|
`//`,
|
|
407
|
-
`// final
|
|
408
|
-
`// await
|
|
409
|
-
`// await
|
|
410
|
-
`// 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)`,
|
|
411
619
|
`// await app.${dirs[0] ? safeMethod(dirs[0].id) : "someDirective"}(/* typed payload */);`,
|
|
412
620
|
``,
|
|
413
621
|
// `dart:convert` is needed ONLY for the interface block's `jsonDecode` — the offer lane no longer encodes
|
|
@@ -427,7 +635,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
427
635
|
`// client file gives an app the whole typed surface.`,
|
|
428
636
|
...(opts.typesImportUri !== undefined ? [`export '${opts.typesImportUri}';`] : [`export '${mod.name}.dart';`]),
|
|
429
637
|
...(opts.typesImportUri === undefined && opts.frameworkImportUri !== undefined ? [`export '${opts.frameworkImportUri}';`] : []),
|
|
430
|
-
`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" : ""};`,
|
|
431
639
|
``,
|
|
432
640
|
`/// sha256 of the deployed \`.package.usda\` — the installed law this client offers under.`,
|
|
433
641
|
`const String ${hashConst} = ${dartStr(opts.domainHash)};`,
|
|
@@ -477,6 +685,29 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
477
685
|
: []),
|
|
478
686
|
` ${className}(this.bridge, {this.domainHash = ${hashConst}, this.actor${encPresent ? ", this.keyStore" : ""}});`,
|
|
479
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
|
+
``,
|
|
480
711
|
` /// The session workspace this client is bound to (from the [NomosSession] handle, when constructed with`,
|
|
481
712
|
` /// one) — the \`workspace\` of every read row's canonical [NomosRef]. '' on a bare (non-session) bridge.`,
|
|
482
713
|
` String get _ws {`,
|
|
@@ -484,6 +715,31 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
484
715
|
` return b is NomosSession ? b.workspace : '';`,
|
|
485
716
|
` }`,
|
|
486
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
|
+
``,
|
|
487
743
|
...(encPresent ? dartEncClassMembers(encMap!) : []),
|
|
488
744
|
// THE OWNER CEREMONY (slice 2): grantAndWrap / revokeAndRotate / rotate — the operator-blind key-epoch
|
|
489
745
|
// handshake as one call. Bind `actor` (the owner) to author the fan-out. Only for a domain with a scope.
|
|
@@ -536,6 +792,9 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
536
792
|
...methods,
|
|
537
793
|
`}`,
|
|
538
794
|
``,
|
|
795
|
+
...(refExtensions.length > 0
|
|
796
|
+
? [`// ---- ref-traversal accessors (dart_codegen_parity.md P2) ----`, refExtensions.join("\n\n"), ``]
|
|
797
|
+
: []),
|
|
539
798
|
...(roleHelpers !== ""
|
|
540
799
|
? [
|
|
541
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 {
|