@githolon/dsl 0.105.16 → 0.105.18

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 CHANGED
@@ -1,3 +1,7 @@
1
+ ## 0.64.33
2
+
3
+ - Converge arbitrary historical generated-model backlogs while failing a genuinely repeated immutable lifecycle step with a typed stall error.
4
+
1
5
  ## 0.64.32
2
6
 
3
7
  - Prove a restored generated-application device is enrolled before exposing business APIs, including recovery from an interrupted bootstrap journal.
@@ -240,6 +240,73 @@ class DomainClientModelAdoptionRequired implements Exception {
240
240
  '"$expectedDomainKey" ($expectedDomainHash) is $status. $summary';
241
241
  }
242
242
 
243
+ /// A generated model step repeated without converging after several retries.
244
+ ///
245
+ /// The limit is per immutable lifecycle step, not a cap on how many historical
246
+ /// model generations a long-lived workspace may lawfully adopt. That distinction
247
+ /// lets an old workspace catch up across an arbitrary backlog while still making
248
+ /// a genuinely stuck stage/accept loop fail loudly and with useful context.
249
+ final class DomainClientModelLifecycleStalled implements Exception {
250
+ const DomainClientModelLifecycleStalled({
251
+ required this.domain,
252
+ required this.candidateHash,
253
+ required this.status,
254
+ required this.attempts,
255
+ });
256
+
257
+ final String domain;
258
+ final String candidateHash;
259
+ final String status;
260
+ final int attempts;
261
+
262
+ @override
263
+ String toString() => 'DomainClientModelLifecycleStalled: generated model for '
264
+ '"$domain" ($candidateHash) remained $status after $attempts governed '
265
+ 'attempts without progress.';
266
+ }
267
+
268
+ /// Converge every pending generated-model generation, bounded by repeated work.
269
+ ///
270
+ /// A workspace can legitimately be more than a fixed number of generations
271
+ /// behind. Each distinct domain/hash/status tuple is evidence of progress, so it
272
+ /// gets its own retry budget. Only repetition of the same immutable step is a
273
+ /// stall.
274
+ Future<T> nmsInternalConvergeGeneratedModel<T>({
275
+ required Future<T> Function() attempt,
276
+ required Future<void> Function(DomainClientModelAdoptionRequired change)
277
+ handle,
278
+ }) async {
279
+ const maxAttemptsPerStep = 12;
280
+ final attemptsByStep = <String, int>{};
281
+ while (true) {
282
+ try {
283
+ return await attempt();
284
+ } on DomainClientModelAdoptionRequired catch (change) {
285
+ final step = '${change.expectedDomainKey}\u0000'
286
+ '${change.expectedDomainHash}\u0000'
287
+ '${change.status}\u0000'
288
+ '${change.canStage}\u0000${change.canAccept}';
289
+ final attempts = (attemptsByStep[step] ?? 0) + 1;
290
+ attemptsByStep[step] = attempts;
291
+ if (attempts > maxAttemptsPerStep) {
292
+ throw DomainClientModelLifecycleStalled(
293
+ domain: change.expectedDomainKey,
294
+ candidateHash: change.expectedDomainHash,
295
+ status: change.status,
296
+ attempts: attempts,
297
+ );
298
+ }
299
+ try {
300
+ await handle(change);
301
+ } on DomainClientModelAdoptionRequired {
302
+ // A transient preflight/acceptance race re-enters the same typed step.
303
+ // The per-step budget above prevents a genuine loop without limiting
304
+ // how many distinct historical generations may converge.
305
+ }
306
+ }
307
+ }
308
+ }
309
+
243
310
  /// The context-rich replacement for the kernel's generic
244
311
  /// `no active installed law declares domain 'x' … deploy the domain package first` refusal —
245
312
  /// thrown wherever that error used to surface (bridge dispatch, birth lanes) so an app dev sees
package/dart/pubspec.yaml CHANGED
@@ -7,7 +7,7 @@ repository: https://github.com/Captain-App/nomos2
7
7
  description: >-
