@beignet/cli 0.0.47 → 0.0.49

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 (144) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +126 -42
  3. package/dist/analysis/contract-factories.d.ts +17 -0
  4. package/dist/analysis/contract-factories.d.ts.map +1 -0
  5. package/dist/analysis/contract-factories.js +176 -0
  6. package/dist/analysis/contract-factories.js.map +1 -0
  7. package/dist/analysis/layers.d.ts +5 -0
  8. package/dist/analysis/layers.d.ts.map +1 -0
  9. package/dist/analysis/layers.js +172 -0
  10. package/dist/analysis/layers.js.map +1 -0
  11. package/dist/analysis/port-wiring.d.ts +23 -0
  12. package/dist/analysis/port-wiring.d.ts.map +1 -0
  13. package/dist/analysis/port-wiring.js +379 -0
  14. package/dist/analysis/port-wiring.js.map +1 -0
  15. package/dist/analysis/source-index.d.ts +5 -0
  16. package/dist/analysis/source-index.d.ts.map +1 -1
  17. package/dist/analysis/source-index.js +171 -42
  18. package/dist/analysis/source-index.js.map +1 -1
  19. package/dist/analysis/workspace.d.ts +10 -4
  20. package/dist/analysis/workspace.d.ts.map +1 -1
  21. package/dist/analysis/workspace.js +66 -6
  22. package/dist/analysis/workspace.js.map +1 -1
  23. package/dist/app-map.d.ts.map +1 -1
  24. package/dist/app-map.js +311 -92
  25. package/dist/app-map.js.map +1 -1
  26. package/dist/check.d.ts +8 -1
  27. package/dist/check.d.ts.map +1 -1
  28. package/dist/check.js +55 -9
  29. package/dist/check.js.map +1 -1
  30. package/dist/db.d.ts +38 -0
  31. package/dist/db.d.ts.map +1 -1
  32. package/dist/db.js +52 -0
  33. package/dist/db.js.map +1 -1
  34. package/dist/explain.js +4 -4
  35. package/dist/explain.js.map +1 -1
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +73 -6
  38. package/dist/index.js.map +1 -1
  39. package/dist/inspect.d.ts.map +1 -1
  40. package/dist/inspect.js +454 -170
  41. package/dist/inspect.js.map +1 -1
  42. package/dist/lib.d.ts +2 -2
  43. package/dist/lib.d.ts.map +1 -1
  44. package/dist/lib.js +1 -1
  45. package/dist/lib.js.map +1 -1
  46. package/dist/lint.d.ts.map +1 -1
  47. package/dist/lint.js +309 -487
  48. package/dist/lint.js.map +1 -1
  49. package/dist/make/inbox.d.ts.map +1 -1
  50. package/dist/make/inbox.js +12 -5
  51. package/dist/make/inbox.js.map +1 -1
  52. package/dist/make/payments.js +3 -3
  53. package/dist/make/payments.js.map +1 -1
  54. package/dist/make/tenancy.js +1 -1
  55. package/dist/make/tenancy.js.map +1 -1
  56. package/dist/make.d.ts.map +1 -1
  57. package/dist/make.js +144 -8
  58. package/dist/make.js.map +1 -1
  59. package/dist/mcp.d.ts +1 -1
  60. package/dist/mcp.d.ts.map +1 -1
  61. package/dist/mcp.js +56 -19
  62. package/dist/mcp.js.map +1 -1
  63. package/dist/preflight.d.ts +11 -3
  64. package/dist/preflight.d.ts.map +1 -1
  65. package/dist/preflight.js +139 -16
  66. package/dist/preflight.js.map +1 -1
  67. package/dist/provider-add.d.ts.map +1 -1
  68. package/dist/provider-add.js +149 -9
  69. package/dist/provider-add.js.map +1 -1
  70. package/dist/provider-audit.d.ts +13 -0
  71. package/dist/provider-audit.d.ts.map +1 -1
  72. package/dist/provider-audit.js +122 -8
  73. package/dist/provider-audit.js.map +1 -1
  74. package/dist/registry-edits.d.ts +7 -0
  75. package/dist/registry-edits.d.ts.map +1 -1
  76. package/dist/registry-edits.js +237 -53
  77. package/dist/registry-edits.js.map +1 -1
  78. package/dist/templates/agents.d.ts.map +1 -1
  79. package/dist/templates/agents.js +29 -7
  80. package/dist/templates/agents.js.map +1 -1
  81. package/dist/templates/base.d.ts +1 -0
  82. package/dist/templates/base.d.ts.map +1 -1
  83. package/dist/templates/base.js +20 -6
  84. package/dist/templates/base.js.map +1 -1
  85. package/dist/templates/db/index.d.ts +1 -0
  86. package/dist/templates/db/index.d.ts.map +1 -1
  87. package/dist/templates/db/index.js.map +1 -1
  88. package/dist/templates/db/migration-status.d.ts +9 -0
  89. package/dist/templates/db/migration-status.d.ts.map +1 -0
  90. package/dist/templates/db/migration-status.js +117 -0
  91. package/dist/templates/db/migration-status.js.map +1 -0
  92. package/dist/templates/db/mysql.d.ts.map +1 -1
  93. package/dist/templates/db/mysql.js +23 -0
  94. package/dist/templates/db/mysql.js.map +1 -1
  95. package/dist/templates/db/postgres.d.ts.map +1 -1
  96. package/dist/templates/db/postgres.js +21 -0
  97. package/dist/templates/db/postgres.js.map +1 -1
  98. package/dist/templates/db/sqlite.d.ts.map +1 -1
  99. package/dist/templates/db/sqlite.js +53 -0
  100. package/dist/templates/db/sqlite.js.map +1 -1
  101. package/dist/templates/index.d.ts.map +1 -1
  102. package/dist/templates/index.js +5 -0
  103. package/dist/templates/index.js.map +1 -1
  104. package/dist/templates/server.d.ts.map +1 -1
  105. package/dist/templates/server.js +2 -0
  106. package/dist/templates/server.js.map +1 -1
  107. package/dist/templates/shared.d.ts +1 -0
  108. package/dist/templates/shared.d.ts.map +1 -1
  109. package/dist/templates/shared.js +4 -2
  110. package/dist/templates/shared.js.map +1 -1
  111. package/package.json +7 -6
  112. package/skills/app-structure/SKILL.md +56 -11
  113. package/src/analysis/contract-factories.ts +256 -0
  114. package/src/analysis/layers.ts +266 -0
  115. package/src/analysis/port-wiring.ts +506 -0
  116. package/src/analysis/source-index.ts +225 -57
  117. package/src/analysis/workspace.ts +115 -6
  118. package/src/app-map.ts +418 -109
  119. package/src/check.ts +86 -10
  120. package/src/db.ts +97 -0
  121. package/src/explain.ts +4 -4
  122. package/src/index.ts +108 -7
  123. package/src/inspect.ts +659 -216
  124. package/src/lib.ts +9 -1
  125. package/src/lint.ts +393 -643
  126. package/src/make/inbox.ts +12 -5
  127. package/src/make/payments.ts +3 -3
  128. package/src/make/tenancy.ts +1 -1
  129. package/src/make.ts +225 -8
  130. package/src/mcp.ts +73 -21
  131. package/src/preflight.ts +195 -16
  132. package/src/provider-add.ts +193 -9
  133. package/src/provider-audit.ts +210 -18
  134. package/src/registry-edits.ts +313 -63
  135. package/src/templates/agents.ts +29 -7
  136. package/src/templates/base.ts +21 -4
  137. package/src/templates/db/index.ts +1 -0
  138. package/src/templates/db/migration-status.ts +126 -0
  139. package/src/templates/db/mysql.ts +23 -0
  140. package/src/templates/db/postgres.ts +21 -0
  141. package/src/templates/db/sqlite.ts +53 -0
  142. package/src/templates/index.ts +5 -0
  143. package/src/templates/server.ts +2 -0
  144. package/src/templates/shared.ts +4 -2
