@githolon/dsl 0.100.5 → 0.101.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.
Files changed (97) hide show
  1. package/dart/CHANGELOG.md +12 -0
  2. package/dart/lib/src/dev_cloud.dart +46 -15
  3. package/dart/lib/src/test_harness.dart +220 -71
  4. package/dart/lib/src/webview_bridge.dart +640 -126
  5. package/dart/pubspec.lock +1 -1
  6. package/dart/pubspec.yaml +2 -2
  7. package/dart/test/acquire_session_credentials_test.dart +1 -1
  8. package/dart/test/adv_realm_interference_test.dart +1 -1
  9. package/dart/test/bundle_version_pairing_test.dart +11 -22
  10. package/dart/test/cluster_attested_test.dart +1 -1
  11. package/dart/test/cluster_births_test.dart +1 -1
  12. package/dart/test/cluster_device_keyed_acceptance_test.dart +1 -1
  13. package/dart/test/cluster_local_autosync_test.dart +1 -1
  14. package/dart/test/cluster_scale_test.dart +1 -1
  15. package/dart/test/co2_product_gauntlet_test.dart +228 -0
  16. package/dart/test/dlq_session_surface_test.dart +1 -1
  17. package/dart/test/errors_test.dart +63 -12
  18. package/dart/test/first_app_gauntlet_test.dart +132 -87
  19. package/dart/test/local_sync_inert_test.dart +10 -5
  20. package/dart/test/parent_signer_recovery_test.dart +1 -1
  21. package/dart/test/sealing_client_test.dart +1 -1
  22. package/dart/test/session_bound_test.dart +1 -1
  23. package/dart/test/test_harness_test.dart +2 -2
  24. package/dart/test/vo_read_typing_test.dart +1 -1
  25. package/dart/test/watch_after_offer_stall_test.dart +1 -1
  26. package/dart/test/webview_bridge_test.dart +274 -0
  27. package/dart_types/CHANGELOG.md +6 -0
  28. package/dart_types/lib/src/errors.dart +136 -30
  29. package/dart_types/lib/src/surfaced.dart +48 -2
  30. package/dart_types/lib/src/wire.dart +3 -1
  31. package/dart_types/pubspec.yaml +1 -1
  32. package/dart_types/test/surfaced_test.dart +19 -0
  33. package/package.json +1 -1
  34. package/src/activity.ts +43 -4
  35. package/src/aggregate.ts +132 -22
  36. package/src/attested_core.ts +1 -1
  37. package/src/authoring.ts +119 -6
  38. package/src/birth_meter.ts +4 -4
  39. package/src/birth_meter_ensure.ts +165 -29
  40. package/src/births.ts +53 -22
  41. package/src/build_package.ts +96 -20
  42. package/src/business_birth.ts +79 -16
  43. package/src/codegen_dart.ts +267 -37
  44. package/src/codegen_dart_client.ts +930 -87
  45. package/src/codegen_openapi.ts +28 -5
  46. package/src/codegen_proof.ts +107 -11
  47. package/src/codegen_ts.ts +961 -135
  48. package/src/compile_package_main.ts +172 -53
  49. package/src/conflicts.ts +252 -0
  50. package/src/describe_model.ts +448 -0
  51. package/src/directive.ts +5 -38
  52. package/src/drivers.ts +12 -4
  53. package/src/engine_entry.ts +4 -0
  54. package/src/evolve_preview.ts +72 -11
  55. package/src/fields.ts +201 -55
  56. package/src/framework/application_home.ts +103 -0
  57. package/src/framework/application_home_domain.ts +2 -0
  58. package/src/framework/attested_receipts.ts +2 -2
  59. package/src/framework/auth_provider_law.ts +51 -24
  60. package/src/framework/business_corrections.ts +170 -0
  61. package/src/framework/disclosure.ts +1 -1
  62. package/src/framework/domain_lifecycle.ts +250 -16
  63. package/src/framework/encryption_identity.ts +1 -1
  64. package/src/framework/encryption_scope.ts +21 -13
  65. package/src/framework/era_certification.ts +1 -1
  66. package/src/framework/framework_aggregates.ts +2 -0
  67. package/src/framework/genesis.ts +1 -1
  68. package/src/framework/home.ts +22 -9
  69. package/src/framework/identity.ts +5 -5
  70. package/src/framework/impure_capability.ts +2 -2
  71. package/src/framework/owned_custody.ts +561 -0
  72. package/src/framework/platform_admin.ts +1 -1
  73. package/src/framework/relations.ts +9 -1
  74. package/src/framework/repair.ts +1 -1
  75. package/src/framework/signing_identity.ts +2 -2
  76. package/src/framework/source_roster.ts +1 -1
  77. package/src/framework/workspaces.ts +17 -10
  78. package/src/generated_client_names.ts +39 -0
  79. package/src/index.ts +20 -15
  80. package/src/kernel_capabilities.ts +25 -1
  81. package/src/law_check.ts +4 -1
  82. package/src/manifest.ts +356 -63
  83. package/src/read_projection_manifest.ts +8 -0
  84. package/src/read_projection_namespace.ts +4 -0
  85. package/src/relation.ts +1 -1
  86. package/src/shape_compat.ts +61 -5
  87. package/src/stable_ids.ts +38 -6
  88. package/src/stable_ids_types.ts +30 -0
  89. package/src/surfaced_lint.ts +0 -0
  90. package/src/usd.ts +62 -11
  91. package/src/usd_layers.ts +54 -3
  92. package/src/value_object.ts +23 -19
  93. package/src/wire.ts +1 -0
  94. package/src/wire_encode.ts +48 -4
  95. package/src/workspace_routing.ts +57 -0
  96. package/src/workspace_sharding.ts +9 -9
  97. package/src/certified_read.ts +0 -313
