@githolon/dsl 0.75.0 → 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 +15 -0
- package/dart/lib/src/webview_bridge.dart +24 -0
- package/dart/pubspec.yaml +1 -1
- package/dart/test/local_sync_inert_test.dart +97 -0
- package/dart/test/webview_bridge_test.dart +42 -0
- package/package.json +1 -1
- package/src/codegen_dart_client.ts +10 -5
- package/src/compile_package_main.ts +1 -1
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.
|
|
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.
|
|
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",
|
|
@@ -627,7 +627,11 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
627
627
|
const targetInThisDomain = aggById.has(targetType) || owner?.name === thisDomainName;
|
|
628
628
|
if (!targetInThisDomain) {
|
|
629
629
|
if (owner === undefined) continue; // target not in this package — unreachable, skip the resolver
|
|
630
|
-
|
|
630
|
+
// LAYERED-packages mode: the sibling domain's TYPES file lives in the TYPES package, not
|
|
631
|
+
// beside this client file — a relative `<name>.dart` import points at a nonexistent path in
|
|
632
|
+
// the client package (the co2 0.75.0 regression). The types-barrel import above already
|
|
633
|
+
// re-exports every generated concrete class, so the extra import is only needed FLAT.
|
|
634
|
+
if (opts.typesImportUri === undefined) addImport(`${owner.name}.dart`, concreteClassName(targetType));
|
|
631
635
|
}
|
|
632
636
|
const targetConcrete = concreteClassName(targetType);
|
|
633
637
|
const member = dartMemberName(fieldName);
|
|
@@ -735,10 +739,11 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
735
739
|
// them (the by-id accessors reference them) and re-export them (once per package, Dart de-dups).
|
|
736
740
|
...(opts.typesImportUri === undefined && opts.frameworkImportUri !== undefined ? [`import '${opts.frameworkImportUri}';`] : []),
|
|
737
741
|
// CROSS-DOMAIN REF TRAVERSAL (§2a follow-on): a cross-domain resolver references a SIBLING domain's
|
|
738
|
-
// generated client class + read model (from `<name>.client.dart
|
|
739
|
-
//
|
|
740
|
-
//
|
|
741
|
-
//
|
|
742
|
+
// generated client class + read model (from `<name>.client.dart`, co-located with this file in BOTH
|
|
743
|
+
// modes) or — FLAT MODE ONLY — a foreignRef target's concrete class from the sibling TYPES
|
|
744
|
+
// `<name>.dart` (in layered mode that file lives in the TYPES package and the types-barrel import
|
|
745
|
+
// above already re-exports it; emitting the relative import there was the co2 0.75.0 regression).
|
|
746
|
+
// Empty for a domain with no cross-domain ref (byte-identical to before this existed).
|
|
742
747
|
...[...crossDomainImports.entries()]
|
|
743
748
|
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
744
749
|
.map(([uri, names]) => `import '${uri}' show ${[...names].sort().join(", ")};`),
|
|
@@ -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.
|
|
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).
|