@githolon/dsl 0.103.3 → 0.103.5

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.
Files changed (42) hide show
  1. package/dart/CHANGELOG.md +15 -0
  2. package/dart/lib/src/runner/node-entry.mjs +411 -149
  3. package/dart/lib/src/runner/offline-install.mjs +222 -99
  4. package/dart/lib/src/session_bound.dart +111 -2
  5. package/dart/lib/src/test_harness.dart +117 -66
  6. package/dart/lib/src/webview_bridge.dart +726 -61
  7. package/dart/pubspec.lock +1 -1
  8. package/dart/pubspec.yaml +4 -5
  9. package/dart/test/acquire_session_test.dart +62 -5
  10. package/dart/test/cloud_bound_topology_test.dart +59 -38
  11. package/dart/test/session_bound_test.dart +25 -10
  12. package/dart/test/webview_bridge_test.dart +357 -9
  13. package/dart_types/CHANGELOG.md +5 -0
  14. package/dart_types/lib/nomos_types.dart +1 -0
  15. package/dart_types/lib/src/git_object_id.dart +37 -0
  16. package/dart_types/pubspec.yaml +1 -1
  17. package/package.json +1 -1
  18. package/runtime/generated-application-host.js +425 -145
  19. package/src/births.ts +16 -1
  20. package/src/business_birth.ts +67 -46
  21. package/src/codegen_dart.ts +68 -12
  22. package/src/codegen_dart_application.ts +114 -19
  23. package/src/codegen_dart_client.ts +149 -6
  24. package/src/codegen_proof.ts +16 -9
  25. package/src/codegen_provider_dart.ts +5 -0
  26. package/src/codegen_ts.ts +115 -19
  27. package/src/compile_package_main.ts +142 -40
  28. package/src/fields.ts +43 -0
  29. package/src/framework/attested_receipts.ts +34 -1
  30. package/src/framework/domain_lifecycle.ts +140 -83
  31. package/src/framework/genesis.ts +2 -1
  32. package/src/framework/home.ts +20 -45
  33. package/src/framework/workspaces.ts +62 -29
  34. package/src/idof_lint.ts +65 -51
  35. package/src/index.ts +21 -1
  36. package/src/intent.ts +11 -1
  37. package/src/law_graph.ts +81 -11
  38. package/src/law_graph_usda.ts +45 -54
  39. package/src/relation_schema.ts +18 -0
  40. package/src/shape_compat.ts +3 -0
  41. package/src/stable_ids_for_module.ts +7 -1
  42. package/src/value_object.ts +40 -0
@@ -20,42 +20,119 @@
20
20
  import { z } from "zod";
21
21
  import { aggregate, instance } from "../aggregate.js";
22
22
  import { Lww, MapOf } from "../drivers.js";
23
- import { attributionField, t } from "../fields.js";
23
+ import {
24
+ attributionField,
25
+ gitObjectId,
26
+ sha256Digest,
27
+ t,
28
+ type Sha256Digest,
29
+ } from "../fields.js";
24
30
  import { directive } from "../directive.js";
25
31
  import { set, withMarker, addToSet, type PlannedOp } from "../ops.js";
