@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.
Files changed (261) hide show
  1. package/dist/CallIdentity.d.ts.map +1 -1
  2. package/dist/CallIdentity.js.map +1 -1
  3. package/dist/DomainCluster.d.ts.map +1 -1
  4. package/dist/DomainCluster.js +1 -1
  5. package/dist/DomainCluster.js.map +1 -1
  6. package/dist/EffectiveOperationModel.d.ts +1 -4
  7. package/dist/EffectiveOperationModel.d.ts.map +1 -1
  8. package/dist/EffectiveOperationModel.js +182 -173
  9. package/dist/EffectiveOperationModel.js.map +1 -1
  10. package/dist/FougereConfig.d.ts +5 -3
  11. package/dist/FougereConfig.d.ts.map +1 -1
  12. package/dist/FougereConfig.js +11 -13
  13. package/dist/FougereConfig.js.map +1 -1
  14. package/dist/FrondConfig.d.ts.map +1 -1
  15. package/dist/FrondConfig.js.map +1 -1
  16. package/dist/FrondDeclaration.d.ts.map +1 -1
  17. package/dist/FrondDeclaration.js +1 -5
  18. package/dist/FrondDeclaration.js.map +1 -1
  19. package/dist/Source.js +3 -3
  20. package/dist/Source.js.map +1 -1
  21. package/dist/SourceView.d.ts +0 -2
  22. package/dist/SourceView.d.ts.map +1 -1
  23. package/dist/boot/App.d.ts +2 -4
  24. package/dist/boot/App.d.ts.map +1 -1
  25. package/dist/boot/AppLifecycle.d.ts +5 -0
  26. package/dist/boot/AppLifecycle.d.ts.map +1 -1
  27. package/dist/boot/AppLifecycle.js +19 -10
  28. package/dist/boot/AppLifecycle.js.map +1 -1
  29. package/dist/boot/AuthRuntime.d.ts +5 -12
  30. package/dist/boot/AuthRuntime.d.ts.map +1 -1
  31. package/dist/boot/AuthRuntime.js +2 -1
  32. package/dist/boot/AuthRuntime.js.map +1 -1
  33. package/dist/boot/CreateAppOptions.d.ts +0 -11
  34. package/dist/boot/CreateAppOptions.d.ts.map +1 -1
  35. package/dist/boot/Emissions.d.ts.map +1 -1
  36. package/dist/boot/Emissions.js +3 -6
  37. package/dist/boot/Emissions.js.map +1 -1
  38. package/dist/boot/Hosting.d.ts +2 -1
  39. package/dist/boot/Hosting.d.ts.map +1 -1
  40. package/dist/boot/ambient.als.d.ts.map +1 -1
  41. package/dist/boot/ambient.als.js.map +1 -1
  42. package/dist/boot/apply.d.ts.map +1 -1
  43. package/dist/boot/apply.js.map +1 -1
  44. package/dist/boot/arity.d.ts +13 -0
  45. package/dist/boot/arity.d.ts.map +1 -0
  46. package/dist/boot/arity.js +33 -0
  47. package/dist/boot/arity.js.map +1 -0
  48. package/dist/boot/bootstrap.d.ts.map +1 -1
  49. package/dist/boot/bootstrap.js +143 -265
  50. package/dist/boot/bootstrap.js.map +1 -1
  51. package/dist/boot/card.d.ts.map +1 -1
  52. package/dist/boot/card.js.map +1 -1
  53. package/dist/boot/frame.d.ts.map +1 -1
  54. package/dist/boot/frame.js.map +1 -1
  55. package/dist/boot/install.d.ts.map +1 -1
  56. package/dist/boot/install.js +105 -115
  57. package/dist/boot/install.js.map +1 -1
  58. package/dist/boot/ownership.d.ts.map +1 -1
  59. package/dist/boot/ownership.js.map +1 -1
  60. package/dist/boot/relations.d.ts +10 -0
  61. package/dist/boot/relations.d.ts.map +1 -1
  62. package/dist/boot/relations.js +35 -2
  63. package/dist/boot/relations.js.map +1 -1
  64. package/dist/boot/remote.d.ts.map +1 -1
  65. package/dist/boot/remote.js.map +1 -1
  66. package/dist/boot/seed.d.ts.map +1 -1
  67. package/dist/boot/seed.js +1 -1
  68. package/dist/boot/seed.js.map +1 -1
  69. package/dist/boot/statement-drift.d.ts +6 -0
  70. package/dist/boot/statement-drift.d.ts.map +1 -1
  71. package/dist/boot/statement-drift.js +19 -0
  72. package/dist/boot/statement-drift.js.map +1 -1
  73. package/dist/boot/together.d.ts.map +1 -1
  74. package/dist/boot/together.js.map +1 -1
  75. package/dist/builtin/Carry.d.ts.map +1 -1
  76. package/dist/builtin/Carry.js.map +1 -1
  77. package/dist/builtin/Logger.d.ts.map +1 -1
  78. package/dist/builtin/Logger.js +3 -1
  79. package/dist/builtin/Logger.js.map +1 -1
  80. package/dist/builtin/config.js.map +1 -1
  81. package/dist/contract.d.ts.map +1 -1
  82. package/dist/contract.js.map +1 -1
  83. package/dist/crypto/encoding.d.ts.map +1 -1
  84. package/dist/crypto/encoding.js.map +1 -1
  85. package/dist/crypto/node.d.ts.map +1 -1
  86. package/dist/crypto/node.js.map +1 -1
  87. package/dist/crypto/webcrypto.d.ts.map +1 -1
  88. package/dist/crypto/webcrypto.js.map +1 -1
  89. package/dist/descriptor/Fronds.d.ts +5 -0
  90. package/dist/descriptor/Fronds.d.ts.map +1 -1
  91. package/dist/descriptor/Fronds.js +8 -0
  92. package/dist/descriptor/Fronds.js.map +1 -1
  93. package/dist/descriptor/index.d.ts +1 -0
  94. package/dist/descriptor/index.d.ts.map +1 -1
  95. package/dist/descriptor/index.js +1 -0
  96. package/dist/descriptor/index.js.map +1 -1
  97. package/dist/dispatch/ArrayResult.d.ts.map +1 -1
  98. package/dist/dispatch/ArrayResult.js.map +1 -1
  99. package/dist/dispatch/Dispatcher.d.ts.map +1 -1
  100. package/dist/dispatch/Dispatcher.js.map +1 -1
  101. package/dist/dispatch/HandlerFacade.d.ts +6 -1
  102. package/dist/dispatch/HandlerFacade.d.ts.map +1 -1
  103. package/dist/dispatch/HandlerFacade.js +11 -3
  104. package/dist/dispatch/HandlerFacade.js.map +1 -1
  105. package/dist/dispatch/InFlight.d.ts +3 -2
  106. package/dist/dispatch/InFlight.d.ts.map +1 -1
  107. package/dist/dispatch/InFlight.js +11 -1
  108. package/dist/dispatch/InFlight.js.map +1 -1
  109. package/dist/dispatch/LocalRoutePolicy.d.ts.map +1 -1
  110. package/dist/dispatch/LocalRoutePolicy.js.map +1 -1
  111. package/dist/dispatch/RouteRegistry.d.ts.map +1 -1
  112. package/dist/dispatch/RouteRegistry.js.map +1 -1
  113. package/dist/dispatch/StorageGuard.d.ts.map +1 -1
  114. package/dist/dispatch/StorageGuard.js +4 -1
  115. package/dist/dispatch/StorageGuard.js.map +1 -1
  116. package/dist/identity-keys.d.ts.map +1 -1
  117. package/dist/identity-keys.js.map +1 -1
  118. package/dist/index.d.ts +2 -4
  119. package/dist/index.d.ts.map +1 -1
  120. package/dist/index.js +2 -0
  121. package/dist/index.js.map +1 -1
  122. package/dist/loader.d.ts.map +1 -1
  123. package/dist/loader.js.map +1 -1
  124. package/dist/node.d.ts +1 -1
  125. package/dist/node.d.ts.map +1 -1
  126. package/dist/node.js +1 -1
  127. package/dist/node.js.map +1 -1
  128. package/dist/prefab/CrudConstructor.d.ts.map +1 -1
  129. package/dist/prefab/CrudConstructor.js.map +1 -1
  130. package/dist/prefab/collector.d.ts.map +1 -1
  131. package/dist/prefab/collector.js.map +1 -1
  132. package/dist/prefab/presenter.d.ts.map +1 -1
  133. package/dist/prefab/presenter.js.map +1 -1
  134. package/dist/storage/Comparison.d.ts +2 -2
  135. package/dist/storage/Comparison.d.ts.map +1 -1
  136. package/dist/storage/Comparison.js +3 -1
  137. package/dist/storage/Comparison.js.map +1 -1
  138. package/dist/storage/Storage.d.ts.map +1 -1
  139. package/dist/storage/Storage.js.map +1 -1
  140. package/dist/storage/Store.d.ts.map +1 -1
  141. package/dist/storage/Store.js +1 -1
  142. package/dist/storage/Store.js.map +1 -1
  143. package/dist/verify.d.ts.map +1 -1
  144. package/dist/verify.js.map +1 -1
  145. package/dist/wire/AppMiddleware.d.ts.map +1 -1
  146. package/dist/wire/AppMiddleware.js.map +1 -1
  147. package/dist/wire/Emit.d.ts +0 -2
  148. package/dist/wire/Emit.d.ts.map +1 -1
  149. package/dist/wire/Emit.js +1 -1
  150. package/dist/wire/Emit.js.map +1 -1
  151. package/dist/wire/Facade.d.ts +7 -0
  152. package/dist/wire/Facade.d.ts.map +1 -1
  153. package/dist/wire/Facade.js +13 -0
  154. package/dist/wire/Facade.js.map +1 -1
  155. package/dist/wire/FougereError.d.ts.map +1 -1
  156. package/dist/wire/FougereError.js.map +1 -1
  157. package/dist/wire/Invocation.d.ts.map +1 -1
  158. package/dist/wire/Invocation.js.map +1 -1
  159. package/dist/wire/OperationContract.d.ts +0 -4
  160. package/dist/wire/OperationContract.d.ts.map +1 -1
  161. package/dist/wire/OperationContract.js +1 -1
  162. package/dist/wire/OperationContract.js.map +1 -1
  163. package/dist/wire/OperationsMap.d.ts +0 -3
  164. package/dist/wire/OperationsMap.d.ts.map +1 -1
  165. package/dist/wire/OperationsMap.js +0 -5
  166. package/dist/wire/OperationsMap.js.map +1 -1
  167. package/dist/wire/RouteAddress.d.ts.map +1 -1
  168. package/dist/wire/RouteAddress.js.map +1 -1
  169. package/dist/wire/binding.d.ts.map +1 -1
  170. package/dist/wire/binding.js.map +1 -1
  171. package/dist/wire/card/IdentityCard.d.ts.map +1 -1
  172. package/dist/wire/card/IdentityCard.js.map +1 -1
  173. package/dist/wire/drift.d.ts.map +1 -1
  174. package/dist/wire/drift.js.map +1 -1
  175. package/dist/wire/http-error.d.ts.map +1 -1
  176. package/dist/wire/http-error.js.map +1 -1
  177. package/dist/wire/loggerMiddleware.d.ts.map +1 -1
  178. package/dist/wire/loggerMiddleware.js.map +1 -1
  179. package/package.json +5 -5
  180. package/src/CallIdentity.ts +6 -0
  181. package/src/DomainCluster.ts +2 -1
  182. package/src/EffectiveOperationModel.ts +236 -198
  183. package/src/FougereConfig.ts +17 -16
  184. package/src/FrondConfig.ts +2 -0
  185. package/src/FrondDeclaration.ts +1 -7
  186. package/src/Source.ts +3 -3
  187. package/src/SourceView.ts +0 -2
  188. package/src/boot/App.ts +2 -4
  189. package/src/boot/AppLifecycle.ts +22 -10
  190. package/src/boot/AuthRuntime.ts +6 -13
  191. package/src/boot/CreateAppOptions.ts +0 -11
  192. package/src/boot/Emissions.ts +7 -7
  193. package/src/boot/Hosting.ts +2 -1
  194. package/src/boot/ambient.als.ts +2 -0
  195. package/src/boot/apply.ts +1 -0
  196. package/src/boot/arity.ts +42 -0
  197. package/src/boot/bootstrap.ts +175 -300
  198. package/src/boot/card.ts +2 -0
  199. package/src/boot/frame.ts +6 -0
  200. package/src/boot/install.ts +123 -146
  201. package/src/boot/ownership.ts +1 -0
  202. package/src/boot/relations.ts +42 -10
  203. package/src/boot/remote.ts +1 -0
  204. package/src/boot/seed.ts +3 -5
  205. package/src/boot/statement-drift.ts +25 -0
  206. package/src/boot/together.ts +3 -0
  207. package/src/builtin/Carry.ts +1 -0
  208. package/src/builtin/Logger.ts +9 -1
  209. package/src/builtin/config.ts +1 -0
  210. package/src/contract.ts +1 -0
  211. package/src/crypto/encoding.ts +4 -0
  212. package/src/crypto/node.ts +2 -0
  213. package/src/crypto/webcrypto.ts +2 -0
  214. package/src/descriptor/Fronds.ts +12 -0
  215. package/src/descriptor/index.ts +1 -0
  216. package/src/dispatch/ArrayResult.ts +1 -0
  217. package/src/dispatch/Dispatcher.ts +1 -0
  218. package/src/dispatch/HandlerFacade.ts +16 -4
  219. package/src/dispatch/InFlight.ts +18 -2
  220. package/src/dispatch/LocalRoutePolicy.ts +1 -0
  221. package/src/dispatch/RouteRegistry.ts +4 -0
  222. package/src/dispatch/StorageGuard.ts +9 -1
  223. package/src/identity-keys.ts +2 -0
  224. package/src/index.ts +2 -4
  225. package/src/loader.ts +1 -0
  226. package/src/node.ts +1 -1
  227. package/src/prefab/CrudConstructor.ts +1 -11
  228. package/src/prefab/collector.ts +1 -0
  229. package/src/prefab/presenter.ts +1 -0
  230. package/src/storage/Comparison.ts +7 -4
  231. package/src/storage/Storage.ts +2 -0
  232. package/src/storage/Store.ts +9 -1
  233. package/src/verify.ts +2 -0
  234. package/src/wire/AppMiddleware.ts +2 -0
  235. package/src/wire/Emit.ts +1 -1
  236. package/src/wire/Facade.ts +17 -10
  237. package/src/wire/FougereError.ts +2 -0
  238. package/src/wire/Invocation.ts +2 -0
  239. package/src/wire/OperationContract.ts +2 -1
  240. package/src/wire/OperationsMap.ts +0 -10
  241. package/src/wire/RouteAddress.ts +2 -0
  242. package/src/wire/binding.ts +1 -0
  243. package/src/wire/card/IdentityCard.ts +1 -0
  244. package/src/wire/drift.ts +3 -0
  245. package/src/wire/http-error.ts +2 -0
  246. package/src/wire/loggerMiddleware.ts +1 -0
  247. package/dist/boot/AuthConfig.d.ts +0 -14
  248. package/dist/boot/AuthConfig.d.ts.map +0 -1
  249. package/dist/boot/AuthConfig.js +0 -2
  250. package/dist/boot/AuthConfig.js.map +0 -1
  251. package/dist/boot/AuthContext.d.ts +0 -12
  252. package/dist/boot/AuthContext.d.ts.map +0 -1
  253. package/dist/boot/AuthContext.js +0 -2
  254. package/dist/boot/AuthContext.js.map +0 -1
  255. package/dist/storage/ComparisonName.d.ts +0 -3
  256. package/dist/storage/ComparisonName.d.ts.map +0 -1
  257. package/dist/storage/ComparisonName.js +0 -2
  258. package/dist/storage/ComparisonName.js.map +0 -1
  259. package/src/boot/AuthConfig.ts +0 -14
  260. package/src/boot/AuthContext.ts +0 -12
  261. 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 produced where schemas are available (the scanner), but it is a
