proteum 2.5.0 → 2.5.2

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 (40) hide show
  1. package/AGENTS.md +2 -2
  2. package/README.md +46 -19
  3. package/agents/project/AGENTS.md +9 -7
  4. package/agents/project/CODING_STYLE.md +1 -1
  5. package/agents/project/client/AGENTS.md +5 -1
  6. package/agents/project/diagnostics.md +1 -1
  7. package/agents/project/root/AGENTS.md +9 -7
  8. package/agents/project/server/services/AGENTS.md +4 -0
  9. package/agents/project/tests/AGENTS.md +1 -1
  10. package/cli/commands/verify.ts +117 -4
  11. package/cli/compiler/artifacts/controllerHelper.ts +66 -0
  12. package/cli/compiler/artifacts/controllers.ts +3 -0
  13. package/cli/compiler/artifacts/services.ts +14 -8
  14. package/cli/compiler/common/generatedRouteModules.ts +270 -53
  15. package/cli/presentation/commands.ts +11 -1
  16. package/cli/runtime/commands.ts +6 -0
  17. package/cli/scaffold/templates.ts +14 -6
  18. package/cli/utils/agents.ts +1 -1
  19. package/cli/verification/changed.ts +460 -0
  20. package/client/app/index.ts +22 -5
  21. package/client/services/router/index.tsx +1 -1
  22. package/client/services/router/request/api.ts +2 -2
  23. package/common/applicationConfig.ts +177 -0
  24. package/common/applicationConfigLoader.ts +33 -1
  25. package/common/dev/contractsDoctor.ts +16 -0
  26. package/config.ts +5 -1
  27. package/docs/migration-2.5.md +269 -0
  28. package/eslint.js +96 -50
  29. package/package.json +1 -1
  30. package/server/app/index.ts +28 -2
  31. package/server/services/router/index.ts +3 -3
  32. package/tests/cli-mcp-command.test.cjs +14 -0
  33. package/tests/client-app-error-handling.test.cjs +100 -0
  34. package/tests/contracts-doctor.test.cjs +98 -0
  35. package/tests/definition-contracts.test.cjs +129 -0
  36. package/tests/dev-transpile-watch.test.cjs +3 -6
  37. package/tests/eslint-rules.test.cjs +246 -7
  38. package/tests/scaffold-templates.test.cjs +43 -0
  39. package/tests/server-app-report-error.test.cjs +135 -0
  40. package/tests/verify-changed.test.cjs +200 -0
