@githolon/dsl 0.59.0 → 0.60.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 +6 -0
- package/dart/lib/src/test_harness.dart +29 -4
- package/dart/lib/src/webview_bridge.dart +51 -6
- package/dart/pubspec.yaml +1 -1
- package/dart/test/test_harness_test.dart +25 -0
- package/dart_types/LICENSE +45 -0
- package/dart_types/README.md +15 -0
- package/package.json +1 -1
- package/src/codegen_proof.ts +26 -19
package/dart/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## 0.39.0
|
|
2
|
+
- `connect(maxMounted: …)` — the realm's mounted-workspace budget (LRU park/remount above it); useful on memory-constrained devices holding many local workspaces.
|
|
3
|
+
- 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
|
+
- `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.
|
|
5
|
+
- 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
|
+
|
|
1
7
|
## 0.38.0
|
|
2
8
|
- SPLIT: the pure data core (wire types, offer codec, NomosReads, aggregate meta, schema validation, error info) moved to the new `nomos_types` package; `nomos_client` depends on and re-exports it, so existing imports are unchanged. Frontend/UI code can now depend on `nomos_types` alone.
|
|
3
9
|
|
|
@@ -43,8 +43,15 @@ class NomosTestHarnessException implements Exception {
|
|
|
43
43
|
String toString() => 'NomosTestHarnessException: $message';
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/// The decoy session [NomosTestHarness.ephemeral] connects alongside the workspace under test — it exists
|
|
47
|
+
/// so the plane is ALWAYS multi-session in tests (sole-session inference never silently passes).
|
|
48
|
+
const String _decoyWorkspace = '__nomos-decoy';
|
|
49
|
+
|
|
46
50
|
class NomosTestHarness {
|
|
47
51
|
/// The connected bridge — hand it to the generated typed clients (the same surface the app UI uses).
|
|
52
|
+
/// For [ephemeral] this is a [NomosSession] HANDLE pinned to [workspace] (the plane underneath also
|
|
53
|
+
/// holds a decoy session, so unpinned plane-level calls refuse the way they would in a real
|
|
54
|
+
/// multi-workspace app); for [cluster] it is the shared plane — name sessions via [session].
|
|
48
55
|
final NomosBridge bridge;
|
|
49
56
|
|
|
50
57
|
/// The local workspace name the law was installed into.
|
|
@@ -129,6 +136,7 @@ class NomosTestHarness {
|
|
|
129
136
|
void Function(Future<void> Function())? registerTearDown,
|
|
130
137
|
void Function(String line)? onDiagnostic,
|
|
131
138
|
Duration readyTimeout = const Duration(seconds: 30),
|
|
139
|
+
bool decoySession = true,
|
|
132
140
|
}) async {
|
|
133
141
|
if (deployJsonPaths.isEmpty) {
|
|
134
142
|
throw NomosTestHarnessException(
|
|
@@ -178,19 +186,36 @@ class NomosTestHarness {
|
|
|
178
186
|
},
|
|
179
187
|
onDiagnostic: onDiagnostic,
|
|
180
188
|
);
|
|
181
|
-
final
|
|
182
|
-
await
|
|
189
|
+
final plane = NomosBridge(transport);
|
|
190
|
+
await plane.ready.timeout(readyTimeout,
|
|
183
191
|
onTimeout: () => throw NomosTestHarnessException(
|
|
184
192
|
'the offline runner never signalled ready within $readyTimeout '
|
|
185
193
|
'(is `node` on PATH and ~/.holon/runtime warm?)'));
|
|
186
|
-
await
|
|
194
|
+
await plane.connect(
|
|
187
195
|
cloud: 'https://offline.invalid', // ignored: the offline runner serves everything locally
|
|
188
196
|
workspace: workspace,
|
|
189
197
|
clientId: clientId,
|
|
190
198
|
allowUnborn: true,
|
|
191
199
|
);
|
|
200
|
+
// THE DECOY SESSION (multi-workspace by default in tests): sole-session inference is how
|
|
201
|
+
// single-workspace assumptions ship — with one session connected, an op that forgot its session
|
|
202
|
+
// silently works in tests and only refuses in production when the app connects its second
|
|
203
|
+
// workspace. The decoy keeps the plane at ≥2 sessions so any unpinned op refuses HERE, in the
|
|
204
|
+
// dev's first test. [bridge] is a SESSION HANDLE pinned to [workspace], so tenant code written
|
|
205
|
+
// against the harness keeps working unchanged; only code that grabs the raw plane and assumes
|
|
206
|
+
// single-session trips. Opt out with `decoySession: false` (please don't).
|
|
207
|
+
if (decoySession) {
|
|
208
|
+
await plane.connect(
|
|
209
|
+
cloud: 'https://offline.invalid',
|
|
210
|
+
workspace: _decoyWorkspace,
|
|
211
|
+
session: _decoyWorkspace,
|
|
212
|
+
clientId: '$clientId-decoy',
|
|
213
|
+
allowUnborn: true,
|
|
214
|
+
restoreFrom: base64Encode(await File(snapshot).readAsBytes()),
|
|
215
|
+
);
|
|
216
|
+
}
|
|
192
217
|
final harness = NomosTestHarness._(
|
|
193
|
-
|
|
218
|
+
plane.session(workspace), workspace, transport, workdir, onDiagnostic, [workspace],
|
|
194
219
|
frameworkHash: info['frameworkHash']?.toString(),
|
|
195
220
|
lawHashes: {
|
|
196
221
|
workspace: ((info['lawHashes'] as List?) ?? const [])
|
|
@@ -626,6 +626,10 @@ class NomosBridge {
|
|
|
626
626
|
final Map<String, StreamController<Map<String, Object?>>> _compatWatches = {};
|
|
627
627
|
// COMPAT SUPERPOWER (#72b): the last computed verdict + the live `lawChanged` event stream.
|
|
628
628
|
NomosCompat? _lastCompat;
|
|
629
|
+
// per-session compat state — a multi-session plane syncs law changes on MANY workspaces; a
|
|
630
|
+
// [NomosSession]'s compat/onLawChanged must answer for ITS workspace, not whichever synced last.
|
|
631
|
+
final Map<String, NomosCompat> _lastCompatBySession = {};
|
|
632
|
+
final Map<String, StreamController<NomosCompat>> _lawChangedBySession = {};
|
|
629
633
|
final StreamController<NomosCompat> _lawChanged =
|
|
630
634
|
StreamController<NomosCompat>.broadcast();
|
|
631
635
|
final StreamController<Map<String, Object?>> _syncEvents =
|
|
@@ -728,7 +732,10 @@ class NomosBridge {
|
|
|
728
732
|
final c = NomosCompat.fromJson((m['compat'] as Map?)?.cast<String, Object?>());
|
|
729
733
|
if (c != null) {
|
|
730
734
|
_lastCompat = c;
|
|
735
|
+
final sess = m['session'] as String?;
|
|
736
|
+
if (sess != null) _lastCompatBySession[sess] = c;
|
|
731
737
|
if (!_lawChanged.isClosed) _lawChanged.add(c);
|
|
738
|
+
if (sess != null) _lawChangedBySession[sess]?.add(c);
|
|
732
739
|
}
|
|
733
740
|
return;
|
|
734
741
|
}
|
|
@@ -856,7 +863,8 @@ class NomosBridge {
|
|
|
856
863
|
String? restoreFrom,
|
|
857
864
|
bool allowUnborn = false,
|
|
858
865
|
bool recover = false,
|
|
859
|
-
String? session
|
|
866
|
+
String? session,
|
|
867
|
+
int? maxMounted}) async {
|
|
860
868
|
final ws = session ?? workspace;
|
|
861
869
|
_sessionNames.add(ws);
|
|
862
870
|
final r = _asMap(await _call('connect', {
|
|
@@ -875,6 +883,8 @@ class NomosBridge {
|
|
|
875
883
|
if (authorSecret != null) 'authorSecret': authorSecret,
|
|
876
884
|
if (restoreFrom != null) 'restoreFrom': restoreFrom,
|
|
877
885
|
if (allowUnborn) 'allowUnborn': true,
|
|
886
|
+
// the realm's mounted-workspace budget (LRU park/remount above it) — first connect wins.
|
|
887
|
+
if (maxMounted != null) 'maxMounted': maxMounted,
|
|
878
888
|
// RECOVER-IN-PLACE over a corrupt restoreFrom snapshot: salvage local-only writes + DLQ, refold fresh
|
|
879
889
|
// from cloud, re-offer the salvaged work. The report lands on [lastRecovery]. Nothing local is lost.
|
|
880
890
|
if (recover) 'recover': true,
|
|
@@ -1494,6 +1504,33 @@ class NomosBridge {
|
|
|
1494
1504
|
return v;
|
|
1495
1505
|
}
|
|
1496
1506
|
|
|
1507
|
+
/// CONNECT AS A HANDLE (the recommended multi-workspace shape): [connect] + a [NomosSession] pinned to
|
|
1508
|
+
/// the opened session in one call — `final home = await bridge.open(workspace: 'home-…');` then
|
|
1509
|
+
/// `HomeClient(home)`. The bare [NomosBridge] methods remain the ONE-workspace sugar over an inferred
|
|
1510
|
+
/// default session; an app that opens a second workspace should hold handles, not call the plane.
|
|
1511
|
+
Future<NomosSession> open(
|
|
1512
|
+
{String? cloud,
|
|
1513
|
+
required String workspace,
|
|
1514
|
+
String? clientId,
|
|
1515
|
+
String? authToken,
|
|
1516
|
+
String? authorSecret,
|
|
1517
|
+
String? restoreFrom,
|
|
1518
|
+
bool allowUnborn = false,
|
|
1519
|
+
bool recover = false,
|
|
1520
|
+
String? session}) async {
|
|
1521
|
+
await connect(
|
|
1522
|
+
cloud: cloud,
|
|
1523
|
+
workspace: workspace,
|
|
1524
|
+
clientId: clientId,
|
|
1525
|
+
authToken: authToken,
|
|
1526
|
+
authorSecret: authorSecret,
|
|
1527
|
+
restoreFrom: restoreFrom,
|
|
1528
|
+
allowUnborn: allowUnborn,
|
|
1529
|
+
recover: recover,
|
|
1530
|
+
session: session);
|
|
1531
|
+
return this.session(session ?? workspace);
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1497
1534
|
/// The last computed compat verdict (set by [checkCompat] / a `lawChanged` event). Null until bind.
|
|
1498
1535
|
NomosCompat? get compat => _lastCompat;
|
|
1499
1536
|
|
|
@@ -1513,7 +1550,8 @@ class NomosBridge {
|
|
|
1513
1550
|
/// workspace's installed law changes (another peer installs/upgrades/retires a domain and it
|
|
1514
1551
|
/// syncs in). The verdict carries `offerKnown`. The app degrades/prompts on `ok:false`.
|
|
1515
1552
|
Stream<Map<String, Object?>> watchCompatibility(
|
|
1516
|
-
Map<String, Object?> requirement
|
|
1553
|
+
Map<String, Object?> requirement,
|
|
1554
|
+
[String? session]) {
|
|
1517
1555
|
final subId = 'c${++_seq}';
|
|
1518
1556
|
late final StreamController<Map<String, Object?>> ctrl;
|
|
1519
1557
|
ctrl = StreamController<Map<String, Object?>>(
|
|
@@ -1521,6 +1559,7 @@ class NomosBridge {
|
|
|
1521
1559
|
'subId': subId,
|
|
1522
1560
|
'kind': 'compat',
|
|
1523
1561
|
'requirement': requirement,
|
|
1562
|
+
if (session != null) 'session': session,
|
|
1524
1563
|
}),
|
|
1525
1564
|
onCancel: () async {
|
|
1526
1565
|
_compatWatches.remove(subId);
|
|
@@ -1656,14 +1695,20 @@ class NomosSession extends NomosBridge {
|
|
|
1656
1695
|
parent.watchSpatial(spatialId, bbox, session ?? workspace);
|
|
1657
1696
|
@override
|
|
1658
1697
|
Stream<Map<String, Object?>> watchCompatibility(
|
|
1659
|
-
Map<String, Object?> requirement
|
|
1660
|
-
|
|
1698
|
+
Map<String, Object?> requirement,
|
|
1699
|
+
[String? session]) =>
|
|
1700
|
+
parent.watchCompatibility(requirement, session ?? workspace);
|
|
1661
1701
|
|
|
1662
1702
|
// Live snapshots / streams reflect the shared plane — read them off the parent's controllers.
|
|
1703
|
+
// compat/onLawChanged are LAW-scoped, so a session answers for ITS workspace (falling back to the
|
|
1704
|
+
// plane-wide value when the runner's events carry no session key — older bundles).
|
|
1663
1705
|
@override
|
|
1664
|
-
NomosCompat? get compat =>
|
|
1706
|
+
NomosCompat? get compat =>
|
|
1707
|
+
parent._lastCompatBySession[workspace] ?? parent.compat;
|
|
1665
1708
|
@override
|
|
1666
|
-
Stream<NomosCompat> get onLawChanged => parent.
|
|
1709
|
+
Stream<NomosCompat> get onLawChanged => (parent._lawChangedBySession[
|
|
1710
|
+
workspace] ??=
|
|
1711
|
+
StreamController<NomosCompat>.broadcast()).stream;
|
|
1667
1712
|
@override
|
|
1668
1713
|
Map<String, Object?>? get lastRecovery => parent.lastRecovery;
|
|
1669
1714
|
@override
|
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.39.0
|
|
15
15
|
|
|
16
16
|
environment:
|
|
17
17
|
sdk: ^3.5.0
|
|
@@ -11,6 +11,7 @@ library;
|
|
|
11
11
|
|
|
12
12
|
import 'dart:io';
|
|
13
13
|
|
|
14
|
+
import 'package:nomos_client/nomos_client.dart';
|
|
14
15
|
import 'package:nomos_client/nomos_headless.dart';
|
|
15
16
|
import 'package:test/test.dart';
|
|
16
17
|
|
|
@@ -82,6 +83,30 @@ void main() {
|
|
|
82
83
|
stdout.writeln('offline harness green: ws=${harness.workspace}');
|
|
83
84
|
}, timeout: const Timeout(Duration(minutes: 2)));
|
|
84
85
|
|
|
86
|
+
test('DECOY: the harness plane is multi-session — an unpinned plane op refuses like production', () async {
|
|
87
|
+
final skip = _missingPrereq();
|
|
88
|
+
if (skip != null) {
|
|
89
|
+
markTestSkipped(skip);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
final harness = await NomosTestHarness.ephemeral(
|
|
93
|
+
deployJsonPaths: [_guestbookDeployJson],
|
|
94
|
+
nodeEntry: _repoEntry,
|
|
95
|
+
runtimeDir: _runtimeDir,
|
|
96
|
+
registerTearDown: addTearDown,
|
|
97
|
+
);
|
|
98
|
+
// harness.bridge is a SESSION HANDLE — pinned ops work as before…
|
|
99
|
+
expect(harness.bridge, isA<NomosSession>());
|
|
100
|
+
expect(await harness.bridge.query('entriesByAuthor', {'author': 'x'}), isEmpty);
|
|
101
|
+
// …but the RAW PLANE holds the decoy too, so an op that forgot its session refuses HERE —
|
|
102
|
+
// exactly the refusal a real multi-workspace app would hit (never a silent sole-session guess).
|
|
103
|
+
final plane = (harness.bridge as NomosSession).parent;
|
|
104
|
+
await expectLater(
|
|
105
|
+
plane.query('entriesByAuthor', {'author': 'x'}),
|
|
106
|
+
throwsA(predicate((e) => '$e'.contains('multiple sessions'))),
|
|
107
|
+
);
|
|
108
|
+
}, timeout: const Timeout(Duration(minutes: 2)));
|
|
109
|
+
|
|
85
110
|
test('OFFLINE: dispose is idempotent and removes the temp dir', () async {
|
|
86
111
|
final skip = _missingPrereq();
|
|
87
112
|
if (skip != null) {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Nomos Pre-Release License (v1)
|
|
2
|
+
|
|
3
|
+
Copyright © 2026 Captain App Ltd. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This is a pre-release of Nomos. This license gives you what you need to BUILD
|
|
6
|
+
with it; we keep the rest for now.
|
|
7
|
+
|
|
8
|
+
## You may
|
|
9
|
+
|
|
10
|
+
- install and use these packages to author Nomos domains, compile them, and
|
|
11
|
+
deploy them to Nomos Cloud or any Nomos instance Captain App operates or
|
|
12
|
+
authorizes;
|
|
13
|
+
- build, run, and ship applications on top of them — including commercial ones;
|
|
14
|
+
- keep everything that's yours: code you write, and everything these tools
|
|
15
|
+
generate FOR you (scaffolds from `create-githolon` / `githolon generate`, generated
|
|
16
|
+
clients, compiled domain packages) carries NO restriction from us — it is
|
|
17
|
+
yours outright.
|
|
18
|
+
|
|
19
|
+
## You may not
|
|
20
|
+
|
|
21
|
+
- redistribute these packages or their source, in whole or in part, outside
|
|
22
|
+
your team;
|
|
23
|
+
- modify them or build derivative tools, SDKs, or runtimes from their source;
|
|
24
|
+
- offer the Nomos runtime, or anything materially similar, as a hosted service;
|
|
25
|
+
- reverse-engineer the holon wasm runtime.
|
|
26
|
+
|
|
27
|
+
## Data retention
|
|
28
|
+
|
|
29
|
+
This is a pre-release we are actively evaluating. Workspaces you retire stop
|
|
30
|
+
counting toward your quota but are NOT deleted: **we retain all workspace data
|
|
31
|
+
(ledgers, law, intents) and may examine it to evaluate how the product is
|
|
32
|
+
used.** Don't put anything in a pre-release workspace you wouldn't want the
|
|
33
|
+
builders to read. If you need something truly expunged, ask:
|
|
34
|
+
jack@captainapp.co.uk.
|
|
35
|
+
|
|
36
|
+
## The rest
|
|
37
|
+
|
|
38
|
+
Provided **as is**, with no warranty of any kind; to the maximum extent
|
|
39
|
+
permitted by law, Captain App Ltd accepts no liability arising from your use.
|
|
40
|
+
This license terminates automatically if you breach it.
|
|
41
|
+
|
|
42
|
+
Want the kernel source, broader rights, or to do something this doesn't cover?
|
|
43
|
+
**Ask: jack@captainapp.co.uk.** The plan is to open Nomos up gradually, with
|
|
44
|
+
the people actually using it — telling us what you're building is how that
|
|
45
|
+
happens faster.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# nomos_types
|
|
2
|
+
|
|
3
|
+
The pure data core for Nomos Dart frontends — kernel wire types, the offer codec,
|
|
4
|
+
the `NomosReads` typed-read seam, aggregate metadata, schema validation, and the
|
|
5
|
+
structured error surface. **No transport, no bridge, no runtime**: this package is
|
|
6
|
+
safe to depend on from UI code.
|
|
7
|
+
|
|
8
|
+
It is the foundation of the Nomos layered-packages emit (`dart.packages` in
|
|
9
|
+
`nomos-compile`): generated per-domain TYPES packages depend only on `nomos_types`,
|
|
10
|
+
while the effectful runtime rides `nomos_client` (which depends on and re-exports
|
|
11
|
+
this package). The layering line: generated types may flow to the UI; generated
|
|
12
|
+
clients/runtime may not.
|
|
13
|
+
|
|
14
|
+
Part of [Nomos](https://github.com/Captain-App/nomos2) — a local-first domain
|
|
15
|
+
runtime where every peer runs the same deterministic wasm holon.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.60.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",
|
package/src/codegen_proof.ts
CHANGED
|
@@ -1878,6 +1878,10 @@ export interface ProofLegs {
|
|
|
1878
1878
|
* When this domain is itself the BORN child's law (no standalone create), the index also registers the
|
|
1879
1879
|
* child domain key pointing at the SAME leg, so `proof --domain <child>` resolves through the parent birth. */
|
|
1880
1880
|
readonly childBirth?: ChildBirthLeg;
|
|
1881
|
+
/** ALL child-birth legs, when this domain declares MORE THAN ONE `.births()` directive (`childBirth` is
|
|
1882
|
+
* always the first of these). Emitted only in the >1 case so single-birth output is byte-unchanged;
|
|
1883
|
+
* readers should treat `childBirths ?? (childBirth ? [childBirth] : [])` as the canonical set. */
|
|
1884
|
+
readonly childBirths?: readonly ChildBirthLeg[];
|
|
1881
1885
|
/** THE RELATION-BOOTSTRAP LEG — present iff this domain has a `.requires(R)`-gated directive AND a
|
|
1882
1886
|
* discoverable granter of R (a `.grants(R)` marker, or a plan-probed granting `writeTuple`). Proves the
|
|
1883
1887
|
* gated write SUCCEEDS once R is granted (the seed-then-prove positive). Additive: absent ⇒ older/ungated. */
|
|
@@ -2014,9 +2018,9 @@ export function proofLegsForModule(mod: DomainModule): ProofLegs | null {
|
|
|
2014
2018
|
};
|
|
2015
2019
|
}
|
|
2016
2020
|
|
|
2017
|
-
/**
|
|
2018
|
-
function
|
|
2019
|
-
return (mod.directives as Directive<unknown>[]).
|
|
2021
|
+
/** Whether a module carries any `.births()` directive (with a synthesizable payload). */
|
|
2022
|
+
function hasBirthDirective(mod: DomainModule): boolean {
|
|
2023
|
+
return (mod.directives as Directive<unknown>[]).some((d) => d?.birthsChild === true && typeof d.payloadSchema === "object");
|
|
2020
2024
|
}
|
|
2021
2025
|
|
|
2022
2026
|
/** Build the legs for ONE domain module — every leg it carries (compile-valid always; standalone-provable
|
|
@@ -2034,10 +2038,11 @@ export function legsForDomain(mod: DomainModule, modules: readonly DomainModule[
|
|
|
2034
2038
|
} catch {
|
|
2035
2039
|
standalone = null;
|
|
2036
2040
|
}
|
|
2037
|
-
// The child-birth
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
const
|
|
2041
|
+
// The child-birth legs (one per `.births()` directive) — discovered by running each plan. A parent
|
|
2042
|
+
// with SEVERAL births (co2: birthCatalogueWorkspace + birthEstateWorkspace) keeps EVERY leg; keeping
|
|
2043
|
+
// 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));
|
|
2045
|
+
const childBirthWithSetup = childBirths[0];
|
|
2041
2046
|
|
|
2042
2047
|
// The relation-bootstrap leg (when the domain has a `.requires(R)`-gated directive AND a discoverable
|
|
2043
2048
|
// granter of R). FAIL SOFT: no granter → no leg (the negative conformance leg still covers the gate).
|
|
@@ -2120,6 +2125,7 @@ export function legsForDomain(mod: DomainModule, modules: readonly DomainModule[
|
|
|
2120
2125
|
}
|
|
2121
2126
|
: {}),
|
|
2122
2127
|
...(childBirthWithSetup !== undefined ? { childBirth: childBirthWithSetup } : {}),
|
|
2128
|
+
...(childBirths.length > 1 ? { childBirths } : {}),
|
|
2123
2129
|
...(relationBootstrap !== undefined ? { relationBootstrap } : {}),
|
|
2124
2130
|
};
|
|
2125
2131
|
}
|
|
@@ -2142,17 +2148,18 @@ export function proofLegsIndex(modules: readonly DomainModule[]): { default: str
|
|
|
2142
2148
|
// no standalone leg of its own. Register that child domain key pointing at the PARENT's child-birth leg
|
|
2143
2149
|
// (so `proof --domain estate` proves it THROUGH the birth that installs it — the only way it comes to be).
|
|
2144
2150
|
for (const legs of [...Object.values(domains)]) {
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2151
|
+
// EVERY birth leg fans out to its child (a parent with several `.births()` registers each child).
|
|
2152
|
+
for (const cb of legs.childBirths ?? (legs.childBirth !== undefined ? [legs.childBirth] : [])) {
|
|
2153
|
+
const existing = domains[cb.childDomain];
|
|
2154
|
+
if (existing === undefined) {
|
|
2155
|
+
// A pure child-only domain whose ONLY proof is the parent birth: register it as child-birth-provable.
|
|
2156
|
+
domains[cb.childDomain] = { domain: cb.childDomain, legs: ["compile-valid", "child-birth-provable"], childBirth: cb };
|
|
2157
|
+
if (!order.includes(cb.childDomain)) order.push(cb.childDomain);
|
|
2158
|
+
} else if (existing.childBirth === undefined) {
|
|
2159
|
+
// The child domain ALSO compiled standalone (e.g. it has its own create) — add the child-birth leg to it.
|
|
2160
|
+
const mergedLegs = [...new Set([...(existing.legs ?? ["compile-valid"]), "child-birth-provable" as const])];
|
|
2161
|
+
domains[cb.childDomain] = { ...existing, legs: mergedLegs, childBirth: cb };
|
|
2162
|
+
}
|
|
2156
2163
|
}
|
|
2157
2164
|
}
|
|
2158
2165
|
if (order.length === 0) return null;
|
|
@@ -2473,7 +2480,7 @@ export function generateTsProof(modules: readonly DomainModule[], opts: TsProofO
|
|
|
2473
2480
|
// domain's FULL legs (so the offline runner + the index never describe different directives).
|
|
2474
2481
|
const hasStandaloneCreate = (m: DomainModule): boolean =>
|
|
2475
2482
|
(m.directives as Directive<unknown>[]).some((d) => isStandaloneCreate(d));
|
|
2476
|
-
const hasBirth = (m: DomainModule): boolean =>
|
|
2483
|
+
const hasBirth = (m: DomainModule): boolean => hasBirthDirective(m);
|
|
2477
2484
|
const isBootstrapProvable = (m: DomainModule): boolean => {
|
|
2478
2485
|
try { return discoverRelationBootstrapLeg(m) !== undefined; } catch { return false; }
|
|
2479
2486
|
};
|