@fougere/cli 0.2.0-alpha.2 → 0.3.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 (62) hide show
  1. package/README.md +10 -1
  2. package/app/commands/BuildCommand.ts +39 -0
  3. package/app/commands/CallCommand.ts +2 -2
  4. package/app/commands/CheckCommand.ts +2 -1
  5. package/app/commands/ExplainCommand.ts +77 -0
  6. package/app/commands/FreezeCommand.ts +107 -0
  7. package/app/commands/GrantCommand.ts +44 -0
  8. package/app/commands/KeysCommand.ts +56 -0
  9. package/app/commands/MigrateCommand.ts +54 -0
  10. package/app/commands/NewCommand.ts +5 -5
  11. package/app/commands/ServeCommand.ts +83 -7
  12. package/app/commands/grant-material.ts +5 -0
  13. package/dist/bin.js +53 -7
  14. package/dist/bin.js.map +1 -1
  15. package/dist/bridge.d.ts.map +1 -1
  16. package/dist/bridge.js +4 -4
  17. package/dist/bridge.js.map +1 -1
  18. package/dist/runner.d.ts.map +1 -1
  19. package/dist/runner.js +6 -6
  20. package/dist/runner.js.map +1 -1
  21. package/fronds/analysis/entities/Build.ts +7 -0
  22. package/fronds/analysis/entities/Explain.ts +8 -0
  23. package/fronds/analysis/entities/Freeze.ts +7 -0
  24. package/fronds/analysis/entities/Migrate.ts +7 -0
  25. package/fronds/analysis/handlers/BuildHandler.ts +60 -0
  26. package/fronds/analysis/handlers/CheckHandler.ts +40 -41
  27. package/fronds/analysis/handlers/ExplainHandler.ts +214 -0
  28. package/fronds/analysis/handlers/FreezeHandler.ts +172 -0
  29. package/fronds/analysis/handlers/MigrateHandler.ts +97 -0
  30. package/fronds/analysis/services/ProjectScan.ts +23 -7
  31. package/fronds/analysis/versions.ts +58 -0
  32. package/fronds/scaffold/entities/Grant.ts +6 -0
  33. package/fronds/scaffold/entities/Keys.ts +4 -0
  34. package/fronds/scaffold/entities/Serve.ts +2 -1
  35. package/fronds/scaffold/handlers/BuildFrondHandler.ts +11 -13
  36. package/fronds/scaffold/handlers/GrantHandler.ts +8 -0
  37. package/fronds/scaffold/handlers/KeysHandler.ts +8 -0
  38. package/fronds/scaffold/handlers/SyncHandler.ts +27 -24
  39. package/fronds/scaffold/services/ProjectWriter.ts +9 -8
  40. package/package.json +8 -7
  41. package/templates/admin/fronds/admin/handlers/UserHandler.ts +3 -5
  42. package/templates/admin/fronds/admin/package.json +1 -1
  43. package/templates/api/fronds/api/handlers/TaskHandler.ts +3 -5
  44. package/templates/api/fronds/api/package.json +1 -1
  45. package/templates/apps/nuxt/app/pages/index.vue +1 -1
  46. package/templates/blog/app/pages/posts/index.vue +1 -1
  47. package/templates/blog/app/pages/posts/manage.vue +1 -1
  48. package/templates/blog/app/pages/posts/new.vue +1 -1
  49. package/templates/blog/fronds/blog/handlers/PostHandler.ts +3 -5
  50. package/templates/blog/fronds/blog/package.json +1 -1
  51. package/templates/flat/AGENTS.md +14 -0
  52. package/templates/flat/CLAUDE.md +25 -3
  53. package/templates/frond/AGENTS.md +14 -0
  54. package/templates/frond/CLAUDE.md +25 -3
  55. package/templates/frond/fronds/__name__/handlers/PostHandler.ts +3 -5
  56. package/templates/frond/fronds/__name__/package.json +1 -1
  57. package/templates/frond/serve.mjs +3 -2
  58. package/templates/fronds/blank/package.json +1 -1
  59. package/templates/fronds/blog/handlers/PostHandler.ts +2 -4
  60. package/templates/fronds/blog/package.json +1 -1
  61. package/templates/workspace/AGENTS.md +14 -0
  62. package/templates/workspace/CLAUDE.md +25 -3