@@ -8,6 +8,9 @@ description: "Maintain Beignet app structure with @beignet/cli: deterministic ap
8
8
  Use this skill when working with Beignet apps through `@beignet/cli`. Prefer
9
9
  generators and validation commands before hand-editing framework wiring.
10
10
 
11
+ Beignet apps require Node.js 22.12 or newer. Bun 1.3.14 or newer is supported
12
+ for Beignet commands.
13
+
11
14
  ## Starter Sources
12
15
 
13
16
  Generated apps include:
@@ -64,6 +67,7 @@ changing `infra/db/schema/`, run the app's db lifecycle commands:
64
67
  ```bash
65
68
  beignet db generate
66
69
  beignet db migrate
70
+ beignet db status
67
71
  ```
68
72
 
69
73
  ## Explicit Registration
@@ -74,6 +78,10 @@ Hand-written files do not run until registered:
74
78
  - schedules -> `server/schedules.ts`
75
79
  - tasks -> `server/tasks.ts`
76
80
  - outbox events/jobs -> `server/outbox.ts`
81
+ - outbox jobs also require `jobs: JobDispatcherPort` in `AppPorts`, bound
82
+ directly or deferred to a provider; `make job` installs an app-owned inline
83
+ dispatcher only when none is already wired, and a later Inngest or BullMQ
84
+ preset replaces only that marked generated fallback
77
85
  - listeners -> `server/listeners.ts` plus `registerListeners(...)` in server provider wiring
