@fougere/core 0.5.0-alpha.0 → 0.6.0-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 (229) hide show
  1. package/dist/boot/Emissions.d.ts +2 -2
  2. package/dist/boot/Emissions.d.ts.map +1 -1
  3. package/dist/boot/Emissions.js +1 -1
  4. package/dist/boot/HandlerFacade.d.ts +1 -1
  5. package/dist/boot/HandlerFacade.d.ts.map +1 -1
  6. package/dist/boot/HandlerFacade.js +1 -1
  7. package/dist/boot/HandlerFacade.js.map +1 -1
  8. package/dist/boot/auth.d.ts +6 -6
  9. package/dist/boot/auth.d.ts.map +1 -1
  10. package/dist/boot/boot.d.ts +2 -2
  11. package/dist/boot/boot.d.ts.map +1 -1
  12. package/dist/boot/boot.js +1 -1
  13. package/dist/boot/boot.js.map +1 -1
  14. package/dist/boot/bootstrap.d.ts.map +1 -1
  15. package/dist/boot/bootstrap.js +47 -38
  16. package/dist/boot/bootstrap.js.map +1 -1
  17. package/dist/boot/card.d.ts +21 -0
  18. package/dist/boot/card.d.ts.map +1 -0
  19. package/dist/boot/card.js +100 -0
  20. package/dist/boot/card.js.map +1 -0
  21. package/dist/boot/frame.d.ts +3 -3
  22. package/dist/boot/frame.d.ts.map +1 -1
  23. package/dist/boot/frame.js +7 -7
  24. package/dist/boot/frame.js.map +1 -1
  25. package/dist/boot/hosted.d.ts +8 -2
  26. package/dist/boot/hosted.d.ts.map +1 -1
  27. package/dist/boot/hosted.js +10 -10
  28. package/dist/boot/hosted.js.map +1 -1
  29. package/dist/boot/ownership.d.ts +7 -5
  30. package/dist/boot/ownership.d.ts.map +1 -1
  31. package/dist/boot/ownership.js +9 -14
  32. package/dist/boot/ownership.js.map +1 -1
  33. package/dist/boot/ports.d.ts +2 -2
  34. package/dist/boot/ports.d.ts.map +1 -1
  35. package/dist/boot/ports.js +1 -1
  36. package/dist/boot/runner.d.ts +28 -0
  37. package/dist/boot/runner.d.ts.map +1 -0
  38. package/dist/boot/runner.js +26 -0
  39. package/dist/boot/runner.js.map +1 -0
  40. package/dist/boot/seed.d.ts +2 -2
  41. package/dist/boot/seed.d.ts.map +1 -1
  42. package/dist/boot/seed.js +5 -5
  43. package/dist/boot/seed.js.map +1 -1
  44. package/dist/boot/statement-drift.d.ts +2 -1
  45. package/dist/boot/statement-drift.d.ts.map +1 -1
  46. package/dist/boot/statement-drift.js.map +1 -1
  47. package/dist/boot/together.d.ts +4 -4
  48. package/dist/boot/together.d.ts.map +1 -1
  49. package/dist/boot/together.js +17 -19
  50. package/dist/boot/together.js.map +1 -1
  51. package/dist/boot/types.d.ts +12 -11
  52. package/dist/boot/types.d.ts.map +1 -1
  53. package/dist/contract/CallLog.d.ts +2 -2
  54. package/dist/contract/CallLog.d.ts.map +1 -1
  55. package/dist/contract/RouteAddress.d.ts +2 -0
  56. package/dist/contract/RouteAddress.d.ts.map +1 -1
  57. package/dist/contract/RouteAddress.js.map +1 -1
  58. package/dist/{scan/declare.d.ts → declare.d.ts} +3 -3
  59. package/dist/declare.d.ts.map +1 -0
  60. package/dist/{scan/declare.js → declare.js} +3 -3
  61. package/dist/declare.js.map +1 -0
  62. package/dist/{scan → descriptor}/Fronds.d.ts +6 -7
  63. package/dist/descriptor/Fronds.d.ts.map +1 -0
  64. package/dist/{scan → descriptor}/Fronds.js +6 -7
  65. package/dist/descriptor/Fronds.js.map +1 -0
  66. package/dist/{scan → descriptor}/frond.d.ts +7 -47
  67. package/dist/descriptor/frond.d.ts.map +1 -0
  68. package/dist/{scan → descriptor}/frond.js.map +1 -1
  69. package/dist/dispatch/ArgumentResolver.d.ts +5 -2
  70. package/dist/dispatch/ArgumentResolver.d.ts.map +1 -1
  71. package/dist/dispatch/ArgumentResolver.js +48 -3
  72. package/dist/dispatch/ArgumentResolver.js.map +1 -1
  73. package/dist/dispatch/DispatchEvent.d.ts +1 -1
  74. package/dist/dispatch/DispatchEvent.d.ts.map +1 -1
  75. package/dist/dispatch/PresenterArgumentResolver.d.ts +1 -1
  76. package/dist/dispatch/PresenterArgumentResolver.d.ts.map +1 -1
  77. package/dist/dispatch/PresenterArgumentResolver.js +1 -1
  78. package/dist/dispatch/Route.d.ts +1 -2
  79. package/dist/dispatch/Route.d.ts.map +1 -1
  80. package/dist/dispatch/StorageGuard.d.ts +2 -2
  81. package/dist/dispatch/StorageGuard.d.ts.map +1 -1
  82. package/dist/dispatch/StorageGuard.js +7 -7
  83. package/dist/dispatch/StorageGuard.js.map +1 -1
  84. package/dist/effective-operation.d.ts +29 -3
  85. package/dist/effective-operation.d.ts.map +1 -1
  86. package/dist/effective-operation.js +52 -1
  87. package/dist/effective-operation.js.map +1 -1
  88. package/dist/emit.d.ts +1 -1
  89. package/dist/frond-config.d.ts +2 -2
  90. package/dist/graph.d.ts +1 -1
  91. package/dist/graph.d.ts.map +1 -1
  92. package/dist/imports.d.ts +1 -1
  93. package/dist/imports.d.ts.map +1 -1
  94. package/dist/index.d.ts +15 -10
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +9 -6
  97. package/dist/index.js.map +1 -1
  98. package/dist/node.d.ts +2 -2
  99. package/dist/node.d.ts.map +1 -1
  100. package/dist/node.js +2 -2
  101. package/dist/node.js.map +1 -1
  102. package/dist/prefab/collector.d.ts +2 -2
  103. package/dist/prefab/collector.js +2 -2
  104. package/dist/prefab/crud.d.ts +5 -5
  105. package/dist/prefab/crud.d.ts.map +1 -1
  106. package/dist/prefab/crud.js +12 -12
  107. package/dist/prefab/crud.js.map +1 -1
  108. package/dist/prefab/mirror.d.ts +5 -5
  109. package/dist/prefab/mirror.d.ts.map +1 -1
  110. package/dist/prefab/mirror.js +5 -5
  111. package/dist/prefab/mirror.js.map +1 -1
  112. package/dist/prefab/presenter.d.ts +2 -2
  113. package/dist/prefab/presenter.js +2 -2
  114. package/dist/prefab/repository.d.ts +14 -14
  115. package/dist/prefab/repository.d.ts.map +1 -1
  116. package/dist/prefab/repository.js +19 -19
  117. package/dist/prefab/repository.js.map +1 -1
  118. package/dist/rows.d.ts +40 -0
  119. package/dist/rows.d.ts.map +1 -0
  120. package/dist/rows.js +146 -0
  121. package/dist/rows.js.map +1 -0
  122. package/dist/scan/conventions.d.ts +8 -12
  123. package/dist/scan/conventions.d.ts.map +1 -1
  124. package/dist/scan/conventions.js +0 -13
  125. package/dist/scan/conventions.js.map +1 -1
  126. package/dist/scan/emit.d.ts +1 -1
  127. package/dist/scan/emit.d.ts.map +1 -1
  128. package/dist/scan/emit.js +1 -1
  129. package/dist/scan/emit.js.map +1 -1
  130. package/dist/scan/handler-parser.d.ts +11 -56
  131. package/dist/scan/handler-parser.d.ts.map +1 -1
  132. package/dist/scan/handler-parser.js +2 -2
  133. package/dist/scan/handler-parser.js.map +1 -1
  134. package/dist/scan/result.d.ts +41 -0
  135. package/dist/scan/result.d.ts.map +1 -0
  136. package/dist/scan/result.js +2 -0
  137. package/dist/scan/result.js.map +1 -0
  138. package/dist/scan/scanner.d.ts +18 -1
  139. package/dist/scan/scanner.d.ts.map +1 -1
  140. package/dist/scan/scanner.js +34 -17
  141. package/dist/scan/scanner.js.map +1 -1
  142. package/dist/scan/statement.d.ts +1 -1
  143. package/dist/scan/statement.d.ts.map +1 -1
  144. package/dist/scan/statement.js.map +1 -1
  145. package/dist/{orm.d.ts → storage.d.ts} +98 -16
  146. package/dist/storage.d.ts.map +1 -0
  147. package/dist/{orm.js → storage.js} +53 -5
  148. package/dist/storage.js.map +1 -0
  149. package/dist/verify.d.ts +1 -1
  150. package/dist/verify.d.ts.map +1 -1
  151. package/dist/verify.js +2 -2
  152. package/dist/verify.js.map +1 -1
  153. package/dist/{boot → wire}/binding.d.ts +2 -10
  154. package/dist/wire/binding.d.ts.map +1 -0
  155. package/dist/{boot → wire}/binding.js +0 -52
  156. package/dist/wire/binding.js.map +1 -0
  157. package/dist/wire/call.d.ts +2 -40
  158. package/dist/wire/call.d.ts.map +1 -1
  159. package/dist/wire/call.js +0 -125
  160. package/dist/wire/call.js.map +1 -1
  161. package/dist/wire/operation.d.ts +5 -31
  162. package/dist/wire/operation.d.ts.map +1 -1
  163. package/dist/wire/operation.js +0 -51
  164. package/dist/wire/operation.js.map +1 -1
  165. package/dist/wire/signature.d.ts +58 -0
  166. package/dist/wire/signature.d.ts.map +1 -0
  167. package/dist/wire/signature.js +2 -0
  168. package/dist/wire/signature.js.map +1 -0
  169. package/package.json +3 -3
  170. package/src/boot/Emissions.ts +2 -2
  171. package/src/boot/HandlerFacade.ts +3 -3
  172. package/src/boot/auth.ts +6 -6
  173. package/src/boot/boot.ts +3 -3
  174. package/src/boot/bootstrap.ts +50 -39
  175. package/src/boot/card.ts +113 -0
  176. package/src/boot/frame.ts +7 -7
  177. package/src/boot/hosted.ts +11 -14
  178. package/src/boot/ownership.ts +14 -15
  179. package/src/boot/ports.ts +2 -2
  180. package/src/boot/runner.ts +34 -0
  181. package/src/boot/seed.ts +6 -6
  182. package/src/boot/statement-drift.ts +5 -4
  183. package/src/boot/together.ts +24 -25
  184. package/src/boot/types.ts +12 -11
  185. package/src/contract/CallLog.ts +2 -2
  186. package/src/contract/RouteAddress.ts +3 -0
  187. package/src/{scan/declare.ts → declare.ts} +5 -5
  188. package/src/{scan → descriptor}/Fronds.ts +6 -7
  189. package/src/{scan → descriptor}/frond.ts +7 -48
  190. package/src/dispatch/ArgumentResolver.ts +53 -7
  191. package/src/dispatch/DispatchEvent.ts +1 -1
  192. package/src/dispatch/PresenterArgumentResolver.ts +2 -2
  193. package/src/dispatch/Route.ts +1 -3
  194. package/src/dispatch/StorageGuard.ts +7 -7
  195. package/src/effective-operation.ts +63 -11
  196. package/src/emit.ts +1 -1
  197. package/src/frond-config.ts +2 -2
  198. package/src/graph.ts +1 -1
  199. package/src/imports.ts +1 -1
  200. package/src/index.ts +15 -13
  201. package/src/node.ts +2 -2
  202. package/src/prefab/collector.ts +2 -2
  203. package/src/prefab/crud.ts +18 -18
  204. package/src/prefab/mirror.ts +8 -8
  205. package/src/prefab/presenter.ts +2 -2
  206. package/src/prefab/repository.ts +32 -32
  207. package/src/rows.ts +155 -0
  208. package/src/scan/conventions.ts +0 -18
  209. package/src/scan/emit.ts +3 -5
  210. package/src/scan/handler-parser.ts +14 -67
  211. package/src/scan/result.ts +42 -0
  212. package/src/scan/scanner.ts +44 -21
  213. package/src/scan/statement.ts +2 -1
  214. package/src/{orm.ts → storage.ts} +126 -17
  215. package/src/verify.ts +3 -3
  216. package/src/{boot → wire}/binding.ts +2 -66
  217. package/src/wire/call.ts +1 -129
  218. package/src/wire/operation.ts +5 -62
  219. package/src/wire/signature.ts +59 -0
  220. package/dist/boot/binding.d.ts.map +0 -1
  221. package/dist/boot/binding.js.map +0 -1
  222. package/dist/orm.d.ts.map +0 -1
  223. package/dist/orm.js.map +0 -1
  224. package/dist/scan/Fronds.d.ts.map +0 -1
  225. package/dist/scan/Fronds.js.map +0 -1
  226. package/dist/scan/declare.d.ts.map +0 -1
  227. package/dist/scan/declare.js.map +0 -1
  228. package/dist/scan/frond.d.ts.map +0 -1
  229. /package/dist/{scan → descriptor}/frond.js +0 -0
