proteum 2.4.4 → 2.5.1

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 (79) hide show
  1. package/README.md +81 -52
  2. package/agents/project/AGENTS.md +112 -31
  3. package/agents/project/CODING_STYLE.md +2 -2
  4. package/agents/project/app-root/AGENTS.md +1 -3
  5. package/agents/project/client/AGENTS.md +5 -1
  6. package/agents/project/client/pages/AGENTS.md +21 -9
  7. package/agents/project/diagnostics.md +2 -2
  8. package/agents/project/optimizations.md +1 -1
  9. package/agents/project/root/AGENTS.md +105 -22
  10. package/agents/project/server/routes/AGENTS.md +30 -1
  11. package/agents/project/server/services/AGENTS.md +4 -0
  12. package/agents/project/tests/AGENTS.md +1 -1
  13. package/cli/commands/doctor.ts +54 -3
  14. package/cli/commands/runtime.ts +6 -0
  15. package/cli/commands/worktree.ts +116 -0
  16. package/cli/compiler/artifacts/controllers.ts +16 -15
  17. package/cli/compiler/artifacts/discovery.ts +129 -17
  18. package/cli/compiler/artifacts/routing.ts +0 -5
  19. package/cli/compiler/artifacts/services.ts +253 -76
  20. package/cli/compiler/common/controllers.ts +159 -57
  21. package/cli/compiler/common/generatedRouteModules.ts +457 -363
  22. package/cli/mcp/router.ts +47 -3
  23. package/cli/presentation/commands.ts +25 -15
  24. package/cli/runtime/commands.ts +39 -12
  25. package/cli/runtime/worktreeBootstrap.ts +608 -0
  26. package/cli/scaffold/index.ts +28 -18
  27. package/cli/scaffold/templates.ts +44 -33
  28. package/cli/utils/agents.ts +14 -1
  29. package/client/app/index.ts +22 -5
  30. package/client/services/router/index.tsx +23 -3
  31. package/client/services/router/request/api.ts +16 -6
  32. package/common/dev/contractsDoctor.ts +1 -1
  33. package/common/dev/mcpPayloads.ts +8 -1
  34. package/common/env/proteumEnv.ts +14 -2
  35. package/common/router/contracts.ts +1 -1
  36. package/common/router/definitions.ts +177 -0
  37. package/common/router/index.ts +23 -12
  38. package/common/router/pageData.ts +5 -5
  39. package/common/router/register.ts +2 -2
  40. package/common/router/request/api.ts +12 -2
  41. package/docs/agent-routing.md +5 -2
  42. package/docs/diagnostics.md +2 -0
  43. package/docs/mcp.md +6 -3
  44. package/docs/migration-2.5.md +226 -0
  45. package/eslint.js +89 -42
  46. package/package.json +1 -1
  47. package/server/app/commands.ts +5 -1
  48. package/server/app/container/console/index.ts +1 -1
  49. package/server/app/controller/index.ts +98 -40
  50. package/server/app/index.ts +120 -3
  51. package/server/app/service/index.ts +5 -1
  52. package/server/index.ts +6 -2
  53. package/server/services/router/index.ts +50 -41
  54. package/server/services/router/response/index.ts +2 -2
  55. package/tests/agents-utils.test.cjs +14 -1
  56. package/tests/cli-mcp-command.test.cjs +84 -0
  57. package/tests/client-app-error-handling.test.cjs +100 -0
  58. package/tests/definition-contracts.test.cjs +453 -0
  59. package/tests/dev-transpile-watch.test.cjs +37 -31
  60. package/tests/eslint-rules.test.cjs +185 -8
  61. package/tests/mcp.test.cjs +90 -0
  62. package/tests/scaffold-templates.test.cjs +18 -0
  63. package/tests/server-app-report-error.test.cjs +135 -0
  64. package/tests/worktree-bootstrap.test.cjs +206 -0
  65. package/types/aliases.d.ts +0 -5
  66. package/types/controller-input.test.ts +23 -17
  67. package/types/controller-request-context.test.ts +10 -11
  68. package/cli/commands/migrate.ts +0 -51
  69. package/cli/migrate/pageContract.ts +0 -516
  70. package/docs/migrate-from-2.1.3.md +0 -396
  71. package/scripts/cleanup-generated-controllers.ts +0 -62
  72. package/scripts/fix-reference-app-typing.ts +0 -490
  73. package/scripts/format-router-registrations.ts +0 -119
  74. package/scripts/migrate-explicit-controllers-and-request.ts +0 -423
  75. package/scripts/refactor-client-app-imports.ts +0 -244
  76. package/scripts/refactor-client-pages.ts +0 -587
  77. package/scripts/refactor-server-controllers.ts +0 -471
  78. package/scripts/refactor-server-runtime-aliases.ts +0 -360
  79. package/scripts/restore-client-app-import-files.ts +0 -41
