@githolon/dsl 0.17.0 → 0.19.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.
@@ -26,11 +26,16 @@ library;
26
26
  export 'src/wire.dart';
27
27
  export 'src/driver.dart';
28
28
  export 'src/offer.dart';
29
+ // homeNameOf(subject) — the canonical home-workspace name (parity with @githolon/client + the law side).
30
+ export 'src/home.dart';
29
31
  export 'src/subscriptions.dart';
30
32
  export 'src/schema_validation.dart';
31
33
  // THE WEBVIEW BRIDGE (app-binding boundary) — the async Dart runtime that drives the REAL githolon running
32
34
  // in a WebView, so the generated typed client never needs a `MemoryPersistence` fake.
33
35
  export 'src/webview_bridge.dart';
36
+ // TYPED DIAGNOSTICS (co2 feedback req 7) — the fixed set of ways the home-birth ceremony can fail, each
37
+ // extending NomosBridgeError so existing handlers still catch them, named so the app can branch on `is`.
38
+ export 'src/errors.dart';
34
39
  // The PROVIDER boundary (server-side capability-provider SDK runtime: TaskOutcome /
35
40
  // TaskSuccess / TaskFailure / TaskResult / TaskProvider / HostClock). The symmetric twin
36
41
  // of `offer.dart`; the generated per-task provider classes build on it.
