@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.
- package/CHANGELOG.md +46 -0
- package/README.md +126 -42
- package/dist/analysis/contract-factories.d.ts +17 -0
- package/dist/analysis/contract-factories.d.ts.map +1 -0
- package/dist/analysis/contract-factories.js +176 -0
- package/dist/analysis/contract-factories.js.map +1 -0
- package/dist/analysis/layers.d.ts +5 -0
- package/dist/analysis/layers.d.ts.map +1 -0
- package/dist/analysis/layers.js +172 -0
- package/dist/analysis/layers.js.map +1 -0
- package/dist/analysis/port-wiring.d.ts +23 -0
- package/dist/analysis/port-wiring.d.ts.map +1 -0
- package/dist/analysis/port-wiring.js +379 -0
- package/dist/analysis/port-wiring.js.map +1 -0
- package/dist/analysis/source-index.d.ts +5 -0
- package/dist/analysis/source-index.d.ts.map +1 -1
- package/dist/analysis/source-index.js +171 -42
- package/dist/analysis/source-index.js.map +1 -1
- package/dist/analysis/workspace.d.ts +10 -4
- package/dist/analysis/workspace.d.ts.map +1 -1
- package/dist/analysis/workspace.js +66 -6
- package/dist/analysis/workspace.js.map +1 -1
- package/dist/app-map.d.ts.map +1 -1
- package/dist/app-map.js +311 -92
- package/dist/app-map.js.map +1 -1
- package/dist/check.d.ts +8 -1
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +55 -9
- package/dist/check.js.map +1 -1
- package/dist/db.d.ts +38 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +52 -0
- package/dist/db.js.map +1 -1
- package/dist/explain.js +4 -4
- package/dist/explain.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +73 -6
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +454 -170
- package/dist/inspect.js.map +1 -1
- package/dist/lib.d.ts +2 -2
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +1 -1
- package/dist/lib.js.map +1 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +309 -487
- package/dist/lint.js.map +1 -1
- package/dist/make/inbox.d.ts.map +1 -1
- package/dist/make/inbox.js +12 -5
- package/dist/make/inbox.js.map +1 -1
- package/dist/make/payments.js +3 -3
- package/dist/make/payments.js.map +1 -1
- package/dist/make/tenancy.js +1 -1
- package/dist/make/tenancy.js.map +1 -1
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +144 -8
- package/dist/make.js.map +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +56 -19
- package/dist/mcp.js.map +1 -1
- package/dist/preflight.d.ts +11 -3
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +139 -16
- package/dist/preflight.js.map +1 -1
- package/dist/provider-add.d.ts.map +1 -1
- package/dist/provider-add.js +149 -9
- package/dist/provider-add.js.map +1 -1
- package/dist/provider-audit.d.ts +13 -0
- package/dist/provider-audit.d.ts.map +1 -1
- package/dist/provider-audit.js +122 -8
- package/dist/provider-audit.js.map +1 -1
- package/dist/registry-edits.d.ts +7 -0
- package/dist/registry-edits.d.ts.map +1 -1
- package/dist/registry-edits.js +237 -53
- package/dist/registry-edits.js.map +1 -1
- package/dist/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +29 -7
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/base.d.ts +1 -0
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +20 -6
- package/dist/templates/base.js.map +1 -1
- package/dist/templates/db/index.d.ts +1 -0
- package/dist/templates/db/index.d.ts.map +1 -1
- package/dist/templates/db/index.js.map +1 -1
- package/dist/templates/db/migration-status.d.ts +9 -0
- package/dist/templates/db/migration-status.d.ts.map +1 -0
- package/dist/templates/db/migration-status.js +117 -0
- package/dist/templates/db/migration-status.js.map +1 -0
- package/dist/templates/db/mysql.d.ts.map +1 -1
- package/dist/templates/db/mysql.js +23 -0
- package/dist/templates/db/mysql.js.map +1 -1
- package/dist/templates/db/postgres.d.ts.map +1 -1
- package/dist/templates/db/postgres.js +21 -0
- package/dist/templates/db/postgres.js.map +1 -1
- package/dist/templates/db/sqlite.d.ts.map +1 -1
- package/dist/templates/db/sqlite.js +53 -0
- package/dist/templates/db/sqlite.js.map +1 -1
- package/dist/templates/index.d.ts.map +1 -1
- package/dist/templates/index.js +5 -0
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/server.d.ts.map +1 -1
- package/dist/templates/server.js +2 -0
- package/dist/templates/server.js.map +1 -1
- package/dist/templates/shared.d.ts +1 -0
- package/dist/templates/shared.d.ts.map +1 -1
- package/dist/templates/shared.js +4 -2
- package/dist/templates/shared.js.map +1 -1
- package/package.json +7 -6
- package/skills/app-structure/SKILL.md +56 -11
- package/src/analysis/contract-factories.ts +256 -0
- package/src/analysis/layers.ts +266 -0
- package/src/analysis/port-wiring.ts +506 -0
- package/src/analysis/source-index.ts +225 -57
- package/src/analysis/workspace.ts +115 -6
- package/src/app-map.ts +418 -109
- package/src/check.ts +86 -10
- package/src/db.ts +97 -0
- package/src/explain.ts +4 -4
- package/src/index.ts +108 -7
- package/src/inspect.ts +659 -216
- package/src/lib.ts +9 -1
- package/src/lint.ts +393 -643
- package/src/make/inbox.ts +12 -5
- package/src/make/payments.ts +3 -3
- package/src/make/tenancy.ts +1 -1
- package/src/make.ts +225 -8
- package/src/mcp.ts +73 -21
- package/src/preflight.ts +195 -16
- package/src/provider-add.ts +193 -9
- package/src/provider-audit.ts +210 -18
- package/src/registry-edits.ts +313 -63
- package/src/templates/agents.ts +29 -7
- package/src/templates/base.ts +21 -4
- package/src/templates/db/index.ts +1 -0
- package/src/templates/db/migration-status.ts +126 -0
- package/src/templates/db/mysql.ts +23 -0
- package/src/templates/db/postgres.ts +21 -0
- package/src/templates/db/sqlite.ts +53 -0
- package/src/templates/index.ts +5 -0
- package/src/templates/server.ts +2 -0
- 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.
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
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,
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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. `
|
|
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
|
+
}
|