@@ -12,6 +12,8 @@ import { t } from "../fields.js";
12
12
  import { Lww } from "../drivers.js";
13
13
  import { set, withMarker } from "../ops.js";
14
14
  import { query } from "../query.js";
15
+ import { GENESIS_WARRANT_ID } from "./genesis.js";
16
+ import { refAs, workspaceInvariant } from "./workspace_invariant.js";
15
17
 
16
18
  const epochMs = z.number().int().nonnegative();
17
19
 
@@ -67,16 +69,13 @@ export const AuthProvider = aggregate("AuthProvider", {
67
69
  setBy: t.string().merge(Lww),
68
70
  /** Epoch-ms it was set (caller-stamped). */
69
71
  setAt: t.int().merge(Lww),
70
- });
72
+ }).public();
71
73
 
72
74
  /**
73
75
  * setAuthProvider — declare (or amend) the platform's identity provider. An
74
76
  * Ensure on the singleton; every change is an attributed, replayable intent.
75
77
  */
76
- export const setAuthProvider = directive("setAuthProvider")
77
- .ensures(AuthProvider)
78
- .payload(
79
- z.object({
78
+ const authProviderPayload = z.object({
80
79
  // NOT z.url(): zod's url() needs the URL global, which the sealed
81
80
  // deterministic engine does not provide — a regex keeps the plan pure.
82
81
  issuer: z.string().regex(/^https?:\/\/\S+$/, "expected an http(s) URL"),
@@ -96,26 +95,54 @@ export const setAuthProvider = directive("setAuthProvider")
96
95
  label: z.string().min(1),
97
96
  setBy: z.string().min(1),
98
97
  setAt: epochMs,
99
- }),
100
- )
101
- .plan((p) => {
102
- const ap = instance(AuthProvider, AUTH_PROVIDER_ID);
103
- return [
104
- withMarker(set(ap, "scope", "platform"), "ensures"),
105
- set(ap, "issuer", p.issuer),
106
- set(ap, "jwksUrl", p.jwksUrl),
107
- ...(p.audience ? [set(ap, "audience", p.audience)] : []),
108
- ...(p.keys ? [set(ap, "keys", p.keys)] : []),
109
- ...(p.principalClaim ? [set(ap, "principalClaim", p.principalClaim)] : []),
110
- ...(p.rootCert ? [set(ap, "rootCert", p.rootCert)] : []),
111
- ...(p.deviceBindingClaim ? [set(ap, "deviceBindingClaim", p.deviceBindingClaim)] : []),
112
- ...(p.requireDeviceBinding ? [set(ap, "requireDeviceBinding", p.requireDeviceBinding)] : []),
113
- set(ap, "label", p.label),
114
- set(ap, "setBy", p.setBy),
115
- set(ap, "setAt", p.setAt),
116
- ];
117
- })
98
+ });
99
+
100
+ type AuthProviderPayload = z.infer<typeof authProviderPayload>;
101
+ function writeAuthProvider(p: AuthProviderPayload) {
102
+ const ap = instance(AuthProvider, AUTH_PROVIDER_ID);
103
+ return [
104
+ withMarker(set(ap, "scope", "platform"), "ensures"),
105
+ set(ap, "issuer", p.issuer),
106
+ set(ap, "jwksUrl", p.jwksUrl),
107
+ ...(p.audience ? [set(ap, "audience", p.audience)] : []),
108
+ ...(p.keys ? [set(ap, "keys", p.keys)] : []),
109
+ ...(p.principalClaim ? [set(ap, "principalClaim", p.principalClaim)] : []),
110
+ ...(p.rootCert ? [set(ap, "rootCert", p.rootCert)] : []),
111
+ ...(p.deviceBindingClaim ? [set(ap, "deviceBindingClaim", p.deviceBindingClaim)] : []),
112
+ ...(p.requireDeviceBinding ? [set(ap, "requireDeviceBinding", p.requireDeviceBinding)] : []),
113
+ set(ap, "label", p.label),
114
+ set(ap, "setBy", p.setBy),
115
+ set(ap, "setAt", p.setAt),
116
+ ];
117
+ }
118
+
119
+ export const setAuthProvider = directive("setAuthProvider")
120
+ .ensures(AuthProvider)
121
+ .payload(authProviderPayload)
122
+ .plan(writeAuthProvider)
118
123
  .requires("admin");