@@ -0,0 +1,79 @@
1
+ // NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
2
+ // remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
3
+ // wasm32-wasip1 artifact {kernel · projection · embedded QuickJS engine}.
4
+ // If a file isn't this / hosting this / authoring for this / proving this — it's gone.
5
+
6
+ /// TYPED DIAGNOSTICS for the home factory (co2 feedback req 7).
7
+ ///
8
+ /// The generic [NomosBridgeError] (in `webview_bridge.dart`) is the catch-all for a refusal the githolon
9
+ /// surfaces verbatim. But the HOME-BIRTH CEREMONY ([NomosScope.home]) has a small, fixed set of ways it can
10
+ /// fail — and each one wants a DIFFERENT remedy from the app (re-auth vs. retry vs. "deploy the home law").
11
+ /// These typed errors name the exact failure so the app's `errorBuilder` can branch on `is`, and so a log
12
+ /// line says WHICH leg of the ceremony broke, not just "connect failed".
13
+ ///
14
+ /// All extend [NomosBridgeError] so existing `catch (NomosBridgeError)` handlers still catch them.
15
+ library;
16
+
17
+ import 'webview_bridge.dart' show NomosBridgeError;
18
+
19
+ /// The auth-provider access token the cloud was given was missing, malformed, expired, or rejected — the
20
+ /// cloud answered 401 / "invalid token" on a lane that requires a verified principal (every birth does).
21
+ /// REMEDY: re-acquire a fresh token from the auth provider and reconnect.
22
+ class AuthTokenInvalid extends NomosBridgeError {
23
+ AuthTokenInvalid([String? message])
24
+ : super(message ?? 'the auth-provider token was missing or rejected (the cloud refused 401) — a birth needs a verified principal');
25
+ }
26
+
27
+ /// The workspace name the cloud reported for the born home does NOT equal `homeNameOf(subject)`. The home's
28
+ /// own law commits to `home-<sha256(subject)>` (the squat invariant), so a mismatch means the cloud routed to
29
+ /// the wrong slot — a fail-closed safety stop, never silently mount the wrong holon.
30
+ class HomeNameMismatch extends NomosBridgeError {
31
+ /// What `homeNameOf(subject)` derived locally.
32
+ final String expected;
33
+
34
+ /// What the cloud reported (its status `lineage.id` / the name it served).
35
+ final String reported;
36
+ HomeNameMismatch({required this.expected, required this.reported, String? message})
37
+ : super(message ?? 'the cloud reported home "$reported" but this subject hashes to "$expected" — refusing to mount a home whose name does not bind its owner');
38
+ }
39
+
40
+ /// The workspace exists and is born, but the HOME LAW is not installed/active on it — so it is a plain
41
+ /// workspace, not a home holon. Per req 2 there is NO generic fallback: a home that cannot be confirmed to
42
+ /// run the home law FAILS here rather than silently mounting a lawless workspace. REMEDY: the tenant's
43
+ /// governance must install the home law (the birth offer should author it; if it didn't, the deploy is wrong).
44
+ class HomeLawMissing extends NomosBridgeError {
45
+ /// The workspace that is missing the home law.
46
+ final String workspace;
47
+ HomeLawMissing({required this.workspace, String? message})
48
+ : super(message ?? 'workspace "$workspace" is born but the home law is not installed/active on it — refusing to mount a plain workspace as a home');
49
+ }
50
+
51
+ /// The offer to the home's birth lane was REFUSED by the parent's gate (the law denied the birth — e.g. the
52
+ /// principal lacks creation authority, the pool is exhausted, or the genesis recipe was rejected). Carries
53
+ /// the cloud's verdict verbatim. REMEDY is tenant-specific (grant creation authority / raise the pool).
54
+ class BirthRejected extends NomosBridgeError {
55
+ /// The birth-lane directive id that was offered (e.g. the tenant's `birthHome` / `seedHomeOwner`).
56
+ final String? directiveId;
57
+
58
+ /// The HTTP status the cloud answered, when known.
59
+ final int? statusCode;
60
+ BirthRejected({String? message, this.directiveId, this.statusCode})
61
+ : super(message ?? 'the home birth offer was refused by the law${directiveId != null ? ' (directive "$directiveId")' : ''}${statusCode != null ? ' [$statusCode]' : ''}');
62
+ }
63
+
64
+ /// A pack the cloud returned (or the local holon produced) was malformed — it could not be parsed/applied as
65
+ /// a git pack. This is a transport/custody integrity fault, not a law refusal. REMEDY: retry; if persistent,
66
+ /// the custody bytes are corrupt.
67
+ class PackMalformed extends NomosBridgeError {
68
+ PackMalformed([String? message]) : super(message ?? 'a returned pack was malformed and could not be applied (custody integrity fault)');
69
+ }
70
+
71
+ /// The home law's content hash could not be resolved — the cloud reported no current law for the home's
72
+ /// domain key, so the app cannot confirm WHICH law the home runs (and thus cannot baked-hash-pin or judge
73
+ /// installed-ness). REMEDY: the home law must be deployed; if it was, retry once the deploy has folded.
74
+ class LawHashUnavailable extends NomosBridgeError {
75
+ /// The workspace whose current law hash could not be resolved.
76
+ final String workspace;
77
+ LawHashUnavailable({required this.workspace, String? message})
78
+ : super(message ?? 'could not resolve the current law hash for "$workspace" — the cloud reported no installed law for the home');
79
+ }
@@ -0,0 +1,20 @@
1
+ import 'dart:convert';
2
+
3
+ import 'package:crypto/crypto.dart';
4
+
5
+ /// The canonical home-workspace name for an identity SUBJECT (e.g. `user:<firebaseUid>`).
6
+ ///
7
+ /// BYTE-IDENTICAL to `@githolon/client`'s `homeNameOf` and the law side
8
+ /// (`dsl/src/framework/home.ts` `HOME_NAME_PREFIX`). A home holon is named by its SUBJECT — never the raw
9
+ /// uid and never an app-chosen workspace name — so the same principal always resolves the same home, on
10
+ /// every peer and on the cloud. Use it directly as the `workspace`:
11
+ ///
12
+ /// ```dart
13
+ /// NomosScope(workspace: homeNameOf('user:$firebaseUid'), ...)
14
+ /// ```
15
+ ///
16
+ /// Formula: `home-` + the first 40 hex chars of `sha256(utf8(subject))`.
17
+ String homeNameOf(String subject) {
18
+ final hex = sha256.convert(utf8.encode(subject)).toString(); // lowercase hex
19
+ return 'home-${hex.substring(0, 40)}';
20
+ }
@@ -156,12 +156,16 @@ class NomosBridge {
156
156
  required String workspace,
157
157
  String? clientId,
158
158
  String? authToken,
159
+ String? authorSecret,
159
160
  String? restoreFrom}) async {
160
161
  await _call('connect', {
161
162
  'cloud': cloud,
162
163
  'workspace': workspace,
163
164
  if (clientId != null) 'clientId': clientId,
164
165
  if (authToken != null) 'authToken': authToken,
166
+ // The device signing secret (base64, from [authorKeygen]) — the warrant default for every offer.
167
+ // Stays client-side; the kernel mints the signed-author attestation so authorship is replay-provable.
168
+ if (authorSecret != null) 'authorSecret': authorSecret,
165
169
  if (restoreFrom != null) 'restoreFrom': restoreFrom
166
170
  });
167
171
  }
@@ -172,6 +176,70 @@ class NomosBridge {
172
176
  return r['head'].toString();
173
177
  }
174
178
 