26
- import { key, relationType, self, user } from "../relation_schema.js";
32
+ import { key, relationRef, relationType, self, user } from "../relation_schema.js";
33
+
34
+ /** The two authenticated capabilities which may execute a lifecycle install.
35
+ * Callers choose a role; the serialized USDA route is an implementation detail. */
36
+ export enum DomainInstallExecutor {
37
+ StageZero = "bootstrap",
38
+ LifecycleController = "nomos",
39
+ }
40
+
41
+ /** One typed vocabulary shared by declaration, gate, genesis seat and generated clients. */
42
+ export const MODEL_LIFECYCLE_AUTHORITY = relationRef("installDomain");
43
+ export const DOMAIN_LIFECYCLE_DIRECTIVES = {
44
+ Install: "installDomain",
45
+ Stage: "stageDomain",
46
+ Promote: "promoteDomain",
47
+ Rollback: "rollbackDomain",
48
+ } as const;
49
+ export type DomainLifecycleDirectiveId =
50
+ (typeof DOMAIN_LIFECYCLE_DIRECTIVES)[keyof typeof DOMAIN_LIFECYCLE_DIRECTIVES];
51
+ export const INSTALL_DOMAIN_DIRECTIVE = DOMAIN_LIFECYCLE_DIRECTIVES.Install;
52
+
53
+ /** A symbolic reference resolved by the kernel against the parent's active law frontier. */
54
+ export type CurrentLawReference = string & { readonly __nomosCurrentLawReference: unique symbol };
55
+ export type LawClosureReference = Sha256Digest | CurrentLawReference;
56
+
57
+ const CURRENT_LAW_PREFIX = "nomos:current-law:";
58
+
59
+ /** Construct the only symbolic law reference admitted in a child genesis recipe. */
60
+ export function currentLawReference(domain: string): CurrentLawReference {
61
+ const name = domain.trim();
62
+ if (name.length === 0) throw new TypeError("a current-law reference requires a non-empty domain");
63
+ return `${CURRENT_LAW_PREFIX}${name}` as CurrentLawReference;
64
+ }
65
+
66
+ /** One complete, contract-shaped law-install call in a child genesis sequence. Keeping this
67
+ * constructor beside the directive schema makes omission of required install provenance a type
68
+ * error at every framework birth site instead of a late kernel repair. */
69
+ export interface DomainInstallGenesisOffer extends Readonly<Record<string, unknown>> {
70
+ readonly domain: DomainInstallExecutor;
71
+ readonly directiveId: typeof INSTALL_DOMAIN_DIRECTIVE;
72
+ readonly actor: string;
73
+ readonly payload: {
74
+ readonly domainHash: LawClosureReference;
75
+ readonly installedBy: string;
76
+ readonly lawClosureIdentity: LawClosureReference;
77
+ };
78
+ readonly domainHash: LawClosureReference;
79
+ }
80
+
81
+ export function domainInstallGenesisOffer(
82
+ domain: DomainInstallExecutor,
83
+ domainHash: LawClosureReference,
84
+ actor: string,
85
+ ): DomainInstallGenesisOffer {
86
+ return {
87
+ domain,
88
+ directiveId: INSTALL_DOMAIN_DIRECTIVE,
89
+ actor,
90
+ payload: { domainHash, installedBy: actor, lawClosureIdentity: domainHash },
91
+ domainHash,
92
+ };
93
+ }
27
94
 
28
95
  /** Framework-global authority to install or roll out a model in this workspace.
29
96
  * The verified founder receives the first leaf in `seedGenesis`; every later
30
97
  * delegation remains an ordinary, replayable Zanzibar edge. */
31
98
  export const modelLifecycleRelations = relationType("workspace")
32
99
  .asGlobal()
33
- .relation("installDomain", { rewrite: self(), allow: [user(), key()] });
100
+ .relation(MODEL_LIFECYCLE_AUTHORITY, { rewrite: self(), allow: [user(), key()] });
101
+
102
+ /** The lifecycle state machine is a framework type, not a bag of coincidentally matching strings. */
103
+ export enum DomainInstallationPhase {
104
+ Pending = "Pending",
105
+ Active = "Active",
106
+ Disabled = "Disabled",
107
+ Superseded = "Superseded",
108
+ Retiring = "Retiring",
109
+ Retired = "Retired",
110
+ Failed = "Failed",
111
+ }
34
112
 
35
113
  export const DOMAIN_INSTALLATION_PHASES = [
36
- "Pending",
37
- "Active",
38
- "Disabled",
39
- "Superseded",
40
- "Retiring",
41
- "Retired",
42
- "Failed",
114
+ DomainInstallationPhase.Pending,
115
+ DomainInstallationPhase.Active,
116
+ DomainInstallationPhase.Disabled,
117
+ DomainInstallationPhase.Superseded,
118
+ DomainInstallationPhase.Retiring,
119
+ DomainInstallationPhase.Retired,
120
+ DomainInstallationPhase.Failed,
43
121
  ] as const;
44
- export type DomainInstallationPhase = (typeof DOMAIN_INSTALLATION_PHASES)[number];
45
122
 
