proteum 2.5.1 → 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.
@@ -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,7 +173,9 @@ import SchemaRouter from '@server/services/schema/router';
171
173
 
172
174
  import * as appConfig from '@/server/config/app';
173
175
 
174
- export default defineApplication({
176
+ export type TControllerRequestServices = {};
177
+
178
+ const ${_args.appIdentifier}Application = defineApplication({
175
179
  services: () => ({}),
176
180
  router: (app) =>
177
181
  new Router(
@@ -185,6 +189,10 @@ export default defineApplication({
185
189
  app,
186
190
  ),
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
  '',