179
+ /// Mint an ed25519 DEVICE KEY in-kernel → `{secret, public}` (base64). The SECRET stays on this device
180
+ /// (persist it in secure storage); pass it to [connect] as `authorSecret` (or per-offer `signWith`) so the
181
+ /// holon SIGNS each intent. The PUBLIC half is what gets enrolled so the gate re-verifies the signature on
182
+ /// replay — the durable, cryptographic answer to "who authored this?" (the warrant). Mint once per device.
183
+ Future<Map<String, Object?>> authorKeygen() async =>
184
+ _asMap(await _call('authorKeygen'));
185
+
186
+ /// Mint a DEVICE KEY for the VA root-of-trust (`{secret, public}`, base64) — an alias of [authorKeygen]
187
+ /// the birth-cert flow speaks. The SECRET stays on this device (secure storage); it signs birth certs
188
+ /// ([signBirthCert]) + writes ([connect]'s `authorSecret`) on the LOCAL kernel; the PUBLIC half is enrolled
189
+ /// on-chain ([enrollDevice]). Never put the secret on the chain.
190
+ Future<Map<String, Object?>> mintDeviceKey() async =>
191
+ _asMap(await _call('mintDeviceKey'));
192
+
193
+ /// VA ROOT-OF-TRUST: sign a BirthCert through the kernel `cert_sign` op (the SINGLE source of the cert
194
+ /// byte-layout — no Dart copy). `fields` = `{child, founder, verified, parentKey (this device's base64
195
+ /// ed25519 PUBLIC half), parentId, epoch}`; `deviceSecret` is that key's base64 SECRET half, handed to the
196
+ /// LOCAL kernel only (it never reaches a server). Returns the snake_case cert JSON `verify_to_root` parses.
197
+ Future<Map<String, Object?>> signBirthCert(
198
+ Map<String, Object?> fields, String deviceSecret) async =>
199
+ _asMap(await _call(
200
+ 'signBirthCert', {'fields': fields, 'deviceSecret': deviceSecret}))['cert'] as Map<String, Object?>? ??
201
+ const {};
202
+
203
+ /// ENROLL THIS DEVICE — the LIVE gate: bind a device key's PUBLIC half to `principal` on-chain as a
204
+ /// `signer` so the holon's gate can PROVE this device's authorship on replay (and the VA chain attest it).
205
+ /// `publicKey` is the base64 PUBLIC half (from [mintDeviceKey]); `authToken` (defaults to the connect-time
206
+ /// bearer) is the VERIFIED identity that authorizes the enrollment. Returns the offer verdict.
207
+ Future<Map<String, Object?>> enrollDevice(
208
+ {required String principal,
209
+ required String publicKey,
210
+ String? authToken}) async =>
211
+ _asMap(await _call('enrollDevice', {
212
+ 'principal': principal,
213
+ 'publicKey': publicKey,
214
+ if (authToken != null) 'authToken': authToken,
215
+ }));
216
+
217
+ /// VERIFY_CHAIN — the kernel's from-genesis self-verification of THIS holon's folded ledger (read-only:
218
+ /// contiguity → every intent re-admitted through the ONE gate against the state folded from the prefix). For
219
+ /// a WARRANTED home this RE-FIRES the parent-attested-birth cert door on the cold-replay lane — so a
220
+ /// freshly-connected peer that CLONES a home and runs this IS the offline peer-verify (replay IS validation;
221
+ /// no new op). Returns the kernel verdict `{valid, error?, plansRerun?}`.
222
+ Future<Map<String, Object?>> verifyChain() async =>
223
+ _asMap(await _call('verifyChain'));
224
+
225
+ /// BIRTH A HOME — THE CEREMONY (VA root-of-trust, drift-proof), run end-to-end inside the connected holon:
226
+ /// derive the deterministic home name, mint + enroll the device key, DEVICE-SIGN the home cert through the
227
+ /// kernel `cert_sign` op (the single source of the byte layout), OFFER the PARAMETERIZED parent's `birthHome`
228
+ /// directive carrying the homeCert, poll the parent for the born home, and CONFIRM the born home's own
229
+ /// `verify_chain` is GREEN (the offline peer-verify — replay IS validation). `spec` MUST carry `parent` (the
230
+ /// tenant's platform whose governance includes `birthHome`), `principal`, `frameworkHash`, and `lawHash`;
231
+ /// `subject`/`authToken`/`directiveId` are optional. OURS is the ceremony; the parent is the TENANT's.
232
+ /// Returns `{home, device:{secret,public}, homeCert, born, verify:{valid,…}}`.
233
+ Future<Map<String, Object?>> birthHome(Map<String, Object?> spec) async =>
234
+ _asMap(await _call('birthHome', spec));
235
+
236
+ /// Sign a cross-custody SHARE attestation `{secret, asserterWorkspace, object, relation, subject,
237
+ /// attestedAt}` → `{publicKey, signature}`. This is what drives `recordShare` into an invitee's home
238
+ /// (the free global invitation system): the granting workspace signs that it shares `object#relation`
239
+ /// with `subject`, and the invitee's home gate verifies the signature before recording it.
240
+ Future<Map<String, Object?>> attestationSign(Map<String, Object?> args) async =>
241
+ _asMap(await _call('attestationSign', {'args': args}));
242
+
175
243
  /// A declared query's rows from the local projection.