@@ -10,9 +10,9 @@
10
10
  import { ambient } from '#ambient';
11
11
  import { upperFirst, lowerFirst, type SchemaView } from '@fougere/schema';
12
12
  import type { Container } from '@fougere/container';
13
- import { membersOfTogetherKey, ormKeyOf, type EntityOrm, type OrmFactory } from '../orm.js';
13
+ import { membersOfTogetherKey, storageKeyOf, entityOfStorageKey, type Storage, type StorageFactory } from '../storage.js';
14
14
  import type { Logger } from '../builtins/logger.js';
15
- import type { ProviderEntry } from '../scan/frond.js';
15
+ import type { ProviderEntry } from '../descriptor/frond.js';
16
16
  import { StorageGuard } from '../dispatch/StorageGuard.js';
17
17
  import { recording, unwind, type Undo } from './frame.js';
18
18
 
@@ -23,9 +23,9 @@ export interface FrameWorld {
23
23
  /** Which frond holds an entity — the question `remotes:` turns into a refusal. */
24
24
  frondOf: Map<string, string>;
25
25
  hostedHere: (frond: string) => boolean;
26
- ormFactory?: OrmFactory;
26
+ storageFactory?: StorageFactory;
27
27
  sourceOf?: (entityName: string) => string;
28
- transacted?: <R>(source: string, fn: (ormFactory: OrmFactory) => Promise<R>) => Promise<R>;
28
+ transacted?: <R>(source: string, fn: (storageFactory: StorageFactory) => Promise<R>) => Promise<R>;
29
29
  log: Logger;
30
30
  }
