@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
@@ -68,9 +68,9 @@ import { z } from "zod";
68
68
  import { aggregate, instance } from "../aggregate.js";
69
69
  import { create, ensure } from "../authoring.js";
70
70
  import { birth } from "../births.js";
71
- import { directive } from "../directive.js";
71
+ import { directive, type Directive } from "../directive.js";
72
72
  import { Lww } from "../drivers.js";
73
- import { t } from "../fields.js";
73
+ import { sha256Digest, t, type Sha256Digest } from "../fields.js";
74
74
  import { set, withMarker } from "../ops.js";
75
75
  import { query } from "../query.js";
76
76
  import { read } from "../read.js";
@@ -79,7 +79,12 @@ import { writeTuple, removeTuple, RelationTuple, relationTupleInstanceId } from
79
79
  import { relationType, self, user, key } from "../relation_schema.js";
80
80
  import { GENESIS_WARRANT_ID } from "./genesis.js";
81
81
  import { idOf } from "../value_object.js";
82
- import { signerObject, SIGNER_RELATION } from "./signing_identity.js";
82
+ import { enrollSigner, signerObject, SIGNER_RELATION } from "./signing_identity.js";
83
+ import { seedBirthPolicy } from "./birth_meter.js";
84
+ import {
85
+ DomainInstallExecutor,
86
+ domainInstallGenesisOffer,
87
+ } from "./domain_lifecycle.js";
83
88
  // THE GENESIS BOOTSTRAP (platform-birth genesis bootstrap, Approach A): governance carries the
84
89
  // warrant-flip primitives so the birth door can lawfully bootstrap a virgin platform's founding admin —
85
90
  // E `enrollSigner` (the founding signer tuple) → W `seedGenesis` (the GenesisWarrant) → A signed
@@ -88,7 +93,7 @@ import { signerObject, SIGNER_RELATION } from "./signing_identity.js";
88
93
  export { GenesisWarrant, seedGenesis } from "./genesis.js";
89
94
  export { identityRelations, enrollSigner } from "./signing_identity.js";
90
95
  // ERA 0 — the keyless verified first-device door rides the GOVERNANCE law too: a freshly BORN platform
91
- // has a virgin roster and a cert-ATTESTED AuthProvider (the S_root-signed platformCert), so its founder's
96
+ // has a virgin roster and a cert-ATTESTED AuthProvider (the S_root-signed birth witness), so its founder's
92
97
  // FIRST action is `enrollFirstDevice` (token-verified against the attested provider) followed by a SIGNED
93
98
  // self-`grantAdmin` (admitted by the kernel genesis clause). Without this export a born platform is
94
99
  // un-administrable — the admin bootstrap has no door.