86
- // resolution failure. Lift it into the same refusal table as kind/binding/topology.
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
- for (const [typeName, collectors] of collectorsByType) {
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 out loud is what keeps the
112
- // win from hiding a rename. Compared here, where both readings are in hand.
113
- resolutionDiagnostics.push(...statementDrift(frond, handler));
114
-
115
- for (const [name, rawContract] of contracts) {
116
- const subject = `${handler.ctor.name}.${name}`;
117
- const contract = normalizeBinding(rawContract, handler, frond, name, resolutionDiagnostics);
118
- if (!contract) continue;
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
- // Topology and DI are part of the same effective program. A dependency targeting an
272
- // actually remote frond is a refusal, not the warning appropriate to a future split.
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
- ): (OperationContract & { binding: BindingPlan }) | undefined {
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
- export function resolveContracts(
590
+ function resolveContracts(
553
591
  handler: Pick<HandlerEntry, 'ctor' | 'operations'>,
554
592
  overrides: FrondDescriptor['operationsOverrides'],
555
593
  collectorTypeNames: Set<string>,
@@ -1,6 +1,6 @@
1
1
  import { resolve } from 'node:path';
2
2
  import { existsSync } from 'node:fs';
3
- import type { AuthConfig } from './boot/AuthConfig.js';
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
- /** Auth declaration — picks a provider package and forwards options to it. */
33
- auth?: AuthConfig;
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 loader = getModuleLoader();
42
- for (const file of CONFIG_FILES) {
43
- const path = resolve(dir, file);
44
- if (existsSync(path)) {
45
- // A module is cached by its specifier, so a second load of an EDITED file hands
46
- // back what was read the first time — measured, and it made re-reading a config
47
- // return the config already in force. The loader owns its own cache, so it is the
48
- // one told; the old module stays in memory, re-reading being for a change.
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
  }
@@ -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
  }
@@ -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[];