176
244
  Future<List<Map<String, dynamic>>> query(String queryId,
177
245
  [Map<String, Object?> params = const {}]) async =>
@@ -92,6 +92,24 @@ sealed class Value {
92
92
  (k, v) => MapEntry(k, WireField.fromJson(v as Map<String, dynamic>)),
93
93
  ));
94
94
  }
95
+ // `Value::Evidence(EvidenceRef)` — the content-addressed BACKING of a claim. The hash is the
96
+ // git blob oid bound at author time; the gate re-derives + backs it (kernel/src/evidence.rs,
97
+ // evidence.md §1/§5). Capture-time-irreversible provenance — decode it so it round-trips
98
+ // (a claim's evidence must survive every peer, not be dropped to an "unknown variant").
99
+ if (json.containsKey('Evidence')) {
100
+ final r = json['Evidence'] as Map<String, dynamic>;
101
+ return ValueEvidence(
102
+ hash: r['hash'] as String,
103
+ mediaType: r['media_type'] as String,
104
+ byteLength: (r['byte_length'] as num).toInt(),
105
+ storageClass: r['storage_class'] as String, // "Git" | "External"
106
+ );
107
+ }
108
+ // `Value::Json(CanonicalJson)` — first-class JSON committed INLINE (RFC 8785/JCS), replacing
109
+ // the old t.json-as-Value::Str. The held value is the decoded JSON tree.
110
+ if (json.containsKey('Json')) {
111
+ return ValueJson(json['Json']);
112
+ }
95
113
  throw FormatException('Unknown Value variant: ${json.keys}');
96
114
  }
97
115
 
@@ -165,6 +183,105 @@ class ValueMap extends Value {
165
183
  String toString() => 'ValueMap($value)';
166
184
  }
167
185
 
186
+ /// `Value::Evidence(EvidenceRef)` ->
187
+ /// `{"Evidence":{"hash":"<git-oid>","media_type":"application/pdf","byte_length":1024,"storage_class":"Git"}}`.
188
+ ///
189
+ /// A content-addressed, framework-owned reference to the bytes that BACK a claim (`evidence.md` §1;
190
+ /// `kernel/src/evidence.rs`). The `hash` is the git blob oid of the bytes — bound AT AUTHOR TIME and
191
+ /// re-derived by the gate, so a claim's evidence binding is provable forever. The bytes live in the
192
+ /// Evidence Store, never the ledger op. This variant is capture-time-irreversible provenance: it must
193
+ /// round-trip on every peer, so the client decodes it (never drops it to "unknown variant").
194
+ class ValueEvidence extends Value {
195
+ final String hash;
196
+ final String mediaType;
197
+ final int byteLength;
198
+ final String storageClass; // "Git" | "External"
199
+ const ValueEvidence({
200
+ required this.hash,
201
+ required this.mediaType,
202
+ required this.byteLength,
203
+ required this.storageClass,
204
+ });
205
+
206
+ @override
207
+ Map<String, dynamic> toJson() => {
208
+ 'Evidence': {
209
+ 'hash': hash,
210
+ 'media_type': mediaType,
211
+ 'byte_length': byteLength,
212
+ 'storage_class': storageClass,
213
+ }
214
+ };
215
+
216
+ @override
217
+ bool operator ==(Object other) =>
218
+ other is ValueEvidence &&
219
+ other.hash == hash &&
220
+ other.mediaType == mediaType &&
221
+ other.byteLength == byteLength &&
222
+ other.storageClass == storageClass;
223
+ @override
224
+ int get hashCode => Object.hash('Evidence', hash, mediaType, byteLength, storageClass);
225
+ @override
226
+ String toString() =>
227
+ 'ValueEvidence($hash, $mediaType, $byteLength, $storageClass)';
228
+ }
229
+
230
+ /// `Value::Json(CanonicalJson)` -> `{"Json": <any canonical-JSON value>}`.
231
+ ///
232
+ /// First-class JSON committed INLINE (the kernel canonicalizes to RFC 8785 / JCS), replacing the old
233
+ /// `t.json`-as-`Value::Str`. The held `value` is the decoded JSON tree (Map / List / num / String /
234
+ /// bool / null). Equality is structural over the decoded tree.
235
+ class ValueJson extends Value {
236
+ final dynamic value;
237
+ const ValueJson(this.value);
238
+
239
+ @override
240
+ Map<String, dynamic> toJson() => {'Json': value};
241
+
242
+ @override
243
+ bool operator ==(Object other) => other is ValueJson && _jsonEq(other.value, value);
244
+ @override
245
+ int get hashCode => Object.hash('Json', _jsonHash(value));
246
+ @override
247
+ String toString() => 'ValueJson($value)';
248
+ }
249
+
250
+ /// Structural deep-equality over a decoded JSON tree (Map / List / scalar) — no `dart:convert` dep.
251
+ bool _jsonEq(dynamic a, dynamic b) {
252
+ if (identical(a, b)) return true;
253
+ if (a is Map && b is Map) {
254
+ if (a.length != b.length) return false;
255
+ for (final e in a.entries) {
256
+ if (!b.containsKey(e.key) || !_jsonEq(e.value, b[e.key])) return false;
257
+ }
258
+ return true;
259
+ }
260
+ if (a is List && b is List) {
261
+ if (a.length != b.length) return false;
262
+ for (var i = 0; i < a.length; i++) {
263
+ if (!_jsonEq(a[i], b[i])) return false;
264
+ }
265
+ return true;
266
+ }
267
+ return a == b;
268
+ }
269
+
270
+ /// Order-independent hash over a decoded JSON tree (consistent with [_jsonEq]).
271
+ int _jsonHash(dynamic v) {
272
+ if (v is Map) {
273
+ var h = 0;
274
+ for (final e in v.entries) {
275
+ h ^= Object.hash(e.key, _jsonHash(e.value));
276
+ }
277
+ return Object.hash('jmap', h);
278
+ }
279
+ if (v is List) {
280
+ return Object.hash('jlist', Object.hashAll(v.map(_jsonHash)));
281
+ }
282
+ return v.hashCode;
283
+ }
284
+
168
285
  /// `Field { value: Value, stamp: Hlc }` — only used when reading state back.
