@githolon/dsl 0.100.4 → 0.100.6
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 +6 -0
- package/dart/lib/src/test_harness.dart +146 -59
- package/dart/pubspec.yaml +1 -1
- package/dart/test/acquire_session_credentials_test.dart +1 -1
- package/dart/test/adv_realm_interference_test.dart +1 -1
- 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/dlq_session_surface_test.dart +1 -1
- package/dart/test/first_app_gauntlet_test.dart +106 -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/package.json +1 -1
- package/src/birth_meter_ensure.ts +162 -18
- package/src/build_package.ts +52 -7
- package/src/business_birth.ts +31 -9
- package/src/codegen_dart_client.ts +11 -7
- package/src/compile_package_main.ts +17 -8
- package/src/framework/birth_meter.ts +82 -0
- package/src/index.ts +5 -0
- package/src/shape_compat.ts +14 -3
|
@@ -32,11 +32,54 @@ import {
|
|
|
32
32
|
} from "./birth_meter.js";
|
|
33
33
|
import {
|
|
34
34
|
reclaimChild,
|
|
35
|
+
reclaimChildAsAdmin,
|
|
36
|
+
reclaimChildAsAdminRequiresRetired,
|
|
35
37
|
reclaimChildRequiresRetired,
|
|
36
38
|
retireChild,
|
|
39
|
+
retireChildAsAdmin,
|
|
40
|
+
retireChildAsAdminNotReclaimed,
|
|
41
|
+
retireChildAsAdminRequiresBirth,
|
|
37
42
|
retireChildRequiresBirth,
|
|
38
43
|
retireChildNotReclaimed,
|
|
39
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 { AuthProvider, authProvider, setAuthProvider } from "./framework/auth_provider_law.js";
|
|
63
|
+
import { refAs, workspaceInvariant } from "./framework/workspace_invariant.js";
|
|
64
|
+
|
|
65
|
+
/** Lightweight copy of the framework's founding-signer guard, kept here to avoid importing the whole
|
|
66
|
+
* governance module (whose unrelated captured-read laws would otherwise survive into every child lump). */
|
|
67
|
+
const bornFounderSignerWriteOnce = workspaceInvariant("FounderSignerWriteOnce")
|
|
68
|
+
.on("enrollSigner")
|
|
69
|
+
.reads((ctx) => [
|
|
70
|
+
refAs(
|
|
71
|
+
"tuple",
|
|
72
|
+
"RelationTuple",
|
|
73
|
+
"pre:" + relationTupleInstanceId(signerObject(String((ctx.intent ?? {}).principal ?? "")), "signer"),
|
|
74
|
+
),
|
|
75
|
+
])
|
|
76
|
+
.assert(({ tuple }, ctx) => {
|
|
77
|
+
const subjects = (tuple as { subjects?: Record<string, unknown> })?.subjects ?? {};
|
|
78
|
+
const want = "key:" + String((ctx?.intent ?? {}).keyHash ?? "");
|
|
79
|
+
return Object.keys(subjects).every((key) => key === want)
|
|
80
|
+
? { accept: true }
|
|
81
|
+
: { reject: "founding-signer-already-enrolled" };
|
|
82
|
+
});
|
|
40
83
|
|
|
41
84
|
/**
|
|
42
85
|
* The sanctioning declaration — an `ensures(NomosBirthRecord)` directive whose plan emits ZERO ops.
|
|
@@ -65,25 +108,126 @@ export function withBirthMeterExports<M extends Record<string, unknown>>(mod: M)
|
|
|
65
108
|
(v as { birthsChild?: boolean }).birthsChild === true &&
|
|
66
109
|
typeof (v as Directive<unknown>).plan === "function",
|
|
67
110
|
);
|
|
68
|
-
if
|
|
111
|
+
// A law that accepts the kernel-injected birth attestation is itself a possible CHILD even if it never
|
|
112
|
+
// births anything. Such a law must also accept the framework-owned founding signer step, otherwise the
|
|
113
|
+
// child can replay but can never sign its first write or birth a descendant without tenant recipe work.
|
|
114
|
+
const acceptsBirth = Object.values(mod).some(
|
|
115
|
+
(v) =>
|
|
116
|
+
v !== null &&
|
|
117
|
+
typeof v === "object" &&
|
|
118
|
+
(v as { id?: unknown }).id === "seedBirthAttestation" &&
|
|
119
|
+
typeof (v as Directive<unknown>).plan === "function",
|
|
120
|
+
);
|
|
121
|
+
if (!declaresBirth && !acceptsBirth) return mod;
|
|
69
122
|
const out: Record<string, unknown> = { ...mod };
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
123
|
+
const internal = <T extends object>(v: T): T => ({ ...v, businessBirthInternal: true }) as T;
|
|
124
|
+
const hasOwn = (key: string): boolean => Object.prototype.hasOwnProperty.call(out, key);
|
|
125
|
+
const isGeneratedInternal = (value: unknown): boolean =>
|
|
126
|
+
value !== null &&
|
|
127
|
+
typeof value === "object" &&
|
|
128
|
+
(value as { businessBirthInternal?: unknown }).businessBirthInternal === true;
|
|
129
|
+
const reservedNameError = (key: string): Error => new Error(
|
|
130
|
+
`Nomos internal export name '${key}' is reserved inside a birth-capable law, but the tenant module ` +
|
|
131
|
+
"already exports a different concept there. Nomos will not silently overwrite or reroute it; rename " +
|
|
132
|
+
"the tenant export.",
|
|
75
133
|
);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
134
|
+
/** Reserve a framework-only JS export slot as well as its stable law id. The keys are deliberately obscure,
|
|
135
|
+
* but obscurity is collision avoidance, not collision handling: an occupied slot is always a loud refusal. */
|
|
136
|
+
const reserve = <T>(key: string, canonical: T): void => {
|
|
137
|
+
if (!hasOwn(key)) {
|
|
138
|
+
out[key] = canonical;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const existing = out[key];
|
|
142
|
+
if (existing !== canonical && !isGeneratedInternal(existing)) throw reservedNameError(key);
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Framework ownership follows the stable law id, not the tenant's export key. Older laws commonly
|
|
146
|
+
* re-exported `enrollSigner`/`seedGenesis` under their public names; merely adding a second hidden
|
|
147
|
+
* `__nomos_*` binding leaves BOTH declarations discoverable and lets codegen expose the old one. Mark
|
|
148
|
+
* every matching binding internal, and add the canonical declaration only when the law carried none.
|
|
149
|
+
*/
|
|
150
|
+
const internalize = <T extends { readonly id: string }>(key: string, canonical: T): void => {
|
|
151
|
+
if (hasOwn(key)) {
|
|
152
|
+
const existing = out[key];
|
|
153
|
+
const sameGeneratedId =
|
|
154
|
+
isGeneratedInternal(existing) &&
|
|
155
|
+
(existing as { id?: unknown }).id === canonical.id;
|
|
156
|
+
if (existing !== canonical && !sameGeneratedId) throw reservedNameError(key);
|
|
157
|
+
}
|
|
158
|
+
let found = false;
|
|
159
|
+
for (const [exportName, value] of Object.entries(out)) {
|
|
160
|
+
if (
|
|
161
|
+
value !== null &&
|
|
162
|
+
typeof value === "object" &&
|
|
163
|
+
(value as { id?: unknown }).id === canonical.id
|
|
164
|
+
) {
|
|
165
|
+
const isCanonicalSingleton = value === canonical;
|
|
166
|
+
const isNomosGeneratedBinding =
|
|
167
|
+
(value as { businessBirthInternal?: unknown }).businessBirthInternal === true;
|
|
168
|
+
if (!isCanonicalSingleton && !isNomosGeneratedBinding) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`Nomos framework id '${canonical.id}' is reserved inside a birth-capable law, but export ` +
|
|
171
|
+
`'${exportName}' declares a different tenant-owned concept with that id. Nomos will not ` +
|
|
172
|
+
`silently reinterpret it as framework machinery; rename the tenant concept.`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
out[exportName] = internal(value);
|
|
176
|
+
found = true;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (!found) out[key] = internal(canonical);
|
|
180
|
+
};
|
|
181
|
+
// THE BORN-WORKSPACE CORE: the kernel appends enrollSigner -> attestation -> warrant to every valid birth.
|
|
182
|
+
// Carry the law needed to judge that internal step in every possible child, plus the write-once guard that
|
|
183
|
+
// keeps the founding lane a genesis primitive. These declarations are certified but never tenant-facing.
|
|
184
|
+
internalize("__nomos_RelationTuple", RelationTuple);
|
|
185
|
+
reserve("__nomos_identityRelations", identityRelations);
|
|
186
|
+
internalize("__nomos_enrollSigner", enrollSigner);
|
|
187
|
+
internalize("__nomos_relationTuplesByObject", relationTuplesByObject);
|
|
188
|
+
internalize("__nomos_GenesisWarrant", GenesisWarrant);
|
|
189
|
+
internalize("__nomos_seedGenesis", seedGenesis);
|
|
190
|
+
internalize("__nomos_BirthAttestation", BirthAttestation);
|
|
191
|
+
internalize("__nomos_seedBirthAttestation", seedBirthAttestation);
|
|
192
|
+
internalize("__nomos_birthAttestation", birthAttestation);
|
|
193
|
+
internalize("__nomos_AuthProvider", AuthProvider);
|
|
194
|
+
internalize("__nomos_setAuthProvider", setAuthProvider);
|
|
195
|
+
internalize("__nomos_authProvider", authProvider);
|
|
196
|
+
reserve("__nomos_founderSignerWriteOnce", bornFounderSignerWriteOnce);
|
|
197
|
+
if (!declaresBirth) return out as M;
|
|
198
|
+
// Birth lifecycle is Nomos machinery for every birth-capable law. It remains certified/routable law,
|
|
199
|
+
// but no generated tenant API gets a second maintenance lane beside business `birth(...)`.
|
|
200
|
+
internalize("__nomos_nomosBirthRecordEnsure", nomosBirthRecordEnsure);
|
|
201
|
+
internalize("__nomos_NomosBirthRecord", NomosBirthRecord);
|
|
202
|
+
internalize("__nomos_NomosChildLifecycle", NomosChildLifecycle);
|
|
203
|
+
internalize("__nomos_BirthPolicy", BirthPolicy);
|
|
204
|
+
internalize("__nomos_BirthGrant", BirthGrant);
|
|
205
|
+
internalize("__nomos_retireChild", retireChild);
|
|
206
|
+
internalize("__nomos_reclaimChild", reclaimChild);
|
|
207
|
+
internalize("__nomos_retireChildAsAdmin", retireChildAsAdmin);
|
|
208
|
+
internalize("__nomos_reclaimChildAsAdmin", reclaimChildAsAdmin);
|
|
209
|
+
reserve("__nomos_reclaimChildRequiresRetired", reclaimChildRequiresRetired);
|
|
210
|
+
reserve("__nomos_reclaimChildAsAdminRequiresRetired", reclaimChildAsAdminRequiresRetired);
|
|
211
|
+
reserve("__nomos_retireChildRequiresBirth", retireChildRequiresBirth);
|
|
212
|
+
reserve("__nomos_retireChildNotReclaimed", retireChildNotReclaimed);
|
|
213
|
+
reserve("__nomos_retireChildAsAdminRequiresBirth", retireChildAsAdminRequiresBirth);
|
|
214
|
+
reserve("__nomos_retireChildAsAdminNotReclaimed", retireChildAsAdminNotReclaimed);
|
|
215
|
+
// ONE BIRTH LANE ALSO MEANS ONE PRIVATE SIGNING LANE. The client completes a `.births()` offer's signer
|
|
216
|
+
// enrollment, delegation and cert retry internally; therefore every birth-declaring parent law must carry
|
|
217
|
+
// the declarations that make those framework writes lawful. Previously only the newer `businessBirth`
|
|
218
|
+
// helper composed them, while an ordinary `.births()` directive forced the harness/app to understand
|
|
219
|
+
// `enrollDelegation` (or failed as an unverifiable black box). Auto-compose the exact same framework
|
|
220
|
+
// declarations for ALL births and mark them presentation-internal: they remain certified/routable law,
|
|
221
|
+
// but never appear in TS/Dart/OpenAPI tenant surfaces.
|
|
222
|
+
internalize("__nomos_SignerDelegation", SignerDelegation);
|
|
223
|
+
internalize("__nomos_enrollDelegation", enrollDelegation);
|
|
224
|
+
internalize("__nomos_signerDelegationByUser", signerDelegationByUser);
|
|
225
|
+
reserve("__nomos_parentAttestedBirth", internal({
|
|
226
|
+
...requireParentAttestedBirth,
|
|
227
|
+
ownerObject: GENESIS_WARRANT_ID,
|
|
228
|
+
ownerField: "principal",
|
|
229
|
+
certObject: BIRTH_ATTESTATION_ID,
|
|
230
|
+
certField: "certChain",
|
|
231
|
+
}));
|
|
88
232
|
return out as M;
|
|
89
233
|
}
|
package/src/build_package.ts
CHANGED
|
@@ -75,6 +75,7 @@ import { collectValueObjectDescriptors } from "./value_object.js";
|
|
|
75
75
|
import { emitUsd } from "./usd.js";
|
|
76
76
|
import { expandCapabilityExports } from "./capability_exports.js";
|
|
77
77
|
import { RelationTuple, relationTuplesByObject } from "./framework/relations.js";
|
|
78
|
+
import { 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";
|
|
@@ -82,7 +83,10 @@ import { assertIdOfLint } from "./idof_lint.js";
|
|
|
82
83
|
import { assertSurfacedResolvers } from "./surfaced_lint.js";
|
|
83
84
|
import { expandBusinessBirthExports } from "./business_birth.js";
|
|
84
85
|
import {
|
|
86
|
+
reclaimChildAsAdminRequiresRetired,
|
|
85
87
|
reclaimChildRequiresRetired,
|
|
88
|
+
retireChildAsAdminNotReclaimed,
|
|
89
|
+
retireChildAsAdminRequiresBirth,
|
|
86
90
|
retireChildNotReclaimed,
|
|
87
91
|
retireChildRequiresBirth,
|
|
88
92
|
} from "./framework/birth_meter.js";
|
|
@@ -122,7 +126,10 @@ export function contractsOf(mod: Mod): import("./contract.js").ContractHandle[]
|
|
|
122
126
|
|
|
123
127
|
/** Collect every exported `Directive` ({id, plan, aggregateId} duck-type). */
|
|
124
128
|
export function directivesOf(mod: Mod): Directive<any>[] {
|
|
125
|
-
|
|
129
|
+
// One wire directive id is one law declaration. Merge seams may retain both an old tenant re-export and
|
|
130
|
+
// Nomos's presentation-internal wrapper under different JS keys; later wins so the framework wrapper
|
|
131
|
+
// controls codegen without duplicating the hashed/routable law.
|
|
132
|
+
const byId = new Map<string, Directive<any>>();
|
|
126
133
|
for (const v of Object.values(mod)) {
|
|
127
134
|
if (
|
|
128
135
|
v &&
|
|
@@ -131,10 +138,10 @@ export function directivesOf(mod: Mod): Directive<any>[] {
|
|
|
131
138
|
typeof (v as { plan?: unknown }).plan === "function" &&
|
|
132
139
|
typeof (v as { aggregateId?: unknown }).aggregateId === "string"
|
|
133
140
|
) {
|
|
134
|
-
|
|
141
|
+
byId.set((v as { id: string }).id, v as Directive<any>);
|
|
135
142
|
}
|
|
136
143
|
}
|
|
137
|
-
return
|
|
144
|
+
return [...byId.values()];
|
|
138
145
|
}
|
|
139
146
|
|
|
140
147
|
/** The NAMED-KEYS spec for one engine domain (the serialization of `module(...)`). */
|
|
@@ -303,7 +310,11 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
303
310
|
}
|
|
304
311
|
const readModels = spec.readModelAggregates ?? aggregates.map((a) => a.id);
|
|
305
312
|
|
|
306
|
-
|
|
313
|
+
// As with directives, one stable query id is one declaration. This also makes an internal framework
|
|
314
|
+
// wrapper win over an older public re-export discovered earlier in the module bag.
|
|
315
|
+
const queryById = new Map<string, QueryDecl>();
|
|
316
|
+
for (const query of spec.queries ?? []) queryById.set(query.id, query);
|
|
317
|
+
const queries = [...queryById.values()];
|
|
307
318
|
const counts = [...(spec.counts ?? [])];
|
|
308
319
|
const spatials = [...(spec.spatials ?? [])];
|
|
309
320
|
const deriveds = [...(spec.deriveds ?? [])];
|
|
@@ -313,10 +324,33 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
313
324
|
const mins = [...(spec.mins ?? [])];
|
|
314
325
|
const maxes = [...(spec.maxes ?? [])];
|
|
315
326
|
const workspaceTypes = [...(spec.workspaceTypes ?? [])];
|
|
316
|
-
|
|
327
|
+
// Invariants exported by framework auto-composition must enter the manifest at this same seam; callers of
|
|
328
|
+
// composeDomainModule (tests and package builders alike) should not need to rediscover/rethread them.
|
|
329
|
+
const workspaceInvariantById = new Map<string, WorkspaceInvariantDecl>();
|
|
330
|
+
for (const value of Object.values(merged)) {
|
|
331
|
+
if (
|
|
332
|
+
value !== null &&
|
|
333
|
+
typeof value === "object" &&
|
|
334
|
+
(value as { __isWorkspaceInvariant?: unknown }).__isWorkspaceInvariant === true
|
|
335
|
+
) {
|
|
336
|
+
const invariant = value as WorkspaceInvariantDecl;
|
|
337
|
+
workspaceInvariantById.set(invariant.id, invariant);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
for (const invariant of spec.workspaceInvariants ?? []) {
|
|
341
|
+
workspaceInvariantById.set(invariant.id, invariant);
|
|
342
|
+
}
|
|
343
|
+
const workspaceInvariants = [...workspaceInvariantById.values()];
|
|
317
344
|
if (declaresBirth) {
|
|
318
345
|
const haveInvariant = new Set(workspaceInvariants.map((w) => w.id));
|
|
319
|
-
for (const lifecycle of [
|
|
346
|
+
for (const lifecycle of [
|
|
347
|
+
reclaimChildRequiresRetired,
|
|
348
|
+
reclaimChildAsAdminRequiresRetired,
|
|
349
|
+
retireChildNotReclaimed,
|
|
350
|
+
retireChildAsAdminNotReclaimed,
|
|
351
|
+
retireChildRequiresBirth,
|
|
352
|
+
retireChildAsAdminRequiresBirth,
|
|
353
|
+
]) {
|
|
320
354
|
if (!haveInvariant.has(lifecycle.id)) workspaceInvariants.push(lifecycle);
|
|
321
355
|
}
|
|
322
356
|
}
|
|
@@ -333,10 +367,21 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
333
367
|
if (declaresBirth) {
|
|
334
368
|
const haveQuery = new Set(queries.map((q) => q.id));
|
|
335
369
|
for (const q of BIRTH_FRAMEWORK_QUERIES) {
|
|
336
|
-
if (!haveQuery.has(q.id))
|
|
370
|
+
if (!haveQuery.has(q.id)) {
|
|
371
|
+
queries.push({ ...q, businessBirthInternal: true } as QueryDecl);
|
|
372
|
+
}
|
|
337
373
|
}
|
|
338
374
|
// The inheritance read (`.reads("authProvider")` on birthHome) routes like any declared query.
|
|
339
375
|
if (!haveQuery.has(authProvider.id)) queries.push(authProvider);
|
|
376
|
+
// The client's private birth ceremony checks the actor's delegation before creating it. Carry the
|
|
377
|
+
// structural read in certified routing law for every `.births()` domain, but keep it off tenant codegen.
|
|
378
|
+
if (!haveQuery.has(signerDelegationByUser.id)) {
|
|
379
|
+
const internalDelegationQuery: QueryDecl & { readonly businessBirthInternal: true } = {
|
|
380
|
+
...signerDelegationByUser,
|
|
381
|
+
businessBirthInternal: true,
|
|
382
|
+
};
|
|
383
|
+
queries.push(internalDelegationQuery);
|
|
384
|
+
}
|
|
340
385
|
}
|
|
341
386
|
const relationTypes = [...(spec.relationTypes ?? [])];
|
|
342
387
|
// RELATION-USING LAW AUTO-COMPOSES ITS AUDIT READ (co2 recovery DX — same idiom as the meter
|
package/src/business_birth.ts
CHANGED
|
@@ -229,7 +229,7 @@ function lowerOne(decl: BusinessBirthDecl, domain: string): Record<string, unkno
|
|
|
229
229
|
__nomos_birthAttestation: internalSurface(birthAttestation),
|
|
230
230
|
__nomos_RelationTuple: internalSurface(RelationTuple),
|
|
231
231
|
__nomos_relationTuplesByObject: internalSurface(relationTuplesByObject),
|
|
232
|
-
__nomos_identityRelations: identityRelations,
|
|
232
|
+
__nomos_identityRelations: internalSurface(identityRelations),
|
|
233
233
|
__nomos_enrollSigner: internalDirective(enrollSigner),
|
|
234
234
|
__nomos_enrollFirstDevice: internalDirective(enrollFirstDevice),
|
|
235
235
|
__nomos_SignerDelegation: internalSurface(SignerDelegation),
|
|
@@ -238,19 +238,19 @@ function lowerOne(decl: BusinessBirthDecl, domain: string): Record<string, unkno
|
|
|
238
238
|
__nomos_AuthProvider: internalSurface(AuthProvider),
|
|
239
239
|
__nomos_setAuthProvider: internalDirective(setAuthProvider),
|
|
240
240
|
__nomos_authProvider: internalSurface(authProvider),
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
241
|
+
// Obscure framework export keys minimize accidental tenant collisions. Stable law ids remain the real
|
|
242
|
+
// identity; the generic composition seam rejects a tenant-owned declaration with any of those ids.
|
|
243
|
+
__nomos_NomosBirthRecord: internalSurface(NomosBirthRecord),
|
|
244
|
+
__nomos_NomosChildLifecycle: internalSurface(NomosChildLifecycle),
|
|
245
|
+
__nomos_BirthPolicy: internalSurface(BirthPolicy),
|
|
246
|
+
__nomos_BirthGrant: internalSurface(BirthGrant),
|
|
247
|
+
__nomos_nomosBirthRecordEnsure: internalDirective(nomosBirthRecordEnsure),
|
|
248
248
|
__nomos_birthRecordsByPrincipal: internalSurface(birthRecordsByPrincipal),
|
|
249
249
|
__nomos_childLifecyclesByPrincipal: internalSurface(childLifecyclesByPrincipal),
|
|
250
250
|
__nomos_childLifecyclesByStatus: internalSurface(childLifecyclesByStatus),
|
|
251
251
|
__nomos_birthPolicy: internalSurface(birthPolicy),
|
|
252
252
|
__nomos_birthGrantByPrincipal: internalSurface(birthGrantByPrincipal),
|
|
253
|
-
__nomos_parentAttestedBirth: businessParentAttestedBirth,
|
|
253
|
+
__nomos_parentAttestedBirth: internalSurface(businessParentAttestedBirth),
|
|
254
254
|
};
|
|
255
255
|
}
|
|
256
256
|
|
|
@@ -268,6 +268,28 @@ export function expandBusinessBirthExports<M extends Record<string, unknown>>(
|
|
|
268
268
|
);
|
|
269
269
|
}
|
|
270
270
|
const lowered = lowerOne(declarations[0]!, domain);
|
|
271
|
+
for (const [key, generated] of Object.entries(lowered)) {
|
|
272
|
+
if (!Object.prototype.hasOwnProperty.call(mod, key)) continue;
|
|
273
|
+
const existing = mod[key];
|
|
274
|
+
const sameGeneratedDeclaration =
|
|
275
|
+
existing !== null &&
|
|
276
|
+
typeof existing === "object" &&
|
|
277
|
+
generated !== null &&
|
|
278
|
+
typeof generated === "object" &&
|
|
279
|
+
(((existing as { businessBirthInternal?: unknown }).businessBirthInternal === true &&
|
|
280
|
+
(generated as { businessBirthInternal?: unknown }).businessBirthInternal === true &&
|
|
281
|
+
(existing as { id?: unknown }).id === (generated as { id?: unknown }).id) ||
|
|
282
|
+
((existing as { businessBirth?: unknown }).businessBirth !== undefined &&
|
|
283
|
+
(generated as { businessBirth?: unknown }).businessBirth !== undefined &&
|
|
284
|
+
(existing as { id?: unknown }).id === (generated as { id?: unknown }).id));
|
|
285
|
+
if (!sameGeneratedDeclaration) {
|
|
286
|
+
throw new Error(
|
|
287
|
+
`Nomos internal export name '${key}' is reserved by birth(${declarations[0]!.aggregate.id}), but ` +
|
|
288
|
+
"the tenant module already exports a different concept there. Nomos will not silently overwrite " +
|
|
289
|
+
"or reroute it; rename the tenant export.",
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
271
293
|
return { ...mod, ...lowered };
|
|
272
294
|
}
|
|
273
295
|
|
|
@@ -939,13 +939,17 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
939
939
|
` if (b is NomosSession) await b.assertCompatible(interface);`,
|
|
940
940
|
` }`,
|
|
941
941
|
``,
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
942
|
+
...(dirs.length > 0 || birthDirs.some((d) => (d as { businessBirth?: unknown }).businessBirth !== undefined)
|
|
943
|
+
? [
|
|
944
|
+
` /// THE ONE DISPATCH LANE every generated directive method routes through: lazy preflight, then`,
|
|
945
|
+
` /// the session's offer (the generated-internal use of the engine-level lane).`,
|
|
946
|
+
` Future<NomosOfferOutcome> _offer(String directiveId, Object? payload, {List<Map<String, Object?>>? attestations}) async {`,
|
|
947
|
+
` await _preflightOnce();`,
|
|
948
|
+
` return bridge.offerOutcome(domain: ${dartStr(domain)}, directiveId: directiveId, payload: payload, actor: actor, domainHash: domainHash, attestations: attestations);`,
|
|
949
|
+
` }`,
|
|
950
|
+
``,
|
|
951
|
+
]
|
|
952
|
+
: []),
|
|
949
953
|
...(encPresent ? dartEncClassMembers(encMap!) : []),
|
|
950
954
|
// THE OWNER CEREMONY (slice 2): grantAndWrap / revokeAndRotate / rotate — the operator-blind key-epoch
|
|
951
955
|
// handshake as one call. Bind `actor` (the owner) to author the fan-out. Only for a domain with a scope.
|
|
@@ -598,10 +598,13 @@ function collectDartSharedSymbols(
|
|
|
598
598
|
const frameworkAggById = new Map<string, AggregateHandle>();
|
|
599
599
|
const frameworkDirById = new Map<string, ModDirective>();
|
|
600
600
|
const frameworkQueryById = new Map<string, ModQuery>();
|
|
601
|
+
const isInternal = (value: unknown): boolean =>
|
|
602
|
+
!!value && typeof value === "object"
|
|
603
|
+
&& (value as { businessBirthInternal?: unknown }).businessBirthInternal === true;
|
|
601
604
|
for (const mod of modules) {
|
|
602
|
-
for (const a of mod.aggregates) if (isFrameworkAggregate(a)) frameworkAggById.set(a.id, a);
|
|
603
|
-
for (const d of mod.directives) if (isFrameworkDirective(d)) frameworkDirById.set(d.id, d);
|
|
604
|
-
for (const q of mod.queries ?? []) if (isFrameworkQuery(q)) frameworkQueryById.set(q.id, q);
|
|
605
|
+
for (const a of mod.aggregates) if (!isInternal(a) && isFrameworkAggregate(a)) frameworkAggById.set(a.id, a);
|
|
606
|
+
for (const d of mod.directives) if (!isInternal(d) && isFrameworkDirective(d)) frameworkDirById.set(d.id, d);
|
|
607
|
+
for (const q of mod.queries ?? []) if (!isInternal(q) && isFrameworkQuery(q)) frameworkQueryById.set(q.id, q);
|
|
605
608
|
}
|
|
606
609
|
const frameworkAggs = [...frameworkAggById.values()];
|
|
607
610
|
const frameworkDirs = [...frameworkDirById.values()];
|
|
@@ -1773,7 +1776,9 @@ async function main(): Promise<void> {
|
|
|
1773
1776
|
if (directivesOf(merged).some((dir) => (dir as { birthsChild?: boolean }).birthsChild === true)) {
|
|
1774
1777
|
const haveQuery = new Set(queries.map((q) => q.id));
|
|
1775
1778
|
for (const q of BIRTH_FRAMEWORK_QUERIES) {
|
|
1776
|
-
if (!haveQuery.has(q.id))
|
|
1779
|
+
if (!haveQuery.has(q.id)) {
|
|
1780
|
+
queries.push({ ...q, businessBirthInternal: true } as QueryDecl);
|
|
1781
|
+
}
|
|
1777
1782
|
}
|
|
1778
1783
|
}
|
|
1779
1784
|
// RELATION-USING LAW AUTO-COMPOSES ITS AUDIT READ (co2 recovery DX, same idiom as the birth-meter
|
|
@@ -2583,12 +2588,16 @@ async function main(): Promise<void> {
|
|
|
2583
2588
|
// the read overlay; the workspace's full spec is the runtime merge of installed packages' specs
|
|
2584
2589
|
// (currentLaw); the host ingests it and builds its router from it — no domain/directive name in host
|
|
2585
2590
|
// source. Self-describing for the whole web ecosystem (Swagger/codegen/Hono/agent tool-calling).
|
|
2586
|
-
const
|
|
2591
|
+
const publicOpenapi = mergeOpenApiFragments(
|
|
2587
2592
|
domainModules.map((m) => generateOpenApiFragment(m, { lawHash: domainHash })),
|
|
2588
2593
|
{ title: `${cfg.name} (nomos package)`, lawHash: domainHash },
|
|
2589
2594
|
);
|
|
2595
|
+
const runtimeOpenapi = mergeOpenApiFragments(
|
|
2596
|
+
domainModules.map((m) => generateOpenApiFragment(m, { lawHash: domainHash, includeFrameworkRoutes: true })),
|
|
2597
|
+
{ title: `${cfg.name} (nomos package)`, lawHash: domainHash },
|
|
2598
|
+
);
|
|
2590
2599
|
const openapiPath = path.join(outDir, `${cfg.name}.openapi.json`);
|
|
2591
|
-
writeFileSync(openapiPath, JSON.stringify(
|
|
2600
|
+
writeFileSync(openapiPath, JSON.stringify(publicOpenapi, null, 2) + "\n", "utf8");
|
|
2592
2601
|
|
|
2593
2602
|
// ── 4. the one-call deploy body (package only) ──
|
|
2594
2603
|
// SELF-DESCRIBING: `summary` states what the (otherwise hex-opaque) package
|
|
@@ -2620,7 +2629,7 @@ async function main(): Promise<void> {
|
|
|
2620
2629
|
interface: packageInterface,
|
|
2621
2630
|
// THE SELF-DESCRIBING API: this package's OpenAPI spec (law-derived). The worker stages it on the
|
|
2622
2631
|
// manifest overlay; a read serves the workspace's merged spec; the host builds its router from it.
|
|
2623
|
-
openapi,
|
|
2632
|
+
openapi: runtimeOpenapi,
|
|
2624
2633
|
// THE CLIENT/LAW COMPAT MANIFEST (§5): a DIAGNOSIS surface (law hash + interface hash + generator
|
|
2625
2634
|
// provenance) the connect-time classifier reads. Frontend-only — the package bytes above ignore it.
|
|
2626
2635
|
nomosCompat,
|
|
@@ -2895,7 +2904,7 @@ async function main(): Promise<void> {
|
|
|
2895
2904
|
}
|
|
2896
2905
|
console.log(` client ${rel(clientPath)} (typed TS client — payloads, read models, query accessors)`);
|
|
2897
2906
|
for (const note of recursiveNotes) console.log(` note ${note}`);
|
|
2898
|
-
console.log(` openapi ${rel(openapiPath)} (${Object.keys(
|
|
2907
|
+
console.log(` openapi ${rel(openapiPath)} (${Object.keys(publicOpenapi.paths).length} operation(s) — the public business API; private framework routes ride only in the deploy manifest)`);
|
|
2899
2908
|
if (proofPath !== undefined) {
|
|
2900
2909
|
console.log(` proof ${rel(proofPath)} (a runnable e2e GENERATED from your law — run: githolon proof)`);
|
|
2901
2910
|
} else {
|
|
@@ -86,6 +86,32 @@ export const retireChild = directive("retireChild")
|
|
|
86
86
|
];
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* The operator half of the same retirement action. This is deliberately a separate, framework-internal
|
|
91
|
+
* directive because the relation gate is structural: creator recovery and platform administration have
|
|
92
|
+
* different authority proofs even though the client presents one `retire` action and both write the same
|
|
93
|
+
* lifecycle fact. A broken creator key must never make a malformed child impossible to converge.
|
|
94
|
+
*/
|
|
95
|
+
export const retireChildAsAdmin = directive("retireChildAsAdmin")
|
|
96
|
+
.ensures(NomosChildLifecycle)
|
|
97
|
+
.payload(z.object({
|
|
98
|
+
child: z.string().min(1),
|
|
99
|
+
principal: z.string().min(1),
|
|
100
|
+
retiredBy: z.string().min(1),
|
|
101
|
+
retiredAt: z.number().int().nonnegative(),
|
|
102
|
+
}))
|
|
103
|
+
.plan((p) => {
|
|
104
|
+
const rec = instance(NomosChildLifecycle, childLifecycleId(p.child));
|
|
105
|
+
return [
|
|
106
|
+
set(rec, "child", p.child),
|
|
107
|
+
set(rec, "principal", p.principal),
|
|
108
|
+
set(rec, "status", "retired"),
|
|
109
|
+
set(rec, "retiredBy", p.retiredBy),
|
|
110
|
+
set(rec, "retiredAt", p.retiredAt),
|
|
111
|
+
];
|
|
112
|
+
})
|
|
113
|
+
.requires("admin");
|
|
114
|
+
|
|
89
115
|
export const reclaimChild = directive("reclaimChild")
|
|
90
116
|
.ensures(NomosChildLifecycle)
|
|
91
117
|
.payload(z.object({
|
|
@@ -102,6 +128,24 @@ export const reclaimChild = directive("reclaimChild")
|
|
|
102
128
|
];
|
|
103
129
|
});
|
|
104
130
|
|
|
131
|
+
/** Admin-gated recovery half of {@link reclaimChild}; hidden behind the client's one reclaim action. */
|
|
132
|
+
export const reclaimChildAsAdmin = directive("reclaimChildAsAdmin")
|
|
133
|
+
.ensures(NomosChildLifecycle)
|
|
134
|
+
.payload(z.object({
|
|
135
|
+
child: z.string().min(1),
|
|
136
|
+
reclaimedBy: z.string().min(1),
|
|
137
|
+
reclaimedAt: z.number().int().nonnegative(),
|
|
138
|
+
}))
|
|
139
|
+
.plan((p) => {
|
|
140
|
+
const rec = instance(NomosChildLifecycle, childLifecycleId(p.child));
|
|
141
|
+
return [
|
|
142
|
+
set(rec, "status", "reclaimed"),
|
|
143
|
+
set(rec, "reclaimedBy", p.reclaimedBy),
|
|
144
|
+
set(rec, "reclaimedAt", p.reclaimedAt),
|
|
145
|
+
];
|
|
146
|
+
})
|
|
147
|
+
.requires("admin");
|
|
148
|
+
|
|
105
149
|
/** Reclaim is two-phase; a child must first be explicitly withdrawn. */
|
|
106
150
|
export const reclaimChildRequiresRetired = workspaceInvariant("ReclaimChildRequiresRetired")
|
|
107
151
|
.on("reclaimChild")
|
|
@@ -115,6 +159,18 @@ export const reclaimChildRequiresRetired = workspaceInvariant("ReclaimChildRequi
|
|
|
115
159
|
: { reject: "reclaim-child-requires-retired" },
|
|
116
160
|
);
|
|
117
161
|
|
|
162
|
+
/** Operator recovery has the same two-phase law, without depending on the broken creator credential. */
|
|
163
|
+
export const reclaimChildAsAdminRequiresRetired = workspaceInvariant("ReclaimChildAsAdminRequiresRetired")
|
|
164
|
+
.on("reclaimChildAsAdmin")
|
|
165
|
+
.reads((ctx) => [
|
|
166
|
+
refAs("record", "NomosChildLifecycle", "pre:" + childLifecycleId(String(ctx.intent.child ?? ""))),
|
|
167
|
+
])
|
|
168
|
+
.assert(({ record }) =>
|
|
169
|
+
(record as { status?: string } | undefined)?.status === "retired"
|
|
170
|
+
? { accept: true }
|
|
171
|
+
: { reject: "reclaim-child-requires-retired" },
|
|
172
|
+
);
|
|
173
|
+
|
|
118
174
|
/** Reclaimed is permanent: retirement cannot move a child back out of the terminal state. */
|
|
119
175
|
export const retireChildNotReclaimed = workspaceInvariant("RetireChildNotReclaimed")
|
|
120
176
|
.on("retireChild")
|
|
@@ -127,6 +183,18 @@ export const retireChildNotReclaimed = workspaceInvariant("RetireChildNotReclaim
|
|
|
127
183
|
: { accept: true },
|
|
128
184
|
);
|
|
129
185
|
|
|
186
|
+
/** Reclaimed is terminal for operator recovery too. */
|
|
187
|
+
export const retireChildAsAdminNotReclaimed = workspaceInvariant("RetireChildAsAdminNotReclaimed")
|
|
188
|
+
.on("retireChildAsAdmin")
|
|
189
|
+
.reads((ctx) => [
|
|
190
|
+
refAs("record", "NomosChildLifecycle", "pre:" + childLifecycleId(String(ctx.intent.child ?? ""))),
|
|
191
|
+
])
|
|
192
|
+
.assert(({ record }) =>
|
|
193
|
+
(record as { status?: string } | undefined)?.status === "reclaimed"
|
|
194
|
+
? { reject: "reclaimed-child-is-terminal" }
|
|
195
|
+
: { accept: true },
|
|
196
|
+
);
|
|
197
|
+
|
|
130
198
|
/** A lifecycle cannot mint authority for arbitrary custody: retirement must name its exact birth witness. */
|
|
131
199
|
export const retireChildRequiresBirth = workspaceInvariant("RetireChildRequiresBirth")
|
|
132
200
|
.on("retireChild")
|
|
@@ -143,6 +211,20 @@ export const retireChildRequiresBirth = workspaceInvariant("RetireChildRequiresB
|
|
|
143
211
|
: { reject: "retire-child-requires-exact-birth-record" };
|
|
144
212
|
});
|
|
145
213
|
|
|
214
|
+
/** Admin recovery may act for the recorded creator, but still cannot invent or retarget custody. */
|
|
215
|
+
export const retireChildAsAdminRequiresBirth = workspaceInvariant("RetireChildAsAdminRequiresBirth")
|
|
216
|
+
.on("retireChildAsAdmin")
|
|
217
|
+
.reads((ctx) => [
|
|
218
|
+
refAs("birth", "NomosBirthRecord", "pre:" + birthRecordId(String(ctx.intent.child ?? ""))),
|
|
219
|
+
])
|
|
220
|
+
.assert(({ birth }, ctx) => {
|
|
221
|
+
const record = birth as { child?: string; principal?: string } | undefined;
|
|
222
|
+
const intent = ctx?.intent as { child?: string; principal?: string } | undefined;
|
|
223
|
+
return record?.child === intent?.child && record?.principal === intent?.principal
|
|
224
|
+
? { accept: true }
|
|
225
|
+
: { reject: "retire-child-requires-exact-birth-record" };
|
|
226
|
+
});
|
|
227
|
+
|
|
146
228
|
function sameLifecyclePrincipal(recorded: string | undefined, author: string | undefined): boolean {
|
|
147
229
|
const normalize = (s: string | undefined): string => s?.startsWith("user:") ? s.slice(5) : (s ?? "");
|
|
148
230
|
return normalize(recorded) !== "" && normalize(recorded) === normalize(author);
|
package/src/index.ts
CHANGED
|
@@ -477,10 +477,15 @@ export {
|
|
|
477
477
|
seedBirthPolicy,
|
|
478
478
|
grantBirthAllowance,
|
|
479
479
|
retireChild,
|
|
480
|
+
retireChildAsAdmin,
|
|
480
481
|
reclaimChild,
|
|
482
|
+
reclaimChildAsAdmin,
|
|
481
483
|
reclaimChildRequiresRetired,
|
|
484
|
+
reclaimChildAsAdminRequiresRetired,
|
|
482
485
|
retireChildRequiresBirth,
|
|
486
|
+
retireChildAsAdminRequiresBirth,
|
|
483
487
|
retireChildNotReclaimed,
|
|
488
|
+
retireChildAsAdminNotReclaimed,
|
|
484
489
|
} from "./framework/birth_meter.js";
|
|
485
490
|
// THE PLATFORM FOUNDING-ADMIN BOOTSTRAP (the platform analog of seedHomeCreator) — a tenant-importable
|
|
486
491
|
// factory that seats a tenant-named governance relation for a write-once, author-bound founder at genesis.
|