@@ -615,10 +615,14 @@ const parseDefineApplicationBootstrap = (
615
615
  if (!definitionArg) return undefined;
616
616
 
617
617
  const servicesProperty = getObjectLiteralProperty(definitionArg, 'services');
618
- const servicesDetails =
618
+ const servicesInitializer =
619
619
  servicesProperty && ts.isPropertyAssignment(servicesProperty)
620
- ? getObjectLiteralFactoryDetails(servicesProperty.initializer)
620
+ ? ts.isIdentifier(unwrapExpression(servicesProperty.initializer)) &&
621
+ topLevelInitializers.has((unwrapExpression(servicesProperty.initializer) as ts.Identifier).text)
622
+ ? topLevelInitializers.get((unwrapExpression(servicesProperty.initializer) as ts.Identifier).text)!
623
+ : servicesProperty.initializer
621
624
  : undefined;
625
+ const servicesDetails = servicesInitializer ? getObjectLiteralFactoryDetails(servicesInitializer) : undefined;
622
626
  const servicesObject = servicesDetails?.object;
623
627
  const localServiceInitializers = servicesDetails?.localInitializers || new Map<string, ts.Expression>();
624
628
  const rootServices = parseRootServiceObject(servicesObject, imports, sourceFile.fileName, localServiceInitializers);
@@ -826,6 +830,7 @@ const createCommandServiceStubDeclarations = (rootServices: TParsedService[]): T
826
830
  const typeNamesByAliasImportPath = new Map<string, string>();
827
831
  const pendingSources: TCommandServiceStubSource[] = [];
828
832
  const seenSources = new Set<string>();
833
+ const appTypeReference = `import("@server/app/index").Application & import("@/server/index").${app.identity.identifier}`;
829
834
  const getStubTypeName = (source: TCommandServiceStubSource) => {
830
835
  const existingTypeName = typeNamesByAliasImportPath.get(source.aliasImportPath);
831
836
  if (existingTypeName) return existingTypeName;
@@ -863,7 +868,7 @@ const createCommandServiceStubDeclarations = (rootServices: TParsedService[]): T
863
868
  stubs.set(
864
869
  source.aliasImportPath,
865
870
  `declare class ${getStubTypeName(source)} {
866
- app: InstanceType<typeof import("@/server/index").default>;
871
+ app: ${appTypeReference};
867
872
  [key: string]: any;
868
873
  }`,
869
874
  );
@@ -871,7 +876,7 @@ const createCommandServiceStubDeclarations = (rootServices: TParsedService[]): T
871
876
  }
872
877
 
873
878
  const className = getStubTypeName(source);
874
- const classMembers = [` app: InstanceType<typeof import("@/server/index").default>;`];
879
+ const classMembers = [` app: ${appTypeReference};`];
875
880
 
876
881
  for (const member of defaultClass.members) {
877
882
  if (isPrivateOrProtectedInstanceMember(member)) continue;
@@ -994,6 +999,7 @@ export const generateServiceArtifacts = () => {
994
999
  const appServices = rootServices.map((service) => resolveManifestService(service, 'app'));
995
1000
  const routerPluginServices = routerPlugins.map((service) => resolveManifestService(service, 'Router.plugins'));
996
1001
  const commandServiceStubs = createCommandServiceStubDeclarations(rootServices);
1002
+ const appTypeReference = `import("@server/app/index").Application & import("@/server/index").${appIdentifier}`;
997
1003
 
998
1004
  writeIfChanged(
999
1005
  path.join(app.paths.client.generated, 'server-index.d.ts'),
@@ -1002,7 +1008,7 @@ export const generateServiceArtifacts = () => {
1002
1008
 
1003
1009
  writeIfChanged(
1004
1010
  path.join(app.paths.client.generated, 'services.d.ts'),
1005
- `declare type ${appIdentifier} = InstanceType<typeof import("@/server/index").default>;
1011
+ `declare type ${appIdentifier} = ${appTypeReference};
1006
1012
 
1007
1013
  declare module '@models/types' {
1008
1014
  export * from '@generated/client/models';
@@ -1066,7 +1072,7 @@ export default (): ClientContext => {
1066
1072
 
1067
1073
  writeIfChanged(
1068
1074
  path.join(app.paths.common.generated, 'services.d.ts'),
1069
- `declare type ${appIdentifier} = InstanceType<typeof import("@/server/index").default>;
1075
+ `declare type ${appIdentifier} = ${appTypeReference};
1070
1076
 
1071
1077
  declare module '@models/types' {
1072
1078
  export * from '@generated/common/models';
@@ -1083,7 +1089,7 @@ declare module '@models/types' {
1083
1089
 
1084
1090
  writeIfChanged(
1085
1091
  path.join(app.paths.server.generated, 'commands.d.ts'),
1086
- `declare type ${appIdentifier} = InstanceType<typeof import("@/server/index").default>;
1092
+ `declare type ${appIdentifier} = ${appTypeReference};
1087
1093
 
1088
1094
  declare module "@models/types" {
1089
1095
  const Models: any;
@@ -1127,7 +1133,7 @@ export default ${appIdentifier};
1127
1133
 
1128
1134
  writeIfChanged(
1129
1135
  path.join(app.paths.server.generated, 'services.d.ts'),
1130
- `declare type ${appIdentifier} = InstanceType<typeof import("@/server/index").default>;
1136
+ `declare type ${appIdentifier} = ${appTypeReference};
1131
1137
 
1132
1138
  declare module '@common/errors' {
1133
1139
 
@@ -64,8 +64,75 @@ const parseSourceFile = (filepath: string, code: string) =>
64
64
  filepath.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
65
65
  );
66
66
 
67
+ const parsedSourceFileCache = new Map<string, ts.SourceFile | null>();
68
+
67
69
  const normalizeFilepath = (value: string) => path.resolve(value).replace(/\\/g, '/');
68
70
 
71
+ const resolveExistingModuleFilepath = (baseFilepath: string) => {
72
+ const candidates = [
73
+ baseFilepath,
74
+ `${baseFilepath}.ts`,
75
+ `${baseFilepath}.tsx`,
76
+ `${baseFilepath}.js`,
77
+ `${baseFilepath}.jsx`,
78
+ path.join(baseFilepath, 'index.ts'),
79
+ path.join(baseFilepath, 'index.tsx'),
80
+ path.join(baseFilepath, 'index.js'),
81
+ path.join(baseFilepath, 'index.jsx'),
82
+ ];
83
+
84
+ return candidates.find((candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isFile());
85
+ };
86
+
87
+ const getAppRootFromSourceFile = (sourceFilepath: string) => {
88
+ const normalized = normalizeFilepath(sourceFilepath);
89
+ const markers = ['/client/', '/server/', '/common/', '/commands/'];
90
+ const markerIndex = markers
91
+ .map((marker) => ({ marker, index: normalized.indexOf(marker) }))
92
+ .filter(({ index }) => index >= 0)
93
+ .sort((left, right) => left.index - right.index)[0];
94
+
95
+ return markerIndex ? normalized.slice(0, markerIndex.index) : path.dirname(sourceFilepath);
96
+ };
97
+
98
+ const resolveStaticImportFilepath = (sourceFile: ts.SourceFile, moduleSpecifier: string) => {
99
+ if (moduleSpecifier.startsWith('.')) {
100
+ return resolveExistingModuleFilepath(path.resolve(path.dirname(sourceFile.fileName), moduleSpecifier));
101
+ }
102
+
103
+ const appRoot = getAppRootFromSourceFile(sourceFile.fileName);
104
+ const aliases: Array<[string, string]> = [
105
+ ['@/', appRoot],
106
+ ['@client/', path.join(appRoot, 'client')],
107
+ ['@server/', path.join(appRoot, 'server')],
108
+ ['@common/', path.join(appRoot, 'common')],
109
+ ];
110
+
111
+ for (const [prefix, root] of aliases) {
112
+ if (!moduleSpecifier.startsWith(prefix)) continue;
113
+
114
+ const relativeImport = moduleSpecifier.slice(prefix.length);
115
+ return resolveExistingModuleFilepath(path.join(root, relativeImport));
116
+ }
117
+
118
+ return undefined;
119
+ };
120
+
121
+ const readStaticImportSourceFile = (filepath: string) => {
122
+ const normalized = normalizeFilepath(filepath);
123
+ if (parsedSourceFileCache.has(normalized)) return parsedSourceFileCache.get(normalized) || undefined;
124
+
125
+ if (!fs.existsSync(normalized)) {
126
+ parsedSourceFileCache.set(normalized, null);
127
+ return undefined;
128
+ }
129
+
130
+ const sourceFile = parseSourceFile(normalized, fs.readFileSync(normalized, 'utf8'));
131
+ parsedSourceFileCache.set(normalized, sourceFile);
132
+
133
+ return sourceFile;
134
+ };
135
+
69
136
  const getNodeText = (sourceFile: ts.SourceFile, node: ts.Node) =>
70
137
  sourceFile.text.slice(node.getStart(sourceFile), node.getEnd());
71
138
 
@@ -115,39 +182,46 @@ const tryEvaluateStaticExpression = (
115
182
  resolvedBindings: Map<string, string | number | undefined>,
116
183
  activeBindings = new Set<string>(),
117
184
  ): string | number | undefined => {
118
- if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text;
185
+ const expression = unwrapStaticExpression(node);
186
+ const resolvedExpression = resolveStaticExpressionNode(expression, bindingInitializers, activeBindings);
119
187
 
120
- if (ts.isNumericLiteral(node)) {
121
- const value = Number(node.text);
188
+ if (resolvedExpression !== expression) {
189
+ return tryEvaluateStaticExpression(resolvedExpression, bindingInitializers, resolvedBindings, activeBindings);
190
+ }
191
+
192
+ if (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression)) return expression.text;
193
+
194
+ if (ts.isNumericLiteral(expression)) {
195
+ const value = Number(expression.text);
122
196
  return Number.isFinite(value) ? value : undefined;
123
197
  }
124
198
 
125
- if (ts.isParenthesizedExpression(node)) {
126
- return tryEvaluateStaticExpression(node.expression, bindingInitializers, resolvedBindings, activeBindings);
199
+ if (ts.isParenthesizedExpression(expression)) {
200
+ return tryEvaluateStaticExpression(expression.expression, bindingInitializers, resolvedBindings, activeBindings);
127
201
  }
128
202
 
129
- if (ts.isIdentifier(node)) {
130
- if (resolvedBindings.has(node.text)) return resolvedBindings.get(node.text);
203
+ if (ts.isIdentifier(expression)) {
204
+ if (resolvedBindings.has(expression.text)) return resolvedBindings.get(expression.text);
131
205
 
132
- const initializer = bindingInitializers.get(node.text);
133
- if (!initializer || activeBindings.has(node.text)) return undefined;
206
+ const initializer = bindingInitializers.get(expression.text);
207
+ if (!initializer || activeBindings.has(expression.text)) return undefined;
134
208
 
135
- activeBindings.add(node.text);
209
+ activeBindings.add(expression.text);
136
210
  const value = tryEvaluateStaticExpression(initializer, bindingInitializers, resolvedBindings, activeBindings);
137
- activeBindings.delete(node.text);
138
- resolvedBindings.set(node.text, value);
211
+ activeBindings.delete(expression.text);
212
+ resolvedBindings.set(expression.text, value);
139
213
 
140
214
  return value;
141
215
  }
142
216
 
143
- if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.MinusToken) {
144
- const operand = tryEvaluateStaticExpression(node.operand, bindingInitializers, resolvedBindings, activeBindings);
217
+ if (ts.isPrefixUnaryExpression(expression) && expression.operator === ts.SyntaxKind.MinusToken) {
218
+ const operand = tryEvaluateStaticExpression(expression.operand, bindingInitializers, resolvedBindings, activeBindings);
145
219
  return typeof operand === 'number' ? -operand : undefined;
146
220
  }
147
221
 
148
- if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
149
- const left = tryEvaluateStaticExpression(node.left, bindingInitializers, resolvedBindings, activeBindings);
150
- const right = tryEvaluateStaticExpression(node.right, bindingInitializers, resolvedBindings, activeBindings);
222
+ if (ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.PlusToken) {
223
+ const left = tryEvaluateStaticExpression(expression.left, bindingInitializers, resolvedBindings, activeBindings);
224
+ const right = tryEvaluateStaticExpression(expression.right, bindingInitializers, resolvedBindings, activeBindings);
151
225
 
152
226
  if (left === undefined || right === undefined) return undefined;
153
227
 
@@ -157,10 +231,10 @@ const tryEvaluateStaticExpression = (
157
231
  return undefined;
158
232
  }
159
233
 
160
- if (ts.isTemplateExpression(node)) {
161
- let output = node.head.text;
234
+ if (ts.isTemplateExpression(expression)) {
235
+ let output = expression.head.text;
162
236
 
163
- for (const span of node.templateSpans) {
237
+ for (const span of expression.templateSpans) {
164
238
  const value = tryEvaluateStaticExpression(span.expression, bindingInitializers, resolvedBindings, activeBindings);
165
239
  if (value === undefined) return undefined;
166
240
 
@@ -183,6 +257,70 @@ const unwrapStaticExpression = (node: ts.Expression): ts.Expression => {
183
257
  return node;
184
258
  };
185
259
 
260
+ const getStaticPropertyName = (node: ts.PropertyName | ts.Expression) => {
261
+ if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) return node.text;
262
+ return undefined;
263
+ };
264
+
265
+ const getStaticObjectPropertyInitializer = (node: ts.ObjectLiteralExpression, propertyName: string) => {
266
+ for (const property of node.properties) {
267
+ if (ts.isPropertyAssignment(property)) {
268
+ const key = getObjectLiteralPropertyKey(property.name);
269
+ if (key === propertyName) return property.initializer;
270
+ continue;
271
+ }
272
+
273
+ if (ts.isShorthandPropertyAssignment(property) && property.name.text === propertyName) {
274
+ return property.name;
275
+ }
276
+ }
277
+
278
+ return undefined;
279
+ };
280
+
281
+ const resolveStaticExpressionNode = (
282
+ node: ts.Expression,
283
+ bindingInitializers: Map<string, ts.Expression>,
284
+ activeBindings = new Set<string>(),
285
+ ): ts.Expression => {
286
+ const expression = unwrapStaticExpression(node);
287
+
288
+ if (ts.isIdentifier(expression)) {
289
+ const initializer = bindingInitializers.get(expression.text);
290
+ if (!initializer || activeBindings.has(expression.text)) return expression;
291
+
292
+ activeBindings.add(expression.text);
293
+ const resolved = resolveStaticExpressionNode(initializer, bindingInitializers, activeBindings);
294
+ activeBindings.delete(expression.text);
295
+
296
+ return resolved;
297
+ }
298
+
299
+ if (ts.isPropertyAccessExpression(expression)) {
300
+ const container = resolveStaticExpressionNode(expression.expression, bindingInitializers, activeBindings);
301
+ const unwrappedContainer = unwrapStaticExpression(container);
302
+ if (!ts.isObjectLiteralExpression(unwrappedContainer)) return expression;
303
+
304
+ const initializer = getStaticObjectPropertyInitializer(unwrappedContainer, expression.name.text);
305
+ return initializer ? resolveStaticExpressionNode(initializer, bindingInitializers, activeBindings) : expression;
306
+ }
307
+
308
+ if (ts.isElementAccessExpression(expression)) {
309
+ const argument = expression.argumentExpression && unwrapStaticExpression(expression.argumentExpression);
310
+ const propertyName = argument && getStaticPropertyName(argument);
311
+ if (!propertyName) return expression;
312
+
313
+ const container = resolveStaticExpressionNode(expression.expression, bindingInitializers, activeBindings);
314
+ const unwrappedContainer = unwrapStaticExpression(container);
315
+ if (!ts.isObjectLiteralExpression(unwrappedContainer)) return expression;
316
+
317
+ const initializer = getStaticObjectPropertyInitializer(unwrappedContainer, propertyName);
318
+ return initializer ? resolveStaticExpressionNode(initializer, bindingInitializers, activeBindings) : expression;
319
+ }
320
+
321
+ return expression;
322
+ };
323
+
186
324
  const isStaticSerializableExpression = (
187
325
  node: ts.Expression,
188
326
  bindingInitializers: Map<string, ts.Expression>,
@@ -190,6 +328,11 @@ const isStaticSerializableExpression = (
190
328
  activeBindings = new Set<string>(),
191
329
  ): boolean => {
192
330
  const expression = unwrapStaticExpression(node);
331
+ const resolvedExpression = resolveStaticExpressionNode(expression, bindingInitializers, activeBindings);
332
+
333
+ if (resolvedExpression !== expression) {
334
+ return isStaticSerializableExpression(resolvedExpression, bindingInitializers, resolvedBindings, activeBindings);
335
+ }
193
336
 
194
337
  if (
195
338
  ts.isStringLiteral(expression) ||
@@ -275,21 +418,9 @@ const assertStaticSerializableMetadata = (
275
418
  );
276
419
  };
277
420
 
278
- const collectStaticBindings = (sourceFile: ts.SourceFile) => {
279
- const bindingInitializers = new Map<string, ts.Expression>();
421
+ const resolveStaticBindings = (bindingInitializers: Map<string, ts.Expression>) => {
280
422
  const resolvedBindings = new Map<string, string | number | undefined>();
281
423
 
282
- for (const statement of sourceFile.statements) {
283
- if (!ts.isVariableStatement(statement)) continue;
284
- if (!(statement.declarationList.flags & ts.NodeFlags.Const)) continue;
285
-
286
- for (const declaration of statement.declarationList.declarations) {
287
- if (!ts.isIdentifier(declaration.name) || !declaration.initializer) continue;
288
-
289
- bindingInitializers.set(declaration.name.text, declaration.initializer);
290
- }
291
- }
292
-
293
424
  for (const bindingName of bindingInitializers.keys()) {
294
425
  if (resolvedBindings.has(bindingName)) continue;
295
426
 
@@ -305,11 +436,15 @@ const collectStaticBindings = (sourceFile: ts.SourceFile) => {
305
436
  return resolvedBindings;
306
437
  };
307
438
 
308
- const collectStaticBindingInitializers = (sourceFile: ts.SourceFile) => {
439
+ const collectStaticBindings = (sourceFile: ts.SourceFile) =>
440
+ resolveStaticBindings(collectStaticBindingInitializers(sourceFile));
441
+
442
+ const collectExportedStaticBindingInitializers = (sourceFile: ts.SourceFile) => {
309
443
  const bindingInitializers = new Map<string, ts.Expression>();
310
444
 
311
445
  for (const statement of sourceFile.statements) {
312
446
  if (!ts.isVariableStatement(statement)) continue;
447
+ if (!statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) continue;
313
448
  if (!(statement.declarationList.flags & ts.NodeFlags.Const)) continue;
314
449
 
315
450
  for (const declaration of statement.declarationList.declarations) {
@@ -322,6 +457,61 @@ const collectStaticBindingInitializers = (sourceFile: ts.SourceFile) => {
322
457
  return bindingInitializers;
323
458
  };
324
459
 
460
+ const collectImportedStaticBindingInitializers = (
461
+ sourceFile: ts.SourceFile,
462
+ visitedFiles = new Set<string>([normalizeFilepath(sourceFile.fileName)]),
463
+ ) => {
464
+ const bindingInitializers = new Map<string, ts.Expression>();
465
+
466
+ for (const statement of sourceFile.statements) {
467
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
468
+
469
+ const namedBindings = statement.importClause?.namedBindings;
470
+ if (!namedBindings || !ts.isNamedImports(namedBindings)) continue;
471
+
472
+ const importedFilepath = resolveStaticImportFilepath(sourceFile, statement.moduleSpecifier.text);
473
+ if (!importedFilepath) continue;
474
+
475
+ const normalizedImportFilepath = normalizeFilepath(importedFilepath);
476
+ if (visitedFiles.has(normalizedImportFilepath)) continue;
477
+
478
+ const importedSourceFile = readStaticImportSourceFile(normalizedImportFilepath);
479
+ if (!importedSourceFile) continue;
480
+
481
+ const exportedInitializers = collectExportedStaticBindingInitializers(importedSourceFile);
482
+ for (const element of namedBindings.elements) {
483
+ const importedName = element.propertyName?.text || element.name.text;
484
+ const initializer = exportedInitializers.get(importedName);
485
+ if (!initializer) continue;
486
+
487
+ bindingInitializers.set(element.name.text, initializer);
488
+ }
489
+ }
490
+
491
+ return bindingInitializers;
492
+ };
493
+
494
+ const collectStatementStaticBindingInitializers = (
495
+ statements: readonly ts.Statement[],
496
+ bindingInitializers = new Map<string, ts.Expression>(),
497
+ ) => {
498
+ for (const statement of statements) {
499
+ if (!ts.isVariableStatement(statement)) continue;
500
+ if (!(statement.declarationList.flags & ts.NodeFlags.Const)) continue;
501
+
502
+ for (const declaration of statement.declarationList.declarations) {
503
+ if (!ts.isIdentifier(declaration.name) || !declaration.initializer) continue;
504
+
505
+ bindingInitializers.set(declaration.name.text, declaration.initializer);
506
+ }
507
+ }
508
+
509
+ return bindingInitializers;
510
+ };
511
+
512
+ const collectStaticBindingInitializers = (sourceFile: ts.SourceFile) =>
513
+ collectStatementStaticBindingInitializers(sourceFile.statements, collectImportedStaticBindingInitializers(sourceFile));
514
+
325
515
  const getPropertyAssignment = (node: ts.ObjectLiteralExpression, propertyName: string) => {
326
516
  for (const property of node.properties) {
327
517
  if (!ts.isPropertyAssignment(property)) continue;
@@ -339,15 +529,42 @@ const getCallExpressionName = (node: ts.Expression) => {
339
529
  return undefined;
340
530
  };
341
531
 
532
+ const collectServerRouteDefinitionExpressions = (routesArg: ts.Expression | undefined) => {
533
+ if (!routesArg) return undefined;
534
+
535
+ if (ts.isArrayLiteralExpression(routesArg)) return [...routesArg.elements];
536
+
537
+ if (!(ts.isArrowFunction(routesArg) || ts.isFunctionExpression(routesArg))) return undefined;
538
+
539
+ if (ts.isArrayLiteralExpression(routesArg.body)) return [...routesArg.body.elements];
540
+ if (!ts.isBlock(routesArg.body)) return undefined;
541
+
542
+ const routeExpressions: ts.Expression[] = [];
543
+
544
+ for (const statement of routesArg.body.statements) {
545
+ if (!ts.isExpressionStatement(statement)) continue;
546
+ if (!ts.isCallExpression(statement.expression)) continue;
547
+ if (!ts.isPropertyAccessExpression(statement.expression.expression)) continue;
548
+ if (statement.expression.expression.name.text !== 'push') continue;
549
+
550
+ for (const argument of statement.expression.arguments) {
551
+ const unwrappedArgument = unwrapStaticExpression(argument);
552
+ if (!ts.isCallExpression(unwrappedArgument)) continue;
553
+
554
+ const helperName = getCallExpressionName(unwrappedArgument.expression);
555
+ if (helperName === 'defineServerRoute') routeExpressions.push(unwrappedArgument);
556
+ }
557
+ }
558
+
559
+ return routeExpressions.length > 0 ? routeExpressions : undefined;
560
+ };
561
+
342
562
  const resolveIdentifierExpression = (
343
563
  expression: ts.Expression,
344
564
  sourceFile: ts.SourceFile,
345
565
  ): ts.Expression => {
346
- const unwrapped = unwrapStaticExpression(expression);
347
- if (!ts.isIdentifier(unwrapped)) return unwrapped;
348
-
349
566
  const bindingInitializers = collectStaticBindingInitializers(sourceFile);
350
- return bindingInitializers.get(unwrapped.text) || unwrapped;
567
+ return resolveStaticExpressionNode(expression, bindingInitializers);
351
568
  };
352
569
 
353
570
  const getDefaultRouteDefinitionExpression = (sourceFile: ts.SourceFile) => {
@@ -402,10 +619,11 @@ const parseExplicitRouteCall = (
402
619
  sourceFile: ts.SourceFile,
403
620
  side: TRouteSide,
404
621
  node: ts.Expression,
622
+ scopedStaticBindingInitializers?: Map<string, ts.Expression>,
405
623
  ): TExplicitRouteDefinition[] => {
406
624
  const expression = unwrapStaticExpression(resolveIdentifierExpression(node, sourceFile));
407
- const staticBindingInitializers = collectStaticBindingInitializers(sourceFile);
408
- const staticBindings = collectStaticBindings(sourceFile);
625
+ const staticBindingInitializers = scopedStaticBindingInitializers || collectStaticBindingInitializers(sourceFile);
626
+ const staticBindings = resolveStaticBindings(staticBindingInitializers);
409
627
 
410
628
  if (!ts.isCallExpression(expression)) {
411
629
  throw new Error(`Route module ${sourceFile.fileName} must default-export a define*Route(...) call.`);
@@ -422,20 +640,19 @@ const parseExplicitRouteCall = (
422
640
  }
423
641
 
424
642
  const [routesArg] = [...expression.arguments];
425
- const routeListExpression =
426
- routesArg && ts.isArrayLiteralExpression(routesArg)
427
- ? routesArg
428
- : routesArg &&
429
- (ts.isArrowFunction(routesArg) || ts.isFunctionExpression(routesArg)) &&
430
- ts.isArrayLiteralExpression(routesArg.body)
431
- ? routesArg.body
432
- : undefined;
433
-
434
- if (!routeListExpression) {
435
- throw new Error(`defineServerRoutes(...) in ${sourceFile.fileName} must receive a static array literal or a factory returning one.`);
643
+ const routeExpressions = collectServerRouteDefinitionExpressions(routesArg);
644
+ const routeBindingInitializers =
645
+ routesArg && (ts.isArrowFunction(routesArg) || ts.isFunctionExpression(routesArg)) && ts.isBlock(routesArg.body)
646
+ ? collectStatementStaticBindingInitializers(routesArg.body.statements, new Map(staticBindingInitializers))
647
+ : staticBindingInitializers;
648
+
649
+ if (!routeExpressions) {
650
+ throw new Error(
651
+ `defineServerRoutes(...) in ${sourceFile.fileName} must receive a static array literal, a factory returning one, or a factory that pushes defineServerRoute(...) entries into a local routes array.`,
652
+ );
436
653
  }
437
654
 
438
- return routeListExpression.elements.map((element) => parseExplicitRouteCall(sourceFile, side, element)).flat();
655
+ return routeExpressions.map((element) => parseExplicitRouteCall(sourceFile, side, element, routeBindingInitializers)).flat();
439
656
  }
440
657
 
441
658
  const [definitionArg] = [...expression.arguments];
@@ -446,7 +663,7 @@ const parseExplicitRouteCall = (
446
663
  const sourceLocation = getNodeLocation(sourceFile, expression);
447
664
  const optionsExpression = getPropertyAssignment(definitionArg, 'options');
448
665
  const resolvedOptionsExpression = optionsExpression
449
- ? unwrapStaticExpression(resolveIdentifierExpression(optionsExpression, sourceFile))
666
+ ? unwrapStaticExpression(resolveStaticExpressionNode(optionsExpression, staticBindingInitializers))
450
667
  : undefined;
451
668
  const optionsArg =
452
669
  resolvedOptionsExpression && ts.isObjectLiteralExpression(resolvedOptionsExpression)
@@ -612,10 +612,18 @@ export const proteumCommands: Record<TProteumCommandName, TProteumCommandDoc> =
612
612
  name: 'verify',
613
613
  category: 'Manifest and contracts',
614
614
  summary: 'Run focused owner/request/browser verification or the full framework reference-app validation pass.',
615
- usage: 'proteum verify [framework-change|owner <query>|request <path>|browser <path>] [--port <port>|--url <baseUrl>] [--session-email <email>] [--session-role <role>] [--method <verb>] [--data-json <json>] [--strict-global] [--crosspath <path>] [--product <path>] [--website <path>] [--crosspath-port <port>] [--product-port <port>] [--website-port <port>] [--route <path>] [--json]',
615
+ usage: 'proteum verify [changed|framework-change|owner <query>|request <path>|browser <path>] [--staged] [--base <ref>] [--dry-run] [--port <port>|--url <baseUrl>] [--session-email <email>] [--session-role <role>] [--method <verb>] [--data-json <json>] [--strict-global] [--crosspath <path>] [--product <path>] [--website <path>] [--crosspath-port <port>] [--product-port <port>] [--website-port <port>] [--route <path>] [--json]',
616
616
  bestFor:
617
617
  'Choosing the smallest trustworthy verification surface first, then separating introduced blocking findings from unrelated pre-existing diagnostics.',
618
618
  examples: [
619
+ {
620
+ description: 'Plan targeted checks for changed files without running them',
621
+ command: 'proteum verify changed --dry-run',
622
+ },
623
+ {
624
+ description: 'Run targeted checks for staged changes only',
625
+ command: 'proteum verify changed --staged',
626
+ },
619
627
  {
620
628
  description: 'Run the default framework smoke verification against the reference apps',
621
629
  command: 'proteum verify framework-change',
@@ -638,6 +646,8 @@ export const proteumCommands: Record<TProteumCommandName, TProteumCommandDoc> =
638
646
  },
639
647
  ],
640
648
  notes: [
649
+ '`proteum verify changed` reads optional `proteum.verify.config.ts`, combines changed-file detection with built-in Vitest defaults, prints the selected plan, then runs every selected check.',
650
+ '`proteum verify changed --dry-run --json` is the safest agent entrypoint when you need to inspect selected checks without changing cache or test artifacts.',
641
651
  '`proteum verify owner` starts from `proteum orient`, then chooses the smallest trustworthy verify path instead of defaulting to broad global checks.',
642
652
  '`proteum verify owner`, `request`, and `browser` emit `introducedFindings`, `preExistingFindings`, `verificationSteps`, and `result` in JSON.',
643
653
  'Focused verification fails on introduced blocking findings by default and only fails on unrelated pre-existing blockers when `--strict-global` is passed.',
@@ -846,6 +846,8 @@ class VerifyCommand extends ProteumCommand {
846
846
 
847
847
  public static usage = buildUsage('verify');
848
848
 
849
+ public base = Option.String('--base', { description: 'Base ref used by `proteum verify changed`.' });
850
+ public dryRun = Option.Boolean('--dry-run', false, { description: 'Print the changed-file verification plan without running checks.' });
849
851
  public json = Option.Boolean('--json', false, { description: 'Print JSON output.' });
850
852
  public port = Option.String('--port', { description: 'Target an existing dev server on the given port for focused verify actions.' });
851
853
  public url = Option.String('--url', { description: 'Target an existing dev server at the given base URL for focused verify actions.' });
@@ -869,6 +871,7 @@ class VerifyCommand extends ProteumCommand {
869
871
  description: 'Port used for the Unique Domains Website validation server.',
870
872
  });
871
873
  public route = Option.String('--route', { description: 'Route loaded in both apps during validation.' });
874
+ public staged = Option.Boolean('--staged', false, { description: 'Verify staged changes only.' });
872
875
  public args = Option.Rest();
873
876
 
874
877
  public async execute() {
@@ -877,9 +880,11 @@ class VerifyCommand extends ProteumCommand {
877
880
 
878
881
  this.setCliArgs({
879
882
  action,
883
+ base: this.base ?? '',
880
884
  crosspath: this.crosspath ?? '',
881
885
  crosspathPort: this.crosspathPort ?? '',
882
886
  dataJson: this.dataJson ?? '',
887
+ dryRun: this.dryRun,
883
888
  json: this.json,
884
889
  method: this.method ?? '',
885
890
  port: this.port ?? '',
@@ -888,6 +893,7 @@ class VerifyCommand extends ProteumCommand {
888
893
  route: this.route ?? '',
889
894
  sessionEmail: this.sessionEmail ?? '',
890
895
  sessionRole: this.sessionRole ?? '',
896
+ staged: this.staged,
891
897
  strictGlobal: this.strictGlobal,
892
898
  target,
893
899
  url: this.url ?? '',
@@ -53,7 +53,7 @@ export const createControllerTemplate = ({
53
53
  appIdentifier: string;
54
54
  className: string;
55
55
  methodName: string;
56
- }) => `import { defineAction, defineController } from '@server/app/controller';
56
+ }) => `import { defineAction, defineController } from '@generated/server/controller';
57
57
 
58
58
  export default defineController({
59
59
  actions: {
@@ -75,7 +75,9 @@ export const createCommandTemplate = ({
75
75
  className: string;
76
76
  methodName: string;
77
77
  }) => `import { Commands } from '@server/app/commands';
78
- import type App from '@/server/index';
78
+ import type AppApplication from '@/server/index';
79
+
80
+ type App = InstanceType<typeof AppApplication>;
79
81
 
80
82
  export default class ${className} extends Commands<App> {
81
83
  public async ${methodName}() {
@@ -171,9 +173,12 @@ import SchemaRouter from '@server/services/schema/router';
171
173
 
172
174
  import * as appConfig from '@/server/config/app';
173
175
 
174
- export default defineApplication({
175
- services: (app) => ({
176
- Router: new Router(
176
+ export type TControllerRequestServices = {};
177
+
178
+ const ${_args.appIdentifier}Application = defineApplication({
179
+ services: () => ({}),
180
+ router: (app) =>
181
+ new Router(
177
182
  app,
178
183
  {
179
184
  ...appConfig.routerBaseConfig,
@@ -183,8 +188,11 @@ export default defineApplication({
183
188
  },
184
189
  app,
185
190
  ),
186
- }),
187
191
  });
192
+
193
+ export type ${_args.appIdentifier} = InstanceType<typeof ${_args.appIdentifier}Application>;
194
+
195
+ export default ${_args.appIdentifier}Application;
188
196
  `;
189
197
 
190
198
  export const createClientTsconfigTemplate = (paths: TTsconfigTemplatePaths) => `{
@@ -643,7 +643,7 @@ function renderEmbeddedProjectInstructions({ appRoot, coreRoot, includeMonorepoR
643
643
  '- App roots default-export `defineApplication({ services, router, models, commands })`; `server/index.ts` is the canonical type root for the project app, services, router plugins, request context, and models.',
644
644
  '- Client page files default-export `definePageRoute({ path, options, data, render })` or `defineErrorRoute({ code, options, render })`; route paths come from the definition object, not from `Router.page(...)` or the file path.',
645
645
  '- Manual HTTP route files default-export `defineServerRoute({ method, path, options, handler })` or `defineServerRoutes(...)`; use `expressHandler(...)` only when raw Express `req`, `res`, or `next` is required.',
646
- '- Controllers default-export `defineController({ path, actions })`; actions use `defineAction({ input, handler })`, and parsed input is read from the handler context.',
646
+ '- Controllers default-export `defineController({ path, actions })` from `@generated/server/controller`; actions use `defineAction({ input, handler })`, and parsed input is read from the app-typed handler context.',
647
647
  '- Never import `@app` in page, route, or controller files. Never call top-level `Router.page(...)`, `Router.error(...)`, `Router.get(...)`, `Router.post(...)`, `Router.put(...)`, `Router.patch(...)`, `Router.delete(...)`, or `Router.express(...)` in app source.',
648
648
  '- Runtime app, service, request, response, router, auth, and custom router-plugin access belongs only in typed callback parameters such as `data`, `render`, route `handler`, controller action `handler`, `defineServerRoutes((app) => ...)`, or typed service `this.app`/`this.services`.',
649
649
  '',