@githolon/dsl 0.60.0 → 0.60.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 CHANGED
@@ -1,7 +1,12 @@
1
- ## 0.39.0
1
+ ## 0.41.0
2
+ - `NomosTestHarness` emits named phase-progress diagnostics (install → spawn runner → await ready → connect, plus per-session progress in `cluster`) and each `connect()` carries a `connectTimeout` (default 120s) that throws a NAMED remedy instead of hanging — so a multi-test AppSim run can tell install vs runner-wait vs session-block vs deadlock (co2 feedback).
3
+
4
+ ## 0.40.0
2
5
  - `connect(maxMounted: …)` — the realm's mounted-workspace budget (LRU park/remount above it); useful on memory-constrained devices holding many local workspaces.
3
6
  - MULTI-WORKSPACE BY DEFAULT IN TESTS: `NomosTestHarness.ephemeral` now connects a DECOY session alongside the workspace under test, and `harness.bridge` is a `NomosSession` HANDLE pinned to it (existing tenant tests work unchanged). Sole-session inference therefore never silently passes under the harness — an op that forgot its session refuses in your first test with the same `multiple sessions connected — pass session` a real multi-workspace app raises. Opt out with `decoySession: false`.
4
7
  - `NomosBridge.open(...)` — connect + pinned `NomosSession` handle in one call (`final home = await bridge.open(workspace: …); HomeClient(home)`): the recommended plane/handle shape; the bare bridge methods remain one-workspace sugar.
8
+
9
+ ## 0.39.0
5
10
  - Multi-session compat surface: `watchCompatibility` takes an optional `session`; `NomosSession` pins its compat watch, `compat`, and `onLawChanged` to ITS workspace (per-session `lawChanged` scoping — previously a multi-session plane's compat verdict was whichever session synced last, and a session's compat watch refused on a >1-session plane).
6
11
 
7
12
  ## 0.38.0