31
31
 
@@ -74,7 +74,7 @@ function resolve(names: { entities: string[]; providers: string[] }, declared: r
74
74
  /**
75
75
  * A member that cannot write here at all — its frond is hosted elsewhere.
76
76
  *
77
- * Refused rather than compensated: a compensated frame still writes through a local ORM,
77
+ * Refused rather than compensated: a compensated frame still writes through a local storage,
78
78
  * and a remote frond registers none. There is nothing to record and nothing to undo.
79
79
  */
80
80
  function refuseRemote(members: Members, world: FrameWorld, key: string): void {
@@ -96,13 +96,12 @@ function refuseRemote(members: Members, world: FrameWorld, key: string): void {
96
96
  * The frame does not widen itself to cover it: doing so would pull an entity from another
97
97
  * source into the group without anyone writing it down. Named instead, with the one-word fix.
98
98
  */
99
- function refuseUncoveredWrites(members: Members, key: string): void {
99
+ function refuseUncoveredWrites(members: Members, key: string, world: FrameWorld): void {
100
100
  const covered = new Set(members.entities.map((member) => member.name));
101
101
  for (const provider of members.providers) {
102
102
  for (const dep of provider.deps) {
103
- if (!dep.endsWith('Orm')) continue;
104
- const entity = lowerFirst(dep.slice(0, -'Orm'.length));
105
- if (covered.has(entity)) continue;
103
+ const entity = entityOfStorageKey(dep, (name) => world.entityByName.has(name));
104
+ if (!entity || covered.has(entity)) continue;
106
105
  throw new Error(
107
106
  `Together<[…]> (${key}): ${provider.ctor.name} writes ${entity}, which is not in the ` +
108
107
  `frame's entity list — its writes would escape the unwind. Add it: ` +
@@ -115,30 +114,30 @@ function refuseUncoveredWrites(members: Members, key: string): void {
115
114
  /**
116
115
  * Open the block: build every member over `factory`, in a scope of their own.
117
116
  *
118
- * The scope is what makes a provider member work without a locator — its ORM keys are
117
+ * The scope is what makes a provider member work without a locator — its storage keys are
119
118
  * registered here, so the container hands it the framed ones through the ordinary
120
119
  * constructor. It is disposed when the block ends, whichever way it ended.
121
120
  */
122
121
  async function inScope<R>(
123
122
  parent: Container,
124
123
  members: Members,
125
- factory: OrmFactory,
126
- wrap: (orm: EntityOrm, name: string, schema: SchemaView) => EntityOrm,
124
+ factory: StorageFactory,
125
+ wrap: (storage: Storage, name: string, schema: SchemaView) => Storage,
127
126
  fn: (entities: unknown[], providers: unknown[]) => Promise<R>,
128
127
  ): Promise<R> {
129
128
  const scope = parent.createScope();
130
129
  try {
131
- const orms = members.entities.map((member) => {
132
- const orm = wrap(factory(member.schema, member.name), member.name, member.schema);
133
- scope.registerValue(ormKeyOf(member.name), orm);
134
- return orm;
130
+ const storages = members.entities.map((member) => {
131
+ const storage = wrap(factory(member.schema, member.name), member.name, member.schema);
132
+ scope.registerValue(storageKeyOf(member.name), storage);
133
+ return storage;
135
134
  });
136
- // Providers after every ORM is in place: one may depend on another member's.
135
+ // Providers after every storage is in place: one may depend on another member's.
137
136
  const built = members.providers.map((provider) => {
138
137
  scope.register(provider.ctor.name, provider.ctor, { deps: provider.deps });
139
138
  return scope.resolve(provider.ctor.name);
140
139
  });
141
- return await fn(orms, built);
140
+ return await fn(storages, built);
142
141
  } finally {
143
142
  await scope.dispose();
144
143
  }
@@ -162,17 +161,17 @@ export function registerFrames(
162
161
  const names = membersOfTogetherKey(key);
163
162
  if (!names) continue;
164
163
  registered += 1;
165
- if (!world.ormFactory) {
164
+ if (!world.storageFactory) {
166
165
  throw new Error(`Together<[${names.entities.join(', ')}]> needs storage, and this boot declares none.`);
167
166
  }
168
167
 
169
168
  const members = resolve(names, providers, world);
170
169
  refuseRemote(members, world, key);
171
- refuseUncoveredWrites(members, key);
170
+ refuseUncoveredWrites(members, key, world);
172
171
 
173
172
  const sources = new Set(members.entities.map((member) => world.sourceOf?.(member.name) ?? 'db'));
174
- const judge = (orm: EntityOrm, name: string, schema: SchemaView) =>
175
- new StorageGuard(schema.getFields(), name).guard(orm);
173
+ const judge = (storage: Storage, name: string, schema: SchemaView) =>
174
+ new StorageGuard(schema.getFields(), name).guard(storage);
176
175
 
177
176
  // One engine and a way into it: the engine gives the unwind AND the isolation.
178
177
  if (world.transacted && sources.size === 1) {
@@ -196,10 +195,10 @@ export function registerFrames(
196
195
  scope.registerValue(key, {
197
196
  run: <R>(fn: (entities: never, providers: never) => Promise<R>): Promise<R> => ambient.enterFrame(key, async () => {
198
197
  const journal: Undo[] = [];
199
- const record = (orm: EntityOrm, name: string, schema: SchemaView) =>
200
- judge(recording(orm, name, schema.getFields(), journal), name, schema);
198
+ const record = (storage: Storage, name: string, schema: SchemaView) =>
199
+ judge(recording(storage, name, schema.getFields(), journal), name, schema);
201
200
  try {
202
- return await inScope(scope, members, world.ormFactory!, record, fn as never);
201
+ return await inScope(scope, members, world.storageFactory!, record, fn as never);
203
202
  } catch (cause) {
204
203
  return unwind(journal, cause, world.log);
205
204
  }
package/src/boot/types.ts CHANGED
@@ -6,10 +6,11 @@
6
6
  * landed in it, seventeen notions deep.
7
7
  */
8
8
  import type { Container } from '@fougere/container';
9
- import type { Fronds } from '../scan/Fronds.js';
10
- import type { FrondDescriptor, ScanResult } from '../scan/frond.js';
9
+ import type { Fronds } from '../descriptor/Fronds.js';
10
+ import type { FrondDescriptor } from '../descriptor/frond.js';
11
+ import type { ScanResult } from '../scan/result.js';
11
12
  import type { SchemaView } from '@fougere/schema';
12
- import type { OrmFactory } from '../orm.js';
13
+ import type { StorageFactory } from '../storage.js';
13
14
  import type { AppMiddleware } from '../wire/middleware.js';
14
15
  import type { RpcAnswer, Transport } from '../wire/call.js';
15
16
  import type { Extension } from './AppLifecycle.js';
@@ -22,23 +23,23 @@ import type { DispatchPort } from '../dispatch/DispatchPort.js';
22
23
  export interface CreateAppOptions {
23
24
  /** Factory function to create the container. Required. */
24
25
  createContainer: () => Container;
25
- /** Factory to auto-generate EntityOrm for each scanned entity. */
26
- ormFactory?: OrmFactory;
26
+ /** Factory to auto-generate Storage for each scanned entity. */
27
+ storageFactory?: StorageFactory;
27
28
  /**
28
29
  * Which source an entity's rows live in, and how to open a transaction on one — the two
29
30
  * questions that decide whether `Together<[…]>` gets the engine's own unwind or replays
30
31
  * inverses itself.
31
32
  *
32
- * Optional together, because a host may hand in a bare `ormFactory` and know neither. A
33
+ * Optional together, because a host may hand in a bare `storageFactory` and know neither. A
33
34
  * frame then compensates: not knowing where the rows are and promising atomicity over
34
35
  * them are two different things, and only one of them is honest.
35
36
  */
36
37
  sourceOf?: (entityName: string) => string;
37
- transacted?: <R>(source: string, fn: (ormFactory: OrmFactory) => Promise<R>) => Promise<R>;
38
+ transacted?: <R>(source: string, fn: (storageFactory: StorageFactory) => Promise<R>) => Promise<R>;
38
39
  /**
39
40
  * Builds the cross-source reader a frond gets when it declares `reads:`.
40
41
  *
41
- * A factory rather than a value, for the same reason `ormFactory` is one: core must
42
+ * A factory rather than a value, for the same reason `storageFactory` is one: core must
42
43
  * not name a storage package, and this one costs 71 MB of downloaded extensions and a
43
44
  * native module — nothing a first run that only wanted sqlite should carry. The host
44
45
  * decides what backs it; `@fougere/adapter-duckdb` is one answer, not the contract.
@@ -219,14 +220,14 @@ export interface App extends DispatchPort {
219
220
  * the dual of {@link facadeFor}. `undefined` when no loaded frond hosts the entity,
220
221
  * or when the app booted with no storage at all.
221
222
  *
222
- * `unknown` because the port belongs to whoever wired it: narrowing it to `EntityOrm`
223
+ * `unknown` because the port belongs to whoever wired it: narrowing it to `Storage`
223
224
  * is the caller saying which implementation they are standing on.
224
225
  */
225
- ormFor(entity: string): unknown | undefined;
226
+ storageFor(entity: string): unknown | undefined;
226
227
  /**
227
228
  * The presenter of an entity, resolved through its owning frond's scope.
228
229
  *
229
- * Same shape as {@link ormFor}, and it exists so an adapter never spells the container
230
+ * Same shape as {@link storageFor}, and it exists so an adapter never spells the container
230
231
  * key itself: `schema-graphql` wrote out `${Name}Presenter` by hand, and a key respelled
231
232
  * in a second place finds nothing and reports nothing the day the convention moves.
232
233
  */
@@ -1,4 +1,4 @@
1
- import type { DispatchEvent } from '../dispatch/DispatchEvent.js';
1
+ import type { RouteKind } from './RouteAddress.js';
2
2
 
3
3
  /**
4
4
  * What crosses the door: the address, the route it took, and the verdict.
@@ -18,7 +18,7 @@ export interface CallRecord {
18
18
  operation: string;
19
19
  surface?: string;
20
20
  /** Known at `resolved`, so absent on a call that never found a route. */
21
- route?: NonNullable<DispatchEvent['routeKind']>;
21
+ route?: RouteKind;
22
22
  /**
23
23
  * The traceparent the invocation carried, when one did.
24
24
  *
@@ -1,3 +1,6 @@
1
+ /** Which kind of destination served a call — it travels in a `CallRecord`. */
2
+ export type RouteKind = 'local' | 'remote' | 'system';
3
+
1
4
  export interface RouteAddressInput {
2
5
  entity: string;
3
6
  operation: string;
@@ -19,9 +19,9 @@ import { lowerFirst, type SchemaView } from '@fougere/schema';
19
19
  import type {
20
20
  CollectorEntry, EntityEntry, FrondDescriptor, HandlerEntry,
21
21
  PresenterEntry, ProviderEntry, SeedEntry,
22
- } from './frond.js';
23
- import { DEFAULT_CONVENTIONS } from './conventions.js';
24
- import { getPresenterFields } from '../prefab/presenter.js';
22
+ } from './descriptor/frond.js';
23
+ import { DEFAULT_CONVENTIONS } from './scan/conventions.js';
24
+ import { getPresenterFields } from './prefab/presenter.js';
25
25
 
26
26
  /** A class, as a declaration hands it over: the constructor itself. */
27
27
  type Ctor = new (...args: never[]) => unknown;
@@ -31,7 +31,7 @@ type Ctor = new (...args: never[]) => unknown;
31
31
  *
32
32
  * `deps` is the one thing no runtime can recover: TypeScript erases the parameter types,
33
33
  * and `registerFrames` reads them to know which frames to build — asking for one IS
34
- * declaring it. A handler that takes only its own entity's ORM needs nothing here.
34
+ * declaring it. A handler that takes only its own entity's storage needs nothing here.
35
35
  */
36
36
  export interface DeclaredSubject {
37
37
  ctor: Ctor;
@@ -98,7 +98,7 @@ export interface FrondDeclaration {
98
98
  *
99
99
  * ```ts
100
100
  * createApp({
101
- * scan: { fronds: Fronds.scanned([frond('blog', { entities: [Post], handlers: [PostHandler] })]), diagnostics: [] },
101
+ * scan: { fronds: Fronds.hosting([frond('blog', { entities: [Post], handlers: [PostHandler] })]), diagnostics: [] },
102
102
  * })
103
103
  * ```
104
104
  */
@@ -2,8 +2,7 @@ import type { SchemaView } from '@fougere/schema';
2
2
  import type { EntityEntry, FrondDescriptor } from './frond.js';
3
3
 
4
4
  /**
5
- * What this app hosts — the scan's answer, with the questions everyone was asking it
6
- * by hand.
5
+ * What this app hosts, with the questions everyone was asking it by hand.
7
6
  *
8
7
  * The list was a bare array, so every reader re-walked it: "which frond owns this
9
8
  * entity" was spelled four times, "every entity by name" three times as three literal
@@ -20,8 +19,8 @@ export class Fronds extends Array<FrondDescriptor> {
20
19
  return Array;
21
20
  }
22
21
 
23
- /** What one scan run found. The only gate in — `Array.of` is taken, and named. */
24
- static scanned(fronds: readonly FrondDescriptor[]): Fronds {
22
+ /** The fronds an app hosts, scanned or stated. The only gate in — `Array.of` is taken. */
23
+ static hosting(fronds: readonly FrondDescriptor[]): Fronds {
25
24
  const held = new Fronds();
26
25
  held.push(...fronds);
27
26
  return held;
@@ -32,7 +31,7 @@ export class Fronds extends Array<FrondDescriptor> {
32
31
  return this.find((frond) => frond.entities.some((e) => e.name === entity));
33
32
  }
34
33
 
35
- /** The entity of that name, wherever it was scanned. */
34
+ /** The entity of that name, wherever it was declared. */
36
35
  entity(name: string): EntityEntry | undefined {
37
36
  for (const frond of this) {
38
37
  const found = frond.entities.find((e) => e.name === name);
@@ -42,7 +41,7 @@ export class Fronds extends Array<FrondDescriptor> {
42
41
  }
43
42
 
44
43
  /**
45
- * Every scanned entity class, by name — across all fronds and never per-frond.
44
+ * Every entity class, by name — across all fronds and never per-frond.
46
45
  *
47
46
  * A fact is announced in one frond and heard in another, so the subscriber's own
48
47
  * frond does not hold the shape it must judge; a `reads:` clause names entities
@@ -52,7 +51,7 @@ export class Fronds extends Array<FrondDescriptor> {
52
51
  return new Map(this.flatMap((frond) => frond.entities.map((e) => [e.name, e.entityClass] as const)));
53
52
  }
54
53
 
55
- /** Every scanned entity, sorted. What was FOUND, not what answers. */
54
+ /** Every declared entity, sorted. What is DECLARED, not what answers. */
56
55
  entityNames(): string[] {
57
56
  return this.flatMap((frond) => frond.entities.map((e) => e.name)).sort();
58
57
  }
@@ -1,12 +1,12 @@
1
1
  /**
2
- * A frond as the SCAN found it — one interface per convention directory, plus what the
3
- * run could not read.
2
+ * What a frond is made of — one interface per convention directory.
4
3
  *
5
- * The declared side is `frond-config.ts`; this is the described one. They meet at
6
- * `FrondDescriptor.operationsOverrides`, where a declaration reaches the façade.
4
+ * Produced two ways and indistinguishable once here: the scan DERIVES it from a
5
+ * directory tree, `frond()` STATES it outright. `frond-config.ts` is the declared
6
+ * side; they meet at `FrondDescriptor.operationsOverrides`.
7
7
  */
8
8
  import type { SchemaView } from '@fougere/schema';
9
- import type { ParsedParam, OperationContract, OperationsMap } from '../wire/operation.js';
9
+ import type { Param, OperationContract, OperationsMap } from '../wire/operation.js';
10
10
  import type { PresenterViews } from '../prefab/presenter.js';
11
11
  import type { Fronds } from './Fronds.js';
12
12
 
@@ -74,7 +74,7 @@ export interface HandlerEntry {
74
74
  filePath: string;
75
75
  /** Whether this handler is part of the frond's public contract. */
76
76
  exposed?: boolean;
77
- /** Output schema override — when Crud(Entity, Output), restricts ORM output. */
77
+ /** Output schema override — when Crud(Entity, Output), restricts storage output. */
78
78
  outputOverride?: SchemaView;
79
79
  /** Surface name — subdirectory in handlers/ (e.g. 'admin', 'public'). */
80
80
  surface?: string;
@@ -104,7 +104,7 @@ export interface PresenterFieldMeta {
104
104
  * A computed field that declares `user?: User` is then fed by the collector
105
105
  * that resolves one. A presenter is not a second mechanism.
106
106
  */
107
- params?: ParsedParam[];
107
+ params?: Param[];
108
108
  }
109
109
 
110
110
  /** A discovered presenter (computed fields for an entity's output). */
@@ -192,44 +192,3 @@ export interface FrondDescriptor {
192
192
  method?: string;
193
193
  }>;
194
194
  }
195
-
196
- /** Result of scanning a project directory. */
197
- /**
198
- * Something the scan could NOT do — recorded instead of swallowed.
199
- *
200
- * The scan answers with what it found. Until now it answered the same way whether
201
- * a directory held nothing or could not be read, and whether a handler declared no
202
- * operation or failed to parse: `catch → empty`. So every downstream reader — the
203
- * façade, the identity card, anything asking "what does this app serve?" — could
204
- * not tell **"there is nothing"** from **"I could not look"**.
205
- *
206
- * That distinction is what makes a rule about an ABSENCE sound. Without it, a check
207
- * derived from the scan reports "nothing wrong" precisely when it read nothing.
208
- */
209
- export interface ScanDiagnostic {
210
- /**
211
- * `blocking` — the app now serves less than its source declares, and no caller
212
- * can know it: a handler that failed to parse contributes zero operations.
213
- * `warning` — something may be missing and the scan cannot decide, e.g. a base
214
- * class it is not allowed to resolve. Statable in `frond.config.ts`.
215
- */
216
- severity: 'blocking' | 'warning';
217
- /** Stable rule name — `handler-parse-failed`, `directory-unreadable`. */
218
- code: string;
219
- /** Absolute path of what could not be read. */
220
- filePath: string;
221
- /** The frond it belongs to, when the scan got far enough to know. */
222
- frond?: string;
223
- /** The declaration the diagnostic is about — e.g. `PostHandler.publish`. */
224
- subject?: string;
225
- /** What could not be done, and what it costs. One sentence, for a human. */
226
- message: string;
227
- /** The underlying failure, kept whole. */
228
- cause?: unknown;
229
- }
230
-
231
- export interface ScanResult {
232
- fronds: Fronds;
233
- /** What the scan could not do. Empty is a claim, not a default — see {@link ScanDiagnostic}. */
234
- diagnostics: ScanDiagnostic[];
235
- }
@@ -1,9 +1,9 @@
1
- import {
2
- resolveArgs,
3
- type BindingPlan,
4
- type CollectorResolver,
5
- } from '../boot/binding.js';
6
1
  import type { InvocationContext } from '../contract/Invocation.js';
2
+ import type { BindingPlan } from '../wire/binding.js';
3
+
4
+ export interface CollectorResolver {
5
+ collect(ctx: InvocationContext): Promise<unknown>;
6
+ }
7
7
 
8
8
  export type CollectorLookup = (typeName: string) => CollectorResolver | undefined;
9
9
 
@@ -11,7 +11,53 @@ export type CollectorLookup = (typeName: string) => CollectorResolver | undefine
11
11
  export class ArgumentResolver {
12
12
  constructor(private readonly collectors?: CollectorLookup) {}
13
13
 
14
- resolve(plan: BindingPlan, invocation: InvocationContext): Promise<unknown[]> {
15
- return resolveArgs(plan, invocation, this.collectors);
14
+ async resolve(plan: BindingPlan, ctx: InvocationContext): Promise<unknown[]> {
15
+ const args: unknown[] = [];
16
+
17
+ for (const binding of plan) {
18
+ switch (binding.source.kind) {
19
+ case 'collector': {
20
+ const collector = this.collectors?.(binding.source.typeName);
21
+ args.push(collector ? await collector.collect(ctx) : undefined);
22
+ break;
23
+ }
24
+ case 'context': {
25
+ args.push(ctx);
26
+ break;
27
+ }
28
+ case 'param': {
29
+ // `null` is a value, not a miss. Nullish coalescing used to make an explicit
30
+ // nullable path/GraphQL argument fall through to query (or become undefined),
31
+ // collapsing `T | null` into `T | undefined`. Only undefined means absent.
32
+ const fromParams = ctx.params[binding.source.name];
33
+ let val: unknown = fromParams === undefined
34
+ ? ctx.query[binding.source.name]
35
+ : fromParams;
36
+ if (val != null && binding.source.coerce === 'number') val = Number(val);
37
+ if (val != null && binding.source.coerce === 'boolean') val = val === 'true' || val === '1' || val === true;
38
+ args.push(val);
39
+ break;
40
+ }
41
+ case 'fact': {
42
+ // A fact IS the payload — the whole of what happened, never a piece of it.
43
+ // Identical to `body` today, and deliberately not sharing its branch: the two
44
+ // agree by coincidence, not by rule, and the day `body` learns to look up a
45
+ // value by parameter name a subscriber would receive ONE FIELD of the fact it
46
+ // subscribed to. Splitting it costs nothing and removes that trap.
47
+ args.push(ctx.body);
48
+ break;
49
+ }
50
+ case 'body': {
51
+ args.push(ctx.body);
52
+ break;
53
+ }
54
+ case 'query': {
55
+ args.push(ctx.query);
56
+ break;
57
+ }
58
+ }
59
+ }
60
+
61
+ return args;
16
62
  }
17
63
  }
@@ -1,5 +1,5 @@
1
1
  import type { Call } from '../contract/Call.js';
2
- import type { RouteKind } from './Route.js';
2
+ import type { RouteKind } from '../contract/RouteAddress.js';
3
3
 
4
4
  export type DispatchStage = 'received' | 'resolved' | 'completed' | 'failed' | 'settled';
5
5
 
@@ -1,6 +1,6 @@
1
- import { computeBindingPlan } from '../boot/binding.js';
1
+ import { computeBindingPlan } from '../wire/binding.js';
2
2
  import type { InvocationContext } from '../contract/Invocation.js';
3
- import type { PresenterEntry } from '../scan/frond.js';
3
+ import type { PresenterEntry } from '../descriptor/frond.js';
4
4
  import type { ArgumentResolver } from './ArgumentResolver.js';
5
5
  import type { PresenterArgs } from './PresenterExecutor.js';
6
6
 
@@ -1,7 +1,5 @@
1
1
  import type { Call } from '../contract/Call.js';
2
- import type { RouteAddress } from '../contract/RouteAddress.js';
3
-
4
- export type RouteKind = 'local' | 'remote' | 'system';
2
+ import type { RouteAddress, RouteKind } from '../contract/RouteAddress.js';
5
3
 
6
4
  /** Executable destination known by the dispatcher. */
7
5
  export interface Route {
@@ -1,5 +1,5 @@
1
1
  import { ValueJudge, type Fields } from '@fougere/schema';
2
- import { assertListOptions } from '../orm.js';
2
+ import { assertListOptions } from '../storage.js';
3
3
  import { ErrorCode, FougereError } from '../wire/errors.js';
4
4
 
5
5
  interface Writer {
@@ -8,7 +8,7 @@ interface Writer {
8
8
  }
9
9
 
10
10
  /**
11
- * Judges storage writes and list options without narrowing the ORM interface.
11
+ * Judges storage writes and list options without narrowing the storage interface.
12
12
  *
13
13
  * Storage is a way out like the client surface, so what goes to it is judged too — the
14
14
  * same rule `OutputProjector` applies on the other exit.
@@ -19,12 +19,12 @@ export class StorageGuard {
19
19
  private readonly entity: string,
20
20
  ) {}
21
21
 
22
- guard<T extends object>(orm: T): T {
23
- const writer = orm as unknown as Writer;
24
- if (typeof writer.create !== 'function' || typeof writer.update !== 'function') return orm;
22
+ guard<T extends object>(storage: T): T {
23
+ const writer = storage as unknown as Writer;
24
+ if (typeof writer.create !== 'function' || typeof writer.update !== 'function') return storage;
25
25
 
26
26
  const validation = this;
27
- const guarded = Object.create(orm) as T & Writer;
27
+ const guarded = Object.create(storage) as T & Writer;
28
28
 
29
29
  guarded.create = async function (...args) {
30
30
  validation.judge(args[0], 'create');
@@ -36,7 +36,7 @@ export class StorageGuard {
36
36
  return writer.update.apply(this, args);
37
37
  };
38
38
 
39
- const reader = orm as unknown as { list?: (...args: unknown[]) => unknown };
39
+ const reader = storage as unknown as { list?: (...args: unknown[]) => unknown };
40
40
  if (typeof reader.list === 'function') {
41
41
  (guarded as unknown as typeof reader).list = async function (...args: unknown[]) {
42
42
  assertListOptions(args[0] as object | undefined, validation.entity);
@@ -7,21 +7,17 @@
7
7
  */
8
8
  import { lowerFirst, type SchemaView } from '@fougere/schema';
9
9
  import { statementDrift } from './boot/statement-drift.js';
10
- import type { BindingPlan } from './boot/binding.js';
10
+ import { computeBindingPlan, type BindingPlan } from './wire/binding.js';
11
11
  import { targetOf } from './prefab/prefab.js';
12
- import type {
13
- CollectorEntry,
14
- FrondDescriptor,
15
- HandlerEntry,
16
- ScanDiagnostic,
17
- } from './scan/frond.js';
12
+ import type { CollectorEntry, FrondDescriptor, HandlerEntry } from './descriptor/frond.js';
13
+ import type { ScanDiagnostic } from './scan/result.js';
18
14
  import { verify } from './verify.js';
19
15
  import {
20
16
  inferOperationKind,
21
- resolveContracts,
22
17
  type OperationContract,
23
18
  type OperationKind,
24
- type ParsedType,
19
+ type OperationsMap,
20
+ type TypeRef,
25
21
  knownVerbs,
26
22
  } from './wire/operation.js';
27
23
 
@@ -491,11 +487,11 @@ function validateProvenance(
491
487
  return valid;
492
488
  }
493
489
 
494
- function structural(type: ParsedType | undefined): boolean {
490
+ function structural(type: TypeRef | undefined): boolean {
495
491
  return type !== undefined && type.raw.includes('{');
496
492
  }
497
493
 
498
- function schemaMatches(schema: SchemaView, type: ParsedType | undefined): boolean {
494
+ function schemaMatches(schema: SchemaView, type: TypeRef | undefined): boolean {
499
495
  const names = new Set([schema.name, schema.derivation?.sourceName].filter(Boolean));
500
496
  if (type?.name && names.has(type.name)) return true;
501
497
  return type?.generics?.some((generic) => generic.name && names.has(generic.name)) ?? false;
@@ -599,3 +595,59 @@ function uniqueDiagnostics(diagnostics: readonly ScanDiagnostic[]): ScanDiagnost
599
595
  return true;
600
596
  });
601
597
  }
598
+
599
+ /**
600
+ * The contract of every operation a handler serves — the three producers, merged once.
601
+ *
602
+ * This lived inline in `buildFacade`, which made the façade the only thing that could
603
+ * answer "what is this op's contract, really?". Anything else asking — a checker, a
604
+ * client generator, a descriptor — had to redo the merge, and a second opinion that
605
+ * drifts reports nothing wrong while looking at the wrong thing.
606
+ *
607
+ * The order is a claim about authority, not a convenience:
608
+ *
609
+ * 1. a prefab DECLARES what it built (`Crud(E).__ops`) — runtime, so it survives a
610
+ * scan that resolved nothing;
611
+ * 2. the scan DERIVES from source — a method written in this very file is the
612
+ * author's own word and beats everything; a method it merely READ on a base
613
+ * class is a guess about someone else's code, and yields to that code's own
614
+ * declaration;
615
+ * 3. `frond.config.ts` STATES — the most explicit statement, made by whoever
616
+ * assembles the app, and the only answer for an op inherited from an installed
617
+ * base class the workspace-only scan cannot see. Merged per key, so stating a
618
+ * `binding` alone does not erase an `input` the scan found.
619
+ *
620
+ * Pure: no container, no instance, no disk. The binding plan is resolved here, so
621
+ * nothing downstream ever meets an AST.
622
+ */
623
+ export function resolveContracts(
624
+ handler: Pick<HandlerEntry, 'ctor' | 'operations'>,
625
+ overrides: FrondDescriptor['operationsOverrides'],
626
+ collectorTypeNames: Set<string>,
627
+ ): OperationsMap {
628
+ const declared = (handler.ctor as { __ops?: Record<string, OperationContract> }).__ops ?? {};
629
+ const contracts: OperationsMap = new Map(Object.entries(declared));
630
+
631
+ for (const [opName, scanned] of handler.operations) {
632
+ if (scanned.signature?.inherited && opName in declared) continue;
633
+ contracts.set(opName, {
634
+ ...scanned,
635
+ binding: scanned.binding
636
+ ?? (scanned.signature ? computeBindingPlan(scanned.signature.params, collectorTypeNames) : undefined),
637
+ });
638
+ }
639
+
640
+ for (const [opName, override] of Object.entries(overrides ?? {})) {
641
+ const { input, output, binding, description } = override;
642
+ if (input === undefined && output === undefined && binding === undefined && description === undefined) continue;
643
+ contracts.set(opName, {
644
+ ...contracts.get(opName),
645
+ ...(input !== undefined && { input }),
646
+ ...(output !== undefined && { output }),
647
+ ...(binding !== undefined && { binding }),
648
+ ...(description !== undefined && { description }),
649
+ });
650
+ }
651
+
652
+ return contracts;
653
+ }
package/src/emit.ts CHANGED
@@ -22,7 +22,7 @@
22
22
  import { lowerFirst } from '@fougere/schema';
23
23
 
24
24
  /**
25
- * What an emitter injects. Resolved by type, like `EntityOrm<Post>` and `Facade<T>`.
25
+ * What an emitter injects. Resolved by type, like `Storage<Post>` and `Facade<T>`.
26
26
  *
27
27
  * The returned promise settles when the fact has been **dispatched**, never when it has
28
28
  * been handled: a subscriber's failure is its own. The `EventBus` this replaces did
@@ -1,7 +1,7 @@
1
1
  import { resolve } from 'node:path';
2
2
  import { existsSync } from 'node:fs';
3
3
  import type { EntityConstructor, SchemaView } from '@fougere/schema';
4
- import type { BindingPlan } from './boot/binding.js';
4
+ import type { BindingPlan } from './wire/binding.js';
5
5
 
6
6
  // ── Types ────────────────────────────────────────
7
7
 
@@ -91,7 +91,7 @@ export interface FrondConfig {
91
91
  * door on purpose ("it would publish the auth tables to anyone who asks"), and a SQL
92
92
  * door at app scope would hand them over; this is what keeps it shut.
93
93
  *
94
- * Declaring it is what makes `Sources` injectable here. A frond that declares none
94
+ * Declaring it is what makes `Reads` injectable here. A frond that declares none
95
95
  * asks for none, and nothing is attached on its behalf.
96
96
  */
97
97
  reads?: string[];
package/src/graph.ts CHANGED
@@ -3,7 +3,7 @@ import { Role } from '@fougere/schema';
3
3
  * Entity dependency graph — analyzes ref() links between entities
4
4
  * to suggest domain boundaries for splitting fronds.
5
5
  */
6
- import type { FrondDescriptor } from './scan/frond.js';
6
+ import type { FrondDescriptor } from './descriptor/frond.js';
7
7
 
8
8
  export interface EntityNode {
9
9
  name: string;