78
86
  - queued notifications -> `server/notifications.ts`, plus the delivery job in
79
87
  every job worker or outbox registry that can receive it
@@ -94,6 +102,18 @@ Feature route groups should import the app-bound `defineRouteGroup` from
94
102
  `lib/routes.ts`. `doctor` checks both that feature routes use that builder and
95
103
  that the builder itself calls `createRoutes<AppContext>()`.
96
104
 
105
+ `beignet routes` recognizes contract factories through value imports from
106
+ `@beignet/core/contracts`, including named aliases and namespace imports. It
107
+ ignores same-named methods on unrelated objects.
108
+
109
+ Doctor also reports an outbox registry containing jobs when the configured
110
+ ports and port-wiring files do not declare and bind or defer the `jobs` port.
111
+ This finding is diagnostic-only because choosing a production dispatcher is an
112
+ application decision. Keep the registry's `jobs` entries and the `jobs` port
113
+ explicit; doctor reports indeterminate registry or port-wiring shapes when
114
+ they prevent it from verifying required wiring, and `make job` stops instead
115
+ of replacing spread- or helper-based wiring it cannot verify.
116
+
97
117
  Run `beignet doctor --strict` after hand edits. Use
98
118
  `beignet doctor --fix --dry-run` to inspect exact low-risk repair patches, then
99
119
  apply the returned plan with `beignet doctor --fix --plan <plan-id>` and
@@ -148,20 +168,38 @@ registration status, doctor/lint findings, and unresolved references. Use
148
168
  The graph resolves local modules with the full TypeScript `baseUrl` and `paths`
149
169
  configuration. Treat `local_import_unresolved` and
150
170
  `contract_declaration_unresolved` as real blind spots to fix, not absent
151
- relationships. Registration diagnostics carry `subject.file` and
152
- `subject.exportName` so agents can act on the declaration without parsing the
153
- diagnostic message.
171
+ relationships. This includes direct and contract-group declarations whose
172
+ method, path, or group prefix is dynamic. Local fluent-builder aliases and
173
+ named local exports retain the exported declaration identity. Registration
174
+ diagnostics carry `subject.file` and `subject.exportName` so agents can act on
175
+ the declaration without parsing the diagnostic message.
154
176
 
155
177
  `beignet lint` enforces dependency direction:
156
178
 
157
179
  - domain and use cases must not import infra, routes, server, providers, React,
158
180
  or client modules
159
- - client-reachable modules must not import server-only code
181
+ - contracts, schemas, and client-reachable modules must not import server-only
182
+ code
160
183
  - routes may import contracts, use cases, policies, and app context types, but
161
184
  not concrete infra
185
+ - contract query schemas require an explicit second `.query(schema, transport)`
186
+ argument built with `defineQueryTransport`; the shared transport, not schema
187
+ coercion, defines client, server, and OpenAPI URL serialization
188
+ - agent capabilities may adapt existing use cases; use cases, workflows, seeds,
189
+ and feature-root helpers must not depend back on agent adapters
190
+ - seeds should compose factories and context ports rather than concrete infra,
191
+ providers, UI, or runtime composition
192
+ - TypeScript imports, re-exports, `require()`, import assignments, and literal
193
+ dynamic imports all participate in the same graph
194
+ - feature-root helpers are classified and followed transitively, so they cannot
195
+ hide a forbidden dependency; computed module references in constrained layers
196
+ fail when lint cannot verify their destination
197
+ - near-miss canonical layers and known aliases such as `usecases/` and
198
+ `events/` report the same expected path from lint and doctor
162
199
 
163
200
  `beignet doctor --strict` reports structure and registration drift:
164
201
 
202
+ - contract query schemas without an explicit transport
165
203
  - missing route group registration
166
204
  - route files that bypass the configured app-bound route builder
167
205
  - route builders that are not bound with `createRoutes<AppContext>()`
@@ -195,7 +233,7 @@ Generated apps register:
195
233
  }