@@ -136,6 +136,7 @@ class NomosTestHarness {
136
136
  void Function(Future<void> Function())? registerTearDown,
137
137
  void Function(String line)? onDiagnostic,
138
138
  Duration readyTimeout = const Duration(seconds: 30),
139
+ Duration connectTimeout = const Duration(seconds: 120),
139
140
  bool decoySession = true,
140
141
  }) async {
141
142
  if (deployJsonPaths.isEmpty) {
@@ -162,8 +163,10 @@ class NomosTestHarness {
162
163
  final workdir = await Directory.systemTemp.createTemp('nomos-harness-');
163
164
  // Total-ordered teardown even on a mid-setup failure: build the transport lazily and clean up on throw.
164
165
  NodeRunnerTransport? transport;
166
+ final phase = (String m) { if (onDiagnostic != null) onDiagnostic('[nomos-test-harness] $m'); };
165
167
  try {
166
168
  final snapshot = '${workdir.path}/snapshot.bin';
169
+ phase('phase 1/4 installing ${deployJsonPaths.length} law package(s) offline into "$workspace" …');
167
170
  final info = await _runInstall(
168
171
  node: node,
169
172
  useInRepoScript: useInRepoScript,
@@ -175,6 +178,7 @@ class NomosTestHarness {
175
178
  runtimeDir: runtimeDir,
176
179
  onDiagnostic: onDiagnostic);
177
180
 
181
+ phase('phase 2/4 spawning the node runner (node=$node) …');
178
182
  transport = await NodeRunnerTransport.start(
179
183
  entry: entry,
180
184
  resolveFrom: resolveFrom,
@@ -187,16 +191,24 @@ class NomosTestHarness {
187
191
  onDiagnostic: onDiagnostic,
188
192
  );
189
193
  final plane = NomosBridge(transport);
194
+ phase('phase 3/4 awaiting runner ready (≤${readyTimeout.inSeconds}s) …');
190
195
  await plane.ready.timeout(readyTimeout,
191
196
  onTimeout: () => throw NomosTestHarnessException(
192
- 'the offline runner never signalled ready within $readyTimeout '
193
- '(is `node` on PATH and ~/.holon/runtime warm?)'));
197
+ 'the offline runner never signalled ready within ${readyTimeout.inSeconds}s '
198
+ '(phase 3/4: is `node` on PATH and ~/.holon/runtime warm? the runner process may have '
199
+ 'failed to boot the engine — run with onDiagnostic to see its stderr)'));
200
+ phase('phase 4/4 connecting session "$workspace" (≤${connectTimeout.inSeconds}s) …');
194
201
  await plane.connect(
195
202
  cloud: 'https://offline.invalid', // ignored: the offline runner serves everything locally
196
203
  workspace: workspace,
197
204
  clientId: clientId,
198
205
  allowUnborn: true,
199
- );
206
+ ).timeout(connectTimeout,
207
+ onTimeout: () => throw NomosTestHarnessException(
208
+ 'connect("$workspace") did not complete within ${connectTimeout.inSeconds}s (phase 4/4) — '
209
+ 'the runner is up but the session never mounted; a large ledger converge-fold can be slow, '
210
+ 'raise connectTimeout, or run with onDiagnostic to see where it blocked'));
211
+ phase('ready — session "$workspace" connected (law ${(info['lawHashes'] as List?)?.isNotEmpty == true ? (info['lawHashes'] as List).first.toString().substring(0, 12) : "installed"})');
200
212
  // THE DECOY SESSION (multi-workspace by default in tests): sole-session inference is how
201
213
  // single-workspace assumptions ship — with one session connected, an op that forgot its session
202
214
  // silently works in tests and only refuses in production when the app connects its second
@@ -266,6 +278,7 @@ class NomosTestHarness {
266
278
  void Function(Future<void> Function())? registerTearDown,
267
279
  void Function(String line)? onDiagnostic,
268
280
  Duration readyTimeout = const Duration(seconds: 30),
281
+ Duration connectTimeout = const Duration(seconds: 120),
269
282
  }) async {
270
283
  if (workspaces.isEmpty) {
271
284
  throw NomosTestHarnessException(
@@ -312,6 +325,8 @@ class NomosTestHarness {
312
325
  }
313
326
 
314
327
  // 2. ONE runner / ONE bridge for the whole plane — no env restore default; each connect brings its own.
328
+ final phase = (String m) { if (onDiagnostic != null) onDiagnostic('[nomos-test-harness] $m'); };
329
+ phase('cluster: spawning the node runner for ${workspaces.length} workspace(s) …');
315
330
  transport = await NodeRunnerTransport.start(
316
331
  entry: entry,
317
332
  resolveFrom: resolveFrom,
@@ -323,24 +338,32 @@ class NomosTestHarness {
323
338
  onDiagnostic: onDiagnostic,
324
339
  );
325
340
  final bridge = NomosBridge(transport);
341
+ phase('cluster: awaiting runner ready (≤${readyTimeout.inSeconds}s) …');
326
342
  await bridge.ready.timeout(readyTimeout,
327
343
  onTimeout: () => throw NomosTestHarnessException(
328
- 'the offline runner never signalled ready within $readyTimeout '
329
- '(is `node` on PATH and ~/.holon/runtime warm?)'));
344
+ 'the offline runner never signalled ready within ${readyTimeout.inSeconds}s '
345
+ '(is `node` on PATH and ~/.holon/runtime warm? run with onDiagnostic to see the runner stderr)'));
330
346
 
331
347
  // 3. connect one SESSION per workspace, each restoring its OWN snapshot — LOCAL-ONLY (no cloud;
332
348
  // these are local githolons on the device plane, offline by nature).
333
349
  final snapshotB64 = <String, String>{};
350
+ var _n = 0;
334
351
  for (final e in workspaces.entries) {
335
352
  snapshotB64[e.key] = base64Encode(await File(snapshots[e.key]!).readAsBytes());
353
+ phase('cluster: connecting session ${++_n}/${workspaces.length} "${e.key}" (≤${connectTimeout.inSeconds}s) …');
336
354
  await bridge.connect(
337
355
  workspace: e.key,
338
356
  session: e.key,
339
357
  clientId: '$clientId-${e.key}',
340
358
  restoreFrom: snapshotB64[e.key],
341
359
  allowUnborn: true,
342
- );
360
+ ).timeout(connectTimeout,
361
+ onTimeout: () => throw NomosTestHarnessException(
362
+ 'connect("${e.key}") did not complete within ${connectTimeout.inSeconds}s — the runner is up '
363
+ 'but this session never mounted (a large ledger converge-fold can be slow; raise connectTimeout '
364
+ 'or run with onDiagnostic)'));
343
365
  }
366
+ phase('cluster: ${workspaces.length} session(s) connected');
344
367
 
345
368
  // 3b. DEVICE-KEY the named sessions: mint a fresh in-kernel key on each (now that it is mounted) and
346
369
  // RE-OPEN it with that key as its signing authorSecret — the resolver's attested-read signing key.
package/dart/pubspec.yaml CHANGED
@@ -11,7 +11,7 @@ description: >-
11
11
  Nomos 2 Dart frontend types — the hand-written kernel wire types plus the
12
12
  code-generated per-domain aggregate + directive-payload types. (De)serializes
13
13
  the EXACT serde JSON the Rust kernel + projection produce/consume.
14
- version: 0.39.0
14
+ version: 0.41.0
15
15
 
16
16
  environment:
17
17
  sdk: ^3.5.0
@@ -107,6 +107,30 @@ void main() {
107
107
  );
108
108
  }, timeout: const Timeout(Duration(minutes: 2)));
109
109
 
110
+ test('DX: ephemeral emits phase-progress diagnostics (install → runner → ready → connect)', () async {
111
+ final skip = _missingPrereq();
112
+ if (skip != null) {
113
+ markTestSkipped(skip);
114
+ return;
115
+ }
116
+ final lines = <String>[];
117
+ final harness = await NomosTestHarness.ephemeral(
118
+ deployJsonPaths: [_guestbookDeployJson],
119
+ nodeEntry: _repoEntry,
120
+ runtimeDir: _runtimeDir,
121
+ registerTearDown: addTearDown,
122
+ onDiagnostic: lines.add,
123
+ );
124
+ final joined = lines.join('\n');
125
+ // the four named phases are surfaced so an AppSim can tell install vs runner-wait vs connect-block.
126
+ expect(joined, contains('phase 1/4 installing'));
127
+ expect(joined, contains('phase 2/4 spawning the node runner'));
128
+ expect(joined, contains('phase 3/4 awaiting runner ready'));
129
+ expect(joined, contains('phase 4/4 connecting session'));
130
+ expect(joined, contains('ready — session "test-ws" connected'));
131
+ expect(harness.bridge, isNotNull);
132
+ }, timeout: const Timeout(Duration(minutes: 2)));
133
+
110
134
  test('OFFLINE: dispose is idempotent and removes the temp dir', () async {
111
135
  final skip = _missingPrereq();
112
136
  if (skip != null) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.60.0",
3
+ "version": "0.60.1",
4
4
  "type": "module",
5
5
  "description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
@@ -350,6 +350,11 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
350
350
  `// Drives the REAL githolon over a NomosBridge (the WebView runner). Regenerated every compile; do`,
351
351
  `// not edit. Construct with a connected bridge:`,
352
352
  `//`,
353
+ // GENERATED-CODE lints: the bridge read/watch surface returns dynamic (JSON rows), so typed accessors
354
+ // do dynamic member access + casts by construction. Silence the lints that only make sense for
355
+ // hand-written code, so this file never adds noise to a tenant's analyzer output (co2 feedback).
356
+ `// ignore_for_file: avoid_dynamic_calls, unnecessary_this, prefer_const_constructors, unused_import, type_annotate_public_apis, always_specify_types`,
357
+ `//`,
353
358
  `// final bridge = NomosBridge(transport); // transport = your WebViewTransport`,
354
359
  `// await bridge.ready;`,
355
360
  `// await bridge.connect(cloud: cloud, workspace: ws, clientId: deviceId);`,
@@ -656,6 +656,10 @@ export interface RelationBootstrapLeg {
656
656
  * would trip the seed's owner-binding prerequisite (HomeOwner unset) and FALSE-fail. When present, the
657
657
  * runner IGNORES the standalone grant fields and proves via {@link BornContextAuthority}. */
658
658
  readonly bornContext?: BornContextAuthority;
659
+ /** THE GATED-GRANTER CHAIN (co2: `grantCo2EstateCreator` itself `.requires("co2Admin")`): the granter's
660
+ * OWN gate bootstrapped recursively — the runner seats THIS authority first (its prerequisite seeds +
661
+ * its grant on the same actor), then the outer grant admits. Depth/cycle-guarded at discovery. */
662
+ readonly granterAuthority?: RelationBootstrapLeg;
659
663
  /** THE PREREQUISITE-SEED CHAIN (the composed co2_platform / `seedPlatformAdmin` case). Present iff the
660
664
  * granter (`grantDirectiveId`) carries a PREREQUISITE invariant whose state is established NOT by a birth
661
665
  * genesis but by a SIBLING SEED in the SAME domain (e.g. `seedPlatformAdmin`'s `PlatformAdminBindsFounder`
@@ -1186,7 +1190,7 @@ export function discoverPrerequisiteChain(mod: DomainModule, granter: Directive<
1186
1190
  * granter, so granting B makes the actor satisfy R. `undefined` ⇒ no gated directive, or no granter for R
1187
1191
  * or any of its bases (the caller may then fall through to the seed-path lane; the negative conformance
1188
1192
  * leg always still runs). */
1189
- export function discoverRelationBootstrapLeg(mod: DomainModule): RelationBootstrapLeg | undefined {
1193
+ export function discoverRelationBootstrapLeg(mod: DomainModule, gatedOverride?: Directive<unknown>, _seen: ReadonlySet<string> = new Set()): RelationBootstrapLeg | undefined {
1190
1194
  const domain = mod.domain ?? mod.name;
1191
1195
  const directives = mod.directives as Directive<unknown>[];
1192
1196
  const isGatedProvable = (d: Directive<unknown> | undefined): boolean =>
@@ -1198,8 +1202,15 @@ export function discoverRelationBootstrapLeg(mod: DomainModule): RelationBootstr
1198
1202
  // naive standalone-create leg (isStandaloneCreate), so the bootstrap leg is its ONLY positive proof — seed
1199
1203
  // authority, then prove the CREATE is admitted. When no gated create exists, fall back to any gated write
1200
1204
  // (a gated `.mutates`/`.ensures`), so an un-created gated law is still proven.
1201
- const gated = directives.find((d) => isGatedProvable(d) && d.marker === "creates")
1202
- ?? directives.find((d) => isGatedProvable(d));
1205
+ // `gatedOverride` targets a SPECIFIC gated directive instead (the gated-`.births()` case — excluded from
1206
+ // the default scan because a birth is proven by its OWN leg; the override reuses the same granter/seed
1207
+ // discovery to SEAT the birth's required relation).
1208
+ const gated = gatedOverride !== undefined && gatedOverride.requiresRelationGate !== undefined && typeof gatedOverride.payloadSchema === "object"
1209
+ ? gatedOverride
1210
+ : gatedOverride !== undefined
1211
+ ? undefined
1212
+ : (directives.find((d) => isGatedProvable(d) && d.marker === "creates")
1213
+ ?? directives.find((d) => isGatedProvable(d)));
1203
1214
  if (gated === undefined) return undefined;
1204
1215
  const relation = gated.requiresRelationGate!.relation;
1205
1216
 
@@ -1313,6 +1324,18 @@ export function discoverRelationBootstrapLeg(mod: DomainModule): RelationBootstr
1313
1324
  if (found === undefined) return undefined; // no granter at all → the caller emits the diagnostic
1314
1325
  const granter = found.granter;
1315
1326
 
1327
+ // GATED GRANTER (co2: grantCo2EstateCreator itself .requires("co2Admin")): recursively bootstrap the
1328
+ // granter's OWN gate so the runner seats that authority first. Cycle/depth-guarded via the seen-set; an
1329
+ // unresolvable granter gate still emits the leg WITHOUT the chain (the runner's refusal names the gap —
1330
+ // never silently weaker than before this lane existed).
1331
+ let granterAuthority: RelationBootstrapLeg | undefined;
1332
+ if (granter.requiresRelationGate !== undefined && !_seen.has(granter.id) && _seen.size < 4) {
1333
+ granterAuthority = discoverRelationBootstrapLeg(mod, granter, new Set([..._seen, gated.id, granter.id]));
1334
+ // a born-context result proves INSIDE a born workspace — not a seatable pre-grant; drop it (the
1335
+ // runner's refusal then names the granter's unseatable gate).
1336
+ if (granterAuthority?.bornContext !== undefined) granterAuthority = undefined;
1337
+ }
1338
+
1316
1339
  // Synthesize the grant payload + resolve which fields route into subject / grantedBy (so runtime seats the
1317
1340
  // grant on the proof actor). Best-effort: an un-probable grant plan still emits the leg (runtime overwrites
1318
1341
  // any field whose name looks like a subject/grantedBy anyway).
@@ -1343,6 +1366,7 @@ export function discoverRelationBootstrapLeg(mod: DomainModule): RelationBootstr
1343
1366
  ...(seedSpeculative ? { seedSpeculative: true } : {}),
1344
1367
  ...(seedWritesRelations !== undefined ? { seedWritesRelations } : {}),
1345
1368
  ...(prerequisiteSeeds !== undefined ? { prerequisiteSeeds } : {}),
1369
+ ...(granterAuthority !== undefined ? { granterAuthority } : {}),
1346
1370
  };
1347
1371
  }
1348
1372
 
@@ -1464,6 +1488,12 @@ export interface ChildBirthLeg {
1464
1488
  * parent workspace (home → estate is the canonical case), the proof runner drives this setup birth first
1465
1489
  * and then authors the child birth inside the born parent. */
1466
1490
  readonly parentSetup?: ChildBirthParentSetup;
1491
+ /** RELATION SETUP for a GATED birth (`.births().requires(R)`) with NO parent-birth authority path: the
1492
+ * relation-bootstrap discovery (granter marker / inferred writeTuple / computed base / prerequisite-seed
1493
+ * chain) reused to SEAT R on the birth actor before the runner drives the birth offer — the law's own
1494
+ * granters, never a proof-only backdoor. Absent when `parentSetup` covers authority or no granter is
1495
+ * discoverable (the runner then reports the gate refusal with the discovery gap named). */
1496
+ readonly relationSetup?: RelationBootstrapLeg;
1467
1497
  /** COMPILE-TIME RECIPE DIAGNOSTICS (births DX): problems found by statically validating each genesis
1468
1498
  * step against the composed package (missing child module, unknown seed directive, payload-schema
1469
1499
  * mismatch, a plan that returns a non-array — the exact things that otherwise surface at proof time as
@@ -2041,7 +2071,19 @@ export function legsForDomain(mod: DomainModule, modules: readonly DomainModule[
2041
2071
  // The child-birth legs (one per `.births()` directive) — discovered by running each plan. A parent
2042
2072
  // with SEVERAL births (co2: birthCatalogueWorkspace + birthEstateWorkspace) keeps EVERY leg; keeping
2043
2073
  // only the first left the other children compile-valid-only (unprovable through their birth).
2044
- const childBirths = allBirths.filter((b) => b.parentDomain === domain).map((b) => attachParentSetup(b, allBirths));
2074
+ const childBirths = allBirths.filter((b) => b.parentDomain === domain).map((b) => {
2075
+ const withParent = attachParentSetup(b, allBirths);
2076
+ // A GATED birth with no parent-birth authority path: seat its relation via the SAME granter/seed
2077
+ // discovery the relation-bootstrap leg uses (the law's own authority path, e.g. co2's
2078
+ // seedPlatformFounder → seedPlatformAdmin → grantCo2EstateCreator → birthEstateWorkspace).
2079
+ if (withParent.birthRequiresRelation !== undefined && withParent.parentSetup === undefined) {
2080
+ const birthDir = (mod.directives as Directive<unknown>[]).find((d) => d?.id === b.birthDirectiveId);
2081
+ const rb = birthDir !== undefined ? discoverRelationBootstrapLeg(mod, birthDir) : undefined;
2082
+ // a born-context path proves INSIDE a born workspace — not a seatable pre-grant; leave it to the gap diagnostic.
2083
+ if (rb !== undefined && rb.bornContext === undefined) return { ...withParent, relationSetup: rb };
2084
+ }
2085
+ return withParent;
2086
+ });
2045
2087
  const childBirthWithSetup = childBirths[0];
2046
2088
 
2047
2089
  // The relation-bootstrap leg (when the domain has a `.requires(R)`-gated directive AND a discoverable
@@ -888,14 +888,26 @@ function emitDartPackages(
888
888
  }
889
889
 
890
890
  const declared = new Set<string>();
891
- for (const src of typesSources.values()) for (const n of declaredDartTypeNames(src)) declared.add(n);
891
+ const declaredByFile = new Map<string, Set<string>>();
892
+ for (const [f, src] of typesSources) {
893
+ const ns = declaredDartTypeNames(src);
894
+ declaredByFile.set(f, ns);
895
+ for (const n of ns) declared.add(n);
896
+ }
897
+ // Only import files that actually CONTRIBUTE an alias target — an alias `typedef A = B` needs B in scope,
898
+ // but a file no alias references needs no import (the `export` re-exports it). Importing every file
899
+ // produced unused_import warnings in the tenant's barrel (co2 feedback).
900
+ const aliasTargets = new Set(Object.keys(names ?? {}));
901
+ const aliasImportUris = [...typesSources.keys()]
902
+ .filter((f) => [...(declaredByFile.get(f) ?? [])].some((n) => aliasTargets.has(n)))
903
+ .map((f) => `${genRel}/${f}.dart`);
892
904
  const typesBarrel = [
893
905
  "// GENERATED by nomos-compile — the TYPES package's public entrypoint. DO NOT EDIT.",
894
906
  "// Pure data: payload classes, read models, VOs/enums, typed read accessors over the",
895
907
  "// injected NomosReads seams. Frontend-safe — no bridge, no sessions, no lifecycle.",
896
908
  "library;",
897
909
  "",
898
- ...aliasImportLines(names, [...typesSources.keys()].map((f) => `${genRel}/${f}.dart`)),
910
+ ...aliasImportLines(names, aliasImportUris),
899
911
  `export 'package:nomos_types/nomos_types.dart';`,
900
912
  ...[...typesSources.keys()].map((f) => `export '${genRel}/${f}.dart';`),
901
913
  ...dartAliasLines(names, declared),
@@ -2291,6 +2303,19 @@ async function main(): Promise<void> {
2291
2303
  }
2292
2304
  }
2293
2305
  console.log(` deploy ${rel(deployPath)}`);
2306
+ if (compact) {
2307
+ // COMPACT PACKAGES ride a base64 blob (nomos:usdJsonB64) — a peer needs the b64 decode arm
2308
+ // (kernel ≥ the 2026-07-04 release; every current cloud/browser/node runtime has it). A NATIVE
2309
+ // (iOS/macOS AOT) client pins its OWN kernel binary: if that xcframework predates the arm, install
2310
+ // fails with `parse embedded USDA IR: domain package missing USDA field nomos:usdJsonHex`. Flag it
2311
+ // so the tenant isn't surprised (the requested "flag compact when targeting native").
2312
+ console.log(``);
2313
+ console.log(` ⚠ compact package (base64 blob): requires a kernel with the b64 decode arm (≥ 2026-07-04).`);
2314
+ console.log(` Current cloud/browser/node runtimes have it. A NATIVE client (nomos_flutter iOS/macOS)`);
2315
+ console.log(` pins its OWN kernel — refetch the current NomosKernel.xcframework (rm ios/Frameworks/`);
2316
+ console.log(` NomosKernel.xcframework && pod install), or set { compact: false } for a hex package that`);
2317
+ console.log(` parses on every kernel (larger deploy body; may need the signed-upload deploy lane).`);
2318
+ }
2294
2319
  console.log(``);
2295
2320
  console.log(`deploy (Nomos Cloud):`);
2296
2321
  console.log(` githolon login --agent && githolon ws create <ws> && githolon deploy <ws> # no secret — deploy is OPEN, kernel-judged at the offer gate`);