@githolon/dsl 0.95.2 → 0.97.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 +17 -0
- package/dart/lib/src/dev_cloud.dart +24 -5
- package/dart/lib/src/test_harness.dart +150 -28
- package/dart/lib/src/webview_bridge.dart +31 -10
- package/dart/pubspec.yaml +1 -1
- package/dart/test/cloud_bound_topology_test.dart +9 -2
- package/dart/test/cluster_births_test.dart +15 -8
- package/dart/test/first_app_gauntlet_test.dart +6 -1
- package/dart/test/four_layer_nested_topology_test.dart +8 -4
- package/dart/test/parent_signer_recovery_test.dart +3 -2
- package/dart/test/session_bound_test.dart +11 -1
- package/dart/test/watch_surfaces_test.QUARANTINED.md +17 -0
- package/package.json +1 -1
- package/src/build_package.ts +23 -0
- package/src/compile_package_main.ts +1 -1
- package/src/framework/genesis.ts +5 -5
- package/src/framework/home.ts +26 -13
- package/src/framework/workspaces.ts +35 -34
- package/dart/test/co2_acceptance_test.dart +0 -101
- package/dart/test/watch_surfaces_test.dart +0 -240
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
import 'dart:convert';
|
|
25
25
|
import 'dart:io';
|
|
26
26
|
|
|
27
|
+
import 'package:crypto/crypto.dart' show sha256;
|
|
27
28
|
import 'package:nomos_client/nomos_headless.dart';
|
|
28
29
|
import 'package:test/test.dart';
|
|
29
30
|
|
|
@@ -55,7 +56,7 @@ void main() {
|
|
|
55
56
|
final diag = Platform.environment['NOMOS_HARNESS_DIAG'] == '1'
|
|
56
57
|
? (String l) => stderr.writeln(l)
|
|
57
58
|
: null;
|
|
58
|
-
final cloud = await NomosTestCloud.start(serverEntry: _devServer, onDiagnostic: diag);
|
|
59
|
+
final cloud = await NomosTestCloud.start(serverEntry: _devServer, platform: 'four-layer-platform', deployJsonPath: _deploy, onDiagnostic: diag);
|
|
59
60
|
NomosTestHarness? harness1;
|
|
60
61
|
NomosTestHarness? harness2;
|
|
61
62
|
try {
|
|
@@ -87,6 +88,7 @@ void main() {
|
|
|
87
88
|
'frameworkHash': harness1.frameworkHash,
|
|
88
89
|
'lawHash': harness1.lawHashes(home1.workspace).first,
|
|
89
90
|
'bornAt': '2026-07-15T00:00:00Z',
|
|
91
|
+
'ownerKeyHash': sha256.convert(base64Decode(harness1.devicePublicKey(home1.workspace)!)).toString(),
|
|
90
92
|
},
|
|
91
93
|
actor: _subject1,
|
|
92
94
|
);
|
|
@@ -103,6 +105,7 @@ void main() {
|
|
|
103
105
|
'frameworkHash': harness1.frameworkHash,
|
|
104
106
|
'lawHash': harness1.lawHashes(home1.workspace).first,
|
|
105
107
|
'bornAt': '2026-07-15T00:00:01Z',
|
|
108
|
+
'ownerKeyHash': sha256.convert(base64Decode(harness1.devicePublicKey(home1.workspace)!)).toString(),
|
|
106
109
|
},
|
|
107
110
|
actor: _subject1,
|
|
108
111
|
);
|
|
@@ -111,8 +114,8 @@ void main() {
|
|
|
111
114
|
expect(projectWs, startsWith('project-'));
|
|
112
115
|
|
|
113
116
|
// LAYER 3+4: Open both space and project as sibling sessions + make WRITES while offline
|
|
114
|
-
final space = await harness1.bridge.acquireSession(spaceWs);
|
|
115
|
-
final project = await harness1.bridge.acquireSession(projectWs);
|
|
117
|
+
final space = await harness1.bridge.acquireSession(spaceWs, authorSecret: harness1.deviceSecret(home1.workspace));
|
|
118
|
+
final project = await harness1.bridge.acquireSession(projectWs, authorSecret: harness1.deviceSecret(home1.workspace));
|
|
116
119
|
|
|
117
120
|
// Write to space
|
|
118
121
|
await space.offerCreates(
|
|
@@ -186,11 +189,12 @@ void main() {
|
|
|
186
189
|
'frameworkHash': harness2.frameworkHash,
|
|
187
190
|
'lawHash': harness2.lawHashes(home2.workspace).first,
|
|
188
191
|
'bornAt': '2026-07-15T00:00:04Z',
|
|
192
|
+
'ownerKeyHash': sha256.convert(base64Decode(harness2.devicePublicKey(home2.workspace)!)).toString(),
|
|
189
193
|
},
|
|
190
194
|
actor: _subject2,
|
|
191
195
|
);
|
|
192
196
|
final space2Ws = space2Outcome.born.single;
|
|
193
|
-
final space2 = await harness2.bridge.acquireSession(space2Ws);
|
|
197
|
+
final space2 = await harness2.bridge.acquireSession(space2Ws, authorSecret: harness2.deviceSecret(home2.workspace));
|
|
194
198
|
|
|
195
199
|
// User2 writes to its own space while partitioned
|
|
196
200
|
await space2.offerCreates(
|
|
@@ -101,7 +101,8 @@ void main() {
|
|
|
101
101
|
// 3. THE REPAIR — idempotent; the signing directive is DISCOVERED from the installed law
|
|
102
102
|
// (this fixture composes it under domain "home", NOT the framework "workspaces").
|
|
103
103
|
final r1 = await session.ensureParentSignerEnrollment(
|
|
104
|
-
|
|
104
|
+
// era-0: BOTH domains compose enrollSigner (the estate is signer-ready too) — name the parent's.
|
|
105
|
+
parent: _ws, principal: _principal, publicKey: pub, authorSecret: secret, domain: 'home');
|
|
105
106
|
expect(r1['enrolled'], true);
|
|
106
107
|
expect(r1['repaired'], true);
|
|
107
108
|
expect(r1['domain'], 'home',
|
|
@@ -109,7 +110,7 @@ void main() {
|
|
|
109
110
|
|
|
110
111
|
// 4. Re-run = pure no-op (the idempotence contract).
|
|
111
112
|
final r2 = await session.ensureParentSignerEnrollment(
|
|
112
|
-
parent: _ws, principal: _principal, publicKey: pub, authorSecret: secret);
|
|
113
|
+
parent: _ws, principal: _principal, publicKey: pub, authorSecret: secret, domain: 'home');
|
|
113
114
|
expect(r2['enrolled'], true);
|
|
114
115
|
expect(r2['repaired'], false);
|
|
115
116
|
|
|
@@ -13,8 +13,11 @@
|
|
|
13
13
|
@TestOn('vm')
|
|
14
14
|
library;
|
|
15
15
|
|
|
16
|
+
import 'dart:convert';
|
|
16
17
|
import 'dart:io';
|
|
17
18
|
|
|
19
|
+
import 'package:crypto/crypto.dart' show sha256;
|
|
20
|
+
|
|
18
21
|
// ignore: deprecated_member_use
|
|
19
22
|
import 'package:nomos_client/nomos_headless.dart';
|
|
20
23
|
import 'package:test/test.dart';
|
|
@@ -52,6 +55,11 @@ String? _missingPrereq() {
|
|
|
52
55
|
return null;
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
/// The owner-device keyHash (the harness device the install lane minted) — the estate enrols it at birth
|
|
59
|
+
/// so the born child accepts the owner's signed writes (era-0 signer-ready births).
|
|
60
|
+
String _ownerKeyHash(NomosTestHarness h) =>
|
|
61
|
+
sha256.convert(base64Decode(h.devicePublicKey(h.workspace)!)).toString();
|
|
62
|
+
|
|
55
63
|
Future<NomosTestHarness> _harness() => NomosTestHarness.ephemeral(
|
|
56
64
|
deployJsonPaths: [_deployJson],
|
|
57
65
|
nodeEntry: _repoEntry,
|
|
@@ -86,6 +94,7 @@ void main() {
|
|
|
86
94
|
frameworkHash: harness.frameworkHash!,
|
|
87
95
|
lawHash: harness.lawHashes(harness.workspace).first,
|
|
88
96
|
bornAt: DateTime.now().toUtc().toIso8601String(),
|
|
97
|
+
ownerKeyHash: _ownerKeyHash(harness),
|
|
89
98
|
),
|
|
90
99
|
);
|
|
91
100
|
expect(birth.workspace, harness.workspace,
|
|
@@ -95,7 +104,7 @@ void main() {
|
|
|
95
104
|
expect(birth.birthOutcomes.first['status'], 'born');
|
|
96
105
|
|
|
97
106
|
// FOLLOW THE REF (ask 8): NomosRef in, mounted session out — never a string pair.
|
|
98
|
-
final estate = await harness.bridge.follow(birth.bornRefs.single);
|
|
107
|
+
final estate = await harness.bridge.follow(birth.bornRefs.single, authorSecret: harness.deviceSecret(harness.workspace));
|
|
99
108
|
expect(estate.workspace, birth.born.single);
|
|
100
109
|
|
|
101
110
|
// SECOND CLIENT + a typed read AND a directive on it.
|
|
@@ -144,6 +153,7 @@ void main() {
|
|
|
144
153
|
frameworkHash: harness.frameworkHash!,
|
|
145
154
|
lawHash: harness.lawHashes(harness.workspace).first,
|
|
146
155
|
bornAt: DateTime.now().toUtc().toIso8601String(),
|
|
156
|
+
ownerKeyHash: _ownerKeyHash(harness),
|
|
147
157
|
),
|
|
148
158
|
);
|
|
149
159
|
final rows = await first.timeout(const Duration(seconds: 30));
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# watch_surfaces_test.dart + co2_acceptance_test.dart — QUARANTINED 2026-07-16 (removed, not skipped)
|
|
2
|
+
|
|
3
|
+
The test was committed importing a generated Dart client from
|
|
4
|
+
`bench/scale/fixtures/co2-acceptance/build/dart/src/generated/estate.client.dart` — a path that has
|
|
5
|
+
NEVER existed (the fixture's dart codegen emits layered packages under
|
|
6
|
+
`dart/co2_nomos_domain_client/lib/...`), from a fixture whose SOURCE was never tracked. It could not
|
|
7
|
+
run anywhere but its author's machine, and it broke `dart analyze` (and therefore the CI dart-gauntlet)
|
|
8
|
+
for everyone else. Removing it makes CI reflect reality: the typed reactive surfaces it exercised
|
|
9
|
+
(watchSpatial bbox stream / watchCompatibility across an evolve / watchActivity) are currently
|
|
10
|
+
UNCOVERED at the Dart layer — stated here loudly, not masked.
|
|
11
|
+
|
|
12
|
+
RECONSTRUCTION (tracked task): re-author against a SMALL tracked fixture (grow
|
|
13
|
+
`bench/scale/fixtures/estate-geo` with the SiteLocation VO + a `sitesByBounds` spatial + dart codegen),
|
|
14
|
+
compile it in the gauntlet's fixture step, and import the layered client via a path dependency in a
|
|
15
|
+
dedicated test pubspec (the layered `co2_nomos_types` path-dep does not resolve from `dsl/dart` itself).
|
|
16
|
+
Original test bodies: `git show 78ccf013:dsl/dart/test/watch_surfaces_test.dart` and `git show 78ccf013:dsl/dart/test/co2_acceptance_test.dart` (same phantom import, same fixture).
|
|
17
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.97.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/build_package.ts
CHANGED
|
@@ -75,6 +75,7 @@ import { collectValueObjectDescriptors } from "./value_object.js";
|
|
|
75
75
|
import { emitUsd } from "./usd.js";
|
|
76
76
|
import { expandCapabilityExports } from "./capability_exports.js";
|
|
77
77
|
import { RelationTuple, relationTuplesByObject } from "./framework/relations.js";
|
|
78
|
+
import { AuthProvider, setAuthProvider, authProvider } from "./framework/workspaces.js";
|
|
78
79
|
import { parentAttestedBirthMarker } from "./births.js";
|
|
79
80
|
import { discoverUnsanctionedEnsures, sanctioningEnsureDirective } from "./secondary_ensure.js";
|
|
80
81
|
import { assertIdOfLint } from "./idof_lint.js";
|
|
@@ -209,7 +210,24 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
209
210
|
// insertion order does not perturb the hash. Era-safe: only a relation-using domain gains the aggregate,
|
|
210
211
|
// so no existing (relation-free) manifest hash drifts, and a tenant that already had the explicit export
|
|
211
212
|
// sees NO further move (identical bytes).
|
|
213
|
+
// AUTH-PROVIDER AUTO-COMPOSE — the "a private workspace can always declare a verifier" DX fix (kills the
|
|
214
|
+
// `no-authprovider-verifier` dead-workspace trap). A package declaring ANY private read (an aggregate
|
|
215
|
+
// `visibility: requires(cap)` — also the ONLY host for `.encrypted()` fields) MUST be able to author its
|
|
216
|
+
// identity verifier, else it is born PERMANENTLY unable to verify a reader's token. Auto-carry the framework
|
|
217
|
+
// `AuthProvider` aggregate + the admin-gated `setAuthProvider` directive (its `.requires("admin")` gate
|
|
218
|
+
// travels with it, so a SHARED private workspace can't let a non-admin swap the verifier) + the
|
|
219
|
+
// `authProvider` query — exactly as RelationTuple is carried. Placed BEFORE the usesRelations check so
|
|
220
|
+
// setAuthProvider's admin gate pulls RelationTuple in too. ERA-SAFE: a PUBLIC package declares no private
|
|
221
|
+
// aggregate, gains nothing, stays byte-identical; a private package recompiled under this DSL moves hash anyway.
|
|
222
|
+
const declaresPrivateRead = [...byId.values()].some(
|
|
223
|
+
(a) => (a as { visibility?: unknown }).visibility !== undefined,
|
|
224
|
+
);
|
|
225
|
+
if (declaresPrivateRead) {
|
|
226
|
+
if (!byId.has(AuthProvider.id)) byId.set(AuthProvider.id, AuthProvider);
|
|
227
|
+
if (!directives.some((d) => d.id === setAuthProvider.id)) directives.push(setAuthProvider);
|
|
228
|
+
}
|
|
212
229
|
const usesRelations =
|
|
230
|
+
declaresPrivateRead ||
|
|
213
231
|
(spec.relationTypes ?? []).length > 0 ||
|
|
214
232
|
directives.some((d) => (d as { requiresRelationGate?: unknown }).requiresRelationGate !== undefined) ||
|
|
215
233
|
directives.some((d) => d.aggregateId === RelationTuple.id);
|
|
@@ -311,6 +329,11 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
311
329
|
// `usesRelations` was computed above the aggregate snapshot (so RelationTuple could be injected into the
|
|
312
330
|
// schema); reuse it here to append the audit query. The two MUST agree — the aggregate and the query it
|
|
313
331
|
// returns have to enter the manifest together, or the read projection can't resolve the return schema.
|
|
332
|
+
// Auth-provider auto-compose (sibling of the RelationTuple query carry): a private package gets the
|
|
333
|
+
// `authProvider` read so the workspace can surface its declared verifier.
|
|
334
|
+
if (declaresPrivateRead && !queries.some((q) => q.id === authProvider.id)) {
|
|
335
|
+
queries.push(authProvider);
|
|
336
|
+
}
|
|
314
337
|
if (usesRelations && !queries.some((q) => q.id === relationTuplesByObject.id)) {
|
|
315
338
|
queries.push(relationTuplesByObject);
|
|
316
339
|
}
|
|
@@ -1051,7 +1051,7 @@ const NOMOS_TYPES_DEP = "^0.6.0";
|
|
|
1051
1051
|
// 0.48.x: session-bound clients (XClient.bind/NomosClientInterface/DomainClientSessionMismatch/
|
|
1052
1052
|
// DomainNotInstalledForSession, engine.follow, pendingConflicts lanes) — the generated client
|
|
1053
1053
|
// REQUIRES this surface. 0.48.1 fixes NomosSession.watchPendingConflicts delegation.
|
|
1054
|
-
const NOMOS_CLIENT_DEP = "^0.
|
|
1054
|
+
const NOMOS_CLIENT_DEP = "^0.61.0";
|
|
1055
1055
|
// DRIFT TRIPWIRE: pubspec_pins.test.ts asserts these equal the CURRENT versions in dsl/dart/pubspec.yaml
|
|
1056
1056
|
// and dsl/dart_types/pubspec.yaml — a Dart release without this bump fails the suite, so tenants never
|
|
1057
1057
|
// land back in dependency_overrides (the exact co2 report this closes, third occurrence).
|
package/src/framework/genesis.ts
CHANGED
|
@@ -81,11 +81,11 @@ export const seedGenesis = directive("seedGenesis")
|
|
|
81
81
|
.payload(
|
|
82
82
|
z.object({
|
|
83
83
|
principal: z.string().min(1),
|
|
84
|
-
// `seedGenesis`
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
84
|
+
// `seedGenesis` records the self-id the genesis recipe hands it. ERA 0 — every birth is WARRANTED:
|
|
85
|
+
// the self-id is the real workspace name (NON-EMPTY), the source binding author signatures cover, so
|
|
86
|
+
// the kernel enforces SIGNED control-plane authorship on every lane (author / receive / verify_chain).
|
|
87
|
+
// (The empty-self-id UNWARRANTED lane was the legacy host-stamped compat path — purged; no pre-VA
|
|
88
|
+
// chain survives to need it.)
|
|
89
89
|
// THE WARRANT-FLIP (THE GOVERNANCE DEFAULT, 2026-06-27): `buildGenesisChain` now seeds a NON-EMPTY
|
|
90
90
|
// self-id (the workspace name) by default for every governance/platform birth — control-plane authority
|
|
91
91
|
// is born warranted unless the recipe explicitly opts out. The choice lives in the recipe builder, not
|
package/src/framework/home.ts
CHANGED
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import { z } from "zod";
|
|
28
28
|
import { aggregate, instance } from "../aggregate.js";
|
|
29
|
+
// The device-key-hash wire shape (same validator signing_identity/workspaces use module-locally).
|
|
30
|
+
const sha256Hex = z.string().regex(/^[0-9a-f]{64}$/, "expected lowercase sha256 hex (the owner's device pubkey hash)");
|
|
29
31
|
import { t } from "../fields.js";
|
|
30
32
|
import { Lww } from "../drivers.js";
|
|
31
33
|
import { directive } from "../directive.js";
|
|
@@ -195,12 +197,21 @@ export const birthHome = directive("birthHome")
|
|
|
195
197
|
lawHash: z.string().min(1),
|
|
196
198
|
/** ISO-8601 birth time, caller-stamped (the timestamp doctrine — the plan reads no clock). */
|
|
197
199
|
bornAt: z.string().min(1),
|
|
200
|
+
/** THE OWNER'S DEVICE KEY HASH (sha256hex of the ed25519 public half — the SAME key `homeCert` is signed
|
|
201
|
+
* by and the `signer` relation enrols). The platform holds it from the user's onboarding and ENROLS it
|
|
202
|
+
* in the home's genesis, so the born home is SIGNER-READY: it can sign + birth its own children (estates,
|
|
203
|
+
* catalogues) from intent 0. WITHOUT this the home is born warranted + owned but signer-less — it can
|
|
204
|
+
* never act (the co2 estate-birth bug). Mirrors `buildGenesisChain`'s `enrollSigner` for a platform.
|
|
205
|
+
* REQUIRED — a payload without it refuses at compile/validation, not an hour into a live deploy with a
|
|
206
|
+
* bricked signer-less home. (The kernel injecting it from the verified cert's `parent_key` is the
|
|
207
|
+
* eventual generalization; until then the field is loud.) */
|
|
208
|
+
ownerKeyHash: sha256Hex,
|
|
198
209
|
/** THE DEVICE-SIGNED HOME CERT (JSON `executor::birth_cert::BirthCert`), signed CLIENT-SIDE by the
|
|
199
|
-
* owner's device key `S_U` (the SAME key the `signer` relation
|
|
200
|
-
* effect VERIFIES this cert's signature against U's enrolled signer pubkey `P_U`
|
|
201
|
-
* `founder == ctx.principal()` (the gate verdict, CAS-as-law) BEFORE accepting it as
|
|
202
|
-
* the travelling chain — the kernel signs nothing (it holds no secret; it only verifies).
|
|
203
|
-
* absent ⇒ the era stays inert for this birth (
|
|
210
|
+
* owner's device key `S_U` (the SAME key `ownerKeyHash` hashes + the `signer` relation enrols). The
|
|
211
|
+
* kernel's birth offer-effect VERIFIES this cert's signature against U's enrolled signer pubkey `P_U`
|
|
212
|
+
* AND that its `founder == ctx.principal()` (the gate verdict, CAS-as-law) BEFORE accepting it as
|
|
213
|
+
* `links[0]` of the travelling chain — the kernel signs nothing (it holds no secret; it only verifies).
|
|
214
|
+
* Optional: absent ⇒ the era stays inert for this birth (pre-feature lane). */
|
|
204
215
|
homeCert: z.string().optional(),
|
|
205
216
|
}),
|
|
206
217
|
)
|
|
@@ -214,15 +225,17 @@ export const birthHome = directive("birthHome")
|
|
|
214
225
|
{ domain: "nomos", directiveId: "installDomain", actor: founder, payload: { domainHash: p.lawHash }, domainHash: p.lawHash, domainPackageB64: "" },
|
|
215
226
|
// SEED THE OWNER — the home is born OWNED; the homeNameBindsOwner invariant in the child's law proves
|
|
216
227
|
// homeName == home-<sha256(owner)> at the child's own gate, so the slot can ONLY be its rightful owner's.
|
|
217
|
-
// THE GENESIS ORDER IS THE CONTRACT (BREAK 1): the parent's offer-effect APPENDS `seedBirthAttestation`
|
|
218
|
-
// (the travelling cert) and THEN `seedGenesis` (the WARRANT) after this step — so the warrant lands LAST,
|
|
219
|
-
// exactly like `enrollSigner → seedGenesis → grantAdmin(SIGNED)`: every genesis seed runs UNSIGNED on a
|
|
220
|
-
// still-warrant-free prefix (door + signed-author both inert), the warrant lands at the very end, and
|
|
221
|
-
// ALL post-genesis writes are then signed AND cert-gated. The home is born WARRANTED (its self-id ==
|
|
222
|
-
// homeName), so `workspace_self_id` resolves and the parent-attested-birth cert door FIRES on every
|
|
223
|
-
// post-genesis write (without a warrant the door was inert for every home it protects). ERA-SAFE: a
|
|
224
|
-
// non-AuthProvider / legacy chain never born under THIS recipe carries no warrant + replays byte-identical.
|
|
225
228
|
{ domain: HOME_LAW_DOMAIN, directiveId: "seedHomeOwner", actor: founder, payload: { principal: p.owner, homeName, bornAt: p.bornAt } },
|
|
229
|
+
// ENROL THE OWNER'S DEVICE on the warrant-free prefix (when supplied) — enrollSigner self-binds to
|
|
230
|
+
// ctx.principal() (the owner), so this binds `ownerKeyHash` as the owner's signer BEFORE the warrant
|
|
231
|
+
// closes bootstrap. This is what makes the born home SIGNER-READY (able to sign + self-serve-birth its own
|
|
232
|
+
// estates/catalogues). Mirrors buildGenesisChain's founding-admin enrollSigner for a platform — homes were
|
|
233
|
+
// the inconsistent one. THE GENESIS ORDER IS THE CONTRACT (BREAK 1): the parent's offer-effect APPENDS
|
|
234
|
+
// `seedBirthAttestation` + `seedGenesis` (the WARRANT) AFTER these steps — the warrant lands LAST (E→W
|
|
235
|
+
// ladder: every seed runs UNSIGNED on the still-warrant-free prefix; the warrant closes it). ERA-SAFE: a
|
|
236
|
+
// home born earlier (pre-feature law, no such step in its recipe) replays byte-identical — the era rule
|
|
237
|
+
// rides the LAW hash, not this branch.
|
|
238
|
+
{ domain: HOME_LAW_DOMAIN, directiveId: "enrollSigner", actor: founder, payload: { keyHash: p.ownerKeyHash, grantedBy: p.owner, grantedAt: 1 } },
|
|
226
239
|
];
|
|
227
240
|
// THE DECLARED BIRTH EFFECT (the unix model): the recipe rides the committed intent as a declared output
|
|
228
241
|
// (captured_ports.births). The kernel — reading the `.births()` declaration off the law manifest — takes
|
|
@@ -86,6 +86,12 @@ import { signerObject, SIGNER_RELATION } from "./signing_identity.js";
|
|
|
86
86
|
// them up as part of the `workspaces` domain. (architecture/platform_birth_genesis_bootstrap.md)
|
|
87
87
|
export { GenesisWarrant, seedGenesis } from "./genesis.js";
|
|
88
88
|
export { identityRelations, enrollSigner } from "./signing_identity.js";
|
|
89
|
+
// ERA 0 — the keyless verified first-device door rides the GOVERNANCE law too: a freshly BORN platform
|
|
90
|
+
// has a virgin roster and a cert-ATTESTED AuthProvider (the S_root-signed platformCert), so its founder's
|
|
91
|
+
// FIRST action is `enrollFirstDevice` (token-verified against the attested provider) followed by a SIGNED
|
|
92
|
+
// self-`grantAdmin` (admitted by the kernel genesis clause). Without this export a born platform is
|
|
93
|
+
// un-administrable — the admin bootstrap has no door.
|
|
94
|
+
export { enrollFirstDevice, revokeSigner } from "./signing_identity.js";
|
|
89
95
|
// THE BIRTH-ATTESTATION CARRIER (the K_root attestation spine for platform-hosted homes). A platform must
|
|
90
96
|
// CARRY its own `platform_cert` at `birth-attestation:self` so the kernel's `assemble_home_cert_chain`
|
|
91
97
|
// PREPENDS `[home_cert, delegation]` to it and the child home roots at K_root. Governance carries the cert
|
|
@@ -522,24 +528,6 @@ export function buildGenesisChain(p: {
|
|
|
522
528
|
* from there. Absent ⇒ the recipe resolves an already-installed law by shape (pre-G3 / test lanes). */
|
|
523
529
|
frameworkHash?: string;
|
|
524
530
|
lawHash?: string;
|
|
525
|
-
/**
|
|
526
|
-
* THE WARRANT-FLIP (VA-1, architecture/identity_keys.md). Controls whether `seedGenesis` records a REAL
|
|
527
|
-
* `workspaceId` (the workspace name as the unique self-id) so the chain is born WARRANTED — meaning
|
|
528
|
-
* `verify_captured_principal` enforces SIGNED control-plane authorship on every lane (author / edge
|
|
529
|
-
* receive / verify_chain). An empty `workspaceId` keeps the workspace UNWARRANTED (host-stamped authorship,
|
|
530
|
-
* the legacy lane), so a control-plane offer (grantAdmin etc.) is taken at its word — the forge that was
|
|
531
|
-
* open on unwarranted root/co2.
|
|
532
|
-
*
|
|
533
|
-
* THE GOVERNANCE DEFAULT (THE WARRANT-FLIP, 2026-06-27): `buildGenesisChain` only ever runs for a
|
|
534
|
-
* PLATFORM/GOVERNANCE birth (every call sits inside the `if (p.foundingAdminHash)` guard — a genesis that
|
|
535
|
-
* carries control-plane authority: grantAdmin/grantCreation). Control-plane authority is EXACTLY what the
|
|
536
|
-
* signed-author regime protects, so a governance birth is born WARRANTED by DEFAULT (`requiresWarrant`
|
|
537
|
-
* undefined/true ⇒ warranted). The ONLY escape is an explicit `requiresWarrant === false`, retained as an
|
|
538
|
-
* era-safe compat hatch (a pre-VA chain re-built byte-identical). NO flag-day: the era is per-workspace,
|
|
539
|
-
* decided at genesis — existing UNWARRANTED chains keep replaying; only newly-born/re-born governance
|
|
540
|
-
* chains arm the signature requirement.
|
|
541
|
-
*/
|
|
542
|
-
requiresWarrant?: boolean;
|
|
543
531
|
/**
|
|
544
532
|
* METERED BIRTH (slice A3 — the descendant budget). When present, the child's genesis includes a
|
|
545
533
|
* `seedBirthPolicy` step so it is BORN BOUNDED (its own default birth allowance per subject).
|
|
@@ -601,13 +589,25 @@ export function buildGenesisChain(p: {
|
|
|
601
589
|
return JSON.stringify([
|
|
602
590
|
...installSteps,
|
|
603
591
|
...poolSteps,
|
|
604
|
-
// THE
|
|
605
|
-
//
|
|
606
|
-
//
|
|
607
|
-
//
|
|
608
|
-
//
|
|
609
|
-
|
|
610
|
-
|
|
592
|
+
// ERA 0 — THE GENESIS-ORDER CONTRACT (the birthHome ordering, unified): every recipe seed below runs
|
|
593
|
+
// UNSIGNED on the still-WARRANT-FREE prefix; the recipe carries NO seedGenesis and NO grantAdmin. The
|
|
594
|
+
// KERNEL's birth effect — after verifying the offer's S_root-signed platformCert against the pinned
|
|
595
|
+
// K_root — appends `seedBirthAttestation` (the travelling cert) and THEN `seedGenesis` (the warrant,
|
|
596
|
+
// self-id == name) LAST, so the platform is born WARRANTED + ATTESTED and every post-genesis write is
|
|
597
|
+
// signed AND cert-gated. A birth whose cert fails verification seeds neither and is REFUSED atomically
|
|
598
|
+
// (the era-0 gate) — there is no unwarranted platform lane.
|
|
599
|
+
//
|
|
600
|
+
// THE FOUNDING KEY — ENROLLED ON THE WARRANT-FREE PREFIX (root's own E→W ladder, uniform): the
|
|
601
|
+
// founder's device key (`foundingAdminHash` = sha256 of its public half) self-binds BEFORE the warrant
|
|
602
|
+
// lands, exactly like root's `enrollSigner`. Once the kernel seeds the warrant LAST, the platform's
|
|
603
|
+
// signed-author regime is armed with this key already enrolled — so the FOUNDING ADMIN arrives
|
|
604
|
+
// POST-BIRTH as one SIGNED `grantAdmin` by this key, admitted by the kernel genesis clause
|
|
605
|
+
// (`genesis_admits`: a virgin #admin roster + the warrant binding the captured principal). A baked
|
|
606
|
+
// in-genesis grantAdmin can never fold (post-warrant writes need a signature the birth effect cannot
|
|
607
|
+
// produce — the pre-era-0 recipe was structurally unbornable); pre-warrant enroll + post-birth signed
|
|
608
|
+
// self-grant is the lawful shape. (The keyless variant — enrollFirstDevice against the platformCert's
|
|
609
|
+
// ATTESTED AuthProvider — remains for founders with an IdP instead of a key.)
|
|
610
|
+
{ domain: "workspaces", directiveId: "enrollSigner", actor: founder, payload: { keyHash: p.foundingAdminHash, grantedBy: p.principal, grantedAt: p.requestedAt } },
|
|
611
611
|
// No `role` — it was a vestigial node-class label (collapsed with `kind`); birthCloud ignores it now.
|
|
612
612
|
{ domain: "workspaces", directiveId: "birthCloud", actor: "", payload: { principal: p.principal, name: p.name, tier: p.tier, bornAt: p.requestedAt, bornUnder, ...(p.rebornFrom ? { rebornFrom: p.rebornFrom } : {}) } },
|
|
613
613
|
]);
|
|
@@ -1263,10 +1263,13 @@ export const birthChild = directive("birthChild")
|
|
|
1263
1263
|
* own custody (never the host). Absent ⇒ the recipe resolves an already-installed law by shape. */
|
|
1264
1264
|
frameworkHash: sha256Hex.optional(),
|
|
1265
1265
|
lawHash: sha256Hex.optional(),
|
|
1266
|
-
/** THE
|
|
1267
|
-
*
|
|
1268
|
-
*
|
|
1269
|
-
|
|
1266
|
+
/** THE PLATFORM ATTESTATION (era 0 — one attested birth lane for every holon kind): the 1-link
|
|
1267
|
+
* platform_cert JSON (`executor::birth_cert::BirthCert`), SIGNED OFFLINE BY S_root (the K_root
|
|
1268
|
+
* holder). The kernel's birth effect VERIFIES it (child == name, founder == `user:<principal>`,
|
|
1269
|
+
* signature vs the pinned K_root) and injects `seedBirthAttestation` + `seedGenesis` (the warrant)
|
|
1270
|
+
* LAST into the child's genesis. REQUIRED for a genesis birth to complete — a birth without a
|
|
1271
|
+
* valid cert is refused atomically (no unwarranted platform lane survives era 0). */
|
|
1272
|
+
platformCert: z.string().optional(),
|
|
1270
1273
|
/** METERED BIRTH (slice A3): the child's own descendant budget — when present, the genesis
|
|
1271
1274
|
* recipe seeds the child's BirthPolicy (`seedBirthPolicy`) so it is BORN BOUNDED. OPT-IN:
|
|
1272
1275
|
* only pass it when the `lawHash` the order names carries `seedBirthPolicy` (a governance
|
|
@@ -1292,11 +1295,9 @@ export const birthChild = directive("birthChild")
|
|
|
1292
1295
|
// LAW AUTHORS THE GENESIS RECIPE (P1): a genesis birth carries its child's [installDomain×2 →]
|
|
1293
1296
|
// seedGenesis→grantAdmin→birthCloud chain. (refine guarantees principal + bornAt here.)
|
|
1294
1297
|
if (p.foundingAdminHash) {
|
|
1295
|
-
//
|
|
1296
|
-
//
|
|
1297
|
-
|
|
1298
|
-
// hatch (an era-safe legacy re-build).
|
|
1299
|
-
const genesisChain = buildGenesisChain({ name: p.name, principal: p.principal, parent: p.parent, tier: p.tier, foundingAdminHash: p.foundingAdminHash, bornAt: p.bornAt as string, requestedAt: p.requestedAt, ...(p.frameworkHash ? { frameworkHash: p.frameworkHash } : {}), ...(p.lawHash ? { lawHash: p.lawHash } : {}), ...(p.requiresWarrant !== undefined ? { requiresWarrant: p.requiresWarrant } : {}), ...(p.descendantPool !== undefined ? { descendantPool: p.descendantPool } : {}) });
|
|
1298
|
+
// ERA 0 — every governance/platform birth is WARRANTED (no compat hatch to thread; buildGenesisChain
|
|
1299
|
+
// always records the real self-id).
|
|
1300
|
+
const genesisChain = buildGenesisChain({ name: p.name, principal: p.principal, parent: p.parent, tier: p.tier, foundingAdminHash: p.foundingAdminHash, bornAt: p.bornAt as string, requestedAt: p.requestedAt, ...(p.frameworkHash ? { frameworkHash: p.frameworkHash } : {}), ...(p.lawHash ? { lawHash: p.lawHash } : {}), ...(p.descendantPool !== undefined ? { descendantPool: p.descendantPool } : {}) });
|
|
1300
1301
|
// THE GENESIS RECIPE IS ALSO A LINEAGE/AUDIT FACT on the parent's chain (the parent records what it
|
|
1301
1302
|
// ORDERED — law naming law, legitimate). The kernel does NOT read this field to detect the birth.
|
|
1302
1303
|
rec.set("genesisChain", genesisChain);
|
|
@@ -1,101 +0,0 @@
|
|
|
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.client.dart';
|
|
14
|
-
|
|
15
|
-
String get _root => '${Directory.current.path}/../..';
|
|
16
|
-
String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.ts';
|
|
17
|
-
String get _deployJson =>
|
|
18
|
-
'$_root/bench/scale/fixtures/co2-acceptance/build/co2-acceptance.deploy.json';
|
|
19
|
-
String get _runtimeDir => '${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(_deployJson).existsSync()) {
|
|
24
|
-
return 'compile the fixture: cd bench/scale/fixtures/co2-acceptance && 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('co2 acceptance: rich typed VO construction → multi-aggregate write → typed reads',
|
|
35
|
-
() async {
|
|
36
|
-
final skip = _missingPrereq();
|
|
37
|
-
if (skip != null) {
|
|
38
|
-
markTestSkipped(skip);
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
final harness = await NomosTestHarness.ephemeral(
|
|
42
|
-
deployJsonPaths: [_deployJson],
|
|
43
|
-
nodeEntry: _repoEntry,
|
|
44
|
-
runtimeDir: _runtimeDir,
|
|
45
|
-
workspace: 'co2-acceptance',
|
|
46
|
-
);
|
|
47
|
-
try {
|
|
48
|
-
final estate = EstateClient(harness.bridge, actor: 'user:ada');
|
|
49
|
-
|
|
50
|
-
// Register a site whose location composes address + geo point (nested VOs).
|
|
51
|
-
await estate.registerSite(RegisterSitePayload(
|
|
52
|
-
name: 'Trafford Depot',
|
|
53
|
-
location: SiteLocation(
|
|
54
|
-
address: PostalAddress(
|
|
55
|
-
line1: '1 Wharf Rd', city: 'Manchester', postcode: 'M17 1AB', countryCode: 'GB'),
|
|
56
|
-
point: GeoPoint(coordinates: [-2.24, 53.48]),
|
|
57
|
-
source: SiteLocationSource.addressSearch,
|
|
58
|
-
),
|
|
59
|
-
registeredAt: DateTime.now().toUtc().toIso8601String(),
|
|
60
|
-
));
|
|
61
|
-
await estate.addDrawing(AddDrawingPayload(
|
|
62
|
-
title: 'Level 2 plan', addedAt: DateTime.now().toUtc().toIso8601String()));
|
|
63
|
-
|
|
64
|
-
final drawings = await estate.drawingsByTitle(title: 'Level 2 plan');
|
|
65
|
-
expect(drawings, hasLength(1));
|
|
66
|
-
final drawingId = drawings.single.aggregateId.value;
|
|
67
|
-
final sites = await estate.sitesByName(name: 'Trafford Depot');
|
|
68
|
-
expect(sites, hasLength(1));
|
|
69
|
-
|
|
70
|
-
// ── THEIR SNIPPET, VERBATIM SHAPE ──
|
|
71
|
-
await estate.recordDrawingGeoreference(RecordDrawingGeoreferencePayload(
|
|
72
|
-
drawingId: drawingId,
|
|
73
|
-
siteId: sites.single.aggregateId.value,
|
|
74
|
-
georeference: DrawingGeoreference(
|
|
75
|
-
transform: DrawingTransform2d(rotationDeg: 2.4),
|
|
76
|
-
controlPoints: [
|
|
77
|
-
DrawingGeoControlPoint(
|
|
78
|
-
drawingPosition: DrawingPositionMm(xMm: 1200, yMm: 800),
|
|
79
|
-
geoPoint: GeoPoint(coordinates: [-2.24, 53.48]),
|
|
80
|
-
),
|
|
81
|
-
],
|
|
82
|
-
),
|
|
83
|
-
recordedAt: DateTime.now().toUtc().toIso8601String(),
|
|
84
|
-
));
|
|
85
|
-
|
|
86
|
-
// Primary aggregate: the full nested VO reads back TYPED.
|
|
87
|
-
final georefs = await estate.georefsByDrawing(drawingId: drawingId);
|
|
88
|
-
expect(georefs, hasLength(1));
|
|
89
|
-
final g = georefs.single.georeference!;
|
|
90
|
-
expect(g.transform!.rotationDeg, 2.4);
|
|
91
|
-
expect(g.controlPoints.single.drawingPosition.xMm, 1200);
|
|
92
|
-
expect(g.controlPoints.single.geoPoint.coordinates.first, -2.24);
|
|
93
|
-
|
|
94
|
-
// THE SIBLING WRITE: EstateDrawing.transform got the VO subfield, typed.
|
|
95
|
-
final again = await estate.drawingsByTitle(title: 'Level 2 plan');
|
|
96
|
-
expect(again.single.transform!.rotationDeg, 2.4);
|
|
97
|
-
} finally {
|
|
98
|
-
await harness.dispose();
|
|
99
|
-
}
|
|
100
|
-
}, timeout: const Timeout(Duration(minutes: 3)));
|
|
101
|
-
}
|