196
234
  ```
197
235
 
198
- The MCP server exposes `app_map`, `explain`, `check`, `db`,
236
+ The MCP server exposes `app_map`, `explain`, `check`, `db`, `db_status`,
199
237
  `db_schema_sync`, `task_run`, `schedule_run`, `outbox_inspect`, `outbox_run`,
200
238
  `routes`, `doctor`, `doctor_fix_plan`, `doctor_fix`, `lint`, `make`, and
201
239
  `provider_add`. It also publishes `beignet://app/guidance` and the
@@ -210,10 +248,15 @@ concept or diagnostic and returns the same deterministic result as
210
248
  `beignet explain <kind> <target> --json`. Explain accepts every app-map node
211
249
  kind plus `diagnostic`; targets may use stable IDs, runtime or declaration
212
250
  names, source selectors, and applicable aliases. `check` accepts optional
213
- `{ preflight?: boolean, timeoutMs?: number }`, bounds failure output, cancels
214
- the active script with the MCP request, and returns the same versioned result
215
- as `beignet check --json`. It does not apply Beignet fixes, but app package
216
- scripts retain their normal side effects. `db` accepts
251
+ `{ preflight?: boolean, preflightConnect?: boolean, connectTimeoutMs?: number, timeoutMs?: number }`.
252
+ Connected preflight adds migration status and dependency health checks. Check
253
+ bounds failure output, cancels the active script with the MCP request, and
254
+ returns the same versioned result as `beignet check --json`. It does not apply
255
+ Beignet fixes, but app package scripts retain their normal side effects. The
256
+ target package must declare `@beignet/core`, `@beignet/next`, or `@beignet/web`
257
+ as a runtime dependency. The root must also contain `app-context.ts` or
258
+ `beignet.config.*`, or the canonical `features/` and `server/index.ts` pair;
259
+ otherwise check stops before analysis or scripts. `db` accepts
217
260
  `{ command: "generate" | "migrate" | "seed" | "reset", dryRun?, timeoutMs? }`
218
261
  and returns the matching versioned `beignet db <command> --json` report with
219
262
  bounded output, cancellation, and process-tree timeouts. `db_schema_sync`
@@ -221,7 +264,9 @@ accepts `{ dialect?, tables?, output?, dryRun? }` and returns the matching
221
264
  `beignet db schema sync --json` report. For lifecycle commands, `dryRun`
222
265
  validates and reports the app-owned script without executing it; it does not
223
266
  simulate SQL or data changes. `reset` remains environment-sensitive even
224
- though the generated entrypoint has a production guard. `task_run` and
267
+ though the generated entrypoint has a production guard. `db_status` runs the
268
+ app-owned read-only migration inspection: current exits 0, pending exits 2,
269
+ and inspection failures use another non-zero exit. `task_run` and
225
270
  `schedule_run` execute registered app workflows. Use `outbox_inspect` for
226
271
  read-only `list` and `show`, and `outbox_run` for state-changing `drain`,
227
272
  `requeue`, `purge`, and `prune`; purge and prune support `dryRun`. Operational
@@ -255,7 +300,7 @@ When MCP is available, call `check` after edits to run the full validation loop
255
300
  through the app-local CLI without reconstructing these commands individually.
256
301
  For a generated persistent resource, the complete structured workflow is
257
302
  `app_map` → `explain` → `make` → `db_schema_sync` when provider tables changed
258
- → `db` generate → `db` migrate → the relevant `task_run`, `schedule_run`, or
303
+ → `db` generate → `db` migrate → `db_status` → the relevant `task_run`, `schedule_run`, or
259
304
  outbox tool when operational work is required → `check`.
260
305
 
261
306
  For generated app template changes inside the Beignet monorepo, also run:
