@githolon/dsl 0.108.3 → 0.108.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dart/CHANGELOG.md +10 -0
  2. package/dart/lib/src/application_framework_calls.dart +21 -0
  3. package/dart/lib/src/dev_cloud.dart +67 -0
  4. package/dart/lib/src/nms_test_environment.dart +1 -1
  5. package/dart/lib/src/runner/node-entry.mjs +372 -106
  6. package/dart/lib/src/runner/offline-install.mjs +171 -11
  7. package/dart/lib/src/session_bound.dart +58 -30
  8. package/dart/lib/src/test_harness.dart +60 -98
  9. package/dart/lib/src/webview_bridge.dart +212 -32
  10. package/dart/pubspec.lock +1 -1
  11. package/dart/pubspec.yaml +2 -2
  12. package/dart/test/node_runner_transport_test.dart +5 -5
  13. package/dart/test/webview_bridge_test.dart +6 -0
  14. package/dart_types/CHANGELOG.md +4 -0
  15. package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pb.dart +428 -2
  16. package/dart_types/pubspec.yaml +1 -1
  17. package/package.json +1 -1
  18. package/runtime/generated-application-host.js +314 -82
  19. package/src/application_topology.ts +120 -28
  20. package/src/births.ts +27 -0
  21. package/src/codegen_call_usda.ts +9 -4
  22. package/src/codegen_dart_application.ts +146 -37
  23. package/src/codegen_dart_client.ts +225 -160
  24. package/src/codegen_dart_framework_calls.ts +89 -4
  25. package/src/codegen_ts.ts +135 -60
  26. package/src/compile_package_main.ts +3544 -1460
  27. package/src/directive.ts +320 -118
  28. package/src/fields.ts +2 -3
  29. package/src/framework/application_home.ts +3 -0
  30. package/src/framework/auth_provider_law.ts +20 -16
  31. package/src/framework/genesis.ts +32 -1
  32. package/src/framework/home.ts +45 -8
  33. package/src/framework/signing_identity.ts +78 -0
  34. package/src/framework/workspaces.ts +9 -1
  35. package/src/framework_call_dart_enums.ts +33 -0
  36. package/src/framework_call_definitions.ts +11 -2
  37. package/src/index.ts +12 -0
  38. package/src/intent.ts +525 -194
  39. package/src/law_graph.ts +32 -4
  40. package/src/law_graph_usda.ts +3 -0
  41. package/src/package_config.ts +16 -1
  42. package/src/read.ts +33 -11
  43. package/src/wire.ts +1 -0
  44. package/src/wire_encode.ts +3 -3
@@ -87,7 +87,10 @@ import {
87
87
  isNamespacedAggregate,
88
88
  qualifyReadProjectionType,
89
89
  } from "./read_projection_namespace.js";
90
- import { isBusinessIntent } from "./intent.js";
90
+ import {
91
+ childAuthenticationRecoveryAggregateId,
92
+ isBusinessIntent,
93
+ } from "./intent.js";
91
94
  import { publicClientInterface, type DomainInterface } from "./shape_compat.js";
92
95
  import { BUSINESS_CORRECTION_DIRECTIVE } from "./framework/business_corrections.js";
93
96
  import { deriveDirectiveRecordTarget } from "./workspace_routing.js";
@@ -114,6 +117,10 @@ export const dartInternalWorkspaceSharingReceiptMethod = (
114
117
  directiveId: string
115
118
  ): string => `nmsInternal${cap(camel(directiveId))}`;
116
119
 
120
+ export const dartInternalChildAuthenticationRecoveryMethod = (
121
+ aggregateId: string,
122
+ ): string => `nmsInternalRepair${cap(camel(aggregateId))}Authentication`;
123
+
117
124
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
118
125
  type AnyDirective = Directive<any>;
119
126
 
