@fougere/core 0.8.2-alpha.0 → 0.8.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 (222) hide show
  1. package/dist/boot/Emissions.d.ts.map +1 -1
  2. package/dist/boot/Emissions.js +3 -3
  3. package/dist/boot/Emissions.js.map +1 -1
  4. package/dist/boot/auth.d.ts +1 -1
  5. package/dist/boot/auth.d.ts.map +1 -1
  6. package/dist/boot/bootstrap.d.ts.map +1 -1
  7. package/dist/boot/bootstrap.js +339 -595
  8. package/dist/boot/bootstrap.js.map +1 -1
  9. package/dist/boot/card.js +1 -1
  10. package/dist/boot/card.js.map +1 -1
  11. package/dist/boot/hosted.d.ts +1 -1
  12. package/dist/boot/hosted.d.ts.map +1 -1
  13. package/dist/boot/install.d.ts +40 -0
  14. package/dist/boot/install.d.ts.map +1 -0
  15. package/dist/boot/install.js +298 -0
  16. package/dist/boot/install.js.map +1 -0
  17. package/dist/boot/ownership.d.ts +1 -1
  18. package/dist/boot/ownership.d.ts.map +1 -1
  19. package/dist/boot/ownership.js +14 -1
  20. package/dist/boot/ownership.js.map +1 -1
  21. package/dist/boot/seed.d.ts +7 -1
  22. package/dist/boot/seed.d.ts.map +1 -1
  23. package/dist/boot/seed.js +7 -2
  24. package/dist/boot/seed.js.map +1 -1
  25. package/dist/boot/statement-drift.d.ts +1 -1
  26. package/dist/boot/statement-drift.d.ts.map +1 -1
  27. package/dist/boot/together.d.ts +1 -1
  28. package/dist/boot/together.d.ts.map +1 -1
  29. package/dist/boot/together.js +1 -1
  30. package/dist/boot/together.js.map +1 -1
  31. package/dist/boot/types.d.ts +2 -2
  32. package/dist/boot/types.d.ts.map +1 -1
  33. package/dist/config-loader.d.ts +1 -1
  34. package/dist/config-loader.d.ts.map +1 -1
  35. package/dist/contract.d.ts +2 -2
  36. package/dist/contract.d.ts.map +1 -1
  37. package/dist/contract.js +1 -1
  38. package/dist/contract.js.map +1 -1
  39. package/dist/conventions.d.ts.map +1 -0
  40. package/dist/conventions.js.map +1 -0
  41. package/dist/crypto/encoding.d.ts +0 -1
  42. package/dist/crypto/encoding.d.ts.map +1 -1
  43. package/dist/crypto/encoding.js +0 -7
  44. package/dist/crypto/encoding.js.map +1 -1
  45. package/dist/declare.js +1 -1
  46. package/dist/declare.js.map +1 -1
  47. package/dist/descriptor/index.d.ts +15 -0
  48. package/dist/descriptor/index.d.ts.map +1 -0
  49. package/dist/descriptor/index.js +14 -0
  50. package/dist/descriptor/index.js.map +1 -0
  51. package/dist/descriptor/surface.d.ts +8 -0
  52. package/dist/descriptor/surface.d.ts.map +1 -0
  53. package/dist/descriptor/surface.js +16 -0
  54. package/dist/descriptor/surface.js.map +1 -0
  55. package/dist/dispatch/HandlerFacade.d.ts +2 -0
  56. package/dist/dispatch/HandlerFacade.d.ts.map +1 -1
  57. package/dist/dispatch/HandlerFacade.js +7 -2
  58. package/dist/dispatch/HandlerFacade.js.map +1 -1
  59. package/dist/dispatch/StorageGuard.d.ts.map +1 -1
  60. package/dist/dispatch/StorageGuard.js +9 -23
  61. package/dist/dispatch/StorageGuard.js.map +1 -1
  62. package/dist/dispatch/presenterArguments.d.ts +7 -1
  63. package/dist/dispatch/presenterArguments.d.ts.map +1 -1
  64. package/dist/dispatch/presenterArguments.js +15 -4
  65. package/dist/dispatch/presenterArguments.js.map +1 -1
  66. package/dist/dispatch/validateInput.d.ts.map +1 -1
  67. package/dist/dispatch/validateInput.js +2 -2
  68. package/dist/dispatch/validateInput.js.map +1 -1
  69. package/dist/effective-operation.d.ts +1 -1
  70. package/dist/effective-operation.d.ts.map +1 -1
  71. package/dist/effective-operation.js +2 -12
  72. package/dist/effective-operation.js.map +1 -1
  73. package/dist/index.d.ts +7 -9
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +4 -2
  76. package/dist/index.js.map +1 -1
  77. package/dist/node.d.ts +2 -12
  78. package/dist/node.d.ts.map +1 -1
  79. package/dist/node.js +2 -12
  80. package/dist/node.js.map +1 -1
  81. package/dist/prefab/crud.d.ts +1 -1
  82. package/dist/prefab/crud.d.ts.map +1 -1
  83. package/dist/prefab/mirror.d.ts +10 -11
  84. package/dist/prefab/mirror.d.ts.map +1 -1
  85. package/dist/prefab/mirror.js +1 -27
  86. package/dist/prefab/mirror.js.map +1 -1
  87. package/dist/prefab/repository.d.ts +1 -1
  88. package/dist/prefab/repository.d.ts.map +1 -1
  89. package/dist/{scan/result.d.ts → scan.d.ts} +2 -2
  90. package/dist/scan.d.ts.map +1 -0
  91. package/dist/scan.js +2 -0
  92. package/dist/scan.js.map +1 -0
  93. package/dist/source.d.ts +1 -1
  94. package/dist/source.d.ts.map +1 -1
  95. package/dist/storage/criterion.d.ts.map +1 -0
  96. package/dist/{criterion.js → storage/criterion.js} +2 -2
  97. package/dist/storage/criterion.js.map +1 -0
  98. package/dist/{storage.d.ts → storage/port.d.ts} +6 -3
  99. package/dist/storage/port.d.ts.map +1 -0
  100. package/dist/{storage.js → storage/port.js} +13 -6
  101. package/dist/storage/port.js.map +1 -0
  102. package/dist/{store.d.ts → storage/store.d.ts} +1 -1
  103. package/dist/storage/store.d.ts.map +1 -0
  104. package/dist/{store.js → storage/store.js} +33 -17
  105. package/dist/storage/store.js.map +1 -0
  106. package/dist/verify.js +1 -1
  107. package/dist/verify.js.map +1 -1
  108. package/dist/wire/emit.d.ts.map +1 -0
  109. package/dist/wire/emit.js.map +1 -0
  110. package/dist/wire/errors.js +2 -2
  111. package/dist/wire/errors.js.map +1 -1
  112. package/package.json +10 -4
  113. package/src/boot/Emissions.ts +3 -3
  114. package/src/boot/auth.ts +1 -1
  115. package/src/boot/bootstrap.ts +386 -674
  116. package/src/boot/card.ts +1 -1
  117. package/src/boot/hosted.ts +1 -1
  118. package/src/boot/install.ts +390 -0
  119. package/src/boot/ownership.ts +17 -2
  120. package/src/boot/seed.ts +18 -3
  121. package/src/boot/statement-drift.ts +1 -1
  122. package/src/boot/together.ts +1 -1
  123. package/src/boot/types.ts +2 -2
  124. package/src/config-loader.ts +1 -1
  125. package/src/contract.ts +2 -2
  126. package/src/crypto/encoding.ts +0 -7
  127. package/src/declare.ts +1 -1
  128. package/src/descriptor/index.ts +15 -0
  129. package/src/descriptor/surface.ts +23 -0
  130. package/src/dispatch/HandlerFacade.ts +9 -2
  131. package/src/dispatch/StorageGuard.ts +8 -21
  132. package/src/dispatch/presenterArguments.ts +21 -9
  133. package/src/dispatch/validateInput.ts +2 -2
  134. package/src/effective-operation.ts +3 -14
  135. package/src/index.ts +10 -9
  136. package/src/node.ts +2 -16
  137. package/src/prefab/crud.ts +1 -1
  138. package/src/prefab/mirror.ts +12 -38
  139. package/src/prefab/repository.ts +1 -1
  140. package/src/{scan/result.ts → scan.ts} +1 -1
  141. package/src/source.ts +1 -1
  142. package/src/{criterion.ts → storage/criterion.ts} +2 -2
  143. package/src/{storage.ts → storage/port.ts} +20 -5
  144. package/src/{store.ts → storage/store.ts} +35 -16
  145. package/src/verify.ts +1 -1
  146. package/src/wire/call.ts +1 -1
  147. package/src/wire/errors.ts +2 -2
  148. package/dist/boot/boot.d.ts +0 -45
  149. package/dist/boot/boot.d.ts.map +0 -1
  150. package/dist/boot/boot.js +0 -63
  151. package/dist/boot/boot.js.map +0 -1
  152. package/dist/criterion.d.ts.map +0 -1
  153. package/dist/criterion.js.map +0 -1
  154. package/dist/declarations.d.ts +0 -29
  155. package/dist/declarations.d.ts.map +0 -1
  156. package/dist/declarations.js +0 -69
  157. package/dist/declarations.js.map +0 -1
  158. package/dist/emit.d.ts.map +0 -1
  159. package/dist/emit.js.map +0 -1
  160. package/dist/imports.d.ts +0 -16
  161. package/dist/imports.d.ts.map +0 -1
  162. package/dist/imports.js +0 -78
  163. package/dist/imports.js.map +0 -1
  164. package/dist/placement.d.ts +0 -25
  165. package/dist/placement.d.ts.map +0 -1
  166. package/dist/placement.js +0 -69
  167. package/dist/placement.js.map +0 -1
  168. package/dist/scan/adapters.d.ts +0 -3
  169. package/dist/scan/adapters.d.ts.map +0 -1
  170. package/dist/scan/adapters.js +0 -25
  171. package/dist/scan/adapters.js.map +0 -1
  172. package/dist/scan/bundling.d.ts +0 -3
  173. package/dist/scan/bundling.d.ts.map +0 -1
  174. package/dist/scan/bundling.js +0 -15
  175. package/dist/scan/bundling.js.map +0 -1
  176. package/dist/scan/contract.d.ts +0 -27
  177. package/dist/scan/contract.d.ts.map +0 -1
  178. package/dist/scan/contract.js +0 -46
  179. package/dist/scan/contract.js.map +0 -1
  180. package/dist/scan/conventions.d.ts.map +0 -1
  181. package/dist/scan/conventions.js.map +0 -1
  182. package/dist/scan/emit.d.ts +0 -10
  183. package/dist/scan/emit.d.ts.map +0 -1
  184. package/dist/scan/emit.js +0 -144
  185. package/dist/scan/emit.js.map +0 -1
  186. package/dist/scan/handler-parser.d.ts +0 -26
  187. package/dist/scan/handler-parser.d.ts.map +0 -1
  188. package/dist/scan/handler-parser.js +0 -607
  189. package/dist/scan/handler-parser.js.map +0 -1
  190. package/dist/scan/result.d.ts.map +0 -1
  191. package/dist/scan/result.js +0 -2
  192. package/dist/scan/result.js.map +0 -1
  193. package/dist/scan/scanner.d.ts +0 -12
  194. package/dist/scan/scanner.d.ts.map +0 -1
  195. package/dist/scan/scanner.js +0 -569
  196. package/dist/scan/scanner.js.map +0 -1
  197. package/dist/scan/statement.d.ts +0 -4
  198. package/dist/scan/statement.d.ts.map +0 -1
  199. package/dist/scan/statement.js +0 -135
  200. package/dist/scan/statement.js.map +0 -1
  201. package/dist/storage.d.ts.map +0 -1
  202. package/dist/storage.js.map +0 -1
  203. package/dist/store.d.ts.map +0 -1
  204. package/dist/store.js.map +0 -1
  205. package/src/boot/boot.ts +0 -110
  206. package/src/declarations.ts +0 -102
  207. package/src/imports.ts +0 -94
  208. package/src/placement.ts +0 -86
  209. package/src/scan/adapters.ts +0 -32
  210. package/src/scan/bundling.ts +0 -14
  211. package/src/scan/contract.ts +0 -80
  212. package/src/scan/emit.ts +0 -160
  213. package/src/scan/handler-parser.ts +0 -695
  214. package/src/scan/scanner.ts +0 -694
  215. package/src/scan/statement.ts +0 -159
  216. /package/dist/{scan/conventions.d.ts → conventions.d.ts} +0 -0
  217. /package/dist/{scan/conventions.js → conventions.js} +0 -0
  218. /package/dist/{criterion.d.ts → storage/criterion.d.ts} +0 -0
  219. /package/dist/{emit.d.ts → wire/emit.d.ts} +0 -0
  220. /package/dist/{emit.js → wire/emit.js} +0 -0
  221. /package/src/{scan/conventions.ts → conventions.ts} +0 -0
  222. /package/src/{emit.ts → wire/emit.ts} +0 -0