119
124
 
125
+ /**
126
+ * Private child-genesis twin of {@link setAuthProvider}. It deliberately has no role requirement because
127
+ * roles do not exist yet while the child's genesis is folding. Its invariant admits it only while the
128
+ * GenesisWarrant is absent; the kernel appends that warrant last, permanently closing this lane before the
129
+ * workspace becomes servable. Later verifier changes can only use the role-gated maintenance directive.
130
+ */
131
+ export const seedAuthProvider = directive("seedAuthProvider")
132
+ .ensures(AuthProvider)
133
+ .payload(authProviderPayload)
134
+ .plan(writeAuthProvider);
135
+
136
+ export const authProviderSeedBeforeWarrant = workspaceInvariant("AuthProviderSeedBeforeWarrant")
137
+ .on("seedAuthProvider")
138
+ .reads(() => [refAs("warrant", "GenesisWarrant", GENESIS_WARRANT_ID)])
139
+ .assert(({ warrant }) => {
140
+ // Missing invariant refs are represented as an empty snapshot by the executor, not necessarily null.
141
+ const principal = (warrant as { principal?: unknown } | null | undefined)?.principal;
142
+ return typeof principal !== "string" || principal.length === 0
143
+ ? { accept: true }
144
+ : { reject: "seedAuthProvider is a child-genesis primitive; use the role-gated setAuthProvider after birth" };
145
+ });
146
+
120
147
  /** The declared read serving a workspace's OWN AuthProvider row (and birthHome's inheritance capture). */
121
148
  export const authProvider = query("authProvider").key("scope").returns(AuthProvider);