@@ -1,6 +1,7 @@
1
1
  import { cpSync, existsSync, renameSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
+ import { type Conventions, DEFAULT_CONVENTIONS, frondPackage } from '@fougere/core/node';
4
5
 
5
6
  /**
6
7
  * The monorepo's `packages/`, found by its workspace marker rather than counted
@@ -67,7 +68,7 @@ export default class ProjectWriter {
67
68
 
68
69
  /**
69
70
  * Put a frond template's directories at the project root. Only the directories: at the
70
- * root the app's own `package.json` is the frond's, and `@frond/<name>` comes from the
71
+ * root the app's own `package.json` is the frond's, and `@fronds/<name>` comes from the
71
72
  * directory through the Nuxt module's alias, so the template's package would only
72
73
  * duplicate it under a second name.
73
74
  */
@@ -81,8 +82,8 @@ export default class ProjectWriter {
81
82
  }
82
83
 
83
84
  /** Add a frond (business hexagon) under fronds/<name>. */
84
- addFrond(wsDir: string, template: string, name: string): { path: string } {
85
- const dest = join(wsDir, 'fronds', name);
85
+ addFrond(wsDir: string, template: string, name: string, conventions: Conventions = DEFAULT_CONVENTIONS): { path: string } {
86
+ const dest = join(wsDir, conventions.fronds, name);
86
87
  cpSync(join(TEMPLATES, 'fronds', template), dest, { recursive: true });
87
88
  // Only the import name. Carrying the convention is what makes a frond — the scan
88
89
  // reads directories. `fougere.frond` IS read now (`scanner.ts`, `frondNameOf`), but
@@ -90,7 +91,7 @@ export default class ProjectWriter {
90
91
  const pkgPath = join(dest, 'package.json');
91
92
  if (existsSync(pkgPath)) {
92
93
  const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as { name: string };
93
- pkg.name = `@frond/${name}`;
94
+ pkg.name = frondPackage(name, conventions);
94
95
  writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
95
96
  }
96
97
  return { path: dest };
@@ -119,11 +120,11 @@ export default class ProjectWriter {
119
120
  * A template cannot carry it: the frond is named at composition time (`blog:catalog`),
120
121
  * so a dependency written into `templates/apps/nuxt` would name the template instead
121
122
  * and resolve to nothing. Which is what happened — the generated app imported
122
- * `@frond/blog` whatever you had called it, and did not start.
123
+ * `@fronds/blog` whatever you had called it, and did not start.
123
124
  *
124
125
  * `fronds/` and `apps/` are the registry, like `listTemplates`: nothing to declare.
125
126
  */
126
- linkFronds(wsDir: string): void {
127
+ linkFronds(wsDir: string, conventions: Conventions = DEFAULT_CONVENTIONS): void {
127
128
  const dirs = (kind: string): string[] => {
128
129
  const dir = join(wsDir, kind);
129
130
  if (!existsSync(dir)) return [];
@@ -131,7 +132,7 @@ export default class ProjectWriter {
131
132
  return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
132
133
  };
133
134
 
134
- const fronds = dirs('fronds');
135
+ const fronds = dirs(conventions.fronds);
135
136
  if (fronds.length === 0) return;
136
137
 
137
138
  for (const app of dirs('apps')) {
@@ -140,7 +141,7 @@ export default class ProjectWriter {
140
141
 
141
142
  const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as { dependencies?: Record<string, string> };
142
143
  pkg.dependencies ??= {};
143
- for (const frond of fronds) pkg.dependencies[`@frond/${frond}`] = 'workspace:*';
144
+ for (const frond of fronds) pkg.dependencies[frondPackage(frond, conventions)] = 'workspace:*';
144
145
  writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
145
146
  }
146
147
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fougere/cli",
3
- "version": "0.2.0-alpha.2",
3
+ "version": "0.3.0-alpha.0",
4
4
  "description": "The Fougere CLI — compose a workspace, serve a frond, call an operation.",
5
5
  "keywords": [
6
6
  "fougere",
@@ -40,11 +40,12 @@
40
40
  "consola": "^3.4.2",
41
41
  "jiti": "^2.4.2",
42
42
  "picocolors": "^1.1.1",
43
- "@fougere/schema": "0.2.0-alpha.2",
44
- "@fougere/container": "0.2.0-alpha.2",
45
- "@fougere/core": "0.2.0-alpha.2",
46
- "@fougere/defaults": "0.2.0-alpha.2",
47
- "@fougere/transport-http": "0.2.0-alpha.2"
43
+ "@fougere/container": "0.3.0-alpha.0",
44
+ "@fougere/schema": "0.3.0-alpha.0",
45
+ "@fougere/adapter-sql": "0.3.0-alpha.0",
46
+ "@fougere/transport-http": "0.3.0-alpha.0",
47
+ "@fougere/core": "0.3.0-alpha.0",
48
+ "@fougere/defaults": "0.3.0-alpha.0"
48
49
  },
49
50
  "devDependencies": {
50
51
  "vitest": "^4.1.0"
@@ -53,7 +54,7 @@
53
54
  "access": "public"
54
55
  },
55
56
  "scripts": {
56
- "build": "rm -rf dist && tsc && chmod +x dist/bin.js",
57
+ "build": "rm -rf dist && tsc && chmod +x dist/bin.js && node dist/bin.js build",
57
58
  "test": "vitest run",
58
59
  "test:watch": "vitest",
59
60
  "typecheck": "tsc --noEmit && tsc -p tsconfig.runtime.json && tsc -p tsconfig.templates.json"
@@ -13,7 +13,7 @@ export default class UserHandler extends Crud(User) {
13
13
  if (!user) {
14
14
  throw new FougereError({ code: ErrorCode.NOT_FOUND, message: `User '${id}' not found`, entity: 'user', operation: 'deactivate' });
15
15
  }
16
- if ((user as { status?: string }).status === 'inactive') {
16
+ if (user.status === 'inactive') {
17
17
  throw new FougereError({ code: ErrorCode.CONFLICT, message: 'Already inactive', entity: 'user', operation: 'deactivate' });
18
18
  }
19
19
  return this.orm.update(id, { status: 'inactive' });
@@ -21,9 +21,7 @@ export default class UserHandler extends Crud(User) {
21
21
 
22
22
  /** Active users, projected to the card contract. */
23
23
  async active(): Promise<UserCard[]> {
24
- const all = await this.orm.list();
25
- return all
26
- .filter((u) => (u as { status?: string }).status === 'active')
27
- .map((u) => ({ id: String(u.id), name: String(u.name), status: 'active' })) as UserCard[];
24
+ const users = await this.orm.list({ where: { status: 'active' } });
25
+ return users.map(({ id, name, status }) => ({ id, name, status }));
28
26
  }
29
27
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "@frond/admin",
2
+ "name": "@fronds/admin",
3
3
  "version": "0.0.1",
4
4
  "type": "module",
5
5
  "exports": {
@@ -13,7 +13,7 @@ export default class TaskHandler extends Crud(Task) {
13
13
  if (!task) {
14
14
  throw new FougereError({ code: ErrorCode.NOT_FOUND, message: `Task '${id}' not found`, entity: 'task', operation: 'complete' });
15
15
  }
16
- if ((task as { status?: string }).status === 'done') {
16
+ if (task.status === 'done') {
17
17
  throw new FougereError({ code: ErrorCode.CONFLICT, message: 'Already done', entity: 'task', operation: 'complete' });
18
18
  }
19
19
  return this.orm.update(id, { status: 'done' });
@@ -21,9 +21,7 @@ export default class TaskHandler extends Crud(Task) {
21
21
 
22
22
  /** Still-open tasks, projected to the card contract. */
23
23
  async open(): Promise<TaskCard[]> {
24
- const all = await this.orm.list();
25
- return all
26
- .filter((t) => (t as { status?: string }).status === 'open')
27
- .map((t) => ({ id: String(t.id), title: String(t.title), status: 'open' })) as TaskCard[];
24
+ const tasks = await this.orm.list({ where: { status: 'open' } });
25
+ return tasks.map(({ id, title, status }) => ({ id, title, status }));
28
26
  }
29
27
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "@frond/api",
2
+ "name": "@fronds/api",
3
3
  "version": "0.0.1",
4
4
  "type": "module",
5
5
  "exports": {
@@ -3,7 +3,7 @@
3
3
  // They take an ENTITY, so this page names none: what is composed here is yours, and a
4
4
  // scaffold that guessed at an entity shipped a page that could not run.
5
5
  //
6
- // import Post from '@frond/<your-frond>/entities/Post'
6
+ // import Post from '@fronds/<your-frond>/entities/Post'
7
7
  // const { items, loading } = await useQuery(Post, 'list')
8
8
  // const { values, errors, submit } = useFormFor(Post)
9
9
  // const { execute } = useCommand(Post, 'publish')
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import Post from '@frond/blog/entities/Post';
2
+ import Post from '@fronds/blog/entities/Post';
3
3
 
4
4
  interface Card { id: string; title: string; status: string }
5
5
  const { items: posts, loading, error } = await useQuery<Card>(Post, 'published');
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import Post from '@frond/blog/entities/Post';
2
+ import Post from '@fronds/blog/entities/Post';
3
3
 
4
4
  interface Row { id: string; title: string; status: 'draft' | 'published' }
5
5
  const { items: posts, loading } = await useQuery<Row>(Post, 'list');
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import Post from '@frond/blog/entities/Post';
2
+ import Post from '@fronds/blog/entities/Post';
3
3
 
4
4
  const { values, errors, submit, loading, error } = useFormFor(Post);
5
5
 
@@ -19,7 +19,7 @@ export default class PostHandler extends Crud(Post) {
19
19
  if (!post) {
20
20
  throw new FougereError({ code: ErrorCode.NOT_FOUND, message: `Post '${id}' not found`, entity: 'post', operation: 'publish' });
21
21
  }
22
- if ((post as { status?: string }).status === 'published') {
22
+ if (post.status === 'published') {
23
23
  throw new FougereError({ code: ErrorCode.CONFLICT, message: 'Already published', entity: 'post', operation: 'publish' });
24
24
  }
25
25
  return this.orm.update(id, { status: 'published' });
@@ -27,9 +27,7 @@ export default class PostHandler extends Crud(Post) {
27
27
 
28
28
  /** Only published posts exist for the outside world, projected to the card. */
29
29
  async published(): Promise<PostCard[]> {
30
- const all = await this.orm.list();
31
- return all
32
- .filter((p) => (p as { status?: string }).status === 'published')
33
- .map((p) => ({ id: String(p.id), title: String(p.title), status: 'published' })) as PostCard[];
30
+ const posts = await this.orm.list({ where: { status: 'published' } });
31
+ return posts.map(({ id, title, status }) => ({ id, title, status }));
34
32
  }
35
33
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "@frond/blog",
2
+ "name": "@fronds/blog",
3
3
  "version": "0.0.1",
4
4
  "type": "module",
5
5
  "exports": {
@@ -0,0 +1,14 @@
1
+ # Working in this application
2
+
3
+ This project is built with **Fougere**. Read `CLAUDE.md` for the model and architecture guidance
4
+ that applies to every coding agent.
5
+
6
+ ## Required verification workflow
7
+
8
+ After every change to handlers, entities, Fronds, configuration, or topology:
9
+
10
+ 1. Run `fougere check`.
11
+ 2. Fix every deterministic error it reports before continuing.
12
+ 3. Run the relevant tests, then run `pnpm typecheck`.
13
+
14
+ `fougere check` is the Fougere model barrier; tests and TypeScript come after it passes.
@@ -44,7 +44,18 @@ exist to add what a projection cannot derive — never to replace it.
44
44
 
45
45
  ## Reading data
46
46
 
47
- `EntityOrm`, injected by type, is the only data access:
47
+ Storage is reached through a repository. Never inject `EntityOrm` directly into a handler,
48
+ presenter or collector — the boot refuses it. With no repository file, ask for the default shape:
49
+
50
+ ```ts
51
+ import type { RepositoryOf } from '@fougere/core';
52
+ import Product from '../entities/Product.js';
53
+
54
+ constructor(private products: RepositoryOf<Product>) {}
55
+ ```
56
+
57
+ If the handler extends `Crud(Product)`, its inherited `this.orm` is already backed by that
58
+ repository; do not add a constructor. The repository forwards the guarded storage gestures:
48
59
 
49
60
  ```
50
61
  list(options?) every row — `options.where` filters, plus paging and sorting
@@ -56,10 +67,21 @@ create / update / delete
56
67
 
57
68
  Read a relation with `findAllBy`. Never read a whole table to filter it in memory.
58
69
 
70
+ When a query deserves a domain name, add `repositories/ProductRepository.ts` with
71
+ `class ProductRepository extends Repository(Product)`, put the query there, and inject
72
+ `ProductRepository`. A repository is registered as a provider and remains the only route to storage.
73
+
59
74
  ## Checking your work
60
75
 
76
+ After every change to handlers, entities, Fronds, configuration, or topology:
77
+
78
+ 1. Run `fougere check`.
79
+ 2. Fix every deterministic error it reports before continuing.
80
+ 3. Run the relevant tests, then run the project typecheck.
81
+
61
82
  ```bash
62
- npx tsc -p tsconfig.frond.json # the compiler — free, immediate, and it catches most of it
83
+ fougere check
84
+ pnpm typecheck
63
85
  ```
64
86
 
65
- Run it. It is the first judge, and the cheapest.
87
+ `fougere check` is the Fougere model barrier; tests and TypeScript come after it passes.
@@ -0,0 +1,14 @@
1
+ # Working in this application
2
+
3
+ This project is built with **Fougere**. Read `CLAUDE.md` for the model and architecture guidance
4
+ that applies to every coding agent.
5
+
6
+ ## Required verification workflow
7
+
8
+ After every change to handlers, entities, Fronds, configuration, or topology:
9
+
10
+ 1. Run `fougere check`.
11
+ 2. Fix every deterministic error it reports before continuing.
12
+ 3. Run the relevant tests, then run `npx tsc -p fronds --noEmit`.
13
+
14
+ `fougere check` is the Fougere model barrier; tests and TypeScript come after it passes.
@@ -44,7 +44,18 @@ exist to add what a projection cannot derive — never to replace it.
44
44
 
45
45
  ## Reading data
46
46
 
47
- `EntityOrm`, injected by type, is the only data access:
47
+ Storage is reached through a repository. Never inject `EntityOrm` directly into a handler,
48
+ presenter or collector — the boot refuses it. With no repository file, ask for the default shape:
49
+
50
+ ```ts
51
+ import type { RepositoryOf } from '@fougere/core';
52
+ import Product from '../entities/Product.js';
53
+
54
+ constructor(private products: RepositoryOf<Product>) {}
55
+ ```
56
+
57
+ If the handler extends `Crud(Product)`, its inherited `this.orm` is already backed by that
58
+ repository; do not add a constructor. The repository forwards the guarded storage gestures:
48
59
 
49
60
  ```
50
61
  list(options?) every row — `options.where` filters, plus paging and sorting
@@ -56,10 +67,21 @@ create / update / delete
56
67
 
57
68
  Read a relation with `findAllBy`. Never read a whole table to filter it in memory.
58
69
 
70
+ When a query deserves a domain name, add `repositories/ProductRepository.ts` with
71
+ `class ProductRepository extends Repository(Product)`, put the query there, and inject
72
+ `ProductRepository`. A repository is registered as a provider and remains the only route to storage.
73
+
59
74
  ## Checking your work
60
75
 
76
+ After every change to handlers, entities, Fronds, configuration, or topology:
77
+
78
+ 1. Run `fougere check`.
79
+ 2. Fix every deterministic error it reports before continuing.
80
+ 3. Run the relevant tests, then run the project typecheck.
81
+
61
82
  ```bash
62
- npx tsc -p fronds --noEmit # the compiler — free, immediate, and it catches most of it
83
+ fougere check
84
+ npx tsc -p fronds --noEmit
63
85
  ```
64
86
 
65
- Run it. It is the first judge, and the cheapest.
87
+ `fougere check` is the Fougere model barrier; tests and TypeScript come after it passes.
@@ -10,7 +10,7 @@ export default class PostHandler extends Crud(Post) {
10
10
  if (!post) {
11
11
  throw new FougereError({ code: ErrorCode.NOT_FOUND, message: `Post '${id}' not found`, entity: 'post', operation: 'publish' });
12
12
  }
13
- if ((post as { status?: string }).status === 'published') {
13
+ if (post.status === 'published') {
14
14
  throw new FougereError({ code: ErrorCode.CONFLICT, message: 'Already published', entity: 'post', operation: 'publish' });
15
15
  }
16
16
  return this.orm.update(id, { status: 'published' });
@@ -18,9 +18,7 @@ export default class PostHandler extends Crud(Post) {
18
18
 
19
19
  /** Only published posts, projected to the card. */
20
20
  async published(): Promise<PostCard[]> {
21
- const all = await this.orm.list();
22
- return all
23
- .filter((p) => (p as { status?: string }).status === 'published')
24
- .map((p) => ({ id: String(p.id), title: String(p.title), status: 'published' })) as PostCard[];
21
+ const posts = await this.orm.list({ where: { status: 'published' } });
22
+ return posts.map(({ id, title, status }) => ({ id, title, status }));
25
23
  }
26
24
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "@frond/__name__",
2
+ "name": "@fronds/__name__",
3
3
  "version": "0.0.1",
4
4
  "type": "module",
5
5
  "exports": {
@@ -8,11 +8,12 @@
8
8
  * Nothing in the frond changes — only where it runs.
9
9
  */
10
10
  import { createJiti } from 'jiti';
11
- import { createLocalRunner, setModuleLoader, frondAliases, Logger } from '@fougere/core';
11
+ import { createLocalRunner, Logger } from '@fougere/core';
12
+ import { setModuleLoader, frondAliases } from '@fougere/core/node';
12
13
  import { bootAppFromConfig } from '@fougere/defaults';
13
14
  import { serve } from '@fougere/transport-http';
14
15
 
15
- // `frondAliases` is what makes `@frond/<neighbour>/entities/X.js` resolve — the
16
+ // `frondAliases` is what makes `@fronds/<neighbour>/entities/X.js` resolve — the
16
17
  // named form a frond uses for its neighbour. A bare jiti loads frond sources but
17
18
  // not that convention, so a collector or handler importing across fronds dies here
18
19
  // while the same code works in-process.
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "@frond/__name__",
2
+ "name": "@fronds/__name__",
3
3
  "version": "0.0.1",
4
4
  "type": "module",
5
5
  "exports": {
@@ -30,9 +30,7 @@ export default class PostHandler extends Crud(Post) {
30
30
 
31
31
  /** Only published posts, projected to the card. */
32
32
  async published(): Promise<PostCard[]> {
33
- const all = await this.orm.list();
34
- return all
35
- .filter((p) => p.status === 'published')
36
- .map(({ id, title, status }) => ({ id, title, status }));
33
+ const posts = await this.orm.list({ where: { status: 'published' } });
34
+ return posts.map(({ id, title, status }) => ({ id, title, status }));
37
35
  }
38
36
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "@frond/__name__",
2
+ "name": "@fronds/__name__",
3
3
  "version": "0.0.1",
4
4
  "type": "module",
5
5
  "exports": {
@@ -0,0 +1,14 @@
1
+ # Working in this application
2
+
3
+ This project is built with **Fougere**. Read `CLAUDE.md` for the model and architecture guidance
4
+ that applies to every coding agent.
5
+
6
+ ## Required verification workflow
7
+
8
+ After every change to handlers, entities, Fronds, configuration, or topology:
9
+
10
+ 1. Run `fougere check`.
11
+ 2. Fix every deterministic error it reports before continuing.
12
+ 3. Run the relevant tests, then run `pnpm typecheck`.
13
+
14
+ `fougere check` is the Fougere model barrier; tests and TypeScript come after it passes.
@@ -44,7 +44,18 @@ exist to add what a projection cannot derive — never to replace it.
44
44
 
45
45
  ## Reading data
46
46
 
47
- `EntityOrm`, injected by type, is the only data access:
47
+ Storage is reached through a repository. Never inject `EntityOrm` directly into a handler,
48
+ presenter or collector — the boot refuses it. With no repository file, ask for the default shape:
49
+
50
+ ```ts
51
+ import type { RepositoryOf } from '@fougere/core';
52
+ import Product from '../entities/Product.js';
53
+
54
+ constructor(private products: RepositoryOf<Product>) {}
55
+ ```
56
+
57
+ If the handler extends `Crud(Product)`, its inherited `this.orm` is already backed by that
58
+ repository; do not add a constructor. The repository forwards the guarded storage gestures:
48
59
 
49
60
  ```
50
61
  list(options?) every row — `options.where` filters, plus paging and sorting
@@ -56,10 +67,21 @@ create / update / delete
56
67
 
57
68
  Read a relation with `findAllBy`. Never read a whole table to filter it in memory.
58
69
 
70
+ When a query deserves a domain name, add `repositories/ProductRepository.ts` with
71
+ `class ProductRepository extends Repository(Product)`, put the query there, and inject
72
+ `ProductRepository`. A repository is registered as a provider and remains the only route to storage.
73
+
59
74
  ## Checking your work
60
75
 
76
+ After every change to handlers, entities, Fronds, configuration, or topology:
77
+
78
+ 1. Run `fougere check`.
79
+ 2. Fix every deterministic error it reports before continuing.
80
+ 3. Run the relevant tests, then run the project typecheck.
81
+
61
82
  ```bash
62
- pnpm typecheck # the compiler — free, immediate, and it catches most of it
83
+ fougere check
84
+ pnpm typecheck
63
85
  ```
64
86
 
65
- Run it. It is the first judge, and the cheapest.
87
+ `fougere check` is the Fougere model barrier; tests and TypeScript come after it passes.