@fougere/core 0.12.1-alpha.0 → 0.12.4-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CallIdentity.d.ts.map +1 -1
- package/dist/CallIdentity.js.map +1 -1
- package/dist/DomainCluster.d.ts.map +1 -1
- package/dist/DomainCluster.js +1 -1
- package/dist/DomainCluster.js.map +1 -1
- package/dist/EffectiveOperationModel.d.ts +1 -4
- package/dist/EffectiveOperationModel.d.ts.map +1 -1
- package/dist/EffectiveOperationModel.js +182 -173
- package/dist/EffectiveOperationModel.js.map +1 -1
- package/dist/FougereConfig.d.ts +5 -3
- package/dist/FougereConfig.d.ts.map +1 -1
- package/dist/FougereConfig.js +11 -13
- package/dist/FougereConfig.js.map +1 -1
- package/dist/FrondConfig.d.ts.map +1 -1
- package/dist/FrondConfig.js.map +1 -1
- package/dist/FrondDeclaration.d.ts.map +1 -1
- package/dist/FrondDeclaration.js +1 -5
- package/dist/FrondDeclaration.js.map +1 -1
- package/dist/Source.js +3 -3
- package/dist/Source.js.map +1 -1
- package/dist/SourceView.d.ts +0 -2
- package/dist/SourceView.d.ts.map +1 -1
- package/dist/boot/App.d.ts +2 -4
- package/dist/boot/App.d.ts.map +1 -1
- package/dist/boot/AppLifecycle.d.ts +5 -0
- package/dist/boot/AppLifecycle.d.ts.map +1 -1
- package/dist/boot/AppLifecycle.js +19 -10
- package/dist/boot/AppLifecycle.js.map +1 -1
- package/dist/boot/AuthRuntime.d.ts +5 -12
- package/dist/boot/AuthRuntime.d.ts.map +1 -1
- package/dist/boot/AuthRuntime.js +2 -1
- package/dist/boot/AuthRuntime.js.map +1 -1
- package/dist/boot/CreateAppOptions.d.ts +0 -11
- package/dist/boot/CreateAppOptions.d.ts.map +1 -1
- package/dist/boot/Emissions.d.ts.map +1 -1
- package/dist/boot/Emissions.js +3 -6
- package/dist/boot/Emissions.js.map +1 -1
- package/dist/boot/Hosting.d.ts +2 -1
- package/dist/boot/Hosting.d.ts.map +1 -1
- package/dist/boot/ambient.als.d.ts.map +1 -1
- package/dist/boot/ambient.als.js.map +1 -1
- package/dist/boot/apply.d.ts.map +1 -1
- package/dist/boot/apply.js.map +1 -1
- package/dist/boot/arity.d.ts +13 -0
- package/dist/boot/arity.d.ts.map +1 -0
- package/dist/boot/arity.js +33 -0
- package/dist/boot/arity.js.map +1 -0
- package/dist/boot/bootstrap.d.ts.map +1 -1
- package/dist/boot/bootstrap.js +143 -265
- package/dist/boot/bootstrap.js.map +1 -1
- package/dist/boot/card.d.ts.map +1 -1
- package/dist/boot/card.js.map +1 -1
- package/dist/boot/frame.d.ts.map +1 -1
- package/dist/boot/frame.js.map +1 -1
- package/dist/boot/install.d.ts.map +1 -1
- package/dist/boot/install.js +105 -115
- package/dist/boot/install.js.map +1 -1
- package/dist/boot/ownership.d.ts.map +1 -1
- package/dist/boot/ownership.js.map +1 -1
- package/dist/boot/relations.d.ts +10 -0
- package/dist/boot/relations.d.ts.map +1 -1
- package/dist/boot/relations.js +35 -2
- package/dist/boot/relations.js.map +1 -1
- package/dist/boot/remote.d.ts.map +1 -1
- package/dist/boot/remote.js.map +1 -1
- package/dist/boot/seed.d.ts.map +1 -1
- package/dist/boot/seed.js +1 -1
- package/dist/boot/seed.js.map +1 -1
- package/dist/boot/statement-drift.d.ts +6 -0
- package/dist/boot/statement-drift.d.ts.map +1 -1
- package/dist/boot/statement-drift.js +19 -0
- package/dist/boot/statement-drift.js.map +1 -1
- package/dist/boot/together.d.ts.map +1 -1
- package/dist/boot/together.js.map +1 -1
- package/dist/builtin/Carry.d.ts.map +1 -1
- package/dist/builtin/Carry.js.map +1 -1
- package/dist/builtin/Logger.d.ts.map +1 -1
- package/dist/builtin/Logger.js +3 -1
- package/dist/builtin/Logger.js.map +1 -1
- package/dist/builtin/config.js.map +1 -1
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js.map +1 -1
- package/dist/crypto/encoding.d.ts.map +1 -1
- package/dist/crypto/encoding.js.map +1 -1
- package/dist/crypto/node.d.ts.map +1 -1
- package/dist/crypto/node.js.map +1 -1
- package/dist/crypto/webcrypto.d.ts.map +1 -1
- package/dist/crypto/webcrypto.js.map +1 -1
- package/dist/descriptor/Fronds.d.ts +5 -0
- package/dist/descriptor/Fronds.d.ts.map +1 -1
- package/dist/descriptor/Fronds.js +8 -0
- package/dist/descriptor/Fronds.js.map +1 -1
- package/dist/descriptor/index.d.ts +1 -0
- package/dist/descriptor/index.d.ts.map +1 -1
- package/dist/descriptor/index.js +1 -0
- package/dist/descriptor/index.js.map +1 -1
- package/dist/dispatch/ArrayResult.d.ts.map +1 -1
- package/dist/dispatch/ArrayResult.js.map +1 -1
- package/dist/dispatch/Dispatcher.d.ts.map +1 -1
- package/dist/dispatch/Dispatcher.js.map +1 -1
- package/dist/dispatch/HandlerFacade.d.ts +6 -1
- package/dist/dispatch/HandlerFacade.d.ts.map +1 -1
- package/dist/dispatch/HandlerFacade.js +11 -3
- package/dist/dispatch/HandlerFacade.js.map +1 -1
- package/dist/dispatch/InFlight.d.ts +3 -2
- package/dist/dispatch/InFlight.d.ts.map +1 -1
- package/dist/dispatch/InFlight.js +11 -1
- package/dist/dispatch/InFlight.js.map +1 -1
- package/dist/dispatch/LocalRoutePolicy.d.ts.map +1 -1
- package/dist/dispatch/LocalRoutePolicy.js.map +1 -1
- package/dist/dispatch/RouteRegistry.d.ts.map +1 -1
- package/dist/dispatch/RouteRegistry.js.map +1 -1
- package/dist/dispatch/StorageGuard.d.ts.map +1 -1
- package/dist/dispatch/StorageGuard.js +4 -1
- package/dist/dispatch/StorageGuard.js.map +1 -1
- package/dist/identity-keys.d.ts.map +1 -1
- package/dist/identity-keys.js.map +1 -1
- package/dist/index.d.ts +2 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js.map +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +1 -1
- package/dist/node.js.map +1 -1
- package/dist/prefab/CrudConstructor.d.ts.map +1 -1
- package/dist/prefab/CrudConstructor.js.map +1 -1
- package/dist/prefab/collector.d.ts.map +1 -1
- package/dist/prefab/collector.js.map +1 -1
- package/dist/prefab/presenter.d.ts.map +1 -1
- package/dist/prefab/presenter.js.map +1 -1
- package/dist/storage/Comparison.d.ts +2 -2
- package/dist/storage/Comparison.d.ts.map +1 -1
- package/dist/storage/Comparison.js +3 -1
- package/dist/storage/Comparison.js.map +1 -1
- package/dist/storage/Storage.d.ts.map +1 -1
- package/dist/storage/Storage.js.map +1 -1
- package/dist/storage/Store.d.ts.map +1 -1
- package/dist/storage/Store.js +1 -1
- package/dist/storage/Store.js.map +1 -1
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js.map +1 -1
- package/dist/wire/AppMiddleware.d.ts.map +1 -1
- package/dist/wire/AppMiddleware.js.map +1 -1
- package/dist/wire/Emit.d.ts +0 -2
- package/dist/wire/Emit.d.ts.map +1 -1
- package/dist/wire/Emit.js +1 -1
- package/dist/wire/Emit.js.map +1 -1
- package/dist/wire/Facade.d.ts +7 -0
- package/dist/wire/Facade.d.ts.map +1 -1
- package/dist/wire/Facade.js +13 -0
- package/dist/wire/Facade.js.map +1 -1
- package/dist/wire/FougereError.d.ts.map +1 -1
- package/dist/wire/FougereError.js.map +1 -1
- package/dist/wire/Invocation.d.ts.map +1 -1
- package/dist/wire/Invocation.js.map +1 -1
- package/dist/wire/OperationContract.d.ts +0 -4
- package/dist/wire/OperationContract.d.ts.map +1 -1
- package/dist/wire/OperationContract.js +1 -1
- package/dist/wire/OperationContract.js.map +1 -1
- package/dist/wire/OperationsMap.d.ts +0 -3
- package/dist/wire/OperationsMap.d.ts.map +1 -1
- package/dist/wire/OperationsMap.js +0 -5
- package/dist/wire/OperationsMap.js.map +1 -1
- package/dist/wire/RouteAddress.d.ts.map +1 -1
- package/dist/wire/RouteAddress.js.map +1 -1
- package/dist/wire/binding.d.ts.map +1 -1
- package/dist/wire/binding.js.map +1 -1
- package/dist/wire/card/IdentityCard.d.ts.map +1 -1
- package/dist/wire/card/IdentityCard.js.map +1 -1
- package/dist/wire/drift.d.ts.map +1 -1
- package/dist/wire/drift.js.map +1 -1
- package/dist/wire/http-error.d.ts.map +1 -1
- package/dist/wire/http-error.js.map +1 -1
- package/dist/wire/loggerMiddleware.d.ts.map +1 -1
- package/dist/wire/loggerMiddleware.js.map +1 -1
- package/package.json +5 -5
- package/src/CallIdentity.ts +6 -0
- package/src/DomainCluster.ts +2 -1
- package/src/EffectiveOperationModel.ts +236 -198
- package/src/FougereConfig.ts +17 -16
- package/src/FrondConfig.ts +2 -0
- package/src/FrondDeclaration.ts +1 -7
- package/src/Source.ts +3 -3
- package/src/SourceView.ts +0 -2
- package/src/boot/App.ts +2 -4
- package/src/boot/AppLifecycle.ts +22 -10
- package/src/boot/AuthRuntime.ts +6 -13
- package/src/boot/CreateAppOptions.ts +0 -11
- package/src/boot/Emissions.ts +7 -7
- package/src/boot/Hosting.ts +2 -1
- package/src/boot/ambient.als.ts +2 -0
- package/src/boot/apply.ts +1 -0
- package/src/boot/arity.ts +42 -0
- package/src/boot/bootstrap.ts +175 -300
- package/src/boot/card.ts +2 -0
- package/src/boot/frame.ts +6 -0
- package/src/boot/install.ts +123 -146
- package/src/boot/ownership.ts +1 -0
- package/src/boot/relations.ts +42 -10
- package/src/boot/remote.ts +1 -0
- package/src/boot/seed.ts +3 -5
- package/src/boot/statement-drift.ts +25 -0
- package/src/boot/together.ts +3 -0
- package/src/builtin/Carry.ts +1 -0
- package/src/builtin/Logger.ts +9 -1
- package/src/builtin/config.ts +1 -0
- package/src/contract.ts +1 -0
- package/src/crypto/encoding.ts +4 -0
- package/src/crypto/node.ts +2 -0
- package/src/crypto/webcrypto.ts +2 -0
- package/src/descriptor/Fronds.ts +12 -0
- package/src/descriptor/index.ts +1 -0
- package/src/dispatch/ArrayResult.ts +1 -0
- package/src/dispatch/Dispatcher.ts +1 -0
- package/src/dispatch/HandlerFacade.ts +16 -4
- package/src/dispatch/InFlight.ts +18 -2
- package/src/dispatch/LocalRoutePolicy.ts +1 -0
- package/src/dispatch/RouteRegistry.ts +4 -0
- package/src/dispatch/StorageGuard.ts +9 -1
- package/src/identity-keys.ts +2 -0
- package/src/index.ts +2 -4
- package/src/loader.ts +1 -0
- package/src/node.ts +1 -1
- package/src/prefab/CrudConstructor.ts +1 -11
- package/src/prefab/collector.ts +1 -0
- package/src/prefab/presenter.ts +1 -0
- package/src/storage/Comparison.ts +7 -4
- package/src/storage/Storage.ts +2 -0
- package/src/storage/Store.ts +9 -1
- package/src/verify.ts +2 -0
- package/src/wire/AppMiddleware.ts +2 -0
- package/src/wire/Emit.ts +1 -1
- package/src/wire/Facade.ts +17 -10
- package/src/wire/FougereError.ts +2 -0
- package/src/wire/Invocation.ts +2 -0
- package/src/wire/OperationContract.ts +2 -1
- package/src/wire/OperationsMap.ts +0 -10
- package/src/wire/RouteAddress.ts +2 -0
- package/src/wire/binding.ts +1 -0
- package/src/wire/card/IdentityCard.ts +1 -0
- package/src/wire/drift.ts +3 -0
- package/src/wire/http-error.ts +2 -0
- package/src/wire/loggerMiddleware.ts +1 -0
- package/dist/boot/AuthConfig.d.ts +0 -14
- package/dist/boot/AuthConfig.d.ts.map +0 -1
- package/dist/boot/AuthConfig.js +0 -2
- package/dist/boot/AuthConfig.js.map +0 -1
- package/dist/boot/AuthContext.d.ts +0 -12
- package/dist/boot/AuthContext.d.ts.map +0 -1
- package/dist/boot/AuthContext.js +0 -2
- package/dist/boot/AuthContext.js.map +0 -1
- package/dist/storage/ComparisonName.d.ts +0 -3
- package/dist/storage/ComparisonName.d.ts.map +0 -1
- package/dist/storage/ComparisonName.js +0 -2
- package/dist/storage/ComparisonName.js.map +0 -1
- package/src/boot/AuthConfig.ts +0 -14
- package/src/boot/AuthContext.ts +0 -12
- package/src/storage/ComparisonName.ts +0 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { lowerFirst, type SchemaView } from '@fougere/schema';
|
|
2
|
-
import { statementDrift } from './boot/statement-drift.js';
|
|
2
|
+
import { statementDrift, unansweredOperations } from './boot/statement-drift.js';
|
|
3
3
|
import { reachedBy, servedBy } from './boot/declared.js';
|
|
4
4
|
import { computeBindingPlan, type BindingPlan } from './wire/binding.js';
|
|
5
5
|
import { targetOf } from './prefab/prefab.js';
|
|
@@ -21,6 +21,9 @@ import type { EffectiveOperationOptions } from './EffectiveOperationOptions.js';
|
|
|
21
21
|
|
|
22
22
|
type Binding = BindingPlan[number];
|
|
23
23
|
|
|
24
|
+
/** A contract whose parameters are all bound. */
|
|
25
|
+
type BoundContract = OperationContract & { binding: BindingPlan };
|
|
26
|
+
|
|
24
27
|
/**
|
|
25
28
|
* Where an op's work goes: the fronds its handler reaches, and how many are a process away.
|
|
26
29
|
*
|
|
@@ -47,26 +50,30 @@ function reachOf(
|
|
|
47
50
|
* boot never has to find a handler again by an order-dependent name lookup.
|
|
48
51
|
*/
|
|
49
52
|
export class EffectiveOperationModel {
|
|
50
|
-
readonly operations: EffectiveOperation[];
|
|
51
|
-
readonly diagnostics: Diagnostic[];
|
|
52
|
-
readonly resolutionDiagnostics: Diagnostic[];
|
|
53
|
-
|
|
54
53
|
constructor(
|
|
55
|
-
operations: EffectiveOperation[],
|
|
56
|
-
diagnostics: Diagnostic[],
|
|
57
|
-
resolutionDiagnostics: Diagnostic[],
|
|
54
|
+
readonly operations: EffectiveOperation[],
|
|
55
|
+
readonly diagnostics: Diagnostic[],
|
|
56
|
+
readonly resolutionDiagnostics: Diagnostic[],
|
|
58
57
|
private readonly byHandler: Map<HandlerEntry, EffectiveOperationsMap>,
|
|
59
|
-
) {
|
|
60
|
-
this.operations = operations;
|
|
61
|
-
this.diagnostics = diagnostics;
|
|
62
|
-
this.resolutionDiagnostics = resolutionDiagnostics;
|
|
63
|
-
}
|
|
58
|
+
) {}
|
|
64
59
|
|
|
65
60
|
forHandler(handler: HandlerEntry): EffectiveOperationsMap {
|
|
66
61
|
return this.byHandler.get(handler) ?? new Map();
|
|
67
62
|
}
|
|
68
63
|
}
|
|
69
64
|
|
|
65
|
+
/** What one handler's operations are resolved against. */
|
|
66
|
+
interface Resolving {
|
|
67
|
+
fronds: readonly FrondDescriptor[];
|
|
68
|
+
frond: FrondDescriptor;
|
|
69
|
+
handler: HandlerEntry;
|
|
70
|
+
schemas: Map<string, SchemaView>;
|
|
71
|
+
served: Map<string, string>;
|
|
72
|
+
collectorsByType: Map<string, CollectorEntry[]>;
|
|
73
|
+
options: EffectiveOperationOptions;
|
|
74
|
+
diagnostics: Diagnostic[];
|
|
75
|
+
}
|
|
76
|
+
|
|
70
77
|
/** Resolve the complete operation model without starting the application. */
|
|
71
78
|
export function resolveEffectiveOperations(
|
|
72
79
|
fronds: readonly FrondDescriptor[],
|
|
@@ -74,211 +81,41 @@ export function resolveEffectiveOperations(
|
|
|
74
81
|
): EffectiveOperationModel {
|
|
75
82
|
const operations: EffectiveOperation[] = [];
|
|
76
83
|
const byHandler = new Map<HandlerEntry, EffectiveOperationsMap>();
|
|
77
|
-
const resolutionDiagnostics: Diagnostic[] = [];
|
|
78
84
|
const scanDiagnostics = [...(options.diagnostics ?? [])];
|
|
79
85
|
const schemas = new Map(
|
|
80
86
|
fronds.flatMap((frond) => frond.entities.map((entity) => [entity.name, entity.entityClass] as const)),
|
|
81
87
|
);
|
|
82
|
-
/** Every address a frond answers, so a dependency naming one is read as the crossing it is. */
|
|
83
88
|
const served = servedBy(fronds);
|
|
84
89
|
|
|
85
|
-
// Input ambiguity is
|
|
86
|
-
|
|
87
|
-
resolutionDiagnostics.push(...scanDiagnostics.filter((diagnostic) =>
|
|
88
|
-
diagnostic.code === 'input-contract-ambiguous'));
|
|
90
|
+
// Input ambiguity is found by the scanner, where schemas are, but it is a resolution failure.
|
|
91
|
+
const diagnostics = scanDiagnostics.filter((diagnostic) => diagnostic.code === 'input-contract-ambiguous');
|
|
89
92
|
|
|
90
93
|
for (const frond of fronds) {
|
|
91
94
|
const collectorsByType = groupedCollectors(frond.collectors);
|
|
92
|
-
|
|
93
|
-
if (collectors.length < 2) continue;
|
|
94
|
-
resolutionDiagnostics.push({
|
|
95
|
-
severity: 'blocking',
|
|
96
|
-
code: 'collector-ambiguous',
|
|
97
|
-
filePath: collectors[0]!.filePath,
|
|
98
|
-
frond: frond.name,
|
|
99
|
-
subject: typeName,
|
|
100
|
-
message: `Frond '${frond.name}' declares ${collectors.length} collectors for '${typeName}' — `
|
|
101
|
-
+ `${collectors.map((collector) => collector.ctor.name).sort().join(', ')}. `
|
|
102
|
-
+ 'Exactly one collector must own a parameter type.',
|
|
103
|
-
});
|
|
104
|
-
}
|
|
95
|
+
refuseAmbiguousCollectors(frond, collectorsByType, diagnostics);
|
|
105
96
|
|
|
106
97
|
const collectorNames = new Set(frond.collectors.map((collector) => collector.typeName));
|
|
107
98
|
for (const handler of frond.handlers) {
|
|
108
99
|
const effective = new Map<string, EffectiveOperation>();
|
|
109
100
|
byHandler.set(handler, effective);
|
|
110
101
|
const contracts = resolveContracts(handler, frond.operationsOverrides, collectorNames);
|
|
111
|
-
// A statement wins over the scan on purpose; saying so
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
if (!
|
|
119
|
-
|
|
120
|
-
// An operation answers DATA — what JSON keeps. A DECLARED output is converted by its
|
|
121
|
-
// fields, so `created()` leaves as an ISO string and nothing is asked of it here; an
|
|
122
|
-
// undeclared one has nothing to convert it, so it must already be data. Measured
|
|
123
|
-
// 2026-09-18: `{ at: new Date(0) }` reaches a local caller as a `Date` and a remote one
|
|
124
|
-
// as a string, and an object carrying methods loses them on both sides, differently.
|
|
125
|
-
if (!contract.output && contract.signature?.notData) {
|
|
126
|
-
resolutionDiagnostics.push({
|
|
127
|
-
severity: 'blocking',
|
|
128
|
-
code: 'operation-output-not-data',
|
|
129
|
-
filePath: handler.filePath,
|
|
130
|
-
frond: frond.name,
|
|
131
|
-
subject,
|
|
132
|
-
message: `${subject}() answers ${contract.signature.notData}, which carries methods.\n`
|
|
133
|
-
+ ' An operation answers data — what JSON keeps.\n'
|
|
134
|
-
+ ' Declare the output as an entity, whose fields convert it, or answer what it produces.',
|
|
135
|
-
});
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const override = frond.operationsOverrides?.[name];
|
|
140
|
-
const inference = inferOperationKind(name);
|
|
141
|
-
const inferredKinds = new Set<OperationKind>();
|
|
142
|
-
if (inference.kind) inferredKinds.add(inference.kind);
|
|
143
|
-
if (inference.queryMatches.length > 0) inferredKinds.add('query');
|
|
144
|
-
if (inference.commandMatches.length > 0) inferredKinds.add('command');
|
|
145
|
-
const kind = override?.kind
|
|
146
|
-
?? (inferredKinds.size === 1 ? [...inferredKinds][0] : undefined);
|
|
147
|
-
if (!kind) {
|
|
148
|
-
const ambiguous = inferredKinds.size > 1;
|
|
149
|
-
resolutionDiagnostics.push({
|
|
150
|
-
severity: 'blocking',
|
|
151
|
-
code: ambiguous ? 'operation-kind-ambiguous' : 'operation-kind-unknown',
|
|
152
|
-
filePath: handler.filePath,
|
|
153
|
-
frond: frond.name,
|
|
154
|
-
subject,
|
|
155
|
-
message: ambiguous
|
|
156
|
-
? `Cannot resolve the kind of ${subject}: its name carries query evidence `
|
|
157
|
-
+ `(${inference.queryMatches.join(', ')}) and command evidence `
|
|
158
|
-
+ `(${inference.commandMatches.join(', ')}). `
|
|
159
|
-
+ `Declare operations.${name}.kind.`
|
|
160
|
-
: `Cannot resolve the kind of ${subject}: '${name}' leads with no known verb. `
|
|
161
|
-
+ `Rename it to lead with one, or declare operations.${name}.kind in `
|
|
162
|
-
+ `frond.config.ts.\n query: ${knownVerbs().query.join(', ')}`
|
|
163
|
-
+ `\n command: ${knownVerbs().command.join(', ')}`,
|
|
164
|
-
});
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const implementation = implementationOf(frond, handler, name, resolutionDiagnostics);
|
|
169
|
-
if (!implementation) continue;
|
|
170
|
-
|
|
171
|
-
const params = contract.signature?.params ?? [];
|
|
172
|
-
const parameters = contract.binding.map((binding, position) => {
|
|
173
|
-
const param = params[position];
|
|
174
|
-
const undefinable = binding.optional
|
|
175
|
-
|| param?.optional === true
|
|
176
|
-
|| param?.type.undefined === true;
|
|
177
|
-
return {
|
|
178
|
-
position,
|
|
179
|
-
name: param?.name ?? binding.name,
|
|
180
|
-
type: param?.type.raw ?? null,
|
|
181
|
-
optional: undefinable,
|
|
182
|
-
nullable: param?.type.nullable === true,
|
|
183
|
-
undefinable,
|
|
184
|
-
binding,
|
|
185
|
-
} satisfies EffectiveParameter;
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
if (!validateProvenance(
|
|
189
|
-
fronds,
|
|
190
|
-
frond,
|
|
191
|
-
handler,
|
|
192
|
-
name,
|
|
193
|
-
contract,
|
|
194
|
-
parameters,
|
|
195
|
-
collectorsByType,
|
|
196
|
-
resolutionDiagnostics,
|
|
197
|
-
)) continue;
|
|
198
|
-
|
|
199
|
-
// A fact is validated by the entity it names. This used to be patched into the
|
|
200
|
-
// facade after resolution, leaving check/explain with a different input.
|
|
201
|
-
const announced = contract.binding.find((binding) =>
|
|
202
|
-
binding.source.kind === 'fact' || binding.source.kind === 'pipe');
|
|
203
|
-
const fact = announced?.source.kind === 'fact' || announced?.source.kind === 'pipe'
|
|
204
|
-
? schemas.get(announced.source.factName)
|
|
205
|
-
: undefined;
|
|
206
|
-
const input = contract.input ?? fact;
|
|
207
|
-
|
|
208
|
-
// An op that FINISHES a fact answers the fact, so its answer is the fact's shape:
|
|
209
|
-
// derived here, where its input already is. Left to the ordinary projection it
|
|
210
|
-
// came back stripped — measured, the subscriber got a value with no `id`.
|
|
211
|
-
const output = announced?.source.kind === 'pipe' && fact
|
|
212
|
-
? { schema: fact, closed: false }
|
|
213
|
-
: effectiveOutput(frond, handler, name, contract);
|
|
214
|
-
const className = handler.ctor.name.endsWith('Handler')
|
|
215
|
-
? handler.ctor.name.slice(0, -'Handler'.length)
|
|
216
|
-
: handler.ctor.name;
|
|
217
|
-
const surface = handler.surface ? `/${handler.surface}` : '/default';
|
|
218
|
-
const remote = options.remotes?.[frond.name];
|
|
219
|
-
const op: EffectiveOperation = {
|
|
220
|
-
...contract,
|
|
221
|
-
...(input ? { input } : {}),
|
|
222
|
-
...(output.schema ? { output: output.schema } : {}),
|
|
223
|
-
id: `${frond.name}${surface}/${className}.${name}`,
|
|
224
|
-
operation: `${className}.${name}`,
|
|
225
|
-
name,
|
|
226
|
-
kind,
|
|
227
|
-
kindSource: override?.kind
|
|
228
|
-
? 'explicit'
|
|
229
|
-
: 'convention',
|
|
230
|
-
handler: {
|
|
231
|
-
className: handler.ctor.name,
|
|
232
|
-
address: handler.address,
|
|
233
|
-
filePath: handler.filePath,
|
|
234
|
-
},
|
|
235
|
-
implementation,
|
|
236
|
-
binding: contract.binding,
|
|
237
|
-
parameters,
|
|
238
|
-
collectors: parameters.flatMap((parameter) => {
|
|
239
|
-
if (parameter.binding.source.kind !== 'collector') return [];
|
|
240
|
-
const collector = collectorsByType.get(parameter.binding.source.typeName)?.[0];
|
|
241
|
-
return collector ? [{
|
|
242
|
-
parameter: parameter.name,
|
|
243
|
-
typeName: collector.typeName,
|
|
244
|
-
className: collector.ctor.name,
|
|
245
|
-
frond: frond.name,
|
|
246
|
-
filePath: collector.filePath,
|
|
247
|
-
}] : [];
|
|
248
|
-
}),
|
|
249
|
-
contexts: parameters
|
|
250
|
-
.filter((parameter) => parameter.binding.source.kind === 'context')
|
|
251
|
-
.map((parameter) => parameter.name),
|
|
252
|
-
placement: {
|
|
253
|
-
frond: frond.name,
|
|
254
|
-
runtime: remote ? 'remote' : 'local',
|
|
255
|
-
...(remote ? { remote } : {}),
|
|
256
|
-
},
|
|
257
|
-
reach: reachOf(handler.deps, frond.name, served, options.remotes ?? {}),
|
|
258
|
-
exposure: {
|
|
259
|
-
surfaces: servedSurfaces(frond, handler).map((surface) => surface ?? 'default'),
|
|
260
|
-
adapters: exposedAdapters(handler, options.adapters),
|
|
261
|
-
},
|
|
262
|
-
outputClosed: output.closed,
|
|
263
|
-
semantics: EFFECTIVE_OPERATION_SEMANTICS,
|
|
264
|
-
};
|
|
102
|
+
// A statement wins over the scan on purpose; saying so is what keeps the win from hiding a rename.
|
|
103
|
+
diagnostics.push(...statementDrift(frond, handler));
|
|
104
|
+
diagnostics.push(...unansweredOperations(frond, handler, contracts.keys()));
|
|
105
|
+
|
|
106
|
+
const resolving: Resolving = { fronds, frond, handler, schemas, served, collectorsByType, options, diagnostics };
|
|
107
|
+
for (const [name, contract] of contracts) {
|
|
108
|
+
const op = resolveOperation(resolving, name, contract);
|
|
109
|
+
if (!op) continue;
|
|
265
110
|
effective.set(name, op);
|
|
266
111
|
operations.push(op);
|
|
267
112
|
}
|
|
268
113
|
}
|
|
269
114
|
}
|
|
270
115
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
for (const misplaced of verify({ fronds })) {
|
|
274
|
-
const remoteBoundary = options.remotes?.[misplaced.frond] !== undefined
|
|
275
|
-
|| options.remotes?.[misplaced.dependsOn.frond] !== undefined;
|
|
276
|
-
resolutionDiagnostics.push(remoteBoundary && misplaced.code === 'cross-frond-dependency'
|
|
277
|
-
? { ...misplaced, severity: 'blocking' }
|
|
278
|
-
: misplaced);
|
|
279
|
-
}
|
|
116
|
+
diagnostics.push(...placementRefusals(fronds, options));
|
|
117
|
+
const resolution = uniqueDiagnostics(diagnostics);
|
|
280
118
|
|
|
281
|
-
const resolution = uniqueDiagnostics(resolutionDiagnostics);
|
|
282
119
|
return new EffectiveOperationModel(
|
|
283
120
|
operations,
|
|
284
121
|
uniqueDiagnostics([...scanDiagnostics, ...resolution]),
|
|
@@ -287,6 +124,193 @@ export function resolveEffectiveOperations(
|
|
|
287
124
|
);
|
|
288
125
|
}
|
|
289
126
|
|
|
127
|
+
function refuseAmbiguousCollectors(
|
|
128
|
+
frond: FrondDescriptor,
|
|
129
|
+
collectorsByType: Map<string, CollectorEntry[]>,
|
|
130
|
+
diagnostics: Diagnostic[],
|
|
131
|
+
): void {
|
|
132
|
+
for (const [typeName, collectors] of collectorsByType) {
|
|
133
|
+
if (collectors.length < 2) continue;
|
|
134
|
+
diagnostics.push({
|
|
135
|
+
severity: 'blocking',
|
|
136
|
+
code: 'collector-ambiguous',
|
|
137
|
+
filePath: collectors[0]!.filePath,
|
|
138
|
+
frond: frond.name,
|
|
139
|
+
subject: typeName,
|
|
140
|
+
message: `Frond '${frond.name}' declares ${collectors.length} collectors for '${typeName}' — `
|
|
141
|
+
+ `${collectors.map((collector) => collector.ctor.name).sort().join(', ')}. `
|
|
142
|
+
+ 'Exactly one collector must own a parameter type.',
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** One operation, or nothing when a refusal was written for it. */
|
|
148
|
+
function resolveOperation(resolving: Resolving, name: string, rawContract: OperationContract): EffectiveOperation | undefined {
|
|
149
|
+
const { fronds, frond, handler, schemas, served, collectorsByType, options, diagnostics } = resolving;
|
|
150
|
+
const contract = normalizeBinding(rawContract, handler, frond, name, diagnostics);
|
|
151
|
+
if (!contract || !answersData(resolving, name, contract)) return undefined;
|
|
152
|
+
|
|
153
|
+
const kind = kindOf(resolving, name);
|
|
154
|
+
if (!kind) return undefined;
|
|
155
|
+
|
|
156
|
+
const implementation = implementationOf(frond, handler, name, diagnostics);
|
|
157
|
+
if (!implementation) return undefined;
|
|
158
|
+
|
|
159
|
+
const parameters = parametersOf(contract);
|
|
160
|
+
if (!validateProvenance(fronds, frond, handler, name, contract, parameters, collectorsByType, diagnostics)) {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// A fact is validated by the entity it names, and an op that FINISHES one answers its shape —
|
|
165
|
+
// left to the ordinary projection, the subscriber got a value with no `id`.
|
|
166
|
+
const announced = contract.binding.find((binding) => binding.source.kind === 'fact' || binding.source.kind === 'pipe');
|
|
167
|
+
const fact = announced?.source.kind === 'fact' || announced?.source.kind === 'pipe'
|
|
168
|
+
? schemas.get(announced.source.factName)
|
|
169
|
+
: undefined;
|
|
170
|
+
const input = contract.input ?? fact;
|
|
171
|
+
const output = announced?.source.kind === 'pipe' && fact
|
|
172
|
+
? { schema: fact, closed: false }
|
|
173
|
+
: effectiveOutput(frond, handler, name, contract);
|
|
174
|
+
|
|
175
|
+
const className = handler.ctor.name.replace(/Handler$/, '');
|
|
176
|
+
const surface = handler.surface ? `/${handler.surface}` : '/default';
|
|
177
|
+
const remote = options.remotes?.[frond.name];
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
...contract,
|
|
181
|
+
...(input ? { input } : {}),
|
|
182
|
+
...(output.schema ? { output: output.schema } : {}),
|
|
183
|
+
id: `${frond.name}${surface}/${className}.${name}`,
|
|
184
|
+
operation: `${className}.${name}`,
|
|
185
|
+
name,
|
|
186
|
+
kind,
|
|
187
|
+
kindSource: frond.operationsOverrides?.[name]?.kind ? 'explicit' : 'convention',
|
|
188
|
+
handler: { className: handler.ctor.name, address: handler.address, filePath: handler.filePath },
|
|
189
|
+
implementation,
|
|
190
|
+
binding: contract.binding,
|
|
191
|
+
parameters,
|
|
192
|
+
collectors: collectorsOf(parameters, collectorsByType, frond),
|
|
193
|
+
contexts: parameters
|
|
194
|
+
.filter((parameter) => parameter.binding.source.kind === 'context')
|
|
195
|
+
.map((parameter) => parameter.name),
|
|
196
|
+
placement: { frond: frond.name, runtime: remote ? 'remote' : 'local', ...(remote ? { remote } : {}) },
|
|
197
|
+
reach: reachOf(handler.deps, frond.name, served, options.remotes ?? {}),
|
|
198
|
+
exposure: {
|
|
199
|
+
surfaces: servedSurfaces(frond, handler).map((surface) => surface ?? 'default'),
|
|
200
|
+
adapters: exposedAdapters(handler, options.adapters),
|
|
201
|
+
},
|
|
202
|
+
outputClosed: output.closed,
|
|
203
|
+
semantics: EFFECTIVE_OPERATION_SEMANTICS,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* An operation answers DATA — what JSON keeps. A declared output is converted by its fields; an
|
|
209
|
+
* undeclared one has nothing to convert it. Measured 2026-09-18: `{ at: new Date(0) }` reaches a
|
|
210
|
+
* local caller as a `Date` and a remote one as a string.
|
|
211
|
+
*/
|
|
212
|
+
function answersData({ frond, handler, diagnostics }: Resolving, name: string, contract: OperationContract): boolean {
|
|
213
|
+
if (contract.output || !contract.signature?.notData) return true;
|
|
214
|
+
|
|
215
|
+
const subject = `${handler.ctor.name}.${name}`;
|
|
216
|
+
diagnostics.push({
|
|
217
|
+
severity: 'blocking',
|
|
218
|
+
code: 'operation-output-not-data',
|
|
219
|
+
filePath: handler.filePath,
|
|
220
|
+
frond: frond.name,
|
|
221
|
+
subject,
|
|
222
|
+
message: `${subject}() answers ${contract.signature.notData}, which carries methods.\n`
|
|
223
|
+
+ ' An operation answers data — what JSON keeps.\n'
|
|
224
|
+
+ ' Declare the output as an entity, whose fields convert it, or answer what it produces.',
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Stated in `frond.config.ts`, or read off the verb the name leads with — never guessed between two. */
|
|
231
|
+
function kindOf({ frond, handler, diagnostics }: Resolving, name: string): OperationKind | undefined {
|
|
232
|
+
const stated = frond.operationsOverrides?.[name]?.kind;
|
|
233
|
+
if (stated) return stated;
|
|
234
|
+
|
|
235
|
+
const inference = inferOperationKind(name);
|
|
236
|
+
const inferred = new Set<OperationKind>();
|
|
237
|
+
if (inference.kind) inferred.add(inference.kind);
|
|
238
|
+
if (inference.queryMatches.length > 0) inferred.add('query');
|
|
239
|
+
if (inference.commandMatches.length > 0) inferred.add('command');
|
|
240
|
+
if (inferred.size === 1) return [...inferred][0];
|
|
241
|
+
|
|
242
|
+
const subject = `${handler.ctor.name}.${name}`;
|
|
243
|
+
const ambiguous = inferred.size > 1;
|
|
244
|
+
diagnostics.push({
|
|
245
|
+
severity: 'blocking',
|
|
246
|
+
code: ambiguous ? 'operation-kind-ambiguous' : 'operation-kind-unknown',
|
|
247
|
+
filePath: handler.filePath,
|
|
248
|
+
frond: frond.name,
|
|
249
|
+
subject,
|
|
250
|
+
message: ambiguous
|
|
251
|
+
? `Cannot resolve the kind of ${subject}: its name carries query evidence `
|
|
252
|
+
+ `(${inference.queryMatches.join(', ')}) and command evidence `
|
|
253
|
+
+ `(${inference.commandMatches.join(', ')}). `
|
|
254
|
+
+ `Declare operations.${name}.kind.`
|
|
255
|
+
: `Cannot resolve the kind of ${subject}: '${name}' leads with no known verb. `
|
|
256
|
+
+ `Rename it to lead with one, or declare operations.${name}.kind in `
|
|
257
|
+
+ `frond.config.ts.\n query: ${knownVerbs().query.join(', ')}`
|
|
258
|
+
+ `\n command: ${knownVerbs().command.join(', ')}`,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function parametersOf(contract: BoundContract): EffectiveParameter[] {
|
|
265
|
+
const params = contract.signature?.params ?? [];
|
|
266
|
+
|
|
267
|
+
return contract.binding.map((binding, position) => {
|
|
268
|
+
const param = params[position];
|
|
269
|
+
const undefinable = binding.optional || param?.optional === true || param?.type.undefined === true;
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
position,
|
|
273
|
+
name: param?.name ?? binding.name,
|
|
274
|
+
type: param?.type.raw ?? null,
|
|
275
|
+
optional: undefinable,
|
|
276
|
+
nullable: param?.type.nullable === true,
|
|
277
|
+
undefinable,
|
|
278
|
+
binding,
|
|
279
|
+
};
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function collectorsOf(
|
|
284
|
+
parameters: readonly EffectiveParameter[],
|
|
285
|
+
collectorsByType: Map<string, CollectorEntry[]>,
|
|
286
|
+
frond: FrondDescriptor,
|
|
287
|
+
): EffectiveOperation['collectors'] {
|
|
288
|
+
return parameters.flatMap((parameter) => {
|
|
289
|
+
if (parameter.binding.source.kind !== 'collector') return [];
|
|
290
|
+
const collector = collectorsByType.get(parameter.binding.source.typeName)?.[0];
|
|
291
|
+
|
|
292
|
+
return collector ? [{
|
|
293
|
+
parameter: parameter.name,
|
|
294
|
+
typeName: collector.typeName,
|
|
295
|
+
className: collector.ctor.name,
|
|
296
|
+
frond: frond.name,
|
|
297
|
+
filePath: collector.filePath,
|
|
298
|
+
}] : [];
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** A dependency on a frond that actually runs elsewhere is a refusal, not the warning a future split deserves. */
|
|
303
|
+
function placementRefusals(fronds: readonly FrondDescriptor[], options: EffectiveOperationOptions): Diagnostic[] {
|
|
304
|
+
return verify({ fronds }).map((misplaced) => {
|
|
305
|
+
const remoteBoundary = options.remotes?.[misplaced.frond] !== undefined
|
|
306
|
+
|| options.remotes?.[misplaced.dependsOn.frond] !== undefined;
|
|
307
|
+
|
|
308
|
+
return remoteBoundary && misplaced.code === 'cross-frond-dependency'
|
|
309
|
+
? { ...misplaced, severity: 'blocking' as const }
|
|
310
|
+
: misplaced;
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
290
314
|
function groupedCollectors(collectors: readonly CollectorEntry[]): Map<string, CollectorEntry[]> {
|
|
291
315
|
const grouped = new Map<string, CollectorEntry[]>();
|
|
292
316
|
for (const collector of collectors) {
|
|
@@ -294,6 +318,7 @@ function groupedCollectors(collectors: readonly CollectorEntry[]): Map<string, C
|
|
|
294
318
|
sameName.push(collector);
|
|
295
319
|
grouped.set(collector.typeName, sameName);
|
|
296
320
|
}
|
|
321
|
+
|
|
297
322
|
return grouped;
|
|
298
323
|
}
|
|
299
324
|
|
|
@@ -303,7 +328,7 @@ function normalizeBinding(
|
|
|
303
328
|
frond: FrondDescriptor,
|
|
304
329
|
name: string,
|
|
305
330
|
diagnostics: Diagnostic[],
|
|
306
|
-
):
|
|
331
|
+
): BoundContract | undefined {
|
|
307
332
|
const params = contract.signature?.params ?? [];
|
|
308
333
|
if (!contract.binding) {
|
|
309
334
|
if (params.length === 0) return { ...contract, binding: [] };
|
|
@@ -315,6 +340,7 @@ function normalizeBinding(
|
|
|
315
340
|
subject: `${handler.ctor.name}.${name}`,
|
|
316
341
|
message: `${handler.ctor.name}.${name} declares ${params.length} parameter(s) and has no binding plan.`,
|
|
317
342
|
});
|
|
343
|
+
|
|
318
344
|
return undefined;
|
|
319
345
|
}
|
|
320
346
|
if (params.length === 0) return { ...contract, binding: contract.binding };
|
|
@@ -341,8 +367,10 @@ function normalizeBinding(
|
|
|
341
367
|
+ `${invalid.length ? `Invalid: ${invalid.map((param) => param.name).join(', ')}. ` : ''}`
|
|
342
368
|
+ `${extras.length ? `Unknown: ${extras.join(', ')}.` : ''}`,
|
|
343
369
|
});
|
|
370
|
+
|
|
344
371
|
return undefined;
|
|
345
372
|
}
|
|
373
|
+
|
|
346
374
|
return {
|
|
347
375
|
...contract,
|
|
348
376
|
binding: params.map((param) => byName.get(param.name)![0]!),
|
|
@@ -387,6 +415,7 @@ function validateProvenance(
|
|
|
387
415
|
+ `${matches.map((parameter) => `${parameter.name}: ${parameter.type ?? 'unknown'}`).join('; ')} `
|
|
388
416
|
+ 'all match the resolved input contract. Declare an explicit binding plan.',
|
|
389
417
|
});
|
|
418
|
+
|
|
390
419
|
return false;
|
|
391
420
|
}
|
|
392
421
|
if (matches.length === 1) inferredBody.add(matches[0]!);
|
|
@@ -448,6 +477,7 @@ function validateProvenance(
|
|
|
448
477
|
+ 'local collector or resolved input schema. Declare an explicit binding.',
|
|
449
478
|
});
|
|
450
479
|
}
|
|
480
|
+
|
|
451
481
|
return valid;
|
|
452
482
|
}
|
|
453
483
|
|
|
@@ -458,6 +488,7 @@ function structural(type: TypeRef | undefined): boolean {
|
|
|
458
488
|
function schemaMatches(schema: SchemaView, type: TypeRef | undefined): boolean {
|
|
459
489
|
const names = new Set([schema.name, schema.derivation?.sourceName].filter(Boolean));
|
|
460
490
|
if (type?.name && names.has(type.name)) return true;
|
|
491
|
+
|
|
461
492
|
return type?.generics?.some((generic) => generic.name && names.has(generic.name)) ?? false;
|
|
462
493
|
}
|
|
463
494
|
|
|
@@ -481,6 +512,7 @@ function implementationOf(
|
|
|
481
512
|
message: `Operation ${handler.ctor.name}.${name} names handler '${override?.handlerName}', `
|
|
482
513
|
+ `but ${candidates.length} matching handlers were found.`,
|
|
483
514
|
});
|
|
515
|
+
|
|
484
516
|
return undefined;
|
|
485
517
|
}
|
|
486
518
|
const implementation = candidates[0]!;
|
|
@@ -498,8 +530,10 @@ function implementationOf(
|
|
|
498
530
|
message: `Operation ${handler.ctor.name}.${name} resolves to `
|
|
499
531
|
+ `${implementation.ctor.name}.${method}, but that method does not exist.`,
|
|
500
532
|
});
|
|
533
|
+
|
|
501
534
|
return undefined;
|
|
502
535
|
}
|
|
536
|
+
|
|
503
537
|
return {
|
|
504
538
|
className: implementation.ctor.name,
|
|
505
539
|
address: implementation.address,
|
|
@@ -524,6 +558,7 @@ function effectiveOutput(
|
|
|
524
558
|
const target = targetOf(handler.ctor) as { name?: string } | undefined;
|
|
525
559
|
const address = target?.name ? lowerFirst(target.name) : handler.address;
|
|
526
560
|
const entity = frond.entities.find((candidate) => candidate.name === address);
|
|
561
|
+
|
|
527
562
|
return { ...(entity ? { schema: entity.entityClass } : {}), closed: false };
|
|
528
563
|
}
|
|
529
564
|
|
|
@@ -532,6 +567,7 @@ function exposedAdapters(
|
|
|
532
567
|
adapters: Record<string, boolean | undefined> | undefined,
|
|
533
568
|
): string[] {
|
|
534
569
|
if (handler.exposed === false && !handler.surface) return [];
|
|
570
|
+
|
|
535
571
|
return Object.entries(adapters ?? {})
|
|
536
572
|
.filter(([, enabled]) => enabled === true)
|
|
537
573
|
.map(([adapter]) => adapter)
|
|
@@ -540,16 +576,18 @@ function exposedAdapters(
|
|
|
540
576
|
|
|
541
577
|
function uniqueDiagnostics(diagnostics: readonly Diagnostic[]): Diagnostic[] {
|
|
542
578
|
const seen = new Set<string>();
|
|
579
|
+
|
|
543
580
|
return diagnostics.filter((diagnostic) => {
|
|
544
581
|
const key = [diagnostic.code, diagnostic.filePath, diagnostic.subject, diagnostic.message].join('\0');
|
|
545
582
|
if (seen.has(key)) return false;
|
|
546
583
|
seen.add(key);
|
|
584
|
+
|
|
547
585
|
return true;
|
|
548
586
|
});
|
|
549
587
|
}
|
|
550
588
|
|
|
551
589
|
/** The contract of every operation a handler serves — the three producers, merged once. */
|
|
552
|
-
|
|
590
|
+
function resolveContracts(
|
|
553
591
|
handler: Pick<HandlerEntry, 'ctor' | 'operations'>,
|
|
554
592
|
overrides: FrondDescriptor['operationsOverrides'],
|
|
555
593
|
collectorTypeNames: Set<string>,
|
package/src/FougereConfig.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
|
-
import type {
|
|
3
|
+
import type { Extension } from './boot/Extension.js';
|
|
4
4
|
import type { LogLevel } from './builtin/LogLevel.js';
|
|
5
5
|
import type { ConventionsInput } from './ConventionsInput.js';
|
|
6
6
|
import type { NameOf } from './NameOf.js';
|
|
@@ -29,28 +29,28 @@ export interface FougereConfig {
|
|
|
29
29
|
fronds?: FrondsStated;
|
|
30
30
|
/** What answers a port — a name, or the chain from the outside in. */
|
|
31
31
|
ports?: PortChoice;
|
|
32
|
-
/**
|
|
33
|
-
auth?:
|
|
32
|
+
/** The auth provider — an extension, which brings the frond its rows live in. */
|
|
33
|
+
auth?: Extension;
|
|
34
34
|
/** Which protocol adapters this app serves. */
|
|
35
35
|
adapters?: AdapterConfig;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
const CONFIG_FILES = ['fougere.config.ts', 'fougere.config.js', 'fougere.config.mjs'];
|
|
39
39
|
|
|
40
|
+
/** The config file a directory holds, if it holds one. */
|
|
41
|
+
export function configFileIn(dir: string): string | undefined {
|
|
42
|
+
return CONFIG_FILES.map((file) => resolve(dir, file)).find((path) => existsSync(path));
|
|
43
|
+
}
|
|
44
|
+
|
|
40
45
|
async function loadConfigFrom(dir: string, fresh?: boolean): Promise<FougereConfig> {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const mod = await loader(path, fresh ? { fresh } : undefined);
|
|
50
|
-
return ((mod as { default?: FougereConfig }).default ?? mod) as FougereConfig;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return {};
|
|
46
|
+
const path = configFileIn(dir);
|
|
47
|
+
if (!path) return {};
|
|
48
|
+
|
|
49
|
+
// A module is cached by its specifier, so a second load of an EDITED file hands back what
|
|
50
|
+
// was read the first time. The loader owns its own cache, so it is the one told.
|
|
51
|
+
const mod = await getModuleLoader()(path, fresh ? { fresh } : undefined);
|
|
52
|
+
|
|
53
|
+
return ((mod as { default?: FougereConfig }).default ?? mod) as FougereConfig;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/** Load the root fougere.config.{ts,js,mjs} from the given directory. */
|
|
@@ -92,5 +92,6 @@ function mergeGlobal(base: FougereConfig, override: Partial<FougereConfig>): Fou
|
|
|
92
92
|
export async function loadCascadedConfig(workspaceRoot: string, appRoot: string): Promise<FougereConfig> {
|
|
93
93
|
const base = await loadConfig(workspaceRoot);
|
|
94
94
|
if (resolve(workspaceRoot) === resolve(appRoot)) return base;
|
|
95
|
+
|
|
95
96
|
return mergeGlobal(base, await loadConfig(appRoot));
|
|
96
97
|
}
|
package/src/FrondConfig.ts
CHANGED
|
@@ -44,8 +44,10 @@ export async function loadFrondConfig(frondPath: string): Promise<FrondConfig |
|
|
|
44
44
|
const path = resolve(frondPath, file);
|
|
45
45
|
if (existsSync(path)) {
|
|
46
46
|
const mod = await import(path);
|
|
47
|
+
|
|
47
48
|
return mod.default ?? mod;
|
|
48
49
|
}
|
|
49
50
|
}
|
|
51
|
+
|
|
50
52
|
return undefined;
|
|
51
53
|
}
|
package/src/FrondDeclaration.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { lowerFirst, type SchemaView } from '@fougere/schema';
|
|
2
|
+
import { addressOf } from './wire/Facade.js';
|
|
2
3
|
import type { CollectorEntry } from './descriptor/CollectorEntry.js';
|
|
3
4
|
import type { EntityEntry } from './descriptor/EntityEntry.js';
|
|
4
5
|
import type { FrondDescriptor } from './descriptor/FrondDescriptor.js';
|
|
@@ -54,13 +55,6 @@ function statedOperations(h: Ctor | DeclaredHandler): Map<string, OperationContr
|
|
|
54
55
|
return merged;
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
/** `PostHandler` answers at `post` — the same rule the scan applies to a file it found. */
|
|
58
|
-
function addressOf(className: string): string {
|
|
59
|
-
const base = className.endsWith('Handler') ? className.slice(0, -7) : className;
|
|
60
|
-
|
|
61
|
-
return lowerFirst(base);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
58
|
/** What a declaration states about one frond. Everything else is derived from the classes. */
|
|
65
59
|
export interface FrondDeclaration {
|
|
66
60
|
entities?: SchemaView[];
|