@githolon/dsl 0.75.1 → 0.76.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 CHANGED
@@ -1,3 +1,18 @@
1
+ ## 0.52.0
2
+
3
+ - **Offer ack decoupled from watch fan-out** (co2's 2.08s 3×3-grid report): the durable acknowledgement
4
+ (plan + gate + fold + git write) resolves before any watch fan-out runs; the post-write nudge —
5
+ including the O(law-size) compat recompute that was the bulk of the unaccounted ~1.1s — is a
6
+ coalesced macrotask after the ack. Ack latency is now watcher-count-independent (fixture: 346ms with
7
+ 12 watchers → 71ms flat). Read-your-writes holds (projection updates at fold time, tested); ticks are
8
+ never lost (coalescing + flush-on-dispose, tested).
9
+ - **Full offer span taxonomy** on `recentSpans`/`spanEvents` with no new API: `payload-encode`,
10
+ `compat-preflight`, `bridge-ipc`, `gate-fold`, `watch-nudge`, `response-delivery` (`git-write` is not
11
+ separable client-side — plan+gate+fold+write is ONE wasm call). Older runner bundles degrade cleanly
12
+ (no bridge-leg spans).
13
+ - Regression test for local-only sync observational inertness (offer → query → sync{localOnly} → head
14
+ unchanged → same row).
15
+
1
16
  ## 0.51.0
2
17
 
3
18
  - **The burn-down wave.** Dead-letter queue surface on the session plane: `deadLetters()`,
@@ -1193,6 +1193,13 @@ class NomosBridge {
1193
1193
  String? session,
1194
1194
  }) async {
1195
1195
  final Map<String, Object?> r;
1196
+ // OFFER SPAN TAXONOMY (bridge legs): stamp the Dart-side send/receive around the op; the runner
1197
+ // echoes { timing: { recv, done } } (its op-pickup and durable-ack instants, ms-epoch, same wall
1198
+ // clock — one machine). bridge-ipc = t0..recv (up-leg transport + queueing); response-delivery =
1199
+ // done..t1 (post-ack down-leg). The runner's own spans (payload-encode / compat-preflight /
1200
+ // gate-fold / watch-nudge) stream over the span event lane, so `recentSpans` carries the full
1201
+ // attributable breakdown of one offer with no new API.
1202
+ final t0 = DateTime.now().millisecondsSinceEpoch;
1196
1203
  try {
1197
1204
  r = _asMap(await _call('offerDirective', {
1198
1205
  'domain': domain,
@@ -1208,6 +1215,23 @@ class NomosBridge {
1208
1215
  throw await _wrapDomainNotInstalled(e,
1209
1216
  domain: domain, domainHash: domainHash, requestedWorkspace: session);
1210
1217
  }
1218
+ final t1 = DateTime.now().millisecondsSinceEpoch;
1219
+ final timing = r['timing'];
1220
+ if (timing is Map) {
1221
+ final recv = timing['recv'], done = timing['done'];
1222
+ if (recv is num && done is num) {
1223
+ int within(num v) {
1224
+ final x = v.toInt();
1225
+ return x < t0 ? t0 : (x > t1 ? t1 : x);
1226
+ }
1227
+ noteLocalSpan(
1228
+ track: 'submit', name: 'offer: bridge ipc (up-leg)', kind: 'bridge-ipc',
1229
+ t0: t0, t1: within(recv));
1230
+ noteLocalSpan(
1231
+ track: 'submit', name: 'offer: response delivery (down-leg)', kind: 'response-delivery',
1232
+ t0: within(done), t1: t1);
1233
+ }
1234
+ }
1211
1235
  final births = ((r['birthOutcomes'] as List?) ?? const [])
1212
1236
  .whereType<Map>()
1213
1237
  .map((m) => m.cast<String, Object?>())
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.51.0
14
+ version: 0.52.0
15
15
 
16
16
  environment:
17
17
  sdk: ^3.5.0
@@ -0,0 +1,97 @@
1
+ // LOCAL-ONLY SYNC MUST BE OBSERVATIONALLY INERT (co2's 0.75.1 blocker). Their repro, verbatim shape:
2
+ // open a local:// session from a snapshot → offer → query returns the row → sync(admit: true) returns
3
+ // {localOnly: true} → the SAME query must return the SAME row, and the head must be unchanged.
4
+ // No branch/fold mutation, no network, identical results before and after.
5
+ @TestOn('vm')
6
+ library;
7
+
8
+ import 'dart:io';
9
+
10
+ import 'package:nomos_client/nomos_headless.dart';
11
+ import 'package:test/test.dart';
12
+
13
+ import '../../../bench/scale/fixtures/estate-birth/build/dart/src/generated/home.client.dart';
14
+
15
+ String get _root => '${Directory.current.path}/../..';
16
+ String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.mjs';
17
+ String get _deployJson =>
18
+ '$_root/bench/scale/fixtures/estate-birth/build/estate-birth.deploy.json';
19
+ String get _runtimeDir =>
20
+ Platform.environment['NOMOS_DART_RUNTIME'] ??
21
+ '${Platform.environment['HOME'] ?? ''}/.holon/runtime';
22
+
23
+ const _workspace = 'estate-birth';
24
+ const _owner = 'user:local-sync-inert';
25
+
26
+ String? _missingPrereq() {
27
+ if (!File(_repoEntry).existsSync())
28
+ return 'in-repo runner not found at $_repoEntry';
29
+ if (!File(_deployJson).existsSync()) {
30
+ return 'compile the fixture: (cd bench/scale/fixtures/estate-birth && node ../../../../dsl/compile_package.mjs)';
31
+ }
32
+ if (!File('$_runtimeDir/holon.wasm').existsSync())
33
+ return 'warm ~/.holon/runtime first';
34
+ return null;
35
+ }
36
+
37
+ void main() {
38
+ test(
39
+ 'local-only sync is OBSERVATIONALLY INERT: offer -> query -> sync -> head unchanged -> same row',
40
+ () async {
41
+ final skip = _missingPrereq();
42
+ if (skip != null) {
43
+ markTestSkipped(skip);
44
+ return;
45
+ }
46
+ final harness = await NomosTestHarness.cluster(
47
+ workspaces: {
48
+ _workspace: [_deployJson],
49
+ },
50
+ deviceKeyed: {_workspace},
51
+ nodeEntry: _repoEntry,
52
+ runtimeDir: _runtimeDir,
53
+ );
54
+ try {
55
+ final session = harness.session(_workspace);
56
+ final client = await HomeClient.bind(session, actor: _owner);
57
+
58
+ // 1-2. offer a write on the snapshot-opened local session.
59
+ final birth = await client.birthEstateWorkspace(
60
+ payload: BirthEstateWorkspacePayload(
61
+ owner: _owner,
62
+ frameworkHash: harness.frameworkHash!,
63
+ lawHash: harness.lawHashes(_workspace).first,
64
+ bornAt: DateTime.now().toUtc().toIso8601String(),
65
+ ),
66
+ );
67
+ expect(birth.born, hasLength(1));
68
+
69
+ // 3. query confirms the row (the home law's birth-record read).
70
+ final before = await client.birthRecordsByPrincipal(principal: _owner);
71
+ expect(before, hasLength(1),
72
+ reason: 'the offer must be readable BEFORE sync');
73
+ final statusBefore = await session.custodyStatus();
74
+ expect(statusBefore.pendingLocalIntents, greaterThan(0),
75
+ reason: 'the local write is pending (nothing upstream to land on)');
76
+
77
+ // 4. local-only sync — must be a no-op in EVERY observable respect.
78
+ final summary = await session.sync(admit: true);
79
+ expect(summary['localOnly'], true,
80
+ reason: 'a local:// session syncs as the documented no-op');
81
+
82
+ // 5. THE REGRESSION (co2): custody unchanged, same query returns the SAME row.
83
+ final statusAfter = await session.custodyStatus();
84
+ expect(statusAfter.pendingLocalIntents, statusBefore.pendingLocalIntents,
85
+ reason:
86
+ 'local-only sync must not consume/move pending local intents');
87
+ final after = await client.birthRecordsByPrincipal(principal: _owner);
88
+ expect(after, hasLength(1),
89
+ reason:
90
+ 'local-only sync must be observationally inert — the row must still read');
91
+ expect(after.single.principal, before.single.principal,
92
+ reason: 'the SAME row, byte-for-byte semantics');
93
+ } finally {
94
+ await harness.dispose();
95
+ }
96
+ }, timeout: const Timeout(Duration(minutes: 4)));
97
+ }
@@ -307,6 +307,48 @@ void main() {
307
307
  }
308
308
  });
309
309
 
310
+ group('offer span taxonomy (bridge legs)', () {
311
+ test(
312
+ 'offerOutcome turns the runner timing stamps into bridge-ipc + response-delivery spans on recentSpans',
313
+ () async {
314
+ final now = DateTime.now().millisecondsSinceEpoch;
315
+ final transport = ReplayTransport({
316
+ 'head': 'H1',
317
+ 'created': <Object?>[],
318
+ 'workspace': 'ws',
319
+ // the runner's op-pickup + durable-ack instants (additive `timing` field)
320
+ 'timing': {'recv': now, 'done': now},
321
+ });
322
+ final bridge = NomosBridge(transport);
323
+ await bridge.ready;
324
+ await bridge.offerOutcome(
325
+ domain: 'guestbook', directiveId: 'signGuestbook', payload: {'author': 'ada'});
326
+ final kinds = bridge.recentSpans
327
+ .map((m) => ((m['span'] as Map?)?['kind'] ?? '').toString())
328
+ .toSet();
329
+ expect(kinds, contains('bridge-ipc'),
330
+ reason: 'up-leg transport attributed as a span');
331
+ expect(kinds, contains('response-delivery'),
332
+ reason: 'post-ack down-leg attributed as a span');
333
+ await bridge.dispose();
334
+ });
335
+
336
+ test('a runner without timing (older bundle) emits no bridge-leg spans and still answers', () async {
337
+ final transport =
338
+ ReplayTransport({'head': 'H1', 'created': <Object?>[], 'workspace': 'ws'});
339
+ final bridge = NomosBridge(transport);
340
+ await bridge.ready;
341
+ final out = await bridge.offerOutcome(
342
+ domain: 'guestbook', directiveId: 'signGuestbook', payload: {'author': 'ada'});
343
+ expect(out.head, 'H1');
344
+ final kinds = bridge.recentSpans
345
+ .map((m) => ((m['span'] as Map?)?['kind'] ?? '').toString())
346
+ .toSet();
347
+ expect(kinds, isNot(contains('bridge-ipc')));
348
+ await bridge.dispose();
349
+ });
350
+ });
351
+
310
352
  late FakeRunner runner;
311
353
  late NomosBridge bridge;
312
354
  setUp(() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.75.1",
3
+ "version": "0.76.0",
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",
@@ -928,7 +928,7 @@ const NOMOS_TYPES_DEP = "^0.3.0";
928
928
  // 0.48.x: session-bound clients (XClient.bind/NomosClientInterface/DomainClientSessionMismatch/
929
929
  // DomainNotInstalledForSession, engine.follow, pendingConflicts lanes) — the generated client
930
930
  // REQUIRES this surface. 0.48.1 fixes NomosSession.watchPendingConflicts delegation.
931
- const NOMOS_CLIENT_DEP = "^0.51.0";
931
+ const NOMOS_CLIENT_DEP = "^0.52.0";
932
932
  // DRIFT TRIPWIRE: pubspec_pins.test.ts asserts these equal the CURRENT versions in dsl/dart/pubspec.yaml
933
933
  // and dsl/dart_types/pubspec.yaml — a Dart release without this bump fails the suite, so tenants never
934
934
  // land back in dependency_overrides (the exact co2 report this closes, third occurrence).