@@ -522,11 +529,12 @@ function dartUsdaFieldValue(
522
529
  ): string {
523
530
  const selected = `${row}.field(${JSON.stringify(field.lawPath)})`;
524
531
  const required = `${row}.requiredField(${JSON.stringify(field.lawPath)})`;
525
- const foreignWorkspaceExpression = field.foreignWorkspacePath === undefined
526
- ? undefined
527
- : `(${row}.field(${JSON.stringify(
528
- field.foreignWorkspacePath
529
- )})?.stringValue() ?? '')`;
532
+ const foreignWorkspaceExpression =
533
+ field.foreignWorkspacePath === undefined
534
+ ? undefined
535
+ : `(${row}.field(${JSON.stringify(
536
+ field.foreignWorkspacePath
537
+ )})?.stringValue() ?? '')`;
530
538
  const present = dartUsdaPresentValue(
531
539
  field.spec,
532
540
  field.spec.optional ? `${selected}!` : required,
@@ -635,14 +643,15 @@ function emitDartUsdaResultDecoder(
635
643
  const computedFields = decoder.computedFields.map((field) => {
636
644
  const selected = `row.field(${JSON.stringify(field.path)})`;
637
645
  const required = `row.requiredField(${JSON.stringify(field.path)})`;
638
- const decode = (value: string) => field.enumType === undefined
639
- ? dartUsdaProjectionValue(field.schema, value)
640
- : `${field.enumType}.fromWire(${value}.jsonValue() as String)`;
646
+ const decode = (value: string) =>
647
+ field.enumType === undefined
648
+ ? dartUsdaProjectionValue(field.schema, value)
649
+ : `${field.enumType}.fromWire(${value}.jsonValue() as String)`;
641
650
  return field.required
642
651
  ? ` ${dartMemberName(field.name)}: ${decode(required)},`
643
652
  : ` ${dartMemberName(
644
653
  field.name
645
- )}: ${selected} == null ? null : ${decode(`${selected}!`)},`;
654
+ )}: ${selected} == null || ${selected}!.typeName == 'NomosNullValue' ? null : ${decode(`${selected}!`)},`;
646
655
  });
647
656
  const provenanceFields = decoder.attributedFields.map((name) => {
648
657
  const field = decoder.baseFields.find(
@@ -893,9 +902,10 @@ export function generateDartBridgeClient(
893
902
  `Dart result USDA codegen: '${domain}.${aggregate.id}' has no canonical USDA path`
894
903
  );
895
904
  }
896
- const aggregateEnumDartTypes = aggregate.framework === true
897
- ? collectDartEnums([aggregate], [], "nomos_framework").dartTypes
898
- : enumDartTypes;
905
+ const aggregateEnumDartTypes =
906
+ aggregate.framework === true
907
+ ? collectDartEnums([aggregate], [], "nomos_framework").dartTypes
908
+ : enumDartTypes;
899
909
  const specs = aggregateFieldSpecs(
900
910
  aggregate,
901
911
  aggregateEnumDartTypes,
@@ -967,35 +977,40 @@ export function generateDartBridgeClient(
967
977
  schema: read.returns,
968
978
  required: false,
969
979
  })),
970
- ].map((field) => {
971
- const readNode = callUsda.graph.nodes.find(
972
- (node) => node.kind === "read" && node.entity.label === field.name
973
- );
974
- if (readNode === undefined) {
975
- throw new Error(
976
- `Dart result USDA codegen: composed law has no '${domain}.${field.name}' read field`
980
+ ]
981
+ .map((field) => {
982
+ const readNode = callUsda.graph.nodes.find(
983
+ (node) => node.kind === "read" && node.entity.label === field.name
977
984
  );
978
- }
979
- const path = callUsda.paths.get(readNode.entity.id);
980
- if (path === undefined) {
981
- throw new Error(
982
- `Dart result USDA codegen: '${domain}.${field.name}' has no canonical USDA path`
983
- );
984
- }
985
- return { ...field, path };
986
- }).map((field) => {
987
- const inner = (() => {
988
- let schema = field.schema;
989
- while (["optional", "default", "nullable"].includes(zodKind(schema))) {
990
- schema = zodDef(schema).innerType!;
985
+ if (readNode === undefined) {
986
+ throw new Error(
987
+ `Dart result USDA codegen: composed law has no '${domain}.${field.name}' read field`
988
+ );
991
989
  }
992
- return schema;
993
- })();
994
- const enumType = field.required && zodKind(inner) === "enum"
995
- ? enumDartTypes.get(dartEnumLookupKey(inner))
996
- : undefined;
997
- return enumType === undefined ? field : { ...field, enumType };
998
- });
990
+ const path = callUsda.paths.get(readNode.entity.id);
991
+ if (path === undefined) {
992
+ throw new Error(
993
+ `Dart result USDA codegen: '${domain}.${field.name}' has no canonical USDA path`
994
+ );
995
+ }
996
+ return { ...field, path };
997
+ })
998
+ .map((field) => {
999
+ const inner = (() => {
1000
+ let schema = field.schema;
1001
+ while (
1002
+ ["optional", "default", "nullable"].includes(zodKind(schema))
1003
+ ) {
1004
+ schema = zodDef(schema).innerType!;
1005
+ }
1006
+ return schema;
1007
+ })();
1008
+ const enumType =
1009
+ field.required && zodKind(inner) === "enum"
1010
+ ? enumDartTypes.get(dartEnumLookupKey(inner))
1011
+ : undefined;
1012
+ return enumType === undefined ? field : { ...field, enumType };
1013
+ });
999
1014
  const decoder = {
1000
1015
  model: readModelClassName(aggregate.id),
1001
1016
  refType: concreteClassName(aggregate.id),
@@ -1055,10 +1070,7 @@ export function generateDartBridgeClient(
1055
1070
  scopes: readonly { scope: string; owned?: true }[]
1056
1071
  ): string =>
1057
1072
  `[${scopes
1058
- .map(
1059
- (s) =>
1060
- `(scope: ${dartStr(s.scope)}, owned: ${s.owned === true})`
1061
- )
1073
+ .map((s) => `(scope: ${dartStr(s.scope)}, owned: ${s.owned === true})`)
1062
1074
  .join(", ")}]`;
1063
1075
  const needsReadPrincipal = (typeId: string): boolean => {
1064
1076
  const audience = aggById.get(typeId)?.audience;
@@ -1184,10 +1196,16 @@ export function generateDartBridgeClient(
1184
1196
  const dirs = allDirs.filter(
1185
1197
  (d) => (d as { birthsChild?: boolean }).birthsChild !== true
1186
1198
  );
1199
+ // Only a no-argument enrolled-device intent can be the client's automatic stale-token
1200
+ // recovery hook. Enrolled-device intents with typed inputs are explicit business operations
1201
+ // (for example all-device-loss recovery bound to a named principal) and remain ordinary
1202
+ // generated APIs; their proof posture does not make them automatic lifecycle hooks.
1187
1203
  const authenticationRecoveryDirs = dirs.filter(
1188
1204
  (directive) =>
1189
1205
  isBusinessIntent(directive) &&
1190
- directive.offerAuthenticationProof === "enrolledDevice"
1206
+ directive.offerAuthenticationProof === "enrolledDevice" &&
1207
+ introspectPayload(directive.payloadSchema as z.ZodTypeAny, enumDartTypes)
1208
+ .length === 0
1191
1209
  );
1192
1210
  if (authenticationRecoveryDirs.length > 1) {
1193
1211
  throw new Error(
@@ -1196,16 +1214,68 @@ export function generateDartBridgeClient(
1196
1214
  );
1197
1215
  }
1198
1216
  const authenticationRecovery = authenticationRecoveryDirs[0];
1199
- if (
1200
- authenticationRecovery !== undefined &&
1201
- introspectPayload(
1202
- authenticationRecovery.payloadSchema as z.ZodTypeAny,
1203
- enumDartTypes
1204
- ).length !== 0
1205
- ) {
1206
- throw new Error(
1207
- `Dart client codegen: '${domain}/${authenticationRecovery.id}' is the automatic enrolled-device ` +
1208
- "authentication recovery intent and therefore must accept an empty payload"
1217
+ const childAuthenticationRecoveries = dirs.flatMap((directive) => {
1218
+ if (!isBusinessIntent(directive)) return [];
1219
+ const aggregateId = childAuthenticationRecoveryAggregateId(directive);
1220
+ if (aggregateId === undefined) return [];
1221
+ const fields = introspectPayload(
1222
+ directive.payloadSchema as z.ZodTypeAny,
1223
+ enumDartTypes,
1224
+ );
1225
+ if (
1226
+ fields.length !== 2 ||
1227
+ fields.some(
1228
+ (field) =>
1229
+ !["workspace", "user"].includes(field.name) ||
1230
+ field.optional ||
1231
+ field.dartType !== "String",
1232
+ )
1233
+ ) {
1234
+ throw new Error(
1235
+ `Dart child-authentication recovery '${domain}/${directive.id}' must accept exactly required string fields { workspace, user }`,
1236
+ );
1237
+ }
1238
+ return [{ aggregateId, directive }];
1239
+ });
1240
+ for (const recovery of childAuthenticationRecoveries) {
1241
+ if (
1242
+ childAuthenticationRecoveries.some(
1243
+ (candidate) =>
1244
+ candidate !== recovery &&
1245
+ candidate.aggregateId === recovery.aggregateId,
1246
+ )
1247
+ ) {
1248
+ throw new Error(
1249
+ `Dart client codegen: '${domain}' declares more than one authentication repair action for '${recovery.aggregateId}'`,
1250
+ );
1251
+ }
1252
+ const method = safeMethod(recovery.directive.id);
1253
+ const input = dartActionInputClassName(recovery.directive);
1254
+ methods.push(
1255
+ ` /// Generated child-open recovery: inspect the child's kernel posture, then ask this`,
1256
+ ` /// authenticated parent law to send its committed signer/provider state to that exact child.`,
1257
+ ` Future<void> ${dartInternalChildAuthenticationRecoveryMethod(
1258
+ recovery.aggregateId,
1259
+ )}(String workspace) async {`,
1260
+ ` final expectedPrincipal = actor ?? bridge.principal ?? '';`,
1261
+ ` if (expectedPrincipal.isEmpty) {`,
1262
+ ` throw StateError('Child authentication recovery requires the signed-in principal.');`,
1263
+ ` }`,
1264
+ ` final child = await bridge.acquireSession(workspace);`,
1265
+ ` final posture = await child.nmsAuthenticationPosture(principal: expectedPrincipal);`,
1266
+ ` if (posture.isReadyFor(expectedPrincipal)) return;`,
1267
+ ` await ${method}(${input}(`,
1268
+ ` workspace: workspace, user: expectedPrincipal,`,
1269
+ ` ));`,
1270
+ ` final parentSync = await bridge.sync(admit: true);`,
1271
+ ` if (parentSync.offline || parentSync.pushFailed) {`,
1272
+ ` throw StateError('Parent authentication recovery could not reach custody: ' +`,
1273
+ ` (parentSync.hasPushError() ? parentSync.pushError.message : 'workspace is offline'));`,
1274
+ ` }`,
1275
+ ` await child.nmsAwaitProviderTokenReady(principal: expectedPrincipal);`,
1276
+ ` await child.nmsAwaitAuthenticationReady(principal: expectedPrincipal);`,
1277
+ ` }`,
1278
+ ``,
1209
1279
  );
1210
1280
  }
1211
1281
  const localOfferDirs = [...dirs, ...birthDirs];
@@ -1261,7 +1331,12 @@ export function generateDartBridgeClient(
1261
1331
  );
1262
1332
  const payloadClass = compilerInternal
1263
1333
  ? `({${specs
1264
- .map((spec) => `${spec.dartType}${spec.optional ? "?" : ""} ${dartMemberName(spec.name)}`)
1334
+ .map(
1335
+ (spec) =>
1336
+ `${spec.dartType}${spec.optional ? "?" : ""} ${dartMemberName(
1337
+ spec.name
1338
+ )}`
1339
+ )
1265
1340
  .join(", ")}})`
1266
1341
  : dartActionInputClassName(directive);
1267
1342
  if (compilerInternal) internalRecordTypes.set(directive.id, payloadClass);
@@ -1305,7 +1380,16 @@ export function generateDartBridgeClient(
1305
1380
  for (const input of dartBusinessRecordInputs(directive, mod.aggregates)) {
1306
1381
  const origin = `NmsInternalRecordOrigins.require(payload.${input.argument})`;
1307
1382
  access.set(input.idField, { value: `${origin}.id` });
1308
- access.set(input.workspaceField, { value: `${origin}.workspace` });
1383
+ access.set(input.workspaceField, {
1384
+ value: `${origin}.workspace`,
1385
+ // A business record always has a canonical origin. When the authored
1386
+ // wire field is optional, the generated application input replaces it
1387
+ // with that required typed record, so the derived workspace is present
1388
+ // by construction rather than nullable caller data.
1389
+ ...(specsByName.get(input.workspaceField)?.optional === true
1390
+ ? { present: "true" }
1391
+ : {}),
1392
+ });
1309
1393
  }
1310
1394
  for (const input of dartForeignReferenceInputs(directive, mod.aggregates)) {
1311
1395
  const member = `payload.${dartMemberName(input.idField)}`;
@@ -1322,8 +1406,8 @@ export function generateDartBridgeClient(
1322
1406
  ...(input.optional
1323
1407
  ? present
1324
1408
  : specsByName.get(input.workspaceField)?.optional === true
1325
- ? { present: "true" }
1326
- : {}),
1409
+ ? { present: "true" }
1410
+ : {}),
1327
1411
  });
1328
1412
  }
