@githolon/dsl 0.103.7 → 0.103.9
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 +11 -0
- package/dart/lib/src/runner/node-entry.mjs +265 -83
- package/dart/lib/src/runner/offline-install.mjs +54 -54
- package/dart/lib/src/webview_bridge.dart +31 -2
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart/test/errors_test.dart +10 -0
- package/dart/test/webview_bridge_test.dart +29 -0
- package/dart_types/CHANGELOG.md +5 -0
- package/dart_types/lib/src/errors.dart +4 -2
- package/dart_types/pubspec.yaml +2 -2
- package/package.json +1 -1
- package/runtime/generated-application-host.js +287 -82
- package/src/codegen_dart_application.ts +10 -4
- package/src/codegen_proof.ts +17 -1
- package/src/codegen_ts.ts +7 -1
- package/src/compile_package_main.ts +27 -11
- package/src/framework/domain_lifecycle.ts +6 -4
- package/src/intent.ts +80 -17
|
@@ -284,7 +284,7 @@ function dartActionOutputs(
|
|
|
284
284
|
const fields = new Set<string>();
|
|
285
285
|
return outputs.map((output) => {
|
|
286
286
|
const dartType = concreteClassName(output.aggregateId);
|
|
287
|
-
const field = member(dartType);
|
|
287
|
+
const field = member(output.many ? plural(dartType) : dartType);
|
|
288
288
|
if (fields.has(field)) {
|
|
289
289
|
throw new Error(
|
|
290
290
|
`Dart application result for '${directive.id}' has two aggregate consequences named '${field}'; ` +
|
|
@@ -1140,7 +1140,11 @@ export function generateDartApplication(
|
|
|
1140
1140
|
` Future<${targetApplication}> _nmsOpenReference(NomosRef<${rootType}> reference) {`,
|
|
1141
1141
|
` final workspace = NmsInternalNomosRefs.workspace(reference);`,
|
|
1142
1142
|
` final existing = _nmsApplications[workspace];`,
|
|
1143
|
-
` if (existing != null)
|
|
1143
|
+
` if (existing != null) {`,
|
|
1144
|
+
` return NmsInternalApplicationHost.nmsInternalAwaitReferencedApplication(`,
|
|
1145
|
+
` workspace: workspace, opening: existing,`,
|
|
1146
|
+
` );`,
|
|
1147
|
+
` }`,
|
|
1144
1148
|
` late final Future<${targetApplication}> opened;`,
|
|
1145
1149
|
` opened = (() async {`,
|
|
1146
1150
|
` try {`,
|
|
@@ -1153,7 +1157,9 @@ export function generateDartApplication(
|
|
|
1153
1157
|
` }`,
|
|
1154
1158
|
` })();`,
|
|
1155
1159
|
` _nmsApplications[workspace] = opened;`,
|
|
1156
|
-
` return
|
|
1160
|
+
` return NmsInternalApplicationHost.nmsInternalAwaitReferencedApplication(`,
|
|
1161
|
+
` workspace: workspace, opening: opened,`,
|
|
1162
|
+
` );`,
|
|
1157
1163
|
` }`,
|
|
1158
1164
|
``,
|
|
1159
1165
|
` /// Open the workspace named by a typed ${rootType} custody reference.`,
|
|
@@ -2080,7 +2086,7 @@ export function generateDartApplication(
|
|
|
2080
2086
|
` Widget Function(BuildContext context, Object error, VoidCallback retry)? errorBuilder,`,
|
|
2081
2087
|
` ApplicationModelChange? onModelChange,`,
|
|
2082
2088
|
` ValueChanged<NomosApplicationStartupState>? onStartupStateChanged,`,
|
|
2083
|
-
` Duration startupTimeout = const Duration(
|
|
2089
|
+
` Duration startupTimeout = const Duration(minutes: 3),`,
|
|
2084
2090
|
` }) => NmsInternalApplicationHost.home<${application}>(`,
|
|
2085
2091
|
` key: key, cloud: cloud, application: application, signIn: signIn,`,
|
|
2086
2092
|
` bind: nomosInternalBind${application}, builder: builder,`,
|
package/src/codegen_proof.ts
CHANGED
|
@@ -3338,7 +3338,14 @@ function generateSealedApplicationProof(
|
|
|
3338
3338
|
` if (!response.ok || !body.token) fail("the generated application could not attest its private device: " + (body.error || response.status));`,
|
|
3339
3339
|
` return body.token;`,
|
|
3340
3340
|
`};`,
|
|
3341
|
-
`const
|
|
3341
|
+
`const performanceEnabled = process.env.NOMOS_PROOF_PERFORMANCE === "1";`,
|
|
3342
|
+
`const reportPerformance = performanceEnabled`,
|
|
3343
|
+
` ? (sample: { readonly operation: string; readonly name: string; readonly durationMs: number; readonly domain?: string; readonly directive?: string }) =>`,
|
|
3344
|
+
` console.error("[nomos-application-performance] " + JSON.stringify(sample))`,
|
|
3345
|
+
` : undefined;`,
|
|
3346
|
+
`const coldOpenStarted = performance.now();`,
|
|
3347
|
+
`const application = await ${opener}({ cloud, application: applicationName, signIn, onPerformance: reportPerformance });`,
|
|
3348
|
+
`if (reportPerformance) reportPerformance({ operation: "open", name: "generated application ready (cold)", durationMs: performance.now() - coldOpenStarted });`,
|
|
3342
3349
|
`ok("opened the generated ${applicationName}; identity, custody, recovery and sync stay inside Nomos");`,
|
|
3343
3350
|
`const waitUntilSaved = async (): Promise<void> => {`,
|
|
3344
3351
|
` const current = await application.currentStatus();`,
|
|
@@ -3467,6 +3474,15 @@ function generateSealedApplicationProof(
|
|
|
3467
3474
|
`const _proofDisposable = application as unknown as { [Symbol.asyncDispose]?: () => Promise<void> };`,
|
|
3468
3475
|
`const _proofDispose = _proofDisposable[Symbol.asyncDispose];`,
|
|
3469
3476
|
`if (_proofDispose) await Promise.race([_proofDispose(), new Promise<void>((resolve) => setTimeout(resolve, 1_000))]);`,
|
|
3477
|
+
`if (performanceEnabled) {`,
|
|
3478
|
+
` const warmOpenStarted = performance.now();`,
|
|
3479
|
+
` const warmApplication = await ${opener}({ cloud, application: applicationName, signIn, onPerformance: reportPerformance });`,
|
|
3480
|
+
` reportPerformance?.({ operation: "open", name: "generated application ready (warm)", durationMs: performance.now() - warmOpenStarted });`,
|
|
3481
|
+
` const warmStatus = await warmApplication.currentStatus();`,
|
|
3482
|
+
` if (warmStatus.phase === "attention") fail(warmStatus.message ?? "the warm application needs attention");`,
|
|
3483
|
+
` const warmDisposable = warmApplication as unknown as { [Symbol.asyncDispose]?: () => Promise<void> };`,
|
|
3484
|
+
` if (warmDisposable[Symbol.asyncDispose]) await Promise.race([warmDisposable[Symbol.asyncDispose]!(), new Promise<void>((resolve) => setTimeout(resolve, 1_000))]);`,
|
|
3485
|
+
`}`,
|
|
3470
3486
|
`process.exit(0);`,
|
|
3471
3487
|
``,
|
|
3472
3488
|
);
|
package/src/codegen_ts.ts
CHANGED
|
@@ -3920,6 +3920,10 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
3920
3920
|
),
|
|
3921
3921
|
`type _${applicationName}Business = ${applicationType};`,
|
|
3922
3922
|
`export type ${applicationName}Status = _NomosApplicationStatus;`,
|
|
3923
|
+
`export interface ${applicationName}PerformanceSample {`,
|
|
3924
|
+
` readonly operation: "open" | "offer" | "birth" | "read" | "sync" | "maintenance";`,
|
|
3925
|
+
` readonly name: string; readonly durationMs: number; readonly domain?: string; readonly directive?: string;`,
|
|
3926
|
+
`}`,
|
|
3923
3927
|
`export type ${applicationName} = _${applicationName}Business & _NomosApplicationParticipant & _NomosApplicationLifecycle & _NomosApplicationModelLifecycle${hasInvitations ? " & _NomosApplicationInvitations" : ""};`,
|
|
3924
3928
|
`export interface ${applicationName}OpenOptions {`,
|
|
3925
3929
|
` /** Nomos Cloud serving this deployed application. Configure it once at the app boundary. */`,
|
|
@@ -3928,6 +3932,8 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
3928
3932
|
` readonly application: string;`,
|
|
3929
3933
|
` /** Return the signed-in authority credential; when supplied, bind its opaque device challenge. */`,
|
|
3930
3934
|
` readonly signIn: (request?: { readonly deviceChallenge?: string }) => Promise<string>;`,
|
|
3935
|
+
` /** Typed timings for developer tooling; Nomos routing, credentials and Git custody remain private. */`,
|
|
3936
|
+
` readonly onPerformance?: (sample: ${applicationName}PerformanceSample) => void;`,
|
|
3931
3937
|
`}`,
|
|
3932
3938
|
`function _nomosPublicDomain<T extends Record<string, unknown>>(domain: T): _NomosPublicDomain<T> {`,
|
|
3933
3939
|
` const application: Record<string, unknown> = {};`,
|
|
@@ -4055,7 +4061,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
4055
4061
|
` * or parallel cache around it. Nomos owns offline custody and synchronization. */`,
|
|
4056
4062
|
`export function ${applicationOpener}(options: ${applicationName}OpenOptions): Promise<${applicationName}> {`,
|
|
4057
4063
|
` return nomosInternalOpenGeneratedApplication(`,
|
|
4058
|
-
` { cloud: options.cloud, application: options.application, signIn: options.signIn, lawHash: ${hashConst} },`,
|
|
4064
|
+
` { cloud: options.cloud, application: options.application, signIn: options.signIn, onPerformance: options.onPerformance, lawHash: ${hashConst} },`,
|
|
4059
4065
|
` (holon, user) => _nomosBind${applicationName}(holon as NomosHolon, user),`,
|
|
4060
4066
|
` );`,
|
|
4061
4067
|
`}`,
|
|
@@ -197,6 +197,10 @@ const DSL_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..")
|
|
|
197
197
|
* here so a generated offer can never silently fall back to initializing the whole tenant domain.
|
|
198
198
|
*/
|
|
199
199
|
const BUSINESS_BIRTH_FRAMEWORK_PLAN_SOURCES: Readonly<Record<string, PlanSourceSliceTarget>> = {
|
|
200
|
+
seedBirthPolicy: {
|
|
201
|
+
modulePath: path.join(DSL_DIR, "src", "framework", "birth_meter.ts"),
|
|
202
|
+
exportName: "seedBirthPolicy",
|
|
203
|
+
},
|
|
200
204
|
enrollSigner: {
|
|
201
205
|
modulePath: path.join(DSL_DIR, "src", "framework", "signing_identity.ts"),
|
|
202
206
|
exportName: "enrollSigner",
|
|
@@ -1642,12 +1646,12 @@ const dartTypesImportFor = (generatedSource: string): string => {
|
|
|
1642
1646
|
* just the floor generated code needs — a lagging pin forces tenants into dependency_overrides. */
|
|
1643
1647
|
// 0.3.0: SurfacedValue<T>/SurfacedObservation/PendingConflictRow — generated Surfaced-field typing +
|
|
1644
1648
|
// the pendingConflicts read rows.
|
|
1645
|
-
const NOMOS_TYPES_DEP = "^0.8.
|
|
1649
|
+
const NOMOS_TYPES_DEP = "^0.8.4";
|
|
1646
1650
|
// 0.48.x: session-bound clients (XClient.bind/NomosClientInterface/DomainClientSessionMismatch/
|
|
1647
1651
|
// DomainNotInstalledForSession, engine.follow, pendingConflicts lanes) — the generated client
|
|
1648
1652
|
// REQUIRES this surface. 0.48.1 fixes NomosSession.watchPendingConflicts delegation.
|
|
1649
|
-
const NOMOS_CLIENT_DEP = "^0.64.
|
|
1650
|
-
const NOMOS_FLUTTER_DEP = "^0.63.
|
|
1653
|
+
const NOMOS_CLIENT_DEP = "^0.64.8";
|
|
1654
|
+
const NOMOS_FLUTTER_DEP = "^0.63.11";
|
|
1651
1655
|
// DRIFT TRIPWIRE: pubspec_pins.test.ts asserts these equal the CURRENT versions in dsl/dart/pubspec.yaml
|
|
1652
1656
|
// and dsl/dart_types/pubspec.yaml — a Dart release without this bump fails the suite, so tenants never
|
|
1653
1657
|
// land back in dependency_overrides (the exact co2 report this closes, third occurrence).
|
|
@@ -1917,14 +1921,25 @@ function generatedPackageApplication(
|
|
|
1917
1921
|
` /// Prove, adopt or restore the package law against this workspace's exact history.`,
|
|
1918
1922
|
` ${rootMember.prefix}.NomosModel get model => ${rootMember.member}.model;`,
|
|
1919
1923
|
``,
|
|
1920
|
-
` /// Current durability and convergence
|
|
1921
|
-
`
|
|
1924
|
+
` /// Current durability and convergence for the receiving-group workspace.`,
|
|
1925
|
+
` /// A slow or offline sibling never makes this workspace's readiness ambiguous.`,
|
|
1926
|
+
` ApplicationStatus get currentStatus => NmsInternalApplicationHost.currentStatus(_nmsSession);`,
|
|
1922
1927
|
``,
|
|
1923
|
-
` /// React whenever this
|
|
1924
|
-
` Stream<ApplicationStatus> watchStatus() => NmsInternalApplicationHost.
|
|
1928
|
+
` /// React whenever this receiving group saves, receives work, goes offline or needs attention.`,
|
|
1929
|
+
` Stream<ApplicationStatus> watchStatus() => NmsInternalApplicationHost.watchStatus(_nmsSession);`,
|
|
1925
1930
|
``,
|
|
1926
|
-
` /// Wait until accepted
|
|
1931
|
+
` /// Wait until this receiving group's accepted work is in custody and caught up.`,
|
|
1927
1932
|
` Future<void> whenCurrent({Duration timeout = const Duration(minutes: 2)}) =>`,
|
|
1933
|
+
` NmsInternalApplicationHost.whenCurrent(_nmsSession, timeout: timeout);`,
|
|
1934
|
+
``,
|
|
1935
|
+
` /// Aggregate posture across every child currently owned by this generated application.`,
|
|
1936
|
+
` ApplicationStatus get applicationStatus => NmsInternalApplicationHost.currentApplicationStatus(_nmsSession);`,
|
|
1937
|
+
``,
|
|
1938
|
+
` /// React to aggregate posture across every generated child currently in this application plane.`,
|
|
1939
|
+
` Stream<ApplicationStatus> watchApplicationStatus() => NmsInternalApplicationHost.watchApplicationStatus(_nmsSession);`,
|
|
1940
|
+
``,
|
|
1941
|
+
` /// Explicitly wait for every generated child currently owned by this application.`,
|
|
1942
|
+
` Future<void> whenApplicationCurrent({Duration timeout = const Duration(minutes: 2)}) =>`,
|
|
1928
1943
|
` NmsInternalApplicationHost.whenApplicationCurrent(_nmsSession, timeout: timeout);`,
|
|
1929
1944
|
...(hasInvitations
|
|
1930
1945
|
? [
|
|
@@ -1973,7 +1988,7 @@ function generatedPackageApplication(
|
|
|
1973
1988
|
` Widget Function(BuildContext context, Object error, VoidCallback retry)? errorBuilder,`,
|
|
1974
1989
|
` ApplicationModelChange? onModelChange,`,
|
|
1975
1990
|
` ValueChanged<NomosApplicationStartupState>? onStartupStateChanged,`,
|
|
1976
|
-
` Duration startupTimeout = const Duration(
|
|
1991
|
+
` Duration startupTimeout = const Duration(minutes: 3),`,
|
|
1977
1992
|
` }) => NmsInternalApplicationHost.home<${application}>(`,
|
|
1978
1993
|
` key: key, cloud: cloud, application: application, signIn: signIn,`,
|
|
1979
1994
|
` bind: nomosInternalBind${application}, builder: builder,`,
|
|
@@ -2544,7 +2559,8 @@ function emitDartPackages(
|
|
|
2544
2559
|
"library;",
|
|
2545
2560
|
"",
|
|
2546
2561
|
`export 'package:${packages.types}/${genRel}/${businessSharedFile}';`,
|
|
2547
|
-
`export 'package:nomos_flutter/src/generated_application_host.dart' show ApplicationModelApproval, ApplicationModelDecision, ApplicationModelPolicy, ApplicationModelReview;`,
|
|
2562
|
+
`export 'package:nomos_flutter/src/generated_application_host.dart' show ApplicationModelApproval, ApplicationModelDecision, ApplicationModelPolicy, ApplicationModelReview, NomosApplicationStartupPhase, NomosApplicationStartupState, NomosApplicationStartupTimeout, NomosWorkspaceOpenTimeout;`,
|
|
2563
|
+
`export 'package:nomos_types/nomos_types.dart' show CustodyError, CustodyPhase, CustodySource, NomosRecommendedAction;`,
|
|
2548
2564
|
...tenantTypesExportKeys
|
|
2549
2565
|
.map((file) => {
|
|
2550
2566
|
const names = tenantDartExportNames(typesSources.get(file) ?? "");
|
|
@@ -5161,7 +5177,7 @@ async function main(): Promise<void> {
|
|
|
5161
5177
|
const temporaryDeclarationPath = temporaryClientPath.replace(/\.ts$/, ".d.ts");
|
|
5162
5178
|
const runtimeTypesSource = [
|
|
5163
5179
|
`export declare function nomosInternalOpenGeneratedApplication<T extends object>(`,
|
|
5164
|
-
` options: { cloud: string; application: string; signIn: (request?: { readonly deviceChallenge?: string }) => Promise<string>; lawHash: string },`,
|
|
5180
|
+
` options: { cloud: string; application: string; signIn: (request?: { readonly deviceChallenge?: string }) => Promise<string>; onPerformance?: (sample: { readonly operation: "open" | "offer" | "birth" | "read" | "sync" | "maintenance"; readonly name: string; readonly durationMs: number; readonly domain?: string; readonly directive?: string }) => void; lawHash: string },`,
|
|
5165
5181
|
` bind: (holon: unknown, user: { readonly identityId: string; readonly identityAddress: string }) => T,`,
|
|
5166
5182
|
`): Promise<T & {`,
|
|
5167
5183
|
` currentStatus(): Promise<any>;`,
|
|
@@ -294,10 +294,12 @@ export const stageDomain = directive(DOMAIN_LIFECYCLE_DIRECTIVES.Stage)
|
|
|
294
294
|
})
|
|
295
295
|
.requires(MODEL_LIFECYCLE_AUTHORITY);
|
|
296
296
|
|
|
297
|
-
/** Make a staged package current. The package bytes are already
|
|
298
|
-
* re-loads them from prefix state and runs the evolve gate on
|
|
299
|
-
*
|
|
300
|
-
* their declared era checked; the newer activation stamp alone
|
|
297
|
+
/** Make a staged or retained Active package current. The package bytes are already
|
|
298
|
+
* committed; the kernel re-loads them from prefix state and runs the evolve gate on
|
|
299
|
+
* this governed transition. Prior versions stay Active by default so gradual-rollout
|
|
300
|
+
* clients can still have their declared era checked; the newer activation stamp alone
|
|
301
|
+
* makes this law current. Re-promoting a shadowed Active package is therefore the lawful,
|
|
302
|
+
* history-proved inverse of a later package taking its domain keys. */
|
|
301
303
|
export const promoteDomain = directive(DOMAIN_LIFECYCLE_DIRECTIVES.Promote)
|
|
302
304
|
.mutates(DomainInstallation)
|
|
303
305
|
.payload(z.object({
|
package/src/intent.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import type { z } from "zod";
|
|
10
10
|
|
|
11
11
|
import type {
|
|
12
|
+
BusinessSourceTrust,
|
|
12
13
|
Directive,
|
|
13
14
|
PreconditionDescriptor,
|
|
14
15
|
ReferentialMarker,
|
|
@@ -233,11 +234,38 @@ function unsupportedKernelGate(directive: Directive<any>): string | undefined {
|
|
|
233
234
|
if (directive.delegatedRoleGate !== undefined) return "delegated authority";
|
|
234
235
|
if (directive.firstDeviceEnrollGate !== undefined) return "first-device enrollment";
|
|
235
236
|
if (directive.certifiesEraGate !== undefined) return "era certification";
|
|
236
|
-
if (directive.businessSourceTrust !== undefined) return "source-trust preflight";
|
|
237
237
|
if ((directive.businessRecordInputs?.length ?? 0) > 0) return "record-input binding";
|
|
238
238
|
return undefined;
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
+
function remapBusinessSourceTrust<I>(
|
|
242
|
+
step: IntentStep<I, any>,
|
|
243
|
+
trust: BusinessSourceTrust,
|
|
244
|
+
): BusinessSourceTrust {
|
|
245
|
+
const field = (privatePath: string): string => {
|
|
246
|
+
const mapped = remapStepPayloadPath(step, privatePath.split("."));
|
|
247
|
+
if (mapped === undefined || mapped.length === 0) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
`compound intent cannot remap source-trust field '${privatePath}' from '${step.directive.id}'`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
return mapped.join(".");
|
|
253
|
+
};
|
|
254
|
+
return {
|
|
255
|
+
viaDirectiveId: trust.viaDirectiveId,
|
|
256
|
+
fromPayload: field(trust.fromPayload),
|
|
257
|
+
queryId: trust.queryId,
|
|
258
|
+
queryArgs: Object.fromEntries(
|
|
259
|
+
Object.entries(trust.queryArgs).map(([target, source]) => [target, field(source)]),
|
|
260
|
+
),
|
|
261
|
+
copy: Object.fromEntries(
|
|
262
|
+
Object.entries(trust.copy).map(([target, source]) => [target, field(source)]),
|
|
263
|
+
),
|
|
264
|
+
genesisField: trust.genesisField,
|
|
265
|
+
keyHashesField: trust.keyHashesField,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
241
269
|
const dedupeBy = <T>(values: readonly T[], key: (value: T) => string): T[] => {
|
|
242
270
|
const out: T[] = [];
|
|
243
271
|
const seen = new Set<string>();
|
|
@@ -333,24 +361,44 @@ class IntentBuilder<I> {
|
|
|
333
361
|
|
|
334
362
|
const primary = this.steps.find((step) => step.directive.birthsChild !== true)?.directive
|
|
335
363
|
?? this.steps[0]!.directive;
|
|
336
|
-
const
|
|
337
|
-
for (const
|
|
364
|
+
const allOutputs: SecondaryOutput[] = [];
|
|
365
|
+
for (const step of this.steps) {
|
|
338
366
|
const candidate = step.directive;
|
|
339
|
-
if (
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
367
|
+
if (candidate.birthsChild !== true) {
|
|
368
|
+
allOutputs.push({
|
|
369
|
+
aggregateId: candidate.aggregateId,
|
|
370
|
+
marker: candidate.marker,
|
|
371
|
+
...(candidate.outputCardinality === "many" ? { many: true } : {}),
|
|
372
|
+
...(candidate.outputCardinality === "optional" ? { optional: true } : {}),
|
|
373
|
+
});
|
|
374
|
+
allOutputs.push(...(candidate.secondaryOutputs ?? []));
|
|
345
375
|
}
|
|
346
|
-
if (candidate.birthsChild !== true) outputs.push(...(candidate.secondaryOutputs ?? []));
|
|
347
376
|
}
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
),
|
|
352
|
-
|
|
377
|
+
const outputGroups = new Map<string, SecondaryOutput[]>();
|
|
378
|
+
for (const output of allOutputs) {
|
|
379
|
+
const key = `${output.marker}\0${output.aggregateId}`;
|
|
380
|
+
outputGroups.set(key, [...(outputGroups.get(key) ?? []), output]);
|
|
381
|
+
}
|
|
382
|
+
const composedOutput = (group: readonly SecondaryOutput[]): SecondaryOutput => ({
|
|
383
|
+
aggregateId: group[0]!.aggregateId,
|
|
384
|
+
marker: group[0]!.marker,
|
|
385
|
+
...(group.length > 1 || group.some((output) => output.many === true)
|
|
386
|
+
? { many: true }
|
|
387
|
+
: group[0]!.optional === true
|
|
388
|
+
? { optional: true }
|
|
389
|
+
: {}),
|
|
390
|
+
});
|
|
391
|
+
const primaryOutput = composedOutput(
|
|
392
|
+
outputGroups.get(`${primary.marker}\0${primary.aggregateId}`) ?? [{
|
|
393
|
+
aggregateId: primary.aggregateId,
|
|
394
|
+
marker: primary.marker,
|
|
395
|
+
}],
|
|
353
396
|
);
|
|
397
|
+
const secondaryOutputs = [...outputGroups.values()]
|
|
398
|
+
.map(composedOutput)
|
|
399
|
+
.filter(
|
|
400
|
+
(output) => !(output.aggregateId === primary.aggregateId && output.marker === primary.marker),
|
|
401
|
+
);
|
|
354
402
|
const inferredReturns = [
|
|
355
403
|
primary.aggregateId,
|
|
356
404
|
...secondaryOutputs.map((output) => output.aggregateId),
|
|
@@ -421,6 +469,16 @@ class IntentBuilder<I> {
|
|
|
421
469
|
}
|
|
422
470
|
}
|
|
423
471
|
const relationGate = oneStaticRelationGate(this.steps);
|
|
472
|
+
const sourceTrusts = this.steps.flatMap((step) =>
|
|
473
|
+
step.directive.businessSourceTrust === undefined
|
|
474
|
+
? []
|
|
475
|
+
: [remapBusinessSourceTrust(step, step.directive.businessSourceTrust)],
|
|
476
|
+
);
|
|
477
|
+
if (sourceTrusts.length > 1) {
|
|
478
|
+
throw new Error(
|
|
479
|
+
`intent('${this.id}') composes more than one source-trust preflight — split the business action by source`,
|
|
480
|
+
);
|
|
481
|
+
}
|
|
424
482
|
const steps = [...this.steps];
|
|
425
483
|
const plan = (facts: I, ctx: Ports): PlannedOp[] => {
|
|
426
484
|
const returned: PlannedOp[] = [];
|
|
@@ -455,8 +513,10 @@ class IntentBuilder<I> {
|
|
|
455
513
|
// engine cannot rebuild its aggregate registry entry and silently drops
|
|
456
514
|
// compound intents that also birth a child.
|
|
457
515
|
...(primary.aggregate === undefined ? {} : { aggregate: primary.aggregate }),
|
|
458
|
-
...(
|
|
459
|
-
? { outputCardinality:
|
|
516
|
+
...(primaryOutput.many === true
|
|
517
|
+
? { outputCardinality: "many" as const }
|
|
518
|
+
: primaryOutput.optional === true
|
|
519
|
+
? { outputCardinality: "optional" as const }
|
|
460
520
|
: {}),
|
|
461
521
|
...(secondaryOutputs.length > 0 ? { secondaryOutputs } : {}),
|
|
462
522
|
payloadSchema: (birthSteps[0]?.directive.payloadSchema ?? this.contractDeclaration.accepts) as unknown as z.ZodType<I>,
|
|
@@ -475,6 +535,9 @@ class IntentBuilder<I> {
|
|
|
475
535
|
].sort(),
|
|
476
536
|
...(preconditions.length > 0 ? { preconditions } : {}),
|
|
477
537
|
...(relationGate !== undefined ? { requiresRelationGate: relationGate } : {}),
|
|
538
|
+
...(sourceTrusts[0] === undefined
|
|
539
|
+
? {}
|
|
540
|
+
: { businessSourceTrust: sourceTrusts[0] }),
|
|
478
541
|
...(businessBirth === undefined
|
|
479
542
|
? {}
|
|
480
543
|
: {
|