8
8
  Nomos 2 Dart client — generated domain values, intent payloads and application
9
9
  lifecycle types over the typed Nomos kernel protocol and USDA custody model.
10
- version: 0.64.32
10
+ version: 0.64.33
11
11
 
12
12
  environment:
13
13
  sdk: ^3.5.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.105.16",
3
+ "version": "0.105.18",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
@@ -2507,16 +2507,12 @@ export function generateDartBridgeClient(
2507
2507
  ` );`,
2508
2508
  ` }`,
2509
2509
  ]),
2510
- ` for (var modelStep = 0; modelStep < 12; modelStep += 1) {`,
2511
- ` try {`,
2512
- ` return await bind(session, actor: actor${
2510
+ ` return nmsInternalConvergeGeneratedModel<${className}>(`,
2511
+ ` attempt: () => bind(session, actor: actor${
2513
2512
  manualEncryptionPresent ? ", keyStore: keyStore" : ""
2514
- });`,
2515
- ` } on DomainClientModelAdoptionRequired catch (change) {`,
2516
- ` await session.nomosInternalHandleModelChange(change);`,
2517
- ` }`,
2518
- ` }`,
2519
- ` throw StateError('Generated model lifecycle did not settle after twelve governed steps.');`,
2513
+ }),`,
2514
+ ` handle: session.nomosInternalHandleModelChange,`,
2515
+ ` );`,
2520
2516
  ` }`,
2521
2517
  ``,
2522
2518
  ` /// The session workspace this client is bound to (from the [NomosSession] handle, when constructed with`,
package/src/codegen_ts.ts CHANGED
@@ -1598,7 +1598,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1598
1598
  // holon; `resolve`/`watch` use the application's captured shared-plane navigator.
1599
1599
  // The reader is captured by the generated application. Tenant code receives navigation, never a reader.
1600
1600
  `interface _NomosForeignWorkspace { queryById(id: string): Promise<unknown>; watchById?(id: string, cb: (rows: unknown) => void, opts?: { intervalMs?: number }): () => void }`,
1601
- `interface NomosForeignReader { __nomosOpenChild?(workspace: string): Promise<_NomosForeignWorkspace> }`,
1601
+ `interface NomosForeignReader { readonly workspace?: string; __nomosOpenChild?(workspace: string): Promise<_NomosForeignWorkspace> }`,
1602
1602
  `async function _openForeign(nomos: NomosForeignReader, workspace: string): Promise<_NomosForeignWorkspace> {`,
1603
1603
  ` if (nomos.__nomosOpenChild === undefined) throw new Error("Nomos cannot follow this business reference on the installed runtime");`,
1604
1604
  ` return nomos.__nomosOpenChild(workspace);`,
@@ -2329,8 +2329,16 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
2329
2329
  }
2330
2330
 
2331
2331
  const aggByType = new Map<string, AggregateHandle>();
2332
+ const runtimeTypeByAggregate = new Map<string, string>();
2332
2333
  for (const mod of modules) {
2333
- for (const agg of mod.aggregates as AggregateHandle[]) aggByType.set(agg.id, agg);
2334
+ const domain = mod.domain ?? mod.name;
2335
+ for (const agg of mod.aggregates as AggregateHandle[]) {
2336
+ aggByType.set(agg.id, agg);
2337
+ runtimeTypeByAggregate.set(
2338
+ agg.id,
2339
+ isNamespacedAggregate(agg) ? qualifyReadProjectionType(domain, agg.id) : agg.id,
2340
+ );
2341
+ }
2334
2342
  }
2335
2343
 
