@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
@@ -4,6 +4,7 @@
4
4
  * The AST says what the author declared; TypeScript's checker says what those types mean.
5
5
  * TypeScript is lazy-loaded to avoid bundling the compiler in production builds.
6
6
  */
7
+ import type { TypeRef, Param, Signature } from '../wire/signature.js';
7
8
  import type ts from '@typescript/typescript6';
8
9
  import { readFileSync, existsSync, statSync } from 'node:fs';
9
10
  import { join, dirname, resolve as resolvePath } from 'node:path';
@@ -195,7 +196,7 @@ function sourceOf(filePath: string): ts.SourceFile {
195
196
  }
196
197
 
197
198
  /** One declared parameter — a constructor's and a method's are read the same way. */
198
- function parsedParam(param: ts.ParameterDeclaration, source: ts.SourceFile, checker?: ts.TypeChecker): ParsedParam {
199
+ function parsedParam(param: ts.ParameterDeclaration, source: ts.SourceFile, checker?: ts.TypeChecker): Param {
199
200
  const ts = getTS();
200
201
  const type = param.type ? parseTypeNode(param.type, source, checker) : { raw: 'unknown', name: 'unknown' };
201
202
  return {
@@ -207,63 +208,9 @@ function parsedParam(param: ts.ParameterDeclaration, source: ts.SourceFile, chec
207
208
  };
208
209
  }
209
210
 
210
- /** A parsed type reference — supports primitives, entities, arrays, generics. */
211
- export interface ParsedType {
212
- /** Raw type text as written in source (e.g. 'Pagination<Post>'). */
213
- raw: string;
214
- /** Base type name (e.g. 'Pagination', 'string', 'Post'). */
215
- name: string;
216
- /** Whether this is an array (T[] or Array<T>) — true at any depth. */
217
- array?: boolean;
218
- /**
219
- * How MANY array levels: `string[]` is 1, `string[][]` is 2. `array` only ever said
220
- * "at least one", which sufficed while one level meant one thing. It stopped sufficing
221
- * when a presenter method started taking the page — there the outer level IS the page
222
- * and what remains is the field, so telling `string[]` from `string[][]` is telling a
223
- * computed string from a computed list.
224
- */
225
- arrayDepth?: number;
226
- /** Generic type arguments (e.g. for Pagination<Post> → [{ name: 'Post' }]). */
227
- generics?: ParsedType[];
228
- /** Whether `null` belongs to the type. */
229
- nullable?: boolean;
230
- /** Whether absence (`undefined` or `void`) belongs to the type. */
231
- undefined?: boolean;
232
- /** Whether this is a Promise wrapper (unwrapped in output). */
233
- promise?: boolean;
234
- }
235
-
236
- /** A parsed method parameter. */
237
- export interface ParsedParam {
238
- name: string;
239
- type: ParsedType;
240
- optional?: boolean;
241
- }
242
-
243
- /** A parsed method signature from a handler source file. */
244
- export interface ParsedMethod {
245
- name: string;
246
- params: ParsedParam[];
247
- returnType?: ParsedType;
248
- /**
249
- * Came from a base class, not from the file being scanned. What a prefab
250
- * handler declares about its own ops beats this — the scan reads a signature
251
- * and guesses, the builder knows.
252
- */
253
- inherited?: boolean;
254
- /**
255
- * The operation in words — the first sentence of the method's doc comment.
256
- *
257
- * Not a new thing to write: handlers already carry it (`/** Judge: the author,
258
- * a draft… *&#47;`), the AST already holds it, and nothing read it. A caller that
259
- * discovers an operation over the wire has its name and its schema; what the
260
- * operation is FOR lived only in the source.
261
- */
262
- description?: string;
263
- }
264
211
 
265
- /** Parse a TypeScript type node into a ParsedType. */
266
- function parseTypeNode(node: ts.TypeNode, source: ts.SourceFile, checker?: ts.TypeChecker): ParsedType {
212
+ /** Parse a TypeScript type node into a TypeRef. */
213
+ function parseTypeNode(node: ts.TypeNode, source: ts.SourceFile, checker?: ts.TypeChecker): TypeRef {
267
214
  const ts = getTS();
268
215
  const raw = node.getText(source);
269
216
 
@@ -373,7 +320,7 @@ function meaningfulSymbolName(type: ts.Type, checker: ts.TypeChecker): string {
373
320
  }
374
321
 
375
322
  /** Turn a checked TypeScript type into the small, serializable vocabulary the runtime reads. */
376
- function parseCheckedType(type: ts.Type, raw: string, checker: ts.TypeChecker, depth = 0): ParsedType {
323
+ function parseCheckedType(type: ts.Type, raw: string, checker: ts.TypeChecker, depth = 0): TypeRef {
377
324
  const typescript = getTS();
378
325
  if (depth > 12) return { raw, name: checker.typeToString(type) };
379
326
 
@@ -545,9 +492,9 @@ function extractClassMethods(
545
492
  source: ts.SourceFile,
546
493
  skip: Set<string>,
547
494
  checker?: ts.TypeChecker,
548
- ): ParsedMethod[] {
495
+ ): Signature[] {
549
496
  const ts = getTS();
550
- const results: ParsedMethod[] = [];
497
+ const results: Signature[] = [];
551
498
 
552
499
  for (const member of cls.members) {
553
500
  if (!ts.isMethodDeclaration(member)) continue;
@@ -628,9 +575,9 @@ function inheritedFromBase(
628
575
  base: ts.ExpressionWithTypeArguments,
629
576
  checker: ts.TypeChecker,
630
577
  skip: Set<string>,
631
- ): ParsedMethod[] {
578
+ ): Signature[] {
632
579
  const typescript = getTS();
633
- const results: ParsedMethod[] = [];
580
+ const results: Signature[] = [];
634
581
 
635
582
  for (const property of checker.getPropertiesOfType(checker.getTypeAtLocation(base))) {
636
583
  if (skip.has(property.name)) continue;
@@ -676,7 +623,7 @@ function parseInheritedMethods(
676
623
  * published base class was absent from the façade without a word.
677
624
  */
678
625
  unresolved: string[],
679
- ): ParsedMethod[] {
626
+ ): Signature[] {
680
627
  const ts = getTS();
681
628
  if (!cls.heritageClauses) return [];
682
629
 
@@ -747,7 +694,7 @@ function findDefaultClass(source: ts.SourceFile): ts.ClassDeclaration | undefine
747
694
  * pair travels through the scan cache, which is why {@link PARSER_VERSION} moved.
748
695
  */
749
696
  export interface HandlerParse {
750
- methods: ParsedMethod[];
697
+ methods: Signature[];
751
698
  /** Base classes whose source this pass could not open. Empty is a claim. */
752
699
  unresolvedHeritage: string[];
753
700
  }
@@ -766,7 +713,7 @@ export async function parseAllHandlerMethods(filePath: string, projectRoot?: str
766
713
  *
767
714
  * Returns all methods (no CRUD filtering) — each method is a computed field.
768
715
  */
769
- export async function parsePresenterMethods(filePath: string, projectRoot?: string): Promise<ParsedMethod[]> {
716
+ export async function parsePresenterMethods(filePath: string, projectRoot?: string): Promise<Signature[]> {
770
717
  await loadTS();
771
718
  // No `projectRoot`, so no heritage pass and nothing to report: a presenter's
772
719
  // computed fields are its own methods.
@@ -775,9 +722,9 @@ export async function parsePresenterMethods(filePath: string, projectRoot?: stri
775
722
 
776
723
  /**
777
724
  * Parse constructor parameter types from a source file's default class.
778
- * Returns type names (e.g. ['PostOrm', 'Logger']) for DI resolution.
725
+ * Returns type names (e.g. ['PostStorage', 'Logger']) for DI resolution.
779
726
  */
780
- export async function parseConstructorParams(filePath: string, projectRoot?: string): Promise<ParsedParam[]> {
727
+ export async function parseConstructorParams(filePath: string, projectRoot?: string): Promise<Param[]> {
781
728
  const ts = await loadTS();
782
729
  const { source, checker } = checkedSourceOf(filePath, projectRoot);
783
730
  const cls = findDefaultClass(source);
@@ -0,0 +1,42 @@
1
+ import type { Fronds } from '../descriptor/Fronds.js';
2
+
3
+ /** Result of scanning a project directory. */
4
+ /**
5
+ * Something the scan could NOT do — recorded instead of swallowed.
6
+ *
7
+ * The scan answers with what it found. Until now it answered the same way whether
8
+ * a directory held nothing or could not be read, and whether a handler declared no
9
+ * operation or failed to parse: `catch → empty`. So every downstream reader — the
10
+ * façade, the identity card, anything asking "what does this app serve?" — could
11
+ * not tell **"there is nothing"** from **"I could not look"**.
12
+ *
13
+ * That distinction is what makes a rule about an ABSENCE sound. Without it, a check
14
+ * derived from the scan reports "nothing wrong" precisely when it read nothing.
15
+ */
16
+ export interface ScanDiagnostic {
17
+ /**
18
+ * `blocking` — the app now serves less than its source declares, and no caller
19
+ * can know it: a handler that failed to parse contributes zero operations.
20
+ * `warning` — something may be missing and the scan cannot decide, e.g. a base
21
+ * class it is not allowed to resolve. Statable in `frond.config.ts`.
22
+ */
23
+ severity: 'blocking' | 'warning';
24
+ /** Stable rule name — `handler-parse-failed`, `directory-unreadable`. */
25
+ code: string;
26
+ /** Absolute path of what could not be read. */
27
+ filePath: string;
28
+ /** The frond it belongs to, when the scan got far enough to know. */
29
+ frond?: string;
30
+ /** The declaration the diagnostic is about — e.g. `PostHandler.publish`. */
31
+ subject?: string;
32
+ /** What could not be done, and what it costs. One sentence, for a human. */
33
+ message: string;
34
+ /** The underlying failure, kept whole. */
35
+ cause?: unknown;
36
+ }
37
+
38
+ export interface ScanResult {
39
+ fronds: Fronds;
40
+ /** What the scan could not do. Empty is a claim, not a default — see {@link ScanDiagnostic}. */
41
+ diagnostics: ScanDiagnostic[];
42
+ }
@@ -1,27 +1,28 @@
1
1
  import { readdir, readFile } from 'node:fs/promises';
2
2
  import { existsSync, type Dirent } from 'node:fs';
3
3
  import { join, dirname, basename, resolve as resolvePath } from 'node:path';
4
- import type { FrondDescriptor, ProviderEntry, EntityEntry, HandlerEntry, PresenterEntry, CollectorEntry, SeedEntry, ScanResult, ScanDiagnostic } from './frond.js';
4
+ import type { FrondDescriptor, ProviderEntry, EntityEntry, HandlerEntry, PresenterEntry, CollectorEntry, SeedEntry } from '../descriptor/frond.js';
5
+ import type { ScanResult, ScanDiagnostic } from './result.js';
5
6
  import { ANONYMOUS_SCHEMA_NAME, type SchemaView } from '@fougere/schema';
6
7
  import type { OperationContract, OperationsMap } from '../wire/operation.js';
7
8
  import { cardinalityOf } from '../wire/operation.js';
8
- import { computeBindingPlan } from '../boot/binding.js';
9
+ import { computeBindingPlan } from '../wire/binding.js';
9
10
  import {
10
11
  parseAllHandlerMethods,
11
12
  parsePresenterMethods,
12
13
  parseConstructorParams,
13
14
  resetTypePrograms,
14
15
  seedTypeProgram,
15
- type ParsedType,
16
16
  } from './handler-parser.js';
17
+ import type { TypeRef } from '../wire/signature.js';
17
18
  import { loadFrondConfig } from '../frond-config.js';
18
19
  import { emitKeyOf } from '../emit.js';
19
20
  import { getPresenterFields } from '../prefab/presenter.js';
20
- import { ormKeyOf, togetherKeyOf } from '../orm.js';
21
+ import { storageKeyOf, togetherKeyOf } from '../storage.js';
21
22
  import { targetOf, viewsOf, outputOf } from '../prefab/prefab.js';
22
23
  import { ownedBy, repositoryKeyOf } from '../prefab/repository.js';
23
24
  import { lowerFirst } from '@fougere/schema';
24
- import { Fronds } from './Fronds.js';
25
+ import { Fronds } from '../descriptor/Fronds.js';
25
26
  import { getModuleLoader } from '../loader.js';
26
27
  import {
27
28
  type Conventions, type ConventionsInput,
@@ -132,17 +133,17 @@ function toAddress(className: string): string {
132
133
  * The container key a constructor parameter asks for — derived from its TYPE, not from
133
134
  * how the type was spelled.
134
135
  *
135
- * `deps` used to be `p.type.name`, so the key WAS the alias's name: `type ListOrm =
136
- * EntityOrm<List>` resolved only because someone had spelled it exactly like the
137
- * registration key (`ListOrm`), while `type ListRepo = EntityOrm<List>` — the same type —
138
- * typechecked and died at boot on `'ListRepo' is not registered`. And `EntityOrm<List>`
139
- * written out in full asked for `'EntityOrm'`, which nothing registers.
136
+ * `deps` used to be `p.type.name`, so the key WAS the alias's name: `type ListStorage =
137
+ * Storage<List>` resolved only because someone had spelled it exactly like the
138
+ * registration key (`ListStorage`), while `type ListRepo = Storage<List>` — the same type —
139
+ * typechecked and died at boot on `'ListRepo' is not registered`. And `Storage<List>`
140
+ * written out in full asked for `'Storage'`, which nothing registers.
140
141
  *
141
- * `EntityOrm<X>` names X's storage, so that is the key. The generic argument was already
142
- * parsed (`ParsedType.generics`) and thrown away. Anything else keeps its own name: a
142
+ * `Storage<X>` names X's storage, so that is the key. The generic argument was already
143
+ * parsed (`TypeRef.generics`) and thrown away. Anything else keeps its own name: a
143
144
  * plain service IS designated by its class name.
144
145
  */
145
- function depKeyOf(type: ParsedType): string {
146
+ function depKeyOf(type: TypeRef): string {
146
147
  // `Facade<PostHandler>` — the second port, read exactly like the first. The type names
147
148
  // what arrives: not the handler (its methods take positional arguments and it is never
148
149
  // injected), but the door built in front of it. Same key whether that door is the local
@@ -168,10 +169,10 @@ function depKeyOf(type: ParsedType): string {
168
169
  const held = type.name === 'RepositoryOf' ? type.generics?.[0]?.name : undefined;
169
170
  if (held) return repositoryKeyOf(held);
170
171
 
171
- const target = type.name === 'EntityOrm' ? type.generics?.[0]?.name : undefined;
172
+ const target = type.name === 'Storage' ? type.generics?.[0]?.name : undefined;
172
173
  if (!target) return type.name;
173
174
 
174
- return ormKeyOf(target);
175
+ return storageKeyOf(target);
175
176
  }
176
177
 
177
178
  /**
@@ -179,7 +180,7 @@ function depKeyOf(type: ParsedType): string {
179
180
  *
180
181
  * The tuple was chosen over the variadic form the parser reads more cleanly, because the
181
182
  * variadic one costs arities-with-defaults and a `never` filter on the TypeScript side
182
- * while the tuple maps to `[EntityOrm<Account>, EntityOrm<Ledger>]` in one line. The
183
+ * while the tuple maps to `[Storage<Account>, Storage<Ledger>]` in one line. The
183
184
  * parser does not get to decide alone; this split is what that choice costs.
184
185
  */
185
186
  function tupleMembers(raw: string): string[] {
@@ -216,9 +217,9 @@ async function toProvider(filePath: string): Promise<ProviderEntry> {
216
217
  const owned = ownedBy(ctor);
217
218
  const target = targetOf(ctor);
218
219
  if (owned.length > 1 && deps.length === 0) {
219
- deps.push(...owned.map((entity) => ormKeyOf(lowerFirst((entity as { name: string }).name))));
220
+ deps.push(...owned.map((entity) => storageKeyOf(lowerFirst((entity as { name: string }).name))));
220
221
  } else if (target && deps.length === 0) {
221
- deps.push(ormKeyOf(lowerFirst((target as { name: string }).name)));
222
+ deps.push(storageKeyOf(lowerFirst((target as { name: string }).name)));
222
223
  }
223
224
 
224
225
  // No `name` beside `ctor`: a provider registers under `ctor.name`, which is what
@@ -243,10 +244,10 @@ async function toEntityEntry(filePath: string): Promise<EntityEntry | null> {
243
244
  }
244
245
 
245
246
  /**
246
- * Resolve a ParsedType to a runtime schema if available in module exports.
247
+ * Resolve a TypeRef to a runtime schema if available in module exports.
247
248
  * Handles arrays, generics (uses base name), and simple references.
248
249
  */
249
- function resolveSchema(type: ParsedType, moduleExports: Record<string, unknown>): SchemaView | undefined {
250
+ function resolveSchema(type: TypeRef, moduleExports: Record<string, unknown>): SchemaView | undefined {
250
251
  // An array's element type IS `type.name` — the arity rides beside it, so nothing has
251
252
  // to be unwrapped here.
252
253
  // For generics like Pagination<Post>, also check inner types
@@ -751,7 +752,29 @@ export async function scanProject(
751
752
 
752
753
  // The app's own domain first, then the ones it took in.
753
754
  const all = rootFrond ? [rootFrond, ...under] : under;
754
- const fronds = Fronds.scanned(filter ? all.filter((f) => filter.includes(f.name)) : all);
755
+ const fronds = Fronds.hosting(filter ? all.filter((f) => filter.includes(f.name)) : all);
755
756
 
756
757
  return { fronds, diagnostics };
757
758
  }
759
+
760
+ /**
761
+ * What changes when a frond's domain changes — the paths a dev loop watches.
762
+ *
763
+ * It lives beside the scan rather than beside the conventions it reads, because it turns
764
+ * names into DISK paths: `join` is `node:path`, and `conventions.ts` is reached from
765
+ * `index` through `frond()`, which an edge bundle imports. Measured — esbuild refused the
766
+ * Worker with `Could not resolve "node:path"`.
767
+ *
768
+ * The root frond IS the scan root, so watching its path would match every write in the
769
+ * project: `.nuxt/`, `node_modules/`, the build output. Its convention directories are
770
+ * the frond, and they are what a scan re-reads.
771
+ */
772
+ export function watchPathsOf(
773
+ frond: { source: { path: string } },
774
+ scanRoot: string,
775
+ conventions: Conventions,
776
+ ): string[] {
777
+ return frond.source.path === scanRoot
778
+ ? frondDirsOf(conventions).map((dir) => join(scanRoot, dir))
779
+ : [frond.source.path];
780
+ }
@@ -18,7 +18,8 @@
18
18
  * — same rows, same computed fields, and no `typescript` loaded at boot.
19
19
  */
20
20
  import { relative } from 'node:path';
21
- import type { FrondDescriptor, ScanResult } from './frond.js';
21
+ import type { FrondDescriptor } from '../descriptor/frond.js';
22
+ import type { ScanResult } from './result.js';
22
23
 
23
24
  type Live = { name?: string };
24
25
 
@@ -1,4 +1,4 @@
1
- import { upperFirst, type SchemaView } from '@fougere/schema';
1
+ import { upperFirst, lowerFirst, type SchemaView } from '@fougere/schema';
2
2
 
3
3
  /** Options for list queries — pagination, sorting, counting. */
4
4
  export interface ListOptions {
@@ -68,8 +68,8 @@ export interface SelectOption {
68
68
  select?: SchemaView;
69
69
  }
70
70
 
71
- /** Per-entity ORM — scoped CRUD operations on a single entity type. */
72
- export interface EntityOrm<T = Record<string, unknown>> {
71
+ /** Per-entity storage — scoped CRUD operations on a single entity type. */
72
+ export interface Storage<T = Record<string, unknown>> {
73
73
  list(options?: ListOptions & SelectOption): Promise<ListResult<T>>;
74
74
  findById(id: string, options?: SelectOption): Promise<T | undefined>;
75
75
  /**
@@ -78,7 +78,7 @@ export interface EntityOrm<T = Record<string, unknown>> {
78
78
  *
79
79
  * Both existed on the SQL implementation from the start and neither was declared here.
80
80
  * A port that hides what it offers is a port nobody can use: `auth-better` cast its way
81
- * in (`orm as OrmWithFindBy`), a presenter that needed the lines of an order read the
81
+ * in (`storage as StorageWithFindBy`), a presenter that needed the lines of an order read the
82
82
  * whole table instead, and the GraphQL relation resolver passed criteria to `list()`
83
83
  * — which drops what it does not know.
84
84
  */
@@ -137,15 +137,15 @@ export interface EntityOrm<T = Record<string, unknown>> {
137
137
  upsertAll(inputs: readonly Partial<T>[], options?: SelectOption): Promise<number>;
138
138
  update(id: string, input: Partial<T>, options?: SelectOption): Promise<T>;
139
139
  delete(id: string): Promise<boolean>;
140
- /** Returns a scoped ORM that restricts all read results to the fields of the given schema. */
141
- output(schema: SchemaView): EntityOrm<T>;
140
+ /** Returns a scoped storage that restricts all read results to the fields of the given schema. */
141
+ output(schema: SchemaView): Storage<T>;
142
142
  /**
143
- * What this ORM wraps — the Kysely instance for the SQL one, something else elsewhere.
143
+ * What this storage wraps — the Kysely instance for the SQL one, something else elsewhere.
144
144
  *
145
- * Every judge sits on the ORM's own methods, so a statement issued here meets none of
145
+ * Every judge sits on the storage's own methods, so a statement issued here meets none of
146
146
  * them: a value the entity refuses lands in the table without a word. It is the port's
147
147
  * own escape hatch rather than a handle on the side, so it keeps the scope the container
148
- * gave you — `productOrm.client` reaches the products, not the whole database.
148
+ * gave you — `productStorage.client` reaches the products, not the whole database.
149
149
  *
150
150
  * `unknown` on purpose: the client belongs to the implementation, and narrowing it is
151
151
  * the caller saying out loud which one they are standing on.
@@ -154,13 +154,106 @@ export interface EntityOrm<T = Record<string, unknown>> {
154
154
  }
155
155
 
156
156
  /**
157
- * Factory that creates an EntityOrm for a given entity.
157
+ * Factory that creates a Storage for a given entity.
158
158
  * Called by bootstrap for every scanned entity.
159
159
  */
160
- export type OrmFactory = (entity: SchemaView, name: string) => EntityOrm;
160
+ export type StorageFactory = (entity: SchemaView, name: string) => Storage;
161
161
 
162
162
  /**
163
- * Container key of an entity's storage — 'reading' → 'ReadingOrm'.
163
+ * A place rows live, whatever realizes it.
164
+ *
165
+ * `StorageFactory` plus its lifecycle, which is why it lives beside it. Three of the four
166
+ * gestures are optional and ABSENCE IS THE ANSWER: no `transacted` and a frame compensates
167
+ * instead of transacting (`boot/together.ts`), no `migrate` and there is no shape to bring
168
+ * up to date. What a source is MADE OF is not here — `adapter/sql` states `dialect`, `db`
169
+ * and `sink` on its own `SqlSource`, reached by narrowing, the rule {@link Storage.client}
170
+ * already obeys one level down.
171
+ */
172
+ export interface Source {
173
+ storageFactory: StorageFactory;
174
+ /** Bring the shape of what lives here up to date. */
175
+ migrate?(view: SourceView): Promise<void>;
176
+ /** Run `fn` as ONE unit of work, with a factory bound to it. */
177
+ transacted?<R>(fn: (factory: StorageFactory) => Promise<R>): Promise<R>;
178
+ close?(): Promise<void>;
179
+ /** What distinguishes it when a query is reported. */
180
+ name?: string;
181
+ }
182
+
183
+ /**
184
+ * The app as ONE source sees it: what lives there, and the NAMES of what does not.
185
+ *
186
+ * The second half is what lets a DDL stop lying. A batch holding every entity cannot tell
187
+ * a cross-source target from a typo, so a `ref()` falls back to a derived table name and
188
+ * the constraint is emitted against a table that may not exist. `elsewhere` says which
189
+ * misses are legitimate — and a target in neither list is a mistake, out loud.
190
+ */
191
+ export interface SourceView {
192
+ fronds: readonly { readonly name: string; readonly entities: readonly { readonly name: string }[] }[];
193
+ /** The auth provider's own entities, when they ride with this source. */
194
+ auth?: unknown;
195
+ elsewhere: readonly string[];
196
+ }
197
+
198
+ /**
199
+ * What a config file can carry about a source — values, never a live driver.
200
+ *
201
+ * `source` names the ADAPTER, and everything else belongs to the adapter it named:
202
+ * `adapter/sql` reads `dialect`, a file one reads `path`. That is the shape
203
+ * `EntityAdapters` already has — addressed by adapter, and what sits below is the
204
+ * adapter's own. Which is why the level `schema` owns is not repeated here.
205
+ */
206
+ export interface SourceConfig {
207
+ /** The adapter that realizes it. Absent means the conventional one. */
208
+ source?: string;
209
+ /** The entities whose rows live here. Absent on the default source: it holds the rest. */
210
+ entities?: string[];
211
+ [key: string]: unknown;
212
+ }
213
+
214
+ /**
215
+ * Which adapter answers a source name — one per process, no subject to hold.
216
+ *
217
+ * The shape `Generators` already has, and the refusal `resolveStorage` already made: only
218
+ * `sqlite` could be resolved from its name, because it was the one driver `defaults`
219
+ * depended on. That refusal had no owner — it lived in the package that happened to import
220
+ * the driver — so a second adapter had nowhere to say it exists. Here it does, and an
221
+ * unknown name is refused naming what this process answers.
222
+ *
223
+ * Registered at IMPORT by each adapter, so nothing central lists them.
224
+ */
225
+ export class Sources {
226
+ private static readonly registry = new Map<string, (conf: SourceConfig) => Source>();
227
+
228
+ static register(name: string, build: (conf: SourceConfig) => Source): void {
229
+ this.registry.set(name, build);
230
+ }
231
+
232
+ /** The source this name stands for, refused by name when nothing does. */
233
+ static resolve(name: string, conf: SourceConfig): Source {
234
+ const build = this.registry.get(name);
235
+ if (build) return build(conf);
236
+
237
+ throw new Error(
238
+ `Unknown source '${name}' — import the adapter that answers it, or register one with `
239
+ + `Sources.register('${name}', build). This process answers `
240
+ + `${[...this.registry.keys()].join(', ') || 'nothing yet'}.`,
241
+ );
242
+ }
243
+
244
+ /** Whether a name is answered, for a caller that must not throw to find out. */
245
+ static answers(name: string): boolean {
246
+ return this.registry.has(name);
247
+ }
248
+
249
+ /** What this process answers — what a refusal elsewhere prints. */
250
+ static answered(): string[] {
251
+ return [...this.registry.keys()];
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Container key of an entity's storage — 'reading' → 'ReadingStorage'.
164
257
  *
165
258
  * The twin of {@link repositoryKeyOf} and of `facadeKeyOf`: the key of a thing
166
259
  * lives with the thing. This one was spelled by hand in four places in
@@ -168,14 +261,30 @@ export type OrmFactory = (entity: SchemaView, name: string) => EntityOrm;
168
261
  * constructor asks for. Two readers of one convention, neither of them naming it,
169
262
  * so a rename would have moved one and left the other resolving to nothing.
170
263
  */
171
- export function ormKeyOf(entity: string): string {
172
- return `${upperFirst(entity)}Orm`;
264
+ export function storageKeyOf(entity: string): string {
265
+ return `${upperFirst(entity)}${HELD}`;
266
+ }
267
+
268
+ /**
269
+ * The entity behind a storage key, or `undefined` when the key is not one.
270
+ *
271
+ * The dual of the line above, for the reason `membersOfTogetherKey` is. `known` is what the
272
+ * suffix alone cannot answer: `FileStorage` is an ordinary class name.
273
+ */
274
+ export function entityOfStorageKey(key: string, known: (entity: string) => boolean): string | undefined {
275
+ if (key.length <= HELD.length || !key.endsWith(HELD)) return undefined;
276
+ const entity = lowerFirst(key.slice(0, -HELD.length));
277
+
278
+ return storageKeyOf(entity) === key && known(entity) ? entity : undefined;
173
279
  }
174
280
 
281
+ /** What a holder keeps, said in the key it is registered under. */
282
+ const HELD = 'Storage';
283
+
175
284
  /**
176
285
  * `Together<[Account, Ledger]>` — writes that stand or fall as one.
177
286
  *
178
- * `EntityOrm` is the port whose every gesture is ONE statement, and one statement is
287
+ * `Storage` is the port whose every gesture is ONE statement, and one statement is
179
288
  * atomic in every engine. This is the port whose unit is a BLOCK: what the callback did
180
289
  * happens entirely, or not at all. Nothing else separates them — the arity of the unit
181
290
  * of work is the whole distinction, which is why this lives here and not in a file of
@@ -191,7 +300,7 @@ export function ormKeyOf(entity: string): string {
191
300
  * ```
192
301
  *
193
302
  * **The second list is providers**, rebuilt inside the frame so that what THEY write is
194
- * covered too — a `Mirror` writes its pages through `EntityOrm<T>`, so naming it puts
303
+ * covered too — a `Mirror` writes its pages through `Storage<T>`, so naming it puts
195
304
  * them under the same unwind, with no locator and no second injection path:
196
305
  *
197
306
  * ```ts
@@ -214,7 +323,7 @@ export function ormKeyOf(entity: string): string {
214
323
  * than letting the author assume the stronger one.
215
324
  */
216
325
  export interface Together<E extends readonly unknown[], P extends readonly unknown[] = []> {
217
- run<R>(fn: (entities: { [K in keyof E]: EntityOrm<E[K]> }, providers: P) => Promise<R>): Promise<R>;
326
+ run<R>(fn: (entities: { [K in keyof E]: Storage<E[K]> }, providers: P) => Promise<R>): Promise<R>;
218
327
  }
219
328
 
220
329
  /**
package/src/verify.ts CHANGED
@@ -1,7 +1,7 @@
1
- import type { FrondDescriptor } from './scan/frond.js';
1
+ import type { FrondDescriptor } from './descriptor/frond.js';
2
2
  import { lowerFirst } from '@fougere/schema';
3
3
  import { repositoryKeyOf } from './prefab/repository.js';
4
- import { ormKeyOf } from './orm.js';
4
+ import { storageKeyOf } from './storage.js';
5
5
  import { presenterKeyOf } from './prefab/presenter.js';
6
6
  import { collectorKeyOf } from './prefab/collector.js';
7
7
 
@@ -63,7 +63,7 @@ function registrationsOf(frond: FrondDescriptor): Map<string, Registration> {
63
63
  for (const p of frond.presenters) put(presenterKeyOf(p.entityName), 'presenter');
64
64
  for (const c of frond.collectors) put(collectorKeyOf(c.typeName), 'collector');
65
65
  for (const e of frond.entities) {
66
- put(ormKeyOf(e.name), 'ORM');
66
+ put(storageKeyOf(e.name), 'storage');
67
67
  put(repositoryKeyOf(e.name), 'repository');
68
68
  }
69
69
  return out;
@@ -6,8 +6,7 @@
6
6
  * be repeated here too, and it had already lost `Fact` — the one whose position is
7
7
  * load-bearing, since the fall-through would otherwise hand it the caller's body.
8
8
  */
9
- import type { ParsedParam } from '../scan/handler-parser.js';
10
- import type { InvocationContext } from '../contract/Invocation.js';
9
+ import type { Param } from './signature.js';
11
10
  import { lowerFirst } from '@fougere/schema';
12
11
 
13
12
  // ── Types ─────────────────────────────────────
@@ -56,7 +55,7 @@ function coercionFor(typeName: string): 'number' | 'boolean' | undefined {
56
55
  * @param collectorTypeNames - Registration keys of the types a Collector answers for
57
56
  */
58
57
  export function computeBindingPlan(
59
- params: ParsedParam[],
58
+ params: Param[],
60
59
  collectorTypeNames: Set<string>,
61
60
  ): BindingPlan {
62
61
  return params.map((param) => {
@@ -117,66 +116,3 @@ export function computeBindingPlan(
117
116
  };
118
117
  });
119
118
  }
120
-
121
- // ── Resolve ───────────────────────────────────
122
-
123
- export interface CollectorResolver {
124
- collect(ctx: InvocationContext): Promise<unknown>;
125
- }
126
-
127
- /**
128
- * Resolve handler arguments from a BindingPlan + InvocationContext.
129
- */
130
- export async function resolveArgs(
131
- plan: BindingPlan,
132
- ctx: InvocationContext,
133
- resolveCollector?: (typeName: string) => CollectorResolver | undefined,
134
- ): Promise<unknown[]> {
135
- const args: unknown[] = [];
136
-
137
- for (const binding of plan) {
138
- switch (binding.source.kind) {
139
- case 'collector': {
140
- const collector = resolveCollector?.(binding.source.typeName);
141
- args.push(collector ? await collector.collect(ctx) : undefined);
142
- break;
143
- }
144
- case 'context': {
145
- args.push(ctx);
146
- break;
147
- }
148
- case 'param': {
149
- // `null` is a value, not a miss. Nullish coalescing used to make an explicit
150
- // nullable path/GraphQL argument fall through to query (or become undefined),
151
- // collapsing `T | null` into `T | undefined`. Only undefined means absent.
152
- const fromParams = ctx.params[binding.source.name];
153
- let val: unknown = fromParams === undefined
154
- ? ctx.query[binding.source.name]
155
- : fromParams;
156
- if (val != null && binding.source.coerce === 'number') val = Number(val);
157
- if (val != null && binding.source.coerce === 'boolean') val = val === 'true' || val === '1' || val === true;
158
- args.push(val);
159
- break;
160
- }
161
- case 'fact': {
162
- // A fact IS the payload — the whole of what happened, never a piece of it.
163
- // Identical to `body` today, and deliberately not sharing its branch: the two
164
- // agree by coincidence, not by rule, and the day `body` learns to look up a
165
- // value by parameter name a subscriber would receive ONE FIELD of the fact it
166
- // subscribed to. Splitting it costs nothing and removes that trap.
167
- args.push(ctx.body);
168
- break;
169
- }
170
- case 'body': {
171
- args.push(ctx.body);
172
- break;
173
- }
174
- case 'query': {
175
- args.push(ctx.query);
176
- break;
177
- }
178
- }
179
- }
180
-
181
- return args;
182
- }