@@ -0,0 +1,170 @@
1
+ // NOMOS — framework-owned business corrections.
2
+ // Tenant code sees generated `corrections.undo(reason)` / `corrections.redo(reason)` only.
3
+
4
+ import { z } from "zod";
5
+ import { aggregate, instance, type AggregateHandle } from "../aggregate.js";
6
+ import { directive } from "../directive.js";
7
+ import { Lww } from "../drivers.js";
8
+ import { attributionField, t } from "../fields.js";
9
+ import { set, strike } from "../ops.js";
10
+ import type { DomainModule } from "../codegen_dart.js";
11
+
12
+ /** Obscure stable law ids: deliberately absent from ordinary directive/aggregate codegen. */
13
+ export const BUSINESS_CORRECTION_AGGREGATE = "nomos_internal_CorrectionRecord_a91e73";
14
+ export const BUSINESS_CORRECTION_DIRECTIVE = "nomos_internal_record_business_correction_a91e73";
15
+
16
+ const correctionRecord = aggregate(BUSINESS_CORRECTION_AGGREGATE, {
17
+ recordId: t.string().merge(Lww),
18
+ kind: t.enum(["undo", "redo"] as const).merge(Lww),
19
+ target: t.string().merge(Lww),
20
+ reason: t.string().merge(Lww),
21
+ correctedBy: attributionField(t.string().merge(Lww)),
22
+ correctedAt: t.string().merge(Lww),
23
+ }).public();
24
+
25
+ const correction = directive(BUSINESS_CORRECTION_DIRECTIVE)
26
+ .ensures(correctionRecord)
27
+ .payload(z.object({
28
+ recordId: z.string().min(1),
29
+ target: z.string().min(1),
30
+ kind: z.enum(["undo", "redo"]),
31
+ reason: z.string().trim().min(1),
32
+ correctedAt: z.string().min(1),
33
+ }))
34
+ .plan((p, ctx) => {
35
+ const record = instance(correctionRecord, p.recordId);
36
+ return [
37
+ // Retraction and testimony are one verified intent. There is no state-only correction lane.
38
+ strike(p.target),
39
+ set(record, "recordId", p.recordId),
40
+ set(record, "kind", p.kind),
41
+ set(record, "target", p.target),
42
+ set(record, "reason", p.reason),
43
+ set(record, "correctedBy", ctx.principal().asAttribution()),
44
+ set(record, "correctedAt", p.correctedAt),
45
+ ];
46
+ });
47
+
48
+ export const businessCorrectionRecord = {
49
+ ...correctionRecord,
50
+ businessBirthInternal: true as const,
51
+ };
52
+ export const recordBusinessCorrection = {
53
+ ...correction,
54
+ businessBirthInternal: true as const,
55
+ };
56
+
57
+ const RECORD_EXPORT = "__nomos_business_correction_record_a91e73";
58
+ const DIRECTIVE_EXPORT = "__nomos_business_correction_a91e73";
59
+
60
+ function declarationsOf(mod: Record<string, unknown>): unknown[] {
61
+ return Object.values(mod).flatMap((value) => Array.isArray(value) ? value : [value]);
62
+ }
63
+
64
+ function reservedCollision(concept: string): never {
65
+ throw new Error(
66
+ `Nomos internal concept '${concept}' is already taken by the framework. Sorry mate, Nomos will not ` +
67
+ "silently overwrite or reroute the tenant declaration; give the business concept a different name.",
68
+ );
69
+ }
70
+
71
+ function isDirective(value: unknown): value is { id: string; payloadSchema: unknown; businessBirthInternal?: true } {
72
+ return value !== null
73
+ && typeof value === "object"
74
+ && typeof (value as { id?: unknown }).id === "string"
75
+ && (value as { payloadSchema?: unknown }).payloadSchema !== undefined
76
+ && typeof (value as { plan?: unknown }).plan === "function";
77
+ }
78
+
79
+ /**
80
+ * Every domain with at least one public business verb carries the same private correction primitive.
81
+ * The package and sealed engine call this at their shared merge seam, so generated availability can
82
+ * never drift from installed law. Stable id/export collisions fail loudly instead of becoming magic.
83
+ */
84
+ export function withBusinessCorrectionExports<M extends Record<string, unknown>>(mod: M): Record<string, unknown> {
85
+ const declarations = declarationsOf(mod);
86
+ const hasBusinessVerb = declarations.some(
87
+ (value) => isDirective(value) && value.businessBirthInternal !== true,
88
+ );
89
+ if (!hasBusinessVerb) return mod;
90
+
91
+ if (Object.prototype.hasOwnProperty.call(mod, RECORD_EXPORT) && mod[RECORD_EXPORT] !== businessCorrectionRecord) {
92
+ reservedCollision(RECORD_EXPORT);
93
+ }
94
+ if (Object.prototype.hasOwnProperty.call(mod, DIRECTIVE_EXPORT) && mod[DIRECTIVE_EXPORT] !== recordBusinessCorrection) {
95
+ reservedCollision(DIRECTIVE_EXPORT);
96
+ }
97
+ for (const value of declarations) {
98
+ if (
99
+ value !== businessCorrectionRecord
100
+ && value !== null
101
+ && typeof value === "object"
102
+ && (value as { id?: unknown }).id === BUSINESS_CORRECTION_AGGREGATE
103
+ ) {
104
+ reservedCollision(BUSINESS_CORRECTION_AGGREGATE);
105
+ }
106
+ if (
107
+ value !== recordBusinessCorrection
108
+ && isDirective(value)
109
+ && value.id === BUSINESS_CORRECTION_DIRECTIVE
110
+ ) {
111
+ reservedCollision(BUSINESS_CORRECTION_DIRECTIVE);
112
+ }
113
+ }
114
+
115
+ return {
116
+ ...mod,
117
+ [RECORD_EXPORT]: businessCorrectionRecord,
118
+ [DIRECTIVE_EXPORT]: recordBusinessCorrection,
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Layered USD lowers each authored module separately, beneath `composeDomainModule`'s normal
124
+ * framework-expansion seam. Carry the universal correction law once, in the weakest layer, so
125
+ * flatten-after-lower remains byte-identical to lower-after-compose. An already-composed layer is
126
+ * left alone; partial or tenant-owned declarations of either reserved id fail loudly.
127
+ */
128
+ export function withBusinessCorrectionLayer(
129
+ modules: readonly DomainModule[],
130
+ ): readonly DomainModule[] {
131
+ const hasBusinessVerb = modules.some((module) =>
132
+ module.directives.some((value) => (value as { businessBirthInternal?: true }).businessBirthInternal !== true),
133
+ );
134
+ if (!hasBusinessVerb) return modules;
135
+
136
+ const correctionAggregates = modules.flatMap((module) =>
137
+ module.aggregates.filter((value) => value.id === BUSINESS_CORRECTION_AGGREGATE),
138
+ );
139
+ const correctionDirectives = modules.flatMap((module) =>
140
+ module.directives.filter((value) => value.id === BUSINESS_CORRECTION_DIRECTIVE),
141
+ );
142
+ if (correctionAggregates.some(
143
+ (value) => (value as AggregateHandle & { businessBirthInternal?: true }).businessBirthInternal !== true,
144
+ )) {
145
+ reservedCollision(BUSINESS_CORRECTION_AGGREGATE);
146
+ }
147
+ if (correctionDirectives.some(
148
+ (value) => (value as typeof value & { businessBirthInternal?: true }).businessBirthInternal !== true,
149
+ )) {
150
+ reservedCollision(BUSINESS_CORRECTION_DIRECTIVE);
151
+ }
152
+ if ((correctionAggregates.length === 0) !== (correctionDirectives.length === 0)) {
153
+ throw new Error(
154
+ "Nomos internal business-correction law is only partially present in the layered domain; " +
155
+ "refusing to emit a law whose testimony and retraction halves can drift.",
156
+ );
157
+ }
158
+ if (correctionAggregates.length > 0) return modules;
159
+
160
+ const [weakest, ...stronger] = modules;
161
+ if (weakest === undefined) return modules;
162
+ return [
163
+ {
164
+ ...weakest,
165
+ aggregates: [...weakest.aggregates, businessCorrectionRecord],
166
+ directives: [...weakest.directives, recordBusinessCorrection],
167
+ },
168
+ ...stronger,
169
+ ];
170
+ }
@@ -62,7 +62,7 @@ export const Disclosure = aggregate("Disclosure", {
62
62
  purpose: t.string().merge(Lww), // why (audit metadata)
63
63
  generatedBy: t.string().merge(Lww), // authoring actor
64
64
  generatedAt: t.string().merge(Lww), // ISO-8601
65
- });
65
+ }).public();
66
66
 