@@ -46,7 +46,7 @@ const formatRouteSource = (route: TAnyRoute) => {
46
46
  };
47
47
 
48
48
  export const getRouteOptionKey = (key: string) => {
49
- if (reservedRouteOptionKeysSet.has(key)) throw new Error(`"${key}" is a reserved Router.page option key.`);
49
+ if (reservedRouteOptionKeysSet.has(key)) throw new Error(`"${key}" is a reserved route option key.`);
50
50
 
51
51
  return routeOptionKeysSet.has(key) ? (key as keyof TRouteOptions) : null;
52
52
  };
@@ -54,8 +54,8 @@ export const getRouteOptionKey = (key: string) => {
54
54
  export const validatePageDataResult = (route: TAnyRoute, result: unknown) => {
55
55
  if (!result || typeof result !== 'object' || Array.isArray(result)) {
56
56
  throw new Error(
57
- `Router.page data for ${formatRouteTarget(route)} in ${formatRouteSource(route)} must return an object. ` +
58
- `If the page has no data loader, pass null as the third argument.`,
57
+ `definePageRoute data for ${formatRouteTarget(route)} in ${formatRouteSource(route)} must return an object. ` +
58
+ `If the page has no data loader, set data to null.`,
59
59
  );
60
60
  }
61
61
 
@@ -63,8 +63,8 @@ export const validatePageDataResult = (route: TAnyRoute, result: unknown) => {
63
63
  if (!reservedPageDataKeys.has(key)) continue;
64
64
 
65
65
  throw new Error(
66
- `Router.page data for ${formatRouteTarget(route)} in ${formatRouteSource(route)} cannot return reserved key "${key}". ` +
67
- `Move route behavior into the explicit Router.page(path, options, data, render) options argument.`,
66
+ `definePageRoute data for ${formatRouteTarget(route)} in ${formatRouteSource(route)} cannot return reserved key "${key}". ` +
67
+ `Move route behavior into definePageRoute({ path, options, data, render }).options.`,
68
68
  );
69
69
  }
70
70
 
@@ -21,12 +21,12 @@ export const getRegisterPageArgs = (...args: TRegisterPageArgs<any, TRouteOption
21
21
  const [path, options, data, renderer] = args;
22
22
 
23
23
  if (!options || typeof options !== 'object' || Array.isArray(options)) {
24
- throw new Error(`Router.page(${JSON.stringify(path)}) requires an explicit options object as its second argument.`);
24
+ throw new Error(`definePageRoute(${JSON.stringify(path)}) requires an explicit options object.`);
25
25
  }
26
26
 
27
27
  if (data !== null && typeof data !== 'function') {
28
28
  throw new Error(
29
- `Router.page(${JSON.stringify(path)}) requires a data function or null as its third argument.`,
29
+ `definePageRoute(${JSON.stringify(path)}) requires a data function or null.`,
30
30
  );
31
31
  }
32
32
 
@@ -43,9 +43,19 @@ export type TApiFetchOptions = {
43
43
 
44
44
  export type TPostData = TPostDataWithFile;
45
45
 
46
- export type TPostDataWithFile = { [key: string]: PrimitiveValue };
46
+ export type TPostDataJsonValue =
47
+ | PrimitiveValue
48
+ | null
49
+ | undefined
50
+ | Date
51
+ | TPostDataJsonValue[]
52
+ | { [key: string]: TPostDataJsonValue };
47
53
 
48
- export type TPostDataWithoutFile = { [key: string]: PrimitiveValue };
54
+ export type TPostDataValue = TPostDataJsonValue | Blob | FileList;
55
+
56
+ export type TPostDataWithFile = { [key: string]: TPostDataValue };
57
+
58
+ export type TPostDataWithoutFile = { [key: string]: TPostDataJsonValue };
49
59
 
50
60
  export type TDataReturnedByFetchers<TProvidedData extends TFetcherList = {}> = {
51
61
  [Property in keyof TProvidedData]: ThenArg<TProvidedData[Property]>;
@@ -68,7 +68,7 @@ proteum mcp
68
68
 
69
69
  The machine router discovers live `proteum dev` sessions and offline Proteum app roots under a cwd. `proteum dev` ensures one managed machine MCP daemon is running; terminal `proteum mcp` starts or reuses that daemon and prints a compact central MCP banner with the HTTP client URL, while MCP clients can use stdio. Agents should call MCP `workflow_start` with `cwd` or a known `projectId`, use `project_resolve { cwd }` when routing is ambiguous or offline, and pass the returned live `projectId` to every follow-up app-bound MCP tool. Offline candidates include port-inspected next actions, so agents should follow those instead of guessing the manifest default port. The router forwards to the selected dev-hosted `/__proteum/mcp` endpoint and strips routing fields before the app sees the call.
70
70
 
71
- If machine MCP routing returns offline candidates, choose the intended app root and follow that candidate's next action from the app root, not from the monorepo wrapper. If machine MCP routing fails, run `proteum mcp status` and `proteum runtime status` from the intended app root; if no live session exists, use the exact Start Dev next action from runtime status so occupied router/HMR ports are avoided. If the same app already responds on the configured port without live tracking, use or repair that runtime instead of starting another server. Do not `curl` normal page routes to identify which app owns a port; use runtime status or Proteum dev-only endpoints. If a live session exists but runtime/MCP is unreachable, stop the listed session file first, then start dev again. Do not run diagnose, trace, or perf reads while runtime health is unreachable. Do not start a second dev server in the same worktree, and do not start a second managed MCP daemon. Then retry MCP `workflow_start`.
71
+ If machine MCP routing returns offline candidates, choose the intended app root and follow that candidate's next action from the app root, not from the monorepo wrapper. In `/.codex/worktrees/`, if `workflow_start` returns a worktree bootstrap block, run `npx proteum worktree init --source <source-app-root>` or the returned `--refresh` command before any runtime read. If machine MCP routing fails, run `proteum mcp status` and `proteum runtime status` from the intended app root; if no live session exists, use the exact Start Dev next action from runtime status so occupied router/HMR ports are avoided. If the same app already responds on the configured port without live tracking, use or repair that runtime instead of starting another server. Do not `curl` normal page routes to identify which app owns a port; use runtime status or Proteum dev-only endpoints. If a live session exists but runtime/MCP is unreachable, stop the listed session file first, then start dev again. Do not run diagnose, trace, or perf reads while runtime health is unreachable. Do not start a second dev server in the same worktree, and do not start a second managed MCP daemon. Then retry MCP `workflow_start`.
72
72
 
73
73
  Prefer CLI over MCP when the result must be reproducible as a shell command, part of verification, or copied into CI/debug instructions.
74
74
 
@@ -93,9 +93,12 @@ The router standard is trigger -> canonical instruction file, not trigger -> cop
93
93
 
94
94
  Standard triggered reads:
95
95
 
96
+ - Worktree Preflight (`cwd` inside `/.codex/worktrees/`, newly created Proteum worktree, or before editing in a Codex worktree): root contract fallback, then `npx proteum worktree init --source <source-app-root>` or the returned `--refresh` command, `npx proteum runtime status`, and tracked `npx proteum dev` for runtime-visible work.
96
97
  - Git lifecycle (`commit`, `and commit`, `stage`, `push`, `PR`, pull request): root contract fallback.
97
- - Before finishing production code changes: root contract fallback, `CODING_STYLE.md`, and touched area `AGENTS.md`.
98
+ - Before git writes after a bug fix, behavior change, decision change, or docs-relevant production change: `DOCUMENTATION.md`.
99
+ - Before finishing production code changes: root contract fallback, `DOCUMENTATION.md`, `CODING_STYLE.md`, and touched area `AGENTS.md`.
98
100
  - Runtime-visible, request-time, router, SSR, browser, or controller behavior: root contract fallback plus `diagnostics.md`.
101
+ - Bug fixes, regressions, incidents, broken public routes, auth/OAuth failures, integration failures, or production behavior fixes: `DOCUMENTATION.md`.
99
102
  - Non-trivial feature, product, business-rule, UX, copy, or docs changes: `DOCUMENTATION.md`.
100
103
  - Implementation edits: `CODING_STYLE.md` plus the matching area file from the routing table.
101
104
 
@@ -112,6 +112,8 @@ Default compact command output follows this shape:
112
112
 
113
113
  `proteum runtime status` emits the current app manifest summary, tracked dev sessions, selected live session, MCP URL, health status, configured router/HMR port inspection, and a suggested next command. Use it before starting another dev server, and use its Start Dev command instead of probing page bodies when the default port is occupied. If it reports that the same app already responds on the configured port without a live tracked session, use or repair that runtime instead of starting a second server.
114
114
 
115
+ Inside `/.codex/worktrees/`, `proteum dev`, `proteum refresh`, `proteum runtime status`, `proteum verify`, and MCP `workflow_start` require a fresh `.proteum/worktree-bootstrap.json`. If the marker is missing, run `npx proteum worktree init --source <source-app-root>`. If hashes, `.env`, `.proteum/manifest.json`, `node_modules`, or the Proteum version are stale, run the returned `npx proteum worktree init --source <source-app-root> --refresh` command. `PROTEUM_ALLOW_UNBOOTSTRAPPED_WORKTREE=1` bypasses the block but remains visible in runtime status, doctor diagnostics, and MCP output.
116
+
115
117
  During `proteum dev`, `/__proteum/mcp` exposes compact `workflow_start`, `runtime_status`, `orient`, `instructions_resolve`, `route_candidates`, `explain_summary`, `doctor`, `diagnose`, `trace_*`, `perf_*`, and `logs_tail` tools without spawning CLI commands for each repeated read. `proteum dev` also ensures one managed machine `proteum mcp` daemon is running. Through the machine router, call `workflow_start` with `cwd` or a known `projectId`; if routing is ambiguous or returns offline app candidates, use `project_resolve { cwd }`, follow the selected app root's port-inspected next action when needed, then pass the selected live `projectId` to follow-up app-bound tools.
116
118
 
117
119
  MCP tool/resource output follows compact single-line `proteum-mcp-v1` JSON:
package/docs/mcp.md CHANGED
@@ -49,6 +49,8 @@ Example tool calls:
49
49
 
50
50
  `workflow_start` is the only app-bound bootstrap tool that may resolve from `cwd` when `projectId` is not known. It may return offline app candidates when no matching dev server is running yet. Other app-bound tools require a live `projectId`; if they omit it, the router returns a compact error that tells the agent to call `projects_list` or `project_resolve`. There is no single-project fallback, because wrong-project reads are worse than an explicit routing retry.
51
51
 
52
+ When the selected app root is inside `/.codex/worktrees/`, `workflow_start` first checks `.proteum/worktree-bootstrap.json`. If the marker is missing or stale, it returns `ok: false` with a single next action such as `npx proteum worktree init --source <source-app-root>` or the same command with `--refresh`. The router does not forward to the app MCP endpoint until bootstrap is complete, unless `PROTEUM_ALLOW_UNBOOTSTRAPPED_WORKTREE=1` is set; bypasses remain visible in MCP, `runtime status`, and `doctor`.
53
+
52
54
  ## Dev Runtime Endpoint
53
55
 
54
56
  During `proteum dev`, the app exposes the same app-level MCP contract through the official streamable HTTP transport:
@@ -76,9 +78,10 @@ If machine MCP routing fails:
76
78
 
77
79
  1. Run `proteum mcp status`.
78
80
  2. Run `proteum runtime status` from the intended app root. If you are in a monorepo wrapper, use the returned app candidates and exact next action instead of starting dev from the wrapper.
79
- 3. If no live app session exists, use the exact Start Dev next action returned by runtime status. It checks the configured router/HMR ports and suggests an alternate free port when the manifest default is occupied.
80
- 4. If a live session exists but runtime/MCP is unreachable, stop the listed session file with `proteum dev stop --session-file <path>`, then start dev again.
81
- 5. Retry MCP `workflow_start` and use the returned `projectId`.
81
+ 3. If the app root is inside `/.codex/worktrees/` and runtime status or workflow start reports missing/stale bootstrap, run `proteum worktree init --source <source-app-root>` or the returned `--refresh` command first.
82
+ 4. If no live app session exists, use the exact Start Dev next action returned by runtime status. It checks the configured router/HMR ports and suggests an alternate free port when the manifest default is occupied.
83
+ 5. If a live session exists but runtime/MCP is unreachable, stop the listed session file with `proteum dev stop --session-file <path>`, then start dev again.
84
+ 6. Retry MCP `workflow_start` and use the returned `projectId`.
82
85
 
83
86
  Offline `project_resolve` and `workflow_start` candidates also inspect configured router/HMR ports before returning `nextAction`. If the configured port already serves the same app but no live machine project is registered, the next action is runtime tracking repair, not starting a second dev server.
84
87
 
@@ -0,0 +1,226 @@
1
+ # Migrating To Proteum 2.5
2
+
3
+ Proteum 2.5 is a breaking cleanup release. It removes contextual app/router magic from user source and makes routes, controllers, pages, services, and app bootstrap machine-readable through explicit definition objects.
4
+
5
+ ## What Changed
6
+
7
+ - App roots default-export `defineApplication({ services, router, models, commands })`.
8
+ - Page files default-export `definePageRoute({ path, options, data, render })` or `defineErrorRoute({ code, options, render })`.
9
+ - Manual HTTP route files default-export `defineServerRoute({ method, path, options, handler })` or `defineServerRoutes(...)`.
10
+ - Raw Express handlers are wrapped with `expressHandler(...)`.
11
+ - Controller files default-export `defineController({ path, actions })`; actions use `defineAction({ input, handler })`.
12
+ - Runtime app, service, router, request, response, auth, and router-plugin access comes from typed callback context.
13
+ - `@app` imports, top-level `Router.page(...)`, top-level server `Router.*(...)`, controller classes, and `this.input(...)` are no longer supported.
14
+
15
+ ## 1. Install The Published Package
16
+
17
+ Update every Proteum app package in the repo to `proteum@^2.5.0`, then reinstall from npm.
18
+
19
+ ```bash
20
+ npm install
21
+ npm ls proteum
22
+ node -p "require('./node_modules/proteum/package.json').version + ' ' + require.resolve('proteum/package.json')"
23
+ ```
24
+
25
+ The resolved path must point inside the app repo `node_modules/proteum`, not to a local framework checkout. If a project was using `npm link`, the reinstall should replace the symlink.
26
+
27
+ ## 2. Refresh Agent Instructions
28
+
29
+ Regenerate project instructions so LLMs receive the explicit 2.5 contracts.
30
+
31
+ ```bash
32
+ npx proteum configure agents
33
+ ```
34
+
35
+ Generated instructions should mention `defineApplication`, `definePageRoute`, `defineServerRoute`, `defineController`, and the ban on `@app` imports in route, page, and controller files.
36
+
37
+ ## 3. Migrate `server/index.ts`
38
+
39
+ Move from an `Application` subclass or service-returned `Router` to an explicit app definition. Keep `server/index.ts` as the canonical type root for services, router plugins, models, and request context.
40
+
41
+ ```ts
42
+ import { defineApplication, type Application } from '@server/app';
43
+ import Router from '@server/services/router';
44
+ import SchemaRouter from '@server/services/schema/router';
45
+ import BillingService from '@/server/services/Billing';
46
+
47
+ import * as appConfig from '@/server/config/app';
48
+
49
+ type ProjectServices = {
50
+ Billing: BillingService;
51
+ };
52
+
53
+ type ProjectRouterPlugins = {
54
+ schema: SchemaRouter;
55
+ };
56
+
57
+ export type ProjectRouter = Router<ProjectApp, ProjectRouterPlugins>;
58
+ export interface ProjectApp extends Application, ProjectServices {
59
+ Router: ProjectRouter;
60
+ }
61
+
62
+ const createProjectRouter = (app: ProjectApp): ProjectRouter =>
63
+ new Router<ProjectApp, ProjectRouterPlugins>(
64
+ app,
65
+ {
66
+ ...appConfig.routerBaseConfig,
67
+ plugins: {
68
+ schema: new SchemaRouter({}, app),
69
+ },
70
+ },
71
+ app,
72
+ );
73
+
74
+ const ProjectApplication = defineApplication<ProjectServices, ProjectRouter>({
75
+ services: (app) => ({
76
+ Billing: new BillingService(app, {}, app),
77
+ }),
78
+ router: createProjectRouter,
79
+ });
80
+
81
+ export default ProjectApplication;
82
+ ```
83
+
84
+ ## 4. Migrate Pages
85
+
86
+ Replace legacy page registration with a default-exported page definition.
87
+
88
+ ```tsx
89
+ import { definePageRoute } from '@common/router/definitions';
90
+
91
+ export default definePageRoute({
92
+ path: '/dashboard',
93
+ options: { auth: true },
94
+ data: ({ AccountController }) => ({
95
+ account: AccountController.accountPage(),
96
+ }),
97
+ render: ({ account }) => <Dashboard account={account} />,
98
+ });
99
+ ```
100
+
101
+ Rules:
102
+
103
+ - `path`, `options`, and error `code` must be static and compiler-readable.
104
+ - Route behavior belongs in `options`, not in data.
105
+ - Use `data: null` when no SSR data is needed.
106
+ - Runtime references are allowed only inside `data` and `render`.
107
+
108
+ ## 5. Migrate Manual Server Routes
109
+
110
+ Replace top-level `Router.get(...)`, `Router.post(...)`, `Router.express(...)`, and similar calls with definition exports.
111
+
112
+ ```ts
113
+ import { defineServerRoute, defineServerRoutes, expressHandler } from '@common/router/definitions';
114
+ import type { ProjectApp } from '@/server/index';
115
+
116
+ export default defineServerRoutes((app: ProjectApp) => [
117
+ defineServerRoute({
118
+ method: 'GET',
119
+ path: '/health',
120
+ options: {},
121
+ handler: ({ response }) => response.json({ ok: true }),
122
+ }),
123
+ defineServerRoute({
124
+ method: 'POST',
125
+ path: '/webhook',
126
+ options: {},
127
+ handler: expressHandler((request, response) => {
128
+ app.Billing.recordWebhook(request.body);
129
+ response.status(204).send('');
130
+ }),
131
+ }),
132
+ ]);
133
+ ```
134
+
135
+ Use `defineServerRoutes((app) => [...])` only when the route definitions need app services at registration time. Otherwise export one `defineServerRoute(...)`.
136
+
137
+ ## 6. Migrate Controllers
138
+
139
+ Replace controller classes and `this.input(schema)` with explicit actions.
140
+
141
+ ```ts
142
+ import { defineAction, defineController, schema } from '@server/app/controller';
143
+
144
+ export default defineController({
145
+ path: 'Billing',
146
+ actions: {
147
+ read: defineAction({
148
+ input: schema.object({ accountId: schema.string() }),
149
+ handler: ({ input, services }) => services.Billing.read(input.accountId),
150
+ }),
151
+ },
152
+ });
153
+ ```
154
+
155
+ Rules:
156
+
157
+ - `input` is parsed before the handler runs.
158
+ - Read parsed input from `context.input`.
159
+ - Read request state from `request`, `response`, `api`, `auth`, and router-plugin context.
160
+ - Call business logic through `services`, `models`, or `app`.
161
+
162
+ ## 7. Remove Legacy Magic
163
+
164
+ Search user source for old contracts and remove every match.
165
+
166
+ ```bash
167
+ rg -n "from ['\"]@app['\"]|Router\\.(page|error|get|post|put|patch|delete|express)\\(|this\\.input\\(" client server common commands
168
+ ```
169
+
170
+ Expected result: no user-source matches.
171
+
172
+ Allowed replacements:
173
+
174
+ - `ctx.app`, `ctx.services`, `ctx.Router`, `ctx.request`, `ctx.response`, `ctx.auth`, and custom router-plugin context inside handlers.
175
+ - `this.app`, `this.services`, and `this.models` inside typed services.
176
+ - `defineServerRoutes((app) => [...])` when server route definitions need app services.
177
+
178
+ ## 8. Standardize Caught Error Handling
179
+
180
+ Every caught error must end at the same framework error surface. Local UI feedback or protocol responses can still happen, but they are not the terminal error handling step by themselves.
181
+
182
+ Server rules:
183
+
184
+ - Use `throw error` when the request/router/controller should fail and let Proteum render the HTTP error response.
185
+ - Use `await app.reportError(error, request)` when a Proteum request is available, or `await app.reportError(error)` for detached/custom Express paths, catch-and-continue server work, and jobs that intentionally keep running.
186
+ - Do not use raw `app.runHook('error', error, request)` in app code. `app.reportError(...)` keeps the `error` versus `error.<code>` routing centralized.
187
+
188
+ Client rules:
189
+
190
+ - Use `throw error` when the action should fail and reach the app-level unhandled rejection path.
191
+ - Use `useContext().app.handleError(error)` or `context.app.handleError(error)` when the UI catches and continues.
192
+ - `handleError` accepts unknown caught values and returns a displayable message. Prefer `setError(context.app.handleError(error, 'Unable to finish this action.'))` over local `instanceof Error` filtering.
193
+ - If an app overrides `handleError`, update it to `handleError(error: unknown, fallbackMessage?: string): string` and return the display message.
194
+ - Toasts, form errors, or `setError(...)` are local feedback only. Route the original caught value through `app.handleError(error)` or `throw error`.
195
+
196
+ Do not treat `console.error(error)`, `console.warn(error)`, or any other `console.*(error)` call as error handling. Console calls can be temporary diagnostics, but they must not be the last stop for a caught error.
197
+
198
+ ## 9. Refresh Generated Artifacts
199
+
200
+ Do not edit `.proteum/**` manually. Regenerate it from source.
201
+
202
+ ```bash
203
+ npx proteum refresh
204
+ npx proteum typecheck
205
+ ```
206
+
207
+ If connected local projects are used through `file:` sources, start or validate producer apps before validating the consumer.
208
+
209
+ ## 10. Validate Runtime Behavior
210
+
211
+ Run the smallest trustworthy checks first, then broaden when the touched surface requires it.
212
+
213
+ ```bash
214
+ npx proteum diagnose /
215
+ npx proteum build --prod
216
+ npx proteum e2e
217
+ ```
218
+
219
+ For protected flows, prefer Proteum session helpers over automating login unless login is the feature under test.
220
+
221
+ ## Common Fixes
222
+
223
+ - Production route-generation errors where top-level `Router.express(...)` was lifted outside registration are fixed by moving the route into `defineServerRoute({ handler: expressHandler(...) })`.
224
+ - `@app` import errors are fixed by moving runtime access into `data`, `render`, route handlers, controller action handlers, or typed services.
225
+ - Missing `this.app.Router` typings are fixed by exporting the concrete app and router types from `server/index.ts`.
226
+ - Static metadata errors are fixed by moving runtime-dependent values out of `path`, `method`, `options`, and error `code`.
package/eslint.js CHANGED
@@ -121,75 +121,89 @@ const getCalleePropertyName = (callee) => {
121
121
  return null;
122
122
  };
123
123
 
124
- const preservingCallNames = new Set([
125
- 'captureError',
126
- 'captureException',
127
- 'consoleError',
128
- 'handleError',
129
- 'logError',
130
- 'onError',
131
- 'reject',
132
- 'reportError',
133
- 'setError',
134
- 'setErrorMessage',
135
- ]);
136
-
137
- const preservingMemberNames = new Set(['captureException', 'error', 'handleError', 'reject', 'warn']);
138
-
139
- const isPreservingCall = (callExpression, names) => {
140
- const propertyName = getCalleePropertyName(callExpression.callee);
141
- if (!propertyName) return false;
142
-
143
- const isKnownPreserver =
144
- preservingCallNames.has(propertyName) ||
145
- (callExpression.callee.type === 'MemberExpression' && preservingMemberNames.has(propertyName));
146
-
147
- return isKnownPreserver && nodeReferencesName(callExpression, names);
124
+ const getErrorHandlingSide = (filename) => {
125
+ const normalized = filename.replace(/\\/g, '/');
126
+ if (/(^|\/)client\//.test(normalized)) return 'client';
127
+ if (/(^|\/)(server|commands)\//.test(normalized)) return 'server';
128
+
129
+ return 'shared';
130
+ };
131
+
132
+ const getMemberPropertyName = (node) => {
133
+ if (node?.type !== 'MemberExpression') return null;
134
+ if (node.property.type === 'Identifier') return node.property.name;
135
+ if (node.property.type === 'Literal') return String(node.property.value);
136
+
137
+ return null;
138
+ };
139
+
140
+ const isConsoleMember = (node) =>
141
+ node?.type === 'MemberExpression' && node.object?.type === 'Identifier' && node.object.name === 'console';
142
+
143
+ const isAppReceiver = (node) => {
144
+ if (!node) return false;
145
+ if (node.type === 'Identifier' && node.name === 'app') return true;
146
+ if (node.type === 'MemberExpression' && getMemberPropertyName(node) === 'app') return true;
147
+
148
+ return false;
148
149
  };
149
150
 
150
- const handlerPreservesCaughtError = (node, names) => {
151
+ const isClientErrorHandlerCall = (callExpression) =>
152
+ callExpression.callee.type === 'MemberExpression' &&
153
+ getMemberPropertyName(callExpression.callee) === 'handleError' &&
154
+ isAppReceiver(callExpression.callee.object);
155
+
156
+ const isServerErrorReporterCall = (callExpression) =>
157
+ callExpression.callee.type === 'MemberExpression' &&
158
+ getMemberPropertyName(callExpression.callee) === 'reportError' &&
159
+ isAppReceiver(callExpression.callee.object);
160
+
161
+ const isPromiseRejectCall = (callExpression) => getCalleePropertyName(callExpression.callee) === 'reject';
162
+
163
+ const isPreservingCall = (callExpression, names, side) => {
164
+ if (!nodeReferencesName(callExpression, names)) return false;
165
+ if (isConsoleMember(callExpression.callee)) return false;
166
+ if (isPromiseRejectCall(callExpression)) return true;
167
+ if (side === 'client') return isClientErrorHandlerCall(callExpression);
168
+ if (side === 'server') return isServerErrorReporterCall(callExpression);
169
+
170
+ return isClientErrorHandlerCall(callExpression) || isServerErrorReporterCall(callExpression);
171
+ };
172
+
173
+ const handlerPreservesCaughtError = (node, names, side) => {
151
174
  let preserves = false;
152
175
 
153
176
  traverseNode(node, (child) => {
154
177
  if (child.type === 'ThrowStatement' && nodeReferencesName(child.argument, names)) preserves = true;
155
- if (child.type === 'CallExpression' && isPreservingCall(child, names)) preserves = true;
178
+ if (child.type === 'CallExpression' && isPreservingCall(child, names, side)) preserves = true;
156
179
  });
157
180
 
158
181
  return preserves;
159
182
  };
160
183
 
161
- const directPromiseCatchHandlers = new Set([
162
- 'captureError',
163
- 'captureException',
164
- 'consoleError',
165
- 'handleError',
166
- 'logError',
167
- 'reportError',
168
- ]);
169
-
170
184
  const isDirectPromiseCatchHandler = (node) => {
171
185
  const name = getCalleePropertyName(node);
172
- if (name && directPromiseCatchHandlers.has(name)) return true;
173
- return node?.type === 'MemberExpression' && node.object?.type === 'Identifier' && node.object.name === 'console';
186
+ return name === 'reject';
174
187
  };
175
188
 
176
189
  const createSwallowedErrorRule = () => ({
177
190
  meta: {
178
191
  type: 'problem',
179
192
  docs: {
180
- description: 'Require caught errors to be preserved, reported, rethrown, or surfaced with original detail.',
193
+ description: 'Require caught errors to reach the standard app error path or be rethrown.',
181
194
  },
182
195
  messages: {
183
196
  missingParam:
184
- 'Caught errors must be bound and preserved. Use `catch (error)` and rethrow, report, route, or surface original details.',
197
+ 'Caught errors must be bound and routed through the standard error path. Use `catch (error)` and rethrow, call app.reportError on the server, or call app.handleError on the client.',
185
198
  unusedParam:
186
- 'Caught error `{{name}}` is discarded. Rethrow it, report it, route it to app error handling, or surface its original details.',
199
+ 'Caught error `{{name}}` is discarded. Rethrow it, call app.reportError on the server, or call app.handleError on the client.',
187
200
  unpreserved:
188
- 'Caught error `{{name}}` is used but not preserved. Rethrow it, report it, route it, or surface original error details.',
201
+ 'Caught error `{{name}}` is used but not routed through the standard error path. Rethrow it, call app.reportError on the server, or call app.handleError on the client.',
189
202
  },
190
203
  schema: [],
191
204
  },
192
205
  create(context) {
206
+ const side = getErrorHandlingSide(context.filename || context.getFilename?.() || '');
193
207
  const reportHandler = (node, params, body) => {
194
208
  const names = params.flatMap((param) => collectPatternNames(param));
195
209
  if (names.length === 0) {
@@ -203,7 +217,7 @@ const createSwallowedErrorRule = () => ({
203
217
  return;
204
218
  }
205
219
 
206
- if (!handlerPreservesCaughtError(body, collectDerivedErrorNames(body, names))) {
220
+ if (!handlerPreservesCaughtError(body, collectDerivedErrorNames(body, names), side)) {
207
221
  context.report({ node, messageId: 'unpreserved', data: { name: referencedName } });
208
222
  }
209
223
  };
@@ -228,6 +242,37 @@ const createSwallowedErrorRule = () => ({
228
242
  },
229
243
  });
230
244
 
245
+ const createNoAppImportRule = () => ({
246
+ meta: {
247
+ type: 'problem',
248
+ docs: {
249
+ description: 'Disallow Proteum contextual @app imports in user code.',
250
+ },
251
+ messages: {
252
+ noAppImport:
253
+ '`@app` is not a real runtime module. Receive app services through typed route/controller callback context instead.',
254
+ },
255
+ schema: [],
256
+ },
257
+ create(context) {
258
+ return {
259
+ ImportDeclaration(node) {
260
+ if (node.source?.value === '@app') context.report({ node, messageId: 'noAppImport' });
261
+ },
262
+ CallExpression(node) {
263
+ if (
264
+ node.callee?.type === 'Identifier' &&
265
+ node.callee.name === 'require' &&
266
+ node.arguments?.[0]?.type === 'Literal' &&
267
+ node.arguments[0].value === '@app'
268
+ ) {
269
+ context.report({ node, messageId: 'noAppImport' });
270
+ }
271
+ },
272
+ };
273
+ },
274
+ });
275
+
231
276
  const createProteumEslintConfig = ({ ignores = [] } = {}) => [
232
277
  {
233
278
  ignores: [...defaultIgnores, ...ignores],
@@ -253,6 +298,7 @@ const createProteumEslintConfig = ({ ignores = [] } = {}) => [
253
298
  '@typescript-eslint': tseslint.plugin,
254
299
  proteum: {
255
300
  rules: {
301
+ 'no-app-import': createNoAppImportRule(),
256
302
  'no-swallowed-caught-error': createSwallowedErrorRule(),
257
303
  },
258
304
  },
@@ -262,6 +308,7 @@ const createProteumEslintConfig = ({ ignores = [] } = {}) => [
262
308
  },
263
309
  rules: {
264
310
  '@typescript-eslint/no-explicit-any': 'error',
311
+ 'proteum/no-app-import': 'error',
265
312
  'proteum/no-swallowed-caught-error': 'error',
266
313
  'no-restricted-syntax': [
267
314
  'error',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "proteum",
3
3
  "description": "LLM-first Opinionated Typescript Framework for web applications.",
4
- "version": "2.4.4",
4
+ "version": "2.5.1",
5
5
  "author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
6
6
  "repository": "git://github.com/gaetanlegac/proteum.git",
7
7
  "license": "MIT",
@@ -44,7 +44,11 @@ export abstract class Commands<TApplication extends TCommandApplication = TComma
44
44
  }
45
45
 
46
46
  public get models(): object {
47
- const models = this.app.models?.client ?? this.app.Models?.client;
47
+ const appRecord = this.app as unknown as Record<string, unknown>;
48
+ const explicitModels = Object.prototype.hasOwnProperty.call(appRecord, 'models')
49
+ ? (appRecord.models as { client?: object } | undefined)
50
+ : undefined;
51
+ const models = explicitModels?.client ?? this.app.Models?.client;
48
52
 
49
53
  if (!models)
50
54
  throw new Error(`${this.constructor.name} tried to access models but no Models service is registered.`);
@@ -539,7 +539,7 @@ Logs: ${
539
539
  public jsonToHTML(json: unknown): string {
540
540
  if (!json) return 'No data';
541
541
 
542
- const coloredJson = highlight(stringify(json, null, 4), { language: 'json', ignoreIllegals: true });
542
+ const coloredJson = highlight(stringify(json, undefined, 4), { language: 'json', ignoreIllegals: true });
543
543
 
544
544
  const html = ansi2Html.toHtml(coloredJson);
545
545