@githolon/dsl 0.75.1 → 0.77.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/watch_after_offer_stall_test.dart +98 -0
- package/dart/test/webview_bridge_test.dart +42 -0
- package/package.json +1 -1
- 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
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// REGRESSION (co2, 0.76 offer-ack decoupling): the generated watch must emit the locally
|
|
2
|
+
// committed projection promptly after an offer's acknowledgement — with NO subsequent bridge op.
|
|
3
|
+
// co2's Flutter AppSim (NodeRunnerTransport → node-entry → realm sessions → watchById → Riverpod)
|
|
4
|
+
// saw the post-offer tick NEVER arrive after upgrading to @githolon/client 0.76 / nomos_client 0.52.
|
|
5
|
+
@TestOn('vm')
|
|
6
|
+
library;
|
|
7
|
+
|
|
8
|
+
import 'dart:async';
|
|
9
|
+
import 'dart:io';
|
|
10
|
+
|
|
11
|
+
import 'package:nomos_client/nomos_headless.dart';
|
|
12
|
+
import 'package:test/test.dart';
|
|
13
|
+
|
|
14
|
+
String get _root => '${Directory.current.path}/../..';
|
|
15
|
+
String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.mjs';
|
|
16
|
+
String get _gbDeploy => '$_root/examples/guestbook/build/guestbook.deploy.json';
|
|
17
|
+
String get _runtimeDir =>
|
|
18
|
+
Platform.environment['NOMOS_DART_RUNTIME'] ??
|
|
19
|
+
'${Platform.environment['HOME'] ?? ''}/.holon/runtime';
|
|
20
|
+
|
|
21
|
+
String? _missingPrereq() {
|
|
22
|
+
if (!File(_repoEntry).existsSync()) return 'in-repo runner not found at $_repoEntry';
|
|
23
|
+
if (!File(_gbDeploy).existsSync()) {
|
|
24
|
+
return 'missing $_gbDeploy — compile it (cd examples/guestbook && npx nomos-compile)';
|
|
25
|
+
}
|
|
26
|
+
if (!File('$_runtimeDir/holon.wasm').existsSync() ||
|
|
27
|
+
!File('$_runtimeDir/packages.json').existsSync()) {
|
|
28
|
+
return 'warm ~/.holon/runtime first (run any `githolon` command once)';
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
void main() {
|
|
34
|
+
test('watch emits the post-offer projection under COMPLETE SILENCE (no follow-up ops)',
|
|
35
|
+
timeout: const Timeout(Duration(minutes: 4)), () async {
|
|
36
|
+
final missing = _missingPrereq();
|
|
37
|
+
if (missing != null) {
|
|
38
|
+
// ignore: avoid_print
|
|
39
|
+
print('SKIP: $missing');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
final harness = await NomosTestHarness.ephemeral(
|
|
43
|
+
deployJsonPaths: [_gbDeploy],
|
|
44
|
+
workspace: 'stall-ws',
|
|
45
|
+
nodeEntry: _repoEntry,
|
|
46
|
+
resolveFrom: '$_root/cloud/web-client',
|
|
47
|
+
runtimeDir: _runtimeDir,
|
|
48
|
+
onDiagnostic: (l) {}, // keep output small
|
|
49
|
+
);
|
|
50
|
+
addTearDown(harness.dispose);
|
|
51
|
+
final s = harness.bridge;
|
|
52
|
+
|
|
53
|
+
// 1. create one entry, find its id.
|
|
54
|
+
await s.offerDirective(
|
|
55
|
+
domain: 'guestbook',
|
|
56
|
+
directiveId: 'signGuestbook',
|
|
57
|
+
payload: {
|
|
58
|
+
'author': 'ada',
|
|
59
|
+
'message': 'hi',
|
|
60
|
+
'mood': 'delighted',
|
|
61
|
+
'signedAt': DateTime.now().toUtc().toIso8601String(),
|
|
62
|
+
});
|
|
63
|
+
final rows = await s.query('entriesByAuthor', {'author': 'ada'});
|
|
64
|
+
expect(rows.length, 1);
|
|
65
|
+
final id = (rows.first as Map)['id'] as String;
|
|
66
|
+
|
|
67
|
+
// 2. subscribe (the app's watchById lane) and wait for the initial tick.
|
|
68
|
+
final ticks = <List<dynamic>>[];
|
|
69
|
+
final gotInitial = Completer<void>();
|
|
70
|
+
final gotMutated = Completer<void>();
|
|
71
|
+
final sub = s.watchById(id).listen((r) {
|
|
72
|
+
ticks.add(r);
|
|
73
|
+
if (!gotInitial.isCompleted) {
|
|
74
|
+
gotInitial.complete();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
final tags = ((r.first as Map)['data'] as Map)['tags'];
|
|
78
|
+
if (tags is List && tags.contains('t-after') && !gotMutated.isCompleted) {
|
|
79
|
+
gotMutated.complete();
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
addTearDown(sub.cancel);
|
|
83
|
+
await gotInitial.future.timeout(const Duration(seconds: 20),
|
|
84
|
+
onTimeout: () => throw StateError('initial watch tick never arrived'));
|
|
85
|
+
|
|
86
|
+
// 3. THE TEST — mutate the watched row, then COMPLETE SILENCE: no further bridge ops.
|
|
87
|
+
// The post-ack deferred nudge (0.76) must still deliver the tick.
|
|
88
|
+
await s.offerDirective(
|
|
89
|
+
domain: 'guestbook',
|
|
90
|
+
directiveId: 'tagEntry',
|
|
91
|
+
payload: {'entryId': id, 'tags': ['t-after']});
|
|
92
|
+
await gotMutated.future.timeout(const Duration(seconds: 8), onTimeout: () {
|
|
93
|
+
throw StateError(
|
|
94
|
+
'REGRESSION: the post-offer watch tick never arrived under silence '
|
|
95
|
+
'(ticks seen: ${ticks.length})');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
@@ -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.77.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.
|
|
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).
|