67
67
  /**
68
68
  * `recordDisclosure` — `.creates` a Disclosure (instance-bound by
@@ -12,13 +12,15 @@
12
12
  * * `PolicyBundle` is the content-addressed package object (`policy:{hash}.bundle`);
13
13
  * * `DomainInstallation` is the workspace resource with spec/status/phase;
14
14
  * * `installDomain` writes BOTH in one intent and declares the `installDomain`
15
- * capability. This is the richer controller API; bootstrap exposes a thinner
16
- * intent with the same name and payload shape for stage-zero install.
15
+ * capability. `stageDomain` writes the same content as Pending without serving it;
16
+ * `promoteDomain` and `rollbackDomain` are phase changes whose admission is judged
17
+ * against the staged package. This is the richer controller API; bootstrap exposes
18
+ * a thinner install-only intent for stage-zero.
17
19
  */
18
20
  import { z } from "zod";
19
21
  import { aggregate, instance } from "../aggregate.js";
20
22
  import { Lww, MapOf } from "../drivers.js";
21
- import { t } from "../fields.js";
23
+ import { attributionField, t } from "../fields.js";
22
24
  import { directive } from "../directive.js";
23
25
  import { set, withMarker, addToSet, type PlannedOp } from "../ops.js";
24
26
 
@@ -35,7 +37,7 @@ export type DomainInstallationPhase = (typeof DOMAIN_INSTALLATION_PHASES)[number
35
37
 
36
38
  export const PolicyBundle = aggregate("PolicyBundle", {
37
39
  bundle: t.string().merge(Lww),
38
- });
40
+ }).public();
39
41
 
40
42
  export const DomainInstallation = aggregate("DomainInstallation", {
41
43
  "spec.domainHash": t.string().merge(Lww),
@@ -51,7 +53,39 @@ export const DomainInstallation = aggregate("DomainInstallation", {
51
53
  "status.observedGeneration": t.int().merge(Lww),
52
54
  "status.supersededBy": t.string().merge(Lww).optional(),
53
55
  "status.conditions": t.map(t.json()).merge(MapOf(Lww)),
54
- });
56
+ }).public();
57
+
58
+ /**
59
+ * The accepted, exact-history evidence for one activation. This is deliberately an
60
+ * obscure framework concept: tenant models never author or name it. The generated
61
+ * lifecycle client runs pre-flight and folds this receipt in the SAME intent that
62
+ * activates the candidate, so recording evidence cannot make its own head anchor stale.
63
+ */
64
+ export const RELEASE_RECEIPT_AGGREGATE = "nomos_internal_ReleaseReceipt_6f0d3a";
65
+ export const RELEASE_RECEIPT_PREFIX = "nomos-release-receipt:";
66
+
67
+ export const ReleaseReceipt = aggregate(RELEASE_RECEIPT_AGGREGATE, {
68
+ candidateHash: t.string().merge(Lww),
69
+ fromHash: t.string().merge(Lww).optional(),
70
+ domain: t.string().merge(Lww),
71
+ atHash: t.string().merge(Lww),
72
+ atStateRoot: t.string().merge(Lww),
73
+ historyHash: t.string().merge(Lww),
74
+ reportHash: t.string().merge(Lww),
75
+ reportJson: t.string().merge(Lww),
76
+ complete: t.bool(),
77
+ evolveOk: t.bool(),
78
+ rawPromotable: t.bool(),
79
+ projectionEqual: t.bool(),
80
+ replayedIntents: t.int().merge(Lww),
81
+ candidateIntents: t.int().merge(Lww),
82
+ divergenceCount: t.int().merge(Lww),
83
+ baselineRoot: t.string().merge(Lww),
84
+ candidateRoot: t.string().merge(Lww),
85
+ acceptanceReason: t.string().merge(Lww).optional(),
86
+ acceptedBy: attributionField(t.string().merge(Lww)),
87
+ acceptedAt: t.int().merge(Lww),
88
+ }).public();
55
89
 
