@githolon/dsl 0.52.0 → 0.54.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/compat_package.mjs +46 -0
- package/dart/lib/src/webview_bridge.dart +160 -13
- package/dart/pubspec.yaml +1 -1
- package/dart/test/co2_acceptance_test.dart +102 -0
- package/dart/test/compat_test.dart +61 -0
- package/dart/test/vo_read_typing_test.dart +113 -0
- package/package.json +2 -1
- package/src/authoring.ts +77 -17
- package/src/codegen_dart.ts +94 -12
- package/src/codegen_dart_client.ts +18 -1
- package/src/codegen_openapi.ts +1 -1
- package/src/codegen_proof.ts +113 -16
- package/src/codegen_ts.ts +87 -12
- package/src/compat_package_main.ts +187 -0
- package/src/compile_package_main.ts +11 -1
- package/src/fields.ts +30 -5
- package/src/index.ts +13 -0
- package/src/law_check.ts +132 -1
- package/src/ops.ts +25 -6
- package/src/path.ts +115 -0
- package/src/query.ts +18 -9
- package/src/shape_compat.ts +46 -0
- package/src/spatial.ts +8 -4
- package/src/value_object.ts +119 -18
- package/src/wire_encode.ts +57 -9
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
|
|
3
|
+
// remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
|
|
4
|
+
// wasm32-wasip1 artifact {kernel · projection · embedded QuickJS engine}.
|
|
5
|
+
// If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
6
|
+
|
|
7
|
+
// `githolon compat` launcher — the twin of `compile_package.mjs`: resolve `tsx`
|
|
8
|
+
// (the caller's, else the DSL's own dependency) and run `src/compat_package_main.ts`
|
|
9
|
+
// under it (the main dynamically imports the caller's TS domain modules, which
|
|
10
|
+
// needs the tsx ESM loader). Resolution goes through Node's resolver (createRequire),
|
|
11
|
+
// NOT node_modules/.bin paths — hoisted/workspace installs put the bin shims elsewhere.
|
|
12
|
+
import { execFileSync } from "node:child_process";
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import { createRequire } from "node:module";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
17
|
+
|
|
18
|
+
const DSL_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const MAIN = path.join(DSL_DIR, "src", "compat_package_main.ts");
|
|
20
|
+
|
|
21
|
+
function resolvePkgDir(name, fromDir) {
|
|
22
|
+
try {
|
|
23
|
+
const req = createRequire(pathToFileURL(path.join(fromDir, "noop.js")));
|
|
24
|
+
return path.dirname(req.resolve(`${name}/package.json`));
|
|
25
|
+
} catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const tsxDir = resolvePkgDir("tsx", process.cwd()) ?? resolvePkgDir("tsx", DSL_DIR);
|
|
31
|
+
if (!tsxDir) {
|
|
32
|
+
console.error("githolon compat: tsx not found — reinstall @githolon/dsl (tsx is one of its dependencies)");
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
const tsxPkg = JSON.parse(readFileSync(path.join(tsxDir, "package.json"), "utf8"));
|
|
36
|
+
const tsxBinRel = typeof tsxPkg.bin === "string" ? tsxPkg.bin : tsxPkg.bin?.tsx;
|
|
37
|
+
const tsxCli = path.join(tsxDir, tsxBinRel ?? "dist/cli.mjs");
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
execFileSync(process.execPath, [tsxCli, MAIN, ...process.argv.slice(2)], {
|
|
41
|
+
stdio: "inherit",
|
|
42
|
+
cwd: process.cwd(),
|
|
43
|
+
});
|
|
44
|
+
} catch (e) {
|
|
45
|
+
process.exit(e.status ?? 1);
|
|
46
|
+
}
|
|
@@ -276,21 +276,122 @@ class NomosCreated {
|
|
|
276
276
|
String toString() => id;
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
-
/// A
|
|
280
|
-
///
|
|
279
|
+
/// A bounding box for spatial reads — the R*Tree probe's extent. Spatial is a GENERIC 2D index,
|
|
280
|
+
/// so the CANONICAL axes are axis-neutral (`[minX, minY] .. [maxX, maxY]`); the axes' meaning
|
|
281
|
+
/// (earth coordinates, floor-plan mm, anything) is the tenant's business. The original geographic
|
|
282
|
+
/// `minLng`/`minLat`/`maxLng`/`maxLat` named params remain accepted forever (era rule) — pass either
|
|
283
|
+
/// shape. On the wire `toJson()` emits BOTH key sets, so any holon runtime (old or new) reads it.
|
|
281
284
|
class NomosBbox {
|
|
282
|
-
final double
|
|
283
|
-
final double
|
|
284
|
-
final double
|
|
285
|
-
final double
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
285
|
+
final double minX;
|
|
286
|
+
final double minY;
|
|
287
|
+
final double maxX;
|
|
288
|
+
final double maxY;
|
|
289
|
+
|
|
290
|
+
/// Construct from either the canonical generic axes (`minX`/`minY`/`maxX`/`maxY`) OR the
|
|
291
|
+
/// geographic aliases (`minLng`/`minLat`/`maxLng`/`maxLat`). Provide exactly one shape per axis.
|
|
292
|
+
NomosBbox({
|
|
293
|
+
double? minX,
|
|
294
|
+
double? minY,
|
|
295
|
+
double? maxX,
|
|
296
|
+
double? maxY,
|
|
297
|
+
double? minLng,
|
|
298
|
+
double? minLat,
|
|
299
|
+
double? maxLng,
|
|
300
|
+
double? maxLat,
|
|
301
|
+
}) : minX = (minX ?? minLng)!,
|
|
302
|
+
minY = (minY ?? minLat)!,
|
|
303
|
+
maxX = (maxX ?? maxLng)!,
|
|
304
|
+
maxY = (maxY ?? maxLat)!;
|
|
305
|
+
|
|
306
|
+
/// Deprecated geographic accessors — spatial axes are generic. Prefer [minX]/[minY]/[maxX]/[maxY].
|
|
307
|
+
@Deprecated('Spatial axes are generic; use minX/minY. Kept for source compat.')
|
|
308
|
+
double get minLng => minX;
|
|
309
|
+
@Deprecated('Spatial axes are generic; use minX/minY. Kept for source compat.')
|
|
310
|
+
double get minLat => minY;
|
|
311
|
+
@Deprecated('Spatial axes are generic; use maxX/maxY. Kept for source compat.')
|
|
312
|
+
double get maxLng => maxX;
|
|
313
|
+
@Deprecated('Spatial axes are generic; use maxX/maxY. Kept for source compat.')
|
|
314
|
+
double get maxLat => maxY;
|
|
315
|
+
|
|
316
|
+
Map<String, Object?> toJson() => {
|
|
317
|
+
'minX': minX,
|
|
318
|
+
'minY': minY,
|
|
319
|
+
'maxX': maxX,
|
|
320
|
+
'maxY': maxY,
|
|
321
|
+
// Geographic aliases so a pre-genericisation runner (which reads minLng/…) still works.
|
|
322
|
+
'minLng': minX,
|
|
323
|
+
'minLat': minY,
|
|
324
|
+
'maxLng': maxX,
|
|
325
|
+
'maxLat': maxY,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/// One member's structural compat verdict — `ok`, plus the exact break [reason] when not.
|
|
330
|
+
class NomosCompatMember {
|
|
331
|
+
final bool ok;
|
|
332
|
+
final String? reason;
|
|
333
|
+
const NomosCompatMember({required this.ok, this.reason});
|
|
334
|
+
factory NomosCompatMember.fromJson(Map<String, Object?> m) =>
|
|
335
|
+
NomosCompatMember(ok: m['ok'] == true, reason: m['reason'] as String?);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/// THE COMPAT SUPERPOWER verdict (#72b) — the Dart mirror of the JS `holon.compat`. Rolls the
|
|
339
|
+
/// per-member structural verdicts up to a three-state [verdict], carries the deployed law's install
|
|
340
|
+
/// phase, and exposes the dark-launch feature flag [has]. The pre-call refusal (a KNOWN-incompatible
|
|
341
|
+
/// directive throwing [LawMismatchError]) lives in the JS runtime; this is the read surface the app
|
|
342
|
+
/// branches on. Kept immutable + JSON-round-trippable so it crosses the bridge and unit-tests bare.
|
|
343
|
+
class NomosCompat {
|
|
344
|
+
final String generatedFor;
|
|
345
|
+
final List<String> workspaceRuns;
|
|
346
|
+
final bool matches;
|
|
347
|
+
final bool offerKnown;
|
|
348
|
+
final bool? structurallyCompatible;
|
|
349
|
+
|
|
350
|
+
/// The three-state roll-up: `compatible` | `degraded` | `incompatible`.
|
|
351
|
+
final String verdict;
|
|
352
|
+
|
|
353
|
+
/// Each directive id (writes) / aggregate label (reads) → its own `{ ok, reason? }`.
|
|
354
|
+
final Map<String, NomosCompatMember> members;
|
|
355
|
+
|
|
356
|
+
/// The deployed law's lifecycle phase (Active / Superseded / Disabled / …) + the succeeding hash.
|
|
357
|
+
final String installPhase;
|
|
358
|
+
final String? supersededBy;
|
|
359
|
+
|
|
360
|
+
const NomosCompat({
|
|
361
|
+
required this.generatedFor,
|
|
362
|
+
required this.workspaceRuns,
|
|
363
|
+
required this.matches,
|
|
364
|
+
required this.offerKnown,
|
|
365
|
+
required this.structurallyCompatible,
|
|
366
|
+
required this.verdict,
|
|
367
|
+
required this.members,
|
|
368
|
+
required this.installPhase,
|
|
369
|
+
this.supersededBy,
|
|
291
370
|
});
|
|
292
|
-
|
|
293
|
-
|
|
371
|
+
|
|
372
|
+
/// THE FEATURE FLAG: true iff the member exists on the offer AND is structurally compatible.
|
|
373
|
+
bool has(String memberId) => members[memberId]?.ok == true;
|
|
374
|
+
|
|
375
|
+
static NomosCompat? fromJson(Map<String, Object?>? m) {
|
|
376
|
+
if (m == null) return null;
|
|
377
|
+
final rawMembers = (m['members'] as Map?)?.cast<String, Object?>() ?? const {};
|
|
378
|
+
final members = <String, NomosCompatMember>{};
|
|
379
|
+
rawMembers.forEach((k, v) {
|
|
380
|
+
if (v is Map) members[k] = NomosCompatMember.fromJson(v.cast<String, Object?>());
|
|
381
|
+
});
|
|
382
|
+
final install = (m['install'] as Map?)?.cast<String, Object?>() ?? const {};
|
|
383
|
+
return NomosCompat(
|
|
384
|
+
generatedFor: (m['generatedFor'] as String?) ?? '',
|
|
385
|
+
workspaceRuns: ((m['workspaceRuns'] as List?) ?? const []).map((e) => '$e').toList(),
|
|
386
|
+
matches: m['matches'] == true,
|
|
387
|
+
offerKnown: m['offerKnown'] == true,
|
|
388
|
+
structurallyCompatible: m['structurallyCompatible'] as bool?,
|
|
389
|
+
verdict: (m['verdict'] as String?) ?? 'degraded',
|
|
390
|
+
members: members,
|
|
391
|
+
installPhase: (install['phase'] as String?) ?? 'Unknown',
|
|
392
|
+
supersededBy: install['supersededBy'] as String?,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
294
395
|
}
|
|
295
396
|
|
|
296
397
|
// ── E2E SEALING (the sealing client) — the GENERIC key-custody surface. Generated domain clients bake the
|
|
@@ -680,6 +781,10 @@ class NomosBridge {
|
|
|
680
781
|
final Map<String, StreamController<List<dynamic>>> _watches = {};
|
|
681
782
|
// SHAPE COMPATIBILITY (#72): compat bindings emit a verdict MAP, not a row list.
|
|
682
783
|
final Map<String, StreamController<Map<String, Object?>>> _compatWatches = {};
|
|
784
|
+
// COMPAT SUPERPOWER (#72b): the last computed verdict + the live `lawChanged` event stream.
|
|
785
|
+
NomosCompat? _lastCompat;
|
|
786
|
+
final StreamController<NomosCompat> _lawChanged =
|
|
787
|
+
StreamController<NomosCompat>.broadcast();
|
|
683
788
|
final StreamController<Map<String, Object?>> _syncEvents =
|
|
684
789
|
StreamController<Map<String, Object?>>.broadcast();
|
|
685
790
|
final StreamController<Map<String, Object?>> _signalEvents =
|
|
@@ -765,6 +870,16 @@ class NomosBridge {
|
|
|
765
870
|
}
|
|
766
871
|
return;
|
|
767
872
|
}
|
|
873
|
+
if (event == 'lawChanged') {
|
|
874
|
+
// COMPAT SUPERPOWER (#72b): a synced install/upgrade/retire moved the workspace's offer under this
|
|
875
|
+
// live client. Cache the fresh verdict + emit it so the app re-checks feature flags without polling.
|
|
876
|
+
final c = NomosCompat.fromJson((m['compat'] as Map?)?.cast<String, Object?>());
|
|
877
|
+
if (c != null) {
|
|
878
|
+
_lastCompat = c;
|
|
879
|
+
if (!_lawChanged.isClosed) _lawChanged.add(c);
|
|
880
|
+
}
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
768
883
|
if (event == 'sync' || event == 'link') {
|
|
769
884
|
// autoSync telemetry: each round { event:'sync', reason, result } and socket state { event:'link', status }.
|
|
770
885
|
if (!_syncEvents.isClosed) _syncEvents.add(m);
|
|
@@ -1391,6 +1506,38 @@ class NomosBridge {
|
|
|
1391
1506
|
if (offer != null) 'offer': offer,
|
|
1392
1507
|
}));
|
|
1393
1508
|
|
|
1509
|
+
/// THE COMPAT SUPERPOWER (#72b): compute the LIVE per-member verdict for this client's baked
|
|
1510
|
+
/// [compat] manifest + structural [requirement] against the workspace's on-ledger offer. The
|
|
1511
|
+
/// generated Dart client factory calls this at bind, so a KNOWN-incompatible directive refuses
|
|
1512
|
+
/// CLIENT-SIDE at write time ([LawMismatchError] surfaced through `offerDirective`) — the JS
|
|
1513
|
+
/// runtime holds the verdict + the pre-call guard, and a Dart offer through this bridge is guarded
|
|
1514
|
+
/// too. Returns the verdict (also cached on [compat]) with per-member `{ ok, reason? }`.
|
|
1515
|
+
Future<NomosCompat?> checkCompat(Map<String, Object?> compat,
|
|
1516
|
+
[Map<String, Object?>? requirement]) async {
|
|
1517
|
+
final r = _asMap(await _call('checkCompat', {
|
|
1518
|
+
'compat': compat,
|
|
1519
|
+
if (requirement != null) 'requirement': requirement,
|
|
1520
|
+
}));
|
|
1521
|
+
final v = NomosCompat.fromJson((r['compat'] as Map?)?.cast<String, Object?>());
|
|
1522
|
+
if (v != null) _lastCompat = v;
|
|
1523
|
+
return v;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
/// The last computed compat verdict (set by [checkCompat] / a `lawChanged` event). Null until bind.
|
|
1527
|
+
NomosCompat? get compat => _lastCompat;
|
|
1528
|
+
|
|
1529
|
+
/// THE FEATURE FLAG (dark-launch): true iff [member] exists on the workspace's offer AND is
|
|
1530
|
+
/// structurally compatible. Ship UI with a member dark, deploy the law, it lights up — no redeploy.
|
|
1531
|
+
/// Re-derives via the JS runtime so a synced law change is reflected without a fresh [checkCompat].
|
|
1532
|
+
Future<bool> compatHas(String member) async {
|
|
1533
|
+
final r = _asMap(await _call('compatHas', {'member': member}));
|
|
1534
|
+
return r['has'] == true;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
/// COMPAT SUPERPOWER live events: emits the fresh verdict whenever a synced install/upgrade/retire
|
|
1538
|
+
/// moves the workspace's offer under this live client. The app re-checks feature flags reactively.
|
|
1539
|
+
Stream<NomosCompat> get onLawChanged => _lawChanged.stream;
|
|
1540
|
+
|
|
1394
1541
|
/// SHAPE COMPATIBILITY as a LIVE BINDING (#72): a stream of verdicts that re-emits whenever the
|
|
1395
1542
|
/// workspace's installed law changes (another peer installs/upgrades/retires a domain and it
|
|
1396
1543
|
/// syncs in). The verdict carries `offerKnown`. The app degrades/prompts on `ok:false`.
|
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.34.0
|
|
15
15
|
|
|
16
16
|
environment:
|
|
17
17
|
sdk: ^3.5.0
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// THE CO2 ACCEPTANCE — DART LEG (task #32). Their feedback's Dart snippet, near-verbatim:
|
|
2
|
+
// construct a rich nested DrawingGeoreference with typed constructors, dispatch it through the
|
|
3
|
+
// generated client against REAL law on a local holon, and read BOTH aggregates back typed —
|
|
4
|
+
// the primary georeference row AND the sibling EstateDrawing.transform write.
|
|
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/co2-acceptance/build/dart/src/generated/estate.dart';
|
|
14
|
+
import '../../../bench/scale/fixtures/co2-acceptance/build/dart/src/generated/estate.client.dart';
|
|
15
|
+
|
|
16
|
+
String get _root => '${Directory.current.path}/../..';
|
|
17
|
+
String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.mjs';
|
|
18
|
+
String get _deployJson =>
|
|
19
|
+
'$_root/bench/scale/fixtures/co2-acceptance/build/co2-acceptance.deploy.json';
|
|
20
|
+
String get _runtimeDir => '${Platform.environment['HOME'] ?? ''}/.holon/runtime';
|
|
21
|
+
|
|
22
|
+
String? _missingPrereq() {
|
|
23
|
+
if (!File(_repoEntry).existsSync()) return 'in-repo runner not found at $_repoEntry';
|
|
24
|
+
if (!File(_deployJson).existsSync()) {
|
|
25
|
+
return 'compile the fixture: cd bench/scale/fixtures/co2-acceptance && npx nomos-compile';
|
|
26
|
+
}
|
|
27
|
+
if (!File('$_runtimeDir/holon.wasm').existsSync() ||
|
|
28
|
+
!File('$_runtimeDir/packages.json').existsSync()) {
|
|
29
|
+
return 'warm ~/.holon/runtime first (run any `githolon` command once)';
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
void main() {
|
|
35
|
+
test('co2 acceptance: rich typed VO construction → multi-aggregate write → typed reads',
|
|
36
|
+
() async {
|
|
37
|
+
final skip = _missingPrereq();
|
|
38
|
+
if (skip != null) {
|
|
39
|
+
markTestSkipped(skip);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
final harness = await NomosTestHarness.ephemeral(
|
|
43
|
+
deployJsonPaths: [_deployJson],
|
|
44
|
+
nodeEntry: _repoEntry,
|
|
45
|
+
runtimeDir: _runtimeDir,
|
|
46
|
+
workspace: 'co2-acceptance',
|
|
47
|
+
);
|
|
48
|
+
try {
|
|
49
|
+
final estate = EstateClient(harness.bridge, actor: 'user:ada');
|
|
50
|
+
|
|
51
|
+
// Register a site whose location composes address + geo point (nested VOs).
|
|
52
|
+
await estate.registerSite(RegisterSitePayload(
|
|
53
|
+
name: 'Trafford Depot',
|
|
54
|
+
location: SiteLocation(
|
|
55
|
+
address: PostalAddress(
|
|
56
|
+
line1: '1 Wharf Rd', city: 'Manchester', postcode: 'M17 1AB', countryCode: 'GB'),
|
|
57
|
+
point: GeoPoint(coordinates: [-2.24, 53.48]),
|
|
58
|
+
source: SiteLocationSource.addressSearch,
|
|
59
|
+
),
|
|
60
|
+
registeredAt: DateTime.now().toUtc().toIso8601String(),
|
|
61
|
+
));
|
|
62
|
+
await estate.addDrawing(AddDrawingPayload(
|
|
63
|
+
title: 'Level 2 plan', addedAt: DateTime.now().toUtc().toIso8601String()));
|
|
64
|
+
|
|
65
|
+
final drawings = await estate.drawingsByTitle(title: 'Level 2 plan');
|
|
66
|
+
expect(drawings, hasLength(1));
|
|
67
|
+
final drawingId = drawings.single.aggregateId.value;
|
|
68
|
+
final sites = await estate.sitesByName(name: 'Trafford Depot');
|
|
69
|
+
expect(sites, hasLength(1));
|
|
70
|
+
|
|
71
|
+
// ── THEIR SNIPPET, VERBATIM SHAPE ──
|
|
72
|
+
await estate.recordDrawingGeoreference(RecordDrawingGeoreferencePayload(
|
|
73
|
+
drawingId: drawingId,
|
|
74
|
+
siteId: sites.single.aggregateId.value,
|
|
75
|
+
georeference: DrawingGeoreference(
|
|
76
|
+
transform: DrawingTransform2d(rotationDeg: 2.4),
|
|
77
|
+
controlPoints: [
|
|
78
|
+
DrawingGeoControlPoint(
|
|
79
|
+
drawingPosition: DrawingPositionMm(xMm: 1200, yMm: 800),
|
|
80
|
+
geoPoint: GeoPoint(coordinates: [-2.24, 53.48]),
|
|
81
|
+
),
|
|
82
|
+
],
|
|
83
|
+
),
|
|
84
|
+
recordedAt: DateTime.now().toUtc().toIso8601String(),
|
|
85
|
+
));
|
|
86
|
+
|
|
87
|
+
// Primary aggregate: the full nested VO reads back TYPED.
|
|
88
|
+
final georefs = await estate.georefsByDrawing(drawingId: drawingId);
|
|
89
|
+
expect(georefs, hasLength(1));
|
|
90
|
+
final g = georefs.single.georeference!;
|
|
91
|
+
expect(g.transform!.rotationDeg, 2.4);
|
|
92
|
+
expect(g.controlPoints.single.drawingPosition.xMm, 1200);
|
|
93
|
+
expect(g.controlPoints.single.geoPoint.coordinates.first, -2.24);
|
|
94
|
+
|
|
95
|
+
// THE SIBLING WRITE: EstateDrawing.transform got the VO subfield, typed.
|
|
96
|
+
final again = await estate.drawingsByTitle(title: 'Level 2 plan');
|
|
97
|
+
expect(again.single.transform!.rotationDeg, 2.4);
|
|
98
|
+
} finally {
|
|
99
|
+
await harness.dispose();
|
|
100
|
+
}
|
|
101
|
+
}, timeout: const Timeout(Duration(minutes: 3)));
|
|
102
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// COMPAT SUPERPOWER (#72b) — the Dart mirror of `holon.compat`. NomosCompat.fromJson round-trips
|
|
2
|
+
// the bridge's plain verdict snapshot; has() is the dark-launch feature flag over the per-member map.
|
|
3
|
+
import 'package:nomos_client/nomos_client.dart';
|
|
4
|
+
import 'package:test/test.dart';
|
|
5
|
+
|
|
6
|
+
Map<String, Object?> verdict({
|
|
7
|
+
required String v,
|
|
8
|
+
required Map<String, Object?> members,
|
|
9
|
+
String phase = 'Active',
|
|
10
|
+
String? supersededBy,
|
|
11
|
+
}) =>
|
|
12
|
+
{
|
|
13
|
+
'generatedFor': 'a' * 64,
|
|
14
|
+
'workspaceRuns': ['b' * 64],
|
|
15
|
+
'matches': false,
|
|
16
|
+
'offerKnown': true,
|
|
17
|
+
'structurallyCompatible': v != 'incompatible',
|
|
18
|
+
'verdict': v,
|
|
19
|
+
'members': members,
|
|
20
|
+
'install': {'phase': phase, if (supersededBy != null) 'supersededBy': supersededBy},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
void main() {
|
|
24
|
+
test('fromJson parses the three-state verdict + per-member map + install phase', () {
|
|
25
|
+
final c = NomosCompat.fromJson(verdict(
|
|
26
|
+
v: 'degraded',
|
|
27
|
+
members: {
|
|
28
|
+
'writeNote': {'ok': true},
|
|
29
|
+
'Note': {'ok': true},
|
|
30
|
+
},
|
|
31
|
+
phase: 'Superseded',
|
|
32
|
+
supersededBy: 'c' * 64,
|
|
33
|
+
))!;
|
|
34
|
+
expect(c.verdict, 'degraded');
|
|
35
|
+
expect(c.offerKnown, isTrue);
|
|
36
|
+
expect(c.structurallyCompatible, isTrue);
|
|
37
|
+
expect(c.installPhase, 'Superseded');
|
|
38
|
+
expect(c.supersededBy, 'c' * 64);
|
|
39
|
+
expect(c.members.keys, containsAll(['writeNote', 'Note']));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('has() is the feature flag — true for a compatible member, false otherwise', () {
|
|
43
|
+
final c = NomosCompat.fromJson(verdict(
|
|
44
|
+
v: 'incompatible',
|
|
45
|
+
members: {
|
|
46
|
+
'writeNote': {'ok': false, 'reason': "workspace requires field 'sig' the client does not always send"},
|
|
47
|
+
'Note': {'ok': true},
|
|
48
|
+
},
|
|
49
|
+
))!;
|
|
50
|
+
// the served + compatible member lights up; the broken one + an unknown one stay dark
|
|
51
|
+
expect(c.has('Note'), isTrue);
|
|
52
|
+
expect(c.has('writeNote'), isFalse);
|
|
53
|
+
expect(c.has('somethingElse'), isFalse);
|
|
54
|
+
// the broken member still carries its reason (for the app to surface)
|
|
55
|
+
expect(c.members['writeNote']!.reason, contains('sig'));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('fromJson(null) is null (compat-less / pre-#72e)', () {
|
|
59
|
+
expect(NomosCompat.fromJson(null), isNull);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// RUNTIME PROOF (task #27) — read-side VO typing end-to-end. Install a REAL VO-bearing law
|
|
2
|
+
// (nested VO + map-of-VO) into a fresh LOCAL holon, write one create through the real engine,
|
|
3
|
+
// then read it back three ways — declared query, by-id, and a WATCH stream — parsing each
|
|
4
|
+
// through the GENERATED read model and asserting TYPED nested + map-of-VO access
|
|
5
|
+
// (`site.location!.point!.coordinates`, `site.rooms['a']!.address!.city`). No cloud, no network.
|
|
6
|
+
//
|
|
7
|
+
// Prereqs (skipped-with-reason when absent, so CI stays green): `node` on PATH; the in-repo
|
|
8
|
+
// runner; a warm `~/.holon/runtime`; and the compiled fixture deploy.json + generated Dart
|
|
9
|
+
// (`cd bench/scale/fixtures/vo-read-typing && npx nomos-compile`).
|
|
10
|
+
@TestOn('vm')
|
|
11
|
+
library;
|
|
12
|
+
|
|
13
|
+
import 'dart:io';
|
|
14
|
+
|
|
15
|
+
import 'package:nomos_client/nomos_client.dart';
|
|
16
|
+
import 'package:nomos_client/nomos_headless.dart';
|
|
17
|
+
import 'package:test/test.dart';
|
|
18
|
+
|
|
19
|
+
// The fixture's GENERATED Dart read models (support: "package" → imports package:nomos_client).
|
|
20
|
+
import '../../../bench/scale/fixtures/vo-read-typing/build/dart/src/generated/estate.dart';
|
|
21
|
+
|
|
22
|
+
String get _root => '${Directory.current.path}/../..';
|
|
23
|
+
String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.mjs';
|
|
24
|
+
String get _deployJson =>
|
|
25
|
+
'$_root/bench/scale/fixtures/vo-read-typing/build/vo-read-typing.deploy.json';
|
|
26
|
+
String get _runtimeDir => '${Platform.environment['HOME'] ?? ''}/.holon/runtime';
|
|
27
|
+
|
|
28
|
+
String? _missingPrereq() {
|
|
29
|
+
if (!File(_repoEntry).existsSync()) return 'in-repo runner not found at $_repoEntry';
|
|
30
|
+
if (!File(_deployJson).existsSync()) {
|
|
31
|
+
return 'compile the fixture: cd bench/scale/fixtures/vo-read-typing && npx nomos-compile';
|
|
32
|
+
}
|
|
33
|
+
if (!File('$_runtimeDir/holon.wasm').existsSync() ||
|
|
34
|
+
!File('$_runtimeDir/packages.json').existsSync()) {
|
|
35
|
+
return 'warm ~/.holon/runtime first (run any `githolon` command once)';
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
void main() {
|
|
41
|
+
test('OFFLINE: nested VO + map-of-VO read back TYPED via query, by-id, and watch', () async {
|
|
42
|
+
final skip = _missingPrereq();
|
|
43
|
+
if (skip != null) {
|
|
44
|
+
markTestSkipped(skip);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
final harness = await NomosTestHarness.ephemeral(
|
|
48
|
+
deployJsonPaths: [_deployJson],
|
|
49
|
+
nodeEntry: _repoEntry,
|
|
50
|
+
runtimeDir: _runtimeDir,
|
|
51
|
+
registerTearDown: addTearDown,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Author one Site through the REAL law: a nested VO `location` + a map-of-VO `rooms`.
|
|
55
|
+
final outcome = await harness.bridge.offerCreates(
|
|
56
|
+
domain: 'estate',
|
|
57
|
+
directiveId: 'placeSite',
|
|
58
|
+
payload: {
|
|
59
|
+
'name': 'HQ',
|
|
60
|
+
'location': {
|
|
61
|
+
'address': {'streetAddress': '1 High St', 'city': 'Leeds'},
|
|
62
|
+
'point': {'type': 'Point', 'coordinates': [-1.55, 53.8]},
|
|
63
|
+
},
|
|
64
|
+
'rooms': {
|
|
65
|
+
'atrium': {
|
|
66
|
+
'address': {'streetAddress': '1 High St', 'city': 'Leeds'},
|
|
67
|
+
'point': {'type': 'Point', 'coordinates': [-1.5, 53.79]},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
expect(outcome.created, hasLength(1));
|
|
73
|
+
final siteId = SiteId.fromRow(outcome.created.single.id);
|
|
74
|
+
|
|
75
|
+
// ── (1) declared query → typed Site, typed nested VO + typed map-of-VO ──
|
|
76
|
+
final qRows = await harness.bridge.query('sitesByName', {'name': 'HQ'});
|
|
77
|
+
expect(qRows, hasLength(1));
|
|
78
|
+
final fromQuery =
|
|
79
|
+
Site.fromJson((qRows.first['data'] as Map).cast<String, dynamic>(), (qRows.first['id'] ?? '').toString());
|
|
80
|
+
|
|
81
|
+
expect(fromQuery.name, 'HQ');
|
|
82
|
+
// Typed nested VO access — no map indexing, no casts.
|
|
83
|
+
expect(fromQuery.location, isA<SiteLocation>());
|
|
84
|
+
expect(fromQuery.location!.point, isA<GeoPoint>());
|
|
85
|
+
expect(fromQuery.location!.point!.coordinates, [-1.55, 53.8]);
|
|
86
|
+
expect(fromQuery.location!.address!.city, 'Leeds');
|
|
87
|
+
// Typed map-of-VO access: rooms is Map<String, SiteLocation>, NOT a raw map.
|
|
88
|
+
expect(fromQuery.rooms, isA<Map<String, SiteLocation>>());
|
|
89
|
+
expect(fromQuery.rooms['atrium'], isA<SiteLocation>());
|
|
90
|
+
expect(fromQuery.rooms['atrium']!.point!.coordinates, [-1.5, 53.79]);
|
|
91
|
+
expect(fromQuery.rooms['atrium']!.address!.city, 'Leeds');
|
|
92
|
+
|
|
93
|
+
// ── (2) by-id read → same typed shape ──
|
|
94
|
+
final byIdRows = await harness.bridge.queryById(siteId.value);
|
|
95
|
+
expect(byIdRows, hasLength(1));
|
|
96
|
+
final fromById =
|
|
97
|
+
Site.fromJson((byIdRows.first['data'] as Map).cast<String, dynamic>(), (byIdRows.first['id'] ?? '').toString());
|
|
98
|
+
expect(fromById.location!.point!.coordinates, [-1.55, 53.8]);
|
|
99
|
+
expect(fromById.rooms['atrium']!.point!.coordinates, [-1.5, 53.79]);
|
|
100
|
+
|
|
101
|
+
// ── (3) watch stream → typed rows, identical decode ──
|
|
102
|
+
final watched = await harness.bridge
|
|
103
|
+
.watchById(siteId.value)
|
|
104
|
+
.map((rows) => rows
|
|
105
|
+
.map((r) => Site.fromJson((r['data'] as Map).cast<String, dynamic>(), (r['id'] ?? '').toString()))
|
|
106
|
+
.toList())
|
|
107
|
+
.firstWhere((rows) => rows.isNotEmpty);
|
|
108
|
+
expect(watched, hasLength(1));
|
|
109
|
+
final fromWatch = watched.single;
|
|
110
|
+
expect(fromWatch.location!.point!.coordinates, [-1.55, 53.8]);
|
|
111
|
+
expect(fromWatch.rooms['atrium']!.address!.city, 'Leeds');
|
|
112
|
+
}, timeout: const Timeout(Duration(minutes: 2)));
|
|
113
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.54.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",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"!dart/build",
|
|
49
49
|
"compile_package.mjs",
|
|
50
50
|
"check_package.mjs",
|
|
51
|
+
"compat_package.mjs",
|
|
51
52
|
"lsp_server.mjs"
|
|
52
53
|
],
|
|
53
54
|
"publishConfig": {
|