2336
2344
  const anyAutoStamp = modules.some((mod) =>
@@ -2533,6 +2541,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
2533
2541
 
2534
2542
  out.push(`// ── read models (projection \`data\` shapes; partial-fold optional, EXCEPT .fromBirth()`, `// fields — their presence is gate law, so the type is non-optional) ──`, ``);
2535
2543
  const foreignRefFields: { agg: string; field: string; workspaceField: string; target: string }[] = [];
2544
+ const directRefFields: { agg: string; field: string; target: string; workspace?: string }[] = [];
2536
2545
  const atomicRefFields: { agg: string; field: string }[] = [];
2537
2546
  const versionedRefFields: { agg: string; field: string; currentField: string }[] = [];
2538
2547
  // wrap a read accessor return expression through the foreignRef row-hydrator (identity + byte-identical
@@ -2540,9 +2549,9 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
2540
2549
  // Every read accessor's rows get their canonical `.ref` stamped from `holon.workspace` (additive); a
2541
2550
  // foreignRef-declaring package ALSO hydrates its foreignRef fields into typed locators first.
2542
2551
  const wrapP = (expr: string): string =>
2543
- `_businessRecordsP(_stampRefsP(holon.workspace, ${foreignRefFields.length + atomicRefFields.length + versionedRefFields.length > 0 ? `_hydrateP(holon, ${expr})` : expr}))`;
2552
+ `_businessRecordsP(_stampRefsP(holon.workspace, ${foreignRefFields.length + directRefFields.length + atomicRefFields.length + versionedRefFields.length > 0 ? `_hydrateP(holon, ${expr})` : expr}))`;
2544
2553
  const wrapRows = (expr: string): string =>
2545
- `_businessRecords(_stampRefs(holon.workspace, ${foreignRefFields.length + atomicRefFields.length + versionedRefFields.length > 0 ? `_hydrateRows(holon, ${expr})` : expr}))`;
2554
+ `_businessRecords(_stampRefs(holon.workspace, ${foreignRefFields.length + directRefFields.length + atomicRefFields.length + versionedRefFields.length > 0 ? `_hydrateRows(holon, ${expr})` : expr}))`;
2546
2555
  // ATTRIBUTED READS (architecture/field_provenance.md): the shared provenance shape +
2547
2556
  // per-aggregate accessors — emitted ONLY when the package declares ≥1 `.attributed()`
2548
2557
  // field, so a provenance-free package's client is byte-identical (the era rule).
@@ -2571,6 +2580,19 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
2571
2580
  out.push(` ${tsKey(name)}${required ? "" : "?"}: ${tsTypeOfField(field as Field)};`);
2572
2581
  const fr = (field as { foreignRef?: { workspaceField: string; targetAggregateId: string } }).foreignRef;
2573
2582
  if (fr !== undefined) foreignRefFields.push({ agg: typeId, field: name, workspaceField: fr.workspaceField, target: fr.targetAggregateId });
2583
+ if (
2584
+ (field as Field).refAggregateId !== undefined &&
2585
+ (field as Field).foreignRef === undefined &&
2586
+ (field as Field).nomosRef === undefined &&
2587
+ (field as Field).versionedForeignRef === undefined
2588
+ ) {
2589
+ directRefFields.push({
2590
+ agg: typeId,
2591
+ field: name,
2592
+ target: runtimeTypeByAggregate.get((field as Field).refAggregateId!) ?? (field as Field).refAggregateId!,
2593
+ ...((field as Field).refWorkspace === undefined ? {} : { workspace: (field as Field).refWorkspace }),
2594
+ });
2595
+ }
2574
2596
  if ((field as Field).nomosRef !== undefined) atomicRefFields.push({ agg: typeId, field: name });
2575
2597
  const vr = (field as Field).versionedForeignRef;
2576
2598
  if (vr !== undefined) versionedRefFields.push({ agg: typeId, field: name, currentField: vr.currentField });
@@ -2614,7 +2636,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
2614
2636
  // row into a `ForeignRef<TargetAggregate>` — reading the foreign id (the field) + the foreign workspace id (the
2615
2637
  // declared sibling field). The read model stays honest (the field is the raw stored id string); this gives
2616
2638
  // the TYPED locator with resolve/watch on top, without cloning the other holon.
2617
- if (foreignRefFields.length + atomicRefFields.length + versionedRefFields.length > 0) {
2639
+ if (foreignRefFields.length + directRefFields.length + atomicRefFields.length + versionedRefFields.length > 0) {
2618
2640
  const byAgg = new Map<string, typeof foreignRefFields>();
2619
2641
  for (const f of foreignRefFields) { const l = byAgg.get(f.agg) ?? []; l.push(f); byAgg.set(f.agg, l); }
2620
2642
  out.push(`// ── foreign-ref locators (t.foreignRef): typed cross-workspace edges (resolve/watch on demand) ──`);
@@ -2637,23 +2659,35 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
2637
2659
  // `row.data.<field>.id` is still the raw id. Identity for any aggregate without foreignRefs.
2638
2660
  const hydrateSpecs: Record<string, { field: string; wsField: string; target: string }[]> = {};
2639
2661
  for (const f of foreignRefFields) { (hydrateSpecs[f.agg] ??= []).push({ field: f.field, wsField: f.workspaceField, target: f.target }); }
2662
+ const directSpecs: Record<string, { field: string; target: string; workspace?: string }[]> = {};
2663
+ for (const f of directRefFields) {
2664
+ (directSpecs[f.agg] ??= []).push({
2665
+ field: f.field,
2666
+ target: f.target,
2667
+ ...(f.workspace === undefined ? {} : { workspace: f.workspace }),
2668
+ });
2669
+ }
2640
2670
  const atomicSpecs: Record<string, { field: string }[]> = {};
2641
2671
  for (const f of atomicRefFields) { (atomicSpecs[f.agg] ??= []).push({ field: f.field }); }
2642
2672
  const versionedSpecs: Record<string, { field: string; currentField: string }[]> = {};
2643
2673
  for (const f of versionedRefFields) { (versionedSpecs[f.agg] ??= []).push({ field: f.field, currentField: f.currentField }); }
2644
2674
  out.push(
2645
2675
  `const _FOREIGN_REFS: Record<string, { field: string; wsField: string; target: string }[]> = ${JSON.stringify(hydrateSpecs)};`,
2676
+ `const _DIRECT_REFS: Record<string, { field: string; target: string; workspace?: string }[]> = ${JSON.stringify(directSpecs)};`,
2646
2677
  `const _ATOMIC_REFS: Record<string, { field: string }[]> = ${JSON.stringify(atomicSpecs)};`,
2647
2678
  `const _VERSIONED_REFS: Record<string, { field: string; currentField: string }[]> = ${JSON.stringify(versionedSpecs)};`,
2648
2679
  `function _hydrateRows<T>(nomos: NomosForeignReader, rows: Row<T>[]): Row<T>[] {`,
2649
2680
  ` return rows.map((row) => {`,
2650
2681
  ` const raw = row as Row<T> & { type: string };`,
2651
- ` const specs = _FOREIGN_REFS[raw.type] ?? [];`,
2652
- ` const atomic = _ATOMIC_REFS[raw.type] ?? [];`,
2653
- ` const versioned = _VERSIONED_REFS[raw.type] ?? [];`,
2654
- ` if ((specs.length + atomic.length + versioned.length) === 0 || row.data == null) return row;`,
2682
+ ` const localType = raw.type.slice(raw.type.lastIndexOf(".") + 1);`,
2683
+ ` const specs = _FOREIGN_REFS[raw.type] ?? _FOREIGN_REFS[localType] ?? [];`,
2684
+ ` const direct = _DIRECT_REFS[raw.type] ?? _DIRECT_REFS[localType] ?? [];`,
2685
+ ` const atomic = _ATOMIC_REFS[raw.type] ?? _ATOMIC_REFS[localType] ?? [];`,
2686
+ ` const versioned = _VERSIONED_REFS[raw.type] ?? _VERSIONED_REFS[localType] ?? [];`,
2687
+ ` if ((specs.length + direct.length + atomic.length + versioned.length) === 0 || row.data == null) return row;`,
2655
2688
  ` const data: Record<string, unknown> = { ...(row.data as Record<string, unknown>) };`,
2656
2689
  ` for (const s of specs) { const v = data[s.field]; if (typeof v === "string") data[s.field] = foreignRef(nomos, s.target, v, String(data[s.wsField] ?? "")); }`,
2690
+ ` for (const s of direct) { const v = data[s.field]; if (typeof v === "string") data[s.field] = _nmsRef(s.workspace ?? nomos.workspace ?? "", s.target, v); }`,
2657
2691
  ` for (const s of atomic) { const v = data[s.field] as { workspace: string; type: string; id: string } | undefined; if (v && typeof v === "object") data[s.field] = _nmsRef(v.workspace, v.type, v.id); }`,
2658
2692
  ...(hasVersionedRefs
2659
2693
  ? [` for (const s of versioned) { const v = data[s.field] as { pinned: _NomosRawAddress; series: _NomosRawAddress } | undefined; if (v && typeof v === "object") data[s.field] = _versionedNomosRef(nomos as NomosHolon, v, s.currentField); }`]
@@ -1664,8 +1664,8 @@ const NOMOS_TYPES_DEP = "^0.8.7";
1664
1664
  // 0.48.x: session-bound clients (XClient.bind/NomosClientInterface/DomainClientSessionMismatch/
1665
1665
  // DomainNotInstalledForSession, engine.follow, pendingConflicts lanes) — the generated client
1666
1666
  // REQUIRES this surface. 0.48.1 fixes NomosSession.watchPendingConflicts delegation.
1667
- const NOMOS_CLIENT_DEP = "^0.64.32";
1668
- const NOMOS_FLUTTER_DEP = "^0.63.55";
1667
+ const NOMOS_CLIENT_DEP = "^0.64.33";
1668
+ const NOMOS_FLUTTER_DEP = "^0.63.57";
1669
1669
  // DRIFT TRIPWIRE: pubspec_pins.test.ts asserts these equal the CURRENT versions in dsl/dart/pubspec.yaml
1670
1670
  // and dsl/dart_types/pubspec.yaml — a Dart release without this bump fails the suite, so tenants never
1671
1671
  // land back in dependency_overrides (the exact co2 report this closes, third occurrence).
@@ -3936,12 +3936,26 @@ async function main(): Promise<void> {
3936
3936
  const domainKey = mod.domain ?? mod.name;
3937
3937
  const locked = priorLock.domains[mod.name] ?? priorLock.domains[domainKey];
3938
3938
  if (locked !== undefined) {
3939
+ const currentImplementationDetailAggregateIds = new Set(
3940
+ mod.aggregates
3941
+ .filter(
3942
+ (aggregate) =>
3943
+ (aggregate as { businessBirthInternal?: unknown }).businessBirthInternal === true,
3944
+ )
3945
+ .map((aggregate) => aggregate.id),
3946
+ );
3939
3947
  // Framework aggregates are shared implementation law, reconciled under @framework rather
3940
- // than owned by this tenant domain. A package may compose a different framework subset as
3941
- // its topology changes; never instruct a tenant to author a retirement for Nomos internals.
3948
+ // than owned by this tenant domain. Domain-scoped implementation aggregates such as
3949
+ // NomosSourceFrontier deliberately retain the tenant domain's law namespace, but are still
3950
+ // hidden from its structural interface. A package may compose a different framework subset
3951
+ // as its topology changes; never instruct a tenant to author a retirement for Nomos internals.
3942
3952
  const localAggregates = Object.fromEntries(
3943
3953
  Object.entries(locked.stableIds.aggregates)
3944
- .filter(([, aggregate]) => !frameworkAggregateSids.has(aggregate.sid)),
3954
+ .filter(
3955
+ ([aggregateId, aggregate]) =>
3956
+ !frameworkAggregateSids.has(aggregate.sid) &&
3957
+ !currentImplementationDetailAggregateIds.has(aggregateId),
3958
+ ),
3945
3959
  );
3946
3960
  priorDomainsForPackage[domainKey] = {
3947
3961
  stableIds: { ...locked.stableIds, aggregates: localAggregates },