169
286
  ///
170
287
  /// Named `WireField` to avoid clashing with the DSL field-builder concept and
package/dart/pubspec.lock CHANGED
@@ -74,7 +74,7 @@ packages:
74
74
  source: hosted
75
75
  version: "1.15.0"
76
76
  crypto:
77
- dependency: transitive
77
+ dependency: "direct main"
78
78
  description:
79
79
  name: crypto
80
80
  sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
package/dart/pubspec.yaml CHANGED
@@ -11,13 +11,14 @@ 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.17.0
14
+ version: 0.18.0
15
15
 
16
16
  environment:
17
17
  sdk: ^3.5.0
18
18
 
19
19
  dependencies:
20
20
  json_schema: ^5.2.2
21
+ crypto: ^3.0.0
21
22
 
22
23
  dev_dependencies:
23
24
  test: ^1.25.0
@@ -0,0 +1,9 @@
1
+ import 'package:nomos_client/nomos_client.dart';
2
+ import 'package:test/test.dart';
3
+ void main() {
4
+ test('homeNameOf matches the JS/law formula', () {
5
+ expect(homeNameOf('user:abc-123'), 'home-5094641143043f9281b6b9146cc345a254a5d3af');
6
+ expect(homeNameOf('user:abc-123').startsWith('home-'), isTrue);
7
+ expect(homeNameOf('user:abc-123').length, 45); // 'home-' + 40 hex
8
+ });
9
+ }
@@ -46,9 +46,57 @@ void main() {
46
46
  (m) => Value.fromJson(m),
47
47
  );
48
48
 
49
+ // Value::Evidence — the content-addressed claim BACKING (capture-time-irreversible provenance).
50
+ // The raw serde shape is snake_case (the Rust EvidenceRef field names) + storage_class as a bare
51
+ // unit-variant string ("Git"/"External"). It MUST round-trip, never drop to "unknown variant".
52
+ roundTrips(
53
+ 'Value::Evidence',
54
+ '{"Evidence":{"hash":"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391","media_type":"application/pdf","byte_length":1024,"storage_class":"Git"}}',
55
+ (m) => Value.fromJson(m),
56
+ );
57
+ // Value::Json — first-class inline JSON (RFC 8785/JCS), nested object value.
58
+ roundTrips(
59
+ 'Value::Json',
60
+ '{"Json":{"a":1,"b":["x","y"],"c":{"d":true}}}',
61
+ (m) => Value.fromJson(m),
62
+ );
63
+
49
64
  test('Value::Int -> ValueInt', () {
50
65
  expect(Value.fromJson({'Int': 10}), const ValueInt(10));
51
66
  });
67
+ test('Value::Evidence decodes the snake_case ref + bare storage class', () {
68
+ final v = Value.fromJson({
69
+ 'Evidence': {
70
+ 'hash': 'abc123',
71
+ 'media_type': 'image/png',
72
+ 'byte_length': 42,
73
+ 'storage_class': 'External',
74
+ }
75
+ });
76
+ expect(
77
+ v,
78
+ const ValueEvidence(
79
+ hash: 'abc123',
80
+ mediaType: 'image/png',
81
+ byteLength: 42,
82
+ storageClass: 'External',
83
+ ),
84
+ );
85
+ });
86
+ test('Value::Json structural equality over the decoded tree', () {
87
+ expect(
88
+ Value.fromJson({
89
+ 'Json': {
90
+ 'a': 1,
91
+ 'b': [true, null]
92
+ }
93
+ }),
94
+ const ValueJson({
95
+ 'a': 1,
96
+ 'b': [true, null]
97
+ }),
98
+ );
99
+ });
52
100
  });