56
90
  export function policyAggregateId(domainHash: string): string {
57
91
  return `policy:${domainHash}`;
@@ -69,16 +103,66 @@ function domainInstallationInstance(domainHash: string) {
69
103
  return instance(DomainInstallation, domainInstallationAggregateId(domainHash));
70
104
  }
71
105
 
106
+ export function releaseReceiptAggregateId(reportHash: string): string {
107
+ return `${RELEASE_RECEIPT_PREFIX}${reportHash}`;
108
+ }
109
+
110
+ function releaseReceiptInstance(reportHash: string) {
111
+ return instance(ReleaseReceipt, releaseReceiptAggregateId(reportHash));
112
+ }
113
+
114
+ const dispositions = z.object({
115
+ retired: z.array(z.string()).optional(),
116
+ retyped: z.record(z.string(), z.string()).optional(),
117
+ rebinds: z.record(z.string(), z.string()).optional(),
118
+ ackBy: z.string().optional(),
119
+ }).optional();
120
+
121
+ const lowerHex64 = z.string().regex(/^[0-9a-f]{64}$/);
122
+ const gitOid = z.string().regex(/^[0-9a-f]{40}$/);
123
+ const preflightReceipt = z.object({
124
+ candidateHash: lowerHex64,
125
+ domain: z.string().min(1),
126
+ atHash: gitOid,
127
+ atStateRoot: lowerHex64,
128
+ historyHash: lowerHex64,
129
+ reportHash: lowerHex64,
130
+ reportJson: z.string().min(2),
131
+ complete: z.literal(true),
132
+ evolveOk: z.literal(true),
133
+ rawPromotable: z.boolean(),
134
+ projectionEqual: z.boolean(),
135
+ replayedIntents: z.number().int().nonnegative(),
136
+ candidateIntents: z.number().int().nonnegative(),
137
+ divergenceCount: z.number().int().nonnegative(),
138
+ baselineRoot: lowerHex64,
139
+ candidateRoot: lowerHex64,
140
+ acceptanceReason: z.string().trim().min(1).optional(),
141
+ }).superRefine((receipt, ctx) => {
142
+ if (!receipt.rawPromotable && receipt.acceptanceReason === undefined) {
143
+ ctx.addIssue({
144
+ code: z.ZodIssueCode.custom,
145
+ path: ["acceptanceReason"],
146
+ message: "a divergent but complete report needs an explicit business acceptance reason",
147
+ });
148
+ }
149
+ });
150
+
151
+ const packagePayload = {
152
+ domainHash: z.string(),
153
+ packageUsda: z.string(),
154
+ installedBy: z.string(),
155
+ authorityScope: z.string().default("workspace/root"),
156
+ dependencies: z.array(z.string()).default([]),
157
+ supersedes: z.string().optional(),
158
+ finalizers: z.array(z.string()).default([]),
159
+ };
160
+
72
161
  export const installDomain = directive("installDomain")
73
162
  .ensures(DomainInstallation)
74
163
  .payload(
75
164
  z.object({
76
- domainHash: z.string(),
77
- packageUsda: z.string(),
78
- installedBy: z.string(),
79
- authorityScope: z.string().default("workspace/root"),
80
- dependencies: z.array(z.string()).default([]),
81
- supersedes: z.string().optional(),
165
+ ...packagePayload,
82
166
  // THE FRONTIER TRANSITION (install-lifecycle): `replaces` names a PRIOR install this
83
167
  // deploy makes current UNEQUIVOCALLY — the prior install is flipped out of the served
84
168
  // frontier (`Superseded`, or `Retired` when `retireReplaced`) and gets the reverse
@@ -86,7 +170,10 @@ export const installDomain = directive("installDomain")
86
170
  // byte-identical ops to the pre-lifecycle plan). `replaces` also sets `spec.supersedes`.
87
171
  replaces: z.string().optional(),
88
172
  retireReplaced: z.boolean().default(false),
89
- finalizers: z.array(z.string()).default([]),
173
+ dispositions,
174
+ }).refine((p) => p.replaces === undefined || p.replaces !== p.domainHash, {
175
+ message: "a domain package cannot replace itself",
176
+ path: ["replaces"],
90
177
  }),
91
178
  )
92
179
  .plan((p): PlannedOp[] => {
@@ -103,8 +190,8 @@ export const installDomain = directive("installDomain")
103
190
  set(install, "status.phase", "Active"),
104
191
  set(install, "status.observedGeneration", 1),
105
192
  ];
106
- if (p.dependencies.length > 0) {
107
- ops.push(addToSet(install, "spec.dependencies", p.dependencies));
193
+ if ((p.dependencies ?? []).length > 0) {
194
+ ops.push(addToSet(install, "spec.dependencies", p.dependencies ?? []));
108
195
  }
109
196
  // The predecessor back-link: `supersedes` (explicit) or `replaces` (the actionable form).
110
197
  const supersedes = p.supersedes ?? p.replaces;
@@ -119,9 +206,156 @@ export const installDomain = directive("installDomain")
119
206
  );
120
207
  ops.push(set(replaced, "status.supersededBy", p.domainHash));
121
208
  }
122
- if (p.finalizers.length > 0) {
123
- ops.push(addToSet(install, "spec.finalizers", p.finalizers));
209
+ if ((p.finalizers ?? []).length > 0) {
210
+ ops.push(addToSet(install, "spec.finalizers", p.finalizers ?? []));
211
+ }
212
+ return ops;
213
+ })
214
+ .requires("installDomain");
215
+
216
+ /** Store candidate law in the ledger without changing the served law frontier. The
217
+ * kernel content-verifies the package and lifecycle pair, while the Pending phase is
218
+ * deliberately invisible to dispatch, reads and replay's current-law map. */
219
+ export const stageDomain = directive("stageDomain")
220
+ .ensures(DomainInstallation)
221
+ .payload(z.object({ ...packagePayload }).refine(
222
+ (p) => p.supersedes === undefined || p.supersedes !== p.domainHash,
223
+ { message: "a staged domain package cannot supersede itself", path: ["supersedes"] },
224
+ ))
225
+ .plan((p): PlannedOp[] => {
226
+ const policy = policyBundleInstance(p.domainHash);
227
+ const install = domainInstallationInstance(p.domainHash);
228
+ const ops: PlannedOp[] = [
229
+ withMarker(set(policy, "bundle", p.packageUsda), "ensures"),
230
+ set(install, "spec.domainHash", p.domainHash),
231
+ set(install, "spec.packageHash", p.domainHash),
232
+ set(install, "spec.policyAggregate", policyAggregateId(p.domainHash)),
233
+ set(install, "spec.authorityScope", p.authorityScope),
234
+ set(install, "spec.installedBy", p.installedBy),
235
+ set(install, "metadata.generation", 1),
236
+ set(install, "status.phase", "Pending"),
237
+ set(install, "status.observedGeneration", 0),
238
+ ];
239
+ if ((p.dependencies ?? []).length > 0) ops.push(addToSet(install, "spec.dependencies", p.dependencies ?? []));
240
+ if (p.supersedes !== undefined) ops.push(set(install, "spec.supersedes", p.supersedes));
241
+ if ((p.finalizers ?? []).length > 0) ops.push(addToSet(install, "spec.finalizers", p.finalizers ?? []));
242
+ return ops;
243
+ })
244
+ .requires("installDomain");
245
+
246
+ /** Make a staged package current. The package bytes are already committed; the kernel
247
+ * re-loads them from prefix state and runs the evolve gate on this phase transition.
248
+ * The prior version stays Active by default so gradual-rollout clients can still have
249
+ * their declared era checked; the newer activation stamp alone makes this law current. */
250
+ export const promoteDomain = directive("promoteDomain")
251
+ .mutates(DomainInstallation)
252
+ .payload(z.object({
253
+ domainHash: z.string(),
254
+ replaces: z.string().optional(),
255
+ retireReplaced: z.boolean().default(false),
256
+ dispositions,
257
+ preflight: preflightReceipt,
258
+ }).refine((p) => p.replaces === undefined || p.replaces !== p.domainHash, {
259
+ message: "a promoted domain package cannot replace itself",
260
+ path: ["replaces"],
261
+ }).refine((p) => p.preflight.candidateHash === p.domainHash, {
262
+ message: "the pre-flight receipt must name the package being promoted",
263
+ path: ["preflight", "candidateHash"],
264
+ }))
265
+ .plan((p, ctx): PlannedOp[] => {
266
+ const promoted = domainInstallationInstance(p.domainHash);
267
+ const receipt = releaseReceiptInstance(p.preflight.reportHash);
268
+ const ops: PlannedOp[] = [
269
+ set(promoted, "status.phase", "Active"),
270
+ set(promoted, "status.observedGeneration", 1),
271
+ withMarker(set(receipt, "candidateHash", p.domainHash), "ensures"),
272
+ ...(p.replaces !== undefined ? [set(receipt, "fromHash", p.replaces)] : []),
273
+ set(receipt, "domain", p.preflight.domain),
274
+ set(receipt, "atHash", p.preflight.atHash),
275
+ set(receipt, "atStateRoot", p.preflight.atStateRoot),
276
+ set(receipt, "historyHash", p.preflight.historyHash),
277
+ set(receipt, "reportHash", p.preflight.reportHash),
278
+ set(receipt, "reportJson", p.preflight.reportJson),
279
+ set(receipt, "complete", true),
280
+ set(receipt, "evolveOk", true),
281
+ set(receipt, "rawPromotable", p.preflight.rawPromotable),
282
+ set(receipt, "projectionEqual", p.preflight.projectionEqual),
283
+ set(receipt, "replayedIntents", p.preflight.replayedIntents),
284
+ set(receipt, "candidateIntents", p.preflight.candidateIntents),
285
+ set(receipt, "divergenceCount", p.preflight.divergenceCount),
286
+ set(receipt, "baselineRoot", p.preflight.baselineRoot),
287
+ set(receipt, "candidateRoot", p.preflight.candidateRoot),
288
+ ...(p.preflight.acceptanceReason !== undefined
289
+ ? [set(receipt, "acceptanceReason", p.preflight.acceptanceReason)]
290
+ : []),
291
+ set(receipt, "acceptedBy", ctx.principal().asAttribution()),
292
+ set(receipt, "acceptedAt", ctx.clock().physical),
293
+ ];
294
+ if (p.replaces !== undefined) {
295
+ const replaced = domainInstallationInstance(p.replaces);
296
+ ops.push(set(promoted, "spec.supersedes", p.replaces));
297
+ if (p.retireReplaced) {
298
+ ops.push(set(replaced, "status.phase", "Retired"));
299
+ ops.push(set(replaced, "status.supersededBy", p.domainHash));
300
+ }
124
301
  }
125
302
  return ops;
126
303
  })