46
123
  export const DomainInstallation = aggregate("DomainInstallation", {
47
- "spec.domainHash": t.string().merge(Lww),
48
- "spec.packageHash": t.string().merge(Lww),
124
+ "spec.domainHash": t.sha256Digest().merge(Lww),
125
+ "spec.packageHash": t.sha256Digest().merge(Lww),
49
126
  "spec.authorityScope": t.string().merge(Lww),
50
127
  "spec.installedBy": t.string().merge(Lww),
51
- "spec.lawClosureIdentity": t.string().merge(Lww),
128
+ "spec.lawClosureIdentity": t.sha256Digest().merge(Lww),
52
129
  "spec.dependencies": t.set(t.string()),
53
- "spec.supersedes": t.string().merge(Lww).optional(),
130
+ "spec.supersedes": t.sha256Digest().merge(Lww).optional(),
54
131
  "spec.finalizers": t.set(t.string()),
55
132
  "metadata.generation": t.int().merge(Lww),
56
133
  "status.phase": t.enum(DOMAIN_INSTALLATION_PHASES).merge(Lww),
57
134
  "status.observedGeneration": t.int().merge(Lww),
58
- "status.supersededBy": t.string().merge(Lww).optional(),
135
+ "status.supersededBy": t.sha256Digest().merge(Lww).optional(),
59
136
  "status.conditions": t.map(t.json()).merge(MapOf(Lww)),
60
137
  }).public();
61
138
 
@@ -69,13 +146,13 @@ export const RELEASE_RECEIPT_AGGREGATE = "nomos_internal_ReleaseReceipt_6f0d3a";
69
146
  export const RELEASE_RECEIPT_PREFIX = "nomos-release-receipt:";
70
147
 