53
101
 
54
102
  group('Op variants', () {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.17.0",
3
+ "version": "0.19.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/births.ts ADDED
@@ -0,0 +1,106 @@
1
+ // NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
2
+ // remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
3
+ // wasm32-wasip1 artifact {kernel · projection · embedded
4
+ // QuickJS engine}. If a file isn't this / hosting this / authoring for this / proving this — it's gone.
5
+
6
+ /**
7
+ * THE BIRTH EFFECT — birth is an EXPLICIT, DECLARED kernel primitive (G3, the unix model).
8
+ *
9
+ * "A syscall does a defined thing and finishes; no implicit side-effects inferred from scanning state."
10
+ * A `.births()` directive's `plan` PRODUCES the child's genesis recipe (reuse `buildGenesisChain`) and
11
+ * records it onto the intent's BIRTH EFFECT via `birth(recipe)`. The recipe rides the committed intent as
12
+ * a DECLARED OUTPUT — exactly like a captured read rides `captured_ports.reads` — NEVER as a field set on a
13
+ * domain aggregate the kernel would have to scan (a circular dependency the kernel must never have).
14
+ *
15
+ * The kernel reads the `.births` DECLARATION from the law manifest (the `nomosBirthGate` era key, the SAME
16
+ * mechanism it reads `.reads`/`.creates`/invariants by — NOT domain state) to KNOW an admitted directive
17
+ * births; it then takes the recipe off this carried effect and spawns + self-validates the child through its
18
+ * OWN gate (the `materialize_genesis` keystone). The dev never sees this sink — they call `birth(recipe)`;
19
+ * the library routes it here and `executeDirectiveToIntent` drains it onto the intent.
20
+ */
21
+
22
+ /** ONE birth recipe the plan produced — the child's genesis chain (the `buildGenesisChain` JSON array of
23
+ * `{domain, directiveId, payload, actor[, domainPackageB64, domainHash]}` steps). Carried verbatim onto the
24
+ * intent's birth effect; the kernel folds it through the child's own gate (each step self-verifies). */
25
+ export interface BirthRecipe {
26
+ /** The child workspace name the parent asked to birth (lineage + the child's `:ws` segment). */
27
+ readonly workspace: string;
28
+ /** The genesis chain: the ordered steps the child runs through its OWN gate to validate itself from intent 0. */
29
+ readonly genesisChain: unknown;
30
+ }
31
+
32
+ // ── the per-dispatch birth footprint — library state drained by `executeDirectiveToIntent` (mirrors the
33
+ // captured-read footprint in read.ts; internal to the directive runtime, never a sandbox side channel). ──
34
+ let footprint: BirthRecipe[] = [];
35
+ /** Clear the footprint before a plan runs (called by `executeDirectiveToIntent`). */
36
+ export function __resetBirths(): void {
37
+ footprint = [];
38
+ }
39
+ /** Take + clear the births recorded during a plan (called by `executeDirectiveToIntent`). */
40
+ export function __drainBirths(): BirthRecipe[] {
41
+ const f = footprint;
42
+ footprint = [];
43
+ return f;
44
+ }
45
+
46
+ /**
47
+ * Record a BIRTH the plan declares: the child's genesis recipe rides the committed intent as a DECLARED
48
+ * effect (the kernel takes it off the intent after admit and spawns + self-validates the child). The
49
+ * directive MUST declare `.births()` (which lands the `nomosBirthGate` era key in the law's USD-IR) — the
50
+ * kernel executes the birth ONLY for a directive its resolved law names there (the era rule: pre-feature
51
+ * chains carry no key, so they replay byte-green and the kernel does nothing). A plan may birth at most one
52
+ * child per intent (one offer, one defined consequence — the unix model).
53
+ *
54
+ * TO BIRTH A LEAF CHILD RUNNING ITS OWN LAW (a home, an estate child) — copy `framework/home.ts`'s
55
+ * `birthHome`. The recipe is just the two-layer law install + your own seed step; there is NO helper to
56
+ * fabricate it and you do NOT need a new primitive (recursion is the law — a home births children exactly
57
+ * as root births platforms). `buildGenesisChain` is the ONE builder for the PLATFORM shape (it adds the
58
+ * `seedGenesis → grantAdmin → birthCloud` tail); a leaf just omits that tail:
59
+ *
60
+ * ```ts
61
+ * export const birthEstateChild = directive("birthEstateChild")
62
+ * .creates(EstateChild)
63
+ * .payload(z.object({ child: z.string(), owner: z.string(),
64
+ * frameworkHash: z.string(), lawHash: z.string(), bornAt: z.string() }))
65
+ * .plan((p) => {
66
+ * const actor = "user:" + p.owner;
67
+ * birth({ workspace: p.child, genesisChain: [
68
+ * { domain: "bootstrap", directiveId: "installDomain", actor, payload: { domainHash: p.frameworkHash }, domainHash: p.frameworkHash, domainPackageB64: "" },
69
+ * { domain: "nomos", directiveId: "installDomain", actor, payload: { domainHash: p.lawHash }, domainHash: p.lawHash, domainPackageB64: "" },
70
+ * { domain: "estate", directiveId: "seedEstateChild", actor, payload: { owner: p.owner, bornAt: p.bornAt } },
71
+ * ] });
72
+ * return [];
73
+ * })
74
+ * .births() // the kernel's run_birth_effect folds the recipe through the CHILD's own gate
75
+ * .requires("creator"); // grant the parent's owner #creator (self-bound) — the birth authority
76
+ * ```
77
+ */
78
+ export function birth(recipe: BirthRecipe): void {
79
+ footprint.push(recipe);
80
+ }
81
+
82
+ /**
83
+ * THE PEER-SOVEREIGN ROOT-OF-TRUST SENTINEL (the BirthAttestation cert chain). A home/child law EXPORTS
84
+ * this marker to declare it REQUIRES a parent-attested-birth: a warranted workspace running this law must
85
+ * travel with a SIGNED cert chain proving its founder was VERIFIED by its parent at birth, rooting at the
86
+ * pinned `K_root`. The composer discovers this by shape (`__isParentAttestedBirth`) — like an aggregate
87
+ * handle — and lowers it to the `nomosParentAttestedBirth` USD era key (emitted iff present). The kernel's
88
+ * verify doors then verify the travelling cert chain OFFLINE (no cross-ws read). ERA-RULED: a law that
89
+ * never exports it carries no key and replays byte-identical (pre-feature + legacy/non-warranted homes).
90
+ *
91
+ * export const parentAttested = requireParentAttestedBirth; // in the home law module
92
+ */
93
+ export interface ParentAttestedBirthMarker {
94
+ readonly __isParentAttestedBirth: true;
95
+ }
96
+ export const requireParentAttestedBirth: ParentAttestedBirthMarker = { __isParentAttestedBirth: true };
97
+
98
+ /** Shape-discover the {@link requireParentAttestedBirth} sentinel in a module's exports (the composer). */
99
+ export function moduleRequiresParentAttestedBirth(mod: Record<string, unknown>): boolean {
100
+ for (const v of Object.values(mod)) {
101
+ if (v && typeof v === "object" && (v as ParentAttestedBirthMarker).__isParentAttestedBirth === true) {
102
+ return true;
103
+ }
104
+ }
105
+ return false;
106
+ }
@@ -19,14 +19,15 @@
19
19
  * 15-positional-parameter `module(...)` helper (dedupe by wire id, the
20
20
  * impure-capability directive check, default read-models, and the
21
21
  * HASH-LOAD-BEARING omit-when-empty key discipline).
22
- * * `buildReadManifest(modules)` — the LEVEL-2 read-projection artifact
23
- * (`domain_manifests.json`: aggregateFieldKinds + query/count/spatial/derived/
24
- * combined routing descriptors), hoisted from `emit_manifests.ts`. Field KINDS,
25
- * not drivers — see that file's header for why the split is load-bearing.
26
- * * `buildIdentity(modules)` / `writeIdentity(emit, outDir)` — the per-domain
27
- * IDENTITY manifest + admits_domain_hash registry (#193/#136), hoisted from
28
- * `emit_identity.ts` (palette-gap domains recorded-and-excluded, the
29
- * cross-language sha anchor asserted per emitted domain).
22
+ * * `buildReadManifest(modules)` — the read-projection descriptor (aggregateFieldKinds +
23
+ * query/count/spatial/derived/combined routing). Field KINDS, not drivers. NO LONGER WRITTEN to
24
+ * `build/` (the old `domain_manifests.json` sidecar was a local diagnostic the contract ignored); the
25
+ * read routing lives in the package OpenAPI now. Kept as a function for callers that want the value.
26
+ * * `buildIdentity(modules)` — the per-domain IDENTITY emit + admits_domain_hash registry (#193/#136),
27
+ * hoisted from `emit_identity.ts` (palette-gap domains recorded-and-excluded). The admission identity
28
+ * is carried in the package USD IR + the committed `nomos.stable-ids.json` lockfile; the old
29
+ * `domain_identity_*.json` sidecars (and `writeIdentity`) are REMOVED. The per-domain hash is surfaced
30
+ * in the compile summary.
30
31
  * * `packageUsda(usdJson, javascript)` — the `.package.usda` envelope, byte-exact
31
32
  * to `build_nomos.mjs` `packageUsda()` == Rust `domain_package_from_js`
32
33
  * (deterministic-rquickjs/src/domain_package.rs).
@@ -57,6 +58,7 @@ import { domainHash, emitManifestBytes } from "./manifest.js";
57
58
  import { emitUsd } from "./usd.js";
58
59
  import { expandCapabilityExports } from "./capability_exports.js";
59
60
  import { RelationTuple } from "./framework/relations.js";
61
+ import { moduleRequiresParentAttestedBirth } from "./births.js";
60
62
 
61
63
  // ─────────────────────────────────────────────────────────────────────────────────
62
64
  // composeDomainModule — the named-keys `module(...)`
@@ -181,6 +183,12 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
181
183
  const workspaceInvariants = [...(spec.workspaceInvariants ?? [])];
182
184
  const relationTypes = [...(spec.relationTypes ?? [])];
183
185
  const dartImports = [...(spec.dartImports ?? [])];
186
+ // THE PEER-SOVEREIGN ROOT-OF-TRUST: a home/child law that exports `requireParentAttestedBirth` declares
187
+ // it requires a parent-attested-birth cert chain. Discovered by shape across the module list (the SAME
188
+ // duck-typing aggregates/directives use). Omit-when-false (era rule): a law that never declares it is
189
+ // byte-identical in the canonical manifest/USD to before the key existed.
190
+ const parentAttestedBirth =
191
+ moduleRequiresParentAttestedBirth(merged) || spec.modules.some((m) => moduleRequiresParentAttestedBirth(m));
184
192
 
185
193
  return {
186
194
  name: spec.name,
@@ -207,6 +215,9 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
207
215
  // Omit-when-empty — the relation schema is hashed law (P1); a relation-free domain's
208
216
  // canonical manifest/USD stays byte-identical (the nomosRelations era rule).
209
217
  ...(relationTypes.length > 0 ? { relationTypes } : {}),
218
+ // THE PEER-SOVEREIGN ROOT-OF-TRUST: omit-when-false — a law that doesn't require parent-attested
219
+ // birth is byte-identical in the canonical manifest/USD (the nomosParentAttestedBirth era rule).
220
+ ...(parentAttestedBirth ? { parentAttestedBirth: true } : {}),
210
221
  ...(dartImports.length > 0 ? { dartImports } : {}),
211
222
  ...(spec.dartRefImports !== undefined && spec.dartRefImports.length > 0
212
223
  ? { dartRefImports: [...spec.dartRefImports] }
@@ -702,25 +713,10 @@ export function buildIdentity(modules: readonly DomainModule[]): IdentityEmit {
702
713
  return { manifests, hashes, excluded };
703
714
  }
704
715
 
705
- /**
706
- * Serialize the identity emit to its on-disk artifacts. The consumed maps stay
707
- * STRICTLY `{domain: value}` (the Rust `parse_identity_map` treats EVERY key as a
708
- * domain and fail-closes the WHOLE map on one bad entry); the exclusion record goes
709
- * to a SEPARATE sidecar the gate never reads.
710
- */
711
- export function writeIdentity(
712
- emit: IdentityEmit,
713
- outDir: string,
714
- ): { manifestsPath: string; hashesPath: string; excludedPath: string } {
715
- mkdirSync(outDir, { recursive: true });
716
- const manifestsPath = join(outDir, "domain_identity_manifests.json");
717
- const hashesPath = join(outDir, "domain_identity_hashes.json");
718
- const excludedPath = join(outDir, "domain_identity_excluded.json");
719
- writeFileSync(manifestsPath, JSON.stringify(emit.manifests, null, 2) + "\n", "utf8");
720
- writeFileSync(hashesPath, JSON.stringify(emit.hashes, null, 2) + "\n", "utf8");
721
- writeFileSync(excludedPath, JSON.stringify(emit.excluded, null, 2) + "\n", "utf8");
722
- return { manifestsPath, hashesPath, excludedPath };
723
- }
716
+ // writeIdentity (the domain_identity_*.json sidecars) — REMOVED. The admission identity is carried in the
717
+ // package USD IR + the committed nomos.stable-ids.json lockfile; the deploy/runtime contract never read the
718
+ // sidecars (they were local diagnostics that only confused /build consumers). `buildIdentity` stays — its
719
+ // per-domain hash is surfaced in the compile summary.
724
720
 
725
721
  // ─────────────────────────────────────────────────────────────────────────────────
726
722
  // The .package.usda envelope + the USD-IR emit