@@ -45,6 +45,41 @@ const RUNOPS_SCHEMA = path.join(
4545) ;
4646const BASELINE_PATH = path . join ( WEBAPP_DIR , "app" , "v3" , "runOpsMigration" , "track1-baseline.json" ) ;
4747
48+ const CP_PACKAGE_DIR = path . dirname ( path . dirname ( CP_SCHEMA ) ) ;
49+ const RUNOPS_PACKAGE_DIR = path . dirname ( path . dirname ( RUNOPS_SCHEMA ) ) ;
50+
51+ /** Absolute path of the generated Prisma client for a schema, from its generator `output = "..."`. */
52+ function generatedClientDir ( schemaFile : string ) : string {
53+ const m = / ^ \s * o u t p u t \s * = \s * " ( [ ^ " ] + ) " / m. exec ( fs . readFileSync ( schemaFile , "utf8" ) ) ;
54+ if ( ! m ) {
55+ console . error ( `No generator output path found in ${ schemaFile } ` ) ;
56+ process . exit ( 2 ) ;
57+ }
58+ return path . resolve ( path . dirname ( schemaFile ) , m [ 1 ] ) ;
59+ }
60+
61+ const CP_GENERATED_DIR = generatedClientDir ( CP_SCHEMA ) ;
62+ const RUNOPS_GENERATED_DIR = generatedClientDir ( RUNOPS_SCHEMA ) ;
63+
64+ function realpathIfExists ( p : string ) : string {
65+ try {
66+ return fs . realpathSync ( p ) ;
67+ } catch {
68+ return p ;
69+ }
70+ }
71+
72+ // Classification roots, realpath'd to match the checker's realpath'd file names.
73+ const RUNOPS_DECL_DIRS = [ RUNOPS_GENERATED_DIR , RUNOPS_PACKAGE_DIR ] . map ( realpathIfExists ) ;
74+ const CP_DECL_DIRS = [ CP_GENERATED_DIR , CP_PACKAGE_DIR ] . map ( realpathIfExists ) ;
75+
76+ // Both client packages are force-resolved to SOURCE (src/ + generated client) when building the
77+ // program, so classification never depends on a built/fresh `dist/` in the running environment.
78+ const FORCED_TYPE_RESOLUTIONS = new Map < string , string > ( [
79+ [ "@trigger.dev/database" , path . join ( CP_PACKAGE_DIR , "src" , "index.ts" ) ] ,
80+ [ "@internal/run-ops-database" , path . join ( RUNOPS_PACKAGE_DIR , "src" , "index.ts" ) ] ,
81+ ] ) ;
82+
4883// Files excluded from the sweep. V1-only files come from .claude/rules/legacy-v3-code.md.
4984const V1_FILES = new Set (
5085 [
@@ -254,7 +289,41 @@ function buildProgram(): ts.Program {
254289 } ;
255290 const parsed = ts . getParsedCommandLineOfConfigFile ( TSCONFIG_PATH , undefined , host ) ;
256291 if ( ! parsed ) throw new Error ( `Failed to parse ${ TSCONFIG_PATH } ` ) ;
257- return ts . createProgram ( { rootNames : parsed . fileNames , options : parsed . options } ) ;
292+ // Resolve workspace packages from source (tsconfig.check.json blanks this to target built dists,
293+ // which may not exist here), and pin the two client packages to their src entrypoints —
294+ // @trigger .dev/database has no exports map, so the condition alone cannot redirect it.
295+ const options : ts . CompilerOptions = {
296+ ...parsed . options ,
297+ customConditions : [ "@triggerdotdev/source" ] ,
298+ } ;
299+ const compilerHost = ts . createCompilerHost ( options ) ;
300+ const resolutionCache = ts . createModuleResolutionCache (
301+ compilerHost . getCurrentDirectory ( ) ,
302+ ( f ) => compilerHost . getCanonicalFileName ( f ) ,
303+ options
304+ ) ;
305+ compilerHost . resolveModuleNameLiterals = ( moduleLiterals , containingFile , redirected , opts ) =>
306+ moduleLiterals . map ( ( lit ) => {
307+ const forced = FORCED_TYPE_RESOLUTIONS . get ( lit . text ) ;
308+ if ( forced ) {
309+ return {
310+ resolvedModule : {
311+ resolvedFileName : forced ,
312+ extension : ts . Extension . Ts ,
313+ isExternalLibraryImport : false ,
314+ } ,
315+ } ;
316+ }
317+ return ts . resolveModuleName (
318+ lit . text ,
319+ containingFile ,
320+ opts ,
321+ compilerHost ,
322+ resolutionCache ,
323+ redirected
324+ ) ;
325+ } ) ;
326+ return ts . createProgram ( { rootNames : parsed . fileNames , options, host : compilerHost } ) ;
258327}
259328
260329// ─────────────────────────────────────────────────────────────────────────────
@@ -263,7 +332,17 @@ function buildProgram(): ts.Program {
263332
264333type ClientKind = "cp" | "runops" | "other" ;
265334
335+ function underDir ( file : string , dir : string ) : boolean {
336+ const rel = path . relative ( dir , file ) ;
337+ return rel !== "" && ! rel . startsWith ( ".." ) && ! path . isAbsolute ( rel ) ;
338+ }
339+
340+ // Absolute-dir match against the two packages first (environment-independent); substring match is
341+ // only a fallback for non-workspace layouts (pnpm store paths) and the virtual self-test files.
266342function declFileKind ( fileName : string ) : ClientKind | undefined {
343+ const abs = path . resolve ( fileName ) ;
344+ if ( RUNOPS_DECL_DIRS . some ( ( d ) => underDir ( abs , d ) ) ) return "runops" ;
345+ if ( CP_DECL_DIRS . some ( ( d ) => underDir ( abs , d ) ) ) return "cp" ;
267346 const f = fileName . split ( path . sep ) . join ( "/" ) ;
268347 if ( f . includes ( "run-ops-database" ) ) return "runops" ;
269348 if ( f . includes ( "internal-packages/database" ) ) return "cp" ;
@@ -629,9 +708,65 @@ function collectCrossSeamHits(
629708// Scan
630709// ─────────────────────────────────────────────────────────────────────────────
631710
711+ // Known-kind receivers in db.server.ts. If either fails to classify, the checker cannot see the
712+ // client types (e.g. generated clients missing) and every verdict would be a silent false-clean,
713+ // so the guard refuses to report instead of rubber-stamping.
714+ const CLASSIFICATION_ANCHORS : Array < { name : string ; expected : ClientKind } > = [
715+ { name : "runOpsLegacyPrisma" , expected : "cp" } ,
716+ { name : "runOpsNewPrismaClient" , expected : "runops" } ,
717+ ] ;
718+
719+ function assertClassificationAnchors ( program : ts . Program , checker : ts . TypeChecker ) : void {
720+ const anchorFile = path . join ( WEBAPP_DIR , "app" , "db.server.ts" ) ;
721+ const sf = program . getSourceFile ( anchorFile ) ;
722+ const problems : string [ ] = [ ] ;
723+ if ( ! sf ) {
724+ problems . push ( `${ repoRel ( anchorFile ) } is not part of the program` ) ;
725+ } else {
726+ for ( const anchor of CLASSIFICATION_ANCHORS ) {
727+ let ident : ts . Identifier | undefined ;
728+ const find = ( node : ts . Node ) : void => {
729+ if ( ident ) return ;
730+ if (
731+ ts . isVariableDeclaration ( node ) &&
732+ ts . isIdentifier ( node . name ) &&
733+ node . name . text === anchor . name
734+ ) {
735+ ident = node . name ;
736+ return ;
737+ }
738+ ts . forEachChild ( node , find ) ;
739+ } ;
740+ find ( sf ) ;
741+ if ( ! ident ) {
742+ problems . push ( `anchor "${ anchor . name } " not found in ${ repoRel ( anchorFile ) } ` ) ;
743+ continue ;
744+ }
745+ const receiverType = checker . getTypeAtLocation ( ident ) ;
746+ const delegateSym = receiverType . getProperty ( "taskRun" ) ;
747+ const got = delegateSym
748+ ? classifyType ( checker , checker . getTypeOfSymbolAtLocation ( delegateSym , ident ) )
749+ : "unresolved" ;
750+ if ( got !== anchor . expected ) {
751+ problems . push ( `${ anchor . name } .taskRun classified "${ got } ", expected "${ anchor . expected } "` ) ;
752+ }
753+ }
754+ }
755+ if ( problems . length ) {
756+ console . error (
757+ `[runops-guard] CLASSIFICATION ANCHORS FAILED — the guard cannot distinguish the ` +
758+ `control-plane and run-ops clients in this environment, so its verdicts would be ` +
759+ `meaningless:\n ${ problems . join ( "\n " ) } \n` +
760+ `Ensure the generated Prisma clients exist (pnpm run generate) and retry.`
761+ ) ;
762+ process . exit ( 2 ) ;
763+ }
764+ }
765+
632766function scan ( ) : ScanResult {
633767 const program = buildProgram ( ) ;
634768 const checker = program . getTypeChecker ( ) ;
769+ assertClassificationAnchors ( program , checker ) ;
635770 return scanProgram ( program , checker , isInScope ) ;
636771}
637772
@@ -1134,6 +1269,15 @@ function main(): void {
11341269 process . exit ( 2 ) ;
11351270 }
11361271
1272+ for ( const dir of [ CP_GENERATED_DIR , RUNOPS_GENERATED_DIR ] ) {
1273+ if ( ! fs . existsSync ( path . join ( dir , "index.d.ts" ) ) ) {
1274+ console . error (
1275+ `[runops-guard] generated Prisma client missing at ${ repoRel ( dir ) } — run: pnpm run generate`
1276+ ) ;
1277+ process . exit ( 2 ) ;
1278+ }
1279+ }
1280+
11371281 console . error ( `[runops-guard] repo root: ${ REPO_ROOT } ` ) ;
11381282 console . error (
11391283 `[runops-guard] run-graph delegates: ${ Array . from ( RUN_GRAPH_DELEGATES ) . join ( ", " ) } `
0 commit comments