1329
1413
  const stamps = autoStampFields(
@@ -2491,7 +2575,9 @@ export function generateDartBridgeClient(
2491
2575
  (spec) =>
2492
2576
  spec.name !== b.valuePayloadField &&
2493
2577
  spec.name !== b.idPayloadField &&
2494
- !autoStampFields(dir.payloadSchema as z.ZodTypeAny).includes(spec.name)
2578
+ !autoStampFields(dir.payloadSchema as z.ZodTypeAny).includes(
2579
+ spec.name
2580
+ )
2495
2581
  );
2496
2582
  const needsTypedDecision = otherPublicFacts.some(
2497
2583
  (spec) => !spec.optional
@@ -2505,8 +2591,7 @@ export function generateDartBridgeClient(
2505
2591
  `final class ${decisionClass} {`,
2506
2592
  ` final ${mergedSpec.dartType} merged;`,
2507
2593
  ...otherPublicFacts.map(
2508
- (spec) =>
2509
- ` final ${spec.dartType} ${dartMemberName(spec.name)};`
2594
+ (spec) => ` final ${spec.dartType} ${dartMemberName(spec.name)};`
2510
2595
  ),
2511
2596
  ` const ${decisionClass}({required this.merged, ${otherPublicFacts
2512
2597
  .map((spec) => `required this.${dartMemberName(spec.name)}`)
@@ -2532,7 +2617,9 @@ export function generateDartBridgeClient(
2532
2617
  "payload",
2533
2618
  ...encoder.arguments,
2534
2619
  ].join(", ")})`;
2535
- const mergedExpression = needsTypedDecision ? "decision.merged" : "merged";
2620
+ const mergedExpression = needsTypedDecision
2621
+ ? "decision.merged"
2622
+ : "merged";
2536
2623
  const payloadArguments = [
2537
2624
  `${dartMemberName(b.valuePayloadField)}: ${mergedExpression}`,
2538
2625
  ...(needsTypedDecision
@@ -2922,15 +3009,11 @@ export function generateDartBridgeClient(
2922
3009
  : aOwned
2923
3010
  ? `, await _client._readKeys(${aScopesExpr}, [(type: ${dartStr(
2924
3011
  runtimeType
2925
- )}, id: id)])${
2926
- aPrivate ? ", _client.actor" : ""
2927
- }`
3012
+ )}, id: id)])${aPrivate ? ", _client.actor" : ""}`
2928
3013
  : pastKeysArg(agg.id);
2929
3014
  const nominalByIdWatch = aScopes.length
2930
3015
  ? `Stream.fromFuture(_readKeys(${aScopesExpr}${
2931
- aOwned
2932
- ? `, [(type: ${dartStr(runtimeType)}, id: id)]`
2933
- : ""
3016
+ aOwned ? `, [(type: ${dartStr(runtimeType)}, id: id)]` : ""
2934
3017
  })).asyncExpand((keys) => bridge.nmsWatchByIdUsda(${dartStr(
2935
3018
  runtimeType
2936
3019
  )}, id, keys: keys${
@@ -2996,7 +3079,9 @@ export function generateDartBridgeClient(
2996
3079
  aOwned
2997
3080
  ? ` _client._readWithOwnedKeys(${aScopesExpr}, (keys) async => _nmsDecode${model}Rows(await _client.bridge.nmsQueryByTypeAtUsda(moment, ${dartStr(
2998
3081
  runtimeType
2999
- )}, keys${aPrivate ? ", _client.actor" : ""}), _client._ws), (row) => row.ref);`
3082
+ )}, keys${
3083
+ aPrivate ? ", _client.actor" : ""
3084
+ }), _client._ws), (row) => row.ref);`
3000
3085
  : ` _nmsDecode${model}Rows(await _client.bridge.nmsQueryByTypeAtUsda(moment, ${dartStr(
3001
3086
  runtimeType
3002
3087
  )}${pastKeysArg(agg.id)}), _client._ws);`,
@@ -3033,13 +3118,35 @@ export function generateDartBridgeClient(
3033
3118
  ` );`,
3034
3119
  ` }`,
3035
3120
  ``,
3036
- ` Future<NomosModelAdoption> _adoptGeneratedModel([String? because]) async {`,
3121
+ ...(authenticationRecovery === undefined
3122
+ ? [` Future<NomosModelAdoption> _adoptGeneratedModel([String? because]) async {`]
3123
+ : [
3124
+ ` Future<NomosModelAdoption> _adoptGeneratedModel([String? because]) =>`,
3125
+ ` _adoptGeneratedModelAttempt(because, false);`,
3126
+ ` Future<NomosModelAdoption> _adoptGeneratedModelAttempt(`,
3127
+ ` String? because, bool recoveredProviderEpoch) async {`,
3128
+ ]),
3037
3129
  ` final result = await bridge.nomosFrameworkAdoptGeneratedModel(`,
3038
3130
  ` domain: ${dartStr(
3039
3131
  domain
3040
3132
  )}, candidateHash: domainHash, because: because);`,
3041
3133
  ` final review = _nomosModelReview(result);`,
3042
3134
  ` final stage = result.stage;`,
3135
+ ...(authenticationRecovery === undefined
3136
+ ? []
3137
+ : [
3138
+ ` if (!result.accepted && !recoveredProviderEpoch) {`,
3139
+ ` final posture = await bridge.nmsAuthenticationPosture(principal: actor);`,
3140
+ ` if (posture.providerToken == NomosProviderTokenVerification.noMatchingKey) {`,
3141
+ ` print('[nomos-model-recovery] local ${domain} posture=' + posture.providerToken.name);`,
3142
+ ` await ${safeMethod(
3143
+ authenticationRecovery.id
3144
+ )}(const ${dartActionInputClassName(authenticationRecovery)}());`,
3145
+ ` await bridge.nmsAwaitAuthenticationReady(principal: actor);`,
3146
+ ` return _adoptGeneratedModelAttempt(because, true);`,
3147
+ ` }`,
3148
+ ` }`,
3149
+ ]),
3043
3150
  ` if ((stage == ModelLifecycleStage.MODEL_LIFECYCLE_STAGE_PROMOTED ||`,
3044
3151
  ` stage == ModelLifecycleStage.MODEL_LIFECYCLE_STAGE_ALREADY_CURRENT) && result.accepted) {`,
3045
3152
  ` return NomosModelAdoption.adopted(`,
@@ -3083,12 +3190,30 @@ export function generateDartBridgeClient(
3083
3190
  ` final archive = await rootBundle.load(${dartStr(
3084
3191
  opts.lawAssetKey
3085
3192
  )});`,
3086
- ` await bridge.nomosInternalStageGeneratedModel(`,
3193
+ ` Future<void> attempt() => bridge.nomosInternalStageGeneratedModel(`,
3087
3194
  ` domain: ${dartStr(domain)},`,
3088
3195
  ` candidateHash: domainHash,`,
3089
3196
  ` lawUsdz: archive.buffer.asUint8List(`,
3090
3197
  ` archive.offsetInBytes, archive.lengthInBytes),`,
3091
3198
  ` );`,
3199
+ ...(authenticationRecovery === undefined
3200
+ ? [` await attempt();`]
3201
+ : [
3202
+ ` try {`,
3203
+ ` await attempt();`,
3204
+ ` } on NomosModelStageFailure {`,
3205
+ ` final posture = await bridge.nmsAuthenticationPosture(principal: actor);`,
3206
+ ` print('[nomos-model-recovery] local ${domain} posture=' + posture.providerToken.name);`,
3207
+ ` if (posture.providerToken != NomosProviderTokenVerification.noMatchingKey) rethrow;`,
3208
+ ` await ${safeMethod(
3209
+ authenticationRecovery.id
3210
+ )}(const ${dartActionInputClassName(
3211
+ authenticationRecovery
3212
+ )}());`,
3213
+ ` await bridge.nmsAwaitAuthenticationReady(principal: actor);`,
3214
+ ` await attempt();`,
3215
+ ` }`,
3216
+ ]),
3092
3217
  ` }`,
3093
3218
  ``,
3094
3219
  ]),
@@ -3788,77 +3913,27 @@ export function generateDartBridgeClient(
3788
3913
  ` /// IMMEDIATELY — at provider construction, not two clicks into the app. The synchronous`,
3789
3914
  ` /// constructor remains for codepaths that can't await; it lazily runs the SAME preflight on the`,
3790
3915
  ` /// first dispatch.`,
3916
+ ` static Future<void> _catchUpBeforeModelPreparation(NomosSession session) async {`,
3917
+ ` try {`,
3918
+ ` await session.sync(admit: true);`,
3919
+ ` } catch (error) {`,
3920
+ ` // Offline-first: inability to reach shared custody does not revoke the local workspace.`,
3921
+ ` // The subsequent typed compatibility/model gate remains authoritative and will either`,
3922
+ ` // accept the local law or surface the precise governed action still required.`,
3923
+ ` print('[nomos-model-catch-up] ${domain} continuing from local custody: ' + error.toString());`,
3924
+ ` }`,
3925
+ ` }`,
3926
+ ``,
3791
3927
  ` static Future<${className}> bind(NomosSession session, {String? actor${
3792
3928
  manualEncryptionPresent ? ", NomosKeyStore? keyStore" : ""
3793
3929
  }}) async {`,
3794
3930
  ` final candidate = ${className}(session, actor: actor${
3795
3931
  manualEncryptionPresent ? ", keyStore: keyStore" : ""
3796
3932
  });`,
3797
- ` Never throwForAdoption(NomosModelAdoption adoption) {`,
3798
- ` throw DomainClientModelAdoptionRequired(`,
3799
- ` expectedDomainKey: interface.domain,`,
3800
- ` expectedDomainHash: interface.expectedDomainHash,`,
3801
- ` status: adoption.status.name,`,
3802
- ` summary: adoption.summary,`,
3803
- ` review: DomainModelReview(`,
3804
- ` checkedActions: adoption.review.checkedActions,`,
3805
- ` candidateActions: adoption.review.candidateActions,`,
3806
- ` sameOutcome: adoption.review.sameOutcome,`,
3807
- ` differences: [`,
3808
- ` for (final difference in adoption.review.differences)`,
3809
- ` DomainModelDifference(`,
3810
- ` kind: switch (difference.kind) {`,
3811
- ` NomosModelDifferenceKind.rules => DomainModelDifferenceKind.rules,`,
3812
- ` NomosModelDifferenceKind.outcome => DomainModelDifferenceKind.outcome,`,
3813
- ` NomosModelDifferenceKind.evidence => DomainModelDifferenceKind.evidence,`,
3814
- ` },`,
3815
- ` area: difference.area,`,
3816
- ` action: difference.action,`,
3817
- ` summary: difference.summary,`,
3818
- ` ),`,
3819
- ` ],`,
3820
- ` ),`,
3821
- ...(opts.lawAssetKey === undefined
3822
- ? [` stage: null,`]
3823
- : [
3824
- ` stage: adoption.status == NomosModelAdoptionStatus.needsStaging`,
3825
- ` ? () => candidate._stageGeneratedModel()`,
3826
- ` : null,`,
3827
- ]),
3828
- ` accept: adoption.status == NomosModelAdoptionStatus.needsDecision`,
3829
- ` ? (reason) async {`,
3830
- ` final accepted = await adoption.accept(reason);`,
3831
- ` // A retry that still isn't adopted (a transient business-outcome/history race,`,
3832
- ` // e.g. concurrent writes landing between the bare preflight and this accept) must`,
3833
- ` // NOT be swallowed as a terminal StateError: re-throw the SAME typed exception so`,
3834
- ` // the caller's governed-steps retry loop gets another decide()/accept() attempt`,
3835
- ` // instead of failing outright on the first race it hits.`,
3836
- ` if (accepted.status != NomosModelAdoptionStatus.adopted) {`,
3837
- ` throwForAdoption(accepted);`,
3838
- ` }`,
3839
- ` }`,
3840
- ` : null,`,
3841
- ` );`,
3842
- ` }`,
3843
- ` Future<void> adoptGeneratedModel() async {`,
3844
- ` final adoption = await candidate._adoptGeneratedModel();`,
3845
- ` if (adoption.status == NomosModelAdoptionStatus.adopted) return;`,
3846
- ` throwForAdoption(adoption);`,
3847
- ` }`,
3848
- ` try {`,
3849
- ` await session.assertCompatible(interface);`,
3850
- ` } on DomainClientSessionMismatch catch (mismatch) {`,
3851
- ` // A deployed successor is deliberately invisible until the generated candidate proves it`,
3852
- ` // against this workspace's exact history. Do that before rejecting the candidate for`,
3853
- ` // expecting its own newer interface; all other bind failures retain their precise cause.`,
3854
- ` if (!mismatch.canAttemptGeneratedModelAdoption) rethrow;`,
3855
- ` await adoptGeneratedModel();`,
3856
- ` }`,
3857
- ` // Shape compatibility governs readable structure; exact package identity governs`,
3858
- ` // which plans may author. A plan-only successor must therefore enter the same`,
3859
- ` // proof-gated lifecycle even when every generated read type remains compatible.`,
3860
- ` final law = await session.lawStatus(interface.expectedDomainHash);`,
3861
- ` if (law['drifted'] == true) await adoptGeneratedModel();`,
3933
+ ` // Binding proves that this client can read and author against the law the`,
3934
+ ` // workspace already runs. It never deploys, stages, promotes, or rewrites`,
3935
+ ` // workspace law. Model rollout is an explicit operator action through`,
3936
+ ` // [model], outside application startup.`,
3862
3937
  ` await session.assertCompatible(interface);`,
3863
3938
  ` // RELEASE COHERENCE: resolve this law's OSGi Contract Namespace requirements before use.`,
3864
3939
  ` await session.assertKernelCoherence(kernelContracts, lawHash: ${hashConst});`,
@@ -3882,38 +3957,26 @@ export function generateDartBridgeClient(
3882
3957
  ` /// privately retains refreshable cloud access for a genuinely-absent authorized workspace.`,
3883
3958
  ` static Future<${className}> open<TAggregate>(NomosBridge bridge, {NomosRef<TAggregate>? ref, String? workspace, String? actor${
3884
3959
  manualEncryptionPresent ? ", NomosKeyStore? keyStore" : ""
3885
- }}) async {`,
3960
+ }, Future<void> Function()? recoverAuthentication}) async {`,
3886
3961
  ` final ws = ref == null ? workspace : NmsInternalNomosRefs.workspace(ref);`,
3887
3962
  ` if (ws == null || ws.isEmpty) {`,
3888
3963
  ` throw ArgumentError('${className}.open: pass a workspace: or ref: — the workspace to open and bind');`,
3889
3964
  ` }`,
3890
3965
  ` final session = await bridge.acquireSession(ws);`,
3891
- ...(opts.lawAssetKey === undefined
3966
+ ...(authenticationRecovery === undefined
3892
3967
  ? []
3893
3968
  : [
3894
- ` final law = await session.lawStatus(${hashConst});`,
3895
- ` if (law['drifted'] == true) {`,
3896
- ` if (bridge is! NomosSession) {`,
3897
- ` throw StateError('Generated child model inheritance needs a bound parent session.');`,
3969
+ ` if (recoverAuthentication != null) {`,
3970
+ ` final posture = await session.nmsAuthenticationPosture(principal: actor);`,
3971
+ ` if (!posture.isReadyFor(actor ?? '')) {`,
3972
+ ` await recoverAuthentication();`,
3973
+ ` await session.nmsAwaitAuthenticationReady(principal: actor);`,
3898
3974
  ` }`,
3899
- ` final archive = await rootBundle.load(${dartStr(
3900
- opts.lawAssetKey
3901
- )});`,
3902
- ` await bridge.nomosInternalEnsureGeneratedModelInChild(`,
3903
- ` childWorkspace: ws,`,
3904
- ` domain: interface.domain,`,
3905
- ` candidateHash: ${hashConst},`,
3906
- ` lawUsdz: archive.buffer.asUint8List(`,
3907
- ` archive.offsetInBytes, archive.lengthInBytes),`,
3908
- ` );`,
3909
3975
  ` }`,
3910
3976
  ]),
3911
- ` return nmsInternalConvergeGeneratedModel<${className}>(`,
3912
- ` attempt: () => bind(session, actor: actor${
3977
+ ` return bind(session, actor: actor${
3913
3978
  manualEncryptionPresent ? ", keyStore: keyStore" : ""
3914
- }),`,
3915
- ` handle: session.nomosInternalHandleModelChange,`,
3916
- ` );`,
3979
+ });`,
3917
3980
  ` }`,
3918
3981
  ``,
3919
3982
  ` /// The session workspace this client is bound to (from the [NomosSession] handle, when constructed with`,
@@ -4010,7 +4073,7 @@ export function generateDartBridgeClient(
4010
4073
  ` Future<NomosOfferOutcome> _offer(NmsGeneratedCall call, {List<NomosConflictSnapshot> conflictSnapshots = const [], NomosOfferAuthenticationProof authenticationProof = NomosOfferAuthenticationProof.sessionCredentials}) async {`,
4011
4074
  ` await _preflightOnce();`,
4012
4075
  ` await _prepareCommand(call.directive);`,
4013
- ` Future<NomosOfferOutcome> attempt() => bridge.offerCallUsdaOutcome(call: call, actor: actor, domainHash: domainHash, conflictSnapshots: conflictSnapshots, authenticationProof: authenticationProof);`,
4076
+ ` Future<NomosOfferOutcome> attempt() => bridge.offerCallUsdaOutcome(call: call, actor: actor, conflictSnapshots: conflictSnapshots, authenticationProof: authenticationProof);`,
4014
4077
  ...(authenticationRecovery === undefined
4015
4078
  ? [` return attempt();`]
4016
4079
  : [
@@ -4020,7 +4083,9 @@ export function generateDartBridgeClient(
4020
4083
  ` } on NomosKernelCallFailure {`,
4021
4084
  ` final posture = await bridge.nmsAuthenticationPosture(principal: actor);`,
4022
4085
  ` if (posture.providerToken != NomosProviderTokenVerification.noMatchingKey) rethrow;`,
4023
- ` await ${safeMethod(authenticationRecovery.id)}(const ${dartActionInputClassName(
4086
+ ` await ${safeMethod(
4087
+ authenticationRecovery.id
4088
+ )}(const ${dartActionInputClassName(
4024
4089
  authenticationRecovery
4025
4090
  )}());`,
4026
4091
  ` await bridge.nmsAwaitAuthenticationReady(principal: actor);`,