@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.
- package/dart/CHANGELOG.md +12 -0
- package/dart/lib/src/dev_cloud.dart +46 -15
- package/dart/lib/src/test_harness.dart +220 -71
- package/dart/lib/src/webview_bridge.dart +640 -126
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart/test/acquire_session_credentials_test.dart +1 -1
- package/dart/test/adv_realm_interference_test.dart +1 -1
- package/dart/test/bundle_version_pairing_test.dart +11 -22
- package/dart/test/cluster_attested_test.dart +1 -1
- package/dart/test/cluster_births_test.dart +1 -1
- package/dart/test/cluster_device_keyed_acceptance_test.dart +1 -1
- package/dart/test/cluster_local_autosync_test.dart +1 -1
- package/dart/test/cluster_scale_test.dart +1 -1
- package/dart/test/co2_product_gauntlet_test.dart +228 -0
- package/dart/test/dlq_session_surface_test.dart +1 -1
- package/dart/test/errors_test.dart +63 -12
- package/dart/test/first_app_gauntlet_test.dart +132 -87
- package/dart/test/local_sync_inert_test.dart +10 -5
- package/dart/test/parent_signer_recovery_test.dart +1 -1
- package/dart/test/sealing_client_test.dart +1 -1
- package/dart/test/session_bound_test.dart +1 -1
- package/dart/test/test_harness_test.dart +2 -2
- package/dart/test/vo_read_typing_test.dart +1 -1
- package/dart/test/watch_after_offer_stall_test.dart +1 -1
- package/dart/test/webview_bridge_test.dart +274 -0
- package/dart_types/CHANGELOG.md +6 -0
- package/dart_types/lib/src/errors.dart +136 -30
- package/dart_types/lib/src/surfaced.dart +48 -2
- package/dart_types/lib/src/wire.dart +3 -1
- package/dart_types/pubspec.yaml +1 -1
- package/dart_types/test/surfaced_test.dart +19 -0
- package/package.json +1 -1
- package/src/activity.ts +43 -4
- package/src/aggregate.ts +132 -22
- package/src/attested_core.ts +1 -1
- package/src/authoring.ts +119 -6
- package/src/birth_meter.ts +4 -4
- package/src/birth_meter_ensure.ts +165 -29
- package/src/births.ts +53 -22
- package/src/build_package.ts +96 -20
- package/src/business_birth.ts +79 -16
- package/src/codegen_dart.ts +267 -37
- package/src/codegen_dart_client.ts +930 -87
- package/src/codegen_openapi.ts +28 -5
- package/src/codegen_proof.ts +107 -11
- package/src/codegen_ts.ts +961 -135
- package/src/compile_package_main.ts +172 -53
- package/src/conflicts.ts +252 -0
- package/src/describe_model.ts +448 -0
- package/src/directive.ts +5 -38
- package/src/drivers.ts +12 -4
- package/src/engine_entry.ts +4 -0
- package/src/evolve_preview.ts +72 -11
- package/src/fields.ts +201 -55
- package/src/framework/application_home.ts +103 -0
- package/src/framework/application_home_domain.ts +2 -0
- package/src/framework/attested_receipts.ts +2 -2
- package/src/framework/auth_provider_law.ts +51 -24
- package/src/framework/business_corrections.ts +170 -0
- package/src/framework/disclosure.ts +1 -1
- package/src/framework/domain_lifecycle.ts +250 -16
- package/src/framework/encryption_identity.ts +1 -1
- package/src/framework/encryption_scope.ts +21 -13
- package/src/framework/era_certification.ts +1 -1
- package/src/framework/framework_aggregates.ts +2 -0
- package/src/framework/genesis.ts +1 -1
- package/src/framework/home.ts +22 -9
- package/src/framework/identity.ts +5 -5
- package/src/framework/impure_capability.ts +2 -2
- package/src/framework/owned_custody.ts +561 -0
- package/src/framework/platform_admin.ts +1 -1
- package/src/framework/relations.ts +9 -1
- package/src/framework/repair.ts +1 -1
- package/src/framework/signing_identity.ts +2 -2
- package/src/framework/source_roster.ts +1 -1
- package/src/framework/workspaces.ts +17 -10
- package/src/generated_client_names.ts +39 -0
- package/src/index.ts +20 -15
- package/src/kernel_capabilities.ts +25 -1
- package/src/law_check.ts +4 -1
- package/src/manifest.ts +356 -63
- package/src/read_projection_manifest.ts +8 -0
- package/src/read_projection_namespace.ts +4 -0
- package/src/relation.ts +1 -1
- package/src/shape_compat.ts +61 -5
- package/src/stable_ids.ts +38 -6
- package/src/stable_ids_types.ts +30 -0
- package/src/surfaced_lint.ts +0 -0
- package/src/usd.ts +62 -11
- package/src/usd_layers.ts +54 -3
- package/src/value_object.ts +23 -19
- package/src/wire.ts +1 -0
- package/src/wire_encode.ts +48 -4
- package/src/workspace_routing.ts +57 -0
- package/src/workspace_sharding.ts +9 -9
- package/src/certified_read.ts +0 -313
package/src/birth_meter.ts
CHANGED
|
@@ -83,7 +83,7 @@ export const NomosBirthRecord = aggregate("NomosBirthRecord", {
|
|
|
83
83
|
childGenesisHash: t.string().merge(Lww),
|
|
84
84
|
/** Caller-stamped birth time from the payload (`bornAt`, else `requestedAt`) — the timestamp doctrine. */
|
|
85
85
|
bornAt: t.string().merge(Lww),
|
|
86
|
-
});
|
|
86
|
+
}).public();
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
89
|
* NomosChildLifecycle — additive lifecycle for a child witnessed by NomosBirthRecord. It is deliberately
|
|
@@ -98,7 +98,7 @@ export const NomosChildLifecycle = aggregate("NomosChildLifecycle", {
|
|
|
98
98
|
retiredAt: t.int().merge(Lww),
|
|
99
99
|
reclaimedBy: t.string().merge(Lww),
|
|
100
100
|
reclaimedAt: t.int().merge(Lww),
|
|
101
|
-
});
|
|
101
|
+
}).public();
|
|
102
102
|
|
|
103
103
|
/** BirthPolicy — the workspace's birth-allowance singleton (id `birth-policy:workspace`), the
|
|
104
104
|
* `CloudPolicy` shape one level down: the default max live births per subject in THIS workspace. */
|
|
@@ -110,7 +110,7 @@ export const BirthPolicy = aggregate("BirthPolicy", {
|
|
|
110
110
|
setBy: t.string().merge(Lww),
|
|
111
111
|
/** Caller-stamped (ISO or epoch-ms string) — the timestamp doctrine. */
|
|
112
112
|
setAt: t.string().merge(Lww),
|
|
113
|
-
});
|
|
113
|
+
}).public();
|
|
114
114
|
|
|
115
115
|
/** BirthGrant — a per-subject allowance override (id `birth-grant:<principal>`), the `QuotaGrant`
|
|
116
116
|
* shape: the LATEST grant (max grantedAt) for a subject is the allowance in force. */
|
|
@@ -123,7 +123,7 @@ export const BirthGrant = aggregate("BirthGrant", {
|
|
|
123
123
|
grantedBy: t.string().merge(Lww),
|
|
124
124
|
/** epoch-ms — the recency tiebreak (latest grant wins), caller-stamped. */
|
|
125
125
|
grantedAt: t.int().merge(Lww),
|
|
126
|
-
});
|
|
126
|
+
}).public();
|
|
127
127
|
|
|
128
128
|
// ── the meter's declared queries (the #58 captured-read recipes) ────────────────────────────────────
|
|
129
129
|
|
|
@@ -42,6 +42,50 @@ import {
|
|
|
42
42
|
retireChildRequiresBirth,
|
|
43
43
|
retireChildNotReclaimed,
|
|
44
44
|
} from "./framework/birth_meter.js";
|
|
45
|
+
import { RelationTuple, relationTupleInstanceId, relationTuplesByObject } from "./framework/relations.js";
|
|
46
|
+
import { GENESIS_WARRANT_ID, GenesisWarrant, seedGenesis } from "./framework/genesis.js";
|
|
47
|
+
import {
|
|
48
|
+
BIRTH_ATTESTATION_ID,
|
|
49
|
+
BirthAttestation,
|
|
50
|
+
birthAttestation,
|
|
51
|
+
seedBirthAttestation,
|
|
52
|
+
} from "./framework/home.js";
|
|
53
|
+
import { requireParentAttestedBirth } from "./births.js";
|
|
54
|
+
import {
|
|
55
|
+
SignerDelegation,
|
|
56
|
+
enrollDelegation,
|
|
57
|
+
enrollSigner,
|
|
58
|
+
identityRelations,
|
|
59
|
+
signerDelegationByUser,
|
|
60
|
+
signerObject,
|
|
61
|
+
} from "./framework/signing_identity.js";
|
|
62
|
+
import {
|
|
63
|
+
AuthProvider,
|
|
64
|
+
authProvider,
|
|
65
|
+
authProviderSeedBeforeWarrant,
|
|
66
|
+
seedAuthProvider,
|
|
67
|
+
setAuthProvider,
|
|
68
|
+
} from "./framework/auth_provider_law.js";
|
|
69
|
+
import { refAs, workspaceInvariant } from "./framework/workspace_invariant.js";
|
|
70
|
+
|
|
71
|
+
/** Lightweight copy of the framework's founding-signer guard, kept here to avoid importing the whole
|
|
72
|
+
* governance module (whose unrelated captured-read laws would otherwise survive into every child lump). */
|
|
73
|
+
const bornFounderSignerWriteOnce = workspaceInvariant("FounderSignerWriteOnce")
|
|
74
|
+
.on("enrollSigner")
|
|
75
|
+
.reads((ctx) => [
|
|
76
|
+
refAs(
|
|
77
|
+
"tuple",
|
|
78
|
+
"RelationTuple",
|
|
79
|
+
"pre:" + relationTupleInstanceId(signerObject(String((ctx.intent ?? {}).principal ?? "")), "signer"),
|
|
80
|
+
),
|
|
81
|
+
])
|
|
82
|
+
.assert(({ tuple }, ctx) => {
|
|
83
|
+
const subjects = (tuple as { subjects?: Record<string, unknown> })?.subjects ?? {};
|
|
84
|
+
const want = "key:" + String((ctx?.intent ?? {}).keyHash ?? "");
|
|
85
|
+
return Object.keys(subjects).every((key) => key === want)
|
|
86
|
+
? { accept: true }
|
|
87
|
+
: { reject: "founding-signer-already-enrolled" };
|
|
88
|
+
});
|
|
45
89
|
|
|
46
90
|
/**
|
|
47
91
|
* The sanctioning declaration — an `ensures(NomosBirthRecord)` directive whose plan emits ZERO ops.
|
|
@@ -70,36 +114,128 @@ export function withBirthMeterExports<M extends Record<string, unknown>>(mod: M)
|
|
|
70
114
|
(v as { birthsChild?: boolean }).birthsChild === true &&
|
|
71
115
|
typeof (v as Directive<unknown>).plan === "function",
|
|
72
116
|
);
|
|
73
|
-
if
|
|
117
|
+
// A law that accepts the kernel-injected birth attestation is itself a possible CHILD even if it never
|
|
118
|
+
// births anything. Such a law must also accept the framework-owned founding signer step, otherwise the
|
|
119
|
+
// child can replay but can never sign its first write or birth a descendant without tenant recipe work.
|
|
120
|
+
const acceptsBirth = Object.values(mod).some(
|
|
121
|
+
(v) =>
|
|
122
|
+
v !== null &&
|
|
123
|
+
typeof v === "object" &&
|
|
124
|
+
(v as { id?: unknown }).id === "seedBirthAttestation" &&
|
|
125
|
+
typeof (v as Directive<unknown>).plan === "function",
|
|
126
|
+
);
|
|
127
|
+
if (!declaresBirth && !acceptsBirth) return mod;
|
|
74
128
|
const out: Record<string, unknown> = { ...mod };
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
129
|
+
const internal = <T extends object>(v: T): T => ({ ...v, businessBirthInternal: true }) as T;
|
|
130
|
+
const hasOwn = (key: string): boolean => Object.prototype.hasOwnProperty.call(out, key);
|
|
131
|
+
const isGeneratedInternal = (value: unknown): boolean =>
|
|
132
|
+
value !== null &&
|
|
133
|
+
typeof value === "object" &&
|
|
134
|
+
(value as { businessBirthInternal?: unknown }).businessBirthInternal === true;
|
|
135
|
+
const reservedNameError = (key: string): Error => new Error(
|
|
136
|
+
`Nomos internal export name '${key}' is reserved inside a birth-capable law, but the tenant module ` +
|
|
137
|
+
"already exports a different concept there. Nomos will not silently overwrite or reroute it; rename " +
|
|
138
|
+
"the tenant export.",
|
|
80
139
|
);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
140
|
+
/** Reserve a framework-only JS export slot as well as its stable law id. The keys are deliberately obscure,
|
|
141
|
+
* but obscurity is collision avoidance, not collision handling: an occupied slot is always a loud refusal. */
|
|
142
|
+
const reserve = <T>(key: string, canonical: T): void => {
|
|
143
|
+
if (!hasOwn(key)) {
|
|
144
|
+
out[key] = canonical;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const existing = out[key];
|
|
148
|
+
if (existing !== canonical && !isGeneratedInternal(existing)) throw reservedNameError(key);
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* Framework ownership follows the stable law id, not the tenant's export key. Older laws commonly
|
|
152
|
+
* re-exported `enrollSigner`/`seedGenesis` under their public names; merely adding a second hidden
|
|
153
|
+
* `__nomos_*` binding leaves BOTH declarations discoverable and lets codegen expose the old one. Mark
|
|
154
|
+
* every matching binding internal, and add the canonical declaration only when the law carried none.
|
|
155
|
+
*/
|
|
156
|
+
const internalize = <T extends { readonly id: string }>(key: string, canonical: T): void => {
|
|
157
|
+
if (hasOwn(key)) {
|
|
158
|
+
const existing = out[key];
|
|
159
|
+
const sameGeneratedId =
|
|
160
|
+
isGeneratedInternal(existing) &&
|
|
161
|
+
(existing as { id?: unknown }).id === canonical.id;
|
|
162
|
+
if (existing !== canonical && !sameGeneratedId) throw reservedNameError(key);
|
|
163
|
+
}
|
|
164
|
+
let found = false;
|
|
165
|
+
for (const [exportName, value] of Object.entries(out)) {
|
|
166
|
+
if (
|
|
167
|
+
value !== null &&
|
|
168
|
+
typeof value === "object" &&
|
|
169
|
+
(value as { id?: unknown }).id === canonical.id
|
|
170
|
+
) {
|
|
171
|
+
const isCanonicalSingleton = value === canonical;
|
|
172
|
+
const isNomosGeneratedBinding =
|
|
173
|
+
(value as { businessBirthInternal?: unknown }).businessBirthInternal === true;
|
|
174
|
+
if (!isCanonicalSingleton && !isNomosGeneratedBinding) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`Nomos framework id '${canonical.id}' is reserved inside a birth-capable law, but export ` +
|
|
177
|
+
`'${exportName}' declares a different tenant-owned concept with that id. Nomos will not ` +
|
|
178
|
+
`silently reinterpret it as framework machinery; rename the tenant concept.`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
out[exportName] = internal(value);
|
|
182
|
+
found = true;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (!found) out[key] = internal(canonical);
|
|
186
|
+
};
|
|
187
|
+
// THE BORN-WORKSPACE CORE: the kernel appends enrollSigner -> attestation -> warrant to every valid birth.
|
|
188
|
+
// Carry the law needed to judge that internal step in every possible child, plus the write-once guard that
|
|
189
|
+
// keeps the founding lane a genesis primitive. These declarations are certified but never tenant-facing.
|
|
190
|
+
internalize("__nomos_RelationTuple", RelationTuple);
|
|
191
|
+
reserve("__nomos_identityRelations", identityRelations);
|
|
192
|
+
internalize("__nomos_enrollSigner", enrollSigner);
|
|
193
|
+
internalize("__nomos_relationTuplesByObject", relationTuplesByObject);
|
|
194
|
+
internalize("__nomos_GenesisWarrant", GenesisWarrant);
|
|
195
|
+
internalize("__nomos_seedGenesis", seedGenesis);
|
|
196
|
+
internalize("__nomos_BirthAttestation", BirthAttestation);
|
|
197
|
+
internalize("__nomos_seedBirthAttestation", seedBirthAttestation);
|
|
198
|
+
internalize("__nomos_birthAttestation", birthAttestation);
|
|
199
|
+
internalize("__nomos_AuthProvider", AuthProvider);
|
|
200
|
+
internalize("__nomos_seedAuthProvider", seedAuthProvider);
|
|
201
|
+
reserve("__nomos_authProviderSeedBeforeWarrant", authProviderSeedBeforeWarrant);
|
|
202
|
+
internalize("__nomos_setAuthProvider", setAuthProvider);
|
|
203
|
+
internalize("__nomos_authProvider", authProvider);
|
|
204
|
+
reserve("__nomos_founderSignerWriteOnce", bornFounderSignerWriteOnce);
|
|
205
|
+
if (!declaresBirth) return out as M;
|
|
206
|
+
// Birth lifecycle is Nomos machinery for every birth-capable law. It remains certified/routable law,
|
|
207
|
+
// but no generated tenant API gets a second maintenance lane beside business `birth(...)`.
|
|
208
|
+
internalize("__nomos_nomosBirthRecordEnsure", nomosBirthRecordEnsure);
|
|
209
|
+
internalize("__nomos_NomosBirthRecord", NomosBirthRecord);
|
|
210
|
+
internalize("__nomos_NomosChildLifecycle", NomosChildLifecycle);
|
|
211
|
+
internalize("__nomos_BirthPolicy", BirthPolicy);
|
|
212
|
+
internalize("__nomos_BirthGrant", BirthGrant);
|
|
213
|
+
internalize("__nomos_retireChild", retireChild);
|
|
214
|
+
internalize("__nomos_reclaimChild", reclaimChild);
|
|
215
|
+
internalize("__nomos_retireChildAsAdmin", retireChildAsAdmin);
|
|
216
|
+
internalize("__nomos_reclaimChildAsAdmin", reclaimChildAsAdmin);
|
|
217
|
+
reserve("__nomos_reclaimChildRequiresRetired", reclaimChildRequiresRetired);
|
|
218
|
+
reserve("__nomos_reclaimChildAsAdminRequiresRetired", reclaimChildAsAdminRequiresRetired);
|
|
219
|
+
reserve("__nomos_retireChildRequiresBirth", retireChildRequiresBirth);
|
|
220
|
+
reserve("__nomos_retireChildNotReclaimed", retireChildNotReclaimed);
|
|
221
|
+
reserve("__nomos_retireChildAsAdminRequiresBirth", retireChildAsAdminRequiresBirth);
|
|
222
|
+
reserve("__nomos_retireChildAsAdminNotReclaimed", retireChildAsAdminNotReclaimed);
|
|
223
|
+
// ONE BIRTH LANE ALSO MEANS ONE PRIVATE SIGNING LANE. The client completes a `.births()` offer's signer
|
|
224
|
+
// enrollment, delegation and cert retry internally; therefore every birth-declaring parent law must carry
|
|
225
|
+
// the declarations that make those framework writes lawful. Previously only the newer `businessBirth`
|
|
226
|
+
// helper composed them, while an ordinary `.births()` directive forced the harness/app to understand
|
|
227
|
+
// `enrollDelegation` (or failed as an unverifiable black box). Auto-compose the exact same framework
|
|
228
|
+
// declarations for ALL births and mark them presentation-internal: they remain certified/routable law,
|
|
229
|
+
// but never appear in TS/Dart/OpenAPI tenant surfaces.
|
|
230
|
+
internalize("__nomos_SignerDelegation", SignerDelegation);
|
|
231
|
+
internalize("__nomos_enrollDelegation", enrollDelegation);
|
|
232
|
+
internalize("__nomos_signerDelegationByUser", signerDelegationByUser);
|
|
233
|
+
reserve("__nomos_parentAttestedBirth", internal({
|
|
234
|
+
...requireParentAttestedBirth,
|
|
235
|
+
ownerObject: GENESIS_WARRANT_ID,
|
|
236
|
+
ownerField: "principal",
|
|
237
|
+
certObject: BIRTH_ATTESTATION_ID,
|
|
238
|
+
certField: "certChain",
|
|
239
|
+
}));
|
|
104
240
|
return out as M;
|
|
105
241
|
}
|
package/src/births.ts
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
// wasm32-wasip1 artifact {kernel · projection · embedded
|
|
4
4
|
// QuickJS engine}. If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
5
5
|
|
|
6
|
-
import
|
|
6
|
+
import { z } from "zod";
|
|
7
7
|
import type { AggregateHandle } from "./aggregate.js";
|
|
8
8
|
import type { AggregateRef } from "./authoring.js";
|
|
9
|
-
import type { Field } from "./fields.js";
|
|
9
|
+
import type { Field, FieldValue } from "./fields.js";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* THE BIRTH EFFECT — birth is an EXPLICIT, DECLARED kernel primitive (G3, the unix model).
|
|
@@ -75,19 +75,29 @@ export interface BusinessBirthDecl<
|
|
|
75
75
|
readonly payloadSchema: z.ZodObject<z.ZodRawShape>;
|
|
76
76
|
/** A stable business key the caller already knows (an estate reference, account number, etc.). */
|
|
77
77
|
readonly key: (payload: P) => string;
|
|
78
|
-
/**
|
|
78
|
+
/** The declared business fact used by {@link key}; retained for generated clients to pre-address custody. */
|
|
79
|
+
readonly keyField: string;
|
|
80
|
+
/** Framework-derived initial authoring. Tenant code marks aggregate fields `.fromBirth()` instead. */
|
|
79
81
|
readonly plan: (root: AggregateRef<F>, payload: P) => unknown;
|
|
80
82
|
/** Optional business noun used only to make the durable address readable. Defaults to the aggregate id. */
|
|
81
83
|
readonly namespace?: string;
|
|
82
84
|
}
|
|
83
85
|
|
|
84
|
-
export
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
86
|
+
export type BirthFactKeys<F extends Record<string, Field>> = {
|
|
87
|
+
[K in keyof F]-?: F[K] extends { readonly isFromBirth: true } ? K : never;
|
|
88
|
+
}[keyof F] & string;
|
|
89
|
+
|
|
90
|
+
export type BirthStringFactKeys<F extends Record<string, Field>> = {
|
|
91
|
+
[K in BirthFactKeys<F>]: FieldValue<F[K]> extends string ? K : never;
|
|
92
|
+
}[BirthFactKeys<F>] & string;
|
|
93
|
+
|
|
94
|
+
export type BusinessBirthPayload<F extends Record<string, Field>> = {
|
|
95
|
+
readonly [K in BirthFactKeys<F>]: FieldValue<F[K]>;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export interface BusinessBirthSpec<F extends Record<string, Field>> {
|
|
99
|
+
/** One declared birth fact that gives this business life its stable address. */
|
|
100
|
+
readonly key: BirthStringFactKeys<F>;
|
|
91
101
|
readonly namespace?: string;
|
|
92
102
|
}
|
|
93
103
|
|
|
@@ -95,30 +105,51 @@ export interface BusinessBirthSpec<
|
|
|
95
105
|
* declares `birth(Aggregate, spec)` and leaves the recipe, identity, and recovery machinery to Nomos. */
|
|
96
106
|
export function birth(recipe: BirthRecipe): void;
|
|
97
107
|
/** Declare that one business aggregate is given its own durable life. */
|
|
98
|
-
export function birth<
|
|
108
|
+
export function birth<F extends Record<string, Field>>(
|
|
99
109
|
aggregate: AggregateHandle<string, F>,
|
|
100
|
-
spec: BusinessBirthSpec<
|
|
101
|
-
): BusinessBirthDecl<
|
|
102
|
-
export function birth<
|
|
110
|
+
spec: BusinessBirthSpec<F>,
|
|
111
|
+
): BusinessBirthDecl<BusinessBirthPayload<F>, F>;
|
|
112
|
+
export function birth<F extends Record<string, Field>>(
|
|
103
113
|
recipeOrAggregate: BirthRecipe | AggregateHandle<string, F>,
|
|
104
|
-
spec?: BusinessBirthSpec<
|
|
105
|
-
): void | BusinessBirthDecl<
|
|
114
|
+
spec?: BusinessBirthSpec<F>,
|
|
115
|
+
): void | BusinessBirthDecl<BusinessBirthPayload<F>, F> {
|
|
106
116
|
if (spec !== undefined) {
|
|
107
117
|
const aggregate = recipeOrAggregate as AggregateHandle<string, F>;
|
|
108
118
|
if (!aggregate || aggregate.__isAggregateHandle !== true) {
|
|
109
119
|
throw new Error("birth(Aggregate, spec): the first argument must be an aggregate handle");
|
|
110
120
|
}
|
|
111
|
-
|
|
112
|
-
|
|
121
|
+
const internalFields = new Set(aggregate.internalFields ?? []);
|
|
122
|
+
const facts = Object.entries(aggregate.fields as Record<string, Field>).filter(
|
|
123
|
+
([name, field]) => field.isFromBirth === true && !internalFields.has(name),
|
|
124
|
+
);
|
|
125
|
+
if (facts.length === 0) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`birth(${aggregate.id}): no birth facts are declared — mark every required initial business field with .fromBirth()`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
const keyField = (aggregate.fields as Record<string, Field>)[spec.key];
|
|
131
|
+
if (keyField === undefined || keyField.isFromBirth !== true) {
|
|
132
|
+
throw new Error(`birth(${aggregate.id}): key '${String(spec.key)}' must be a declared .fromBirth() field`);
|
|
133
|
+
}
|
|
134
|
+
if (keyField.kind !== "string" && keyField.kind !== "enum" && keyField.kind !== "ref") {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`birth(${aggregate.id}): key '${String(spec.key)}' must be a string-like business fact; got ${keyField.kind}`,
|
|
137
|
+
);
|
|
113
138
|
}
|
|
139
|
+
const payloadSchema = z.object(Object.fromEntries(facts.map(([name, field]) => [name, field.zod])));
|
|
140
|
+
type Payload = BusinessBirthPayload<F>;
|
|
114
141
|
return {
|
|
115
142
|
__isBusinessBirth: true,
|
|
116
143
|
aggregate,
|
|
117
|
-
payloadSchema
|
|
118
|
-
key: spec.key,
|
|
119
|
-
|
|
144
|
+
payloadSchema,
|
|
145
|
+
key: (payload: Payload) => String(payload[spec.key]),
|
|
146
|
+
keyField: spec.key,
|
|
147
|
+
plan: (root: AggregateRef<F>, payload: Payload) => {
|
|
148
|
+
const writable = root as unknown as { set(field: string, value: unknown): unknown };
|
|
149
|
+
for (const [name] of facts) writable.set(name, payload[name as keyof Payload]);
|
|
150
|
+
},
|
|
120
151
|
...(spec.namespace !== undefined ? { namespace: spec.namespace } : {}),
|
|
121
|
-
}
|
|
152
|
+
} as BusinessBirthDecl<Payload, F>;
|
|
122
153
|
}
|
|
123
154
|
footprint.push(recipeOrAggregate as BirthRecipe);
|
|
124
155
|
}
|
package/src/build_package.ts
CHANGED
|
@@ -69,18 +69,26 @@ import {
|
|
|
69
69
|
type TrustSourceDecl,
|
|
70
70
|
type TrustPolicyDecl,
|
|
71
71
|
} from "./attested.js";
|
|
72
|
-
import type
|
|
72
|
+
import { isRelationTypeDecl, type RelationTypeDecl } from "./relation_schema.js";
|
|
73
73
|
import { domainHash, emitManifestBytes } from "./manifest.js";
|
|
74
74
|
import { collectValueObjectDescriptors } from "./value_object.js";
|
|
75
75
|
import { emitUsd } from "./usd.js";
|
|
76
76
|
import { expandCapabilityExports } from "./capability_exports.js";
|
|
77
77
|
import { RelationTuple, relationTuplesByObject } from "./framework/relations.js";
|
|
78
|
+
import { signerDelegationByUser } from "./framework/signing_identity.js";
|
|
78
79
|
import { AuthProvider, setAuthProvider, authProvider } from "./framework/workspaces.js";
|
|
79
80
|
import { parentAttestedBirthMarker } from "./births.js";
|
|
80
81
|
import { discoverUnsanctionedEnsures, sanctioningEnsureDirective } from "./secondary_ensure.js";
|
|
81
82
|
import { assertIdOfLint } from "./idof_lint.js";
|
|
82
83
|
import { assertSurfacedResolvers } from "./surfaced_lint.js";
|
|
84
|
+
import {
|
|
85
|
+
applyConflictPoliciesToAggregates,
|
|
86
|
+
applyConflictPolicyResolvers,
|
|
87
|
+
conflictPoliciesOf,
|
|
88
|
+
} from "./conflicts.js";
|
|
83
89
|
import { expandBusinessBirthExports } from "./business_birth.js";
|
|
90
|
+
import { withOwnedCustodyExports } from "./framework/owned_custody.js";
|
|
91
|
+
import { withBusinessCorrectionExports } from "./framework/business_corrections.js";
|
|
84
92
|
import {
|
|
85
93
|
reclaimChildAsAdminRequiresRetired,
|
|
86
94
|
reclaimChildRequiresRetired,
|
|
@@ -125,7 +133,10 @@ export function contractsOf(mod: Mod): import("./contract.js").ContractHandle[]
|
|
|
125
133
|
|
|
126
134
|
/** Collect every exported `Directive` ({id, plan, aggregateId} duck-type). */
|
|
127
135
|
export function directivesOf(mod: Mod): Directive<any>[] {
|
|
128
|
-
|
|
136
|
+
// One wire directive id is one law declaration. Merge seams may retain both an old tenant re-export and
|
|
137
|
+
// Nomos's presentation-internal wrapper under different JS keys; later wins so the framework wrapper
|
|
138
|
+
// controls codegen without duplicating the hashed/routable law.
|
|
139
|
+
const byId = new Map<string, Directive<any>>();
|
|
129
140
|
for (const v of Object.values(mod)) {
|
|
130
141
|
if (
|
|
131
142
|
v &&
|
|
@@ -134,10 +145,10 @@ export function directivesOf(mod: Mod): Directive<any>[] {
|
|
|
134
145
|
typeof (v as { plan?: unknown }).plan === "function" &&
|
|
135
146
|
typeof (v as { aggregateId?: unknown }).aggregateId === "string"
|
|
136
147
|
) {
|
|
137
|
-
|
|
148
|
+
byId.set((v as { id: string }).id, v as Directive<any>);
|
|
138
149
|
}
|
|
139
150
|
}
|
|
140
|
-
return
|
|
151
|
+
return [...byId.values()];
|
|
141
152
|
}
|
|
142
153
|
|
|
143
154
|
/** The NAMED-KEYS spec for one engine domain (the serialization of `module(...)`). */
|
|
@@ -180,9 +191,13 @@ export interface DomainModuleSpec {
|
|
|
180
191
|
* (an empty optional key must be ABSENT or every domain's canonical manifest hash drifts).
|
|
181
192
|
*/
|
|
182
193
|
export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
194
|
+
const domain = spec.domain ?? spec.name;
|
|
195
|
+
const conflictPolicies = conflictPoliciesOf(spec.modules, domain);
|
|
183
196
|
let merged: Mod = {};
|
|
184
197
|
for (const m of spec.modules) merged = { ...merged, ...m };
|
|
185
198
|
merged = expandBusinessBirthExports(merged, spec.domain ?? spec.name);
|
|
199
|
+
merged = withOwnedCustodyExports(merged);
|
|
200
|
+
merged = withBusinessCorrectionExports(merged);
|
|
186
201
|
// `capability()` bundles expand into discoverable entries (the package plane's
|
|
187
202
|
// merge point) — the quartet validation below then passes without re-exports.
|
|
188
203
|
merged = expandCapabilityExports(merged);
|
|
@@ -207,7 +222,7 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
207
222
|
|
|
208
223
|
const byId = new Map<string, AggregateHandle>();
|
|
209
224
|
for (const a of [...aggregatesOf(merged), ...(spec.extraAggregates ?? [])]) byId.set(a.id, a);
|
|
210
|
-
const directives = directivesOf(merged);
|
|
225
|
+
const directives = applyConflictPolicyResolvers(directivesOf(merged), conflictPolicies);
|
|
211
226
|
// RELATIONS AUTO-ENSURE + AUTO-COMPOSE (co2 recovery DX): a domain that USES relation law — declares a
|
|
212
227
|
// relation schema, a `.requires(...)`-gated directive, or relate-family/signer directives ensuring
|
|
213
228
|
// RelationTuple — needs the framework RelationTuple aggregate IN the schema on TWO counts: (1) the deploy
|
|
@@ -232,9 +247,20 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
232
247
|
const declaresPrivateRead = [...byId.values()].some(
|
|
233
248
|
(a) => (a as { visibility?: unknown }).visibility !== undefined,
|
|
234
249
|
);
|
|
250
|
+
const declaresOwnedRead = [...byId.values()].some(
|
|
251
|
+
(a) => (a as { audience?: { kind?: unknown } }).audience?.kind === "owned",
|
|
252
|
+
);
|
|
235
253
|
if (declaresPrivateRead) {
|
|
236
|
-
if (!byId.has(AuthProvider.id))
|
|
237
|
-
|
|
254
|
+
if (!byId.has(AuthProvider.id)) {
|
|
255
|
+
byId.set(AuthProvider.id, declaresOwnedRead
|
|
256
|
+
? { ...AuthProvider, businessBirthInternal: true } as AggregateHandle
|
|
257
|
+
: AuthProvider);
|
|
258
|
+
}
|
|
259
|
+
if (!directives.some((d) => d.id === setAuthProvider.id)) {
|
|
260
|
+
directives.push(declaresOwnedRead
|
|
261
|
+
? { ...setAuthProvider, businessBirthInternal: true } as Directive<any>
|
|
262
|
+
: setAuthProvider);
|
|
263
|
+
}
|
|
238
264
|
}
|
|
239
265
|
const usesRelations =
|
|
240
266
|
declaresPrivateRead ||
|
|
@@ -242,7 +268,9 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
242
268
|
directives.some((d) => (d as { requiresRelationGate?: unknown }).requiresRelationGate !== undefined) ||
|
|
243
269
|
directives.some((d) => d.aggregateId === RelationTuple.id);
|
|
244
270
|
if (usesRelations && !byId.has(RelationTuple.id)) {
|
|
245
|
-
byId.set(RelationTuple.id,
|
|
271
|
+
byId.set(RelationTuple.id, declaresOwnedRead
|
|
272
|
+
? { ...RelationTuple, businessBirthInternal: true } as AggregateHandle
|
|
273
|
+
: RelationTuple);
|
|
246
274
|
}
|
|
247
275
|
// BIRTH-METER AUTO-ENSURE (metered birth, slice A — the same "discoverable without re-exports"
|
|
248
276
|
// courtesy as RelationTuple): a domain declaring ≥1 `.births()` directive gets the meter aggregates
|
|
@@ -292,7 +320,11 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
292
320
|
directives.push(dir);
|
|
293
321
|
autoDeclaredEnsures.push(ty);
|
|
294
322
|
}
|
|
295
|
-
const aggregates =
|
|
323
|
+
const aggregates = applyConflictPoliciesToAggregates(
|
|
324
|
+
[...byId.values()],
|
|
325
|
+
conflictPolicies,
|
|
326
|
+
domain,
|
|
327
|
+
);
|
|
296
328
|
const directiveIds = new Set(directives.map((d) => d.id));
|
|
297
329
|
for (const task of spec.impureCapabilities ?? []) {
|
|
298
330
|
for (const directive of [task.obligation, task.complete, task.fail, task.block, task.deadLetter]) {
|
|
@@ -306,7 +338,11 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
306
338
|
}
|
|
307
339
|
const readModels = spec.readModelAggregates ?? aggregates.map((a) => a.id);
|
|
308
340
|
|
|
309
|
-
|
|
341
|
+
// As with directives, one stable query id is one declaration. This also makes an internal framework
|
|
342
|
+
// wrapper win over an older public re-export discovered earlier in the module bag.
|
|
343
|
+
const queryById = new Map<string, QueryDecl>();
|
|
344
|
+
for (const query of spec.queries ?? []) queryById.set(query.id, query);
|
|
345
|
+
const queries = [...queryById.values()];
|
|
310
346
|
const counts = [...(spec.counts ?? [])];
|
|
311
347
|
const spatials = [...(spec.spatials ?? [])];
|
|
312
348
|
const deriveds = [...(spec.deriveds ?? [])];
|
|
@@ -316,7 +352,23 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
316
352
|
const mins = [...(spec.mins ?? [])];
|
|
317
353
|
const maxes = [...(spec.maxes ?? [])];
|
|
318
354
|
const workspaceTypes = [...(spec.workspaceTypes ?? [])];
|
|
319
|
-
|
|
355
|
+
// Invariants exported by framework auto-composition must enter the manifest at this same seam; callers of
|
|
356
|
+
// composeDomainModule (tests and package builders alike) should not need to rediscover/rethread them.
|
|
357
|
+
const workspaceInvariantById = new Map<string, WorkspaceInvariantDecl>();
|
|
358
|
+
for (const value of Object.values(merged)) {
|
|
359
|
+
if (
|
|
360
|
+
value !== null &&
|
|
361
|
+
typeof value === "object" &&
|
|
362
|
+
(value as { __isWorkspaceInvariant?: unknown }).__isWorkspaceInvariant === true
|
|
363
|
+
) {
|
|
364
|
+
const invariant = value as WorkspaceInvariantDecl;
|
|
365
|
+
workspaceInvariantById.set(invariant.id, invariant);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
for (const invariant of spec.workspaceInvariants ?? []) {
|
|
369
|
+
workspaceInvariantById.set(invariant.id, invariant);
|
|
370
|
+
}
|
|
371
|
+
const workspaceInvariants = [...workspaceInvariantById.values()];
|
|
320
372
|
if (declaresBirth) {
|
|
321
373
|
const haveInvariant = new Set(workspaceInvariants.map((w) => w.id));
|
|
322
374
|
for (const lifecycle of [
|
|
@@ -343,12 +395,30 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
343
395
|
if (declaresBirth) {
|
|
344
396
|
const haveQuery = new Set(queries.map((q) => q.id));
|
|
345
397
|
for (const q of BIRTH_FRAMEWORK_QUERIES) {
|
|
346
|
-
if (!haveQuery.has(q.id))
|
|
398
|
+
if (!haveQuery.has(q.id)) {
|
|
399
|
+
queries.push({ ...q, businessBirthInternal: true } as QueryDecl);
|
|
400
|
+
}
|
|
347
401
|
}
|
|
348
402
|
// The inheritance read (`.reads("authProvider")` on birthHome) routes like any declared query.
|
|
349
403
|
if (!haveQuery.has(authProvider.id)) queries.push(authProvider);
|
|
404
|
+
// The client's private birth ceremony checks the actor's delegation before creating it. Carry the
|
|
405
|
+
// structural read in certified routing law for every `.births()` domain, but keep it off tenant codegen.
|
|
406
|
+
if (!haveQuery.has(signerDelegationByUser.id)) {
|
|
407
|
+
const internalDelegationQuery: QueryDecl & { readonly businessBirthInternal: true } = {
|
|
408
|
+
...signerDelegationByUser,
|
|
409
|
+
businessBirthInternal: true,
|
|
410
|
+
};
|
|
411
|
+
queries.push(internalDelegationQuery);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
const relationTypeByKey = new Map<string, RelationTypeDecl>();
|
|
415
|
+
for (const value of Object.values(merged)) {
|
|
416
|
+
if (isRelationTypeDecl(value)) relationTypeByKey.set(`${value.global === true ? "global" : "local"}:${value.type}`, value);
|
|
417
|
+
}
|
|
418
|
+
for (const value of spec.relationTypes ?? []) {
|
|
419
|
+
relationTypeByKey.set(`${value.global === true ? "global" : "local"}:${value.type}`, value);
|
|
350
420
|
}
|
|
351
|
-
const relationTypes = [...(
|
|
421
|
+
const relationTypes = [...relationTypeByKey.values()];
|
|
352
422
|
// RELATION-USING LAW AUTO-COMPOSES ITS AUDIT READ (co2 recovery DX — same idiom as the meter
|
|
353
423
|
// queries above): a domain that declares relation law (a relation schema, a `.requires(...)` gated
|
|
354
424
|
// directive, or relate-family/signer directives ensuring RelationTuple) gets the framework's
|
|
@@ -363,10 +433,14 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
363
433
|
// Auth-provider auto-compose (sibling of the RelationTuple query carry): a private package gets the
|
|
364
434
|
// `authProvider` read so the workspace can surface its declared verifier.
|
|
365
435
|
if (declaresPrivateRead && !queries.some((q) => q.id === authProvider.id)) {
|
|
366
|
-
queries.push(
|
|
436
|
+
queries.push(declaresOwnedRead
|
|
437
|
+
? { ...authProvider, businessBirthInternal: true } as QueryDecl
|
|
438
|
+
: authProvider);
|
|
367
439
|
}
|
|
368
440
|
if (usesRelations && !queries.some((q) => q.id === relationTuplesByObject.id)) {
|
|
369
|
-
queries.push(
|
|
441
|
+
queries.push(declaresOwnedRead
|
|
442
|
+
? { ...relationTuplesByObject, businessBirthInternal: true } as QueryDecl
|
|
443
|
+
: relationTuplesByObject);
|
|
370
444
|
}
|
|
371
445
|
// FIRST-CLASS VALUE OBJECTS (value_object.ts): the deduped, sorted descriptors of every
|
|
372
446
|
// named VO the domain's aggregates carry (transitive nested VOs included) — the input to
|
|
@@ -492,10 +566,11 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
492
566
|
// closed is dissolved by construction. See `dsl/src/framework/signing_identity.ts::enrollSigner`.)
|
|
493
567
|
|
|
494
568
|
// THE SURFACED-RESOLVER LINT (surfaced_lint.ts — the fail-closed teeth): every
|
|
495
|
-
//
|
|
569
|
+
// Surfaced field must have EXACTLY ONE designated merge directive, whose plan
|
|
496
570
|
// provably set()s the field from a payload field. Returns the resolver bindings the typed
|
|
497
|
-
//
|
|
498
|
-
//
|
|
571
|
+
// chained conflict-reference codegen consumes AND the canonical manifest records as the
|
|
572
|
+
// hash-bearing conflictResolution contract. A Surfaced-free domain runs zero checks and is
|
|
573
|
+
// byte-identical; a resolver policy deliberately becomes part of business law.
|
|
499
574
|
const surfacedResolvers = assertSurfacedResolvers(
|
|
500
575
|
aggregates,
|
|
501
576
|
directives,
|
|
@@ -512,8 +587,9 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
512
587
|
// ROLE SUGAR codegen metadata — NOT law (not hashed): drives the `canEdit<Agg>`/`canView<Agg>`
|
|
513
588
|
// UI-hint helpers. Omit-when-empty (a role-free domain is byte-identical).
|
|
514
589
|
...(roles.length > 0 ? { roles } : {}),
|
|
515
|
-
// SURFACED RESOLVER BINDINGS —
|
|
516
|
-
//
|
|
590
|
+
// SURFACED RESOLVER BINDINGS — the aggregate/field/directive tuple is lowered into the
|
|
591
|
+
// hash-bearing manifest; payload-field tracing also drives generated typed references.
|
|
592
|
+
// Omit-when-empty because ordinary domains need no resolver metadata.
|
|
517
593
|
...(surfacedResolvers.length > 0 ? { surfacedResolvers } : {}),
|
|
518
594
|
...(queries.length > 0 ? { queries } : {}),
|
|
519
595
|
...(counts.length > 0 ? { counts } : {}),
|