package/src/boot/card.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /** What an app says about ITSELF — the `rpc.discover` answer, built from the app. */
2
2
  import { Card } from '@fougere/schema';
3
3
  import type { App } from './types.js';
4
- import { factsAnnouncedBy } from '../emit.js';
4
+ import { factsAnnouncedBy } from '../wire/emit.js';
5
5
  import type { InvocationContext } from '../wire/Invocation.js';
6
6
  import { facadeKeyOf, contractsKeyOf, type CardOp, type IdentityCard } from '../wire/call.js';
7
7
  import type { OperationContract } from '../wire/operation.js';
@@ -1,6 +1,6 @@
1
1
  /** What this app hosts — what it STATED, or what a scan FOUND. */
2
2
  import type { FrondDescriptor } from '../descriptor/frond.js';
3
- import type { ScanResult } from '../scan/result.js';
3
+ import type { ScanResult } from '../scan.js';
4
4
  import { Fronds } from '../descriptor/Fronds.js';
5
5
 
6
6
  /** Where an app's fronds come from — either, or both. */
@@ -0,0 +1,390 @@
1
+ /** Putting one frond into the app being built: its scope, what it serves, what it takes. */
2
+ import type { Container } from '@fougere/container';
3
+ import { lowerFirst, type Fields, type SchemaView } from '@fougere/schema';
4
+ import type { Logger } from '../builtin/logger.js';
5
+ import type { Dispatcher } from '../dispatch/Dispatcher.js';
6
+ import type { RouteRegistry } from '../dispatch/RouteRegistry.js';
7
+ import type { Emissions } from './Emissions.js';
8
+ import type { EffectiveOperationsMap, EffectiveOperationModel } from '../effective-operation.js';
9
+ import { nameOf } from '../descriptor/frond.js';
10
+ import type { EntityEntry, FrondDescriptor, HandlerEntry } from '../descriptor/frond.js';
11
+ import type { OperationsMap } from '../wire/operation.js';
12
+ import type { AppMiddleware } from '../wire/middleware.js';
13
+ import type { CreateAppOptions } from './types.js';
14
+ import { registerFrames } from './together.js';
15
+ import { HandlerFacade } from '../dispatch/HandlerFacade.js';
16
+ import { targetOf } from '../prefab/prefab.js';
17
+ import { ownersOf, refuseStorageInUserCode, refuseCrudOnOwned } from './ownership.js';
18
+ import { StorageGuard } from '../dispatch/StorageGuard.js';
19
+ import { portBindings } from './ports.js';
20
+ import { facadeKeyOf, contractsKeyOf } from '../wire/call.js';
21
+ import { inheritsCrud, subjectOf } from '../prefab/crud.js';
22
+ import { repositoryKeyOf } from '../prefab/repository.js';
23
+ import { storageKeyOf } from '../storage/port.js';
24
+ import { declares } from '../source.js';
25
+ import { presenterKeyOf } from '../prefab/presenter.js';
26
+ import { collectorKeyOf } from '../prefab/collector.js';
27
+ import { RouteAddress } from '../wire/RouteAddress.js';
28
+ import { servedSurfaces } from '../descriptor/surface.js';
29
+ import { OperationRoute } from '../dispatch/OperationRoute.js';
30
+ import { facadeOperations } from '../entry/facade.js';
31
+
32
+ /**
33
+ * What the app is made of while it is still being made. `createApp` builds these, hands
34
+ * them to every frond in turn, and the App it returns is assembled FROM them — so a frond
35
+ * adds to the same container, the same routes and the same tables the next one will read.
36
+ */
37
+ export interface Assembly {
38
+ container: Container;
39
+ routeRegistry: RouteRegistry;
40
+ emissions: Emissions;
41
+ dispatcher: Dispatcher;
42
+ /** A door under a named surface answers only what that surface serves. */
43
+ localDispatcher: Dispatcher;
44
+ /** Filled per façade key, read back by `operationsFor` and the identity card. */
45
+ effectiveByKey: Map<string, EffectiveOperationsMap>;
46
+ /** Every `ports:` key some frond actually settled — what is left is a typo. */
47
+ boundPorts: Set<string>;
48
+ /** What the model resolved before the boot performed any side effect. */
49
+ operationModel: EffectiveOperationModel;
50
+ entityByName: Map<string, SchemaView>;
51
+ frondOf: Map<string, string>;
52
+ contractsOf: (operations: EffectiveOperationsMap) => OperationsMap;
53
+ /** Read at call time and never at boot, so a late registration still applies. */
54
+ getMiddlewares: (entity: string) => AppMiddleware[];
55
+ log: Logger;
56
+ options: CreateAppOptions;
57
+ }
58
+
59
+ export async function installFrond(frond: FrondDescriptor, assembly: Assembly): Promise<void> {
60
+ const {
61
+ container, routeRegistry, emissions, dispatcher, localDispatcher, effectiveByKey,
62
+ boundPorts, operationModel, entityByName, frondOf, contractsOf, getMiddlewares,
63
+ log, options,
64
+ } = assembly;
65
+
66
+ // Declared remote: keep the scanned metadata (bridges route with it),
67
+ // register nothing locally — resolve() falls through to the remote façade.
68
+ if (options.remotes && frond.name in options.remotes) {
69
+ log.child(frond.name).info('declared remote — not hosted locally');
70
+ // Its doors answer elsewhere, but what they LISTEN to was read here.
71
+ for (const handler of frond.handlers) {
72
+ const key = facadeKeyOf(handler.address, handler.surface);
73
+ const operations = operationModel.forHandler(handler);
74
+ effectiveByKey.set(key, operations);
75
+ emissions.note(contractsOf(operations), key);
76
+
77
+ }
78
+ return;
79
+ }
80
+ const scope = container.createScope();
81
+ const frondLog = log.child(frond.name);
82
+
83
+ // `reads:` is what makes a cross-source reader exist here, and the list IS its
84
+ // environment — a source holding none of these is never opened. Registered under
85
+ // the type's own name, which is the key `depKeyOf` already derives for a plain
86
+ // parameter: `constructor(private reads: Reads)` and nothing else to say.
87
+ // Declaring `reads:` with nothing to build the reader is a boot that ignores a
88
+ // clause: the handler asking for `Reads` then dies at its first call, on a
89
+ // container message that names neither the clause nor what is missing.
90
+ if (frond.reads?.length && !options.sourcesFactory) {
91
+ frondLog.warn(
92
+ `[reads] ${frond.reads.join(', ')} — declared in frond.config.ts, but this boot passes no `
93
+ + '`sourcesFactory`, so no reader is registered and a handler asking for `Reads` will fail '
94
+ + 'at its first call. Pass one (`@fougere/adapter-duckdb`), or drop the clause.',
95
+ );
96
+ }
97
+ if (frond.reads?.length && options.sourcesFactory) {
98
+ // Resolved across the WHOLE app, not this frond's own entities: a cross-source
99
+ // query joins entities from different fronds by definition — `Progress` here,
100
+ // `Book` next door — so restricting the list to its own would make it useless.
101
+ // Naming one IS the authorization; that is what the declaration is for.
102
+ const named = frond.reads
103
+ .map((name) => entityByName.get(lowerFirst(name)))
104
+ .filter((entity): entity is NonNullable<typeof entity> => entity !== undefined);
105
+ if (named.length !== frond.reads.length) {
106
+ const missing = frond.reads.filter((name) => !entityByName.has(lowerFirst(name)));
107
+ frondLog.warn(
108
+ `[reads] ${missing.join(', ')} — named in frond.config.ts but scanned nowhere in this app, `
109
+ + 'so a query naming one would find no table. Check the spelling, or the entity file.',
110
+ );
111
+ }
112
+ scope.registerValue('Reads', await options.sourcesFactory(named, frond.name));
113
+ frondLog.debug(`cross-source reader over ${named.length} entit(ies)`);
114
+ }
115
+
116
+ // Who owns what, and the rule that makes owning mean something. Before anything is
117
+ // registered, so a bad line is named by this refusal rather than by the container's.
118
+ const owners = ownersOf(frond.providers);
119
+ refuseStorageInUserCode(frond, owners, (entity) => entityByName.has(entity));
120
+ refuseCrudOnOwned(frond, owners);
121
+
122
+ for (const provider of frond.providers) {
123
+ scope.register(nameOf(provider), provider.ctor, { deps: provider.deps });
124
+ }
125
+ // …and again under the port each one extends, so `private payment: Payment`
126
+ // reaches the realization instead of the base class it is declared against.
127
+ // Registered AFTER the loop above so a port key always wins over the base's
128
+ // own registration — same precedence as a declared repository over its default.
129
+ for (const [port, impl] of portBindings(frond.providers, (n) => scope.has(n), options.ports)) {
130
+ scope.register(port, impl.ctor, { deps: impl.deps });
131
+ boundPorts.add(port);
132
+ frondLog.debug(`port ${port} → ${impl.ctor.name}`);
133
+ }
134
+ if (frond.providers.length > 0) {
135
+ frondLog.debug(`${frond.providers.length} provider(s): ${frond.providers.map(nameOf).join(', ')}`);
136
+ }
137
+
138
+ // Register Storage for each entity — PascalCase type name (e.g. 'PostStorage')
139
+ // When a handler declares Crud(Entity, Output), scope the storage via .output(Output)
140
+ if (options.storageFactory) {
141
+ const unenforced: string[] = [];
142
+ for (const entity of frond.entities) {
143
+ const key = storageKeyOf(entity.name);
144
+ const source = options.sourceOf?.(entity.name) ?? 'db';
145
+ if (declares(entity.entityClass, 'unique') && options.enforces?.(source, 'unique') === false) {
146
+ unenforced.push(`${entity.name} in '${source}'`);
147
+ }
148
+ const baseStorage = options.storageFactory(entity.entityClass, entity.name);
149
+
150
+ // Check if the default handler (no surface) declares an output override
151
+ const defaultHandler = frond.handlers.find((h) => h.address === entity.name && !h.surface);
152
+ const outputSchema = defaultHandler?.outputOverride ?? (defaultHandler?.ctor as any)?.__output;
153
+ const scoped = outputSchema && outputSchema !== entity.entityClass
154
+ ? baseStorage.output(outputSchema)
155
+ : baseStorage;
156
+
157
+ // Storage is a way out like the client surface — see `StorageGuard`.
158
+ const guarded = new StorageGuard(entity.entityClass.getFields(), entity.name).guard(scoped);
159
+ scope.registerValue(key, guarded);
160
+
161
+ // The default repository IS the guarded port — it already answers every gesture a
162
+ // declared one forwards, so the two forms have the same shape and a handler reads
163
+ // `repo.list()` either way. The wrapper that used to sit here (`{ storage: guarded }`)
164
+ // existed to make `repo.storage` true in both, back when `.storage` was the way in.
165
+ //
166
+ // Not registered for an OWNED entity: an aggregate's members are reached through it
167
+ // and nowhere else, and the default would be a second door under a name a handler
168
+ // can spell. Every member is skipped, not just the one the key is named after —
169
+ // that asymmetry was the whole hole.
170
+ const repoKey = repositoryKeyOf(entity.name);
171
+ const owner = owners.get(entity.name);
172
+ if (owner) {
173
+ frondLog.debug(`${entity.name} — owned by ${owner}, no default repository`);
174
+ } else if (!scope.has(repoKey)) {
175
+ scope.registerValue(repoKey, guarded);
176
+ }
177
+ }
178
+ if (frond.entities.length > 0) {
179
+ frondLog.debug(`${frond.entities.length} entity storage(s): ${frond.entities.map((e) => e.name).join(', ')}`);
180
+ }
181
+ // The judge refuses a duplicate it can SEE — the row already stored. Two writes arriving
182
+ // together see the same absence, and only the place they land can refuse the second.
183
+ if (unenforced.length > 0) {
184
+ frondLog.warn(
185
+ `unique declared, and the source does not enforce it: ${unenforced.join(', ')} — `
186
+ + 'two concurrent writes can both pass',
187
+ );
188
+ }
189
+ }
190
+
191
+ // Frames, after the ORMs and before anything that may ask for one. A frame is read
192
+ // from the same `deps` every other port is read from — asking for it IS declaring it,
193
+ // so nothing is registered for a frame nobody wants.
194
+ registerFrames(
195
+ scope,
196
+ [...frond.handlers, ...frond.providers, ...frond.presenters, ...frond.collectors].flatMap((d) => d.deps),
197
+ frond.providers,
198
+ {
199
+ entityByName,
200
+ frondOf,
201
+ hostedHere: (name) => !(options.remotes && name in options.remotes),
202
+ storageFactory: options.storageFactory,
203
+ sourceOf: options.sourceOf,
204
+ transacts: options.transacts,
205
+ transacted: options.transacted,
206
+ log: frondLog,
207
+ },
208
+ );
209
+
210
+ // Register presenters in scope — PascalCase type name (e.g. 'PostPresenter')
211
+ const presenterMap = new Map(frond.presenters.map((p) => [p.entityName, p]));
212
+ for (const presenter of frond.presenters) {
213
+ scope.register(presenterKeyOf(presenter.entityName), presenter.ctor, { deps: presenter.deps });
214
+ }
215
+ if (frond.presenters.length > 0) {
216
+ frondLog.debug(`${frond.presenters.length} presenter(s): ${frond.presenters.map((p) => p.entityName).join(', ')}`);
217
+ }
218
+
219
+ // Register collectors in scope — PascalCase type name (e.g. 'UserCollector')
220
+ const collectorTypeNames = new Set(frond.collectors.map((c) => c.typeName));
221
+ for (const collector of frond.collectors) {
222
+ const key = collectorKeyOf(collector.typeName);
223
+ scope.register(key, collector.ctor, { deps: collector.deps });
224
+ }
225
+ if (frond.collectors.length > 0) {
226
+ frondLog.debug(`${frond.collectors.length} collector(s): ${frond.collectors.map((c) => c.typeName).join(', ')}`);
227
+ }
228
+
229
+ // Build handler facades → registered in ROOT container (public contract)
230
+ const defaultHandlers = frond.handlers.filter((h) => !h.surface);
231
+ const surfaceHandlers = frond.handlers.filter((h) => h.surface);
232
+ const defaultHandlerMap = new Map(defaultHandlers.map((h) => [h.address, h]));
233
+
234
+ /** Build the door of a handler and register it under the audience it serves. */
235
+ const buildFacade = (
236
+ entity: EntityEntry | undefined,
237
+ handler: HandlerEntry,
238
+ targetScope: Container,
239
+ facadeKey: string,
240
+ ) => {
241
+ if (inheritsCrud(handler.ctor) && !entity) {
242
+ // An installed Crud subject may be absent from the local scan.
243
+ frondLog.debug(`${handler.ctor.name} extends Crud() and no scanned entity is named `
244
+ + `'${subjectOf(handler.ctor, handler.address)}' — installed entity, or a missing `
245
+ + `one: no storage will be injected`);
246
+ }
247
+
248
+ const facade = new HandlerFacade(handler, targetScope, {
249
+ key: facadeKey,
250
+ frond: frond.name,
251
+ handlers: frond.handlers,
252
+ operations: operationModel.forHandler(handler),
253
+ collectors: collectorTypeNames,
254
+ presenter: presenterMap.get(handler.address),
255
+ presenterScope: scope,
256
+ middlewares: () => getMiddlewares(handler.address),
257
+ });
258
+ // Emissions use the same contracts and execution path as direct calls.
259
+ emissions.note(facade.contracts, facadeKey);
260
+ // The terms alongside the door, under the same audience — a surface that serves
261
+ // fewer ops describes fewer ops.
262
+ container.registerValue(contractsKeyOf(handler.address, handler.surface), facade.contracts);
263
+ effectiveByKey.set(facadeKey, facade.effectiveOperations);
264
+
265
+ const surfaces = servedSurfaces(frond, handler);
266
+
267
+ for (const operation of facade.contracts.keys()) {
268
+ for (const surface of surfaces) {
269
+ const address = new RouteAddress({
270
+ entity: handler.address,
271
+ operation,
272
+ ...(surface !== undefined ? { surface } : {}),
273
+ });
274
+ routeRegistry.register(new OperationRoute(
275
+ 'local',
276
+ address,
277
+ (call) => facade.execute(operation, call.invocation),
278
+ ));
279
+ }
280
+ }
281
+
282
+ const operations = facadeOperations(
283
+ handler.surface ? localDispatcher : dispatcher,
284
+ handler.address,
285
+ routeRegistry.operationNames(handler.address, handler.surface),
286
+ handler.surface,
287
+ );
288
+ container.registerValue(facadeKey, operations);
289
+ };
290
+
291
+ // A presenter is about an entity — computed fields sit on a shape — so this walks
292
+ // entities. Exposing the instance lazily; the bridge resolves it on first access.
293
+ for (const entity of frond.entities) {
294
+ if (!presenterMap.has(entity.name)) continue;
295
+ const presenterKey = presenterKeyOf(entity.name);
296
+ let presenterInstance: any;
297
+ container.registerValue(presenterKey, new Proxy({} as any, {
298
+ get(_target, prop) {
299
+ if (!presenterInstance) presenterInstance = scope.resolve(presenterKey);
300
+ return presenterInstance[prop];
301
+ },
302
+ }));
303
+ }
304
+
305
+ // A facade is about a handler, so this walks HANDLERS. It walked entities before,
306
+ // which made an entity a precondition for being callable at all: a handler naming
307
+ // none was scanned, then never built, and nothing said so.
308
+ for (const handler of defaultHandlers) {
309
+ // Two ways to know the subject, and the explicit one wins: `Crud(Item)` names the
310
+ // entity it was built on, whatever the handler is called. Otherwise the handler's
311
+ // own name is the only thing pointing at one — and pointing at nothing is legal.
312
+ //
313
+ // By NAME, not by identity: the scanner loads an entity through its own loader and
314
+ // the handler imports it through the runtime's, so the same class arrives as two
315
+ // objects. `===` compares module instances, which is not the question being asked.
316
+ const crudTarget = targetOf(handler.ctor);
317
+ const subject = crudTarget?.name ? lowerFirst(crudTarget.name) : handler.address;
318
+ const entity = frond.entities.find((e) => e.name === subject);
319
+ const facadeKey = facadeKeyOf(handler.address);
320
+ buildFacade(entity, handler, scope, facadeKey);
321
+ frondLog.debug(`${facadeKey} [${Object.keys(container.resolve(facadeKey) as any).join(', ')}]`
322
+ + (entity ? '' : ' — no entity of that name: no storage, no projection, no presenter'));
323
+ }
324
+
325
+ // The dual, and it stays: a shape that declares no operation answers nothing. Said
326
+ // once per entity rather than deduced from a silence.
327
+ for (const entity of frond.entities) {
328
+ if (!defaultHandlerMap.has(entity.name)) {
329
+ frondLog.debug(`${entity.name} — entity only, no handler: exposes nothing`);
330
+ }
331
+ }
332
+
333
+ // Surface handlers — create sub-scope per surface handler with scoped storage
334
+ //
335
+ // Pointing at nothing is legal HERE TOO. This loop used to `continue` when no entity
336
+ // carried the handler's name, so `handlers/public/SearchHandler.ts` with no `Search`
337
+ // entity got no door at all and no line saying why — while the very same handler at
338
+ // the default surface is built and logged. One rule, both surfaces.
339
+ for (const handler of surfaceHandlers) {
340
+ const entity = frond.entities.find((e) => e.name === handler.address);
341
+ const surfaceScope = scope.createScope();
342
+
343
+ // Register scoped storage if output override differs from entity — under the REPOSITORY
344
+ // key, which is what a Crud handler asks for, and under the port's own for a holder
345
+ // that legitimately names it. Registering only the latter left a named surface with
346
+ // no door at all once the façade stopped spelling the storage.
347
+ if (entity && options.storageFactory) {
348
+ const baseStorage = options.storageFactory(entity.entityClass, entity.name);
349
+ const outputSchema = handler.outputOverride ?? (handler.ctor as any).__output;
350
+ const scoped = outputSchema && outputSchema !== entity.entityClass
351
+ ? baseStorage.output(outputSchema)
352
+ : baseStorage;
353
+ // The view is handed over so a filter on a field this door hides is SAID. The
354
+ // guard holds no logger — a warning is the boot's to voice, as a seed's report is.
355
+ const guarded = new StorageGuard(entity.entityClass.getFields(), entity.name, {
356
+ ...(outputSchema && outputSchema !== entity.entityClass
357
+ ? { view: (outputSchema as { getFields(): Fields }).getFields() }
358
+ : {}),
359
+ outOfView: (message) => frondLog.warn(message),
360
+ }).guard(scoped);
361
+ surfaceScope.registerValue(storageKeyOf(entity.name), guarded);
362
+ surfaceScope.registerValue(repositoryKeyOf(entity.name), guarded);
363
+ }
364
+
365
+ const facadeKey = facadeKeyOf(handler.address, handler.surface);
366
+ buildFacade(entity, handler, surfaceScope, facadeKey);
367
+ frondLog.debug(`${facadeKey} [${Object.keys(container.resolve(facadeKey) as any).join(', ')}]`
368
+ + (entity ? '' : ' — no entity of that name: no storage, no projection, no presenter'));
369
+ }
370
+
371
+ // A named surface is closed, so what it contains is a fact worth stating.
372
+ // Saying it at boot is the difference between a rule and a rule you can
373
+ // check: an entity you meant to serve and never wrote a handler for is
374
+ // absent HERE, in one line, instead of being discovered missing later.
375
+ const surfaceNames = [...new Set(surfaceHandlers.map((h) => h.surface as string))].sort();
376
+ for (const surfaceName of surfaceNames) {
377
+ const served = surfaceHandlers
378
+ .filter((h) => h.surface === surfaceName)
379
+ .map((h) => h.address)
380
+ .sort();
381
+ const absent = frond.entities.map((e) => e.name).filter((n) => !served.includes(n));
382
+ frondLog.info(
383
+ `surface '${surfaceName}' — ${served.length} entit${served.length === 1 ? 'y' : 'ies'}: ${served.join(', ')}` +
384
+ (absent.length > 0 ? ` (not served: ${absent.join(', ')})` : ''),
385
+ );
386
+ }
387
+
388
+ container.registerValue(`frond:${frond.name}`, scope);
389
+ frondLog.info(`registered — ${frond.entities.length} entities, ${frond.handlers.length} handlers, ${frond.seeds.length} seeds`);
390
+ }
@@ -1,9 +1,9 @@
1
1
  import { lowerFirst } from '@fougere/schema';