71
148
  export const ReleaseReceipt = aggregate(RELEASE_RECEIPT_AGGREGATE, {
72
- candidateHash: t.string().merge(Lww),
73
- fromHash: t.string().merge(Lww).optional(),
149
+ candidateHash: t.sha256Digest().merge(Lww),
150
+ fromHash: t.sha256Digest().merge(Lww).optional(),
74
151
  domain: t.string().merge(Lww),
75
- atHash: t.string().merge(Lww),
76
- atStateRoot: t.string().merge(Lww),
77
- historyHash: t.string().merge(Lww),
78
- reportHash: t.string().merge(Lww),
152
+ atHash: t.gitObjectId().merge(Lww),
153
+ atStateRoot: t.gitObjectId().merge(Lww),
154
+ historyHash: t.sha256Digest().merge(Lww),
155
+ reportHash: t.sha256Digest().merge(Lww),
79
156
  reportJson: t.string().merge(Lww),
80
157
  complete: t.bool(),
81
158
  evolveOk: t.bool(),
@@ -84,26 +161,26 @@ export const ReleaseReceipt = aggregate(RELEASE_RECEIPT_AGGREGATE, {
84
161
  replayedIntents: t.int().merge(Lww),
85
162
  candidateIntents: t.int().merge(Lww),
86
163
  divergenceCount: t.int().merge(Lww),
87
- baselineRoot: t.string().merge(Lww),
88
- candidateRoot: t.string().merge(Lww),
164
+ baselineRoot: t.sha256Digest().merge(Lww),
165
+ candidateRoot: t.sha256Digest().merge(Lww),
89
166
  acceptanceReason: t.string().merge(Lww).optional(),
90
167
  acceptedBy: attributionField(t.string().merge(Lww)),
91
168
  acceptedAt: t.int().merge(Lww),
92
169
  }).public();
93
170
 
94
- export function domainInstallationAggregateId(domainHash: string): string {
171
+ export function domainInstallationAggregateId(domainHash: Sha256Digest): string {
95
172
  return `domain-installation:${domainHash}`;
96
173
  }
97
174
 
98
- function domainInstallationInstance(domainHash: string) {
175
+ function domainInstallationInstance(domainHash: Sha256Digest) {
99
176
  return instance(DomainInstallation, domainInstallationAggregateId(domainHash));
100
177
  }
101
178
 
102
- export function releaseReceiptAggregateId(reportHash: string): string {
179
+ export function releaseReceiptAggregateId(reportHash: Sha256Digest): string {
103
180
  return `${RELEASE_RECEIPT_PREFIX}${reportHash}`;
104
181
  }
105
182
 
106
- function releaseReceiptInstance(reportHash: string) {
183
+ function releaseReceiptInstance(reportHash: Sha256Digest) {
107
184
  return instance(ReleaseReceipt, releaseReceiptAggregateId(reportHash));
108
185
  }
109
186
 
@@ -114,13 +191,13 @@ const dispositions = z.object({
114
191
  ackBy: z.string().optional(),
115
192
  }).optional();
116
193
 
117
- const lowerHex64 = z.string().regex(/^[0-9a-f]{64}$/);
118
- const gitOid = z.string().regex(/^[0-9a-f]{40}$/);
194
+ const lowerHex64 = sha256Digest();
195
+ const gitOid = gitObjectId();
119
196
  const preflightReceipt = z.object({
120
197
  candidateHash: lowerHex64,
121
198
  domain: z.string().min(1),
122
199
  atHash: gitOid,
123
- atStateRoot: lowerHex64,
200
+ atStateRoot: gitOid,
124
201
  historyHash: lowerHex64,
125
202
  reportHash: lowerHex64,
126
203
  reportJson: z.string().min(2),
@@ -144,36 +221,29 @@ const preflightReceipt = z.object({
144
221
  }
145
222
  });
146
223
 
147
- const packagePayload = {
224
+ const installPackagePayload = {
148
225
  domainHash: lowerHex64,
149
226
  installedBy: z.string(),
150
227
  lawClosureIdentity: lowerHex64,
151
228
  authorityScope: z.string().default("workspace/root"),
152
229
  // Omission means the empty set; keep that in the plan instead of encoding a composite default
153
230
  // into canonical USDA merely to save `?? []` at runtime.
154
- dependencies: z.array(z.string()).optional(),
155
- supersedes: z.string().optional(),
156
- finalizers: z.array(z.string()).optional(),
231
+ dependencies: z.array(sha256Digest()).optional(),
232
+ finalizers: z.array(sha256Digest()).optional(),
233
+ };
234
+
235
+ const stagedPackagePayload = {
236
+ ...installPackagePayload,
237
+ supersedes: sha256Digest().optional(),
157
238
  };
158
239
 
159
- export const installDomain = directive("installDomain")
240
+ export const installDomain = directive(DOMAIN_LIFECYCLE_DIRECTIVES.Install)
160
241
  .ensures(DomainInstallation)
161
- .payload(
162
- z.object({
163
- ...packagePayload,
164
- // THE FRONTIER TRANSITION (install-lifecycle): `replaces` names a PRIOR install this
165
- // deploy makes current UNEQUIVOCALLY — the prior install is flipped out of the served
166
- // frontier (`Superseded`, or `Retired` when `retireReplaced`) and gets the reverse
167
- // `status.supersededBy` edge to this install. Absent ⇒ install-BESIDE (backward-compat:
168
- // byte-identical ops to the pre-lifecycle plan). `replaces` also sets `spec.supersedes`.
169
- replaces: z.string().optional(),
170
- retireReplaced: z.boolean().default(false),
171
- dispositions,
172
- }).refine((p) => p.replaces === undefined || p.replaces !== p.domainHash, {
173
- message: "a domain package cannot replace itself",
174
- path: ["replaces"],
175
- }),
176
- )
242
+ // Installation adds law for domain keys that are not currently served. Replacement is a
243
+ // different operation with different evidence: stageDomain followed by promoteDomain. Keeping
244
+ // predecessor/retirement fields out of this type makes bypassing that state machine impossible
245
+ // for generated clients instead of relying on callers to remember a convention.
246
+ .payload(z.object({ ...installPackagePayload }).strict())
177
247
  .plan((p): PlannedOp[] => {
178
248
  const install = domainInstallationInstance(p.domainHash);
179
249
  const ops: PlannedOp[] = [
@@ -183,38 +253,25 @@ export const installDomain = directive("installDomain")
183
253
  set(install, "spec.installedBy", p.installedBy),
184
254
  set(install, "spec.lawClosureIdentity", p.lawClosureIdentity),
185
255
  set(install, "metadata.generation", 1),
186
- set(install, "status.phase", "Active"),
256
+ set(install, "status.phase", DomainInstallationPhase.Active),
187
257
  set(install, "status.observedGeneration", 1),
188
258
  ];
189
259
  if ((p.dependencies ?? []).length > 0) {
190
260
  ops.push(addToSet(install, "spec.dependencies", p.dependencies ?? []));
191
261
  }
192
- // The predecessor back-link: `supersedes` (explicit) or `replaces` (the actionable form).
193
- const supersedes = p.supersedes ?? p.replaces;
194
- if (supersedes !== undefined) {
195
- ops.push(set(install, "spec.supersedes", supersedes));
196
- }
197
- // The transition on the PRIOR install: drop it from the served frontier + point it here.
198
- if (p.replaces !== undefined) {
199
- const replaced = domainInstallationInstance(p.replaces);
200
- ops.push(
201
- set(replaced, "status.phase", p.retireReplaced ? "Retired" : "Superseded"),
202
- );
203
- ops.push(set(replaced, "status.supersededBy", p.domainHash));
204
- }
205
262
  if ((p.finalizers ?? []).length > 0) {
206
263
  ops.push(addToSet(install, "spec.finalizers", p.finalizers ?? []));
207
264
  }
208
265
  return ops;
209
266
  })
210
- .requires("installDomain");
267
+ .requires(MODEL_LIFECYCLE_AUTHORITY);
211
268
 
212
269
  /** Store candidate law in the ledger without changing the served law frontier. The
213
270
  * kernel content-verifies the package and lifecycle pair, while the Pending phase is
214
271
  * deliberately invisible to dispatch, reads and replay's current-law map. */
215
- export const stageDomain = directive("stageDomain")
272
+ export const stageDomain = directive(DOMAIN_LIFECYCLE_DIRECTIVES.Stage)
216
273
  .ensures(DomainInstallation)
217
- .payload(z.object({ ...packagePayload }).refine(
274
+ .payload(z.object({ ...stagedPackagePayload }).refine(
218
275
  (p) => p.supersedes === undefined || p.supersedes !== p.domainHash,
219
276
  { message: "a staged domain package cannot supersede itself", path: ["supersedes"] },
220
277
  ))
@@ -227,7 +284,7 @@ export const stageDomain = directive("stageDomain")
227
284
  set(install, "spec.installedBy", p.installedBy),
228
285
  set(install, "spec.lawClosureIdentity", p.lawClosureIdentity),
229
286
  set(install, "metadata.generation", 1),
230
- set(install, "status.phase", "Pending"),
287
+ set(install, "status.phase", DomainInstallationPhase.Pending),
231
288
  set(install, "status.observedGeneration", 0),
232
289
  ];
233
290
  if ((p.dependencies ?? []).length > 0) ops.push(addToSet(install, "spec.dependencies", p.dependencies ?? []));
@@ -235,17 +292,17 @@ export const stageDomain = directive("stageDomain")
235
292
  if ((p.finalizers ?? []).length > 0) ops.push(addToSet(install, "spec.finalizers", p.finalizers ?? []));
236
293
  return ops;
237
294
  })
238
- .requires("installDomain");
295
+ .requires(MODEL_LIFECYCLE_AUTHORITY);
239
296
 
240
297
  /** Make a staged package current. The package bytes are already committed; the kernel
241
298
  * re-loads them from prefix state and runs the evolve gate on this phase transition.
242
299
  * The prior version stays Active by default so gradual-rollout clients can still have
243
300
  * their declared era checked; the newer activation stamp alone makes this law current. */
244
- export const promoteDomain = directive("promoteDomain")
301
+ export const promoteDomain = directive(DOMAIN_LIFECYCLE_DIRECTIVES.Promote)
245
302
  .mutates(DomainInstallation)
246
303
  .payload(z.object({
247
- domainHash: z.string(),
248
- replaces: z.string().optional(),
304
+ domainHash: sha256Digest(),
305
+ replaces: sha256Digest().optional(),
249
306
  retireReplaced: z.boolean().default(false),
250
307
  dispositions,
251
308
  preflight: preflightReceipt,
@@ -260,7 +317,7 @@ export const promoteDomain = directive("promoteDomain")
260
317
  const promoted = domainInstallationInstance(p.domainHash);
261
318
  const receipt = releaseReceiptInstance(p.preflight.reportHash);
262
319
  const ops: PlannedOp[] = [
263
- set(promoted, "status.phase", "Active"),
320
+ set(promoted, "status.phase", DomainInstallationPhase.Active),
264
321
  set(promoted, "status.observedGeneration", 1),
265
322
  withMarker(set(receipt, "candidateHash", p.domainHash), "ensures"),
266
323
  ...(p.replaces !== undefined ? [set(receipt, "fromHash", p.replaces)] : []),
@@ -289,23 +346,23 @@ export const promoteDomain = directive("promoteDomain")
289
346
  const replaced = domainInstallationInstance(p.replaces);
290
347
  ops.push(set(promoted, "spec.supersedes", p.replaces));
291
348
  if (p.retireReplaced) {
292
- ops.push(set(replaced, "status.phase", "Retired"));
349
+ ops.push(set(replaced, "status.phase", DomainInstallationPhase.Retired));
293
350
  ops.push(set(replaced, "status.supersededBy", p.domainHash));
294
351
  }
295
352
  }
296
353
  return ops;
297
354
  })
298
355
  .alsoEnsures(ReleaseReceipt)
299
- .requires("installDomain");
356
+ .requires(MODEL_LIFECYCLE_AUTHORITY);
300
357
 
301
358
  /** Rollback is a new governed transition, never a history-pointer rewind. The prior
302
359
  * package is promoted from committed custody and the failed candidate becomes Disabled;
303
360
  * the kernel applies the same evolve judgment in the reverse direction. */
304
- export const rollbackDomain = directive("rollbackDomain")
361
+ export const rollbackDomain = directive(DOMAIN_LIFECYCLE_DIRECTIVES.Rollback)
305
362
  .mutates(DomainInstallation)
306
363
  .payload(z.object({
307
- candidateHash: z.string(),
308
- priorHash: z.string(),
364
+ candidateHash: sha256Digest(),
365
+ priorHash: sha256Digest(),
309
366
  dispositions,
310
367
  preflight: preflightReceipt,
311
368
  }).refine((p) => p.candidateHash !== p.priorHash, {
@@ -323,9 +380,9 @@ export const rollbackDomain = directive("rollbackDomain")
323
380
  const prior = domainInstallationInstance(p.priorHash);
324
381
  const receipt = releaseReceiptInstance(p.preflight.reportHash);
325
382
  return [
326
- set(candidate, "status.phase", "Disabled"),
383
+ set(candidate, "status.phase", DomainInstallationPhase.Disabled),
327
384
  set(candidate, "status.supersededBy", p.priorHash),
328
- set(prior, "status.phase", "Active"),
385
+ set(prior, "status.phase", DomainInstallationPhase.Active),
329
386
  set(prior, "status.observedGeneration", 1),
330
387
  withMarker(set(receipt, "candidateHash", p.priorHash), "ensures"),
331
388
  set(receipt, "fromHash", p.candidateHash),
@@ -352,4 +409,4 @@ export const rollbackDomain = directive("rollbackDomain")
352
409
  ];
353
410
  })
354
411
  .alsoEnsures(ReleaseReceipt)
355
- .requires("installDomain");
412
+ .requires(MODEL_LIFECYCLE_AUTHORITY);
@@ -34,6 +34,7 @@ import { Lww } from "../drivers.js";
34
34
  import { directive } from "../directive.js";
35
35
  import { set, type PlannedOp } from "../ops.js";
36
36
  import { RelationTuple, writeTuple } from "./relations.js";
37
+ import { MODEL_LIFECYCLE_AUTHORITY } from "./domain_lifecycle.js";
37
38
 
38
39
  /**
39
40
  * Deterministic singleton id of the founding warrant — MUST equal
@@ -100,7 +101,7 @@ export const seedGenesis = directive("seedGenesis")
100
101
  ...writeGenesisWarrant(p),
101
102
  ...writeTuple({
102
103
  object: "workspace:self",
103
- relation: "installDomain",
104
+ relation: MODEL_LIFECYCLE_AUTHORITY,
104
105
  subject: /^(user|key):/.test(p.principal) ? p.principal : `user:${p.principal}`,
105
106
  grantedBy: p.principal,
106
107
  grantedAt: p.bornAt,
@@ -27,9 +27,7 @@
27
27
  import { z } from "zod";
28
28
  import type { ApplicationHomeGenesis } from "../application_home_genesis.js";
29
29
  import { aggregate, instance, OWNED_READ_RELATION } from "../aggregate.js";
30
- // The device-key-hash wire shape (same validator signing_identity/workspaces use module-locally).
31
- const sha256Hex = z.string().regex(/^[0-9a-f]{64}$/, "expected lowercase sha256 hex (the owner's device pubkey hash)");
32
- import { t } from "../fields.js";
30
+ import { sha256Digest, t } from "../fields.js";
33
31
  import { Lww } from "../drivers.js";
34
32
  import { directive } from "../directive.js";
35
33
  import { ensure } from "../authoring.js";
@@ -45,6 +43,10 @@ import { relationType, self, user, selfObject } from "../relation_schema.js";
45
43
  import { requireParentAttestedBirth, type ParentAttestedBirthMarker } from "../births.js";
46
44
  import { GenesisWarrant, seedGenesis } from "./genesis.js";
47
45
  import { idOf } from "../value_object.js";
46
+ import {
47
+ DomainInstallExecutor,
48
+ domainInstallGenesisOffer,
49
+ } from "./domain_lifecycle.js";
48
50
 
49
51
  // `nomos.sha256` — the TRUSTLESS, pure hash computed IN the githolon (Rust sha2 bound in the sealed engine;
50
52
  // see deterministic-rquickjs). Available as a global to plans + invariant predicates (like `nomos.mint`); it
@@ -315,27 +317,11 @@ export function birthReceivingGroupForDomain(
315
317
  owner: z.string().min(1),
316
318
  /** The bootstrap framework controller content hash — tenant-pinned at governance compile (the child
317
319
  * installs it IN-CHAIN as intent 0; the kernel resolves the bytes by hash from the parent's custody). */
318
- frameworkHash: z.string().min(1),
320
+ frameworkHash: sha256Digest(),
319
321
  /** The HOME law package domainHash — tenant-pinned at governance compile (the child's second install). */
320
- lawHash: z.string().min(1),
322
+ lawHash: sha256Digest(),
321
323
  /** ISO-8601 birth time, caller-stamped (the timestamp doctrine — the plan reads no clock). */
322
324
  bornAt: z.string().min(1),
323
- /** THE OWNER'S DEVICE KEY HASH (sha256hex of the ed25519 public half — the SAME key `homeCert` is signed
324
- * by and the `signer` relation enrols). The platform holds it from the user's onboarding and ENROLS it
325
- * in the home's genesis, so the born home is SIGNER-READY: it can sign + birth its own children (estates,
326
- * catalogues) from intent 0. WITHOUT this the home is born warranted + owned but signer-less — it can
327
- * never act (the co2 estate-birth bug). Mirrors `buildGenesisChain`'s `enrollSigner` for a platform.
328
- * REQUIRED — a payload without it refuses at compile/validation, not an hour into a live deploy with a
329
- * bricked signer-less home. (The kernel injecting it from the verified cert's `parent_key` is the
330
- * eventual generalization; until then the field is loud.) */
331
- ownerKeyHash: sha256Hex,
332
- /** THE DEVICE-SIGNED HOME CERT (JSON `executor::birth_cert::BirthCert`), signed CLIENT-SIDE by the
333
- * owner's device key `S_U` (the SAME key `ownerKeyHash` hashes + the `signer` relation enrols). The
334
- * kernel's birth offer-effect VERIFIES this cert's signature against U's enrolled signer pubkey `P_U`
335
- * AND that its `founder == ctx.principal()` (the gate verdict, CAS-as-law) BEFORE accepting it as
336
- * `links[0]` of the travelling chain — the kernel signs nothing (it holds no secret; it only verifies).
337
- * Optional: absent ⇒ the era stays inert for this birth (pre-feature lane). */
338
- homeCert: z.string().optional(),
339
325
  /** AUTHPROVIDER INHERITANCE — DEFAULT ON, opt out with `false`. Verification is PER-WORKSPACE (the
340
326
  * kernel never walks up to the parent), so a PRIVATE child born without its own AuthProvider cannot
341
327
  * verify any token — its own owner is locked out of verified reads (the co2 phase-6 wall). By
@@ -352,9 +338,9 @@ export function birthReceivingGroupForDomain(
352
338
  .plan((p) => {
353
339
  const founder = p.owner; // the actor for the child's own genesis steps (its self-bound owner subject)
354
340
  const groupName = receivingGroupNameOf(p.owner);
355
- // THE PARENT'S DECLARED VERIFIER (one captured read serves BOTH consumers below): the inherited
356
- // `setAuthProvider` genesis step AND the enroll step's `verifiedVia` provenance stamp. Read it up
357
- // front so the enroll step (which precedes the inheritance step in the chain) can see the issuer.
341
+ // THE PARENT'S DECLARED VERIFIER. This captured read supplies the inherited provider seed.
342
+ // Birth-certificate verification and founding-signer enrollment belong to the kernel-owned
343
+ // attestation step, so neither fact is duplicated in this tenant plan or payload.
358
344
  const apRows = p.inheritAuthProvider !== false
359
345
  ? readFrameworkDeclared<Array<Record<string, unknown>>>("authProvider", { scope: "platform" })
360
346
  : undefined;
@@ -367,20 +353,19 @@ export function birthReceivingGroupForDomain(
367
353
  const v = ap?.[k];
368
354
  return typeof v === "string" && v.length > 0 ? v : undefined;
369
355
  };
370
- // PROVENANCE (the verified-lineage thread, 2026-07-16): the kernel derives every later write's
371
- // `principal_verified` from the ENROLL LEAF's `verifiedVia` — so a verified birth must stamp it or the
372
- // born home's owner writes UNVERIFIED forever (and its own children are then born `verified:false`,
373
- // whose chains refuse `verify_to_root`). The cert's `verified` IS the gate verdict post-admission
374
- // (assemble_birth_attestation CAS-refuses a mismatch), so reading it in-plan is trustworthy — a lying
375
- // cert never reaches this fold. An unverified birth stamps nothing (the unverified lane stays honest).
376
- let certVerified = false;
377
- try { certVerified = p.homeCert !== undefined && JSON.parse(p.homeCert).verified === true; } catch { /* malformed cert ⇒ unverified (the gate will refuse the birth anyway) */ }
378
- const verifiedVia = certVerified ? apStr("issuer") : undefined;
379
356
  const genesisChain: Array<{ domain: string; directiveId: string; actor: string; payload: Record<string, unknown>; domainHash?: string }> = [
380
357
  // THE ORDER-DECLARED LAW INSTALL: two typed closure identities. The kernel resolves each
381
358
  // closure from the parent's custody when it folds the birth; law bytes never ride the intent.
382
- { domain: "bootstrap", directiveId: "installDomain", actor: founder, payload: { domainHash: p.frameworkHash }, domainHash: p.frameworkHash },
383
- { domain: "nomos", directiveId: "installDomain", actor: founder, payload: { domainHash: p.lawHash }, domainHash: p.lawHash },
359
+ domainInstallGenesisOffer(
360
+ DomainInstallExecutor.StageZero,
361
+ p.frameworkHash,
362
+ founder,
363
+ ),
364
+ domainInstallGenesisOffer(
365
+ DomainInstallExecutor.LifecycleController,
366
+ p.lawHash,
367
+ founder,
368
+ ),
384
369
  {
385
370
  domain: groupLawDomain,
386
371
  directiveId: "nomosInternalSeedReceivingGroupPolicy7c1f5e",
@@ -421,16 +406,6 @@ export function birthReceivingGroupForDomain(
421
406
  bornAt: p.bornAt,
422
407
  })),
423
408
  })),
424
- // ENROL THE OWNER'S DEVICE on the warrant-free prefix (when supplied) — enrollSigner self-binds to
425
- // ctx.principal() (the owner), so this binds `ownerKeyHash` as the owner's signer BEFORE the warrant
426
- // closes bootstrap. This is what makes the born home SIGNER-READY (able to sign + self-serve-birth its own
427
- // estates/catalogues). Mirrors buildGenesisChain's founding-admin enrollSigner for a platform — homes were
428
- // the inconsistent one. THE GENESIS ORDER IS THE CONTRACT (BREAK 1): the parent's offer-effect APPENDS
429
- // `seedBirthAttestation` + `seedGenesis` (the WARRANT) AFTER these steps — the warrant lands LAST (E→W
430
- // ladder: every seed runs UNSIGNED on the still-warrant-free prefix; the warrant closes it). ERA-SAFE: a
431
- // home born earlier (pre-feature law, no such step in its recipe) replays byte-identical — the era rule
432
- // rides the LAW hash, not this branch.
433
- { domain: groupLawDomain, directiveId: "enrollSigner", actor: founder, payload: { keyHash: p.ownerKeyHash, grantedBy: p.owner, grantedAt: 1, ...(verifiedVia !== undefined ? { verifiedVia } : {}) } },
434
409
  ];
435
410
  // AUTHPROVIDER INHERITANCE (default ON — see the payload doc): copy the parent's declared provider
436
411
  // (the up-front captured read above) AS DATA into the child genesis. The child owns the copy as its