@githolon/dsl 0.108.4 → 0.108.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 +5 -0
- package/dart/lib/src/application_framework_calls.dart +21 -0
- package/dart/lib/src/dev_cloud.dart +67 -0
- package/dart/lib/src/nms_test_environment.dart +1 -1
- package/dart/lib/src/runner/node-entry.mjs +354 -104
- package/dart/lib/src/runner/offline-install.mjs +64 -8
- package/dart/lib/src/session_bound.dart +58 -30
- package/dart/lib/src/test_harness.dart +60 -98
- package/dart/lib/src/webview_bridge.dart +212 -32
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart/test/node_runner_transport_test.dart +5 -5
- package/dart/test/webview_bridge_test.dart +6 -0
- package/dart_types/CHANGELOG.md +4 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pb.dart +428 -2
- package/dart_types/pubspec.yaml +1 -1
- package/package.json +1 -1
- package/runtime/generated-application-host.js +293 -79
- package/src/application_topology.ts +120 -28
- package/src/births.ts +27 -0
- package/src/codegen_call_usda.ts +9 -4
- package/src/codegen_dart_application.ts +146 -37
- package/src/codegen_dart_client.ts +225 -160
- package/src/codegen_dart_framework_calls.ts +89 -4
- package/src/codegen_ts.ts +135 -60
- package/src/compile_package_main.ts +3544 -1460
- package/src/directive.ts +320 -118
- package/src/fields.ts +2 -3
- package/src/framework/application_home.ts +3 -0
- package/src/framework/auth_provider_law.ts +20 -28
- package/src/framework/genesis.ts +32 -1
- package/src/framework/home.ts +45 -8
- package/src/framework/signing_identity.ts +78 -0
- package/src/framework/workspaces.ts +9 -1
- package/src/framework_call_dart_enums.ts +33 -0
- package/src/framework_call_definitions.ts +11 -2
- package/src/index.ts +12 -0
- package/src/intent.ts +525 -194
- package/src/law_graph.ts +32 -4
- package/src/law_graph_usda.ts +3 -0
- package/src/package_config.ts +16 -1
- package/src/read.ts +33 -11
- package/src/wire.ts +1 -0
- package/src/wire_encode.ts +3 -3
package/src/intent.ts
CHANGED
|
@@ -40,6 +40,9 @@ import {
|
|
|
40
40
|
|
|
41
41
|
const INTENT_BRAND = "__nomos_business_intent_v1" as const;
|
|
42
42
|
const LANGUAGE_BRAND = "__nomos_application_language_v1" as const;
|
|
43
|
+
const CHILD_AUTHENTICATION_RECOVERY = Symbol(
|
|
44
|
+
"nomos.child-authentication-recovery"
|
|
45
|
+
);
|
|
43
46
|
|
|
44
47
|
export interface IntentStep<I = unknown, P = unknown> {
|
|
45
48
|
readonly directive: Directive<P>;
|
|
@@ -77,9 +80,15 @@ function payloadTrace(path: readonly string[] = []): PayloadTrace {
|
|
|
77
80
|
get(_target, property) {
|
|
78
81
|
if (property === PAYLOAD_TRACE_PATH) return path;
|
|
79
82
|
if (property === "then") return undefined;
|
|
80
|
-
if (
|
|
83
|
+
if (
|
|
84
|
+
property === Symbol.toPrimitive ||
|
|
85
|
+
property === "toString" ||
|
|
86
|
+
property === "valueOf"
|
|
87
|
+
) {
|
|
81
88
|
return () => {
|
|
82
|
-
throw new Error(
|
|
89
|
+
throw new Error(
|
|
90
|
+
"computed application-language payload field is not a direct path"
|
|
91
|
+
);
|
|
83
92
|
};
|
|
84
93
|
}
|
|
85
94
|
return payloadTrace([...path, String(property)]);
|
|
@@ -96,7 +105,10 @@ function tracedPath(value: unknown): readonly string[] | undefined {
|
|
|
96
105
|
function valueAtPath(value: unknown, path: readonly string[]): unknown {
|
|
97
106
|
let current = value;
|
|
98
107
|
for (const part of path) {
|
|
99
|
-
if (
|
|
108
|
+
if (
|
|
109
|
+
current === null ||
|
|
110
|
+
(typeof current !== "object" && typeof current !== "function")
|
|
111
|
+
) {
|
|
100
112
|
return undefined;
|
|
101
113
|
}
|
|
102
114
|
current = (current as Record<string, unknown>)[part];
|
|
@@ -106,14 +118,14 @@ function valueAtPath(value: unknown, path: readonly string[]): unknown {
|
|
|
106
118
|
|
|
107
119
|
function remapPayloadPath(
|
|
108
120
|
mapped: unknown,
|
|
109
|
-
privatePath: readonly string[]
|
|
121
|
+
privatePath: readonly string[]
|
|
110
122
|
): readonly string[] | undefined {
|
|
111
123
|
return tracedPath(valueAtPath(mapped, privatePath));
|
|
112
124
|
}
|
|
113
125
|
|
|
114
126
|
function remapStepPayloadPath(
|
|
115
127
|
step: IntentStep<any, any>,
|
|
116
|
-
privatePath: readonly string[]
|
|
128
|
+
privatePath: readonly string[]
|
|
117
129
|
): readonly string[] | undefined {
|
|
118
130
|
try {
|
|
119
131
|
return remapPayloadPath(step.payload(payloadTrace()), privatePath);
|
|
@@ -124,14 +136,17 @@ function remapStepPayloadPath(
|
|
|
124
136
|
|
|
125
137
|
function remapReadPart(
|
|
126
138
|
part: InvariantReadIdPart,
|
|
127
|
-
mapped: unknown
|
|
139
|
+
mapped: unknown
|
|
128
140
|
): InvariantReadIdPart | undefined {
|
|
129
141
|
if (part.source !== "payload") return part;
|
|
130
142
|
const path = remapPayloadPath(mapped, part.path);
|
|
131
143
|
return path === undefined ? undefined : { ...part, path };
|
|
132
144
|
}
|
|
133
145
|
|
|
134
|
-
function remapReadId(
|
|
146
|
+
function remapReadId(
|
|
147
|
+
id: InvariantReadId,
|
|
148
|
+
mapped: unknown
|
|
149
|
+
): InvariantReadId | undefined {
|
|
135
150
|
if (id.source === "literal") return id;
|
|
136
151
|
if (id.source === "payload") {
|
|
137
152
|
const path = remapPayloadPath(mapped, id.path);
|
|
@@ -150,12 +165,14 @@ function remapReadId(id: InvariantReadId, mapped: unknown): InvariantReadId | un
|
|
|
150
165
|
* Returns undefined unless every payload dependency is structurally proven. */
|
|
151
166
|
function remapNativeReads(
|
|
152
167
|
reads: readonly InvariantReadBinding[],
|
|
153
|
-
step: IntentStep<any, any
|
|
168
|
+
step: IntentStep<any, any>
|
|
154
169
|
): readonly InvariantReadBinding[] | undefined {
|
|
155
|
-
const needsPayload = reads.some(
|
|
156
|
-
read
|
|
157
|
-
|
|
158
|
-
|
|
170
|
+
const needsPayload = reads.some(
|
|
171
|
+
(read) =>
|
|
172
|
+
read.eachPayloadPath !== undefined ||
|
|
173
|
+
read.id.source === "payload" ||
|
|
174
|
+
(read.id.source === "template" &&
|
|
175
|
+
read.id.parts.some((part) => part.source === "payload"))
|
|
159
176
|
);
|
|
160
177
|
if (!needsPayload) return reads;
|
|
161
178
|
|
|
@@ -168,10 +185,12 @@ function remapNativeReads(
|
|
|
168
185
|
|
|
169
186
|
const out: InvariantReadBinding[] = [];
|
|
170
187
|
for (const read of reads) {
|
|
171
|
-
const eachPayloadPath =
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
188
|
+
const eachPayloadPath =
|
|
189
|
+
read.eachPayloadPath === undefined
|
|
190
|
+
? undefined
|
|
191
|
+
: remapPayloadPath(mapped, read.eachPayloadPath);
|
|
192
|
+
if (read.eachPayloadPath !== undefined && eachPayloadPath === undefined)
|
|
193
|
+
return undefined;
|
|
175
194
|
|
|
176
195
|
const id = remapReadId(read.id, mapped);
|
|
177
196
|
if (id === undefined) return undefined;
|
|
@@ -186,16 +205,81 @@ function remapNativeReads(
|
|
|
186
205
|
|
|
187
206
|
export interface BusinessIntent<I = unknown> extends Directive<I> {
|
|
188
207
|
readonly [INTENT_BRAND]: true;
|
|
189
|
-
readonly [BUSINESS_BIRTH_POLICY_BINDING]?: (
|
|
208
|
+
readonly [BUSINESS_BIRTH_POLICY_BINDING]?: (
|
|
209
|
+
domains: readonly string[]
|
|
210
|
+
) => void;
|
|
190
211
|
readonly steps: readonly IntentStep<I, any>[];
|
|
191
212
|
readonly workspaceEffects: readonly WorkspaceEffectStep<I, any>[];
|
|
192
213
|
/** Law-selected proof for this exact public offer. Omitted means the ordinary session proof
|
|
193
214
|
* posture. Tenant callers never choose this at invocation time. */
|
|
194
215
|
readonly offerAuthenticationProof?: "enrolledDevice";
|
|
216
|
+
/** Compiler-visible application lifecycle declaration. When opening a child of this aggregate
|
|
217
|
+
* type proves that its verifier/signer posture is stale, the generated parent application may
|
|
218
|
+
* invoke this exact nominal action on the already-authenticated parent. The action itself remains
|
|
219
|
+
* ordinary tenant law and the child independently authorizes the resulting workspace effect. */
|
|
220
|
+
readonly [CHILD_AUTHENTICATION_RECOVERY]?: string;
|
|
195
221
|
/** The complete public business boundary. Runtime lowering still uses Directive.payloadSchema privately. */
|
|
196
222
|
readonly contract: IntentContract<I>;
|
|
197
223
|
}
|
|
198
224
|
|
|
225
|
+
/** Select the already-enrolled device proof for a finished one-effect intent.
|
|
226
|
+
*
|
|
227
|
+
* `intent(...).performs(...)` deliberately returns the finished nominal action
|
|
228
|
+
* so the effect's complete framework metadata is preserved. This companion is
|
|
229
|
+
* the finished-action equivalent of `IntentBuilder.authenticatesWithEnrolledDevice()`;
|
|
230
|
+
* the compiler records the same exact per-intent proof requirement either way.
|
|
231
|
+
*/
|
|
232
|
+
export function authenticatesWithEnrolledDevice<I>(
|
|
233
|
+
action: BusinessIntent<I>
|
|
234
|
+
): BusinessIntent<I> {
|
|
235
|
+
return Object.freeze({
|
|
236
|
+
...action,
|
|
237
|
+
offerAuthenticationProof: "enrolledDevice" as const,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Mark one parent-law action as the typed recovery operation for children rooted at [child].
|
|
242
|
+
*
|
|
243
|
+
* This does not grant authority or create a generic host callback. It only lets generated application
|
|
244
|
+
* code connect a failed child-open posture to an exact, nominal parent action which already has its own
|
|
245
|
+
* enrolled-device proof and emits one law-authored effect to that child.
|
|
246
|
+
*/
|
|
247
|
+
export function repairsChildAuthenticationFor<I>(
|
|
248
|
+
child: AggregateHandle,
|
|
249
|
+
action: BusinessIntent<I>
|
|
250
|
+
): BusinessIntent<I> {
|
|
251
|
+
if (action.offerAuthenticationProof !== "enrolledDevice") {
|
|
252
|
+
throw new Error(
|
|
253
|
+
`repairsChildAuthenticationFor(${child.id}): '${action.id}' must authenticate with the parent's enrolled device`
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
if (action.steps.length !== 0 || action.workspaceEffects.length === 0) {
|
|
257
|
+
throw new Error(
|
|
258
|
+
`repairsChildAuthenticationFor(${child.id}): '${action.id}' must emit at least one child effect and make no parent-local write`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
if (
|
|
262
|
+
action.workspaceEffects.some(
|
|
263
|
+
(effect) => effect.destination.kind !== "workspace"
|
|
264
|
+
)
|
|
265
|
+
) {
|
|
266
|
+
throw new Error(
|
|
267
|
+
`repairsChildAuthenticationFor(${child.id}): '${action.id}' must address the exact child workspace from its typed input`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
return Object.freeze({
|
|
271
|
+
...action,
|
|
272
|
+
[CHILD_AUTHENTICATION_RECOVERY]: child.id,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Internal compiler projection of {@link repairsChildAuthenticationFor}. */
|
|
277
|
+
export function childAuthenticationRecoveryAggregateId(
|
|
278
|
+
action: BusinessIntent<unknown>
|
|
279
|
+
): string | undefined {
|
|
280
|
+
return action[CHILD_AUTHENTICATION_RECOVERY];
|
|
281
|
+
}
|
|
282
|
+
|
|
199
283
|
/** The public promise of one business action. Outputs are inferred from its composed consequences unless
|
|
200
284
|
* the author names them explicitly, in which case the compiler verifies the promise exactly. */
|
|
201
285
|
export interface IntentContract<I = unknown> {
|
|
@@ -231,43 +315,51 @@ const stable = (value: unknown): string => {
|
|
|
231
315
|
.join(",")}}`;
|
|
232
316
|
};
|
|
233
317
|
|
|
234
|
-
function oneStaticRelationGate<I>(
|
|
318
|
+
function oneStaticRelationGate<I>(
|
|
319
|
+
steps: readonly IntentStep<I, any>[]
|
|
320
|
+
): Directive["requiresRelationGate"] {
|
|
235
321
|
const gates = steps
|
|
236
322
|
.map((step) => step.directive.requiresRelationGate)
|
|
237
|
-
.filter(
|
|
323
|
+
.filter(
|
|
324
|
+
(gate): gate is NonNullable<Directive["requiresRelationGate"]> =>
|
|
325
|
+
gate !== undefined
|
|
326
|
+
);
|
|
238
327
|
if (gates.length === 0) return undefined;
|
|
239
328
|
if (gates.some((gate) => gate.objectFrom !== undefined)) {
|
|
240
329
|
throw new Error(
|
|
241
|
-
"compound intent cannot yet remap a per-record access gate — bind that gate on the intent itself"
|
|
330
|
+
"compound intent cannot yet remap a per-record access gate — bind that gate on the intent itself"
|
|
242
331
|
);
|
|
243
332
|
}
|
|
244
333
|
const first = stable(gates[0]);
|
|
245
334
|
if (gates.some((gate) => stable(gate) !== first)) {
|
|
246
335
|
throw new Error(
|
|
247
|
-
"compound intent directives require different access gates — declare one jointly satisfiable intent gate"
|
|
336
|
+
"compound intent directives require different access gates — declare one jointly satisfiable intent gate"
|
|
248
337
|
);
|
|
249
338
|
}
|
|
250
339
|
return gates[0];
|
|
251
340
|
}
|
|
252
341
|
|
|
253
342
|
function unsupportedKernelGate(directive: Directive<any>): string | undefined {
|
|
254
|
-
if (directive.attestationAuthorizedGate !== undefined)
|
|
343
|
+
if (directive.attestationAuthorizedGate !== undefined)
|
|
344
|
+
return "attestation authorization";
|
|
255
345
|
if (directive.delegatedRoleGate !== undefined) return "delegated authority";
|
|
256
|
-
if (directive.firstDeviceEnrollGate !== undefined)
|
|
346
|
+
if (directive.firstDeviceEnrollGate !== undefined)
|
|
347
|
+
return "first-device enrollment";
|
|
257
348
|
if (directive.certifiesEraGate !== undefined) return "era certification";
|
|
258
|
-
if ((directive.businessRecordInputs?.length ?? 0) > 0)
|
|
349
|
+
if ((directive.businessRecordInputs?.length ?? 0) > 0)
|
|
350
|
+
return "record-input binding";
|
|
259
351
|
return undefined;
|
|
260
352
|
}
|
|
261
353
|
|
|
262
354
|
function remapBusinessSourceTrust<I>(
|
|
263
355
|
step: IntentStep<I, any>,
|
|
264
|
-
trust: BusinessSourceTrust
|
|
356
|
+
trust: BusinessSourceTrust
|
|
265
357
|
): BusinessSourceTrust {
|
|
266
358
|
const field = (privatePath: string): string => {
|
|
267
359
|
const mapped = remapStepPayloadPath(step, privatePath.split("."));
|
|
268
360
|
if (mapped === undefined || mapped.length === 0) {
|
|
269
361
|
throw new Error(
|
|
270
|
-
`compound intent cannot remap source-trust field '${privatePath}' from '${step.directive.id}'
|
|
362
|
+
`compound intent cannot remap source-trust field '${privatePath}' from '${step.directive.id}'`
|
|
271
363
|
);
|
|
272
364
|
}
|
|
273
365
|
return mapped.join(".");
|
|
@@ -277,13 +369,29 @@ function remapBusinessSourceTrust<I>(
|
|
|
277
369
|
fromPayload: field(trust.fromPayload),
|
|
278
370
|
queryId: trust.queryId,
|
|
279
371
|
queryArgs: Object.fromEntries(
|
|
280
|
-
Object.entries(trust.queryArgs).map(([target, source]) => [
|
|
372
|
+
Object.entries(trust.queryArgs).map(([target, source]) => [
|
|
373
|
+
target,
|
|
374
|
+
field(source),
|
|
375
|
+
])
|
|
281
376
|
),
|
|
282
377
|
copy: Object.fromEntries(
|
|
283
|
-
Object.entries(trust.copy).map(([target, source]) => [
|
|
378
|
+
Object.entries(trust.copy).map(([target, source]) => [
|
|
379
|
+
target,
|
|
380
|
+
field(source),
|
|
381
|
+
])
|
|
284
382
|
),
|
|
285
383
|
genesisField: trust.genesisField,
|
|
286
384
|
keyHashesField: trust.keyHashesField,
|
|
385
|
+
...(trust.satisfiedByQueryId === undefined
|
|
386
|
+
? {}
|
|
387
|
+
: {
|
|
388
|
+
satisfiedByQueryId: trust.satisfiedByQueryId,
|
|
389
|
+
satisfiedByQueryArgs: Object.fromEntries(
|
|
390
|
+
Object.entries(trust.satisfiedByQueryArgs ?? {}).map(
|
|
391
|
+
([target, source]) => [target, field(source)]
|
|
392
|
+
)
|
|
393
|
+
),
|
|
394
|
+
}),
|
|
287
395
|
};
|
|
288
396
|
}
|
|
289
397
|
|
|
@@ -307,12 +415,13 @@ const dedupeBy = <T>(values: readonly T[], key: (value: T) => string): T[] => {
|
|
|
307
415
|
*/
|
|
308
416
|
function composePrimaryConsequenceOp(
|
|
309
417
|
directive: Directive<any>,
|
|
310
|
-
op: PlannedOp
|
|
418
|
+
op: PlannedOp
|
|
311
419
|
): PlannedOp {
|
|
312
420
|
if (op.kind === "strike") return op;
|
|
313
|
-
const targetsPrimary =
|
|
314
|
-
|
|
315
|
-
|
|
421
|
+
const targetsPrimary =
|
|
422
|
+
op.aggregateType !== undefined
|
|
423
|
+
? op.aggregateType === directive.aggregateId
|
|
424
|
+
: op.aggregateId === directive.aggregateId;
|
|
316
425
|
if (!targetsPrimary) return op;
|
|
317
426
|
return {
|
|
318
427
|
...op,
|
|
@@ -328,25 +437,45 @@ class IntentBuilder<I> {
|
|
|
328
437
|
private readonly id: string,
|
|
329
438
|
private readonly contractDeclaration: IntentContractDeclaration<I>,
|
|
330
439
|
private readonly steps: readonly IntentStep<I, any>[] = [],
|
|
331
|
-
private readonly workspaceEffects: readonly WorkspaceEffectStep<
|
|
440
|
+
private readonly workspaceEffects: readonly WorkspaceEffectStep<
|
|
441
|
+
I,
|
|
442
|
+
any
|
|
443
|
+
>[] = [],
|
|
332
444
|
private readonly intentReads: readonly (string | QueryDecl)[] = [],
|
|
333
445
|
private readonly acceptsWorkspaceEffects = false,
|
|
334
|
-
private readonly intentRelationGate?: NonNullable<
|
|
446
|
+
private readonly intentRelationGate?: NonNullable<
|
|
447
|
+
Directive["requiresRelationGate"]
|
|
448
|
+
>,
|
|
335
449
|
private readonly offerAuthenticationProof?: "enrolledDevice",
|
|
450
|
+
private readonly acceptsVerifiedBirthParentRecovery = false
|
|
336
451
|
) {}
|
|
337
452
|
|
|
338
453
|
/** Add one private directive consequence to this business intent. */
|
|
339
|
-
directs<P>(
|
|
454
|
+
directs<P>(
|
|
455
|
+
directive: Directive<P>,
|
|
456
|
+
payload: (facts: I) => P
|
|
457
|
+
): IntentBuilder<I> {
|
|
340
458
|
if (isBusinessIntent(directive)) {
|
|
341
|
-
throw new Error(
|
|
459
|
+
throw new Error(
|
|
460
|
+
`intent('${this.id}') cannot direct another intent — compose its private directives instead`
|
|
461
|
+
);
|
|
342
462
|
}
|
|
343
463
|
if (directive.birthsChild === true) {
|
|
344
|
-
throw new Error(
|
|
464
|
+
throw new Error(
|
|
465
|
+
`intent('${this.id}') composes a declared birth with .births(...), never a raw birth directive`
|
|
466
|
+
);
|
|
345
467
|
}
|
|
346
|
-
return new IntentBuilder(
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
468
|
+
return new IntentBuilder(
|
|
469
|
+
this.id,
|
|
470
|
+
this.contractDeclaration,
|
|
471
|
+
[...this.steps, { directive, payload }],
|
|
472
|
+
this.workspaceEffects,
|
|
473
|
+
this.intentReads,
|
|
474
|
+
this.acceptsWorkspaceEffects,
|
|
475
|
+
this.intentRelationGate,
|
|
476
|
+
this.offerAuthenticationProof,
|
|
477
|
+
this.acceptsVerifiedBirthParentRecovery
|
|
478
|
+
);
|
|
350
479
|
}
|
|
351
480
|
|
|
352
481
|
/** Author one exact nominal call into another workspace. The caller chooses only the target
|
|
@@ -354,21 +483,37 @@ class IntentBuilder<I> {
|
|
|
354
483
|
* the compiler bind its stable USDA identity; no operation string or host-selected mapping exists. */
|
|
355
484
|
directsWorkspace<P>(
|
|
356
485
|
target: Directive<P>,
|
|
357
|
-
effect: {
|
|
486
|
+
effect: {
|
|
487
|
+
readonly workspace: (facts: I) => string;
|
|
488
|
+
readonly payload: (facts: I) => P;
|
|
489
|
+
}
|
|
358
490
|
): IntentBuilder<I> {
|
|
359
|
-
if (
|
|
491
|
+
if (
|
|
492
|
+
isBusinessIntent(target) &&
|
|
493
|
+
target.acceptsWorkspaceEffectGate !== true
|
|
494
|
+
) {
|
|
360
495
|
throw new Error(
|
|
361
|
-
`intent('${this.id}') workspace effect may target a public intent only when target law explicitly accepts workspace effects
|
|
496
|
+
`intent('${this.id}') workspace effect may target a public intent only when target law explicitly accepts workspace effects`
|
|
362
497
|
);
|
|
363
498
|
}
|
|
364
|
-
return new IntentBuilder(
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
499
|
+
return new IntentBuilder(
|
|
500
|
+
this.id,
|
|
501
|
+
this.contractDeclaration,
|
|
502
|
+
this.steps,
|
|
503
|
+
[
|
|
504
|
+
...this.workspaceEffects,
|
|
505
|
+
{
|
|
506
|
+
target,
|
|
507
|
+
destination: { kind: "workspace", address: effect.workspace },
|
|
508
|
+
payload: effect.payload,
|
|
509
|
+
},
|
|
510
|
+
],
|
|
511
|
+
this.intentReads,
|
|
512
|
+
this.acceptsWorkspaceEffects,
|
|
513
|
+
this.intentRelationGate,
|
|
514
|
+
this.offerAuthenticationProof,
|
|
515
|
+
this.acceptsVerifiedBirthParentRecovery
|
|
516
|
+
);
|
|
372
517
|
}
|
|
373
518
|
|
|
374
519
|
/** Send an exact nominal request to this workspace's verified immediate parent. The route is
|
|
@@ -376,21 +521,34 @@ class IntentBuilder<I> {
|
|
|
376
521
|
* enters the public payload or the JavaScript execution boundary. */
|
|
377
522
|
directsParentWorkspace<P>(
|
|
378
523
|
target: Directive<P>,
|
|
379
|
-
effect: { readonly payload: (facts: I) => P }
|
|
524
|
+
effect: { readonly payload: (facts: I) => P }
|
|
380
525
|
): IntentBuilder<I> {
|
|
381
|
-
if (
|
|
526
|
+
if (
|
|
527
|
+
isBusinessIntent(target) &&
|
|
528
|
+
target.acceptsWorkspaceEffectGate !== true
|
|
529
|
+
) {
|
|
382
530
|
throw new Error(
|
|
383
|
-
`intent('${this.id}') parent request may target a public intent only when target law explicitly accepts workspace effects
|
|
531
|
+
`intent('${this.id}') parent request may target a public intent only when target law explicitly accepts workspace effects`
|
|
384
532
|
);
|
|
385
533
|
}
|
|
386
|
-
return new IntentBuilder(
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
534
|
+
return new IntentBuilder(
|
|
535
|
+
this.id,
|
|
536
|
+
this.contractDeclaration,
|
|
537
|
+
this.steps,
|
|
538
|
+
[
|
|
539
|
+
...this.workspaceEffects,
|
|
540
|
+
{
|
|
541
|
+
target,
|
|
542
|
+
destination: { kind: "verifiedParent" },
|
|
543
|
+
payload: effect.payload,
|
|
544
|
+
},
|
|
545
|
+
],
|
|
546
|
+
this.intentReads,
|
|
547
|
+
this.acceptsWorkspaceEffects,
|
|
548
|
+
this.intentRelationGate,
|
|
549
|
+
this.offerAuthenticationProof,
|
|
550
|
+
this.acceptsVerifiedBirthParentRecovery
|
|
551
|
+
);
|
|
394
552
|
}
|
|
395
553
|
|
|
396
554
|
/** Reply to the exact workspace whose carried effect invoked this intent. The destination is
|
|
@@ -399,21 +557,34 @@ class IntentBuilder<I> {
|
|
|
399
557
|
* still authorizes the verified workspace actor through its ordinary relation gate. */
|
|
400
558
|
repliesWorkspace<P>(
|
|
401
559
|
target: Directive<P>,
|
|
402
|
-
effect: { readonly payload: (facts: I) => P }
|
|
560
|
+
effect: { readonly payload: (facts: I) => P }
|
|
403
561
|
): IntentBuilder<I> {
|
|
404
|
-
if (
|
|
562
|
+
if (
|
|
563
|
+
isBusinessIntent(target) &&
|
|
564
|
+
target.acceptsWorkspaceEffectGate !== true
|
|
565
|
+
) {
|
|
405
566
|
throw new Error(
|
|
406
|
-
`intent('${this.id}') workspace reply
|
|
567
|
+
`intent('${this.id}') workspace reply may target a public intent only when target law explicitly accepts workspace effects`
|
|
407
568
|
);
|
|
408
569
|
}
|
|
409
|
-
return new IntentBuilder(
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
570
|
+
return new IntentBuilder(
|
|
571
|
+
this.id,
|
|
572
|
+
this.contractDeclaration,
|
|
573
|
+
this.steps,
|
|
574
|
+
[
|
|
575
|
+
...this.workspaceEffects,
|
|
576
|
+
{
|
|
577
|
+
target,
|
|
578
|
+
destination: { kind: "verifiedSource" },
|
|
579
|
+
payload: effect.payload,
|
|
580
|
+
},
|
|
581
|
+
],
|
|
582
|
+
this.intentReads,
|
|
583
|
+
this.acceptsWorkspaceEffects,
|
|
584
|
+
this.intentRelationGate,
|
|
585
|
+
this.offerAuthenticationProof,
|
|
586
|
+
this.acceptsVerifiedBirthParentRecovery
|
|
587
|
+
);
|
|
417
588
|
}
|
|
418
589
|
|
|
419
590
|
/** Admit this public request only through the cross-workspace effect door. Authority remains the
|
|
@@ -428,6 +599,24 @@ class IntentBuilder<I> {
|
|
|
428
599
|
true,
|
|
429
600
|
this.intentRelationGate,
|
|
430
601
|
this.offerAuthenticationProof,
|
|
602
|
+
this.acceptsVerifiedBirthParentRecovery
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/** Recovery-only cross-workspace door for an existing child whose ordinary parent relation may not
|
|
607
|
+
* be seated yet. The certified manifest selects this exact intent; admission then requires the
|
|
608
|
+
* carried K_root-rooted workspace actor to equal the target's persisted birth parent. */
|
|
609
|
+
acceptsVerifiedBirthParentRecoveryEffect(): IntentBuilder<I> {
|
|
610
|
+
return new IntentBuilder(
|
|
611
|
+
this.id,
|
|
612
|
+
this.contractDeclaration,
|
|
613
|
+
this.steps,
|
|
614
|
+
this.workspaceEffects,
|
|
615
|
+
this.intentReads,
|
|
616
|
+
true,
|
|
617
|
+
this.intentRelationGate,
|
|
618
|
+
this.offerAuthenticationProof,
|
|
619
|
+
true
|
|
431
620
|
);
|
|
432
621
|
}
|
|
433
622
|
|
|
@@ -444,6 +633,7 @@ class IntentBuilder<I> {
|
|
|
444
633
|
this.acceptsWorkspaceEffects,
|
|
445
634
|
this.intentRelationGate,
|
|
446
635
|
"enrolledDevice",
|
|
636
|
+
this.acceptsVerifiedBirthParentRecovery
|
|
447
637
|
);
|
|
448
638
|
}
|
|
449
639
|
|
|
@@ -451,12 +641,20 @@ class IntentBuilder<I> {
|
|
|
451
641
|
* a pure request/reply action that has no local storage consequence on which to hang a gate. */
|
|
452
642
|
requires(
|
|
453
643
|
relation: string,
|
|
454
|
-
options: { readonly object: "workspace:self" } = {
|
|
644
|
+
options: { readonly object: "workspace:self" } = {
|
|
645
|
+
object: "workspace:self",
|
|
646
|
+
}
|
|
455
647
|
): IntentBuilder<I> {
|
|
456
|
-
if (!relation.trim())
|
|
648
|
+
if (!relation.trim())
|
|
649
|
+
throw new Error(`intent('${this.id}') requires a non-empty relation`);
|
|
457
650
|
const gate = { relation, object: options.object };
|
|
458
|
-
if (
|
|
459
|
-
|
|
651
|
+
if (
|
|
652
|
+
this.intentRelationGate !== undefined &&
|
|
653
|
+
stable(this.intentRelationGate) !== stable(gate)
|
|
654
|
+
) {
|
|
655
|
+
throw new Error(
|
|
656
|
+
`intent('${this.id}') already has a different authorization gate`
|
|
657
|
+
);
|
|
460
658
|
}
|
|
461
659
|
return new IntentBuilder(
|
|
462
660
|
this.id,
|
|
@@ -467,6 +665,7 @@ class IntentBuilder<I> {
|
|
|
467
665
|
this.acceptsWorkspaceEffects,
|
|
468
666
|
gate,
|
|
469
667
|
this.offerAuthenticationProof,
|
|
668
|
+
this.acceptsVerifiedBirthParentRecovery
|
|
470
669
|
);
|
|
471
670
|
}
|
|
472
671
|
|
|
@@ -476,7 +675,10 @@ class IntentBuilder<I> {
|
|
|
476
675
|
reads(...reads: readonly (string | QueryDecl)[]): IntentBuilder<I> {
|
|
477
676
|
const byKey = new Map<string, string | QueryDecl>();
|
|
478
677
|
for (const read of [...this.intentReads, ...reads]) {
|
|
479
|
-
byKey.set(
|
|
678
|
+
byKey.set(
|
|
679
|
+
typeof read === "string" ? `ref:${read}` : `query:${read.id}`,
|
|
680
|
+
read
|
|
681
|
+
);
|
|
480
682
|
}
|
|
481
683
|
return new IntentBuilder(
|
|
482
684
|
this.id,
|
|
@@ -487,48 +689,69 @@ class IntentBuilder<I> {
|
|
|
487
689
|
this.acceptsWorkspaceEffects,
|
|
488
690
|
this.intentRelationGate,
|
|
489
691
|
this.offerAuthenticationProof,
|
|
692
|
+
this.acceptsVerifiedBirthParentRecovery
|
|
490
693
|
);
|
|
491
694
|
}
|
|
492
695
|
|
|
493
696
|
/** Add one declared child birth as a consequence; Nomos owns the private genesis recipe. */
|
|
494
697
|
births<P extends Record<string, unknown>, F extends Record<string, Field>>(
|
|
495
698
|
declaration: BusinessBirthDecl<P, F>,
|
|
496
|
-
payload: (facts: I) => P
|
|
699
|
+
payload: (facts: I) => P
|
|
497
700
|
): IntentBuilder<I> {
|
|
498
701
|
const directive = businessBirthConsequence(declaration);
|
|
499
|
-
return new IntentBuilder(
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
702
|
+
return new IntentBuilder(
|
|
703
|
+
this.id,
|
|
704
|
+
this.contractDeclaration,
|
|
705
|
+
[...this.steps, { directive, payload }],
|
|
706
|
+
this.workspaceEffects,
|
|
707
|
+
this.intentReads,
|
|
708
|
+
this.acceptsWorkspaceEffects,
|
|
709
|
+
this.intentRelationGate,
|
|
710
|
+
this.offerAuthenticationProof,
|
|
711
|
+
this.acceptsVerifiedBirthParentRecovery
|
|
712
|
+
);
|
|
503
713
|
}
|
|
504
714
|
|
|
505
715
|
/** Finish the intent and lower its directive graph to one atomic offer. */
|
|
506
716
|
atomically(): BusinessIntent<I> {
|
|
507
717
|
if (this.steps.length === 0 && this.workspaceEffects.length === 0) {
|
|
508
|
-
throw new Error(
|
|
718
|
+
throw new Error(
|
|
719
|
+
`intent('${this.id}') needs at least one directed consequence`
|
|
720
|
+
);
|
|
509
721
|
}
|
|
510
722
|
const duplicate = this.steps.find(
|
|
511
|
-
(step, index) =>
|
|
723
|
+
(step, index) =>
|
|
724
|
+
this.steps.findIndex(
|
|
725
|
+
(candidate) => candidate.directive.id === step.directive.id
|
|
726
|
+
) !== index
|
|
512
727
|
);
|
|
513
728
|
if (duplicate !== undefined) {
|
|
514
|
-
throw new Error(
|
|
729
|
+
throw new Error(
|
|
730
|
+
`intent('${this.id}') directs '${duplicate.directive.id}' more than once`
|
|
731
|
+
);
|
|
515
732
|
}
|
|
516
|
-
const birthSteps = this.steps.filter(
|
|
733
|
+
const birthSteps = this.steps.filter(
|
|
734
|
+
(step) => step.directive.birthsChild === true
|
|
735
|
+
);
|
|
517
736
|
if (birthSteps.length > 1) {
|
|
518
|
-
throw new Error(
|
|
737
|
+
throw new Error(
|
|
738
|
+
`intent('${this.id}') cannot birth more than one durable life atomically`
|
|
739
|
+
);
|
|
519
740
|
}
|
|
520
741
|
for (const step of this.steps) {
|
|
521
742
|
const unsupported = unsupportedKernelGate(step.directive);
|
|
522
743
|
if (unsupported !== undefined) {
|
|
523
744
|
throw new Error(
|
|
524
|
-
`intent('${this.id}') cannot compose '${step.directive.id}' yet: ${unsupported} needs intent-level lowering
|
|
745
|
+
`intent('${this.id}') cannot compose '${step.directive.id}' yet: ${unsupported} needs intent-level lowering`
|
|
525
746
|
);
|
|
526
747
|
}
|
|
527
748
|
}
|
|
528
749
|
|
|
529
|
-
const primary =
|
|
530
|
-
|
|
531
|
-
|
|
750
|
+
const primary =
|
|
751
|
+
this.steps.find((step) => step.directive.birthsChild !== true)
|
|
752
|
+
?.directive ??
|
|
753
|
+
this.steps[0]?.directive ??
|
|
754
|
+
this.workspaceEffects[0]!.target;
|
|
532
755
|
const allOutputs: SecondaryOutput[] = [];
|
|
533
756
|
for (const step of this.steps) {
|
|
534
757
|
const candidate = step.directive;
|
|
@@ -537,7 +760,9 @@ class IntentBuilder<I> {
|
|
|
537
760
|
aggregateId: candidate.aggregateId,
|
|
538
761
|
marker: candidate.marker,
|
|
539
762
|
...(candidate.outputCardinality === "many" ? { many: true } : {}),
|
|
540
|
-
...(candidate.outputCardinality === "optional"
|
|
763
|
+
...(candidate.outputCardinality === "optional"
|
|
764
|
+
? { optional: true }
|
|
765
|
+
: {}),
|
|
541
766
|
});
|
|
542
767
|
allOutputs.push(...(candidate.secondaryOutputs ?? []));
|
|
543
768
|
}
|
|
@@ -547,7 +772,9 @@ class IntentBuilder<I> {
|
|
|
547
772
|
const key = `${output.marker}\0${output.aggregateId}`;
|
|
548
773
|
outputGroups.set(key, [...(outputGroups.get(key) ?? []), output]);
|
|
549
774
|
}
|
|
550
|
-
const composedOutput = (
|
|
775
|
+
const composedOutput = (
|
|
776
|
+
group: readonly SecondaryOutput[]
|
|
777
|
+
): SecondaryOutput => ({
|
|
551
778
|
aggregateId: group[0]!.aggregateId,
|
|
552
779
|
marker: group[0]!.marker,
|
|
553
780
|
...(group.length > 1 || group.some((output) => output.many === true)
|
|
@@ -556,35 +783,48 @@ class IntentBuilder<I> {
|
|
|
556
783
|
? { optional: true }
|
|
557
784
|
: {}),
|
|
558
785
|
});
|
|
559
|
-
const primaryOutput =
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
786
|
+
const primaryOutput =
|
|
787
|
+
this.steps.length === 0
|
|
788
|
+
? undefined
|
|
789
|
+
: composedOutput(
|
|
790
|
+
outputGroups.get(`${primary.marker}\0${primary.aggregateId}`) ?? [
|
|
791
|
+
{
|
|
792
|
+
aggregateId: primary.aggregateId,
|
|
793
|
+
marker: primary.marker,
|
|
794
|
+
},
|
|
795
|
+
]
|
|
796
|
+
);
|
|
567
797
|
const secondaryOutputs = [...outputGroups.values()]
|
|
568
798
|
.map(composedOutput)
|
|
569
799
|
.filter(
|
|
570
|
-
(output) =>
|
|
800
|
+
(output) =>
|
|
801
|
+
!(
|
|
802
|
+
output.aggregateId === primary.aggregateId &&
|
|
803
|
+
output.marker === primary.marker
|
|
804
|
+
)
|
|
571
805
|
);
|
|
572
806
|
const inferredReturns = [
|
|
573
807
|
...(primaryOutput === undefined ? [] : [primary.aggregateId]),
|
|
574
808
|
...secondaryOutputs.map((output) => output.aggregateId),
|
|
575
809
|
...birthSteps.flatMap((step) => {
|
|
576
|
-
const aggregateId = (step.directive as BusinessBirthDirective)
|
|
810
|
+
const aggregateId = (step.directive as BusinessBirthDirective)
|
|
811
|
+
.businessBirth?.aggregateId;
|
|
577
812
|
return aggregateId === undefined ? [] : [aggregateId];
|
|
578
813
|
}),
|
|
579
814
|
].filter((value, index, values) => values.indexOf(value) === index);
|
|
580
|
-
const declaredReturns = this.contractDeclaration.returns?.map(
|
|
815
|
+
const declaredReturns = this.contractDeclaration.returns?.map(
|
|
816
|
+
(aggregate) => aggregate.id
|
|
817
|
+
);
|
|
581
818
|
if (
|
|
582
819
|
declaredReturns !== undefined &&
|
|
583
|
-
stable([...new Set(declaredReturns)].sort()) !==
|
|
820
|
+
stable([...new Set(declaredReturns)].sort()) !==
|
|
821
|
+
stable([...inferredReturns].sort())
|
|
584
822
|
) {
|
|
585
823
|
throw new Error(
|
|
586
|
-
`intent('${this.id}') contract returns ${
|
|
587
|
-
|
|
824
|
+
`intent('${this.id}') contract returns ${
|
|
825
|
+
declaredReturns.join(", ") || "nothing"
|
|
826
|
+
}, but its ` +
|
|
827
|
+
`consequences affect ${inferredReturns.join(", ") || "nothing"}`
|
|
588
828
|
);
|
|
589
829
|
}
|
|
590
830
|
|
|
@@ -595,15 +835,16 @@ class IntentBuilder<I> {
|
|
|
595
835
|
readId: privateReadId,
|
|
596
836
|
...rest
|
|
597
837
|
} = condition;
|
|
598
|
-
const publicIdPayloadPath =
|
|
599
|
-
|
|
600
|
-
|
|
838
|
+
const publicIdPayloadPath =
|
|
839
|
+
privateIdPayloadPath === undefined
|
|
840
|
+
? undefined
|
|
841
|
+
: remapStepPayloadPath(step, privateIdPayloadPath);
|
|
601
842
|
let publicReadId: InvariantReadId | undefined;
|
|
602
843
|
if (privateReadId !== undefined) {
|
|
603
844
|
try {
|
|
604
845
|
publicReadId = remapReadId(
|
|
605
846
|
privateReadId,
|
|
606
|
-
step.payload(payloadTrace() as unknown as I)
|
|
847
|
+
step.payload(payloadTrace() as unknown as I)
|
|
607
848
|
);
|
|
608
849
|
} catch {
|
|
609
850
|
publicReadId = undefined;
|
|
@@ -611,32 +852,43 @@ class IntentBuilder<I> {
|
|
|
611
852
|
}
|
|
612
853
|
return {
|
|
613
854
|
...rest,
|
|
614
|
-
...(publicIdPayloadPath === undefined
|
|
855
|
+
...(publicIdPayloadPath === undefined
|
|
856
|
+
? {}
|
|
857
|
+
: { idPayloadPath: publicIdPayloadPath }),
|
|
615
858
|
...(publicReadId === undefined ? {} : { readId: publicReadId }),
|
|
616
|
-
idField: (facts: unknown) =>
|
|
859
|
+
idField: (facts: unknown) =>
|
|
860
|
+
condition.idField(step.payload(facts as I)),
|
|
617
861
|
};
|
|
618
|
-
})
|
|
862
|
+
})
|
|
619
863
|
);
|
|
620
864
|
const intentQueryReads = this.intentReads.filter(
|
|
621
|
-
(read): read is QueryDecl => typeof read !== "string"
|
|
865
|
+
(read): read is QueryDecl => typeof read !== "string"
|
|
622
866
|
);
|
|
623
867
|
const intentRefReads = this.intentReads.filter(
|
|
624
|
-
(read): read is string => typeof read === "string"
|
|
868
|
+
(read): read is string => typeof read === "string"
|
|
625
869
|
);
|
|
626
870
|
const queryReads = dedupeBy(
|
|
627
|
-
[
|
|
628
|
-
|
|
871
|
+
[
|
|
872
|
+
...intentQueryReads,
|
|
873
|
+
...this.steps.flatMap(
|
|
874
|
+
(step) => step.directive.declaredQueryReads ?? []
|
|
875
|
+
),
|
|
876
|
+
],
|
|
877
|
+
(query) => query.id
|
|
629
878
|
);
|
|
630
879
|
const foreignReads = dedupeBy(
|
|
631
880
|
this.steps.flatMap((step) => step.directive.declaredForeignReads ?? []),
|
|
632
|
-
(read) => stable(read)
|
|
881
|
+
(read) => stable(read)
|
|
633
882
|
);
|
|
634
883
|
const emits: Record<string, { max?: number }> = {};
|
|
635
884
|
for (const step of this.steps) {
|
|
636
|
-
for (const [event, declaration] of Object.entries(
|
|
885
|
+
for (const [event, declaration] of Object.entries(
|
|
886
|
+
step.directive.declaredEmits
|
|
887
|
+
)) {
|
|
637
888
|
const prior = emits[event];
|
|
638
889
|
if (prior === undefined) {
|
|
639
|
-
emits[event] =
|
|
890
|
+
emits[event] =
|
|
891
|
+
declaration.max === undefined ? {} : { max: declaration.max };
|
|
640
892
|
} else if (prior.max === undefined || declaration.max === undefined) {
|
|
641
893
|
emits[event] = {};
|
|
642
894
|
} else {
|
|
@@ -650,26 +902,31 @@ class IntentBuilder<I> {
|
|
|
650
902
|
inheritedRelationGate !== undefined &&
|
|
651
903
|
stable(this.intentRelationGate) !== stable(inheritedRelationGate)
|
|
652
904
|
) {
|
|
653
|
-
throw new Error(
|
|
905
|
+
throw new Error(
|
|
906
|
+
`intent('${this.id}') has conflicting intent and consequence authorization gates`
|
|
907
|
+
);
|
|
654
908
|
}
|
|
655
909
|
const relationGate = this.intentRelationGate ?? inheritedRelationGate;
|
|
656
910
|
if (
|
|
657
911
|
this.acceptsWorkspaceEffects &&
|
|
658
|
-
|
|
912
|
+
!this.acceptsVerifiedBirthParentRecovery &&
|
|
913
|
+
(relationGate === undefined ||
|
|
914
|
+
relationGate.object !== "workspace:self" ||
|
|
915
|
+
relationGate.objectFrom !== undefined)
|
|
659
916
|
) {
|
|
660
917
|
throw new Error(
|
|
661
918
|
`intent('${this.id}') .acceptsWorkspaceEffect() requires one static ` +
|
|
662
|
-
`.requires(relation, { object: "workspace:self" }) consequence gate
|
|
919
|
+
`.requires(relation, { object: "workspace:self" }) consequence gate`
|
|
663
920
|
);
|
|
664
921
|
}
|
|
665
922
|
const sourceTrusts = this.steps.flatMap((step) =>
|
|
666
923
|
step.directive.businessSourceTrust === undefined
|
|
667
924
|
? []
|
|
668
|
-
: [remapBusinessSourceTrust(step, step.directive.businessSourceTrust)]
|
|
925
|
+
: [remapBusinessSourceTrust(step, step.directive.businessSourceTrust)]
|
|
669
926
|
);
|
|
670
927
|
if (sourceTrusts.length > 1) {
|
|
671
928
|
throw new Error(
|
|
672
|
-
`intent('${this.id}') composes more than one source-trust preflight — split the business action by source
|
|
929
|
+
`intent('${this.id}') composes more than one source-trust preflight — split the business action by source`
|
|
673
930
|
);
|
|
674
931
|
}
|
|
675
932
|
const steps = [...this.steps];
|
|
@@ -680,19 +937,26 @@ class IntentBuilder<I> {
|
|
|
680
937
|
const result = step.directive.plan(mapped, ctx);
|
|
681
938
|
if (!Array.isArray(result)) {
|
|
682
939
|
throw new Error(
|
|
683
|
-
`intent '${this.id}' consequence '${step.directive.id}' did not return an operation array
|
|
940
|
+
`intent '${this.id}' consequence '${step.directive.id}' did not return an operation array`
|
|
684
941
|
);
|
|
685
942
|
}
|
|
686
|
-
returned.push(
|
|
943
|
+
returned.push(
|
|
944
|
+
...result.map((op) => composePrimaryConsequenceOp(step.directive, op))
|
|
945
|
+
);
|
|
687
946
|
}
|
|
688
947
|
return returned;
|
|
689
948
|
};
|
|
690
949
|
|
|
691
|
-
const businessBirth = (
|
|
692
|
-
|
|
693
|
-
|
|
950
|
+
const businessBirth = (
|
|
951
|
+
birthSteps[0]?.directive as BusinessBirthDirective | undefined
|
|
952
|
+
)?.businessBirth;
|
|
953
|
+
const bindBusinessBirthPolicy = (
|
|
954
|
+
birthSteps[0]?.directive as BusinessBirthDirective | undefined
|
|
955
|
+
)?.[BUSINESS_BIRTH_POLICY_BINDING];
|
|
694
956
|
const carriedCapabilities = [
|
|
695
|
-
...new Set(
|
|
957
|
+
...new Set(
|
|
958
|
+
this.steps.flatMap((step) => capabilityDeclarationsOf(step.directive))
|
|
959
|
+
),
|
|
696
960
|
];
|
|
697
961
|
return {
|
|
698
962
|
[INTENT_BRAND]: true,
|
|
@@ -708,21 +972,29 @@ class IntentBuilder<I> {
|
|
|
708
972
|
// runtime plan may retain only this intent export; without the handle the
|
|
709
973
|
// engine cannot rebuild its aggregate registry entry and silently drops
|
|
710
974
|
// compound intents that also birth a child.
|
|
711
|
-
...(primary.aggregate === undefined
|
|
975
|
+
...(primary.aggregate === undefined
|
|
976
|
+
? {}
|
|
977
|
+
: { aggregate: primary.aggregate }),
|
|
712
978
|
...(primaryOutput?.many === true
|
|
713
979
|
? { outputCardinality: "many" as const }
|
|
714
980
|
: primaryOutput?.optional === true
|
|
715
981
|
? { outputCardinality: "optional" as const }
|
|
716
982
|
: {}),
|
|
717
983
|
...(secondaryOutputs.length > 0 ? { secondaryOutputs } : {}),
|
|
718
|
-
payloadSchema: (birthSteps[0]?.directive.payloadSchema ??
|
|
984
|
+
payloadSchema: (birthSteps[0]?.directive.payloadSchema ??
|
|
985
|
+
this.contractDeclaration.accepts) as unknown as z.ZodType<I>,
|
|
719
986
|
plan,
|
|
720
987
|
steps,
|
|
721
988
|
workspaceEffects: this.workspaceEffects,
|
|
722
989
|
...(this.offerAuthenticationProof === undefined
|
|
723
990
|
? {}
|
|
724
991
|
: { offerAuthenticationProof: this.offerAuthenticationProof }),
|
|
725
|
-
...(this.acceptsWorkspaceEffects
|
|
992
|
+
...(this.acceptsWorkspaceEffects
|
|
993
|
+
? { acceptsWorkspaceEffectGate: true as const }
|
|
994
|
+
: {}),
|
|
995
|
+
...(this.acceptsVerifiedBirthParentRecovery
|
|
996
|
+
? { acceptsVerifiedBirthParentRecoveryGate: true as const }
|
|
997
|
+
: {}),
|
|
726
998
|
declaredReads: [
|
|
727
999
|
...new Set([
|
|
728
1000
|
...intentRefReads,
|
|
@@ -730,17 +1002,23 @@ class IntentBuilder<I> {
|
|
|
730
1002
|
]),
|
|
731
1003
|
].sort(),
|
|
732
1004
|
...(queryReads.length > 0 ? { declaredQueryReads: queryReads } : {}),
|
|
733
|
-
...(foreignReads.length > 0
|
|
1005
|
+
...(foreignReads.length > 0
|
|
1006
|
+
? { declaredForeignReads: foreignReads }
|
|
1007
|
+
: {}),
|
|
734
1008
|
declaredEmits: emits,
|
|
735
1009
|
declaredRelations: dedupeBy(
|
|
736
1010
|
this.steps.flatMap((step) => step.directive.declaredRelations),
|
|
737
|
-
(relation) => stable(relation)
|
|
1011
|
+
(relation) => stable(relation)
|
|
738
1012
|
),
|
|
739
1013
|
declaredHostCapabilities: [
|
|
740
|
-
...new Set(
|
|
1014
|
+
...new Set(
|
|
1015
|
+
this.steps.flatMap((step) => step.directive.declaredHostCapabilities)
|
|
1016
|
+
),
|
|
741
1017
|
].sort(),
|
|
742
1018
|
...(preconditions.length > 0 ? { preconditions } : {}),
|
|
743
|
-
...(relationGate !== undefined
|
|
1019
|
+
...(relationGate !== undefined
|
|
1020
|
+
? { requiresRelationGate: relationGate }
|
|
1021
|
+
: {}),
|
|
744
1022
|
...(sourceTrusts[0] === undefined
|
|
745
1023
|
? {}
|
|
746
1024
|
: { businessSourceTrust: sourceTrusts[0] }),
|
|
@@ -775,9 +1053,14 @@ class IntentContractStep {
|
|
|
775
1053
|
*/
|
|
776
1054
|
performs<P>(effect: Directive<P>): BusinessIntent<P> {
|
|
777
1055
|
if (isBusinessIntent(effect)) {
|
|
778
|
-
throw new Error(
|
|
1056
|
+
throw new Error(
|
|
1057
|
+
`intent('${this.id}').performs(...) expects a private directive effect, not an intent`
|
|
1058
|
+
);
|
|
779
1059
|
}
|
|
780
|
-
const step: IntentStep<P, P> = {
|
|
1060
|
+
const step: IntentStep<P, P> = {
|
|
1061
|
+
directive: effect,
|
|
1062
|
+
payload: (facts) => facts,
|
|
1063
|
+
};
|
|
781
1064
|
// A one-effect publication does not need to synthesize or approximate any
|
|
782
1065
|
// kernel policy. Preserve the effect's complete metadata (birth, access,
|
|
783
1066
|
// custody, recovery, preconditions, activity...) and change only the public
|
|
@@ -789,33 +1072,48 @@ class IntentContractStep {
|
|
|
789
1072
|
id: this.id,
|
|
790
1073
|
contract: {
|
|
791
1074
|
accepts: effect.payloadSchema,
|
|
792
|
-
returns: Object.freeze(
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
1075
|
+
returns: Object.freeze(
|
|
1076
|
+
[
|
|
1077
|
+
effect.aggregateId,
|
|
1078
|
+
...(effect.secondaryOutputs ?? []).map(
|
|
1079
|
+
(output) => output.aggregateId
|
|
1080
|
+
),
|
|
1081
|
+
].filter((value, index, values) => values.indexOf(value) === index)
|
|
1082
|
+
),
|
|
796
1083
|
refuses: Object.freeze([]),
|
|
797
1084
|
},
|
|
798
1085
|
steps: [step],
|
|
799
1086
|
workspaceEffects: [],
|
|
800
|
-
plan: (facts, ctx) =>
|
|
801
|
-
|
|
802
|
-
|
|
1087
|
+
plan: (facts, ctx) =>
|
|
1088
|
+
effect
|
|
1089
|
+
.plan(effect.payloadSchema.parse(facts), ctx)
|
|
1090
|
+
.map((op) => composePrimaryConsequenceOp(effect, op)),
|
|
803
1091
|
};
|
|
804
1092
|
}
|
|
805
1093
|
|
|
806
1094
|
/** Begin an intent from one declared business birth without repeating its payload schema. */
|
|
807
1095
|
births<P extends Record<string, unknown>, F extends Record<string, Field>>(
|
|
808
|
-
declaration: BusinessBirthDecl<P, F
|
|
1096
|
+
declaration: BusinessBirthDecl<P, F>
|
|
809
1097
|
): IntentBuilder<P> {
|
|
810
|
-
return new IntentBuilder<P>(
|
|
811
|
-
|
|
812
|
-
|
|
1098
|
+
return new IntentBuilder<P>(
|
|
1099
|
+
this.id,
|
|
1100
|
+
{ accepts: declaration.payloadSchema as z.ZodType<P> },
|
|
1101
|
+
[
|
|
1102
|
+
{
|
|
1103
|
+
directive: businessBirthConsequence(declaration),
|
|
1104
|
+
payload: (facts) => facts,
|
|
1105
|
+
},
|
|
1106
|
+
]
|
|
1107
|
+
);
|
|
813
1108
|
}
|
|
814
1109
|
|
|
815
1110
|
contract<S extends z.ZodTypeAny>(
|
|
816
|
-
declaration: IntentContractDeclaration<z.infer<S>> & { readonly accepts: S }
|
|
1111
|
+
declaration: IntentContractDeclaration<z.infer<S>> & { readonly accepts: S }
|
|
817
1112
|
): IntentBuilder<z.infer<S>> {
|
|
818
|
-
return new IntentBuilder(
|
|
1113
|
+
return new IntentBuilder(
|
|
1114
|
+
this.id,
|
|
1115
|
+
declaration as IntentContractDeclaration<z.infer<S>>
|
|
1116
|
+
);
|
|
819
1117
|
}
|
|
820
1118
|
}
|
|
821
1119
|
|
|
@@ -823,7 +1121,9 @@ class IntentContractStep {
|
|
|
823
1121
|
export function intent<const Id extends string>(id: Id): IntentContractStep {
|
|
824
1122
|
if (id.trim().length === 0) throw new Error("intent id must not be empty");
|
|
825
1123
|
if (id.startsWith("_nms")) {
|
|
826
|
-
throw new Error(
|
|
1124
|
+
throw new Error(
|
|
1125
|
+
`intent id '${id}' uses Nomos's reserved internal prefix '_nms'`
|
|
1126
|
+
);
|
|
827
1127
|
}
|
|
828
1128
|
return new IntentContractStep(id);
|
|
829
1129
|
}
|
|
@@ -840,16 +1140,21 @@ export function isBusinessIntent(value: unknown): value is BusinessIntent<any> {
|
|
|
840
1140
|
export function language(
|
|
841
1141
|
domain: string,
|
|
842
1142
|
intents: readonly BusinessIntent<any>[],
|
|
843
|
-
options: ApplicationLanguageOptions = {}
|
|
1143
|
+
options: ApplicationLanguageOptions = {}
|
|
844
1144
|
): ApplicationLanguage {
|
|
845
|
-
if (domain.trim().length === 0)
|
|
1145
|
+
if (domain.trim().length === 0)
|
|
1146
|
+
throw new Error("language domain must not be empty");
|
|
846
1147
|
const seen = new Set<string>();
|
|
847
1148
|
for (const candidate of intents) {
|
|
848
1149
|
if (!isBusinessIntent(candidate)) {
|
|
849
|
-
throw new Error(
|
|
1150
|
+
throw new Error(
|
|
1151
|
+
`language('${domain}') accepts finished intent(...).atomically() declarations only`
|
|
1152
|
+
);
|
|
850
1153
|
}
|
|
851
1154
|
if (seen.has(candidate.id)) {
|
|
852
|
-
throw new Error(
|
|
1155
|
+
throw new Error(
|
|
1156
|
+
`language('${domain}') publishes intent '${candidate.id}' more than once`
|
|
1157
|
+
);
|
|
853
1158
|
}
|
|
854
1159
|
seen.add(candidate.id);
|
|
855
1160
|
}
|
|
@@ -858,7 +1163,7 @@ export function language(
|
|
|
858
1163
|
for (const initiator of policy.initiatedBy) {
|
|
859
1164
|
if (!intents.some((candidate) => candidate.id === initiator.id)) {
|
|
860
1165
|
throw new Error(
|
|
861
|
-
`language('${domain}') workspaceSharing(${policy.orders.id}) initiator '${initiator.id}' is not a public business action
|
|
1166
|
+
`language('${domain}') workspaceSharing(${policy.orders.id}) initiator '${initiator.id}' is not a public business action`
|
|
862
1167
|
);
|
|
863
1168
|
}
|
|
864
1169
|
}
|
|
@@ -871,7 +1176,9 @@ export function language(
|
|
|
871
1176
|
};
|
|
872
1177
|
}
|
|
873
1178
|
|
|
874
|
-
export function isApplicationLanguage(
|
|
1179
|
+
export function isApplicationLanguage(
|
|
1180
|
+
value: unknown
|
|
1181
|
+
): value is ApplicationLanguage {
|
|
875
1182
|
return (
|
|
876
1183
|
value !== null &&
|
|
877
1184
|
typeof value === "object" &&
|
|
@@ -881,7 +1188,7 @@ export function isApplicationLanguage(value: unknown): value is ApplicationLangu
|
|
|
881
1188
|
|
|
882
1189
|
export function applicationLanguageOf(
|
|
883
1190
|
modules: readonly Record<string, unknown>[],
|
|
884
|
-
domain: string
|
|
1191
|
+
domain: string
|
|
885
1192
|
): ApplicationLanguage | undefined {
|
|
886
1193
|
const found: ApplicationLanguage[] = [];
|
|
887
1194
|
for (const mod of modules) {
|
|
@@ -891,12 +1198,14 @@ export function applicationLanguageOf(
|
|
|
891
1198
|
}
|
|
892
1199
|
const unique = [...new Set(found)];
|
|
893
1200
|
if (unique.length > 1) {
|
|
894
|
-
throw new Error(
|
|
1201
|
+
throw new Error(
|
|
1202
|
+
`domain '${domain}' exports more than one application language`
|
|
1203
|
+
);
|
|
895
1204
|
}
|
|
896
1205
|
const language = unique[0];
|
|
897
1206
|
if (language !== undefined && language.domain !== domain) {
|
|
898
1207
|
throw new Error(
|
|
899
|
-
`domain '${domain}' exports language('${language.domain}') — the language name must match its domain
|
|
1208
|
+
`domain '${domain}' exports language('${language.domain}') — the language name must match its domain`
|
|
900
1209
|
);
|
|
901
1210
|
}
|
|
902
1211
|
return language;
|
|
@@ -910,17 +1219,21 @@ export function applicationLanguageOf(
|
|
|
910
1219
|
export function executableDirectivesForLanguage(
|
|
911
1220
|
language: ApplicationLanguage,
|
|
912
1221
|
discovered: readonly Directive<any>[],
|
|
913
|
-
domain: string
|
|
1222
|
+
domain: string
|
|
914
1223
|
): Directive<any>[] {
|
|
915
|
-
const publicById = new Map(
|
|
1224
|
+
const publicById = new Map(
|
|
1225
|
+
language.intents.map((candidate) => [candidate.id, candidate])
|
|
1226
|
+
);
|
|
916
1227
|
const privateIds = new Set(
|
|
917
|
-
language.intents.flatMap((candidate) =>
|
|
1228
|
+
language.intents.flatMap((candidate) =>
|
|
1229
|
+
candidate.steps.map((step) => step.directive.id)
|
|
1230
|
+
)
|
|
918
1231
|
);
|
|
919
1232
|
|
|
920
1233
|
for (const candidate of language.intents) {
|
|
921
1234
|
if (privateIds.has(candidate.id)) {
|
|
922
1235
|
throw new Error(
|
|
923
|
-
`domain '${domain}' publishes intent '${candidate.id}' with the same name as one of its private directives
|
|
1236
|
+
`domain '${domain}' publishes intent '${candidate.id}' with the same name as one of its private directives`
|
|
924
1237
|
);
|
|
925
1238
|
}
|
|
926
1239
|
}
|
|
@@ -933,22 +1246,23 @@ export function executableDirectivesForLanguage(
|
|
|
933
1246
|
candidate.marker === published.marker &&
|
|
934
1247
|
candidate.steps.length === published.steps.length &&
|
|
935
1248
|
candidate.steps.every(
|
|
936
|
-
(step, index) =>
|
|
1249
|
+
(step, index) =>
|
|
1250
|
+
step.directive.id === published.steps[index]?.directive.id
|
|
937
1251
|
);
|
|
938
1252
|
if (published !== undefined && candidate !== published && !sameLowering) {
|
|
939
1253
|
throw new Error(
|
|
940
|
-
`domain '${domain}' intent '${candidate.id}' collides with a different directive — rename one of them
|
|
1254
|
+
`domain '${domain}' intent '${candidate.id}' collides with a different directive — rename one of them`
|
|
941
1255
|
);
|
|
942
1256
|
}
|
|
943
1257
|
if (isBusinessIntent(candidate) && published === undefined) {
|
|
944
1258
|
throw new Error(
|
|
945
|
-
`domain '${domain}' exports intent '${candidate.id}' outside its declared language — add it to language('${domain}', [...]) or remove it
|
|
1259
|
+
`domain '${domain}' exports intent '${candidate.id}' outside its declared language — add it to language('${domain}', [...]) or remove it`
|
|
946
1260
|
);
|
|
947
1261
|
}
|
|
948
1262
|
}
|
|
949
1263
|
|
|
950
1264
|
const remaining = discovered.filter(
|
|
951
|
-
(candidate) => !privateIds.has(candidate.id) && !isBusinessIntent(candidate)
|
|
1265
|
+
(candidate) => !privateIds.has(candidate.id) && !isBusinessIntent(candidate)
|
|
952
1266
|
);
|
|
953
1267
|
return [...remaining, ...language.intents];
|
|
954
1268
|
}
|
|
@@ -962,16 +1276,20 @@ export function executableDirectivesForLanguage(
|
|
|
962
1276
|
export function assertApplicationLanguageCoversAuthoredDirectives(
|
|
963
1277
|
language: ApplicationLanguage,
|
|
964
1278
|
authored: readonly Directive<any>[],
|
|
965
|
-
domain: string
|
|
1279
|
+
domain: string
|
|
966
1280
|
): void {
|
|
967
1281
|
const publicIds = new Set(language.intents.map((candidate) => candidate.id));
|
|
968
1282
|
const privateIds = new Set(
|
|
969
|
-
language.intents.flatMap((candidate) =>
|
|
1283
|
+
language.intents.flatMap((candidate) =>
|
|
1284
|
+
candidate.steps.map((step) => step.directive.id)
|
|
1285
|
+
)
|
|
970
1286
|
);
|
|
971
1287
|
const sharingReceiptIds = new Set(
|
|
972
1288
|
language.workspaceSharing.flatMap((policy) =>
|
|
973
|
-
[...policy.delivered, ...policy.refused].map(
|
|
974
|
-
|
|
1289
|
+
[...policy.delivered, ...policy.refused].map(
|
|
1290
|
+
(receipt) => receipt.records.id
|
|
1291
|
+
)
|
|
1292
|
+
)
|
|
975
1293
|
);
|
|
976
1294
|
const uncovered = authored
|
|
977
1295
|
.filter(
|
|
@@ -979,15 +1297,17 @@ export function assertApplicationLanguageCoversAuthoredDirectives(
|
|
|
979
1297
|
directive.businessBirthInternal !== true &&
|
|
980
1298
|
!publicIds.has(directive.id) &&
|
|
981
1299
|
!privateIds.has(directive.id) &&
|
|
982
|
-
!sharingReceiptIds.has(directive.id)
|
|
1300
|
+
!sharingReceiptIds.has(directive.id)
|
|
983
1301
|
)
|
|
984
1302
|
.map((directive) => directive.id)
|
|
985
1303
|
.sort();
|
|
986
1304
|
if (uncovered.length > 0) {
|
|
987
1305
|
throw new Error(
|
|
988
1306
|
`domain '${domain}' declares language('${domain}', [...]) but still exports routable directive ` +
|
|
989
|
-
`${uncovered
|
|
990
|
-
|
|
1307
|
+
`${uncovered
|
|
1308
|
+
.map((id) => `'${id}'`)
|
|
1309
|
+
.join(", ")} outside it — publish business intents for ` +
|
|
1310
|
+
`those actions or make their effects private`
|
|
991
1311
|
);
|
|
992
1312
|
}
|
|
993
1313
|
}
|
|
@@ -995,36 +1315,43 @@ export function assertApplicationLanguageCoversAuthoredDirectives(
|
|
|
995
1315
|
/** Carry a private directive's invariant onto every public intent that uses it. */
|
|
996
1316
|
export function remapWorkspaceInvariantsForLanguage(
|
|
997
1317
|
invariants: readonly WorkspaceInvariantDecl[],
|
|
998
|
-
language: ApplicationLanguage
|
|
1318
|
+
language: ApplicationLanguage
|
|
999
1319
|
): WorkspaceInvariantDecl[] {
|
|
1000
1320
|
const out: WorkspaceInvariantDecl[] = [];
|
|
1001
1321
|
for (const invariant of invariants) {
|
|
1002
1322
|
const uses = language.intents.flatMap((candidate) =>
|
|
1003
1323
|
candidate.steps
|
|
1004
1324
|
.filter((step) => step.directive.id === invariant.on)
|
|
1005
|
-
.map((step) => ({ intent: candidate, step }))
|
|
1325
|
+
.map((step) => ({ intent: candidate, step }))
|
|
1006
1326
|
);
|
|
1007
1327
|
if (uses.length === 0) {
|
|
1008
1328
|
out.push(invariant);
|
|
1009
1329
|
continue;
|
|
1010
1330
|
}
|
|
1011
1331
|
for (const use of uses) {
|
|
1012
|
-
const mapFacts = (
|
|
1332
|
+
const mapFacts = (
|
|
1333
|
+
facts: Record<string, unknown>
|
|
1334
|
+
): Record<string, unknown> => {
|
|
1013
1335
|
const mapped = use.step.payload(facts);
|
|
1014
1336
|
const parsed = use.step.directive.payloadSchema.safeParse(mapped);
|
|
1015
1337
|
// The flow/security compiler probes invariant read-sets with empty
|
|
1016
1338
|
// placeholder facts. That probe must remain total even when the real
|
|
1017
1339
|
// business payload requires non-empty values. Runtime intent planning
|
|
1018
1340
|
// still validates every mapped consequence before it can commit.
|
|
1019
|
-
return (parsed.success ? parsed.data : mapped) as Record<
|
|
1341
|
+
return (parsed.success ? parsed.data : mapped) as Record<
|
|
1342
|
+
string,
|
|
1343
|
+
unknown
|
|
1344
|
+
>;
|
|
1020
1345
|
};
|
|
1021
1346
|
// Never carry a private payload path blindly onto a public intent. Prove the
|
|
1022
1347
|
// mapping structurally with inert path values; otherwise leave the selector
|
|
1023
1348
|
// compiler-visible so the native-only gate can name the exact unmapped law.
|
|
1024
|
-
const remappedNativeReads =
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1349
|
+
const remappedNativeReads =
|
|
1350
|
+
invariant.__nomosReadBindings === undefined
|
|
1351
|
+
? undefined
|
|
1352
|
+
: remapNativeReads(invariant.__nomosReadBindings, use.step);
|
|
1353
|
+
const { __nomosReadBindings: _privateReads, ...withoutPrivateReads } =
|
|
1354
|
+
invariant;
|
|
1028
1355
|
out.push({
|
|
1029
1356
|
...withoutPrivateReads,
|
|
1030
1357
|
id: `${use.intent.id}__${invariant.id}`,
|
|
@@ -1032,11 +1359,15 @@ export function remapWorkspaceInvariantsForLanguage(
|
|
|
1032
1359
|
...(remappedNativeReads === undefined
|
|
1033
1360
|
? {}
|
|
1034
1361
|
: { __nomosReadBindings: remappedNativeReads }),
|
|
1035
|
-
reads: (ctx) =>
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1362
|
+
reads: (ctx) =>
|
|
1363
|
+
invariant.reads({ ...ctx, intent: mapFacts(ctx.intent) }),
|
|
1364
|
+
assert: (snapshots, ctx) =>
|
|
1365
|
+
invariant.assert(
|
|
1366
|
+
snapshots,
|
|
1367
|
+
ctx === undefined
|
|
1368
|
+
? undefined
|
|
1369
|
+
: { ...ctx, intent: mapFacts(ctx.intent) }
|
|
1370
|
+
),
|
|
1040
1371
|
});
|
|
1041
1372
|
}
|
|
1042
1373
|
}
|