@@ -0,0 +1,256 @@
1
+ import ts from "typescript";
2
+
3
+ const contractFactoryModule = "@beignet/core/contracts";
4
+
5
+ export type ContractFactoryName = "defineContract" | "defineContractGroup";
6
+
7
+ export type ContractFactoryBindings = {
8
+ localNames: Record<ContractFactoryName, Set<string>>;
9
+ namespaceNames: Set<string>;
10
+ };
11
+
12
+ export type ContractBindingNames = {
13
+ contracts: Set<string>;
14
+ groups: Set<string>;
15
+ };
16
+
17
+ type ContractCall = {
18
+ name: string;
19
+ node: ts.CallExpression;
20
+ };
21
+
22
+ type ContractCallChain = {
23
+ baseIdentifier?: string;
24
+ rootCall?: ContractCall;
25
+ calls: ContractCall[];
26
+ };
27
+
28
+ const contractGroupMethods = new Set([
29
+ "defineContractGroup",
30
+ "deprecated",
31
+ "errors",
32
+ "headers",
33
+ "meta",
34
+ "namespace",
35
+ "prefix",
36
+ "responses",
37
+ ]);
38
+
39
+ const httpContractMethods = new Set([
40
+ "delete",
41
+ "get",
42
+ "head",
43
+ "options",
44
+ "patch",
45
+ "post",
46
+ "put",
47
+ ]);
48
+
49
+ export function contractFactoryBindings(
50
+ sourceFile: ts.SourceFile,
51
+ ): ContractFactoryBindings {
52
+ const bindings: ContractFactoryBindings = {
53
+ localNames: {
54
+ defineContract: new Set(),
55
+ defineContractGroup: new Set(),
56
+ },
57
+ namespaceNames: new Set(),
58
+ };
59
+
60
+ for (const statement of sourceFile.statements) {
61
+ if (
62
+ !ts.isImportDeclaration(statement) ||
63
+ !ts.isStringLiteral(statement.moduleSpecifier) ||
64
+ statement.moduleSpecifier.text !== contractFactoryModule ||
65
+ !statement.importClause ||
66
+ statement.importClause.isTypeOnly
67
+ ) {
68
+ continue;
69
+ }
70
+
71
+ const namedBindings = statement.importClause.namedBindings;
72
+ if (namedBindings && ts.isNamespaceImport(namedBindings)) {
73
+ bindings.namespaceNames.add(namedBindings.name.text);
74
+ continue;
75
+ }
76
+ if (!namedBindings || !ts.isNamedImports(namedBindings)) continue;
77
+
78
+ for (const element of namedBindings.elements) {
79
+ if (element.isTypeOnly) continue;
80
+ const importedName = (element.propertyName ?? element.name).text;
81
+ if (!isContractFactoryName(importedName)) continue;
82
+ bindings.localNames[importedName].add(element.name.text);
83
+ }
84
+ }
85
+
86
+ return bindings;
87
+ }
88
+
89
+ export function isImportedContractFactoryCall(
90
+ call: ts.CallExpression,
91
+ name: ContractFactoryName,
92
+ bindings: ContractFactoryBindings,
93
+ ): boolean {
94
+ const callee = unwrapExpression(call.expression);
95
+ if (ts.isIdentifier(callee)) {
96
+ return bindings.localNames[name].has(callee.text);
97
+ }
98
+ if (!ts.isPropertyAccessExpression(callee) || callee.name.text !== name) {
99
+ return false;
100
+ }
101
+
102
+ const receiver = unwrapExpression(callee.expression);
103
+ return (
104
+ ts.isIdentifier(receiver) && bindings.namespaceNames.has(receiver.text)
105
+ );
106
+ }
107
+
108
+ /** Follow imported factories through local group and contract builder aliases. */
109
+ export function contractBindingNames(
110
+ sourceFile: ts.SourceFile,
111
+ bindings: ContractFactoryBindings,
112
+ ): ContractBindingNames {
113
+ const declarations = sourceFile.statements
114
+ .filter(ts.isVariableStatement)
115
+ .flatMap((statement) => [...statement.declarationList.declarations]);
116
+ const names: ContractBindingNames = {
117
+ contracts: new Set(),
118
+ groups: new Set(),
119
+ };
120
+
121
+ let changed = true;
122
+ while (changed) {
123
+ changed = false;
124
+ for (const declaration of declarations) {
125
+ if (
126
+ !ts.isIdentifier(declaration.name) ||
127
+ !declaration.initializer ||
128
+ names.contracts.has(declaration.name.text) ||
129
+ names.groups.has(declaration.name.text)
130
+ ) {
131
+ continue;
132
+ }
133
+ const chain = contractCallChain(declaration.initializer);
134
+ if (!chain) continue;
135
+
136
+ if (
137
+ contractFactoryCall(chain, "defineContract", bindings) ||
138
+ (chain.baseIdentifier && names.contracts.has(chain.baseIdentifier))
139
+ ) {
140
+ names.contracts.add(declaration.name.text);
141
+ changed = true;
142
+ continue;
143
+ }
144
+
145
+ if (!isContractGroupDerived(chain, bindings, names.groups)) continue;
146
+ if (chain.calls.some((call) => httpContractMethods.has(call.name))) {
147
+ names.contracts.add(declaration.name.text);
148
+ changed = true;
149
+ continue;
150
+ }
151
+
152
+ if (chain.calls.some((call) => !contractGroupMethods.has(call.name))) {
153
+ continue;
154
+ }
155
+ names.groups.add(declaration.name.text);
156
+ changed = true;
157
+ }
158
+ }
159
+
160
+ return names;
161
+ }
162
+
163
+ /** Determine whether an expression declares a direct or group-built contract. */
164
+ export function isImportedContractDeclaration(
165
+ expression: ts.Expression,
166
+ bindings: ContractFactoryBindings,
167
+ names: ContractBindingNames,
168
+ ): boolean {
169
+ const chain = contractCallChain(expression);
170
+ if (!chain) return false;
171
+ if (
172
+ contractFactoryCall(chain, "defineContract", bindings) ||
173
+ (chain.baseIdentifier && names.contracts.has(chain.baseIdentifier))
174
+ ) {
175
+ return true;
176
+ }
177
+ return (
178
+ isContractGroupDerived(chain, bindings, names.groups) &&
179
+ chain.calls.some((call) => httpContractMethods.has(call.name))
180
+ );
181
+ }
182
+
183
+ function isContractGroupDerived(
184
+ chain: ContractCallChain,
185
+ bindings: ContractFactoryBindings,
186
+ groupNames: ReadonlySet<string>,
187
+ ): boolean {
188
+ return Boolean(
189
+ contractFactoryCall(chain, "defineContractGroup", bindings) ||
190
+ (chain.baseIdentifier && groupNames.has(chain.baseIdentifier)),
191
+ );
192
+ }
193
+
194
+ function contractFactoryCall(
195
+ chain: ContractCallChain,
196
+ name: ContractFactoryName,
197
+ bindings: ContractFactoryBindings,
198
+ ): ContractCall | undefined {
199
+ const calls = chain.rootCall ? [chain.rootCall, ...chain.calls] : chain.calls;
200
+ return calls.find((call) =>
201
+ isImportedContractFactoryCall(call.node, name, bindings),
202
+ );
203
+ }
204
+
205
+ function contractCallChain(
206
+ rawExpression: ts.Expression,
207
+ ): ContractCallChain | undefined {
208
+ const calls: ContractCall[] = [];
209
+ let expression = rawExpression;
210
+
211
+ while (true) {
212
+ expression = unwrapExpression(expression);
213
+ if (ts.isPropertyAccessExpression(expression)) {
214
+ expression = expression.expression;
215
+ continue;
216
+ }
217
+ if (ts.isCallExpression(expression)) {
218
+ const callee = unwrapExpression(expression.expression);
219
+ if (ts.isPropertyAccessExpression(callee)) {
220
+ calls.unshift({ name: callee.name.text, node: expression });
221
+ expression = callee.expression;
222
+ continue;
223
+ }
224
+ if (ts.isIdentifier(callee)) {
225
+ return {
226
+ baseIdentifier: callee.text,
227
+ rootCall: { name: callee.text, node: expression },
228
+ calls,
229
+ };
230
+ }
231
+ return undefined;
232
+ }
233
+ if (ts.isIdentifier(expression)) {
234
+ return { baseIdentifier: expression.text, calls };
235
+ }
236
+ return undefined;
237
+ }
238
+ }
239
+
240
+ function isContractFactoryName(value: string): value is ContractFactoryName {
241
+ return value === "defineContract" || value === "defineContractGroup";
242
+ }
243
+
244
+ function unwrapExpression(expression: ts.Expression): ts.Expression {
245
+ let current = expression;
246
+ while (
247
+ ts.isParenthesizedExpression(current) ||
248
+ ts.isAsExpression(current) ||
249
+ ts.isTypeAssertionExpression(current) ||
250
+ ts.isNonNullExpression(current) ||
251
+ ts.isSatisfiesExpression(current)
252
+ ) {
253
+ current = current.expression;
254
+ }
255
+ return current;
256
+ }
@@ -0,0 +1,266 @@
1
+ import path from "node:path";
2
+ import {
3
+ clientDirPath,
4
+ directoryPath,
5
+ normalizePath,
6
+ type ResolvedBeignetConfig,
7
+ } from "../config.js";
8
+
9
+ export type SourceLayer =
10
+ | "app"
11
+ | "app-context"
12
+ | "agent-capability"
13
+ | "client"
14
+ | "component"
15
+ | "contract"
16
+ | "domain"
17
+ | "feature-helper"
18
+ | "feature-port"
19
+ | "infra"
20
+ | "lib"
21
+ | "policy"
22
+ | "port"
23
+ | "route"
24
+ | "schema"
25
+ | "seed"
26
+ | "server"
27
+ | "test"
28
+ | "use-case"
29
+ | "workflow"
30
+ | "unknown";
31
+
32
+ type FeatureLayerConvention = {
33
+ path: string;
34
+ layer: SourceLayer;
35
+ };
36
+
37
+ const canonicalFeatureDirectories = {
38
+ client: "client",
39
+ components: "component",
40
+ domain: "domain",
41
+ jobs: "workflow",
42
+ listeners: "workflow",
43
+ notifications: "workflow",
44
+ schedules: "workflow",
45
+ seeds: "seed",
46
+ tasks: "workflow",
47
+ tests: "test",
48
+ uploads: "workflow",
49
+ "use-cases": "use-case",
50
+ } as const satisfies Record<string, SourceLayer>;
51
+
52
+ const canonicalFeatureRootFiles = {
53
+ "agent-capabilities": "agent-capability",
54
+ contracts: "contract",
55
+ policy: "policy",
56
+ ports: "feature-port",
57
+ routes: "route",
58
+ schemas: "schema",
59
+ } as const satisfies Record<string, SourceLayer>;
60
+
61
+ const featureDirectoryAliases = {
62
+ command: "tasks",
63
+ commands: "tasks",
64
+ event: "domain/events",
65
+ events: "domain/events",
66
+ job: "jobs",
67
+ listener: "listeners",
68
+ notification: "notifications",
69
+ schedule: "schedules",
70
+ scheduled: "schedules",
71
+ task: "tasks",
72
+ upload: "uploads",
73
+ } as const;
74
+
75
+ export function classifySourcePath(
76
+ filePath: string,
77
+ config: ResolvedBeignetConfig,
78
+ ): SourceLayer {
79
+ const normalizedPath = stripKnownExtension(normalizePath(filePath));
80
+ const featuresPath = directoryPath(config.paths.features);
81
+ const portsDir = directoryPath(path.dirname(config.paths.ports));
82
+
83
+ if (
84
+ isTestPath(normalizedPath) ||
85
+ isUnder(normalizedPath, directoryPath(config.paths.tests))
86
+ ) {
87
+ return "test";
88
+ }
89
+ if (normalizedPath === stripKnownExtension(config.paths.appContext)) {
90
+ return "app-context";
91
+ }
92
+
93
+ const featurePath = featureRelativePath(normalizedPath, featuresPath);
94
+ if (featurePath) {
95
+ const layerSegment = featurePath.split("/")[1];
96
+ if (!layerSegment) return "feature-helper";
97
+ const convention = featureDirectoryConvention(layerSegment);
98
+ if (convention) return convention.layer;
99
+ const rootConvention = featureRootFileConvention(layerSegment);
100
+ if (rootConvention) return rootConvention.layer;
101
+ if (layerSegment.endsWith("-contracts")) return "contract";
102
+ return "feature-helper";
103
+ }
104
+
105
+ if (
106
+ directoryPath(config.paths.contracts) !== featuresPath &&
107
+ isUnder(normalizedPath, directoryPath(config.paths.contracts))
108
+ ) {
109
+ return "contract";
110
+ }
111
+ if (
112
+ directoryPath(config.paths.useCases) !== featuresPath &&
113
+ isUnder(normalizedPath, directoryPath(config.paths.useCases))
114
+ ) {
115
+ return "use-case";
116
+ }
117
+ if (
118
+ directoryPath(config.paths.policies) !== featuresPath &&
119
+ isUnder(normalizedPath, directoryPath(config.paths.policies))
120
+ ) {
121
+ return "policy";
122
+ }
123
+
124
+ if (
125
+ isUnder(normalizedPath, directoryPath(path.dirname(config.paths.server)))
126
+ ) {
127
+ return "server";
128
+ }
129
+ if (
130
+ isUnder(
131
+ normalizedPath,
132
+ directoryPath(path.dirname(config.paths.portWiring)),
133
+ )
134
+ ) {
135
+ return "infra";
136
+ }
137
+ if (isUnder(normalizedPath, portsDir)) return "port";
138
+ if (isUnder(normalizedPath, clientDirPath(config))) return "client";
139
+ if (isUnder(normalizedPath, "components")) return "component";
140
+ if (isUnder(normalizedPath, legacySharedDomainPath(config))) return "domain";
141
+ if (isUnder(normalizedPath, "lib")) return "lib";
142
+ if (
143
+ isUnder(normalizedPath, directoryPath(path.dirname(config.paths.routes)))
144
+ ) {
145
+ return "app";
146
+ }
147
+
148
+ return "unknown";
149
+ }
150
+
151
+ export function canonicalFeatureLayerPath(
152
+ filePath: string,
153
+ config: ResolvedBeignetConfig,
154
+ ): string | undefined {
155
+ const featuresPath = directoryPath(config.paths.features);
156
+ const relativePath = featureRelativePath(
157
+ normalizePath(filePath),
158
+ featuresPath,
159
+ );
160
+ if (!relativePath) return undefined;
161
+
162
+ const [feature, segment, ...rest] = relativePath.split("/");
163
+ if (!feature || !segment) return undefined;
164
+
165
+ if (isTestSourceFile(segment, rest) && segment !== "tests") {
166
+ return path.posix.join(
167
+ featuresPath,
168
+ feature,
169
+ "tests",
170
+ ...(rest.length > 0 ? rest : [segment]),
171
+ );
172
+ }
173
+
174
+ if (rest.length > 0) {
175
+ const convention = featureDirectoryConvention(segment);
176
+ if (!convention || convention.path === segment) return undefined;
177
+ return path.posix.join(featuresPath, feature, convention.path, ...rest);
178
+ }
179
+
180
+ const extension = path.posix.extname(segment);
181
+ const stem = extension ? segment.slice(0, -extension.length) : segment;
182
+ const convention = featureRootFileConvention(stem);
183
+ if (!convention || convention.path === stem) return undefined;
184
+ return path.posix.join(
185
+ featuresPath,
186
+ feature,
187
+ `${convention.path}${extension}`,
188
+ );
189
+ }
190
+
191
+ function isTestSourceFile(segment: string, rest: string[]): boolean {
192
+ const fileName = rest.at(-1) ?? segment;
193
+ return /\.(?:test|spec)\.(?:ts|tsx|mts|cts)$/.test(fileName);
194
+ }
195
+
196
+ function featureDirectoryConvention(
197
+ segment: string,
198
+ ): FeatureLayerConvention | undefined {
199
+ const canonical = Object.entries(canonicalFeatureDirectories).find(
200
+ ([candidate]) =>
201
+ normalizeLayerSegment(candidate) === normalizeLayerSegment(segment),
202
+ );
203
+ if (canonical) return { path: canonical[0], layer: canonical[1] };
204
+
205
+ const alias =
206
+ featureDirectoryAliases[
207
+ normalizeLayerSegment(segment) as keyof typeof featureDirectoryAliases
208
+ ];
209
+ if (!alias) return undefined;
210
+ return {
211
+ path: alias,
212
+ layer:
213
+ alias === "domain/events"
214
+ ? "domain"
215
+ : canonicalFeatureDirectories[
216
+ alias as keyof typeof canonicalFeatureDirectories
217
+ ],
218
+ };
219
+ }
220
+
221
+ function featureRootFileConvention(
222
+ segment: string,
223
+ ): FeatureLayerConvention | undefined {
224
+ const canonical = Object.entries(canonicalFeatureRootFiles).find(
225
+ ([candidate]) =>
226
+ normalizeLayerSegment(candidate) === normalizeLayerSegment(segment),
227
+ );
228
+ return canonical ? { path: canonical[0], layer: canonical[1] } : undefined;
229
+ }
230
+
231
+ function normalizeLayerSegment(segment: string): string {
232
+ return segment.replaceAll("-", "").replaceAll("_", "").toLowerCase();
233
+ }
234
+
235
+ function stripKnownExtension(filePath: string): string {
236
+ return filePath.replace(/\.(c|m)?tsx?$/, "");
237
+ }
238
+
239
+ function isTestPath(filePath: string): boolean {
240
+ return (
241
+ filePath.includes("/tests/") ||
242
+ filePath.endsWith(".test") ||
243
+ filePath.endsWith(".spec")
244
+ );
245
+ }
246
+
247
+ function isUnder(filePath: string, directory: string): boolean {
248
+ const normalizedDir = directoryPath(directory);
249
+ if (!normalizedDir || normalizedDir === ".") return false;
250
+ return filePath === normalizedDir || filePath.startsWith(`${normalizedDir}/`);
251
+ }
252
+
253
+ function featureRelativePath(
254
+ filePath: string,
255
+ featuresPath: string,
256
+ ): string | undefined {
257
+ if (!isUnder(filePath, featuresPath)) return undefined;
258
+ return filePath.slice(`${directoryPath(featuresPath)}/`.length);
259
+ }
260
+
261
+ function legacySharedDomainPath(config: ResolvedBeignetConfig): string {
262
+ const featuresDir = directoryPath(config.paths.features);
263
+ const featuresParent = directoryPath(path.dirname(featuresDir));
264
+ if (!featuresParent || featuresParent === ".") return "domain";
265
+ return path.join(featuresParent, "domain");
266
+ }