2
- import type { FrondDescriptor, ProviderEntry } from '../descriptor/frond.js';
2
+ import { nameOf, type FrondDescriptor, type ProviderEntry } from '../descriptor/frond.js';
3
3
  import { inheritsCrud } from '../prefab/crud.js';
4
4
  import { targetOf } from '../prefab/prefab.js';
5
5
  import { ownedBy, repositoryKeyOf } from '../prefab/repository.js';
6
- import { entityOfStorageKey } from '../storage.js';
6
+ import { entityOfStorageKey } from '../storage/port.js';
7
7
 
8
8
  /** Who owns an entity's storage. */
9
9
  export function ownersOf(providers: readonly ProviderEntry[]): Map<string, string> {
@@ -53,6 +53,21 @@ export function refuseStorageInUserCode(
53
53
  ];
54
54
  const holders = frond.providers.map((p) => ({ ...p, kind: 'provider' as const }));
55
55
 
56
+ for (const provider of holders) {
57
+ // The key the boot registers it under, which a bundler may have renamed — the same
58
+ // reading `ProviderEntry.name` exists for.
59
+ const registered = nameOf(provider);
60
+ const held = entityOfStorageKey(registered, known);
61
+ if (!held) continue;
62
+
63
+ throw new Error(
64
+ `[storage] ${registered} is a provider, and it is the container key of ${held}'s `
65
+ + `own storage — the storage is registered second, so the provider is never resolved.\n`
66
+ + ` Name it for what it holds; \`<Entity>Storage\` belongs to the entity.\n`
67
+ + ` ${provider.filePath}`,
68
+ );
69
+ }
70
+
56
71
  for (const decl of [...doors, ...holders]) {
57
72
  const allowed = decl.kind === 'provider' ? builtOn(decl.ctor) : [];
58
73
  for (const dep of decl.deps) {
package/src/boot/seed.ts CHANGED
@@ -4,8 +4,15 @@ import type { FrondDescriptor, SeedEntry, SeedFactory } from '../descriptor/fron
4
4
  import type { App } from './types.js';
5
5
  import type { Extension } from './AppLifecycle.js';
6
6
 
7
+ /** What the ordering satisfied, and what no order can. */
8
+ export interface SeedOrder {
9
+ ordered: SeedEntry[];
10
+ /** Seeds whose `ref()` targets wait on each other — no order plants them all. */
11
+ cycle: SeedEntry[];
12
+ }
13
+
7
14
  /** Seeds in dependency order — a `ref()` target is planted before its referrer. */
8
- export function orderSeeds(fronds: FrondDescriptor[]): SeedEntry[] {
15
+ export function orderSeeds(fronds: FrondDescriptor[]): SeedOrder {
9
16
  const refs = new Map<string, Set<string>>();
10
17
  for (const frond of fronds) {
11
18
  for (const entity of frond.entities) {
@@ -45,7 +52,7 @@ export function orderSeeds(fronds: FrondDescriptor[]): SeedEntry[] {
45
52
  waiting.delete(ready);
46
53
  }
47
54
 
48
- return [...ordered, ...waiting.keys()];
55
+ return { ordered, cycle: [...waiting.keys()] };
49
56
  }
50
57
 
51
58
  /** Where a seed writes, and what it may skip — resolved per entity. */
@@ -120,7 +127,15 @@ export function seeding(report?: (message: string) => void): Extension {
120
127
  return {
121
128
  name: 'seeds',
122
129
  up: async (app: App) => {
123
- const seeds = orderSeeds(app.fronds);
130
+ const { ordered, cycle } = orderSeeds(app.fronds);
131
+ if (cycle.length > 0) {
132
+ report?.(
133
+ ` ${cycle.map((seed) => seed.entityName).join(', ')}: a ref() cycle, and no order `
134
+ + 'plants them all — planted in the order they are declared, and the source answers for the rest',
135
+ );
136
+ }
137
+
138
+ const seeds = [...ordered, ...cycle];
124
139
  if (seeds.length > 0) await runSeeds(app, seeds, report);
125
140
  },
126
141
  };
@@ -1,6 +1,6 @@
1
1
  /** Where a STATED contract and the signature it is about have stopped agreeing. */
2
2
  import type { FrondDescriptor, HandlerEntry } from '../descriptor/frond.js';
3
- import type { ScanDiagnostic } from '../scan/result.js';
3
+ import type { ScanDiagnostic } from '../scan.js';
4
4
  import type { BindingPlan } from '../wire/binding.js';
5
5
  import type { Signature } from '../wire/signature.js';
6
6
 
@@ -2,7 +2,7 @@
2
2
  import { ambient } from '#ambient';
3
3
  import { upperFirst, lowerFirst, type SchemaView } from '@fougere/schema';
4
4
  import type { Container } from '@fougere/container';
5
- import { membersOfTogetherKey, storageKeyOf, entityOfStorageKey, type Storage, type StorageFactory } from '../storage.js';
5
+ import { membersOfTogetherKey, storageKeyOf, entityOfStorageKey, type Storage, type StorageFactory } from '../storage/port.js';
6
6
  import type { Logger } from '../builtin/logger.js';
7
7
  import type { ProviderEntry } from '../descriptor/frond.js';
8
8
  import { StorageGuard } from '../dispatch/StorageGuard.js';
package/src/boot/types.ts CHANGED
@@ -2,9 +2,9 @@
2
2
  import type { Container } from '@fougere/container';
3
3
  import type { Fronds } from '../descriptor/Fronds.js';
4
4
  import type { FrondDescriptor } from '../descriptor/frond.js';
5
- import type { ScanResult } from '../scan/result.js';
5
+ import type { ScanResult } from '../scan.js';
6
6
  import type { SchemaView } from '@fougere/schema';
7
- import type { StorageFactory } from '../storage.js';
7
+ import type { StorageFactory } from '../storage/port.js';
8
8
  import type { Constraint } from '../source.js';
9
9
  import type { AppMiddleware } from '../wire/middleware.js';
10
10
  import type { RpcAnswer, Transport } from '../wire/call.js';
@@ -2,7 +2,7 @@ import { resolve } from 'node:path';
2
2
  import { existsSync } from 'node:fs';
3
3
  import type { AuthConfig } from './boot/auth.js';
4
4
  import type { LogLevel } from './builtin/logger.js';
5
- import type { ConventionsInput } from './scan/conventions.js';
5
+ import type { ConventionsInput } from './conventions.js';
6
6
  import { getModuleLoader } from './loader.js';
7
7
 
8
8
  // ── Types ────────────────────────────────────────
package/src/contract.ts CHANGED
@@ -9,8 +9,8 @@ import type { FrondCall } from './wire/call.js';
9
9
  export { FougereError, ErrorCode, validationErrorsOf } from './wire/errors.js';
10
10
  // What a read may ask about one field. Here rather than on the main entry because an
11
11
  // adapter reads it to compile a query, and an adapter carries no boot.
12
- export { comparisonOf, comparisonsIn, unknownIn, COMPARISONS } from './criterion.js';
13
- export type { Comparison, ComparisonName } from './criterion.js';
12
+ export { comparisonOf, comparisonsIn, unknownIn, COMPARISONS } from './storage/criterion.js';
13
+ export type { Comparison, ComparisonName } from './storage/criterion.js';
14
14
  // A receiver turns a refusal into what may cross a process boundary, and it is not
15
15
  // core's boot — reaching it through the main entry dragged the scanner into a bundle.
16
16
  export { toPublicError } from './wire/http-error.js';
@@ -24,13 +24,6 @@ export function unb64url(input: string): Uint8Array {
24
24
  }
25
25
 
26
26
  /** Plain base64, padded — what an environment variable carries a PEM as. */
27
- export function b64(input: Uint8Array | string): string {
28
- const bytes = typeof input === 'string' ? bytesOf(input) : input;
29
- let binary = '';
30
- for (const byte of bytes) binary += String.fromCharCode(byte);
31
- return btoa(binary);
32
- }
33
-
34
27
  export const unb64 = (input: string): Uint8Array => {
35
28
  const binary = atob(input);
36
29
  const bytes = new Uint8Array(binary.length);
package/src/declare.ts CHANGED
@@ -4,7 +4,7 @@ import type {
4
4
  CollectorEntry, EntityEntry, FrondDescriptor, HandlerEntry,
5
5
  PresenterEntry, ProviderEntry, SeedEntry,
6
6
  } from './descriptor/frond.js';
7
- import { DEFAULT_CONVENTIONS } from './scan/conventions.js';
7
+ import { DEFAULT_CONVENTIONS } from './conventions.js';
8
8
  import { getPresenterFields } from './prefab/presenter.js';
9
9
  import type { OperationContract } from './wire/operation.js';
10
10
 
@@ -0,0 +1,15 @@
1
+ /**
2
+ * What it takes to PRODUCE a descriptor — the surface a scan reads, whoever wrote it.
3
+ * The main entry serves what an app RUNS; this one serves what fills it in.
4
+ */
5
+ export * from './frond.js';
6
+ export { Fronds } from './Fronds.js';
7
+
8
+ export { cardinalityOf, type OperationContract, type OperationsMap, type Param, type TypeRef } from '../wire/operation.js';
9
+ export { computeBindingPlan, type BindingPlan } from '../wire/binding.js';
10
+ export type { Signature } from '../wire/signature.js';
11
+ export { emitKeyOf } from '../wire/emit.js';
12
+ export { storageKeyOf } from '../storage/port.js';
13
+ export { getPresenterFields } from '../prefab/presenter.js';
14
+ export { targetOf, viewsOf, outputOf } from '../prefab/prefab.js';
15
+ export { ownedBy, repositoryKeyOf } from '../prefab/repository.js';
@@ -0,0 +1,23 @@
1
+ import type { FrondDescriptor, HandlerEntry } from './frond.js';
2
+
3
+ /**
4
+ * The surfaces a handler answers on — its own when it has one, otherwise the default
5
+ * and every surface that names its address without opening a door of its own.
6
+ * `undefined` is the default surface, as `facadeKeyOf` and `RouteAddress` spell it.
7
+ */
8
+ export function servedSurfaces(
9
+ frond: FrondDescriptor,
10
+ handler: HandlerEntry,
11
+ ): (string | undefined)[] {
12
+ if (handler.surface) return [handler.surface];
13
+
14
+ const declared = Object.entries(frond.surfaces ?? {})
15
+ .filter(([surface, addresses]) =>
16
+ addresses.some((address) => address.toLowerCase() === handler.address.toLowerCase())
17
+ && !frond.handlers.some((other) =>
18
+ other.address === handler.address && other.surface === surface))
19
+ .map(([surface]) => surface)
20
+ .sort();
21
+
22
+ return [undefined, ...declared];
23
+ }
@@ -12,12 +12,13 @@ import { presenterKeyOf } from '../prefab/presenter.js';
12
12
  import { repositoryKeyOf } from '../prefab/repository.js';
13
13
  import type { OperationContract, OperationsMap } from '../wire/operation.js';
14
14
  import type { EffectiveOperation, EffectiveOperationsMap } from '../effective-operation.js';
15
+ import type { BindingPlan } from '../wire/binding.js';
15
16
  import { canonicalInvocation, type InvocationContext } from '../wire/Invocation.js';
16
17
  import type { HandlerEntry, PresenterEntry } from '../descriptor/frond.js';
17
18
  import { ArgumentResolver } from './ArgumentResolver.js';
18
19
  import { OutputView } from './OutputView.js';
19
20
  import { PresenterExecutor } from './PresenterExecutor.js';
20
- import { presenterArguments } from './presenterArguments.js';
21
+ import { presenterArguments, presenterPlans } from './presenterArguments.js';
21
22
  import { validateInput } from './validateInput.js';
22
23
 
23
24
  /** What boot resolved around one handler, beyond the handler and the scope it resolves in. */
@@ -54,6 +55,8 @@ export class HandlerFacade {
54
55
  private readonly arguments = new ArgumentResolver(
55
56
  (typeName) => this.collectorResolver(typeName),
56
57
  );
58
+ /** A computed field's parameters and the collectors in scope are both boot-time facts. */
59
+ private readonly presenterPlans: Map<string, BindingPlan>;
57
60
 
58
61
  constructor(
59
62
  private readonly handler: HandlerEntry,
@@ -62,6 +65,10 @@ export class HandlerFacade {
62
65
  ) {
63
66
  this.refuseCrudWithoutRepository(handler);
64
67
 
68
+ this.presenterPlans = door.presenter
69
+ ? presenterPlans(door.presenter, door.collectors)
70
+ : new Map();
71
+
65
72
  scope.register(this.handlerKey, handler.ctor, { deps: this.depsOf(handler) });
66
73
 
67
74
  this.effectiveOperations = door.operations;
@@ -135,7 +142,7 @@ export class HandlerFacade {
135
142
  op,
136
143
  );
137
144
 
138
- return presenterArguments(presenter, invocation, this.arguments, this.door.collectors)
145
+ return presenterArguments(this.presenterPlans, invocation, this.arguments)
139
146
  .then((args) => executor.present(output, args));
140
147
  }
141
148