304
+ .alsoEnsures(ReleaseReceipt)
305
+ .requires("installDomain");
306
+
307
+ /** Rollback is a new governed transition, never a history-pointer rewind. The prior
308
+ * package is promoted from committed custody and the failed candidate becomes Disabled;
309
+ * the kernel applies the same evolve judgment in the reverse direction. */
310
+ export const rollbackDomain = directive("rollbackDomain")
311
+ .mutates(DomainInstallation)
312
+ .payload(z.object({
313
+ candidateHash: z.string(),
314
+ priorHash: z.string(),
315
+ dispositions,
316
+ preflight: preflightReceipt,
317
+ }).refine((p) => p.candidateHash !== p.priorHash, {
318
+ message: "rollback candidate and prior package must differ",
319
+ path: ["priorHash"],
320
+ }).refine((p) => p.preflight.candidateHash === p.priorHash, {
321
+ message: "the rollback pre-flight receipt must name the prior package being restored",
322
+ path: ["preflight", "candidateHash"],
323
+ }).refine((p) => (p.preflight.acceptanceReason ?? "").trim().length > 0, {
324
+ message: "restoring previous law needs an attributed business reason",
325
+ path: ["preflight", "acceptanceReason"],
326
+ }))
327
+ .plan((p, ctx): PlannedOp[] => {
328
+ const candidate = domainInstallationInstance(p.candidateHash);
329
+ const prior = domainInstallationInstance(p.priorHash);
330
+ const receipt = releaseReceiptInstance(p.preflight.reportHash);
331
+ return [
332
+ set(candidate, "status.phase", "Disabled"),
333
+ set(candidate, "status.supersededBy", p.priorHash),
334
+ set(prior, "status.phase", "Active"),
335
+ set(prior, "status.observedGeneration", 1),
336
+ withMarker(set(receipt, "candidateHash", p.priorHash), "ensures"),
337
+ set(receipt, "fromHash", p.candidateHash),
338
+ set(receipt, "domain", p.preflight.domain),
339
+ set(receipt, "atHash", p.preflight.atHash),
340
+ set(receipt, "atStateRoot", p.preflight.atStateRoot),
341
+ set(receipt, "historyHash", p.preflight.historyHash),
342
+ set(receipt, "reportHash", p.preflight.reportHash),
343
+ set(receipt, "reportJson", p.preflight.reportJson),
344
+ set(receipt, "complete", true),
345
+ set(receipt, "evolveOk", true),
346
+ set(receipt, "rawPromotable", p.preflight.rawPromotable),
347
+ set(receipt, "projectionEqual", p.preflight.projectionEqual),
348
+ set(receipt, "replayedIntents", p.preflight.replayedIntents),
349
+ set(receipt, "candidateIntents", p.preflight.candidateIntents),
350
+ set(receipt, "divergenceCount", p.preflight.divergenceCount),
351
+ set(receipt, "baselineRoot", p.preflight.baselineRoot),
352
+ set(receipt, "candidateRoot", p.preflight.candidateRoot),
353
+ ...(p.preflight.acceptanceReason !== undefined
354
+ ? [set(receipt, "acceptanceReason", p.preflight.acceptanceReason)]
355
+ : []),
356
+ set(receipt, "acceptedBy", ctx.principal().asAttribution()),
357
+ set(receipt, "acceptedAt", ctx.clock().physical),
358
+ ];
359
+ })
360
+ .alsoEnsures(ReleaseReceipt)
127
361
  .requires("installDomain");
@@ -50,7 +50,7 @@ export const EncKey = aggregate("EncKey", {
50
50
  principal: t.string().merge(Lww),
51
51
  encPublicKey: t.string().merge(Lww), // base64 X25519 (32 bytes)
52
52
  publishedAt: t.string().merge(Lww), // ISO-8601, caller-stamped (determinism)
53
- });
53
+ }).public();
54
54
  export type EncKeyHandle = typeof EncKey;
55
55
 
56
56
  /** Deterministic per-principal instance id — `enc-key-<principal>`. */