@@ -105,6 +110,10 @@ export { BIRTH_ATTESTATION_ID, BirthAttestation, birthAttestation, seedBirthAtte
105
110
  // onboarding signer S_P) so the kernel reads it at `signer-delegation:<user>` when assembling a home's chain.
106
111
  export { SignerDelegation, enrollDelegation, signerDelegationByUser } from "./signing_identity.js";
107
112
 
113
+ /** Canonical governance-law namespace. Genesis routes refer to this declaration, never a repeated
114
+ * string literal, so renaming the module cannot silently strand part of the founding ceremony. */
115
+ export const WORKSPACES_DOMAIN = "workspaces" as const;
116
+
108
117
  // ── WORKSPACE SELF-IDENTITY (the law reads its OWN install-context) ──────────────────────────────────
109
118
  // The governance bundle runs on root AND every born platform. A workspace knows its ROLE via this
110
119
  // singleton, at a CONSTANT fixed instance id (the RelationTuple discipline — a literal, never minted, so
@@ -191,6 +200,10 @@ export const workspaceRelations = relationType("workspace")
191
200
  /** Where a workspace's holon runs: an edge DO or the dedicated 12GiB container. */
192
201
  export const WORKSPACE_TIERS = ["edge", "heavy"] as const;
193
202
 
203
+ /** The only cloud-level creation postures. This vocabulary is shared by the stored law field,
204
+ * directive payload and operator tooling so an unknown posture cannot survive as an arbitrary string. */
205
+ export const CLOUD_CREATION_POSTURES = ["open", "invite", "closed"] as const;
206
+
194
207
  /**
195
208
  * GOVERNANCE POSTURE (architecture/governance_postures.md) — every platform DECLARES how the operator may
196
209
  * act in its subtree. `managed` (the default when absent) folds an OperatorGrant: the operator may run
@@ -224,7 +237,7 @@ export const WORKSPACE_STATUSES = [
224
237
  export const GRANT_STATUSES = ["active", "revoked"] as const;
225
238
 
226
239
  /** sha256 hex — the ONLY shape a credential may take inside the law. */
227
- const sha256Hex = z.string().regex(/^[0-9a-f]{64}$/, "expected lowercase sha256 hex");
240
+ const sha256Hex = sha256Digest();
228
241
 
229
242
  /** Epoch-ms, caller-stamped (a plan never reads a clock). */
230
243
  const epochMs = z.number().int().nonnegative();
@@ -375,7 +388,7 @@ export const CloudPolicy = aggregate("CloudPolicy", {
375
388
  * historical replay, but absence fails closed for new births. Merely
376
389
  * granting somebody authority never changes the door for everybody else.
377
390
  */
378
- creationPosture: t.string().merge(Lww).optional(),
391
+ creationPosture: t.enum(CLOUD_CREATION_POSTURES).merge(Lww).optional(),
379
392
  /** Who last set the policy (attribution). */
380
393
  setBy: t.string().merge(Lww),
381
394
  /** Epoch-ms the policy was last set (caller-stamped). */
@@ -527,8 +540,8 @@ export function buildGenesisChain(p: {
527
540
  * the recipe PREPENDS `bootstrap/installDomain(framework)` + `nomos/installDomain(law)` — the two-layer
528
541
  * install the genesis bootstrap does — so the child installs ITS OWN law as intent 0 and self-validates
529
542
  * from there. Absent ⇒ the recipe resolves an already-installed law by shape (pre-G3 / test lanes). */
530
- frameworkHash?: string;
531
- lawHash?: string;
543
+ frameworkHash?: Sha256Digest;
544
+ lawHash?: Sha256Digest;
532
545
  /** METERED BIRTH (slice A3 — the descendant budget). Required: a governance child may not be
533
546
  * born without explicit BirthPolicy state. Zero is a legitimate declared "cannot birth" policy. */
534
547
  descendantPool: number;
@@ -537,15 +550,23 @@ export function buildGenesisChain(p: {
537
550
  * ancestry while `parent` stays the SUPERSEDED record's parent (a rebirth may never reparent). */
538
551
  rebornFrom?: string;
539
552
  }): string {
540
- const founder = "user:" + p.principal;
553
+ const founder: `user:${string}` = `user:${p.principal}`;
541
554
  // THE ORDER-DECLARED LAW INSTALL (G3): the child installs the EXACT typed USDA closures the order names.
542
555
  // Bytes are NOT in the recipe; the kernel resolves each identity from its own custody when it folds the
543
556
  // birth. Two layers, matching the genesis bootstrap: bootstrap controller, then law.
544
557
  const installSteps =
545
558
  p.frameworkHash && p.lawHash
546
559
  ? [
547
- { domain: "bootstrap", directiveId: "installDomain", actor: founder, payload: { domainHash: p.frameworkHash }, domainHash: p.frameworkHash },
548
- { domain: "nomos", directiveId: "installDomain", actor: founder, payload: { domainHash: p.lawHash }, domainHash: p.lawHash },
560
+ domainInstallGenesisOffer(
561
+ DomainInstallExecutor.StageZero,
562
+ p.frameworkHash,
563
+ founder,
564
+ ),
565
+ domainInstallGenesisOffer(
566
+ DomainInstallExecutor.LifecycleController,
567
+ p.lawHash,
568
+ founder,
569
+ ),
549
570
  ]
550
571
  : [];
551
572
  // THE LINEAGE-AUTHORIZATION BINDING (proof-friendly): the child's birthCloud records who authorized it, so
@@ -571,20 +592,28 @@ export function buildGenesisChain(p: {
571
592
  // METERED BIRTH (slice A3 — THE POOL CHAINS TO ROOT): every governance child is BORN BOUNDED.
572
593
  // Historical chains keep replaying under their historical law; this constructor authors only the
573
594
  // current law, whose seedBirthPolicy directive is compiler-carried. Missing policy is invalid genesis.
595
+ const governanceGenesisStep = <P extends Record<string, unknown>>(
596
+ declaration: Directive<P>,
597
+ payload: P,
598
+ ) => ({
599
+ domain: WORKSPACES_DOMAIN,
600
+ directiveId: declaration.id,
601
+ actor: founder,
602
+ payload,
603
+ });
574
604
  const poolSteps = [
575
- {
576
- domain: "workspaces",
577
- directiveId: "seedBirthPolicy",
578
- actor: founder,
579
- payload: { maxBirthsPerPrincipal: p.descendantPool, setBy: p.principal, setAt: p.bornAt },
580
- },
605
+ governanceGenesisStep(seedBirthPolicy, {
606
+ maxBirthsPerPrincipal: p.descendantPool,
607
+ setBy: p.principal,
608
+ setAt: p.bornAt,
609
+ }),
581
610
  ];
582
611
  return JSON.stringify([
583
612
  ...installSteps,
584
613
  ...poolSteps,
585
614
  // ERA 0 — THE GENESIS-ORDER CONTRACT (the birthHome ordering, unified): every recipe seed below runs
586
615
  // UNSIGNED on the still-WARRANT-FREE prefix; the recipe carries NO seedGenesis and NO grantAdmin. The
587
- // KERNEL's birth effect — after verifying the offer's S_root-signed platformCert against the pinned
616
+ // KERNEL's birth effect — after verifying the offer's typed S_root-signed certificate against the pinned
588
617
  // K_root — appends `seedBirthAttestation` (the travelling cert) and THEN `seedGenesis` (the warrant,
589
618
  // self-id == name) LAST, so the platform is born WARRANTED + ATTESTED and every post-genesis write is
590
619
  // signed AND cert-gated. A birth whose cert fails verification seeds neither and is REFUSED atomically
@@ -598,11 +627,22 @@ export function buildGenesisChain(p: {
598
627
  // (`genesis_admits`: a virgin #admin roster + the warrant binding the captured principal). A baked
599
628
  // in-genesis grantAdmin can never fold (post-warrant writes need a signature the birth effect cannot
600
629
  // produce — the pre-era-0 recipe was structurally unbornable); pre-warrant enroll + post-birth signed
601
- // self-grant is the lawful shape. (The keyless variant — enrollFirstDevice against the platformCert's
630
+ // self-grant is the lawful shape. (The keyless variant — enrollFirstDevice against the birth certificate's
602
631
  // ATTESTED AuthProvider — remains for founders with an IdP instead of a key.)
603
- { domain: "workspaces", directiveId: "enrollSigner", actor: founder, payload: { keyHash: p.foundingAdminHash, grantedBy: p.principal, grantedAt: p.requestedAt } },
632
+ governanceGenesisStep(enrollSigner, {
633
+ keyHash: p.foundingAdminHash,
634
+ grantedBy: p.principal,
635
+ grantedAt: p.requestedAt,
636
+ }),
604
637
  // No `role` — it was a vestigial node-class label (collapsed with `kind`); birthCloud ignores it now.
605
- { domain: "workspaces", directiveId: "birthCloud", actor: "", payload: { principal: p.principal, name: p.name, tier: p.tier, bornAt: p.requestedAt, bornUnder, ...(p.rebornFrom ? { rebornFrom: p.rebornFrom } : {}) } },
638
+ governanceGenesisStep(birthCloud, {
639
+ principal: p.principal,
640
+ name: p.name,
641
+ tier: p.tier,
642
+ bornAt: p.requestedAt,
643
+ bornUnder,
644
+ ...(p.rebornFrom ? { rebornFrom: p.rebornFrom } : {}),
645
+ }),
606
646
  ]);
607
647
  }
608
648
 
@@ -1248,13 +1288,6 @@ export const birthChild = directive("birthChild")
1248
1288
  * own custody (never the host). Absent ⇒ the recipe resolves an already-installed law by shape. */
1249
1289
  frameworkHash: sha256Hex.optional(),
1250
1290
  lawHash: sha256Hex.optional(),
1251
- /** THE PLATFORM ATTESTATION (era 0 — one attested birth lane for every holon kind): the 1-link
1252
- * platform_cert JSON (`executor::birth_cert::BirthCert`), SIGNED OFFLINE BY S_root (the K_root
1253
- * holder). The kernel's birth effect VERIFIES it (child == name, founder == `user:<principal>`,
1254
- * signature vs the pinned K_root) and injects `seedBirthAttestation` + `seedGenesis` (the warrant)
1255
- * LAST into the child's genesis. REQUIRED for a genesis birth to complete — a birth without a
1256
- * valid cert is refused atomically (no unwarranted platform lane survives era 0). */
1257
- platformCert: z.string().optional(),
1258
1291
  /** GOVERNANCE POSTURE (governance_postures.md) — the born child's declared posture; default
1259
1292
  * `managed`. Mirrored onto its lineage record so root's bailiff can consult it on the hot path
1260
1293
  * (the tenant's own {@link GovernancePosture} singleton is authoritative). Additive/era-safe. */
@@ -1505,7 +1538,7 @@ export const setCloudPolicy = directive("setCloudPolicy")
1505
1538
  // The creation-posture dial (cloud_sovereignty.md S6). Optional:
1506
1539
  // omitted leaves the field untouched (a partial amend never silently
1507
1540
  // resets the door).
1508
- creationPosture: z.enum(["open", "invite", "closed"]).optional(),
1541
+ creationPosture: z.enum(CLOUD_CREATION_POSTURES).optional(),
1509
1542
  setBy: z.string().min(1),
1510
1543
  setAt: epochMs,
1511
1544
  }),
package/src/idof_lint.ts CHANGED
@@ -105,72 +105,86 @@ export function assertIdOfLintForDirective(dir: Directive<unknown>, where: strin
105
105
  // tenant-facing idOf lint here would force framework wire schemas to masquerade as public
106
106
  // application schemas and, worse, reject every aggregate that opts into owned custody.
107
107
  if ((dir as { businessBirthInternal?: unknown }).businessBirthInternal === true) return;
108
- const probed = probePlanOps(dir, where);
109
- if (probed === undefined) return;
110
108
  let shape: Record<string, z.ZodTypeAny>;
111
109
  try {
112
110
  shape = zodObjectShape(dir.payloadSchema as z.ZodTypeAny);
113
111
  } catch {
114
112
  return;
115
113
  }
116
- // Reverse index: sampled STRING value → the top-level payload field name(s) that produced it.
117
- // `synthesizePayload` gives every string-kind field a DISTINCT `"sample-<field>"` value (or a
118
- // schema-constrained fallback), so an exact match is unambiguous in practice.
119
- const byValue = new Map<string, string>();
120
- for (const [name, value] of Object.entries(probed.synth.value)) {
121
- if (typeof value === "string") byValue.set(value, name);
122
- }
123
- for (const op of probed.ops) {
124
- // A bound mutation addresses one existing aggregate instance. The payload field carrying that
125
- // address is therefore not "some string" or merely "some branded id": it must be idOf(the exact
126
- // aggregate being mutated). This is the identity spine of the generated application API. Without
127
- // this guard a model can compile `entryId: z.string()`, after which every generated caller can mix
128
- // unrelated record ids and is encouraged to reconstruct identity as application state.
129
- const effectiveMarker = op.marker ?? (op.aggregateType === dir.aggregateId ? dir.marker : "mutates");
130
- if (op.aggregateType !== undefined && (effectiveMarker === "mutates" || effectiveMarker === "archives")) {
131
- const payloadField = byValue.get(op.aggregateId);
132
- if (payloadField !== undefined) {
133
- const fieldSchema = shape[payloadField];
134
- const actualTarget = fieldSchema === undefined ? undefined : idOfMetaOf(fieldSchema);
135
- if (actualTarget !== op.aggregateType) {
136
- const expected = shortAggName(op.aggregateType);
137
- const actual = actualTarget === undefined
138
- ? "an untyped string"
139
- : `idOf(${shortAggName(actualTarget)})`;
140
- const verb = effectiveMarker === "archives" ? "archive" : "mutate";
141
- throw new IdOfLintError(
114
+ // Probe both payload presences. The ordinary proof deliberately omits optional fields, but that
115
+ // would make `optional().setOptional(...)` invisible to the identity lint: exactly the shape that
116
+ // can smuggle a business string into a typed aggregate reference. The second probe includes every
117
+ // sampleable top-level optional and deliberately ignores a possibly sparse author example. It is
118
+ // lint-only; all existing proof/effect consumers retain the ordinary omitted-optional semantics.
119
+ const probedVariants = [
120
+ probePlanOps(dir, where),
121
+ probePlanOps(dir, `${where} (optional fields present)`, {
122
+ includeOptionalTopLevel: true,
123
+ useDirectiveExample: false,
124
+ }),
125
+ ];
126
+ if (probedVariants.every((variant) => variant === undefined)) return;
127
+ for (const probed of probedVariants) {
128
+ if (probed === undefined) continue;
129
+ // Reverse index: sampled STRING value → the top-level payload field name(s) that produced it.
130
+ // `synthesizePayload` gives every string-kind field a DISTINCT `"sample-<field>"` value (or a
131
+ // schema-constrained fallback), so an exact match is unambiguous in practice.
132
+ const byValue = new Map<string, string>();
133
+ for (const [name, value] of Object.entries(probed.synth.value)) {
134
+ if (typeof value === "string") byValue.set(value, name);
135
+ }
136
+ for (const op of probed.ops) {
137
+ // A bound mutation addresses one existing aggregate instance. The payload field carrying that
138
+ // address is therefore not "some string" or merely "some branded id": it must be idOf(the exact
139
+ // aggregate being mutated). This is the identity spine of the generated application API. Without
140
+ // this guard a model can compile `entryId: z.string()`, after which every generated caller can mix
141
+ // unrelated record ids and is encouraged to reconstruct identity as application state.
142
+ const effectiveMarker = op.marker ?? (op.aggregateType === dir.aggregateId ? dir.marker : "mutates");
143
+ if (op.aggregateType !== undefined && (effectiveMarker === "mutates" || effectiveMarker === "archives")) {
144
+ const payloadField = byValue.get(op.aggregateId);
145
+ if (payloadField !== undefined) {
146
+ const fieldSchema = shape[payloadField];
147
+ const actualTarget = fieldSchema === undefined ? undefined : idOfMetaOf(fieldSchema);
148
+ if (actualTarget !== op.aggregateType) {
149
+ const expected = shortAggName(op.aggregateType);
150
+ const actual = actualTarget === undefined
151
+ ? "an untyped string"
152
+ : `idOf(${shortAggName(actualTarget)})`;
153
+ const verb = effectiveMarker === "archives" ? "archive" : "mutate";
154
+ throw new IdOfLintError(
142
155
  `directive '${dir.id}': payload field '${payloadField}' selects the ${expected} record to ${verb} ` +
143
156
  `but carries ${actual} — declare it as idOf(${expected}) so create, update, find and watch use ` +
144
157
  `the same generated NomosRef<${expected}>.`,
145
- );
158
+ );
159
+ }
146
160
  }
147
161
  }
148
- }
149
- if (op.kind !== "set" && op.kind !== "setEntry") continue;
150
- if (typeof op.value !== "string") continue;
151
- const targetAgg = refTargetOf(op);
152
- if (targetAgg === undefined) continue; // not a ref-kind field (or one the DSL can't name a target for)
153
- const payloadField = byValue.get(op.value);
154
- if (payloadField === undefined) continue; // not traceable to a top-level payload field — no verdict
155
- const fieldSchema = shape[payloadField];
156
- if (fieldSchema === undefined) continue;
157
- const onAgg = shortAggName(op.aggregateType ?? op.aggregateId);
158
- const targetName = shortAggName(targetAgg);
159
- const actualTarget = idOfMetaOf(fieldSchema);
160
- if (actualTarget === targetAgg) continue;
161
- if (actualTarget !== undefined) {
162
+ if (op.kind !== "set" && op.kind !== "setEntry") continue;
163
+ if (typeof op.value !== "string") continue;
164
+ const targetAgg = refTargetOf(op);
165
+ if (targetAgg === undefined) continue; // not a ref-kind field (or one the DSL can't name a target for)
166
+ const payloadField = byValue.get(op.value);
167
+ if (payloadField === undefined) continue; // not traceable to a top-level payload field — no verdict
168
+ const fieldSchema = shape[payloadField];
169
+ if (fieldSchema === undefined) continue;
170
+ const onAgg = shortAggName(op.aggregateType ?? op.aggregateId);
171
+ const targetName = shortAggName(targetAgg);
172
+ const actualTarget = idOfMetaOf(fieldSchema);
173
+ if (actualTarget === targetAgg) continue;
174
+ if (actualTarget !== undefined) {
175
+ throw new IdOfLintError(
176
+ `directive '${dir.id}': payload field '${payloadField}' feeds ref field '${onAgg}.${op.field}' ` +
177
+ `(-> ${targetName}) but carries idOf(${shortAggName(actualTarget)}) — declare it as ` +
178
+ `idOf(${targetName}) so generated callers carry NomosRef<${targetName}>.`,
179
+ );
180
+ }
181
+ if (!isBareString(fieldSchema) && brandedIdMetaOf(fieldSchema) === undefined) continue;
162
182
  throw new IdOfLintError(
163
183
  `directive '${dir.id}': payload field '${payloadField}' feeds ref field '${onAgg}.${op.field}' ` +
164
- `(-> ${targetName}) but carries idOf(${shortAggName(actualTarget)}) — declare it as ` +
165
- `idOf(${targetName}) so generated callers carry NomosRef<${targetName}>.`,
184
+ `(-> ${targetName}) but is not idOf(${targetName}) — declare it as idOf(${targetName}) so generated ` +
185
+ `Dart/TS carry NomosRef<${targetName}>, matching the read model. (dart_codegen_parity.md P2)`,
166
186
  );
167
187
  }
168
- if (!isBareString(fieldSchema) && brandedIdMetaOf(fieldSchema) === undefined) continue;
169
- throw new IdOfLintError(
170
- `directive '${dir.id}': payload field '${payloadField}' feeds ref field '${onAgg}.${op.field}' ` +
171
- `(-> ${targetName}) but is not idOf(${targetName}) — declare it as idOf(${targetName}) so generated ` +
172
- `Dart/TS carry NomosRef<${targetName}>, matching the read model. (dart_codegen_parity.md P2)`,
173
- );
174
188
  }
175
189
  }
176
190
 
package/src/index.ts CHANGED
@@ -56,10 +56,16 @@ export {
56
56
  jsonWithNullSchema,
57
57
  type EvidenceRefValue,
58
58
  evidenceRefSchema,
59
+ type NomosRefValue,
60
+ nomosRefValueSchema,
59
61
  type Sha256Digest,
62
+ type GitObjectId,
60
63
  sha256Digest,
64
+ gitObjectId,
61
65
  isSha256DigestSchema,
66
+ isGitObjectIdSchema,
62
67
  NOMOS_SHA256_DIGEST_BRAND,
68
+ NOMOS_GIT_OBJECT_ID_BRAND,
63
69
  // FIELD FACETS (the endorse model — security_model.md): authority/standing/attribution gate
64
70
  // set() acceptance of PrincipalEvidence; secret/public are the confidentiality lattice.
65
71
  authorityField,
@@ -80,6 +86,8 @@ export {
80
86
  idOf,
81
87
  idOfMetaOf,
82
88
  idOfTargetKindOf,
89
+ refOf,
90
+ refOfMetaOf,
83
91
  versionedRefOf,
84
92
  versionedRefOfMetaOf,
85
93
  brandedId,
@@ -87,7 +95,9 @@ export {
87
95
  voMetaOf,
88
96
  voSample,
89
97
  NOMOS_VO_BRAND,
98
+ NOMOS_REFOF_BRAND,
90
99
  NOMOS_VERSIONED_REF_BRAND,
100
+ type RefOfMeta,
91
101
  type VersionedRefOfMeta,
92
102
  geo,
93
103
  type ValueObject,
@@ -617,13 +627,21 @@ export {
617
627
  } from "./framework/capability.js";
618
628
  export {
619
629
  DOMAIN_INSTALLATION_PHASES,
630
+ DOMAIN_LIFECYCLE_DIRECTIVES,
631
+ MODEL_LIFECYCLE_AUTHORITY,
620
632
  DomainInstallation,
633
+ DomainInstallExecutor,
634
+ DomainInstallationPhase,
635
+ currentLawReference,
636
+ domainInstallGenesisOffer,
621
637
  domainInstallationAggregateId,
622
638
  installDomain,
623
639
  stageDomain,
624
640
  promoteDomain,
625
641
  rollbackDomain,
626
- type DomainInstallationPhase,
642
+ type DomainLifecycleDirectiveId,
643
+ type CurrentLawReference,
644
+ type LawClosureReference,
627
645
  } from "./framework/domain_lifecycle.js";
628
646
  export {
629
647
  WORKSPACE_SYNC_DIRECTIONS,
@@ -666,6 +684,7 @@ export {
666
684
  // NATIVE ZANZIBAR RELATION SCHEMA (P1) — the userset-rewrite law as pure data.
667
685
  export {
668
686
  relationType,
687
+ relationRef,
669
688
  isRelationTypeDecl,
670
689
  validateRelationType,
671
690
  self,
@@ -691,6 +710,7 @@ export {
691
710
  selfObject,
692
711
  objectOf,
693
712
  type RelationObjectToken,
713
+ type RelationRef,
694
714
  type RelationTypeDecl,
695
715
  type RelationTypeBuilder,
696
716
  type RelationDefAst,
package/src/intent.ts CHANGED
@@ -24,7 +24,11 @@ import type {
24
24
  InvariantReadIdPart,
25
25
  WorkspaceInvariantDecl,
26
26
  } from "./framework/workspace_invariant.js";
27
- import { businessBirthConsequence, type BusinessBirthDirective } from "./business_birth.js";
27
+ import {
28
+ BUSINESS_BIRTH_POLICY_BINDING,
29
+ businessBirthConsequence,
30
+ type BusinessBirthDirective,
31
+ } from "./business_birth.js";
28
32
  import type { BusinessBirthDecl } from "./births.js";
29
33
  import type { WorkspaceSharingPolicy } from "./workspace_sharing.js";
30
34
 
@@ -164,6 +168,7 @@ function remapNativeReads(
164
168
 
165
169
  export interface BusinessIntent<I = unknown> extends Directive<I> {
166
170
  readonly [INTENT_BRAND]: true;
171
+ readonly [BUSINESS_BIRTH_POLICY_BINDING]?: (domains: readonly string[]) => void;
167
172
  readonly steps: readonly IntentStep<I, any>[];
168
173
  /** The complete public business boundary. Runtime lowering still uses Directive.payloadSchema privately. */
169
174
  readonly contract: IntentContract<I>;
@@ -433,6 +438,8 @@ class IntentBuilder<I> {
433
438
  };
434
439
 
435
440
  const businessBirth = (birthSteps[0]?.directive as BusinessBirthDirective | undefined)?.businessBirth;
441
+ const bindBusinessBirthPolicy =
442
+ (birthSteps[0]?.directive as BusinessBirthDirective | undefined)?.[BUSINESS_BIRTH_POLICY_BINDING];
436
443
  return {
437
444
  [INTENT_BRAND]: true,
438
445
  id: this.id,
@@ -472,6 +479,9 @@ class IntentBuilder<I> {
472
479
  ? {}
473
480
  : {
474
481
  birthsChild: true,
482
+ ...(bindBusinessBirthPolicy === undefined
483
+ ? {}
484
+ : { [BUSINESS_BIRTH_POLICY_BINDING]: bindBusinessBirthPolicy }),
475
485
  businessBirth: {
476
486
  ...businessBirth,
477
487
  publicPayloadSchema: this.contractDeclaration.accepts,
package/src/law_graph.ts CHANGED
@@ -31,6 +31,7 @@ import {
31
31
  lowerLawValueType,
32
32
  type LawConstant,
33
33
  type LawLiteral,
34
+ type LawValueMember,
34
35
  type LawValueConstraint,
35
36
  type LawValueType,
36
37
  } from "./law_value_type.js";
@@ -176,6 +177,47 @@ export interface LawPayloadContractNode {
176
177
  readonly fields: readonly LawStructuralRef<"value-field">[];
177
178
  }
178
179
 
180
+ /** Compiler-owned facts used to author `.owned()` effects. These are part of the
181
+ * internal typed USDA call contract, never the tenant-facing business contract. */
182
+ const OWNED_AUTHORING_PAYLOAD_MEMBERS = [
183
+ {
184
+ name: OWNED_KEY_PAYLOAD_FIELD,
185
+ required: false,
186
+ value: { kind: "string", constraints: [] },
187
+ },
188
+ {
189
+ name: OWNED_CUSTODY_PAYLOAD_FIELD,
190
+ required: false,
191
+ value: { kind: "string", constraints: [] },
192
+ },
193
+ {
194
+ name: OWNED_PREMINTED_RECORDS_PAYLOAD_FIELD,
195
+ required: false,
196
+ value: {
197
+ kind: "list",
198
+ element: {
199
+ kind: "record",
200
+ fields: [
201
+ { name: "type", required: true, value: { kind: "string", constraints: [] } },
202
+ { name: "id", required: true, value: { kind: "string", constraints: [] } },
203
+ ],
204
+ },
205
+ },
206
+ },
207
+ ] as const satisfies readonly LawValueMember[];
208
+
209
+ function internalPayloadContract(
210
+ tenantContract: LawValueType,
211
+ capabilities: ReadonlySet<"owned-authoring">,
212
+ ): LawValueType {
213
+ if (tenantContract.kind !== "record") return tenantContract;
214
+ if (!capabilities.has("owned-authoring")) return tenantContract;
215
+ return {
216
+ kind: "record",
217
+ fields: [...tenantContract.fields, ...OWNED_AUTHORING_PAYLOAD_MEMBERS],
218
+ };
219
+ }
220
+
179
221
  export interface LawReadParameter {
180
222
  readonly name: string;
181
223
  readonly field?: LawFieldPathRef;
@@ -1112,7 +1154,7 @@ export function discoverLawGraph(
1112
1154
  const intentNodes: LawIntentNode[] = intents.map((intent) => {
1113
1155
  const intentEntity = intentEntities.get(intent.id)!;
1114
1156
  const businessBirth = (intent as {
1115
- businessBirth?: { publicPayloadSchema?: z.ZodTypeAny };
1157
+ businessBirth?: { publicPayloadSchema?: z.ZodTypeAny; owned?: boolean };
1116
1158
  }).businessBirth;
1117
1159
  const payloadContractEntity = ownedEntity(
1118
1160
  "contract",
@@ -1122,21 +1164,32 @@ export function discoverLawGraph(
1122
1164
  `${intent.id}#accepts`,
1123
1165
  options.source?.("intent", intent.id, module.name),
1124
1166
  );
1125
- const payloadShape = lowerAnonymousContractFields(
1126
- lowerLawValueType(isBusinessIntent(intent) ? intent.contract.accepts : intent.payloadSchema),
1127
- payloadContractEntity,
1167
+ // A business birth has two deliberately different typed boundaries:
1168
+ //
1169
+ // * `contract.accepts` / `businessBirth.publicPayloadSchema` is the tenant-facing input;
1170
+ // * `payloadSchema` is the executable call contract after Nomos has added its private
1171
+ // principal, timestamp and (when needed) sealed-genesis facts.
1172
+ //
1173
+ // Collapsing both to `contract.accepts` made the generated client author the correct private
1174
+ // facts into a USDA call whose `nomos:accepts` contract did not declare them. Keep ordinary
1175
+ // compound intents on their public contract because they have no framework-only birth carrier;
1176
+ // only a declared business birth owns this second typed boundary.
1177
+ const tenantPayloadContract = lowerLawValueType(
1178
+ isBusinessIntent(intent) && businessBirth === undefined
1179
+ ? intent.contract.accepts
1180
+ : intent.payloadSchema,
1128
1181
  );
1129
- for (const node of payloadShape.nodes) anonymousValueNodes.set(node.entity.id, node);
1130
- for (const fieldRef of payloadShape.fields) {
1131
- const field = anonymousValueNodes.get(fieldRef.id);
1132
- if (field?.structuralKind !== "value-field") continue;
1133
- const reserved = [
1182
+ if (tenantPayloadContract.kind !== "record") {
1183
+ fail(`directive '${intent.id}': payload must lower to a record`);
1184
+ }
1185
+ for (const field of tenantPayloadContract.fields) {
1186
+ const reserved = new Set<string>([
1134
1187
  OWNED_KEY_PAYLOAD_FIELD,
1135
1188
  OWNED_CUSTODY_PAYLOAD_FIELD,
1136
1189
  OWNED_PREMINTED_RECORDS_PAYLOAD_FIELD,
1137
1190
  BUSINESS_BIRTH_SEED_PAYLOAD_FIELD,
1138
- ];
1139
- if (!reserved.includes(field.name)) continue;
1191
+ ]);
1192
+ if (!reserved.has(field.name)) continue;
1140
1193
  const frameworkBirthCarrier = field.name === BUSINESS_BIRTH_SEED_PAYLOAD_FIELD && businessBirth !== undefined;
1141
1194
  if (frameworkBirthCarrier) continue;
1142
1195
  fail(
@@ -1145,6 +1198,23 @@ export function discoverLawGraph(
1145
1198
  `a different name.`,
1146
1199
  );
1147
1200
  }
1201
+ const affectedAggregateIds = [
1202
+ intent.aggregateId,
1203
+ ...(intent.secondaryOutputs ?? []).map((output) => output.aggregateId),
1204
+ ];
1205
+ const frameworkCapabilities = new Set<"owned-authoring">();
1206
+ if (
1207
+ businessBirth?.owned === true ||
1208
+ affectedAggregateIds.some((aggregateId) =>
1209
+ aggregateByLabel.get(aggregateId)?.node.ownerField !== undefined)
1210
+ ) {
1211
+ frameworkCapabilities.add("owned-authoring");
1212
+ }
1213
+ const payloadShape = lowerAnonymousContractFields(
1214
+ internalPayloadContract(tenantPayloadContract, frameworkCapabilities),
1215
+ payloadContractEntity,
1216
+ );
1217
+ for (const node of payloadShape.nodes) anonymousValueNodes.set(node.entity.id, node);
1148
1218
  payloadContractNodes.push({
1149
1219
  kind: "payload-contract",
1150
1220
  entity: payloadContractEntity,