Skip to content

Commit 94ba2af

Browse files
rootclaude
andcommitted
fix(schema-drift): detect indirect model usage via Prisma relation closure
The schema-drift agent was incorrectly flagging models as unused when they were only referenced via Prisma include clauses (e.g. ContentQuestion used via prisma.contentQuiz.findMany({ include: { questions: true } })). Fix: after collecting directly used models, expand the set transitively through schema relations — if model A is used and has a relation to model B, B is considered in use regardless of direct prisma.B calls. This prevents false-positive "remove this model" suggestions that would break applications relying on included relations. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent e100e9a commit 94ba2af

1 file changed

Lines changed: 36 additions & 4 deletions

File tree

src/agents/pro/schema-drift.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,43 @@ export const schemaDriftAgent: Agent = {
3737
}
3838
}
3939

40+
// Expand usedModels via transitive relation closure:
41+
// If model A is used and has a relation to model B, B is also "in use"
42+
// (e.g. prisma.contentQuiz.findMany({ include: { questions: true } })
43+
// uses ContentQuestion even though prisma.contentQuestion is never called directly)
44+
const relationMap = new Map<string, Set<string>>();
45+
for (const model of graph.schema.models) {
46+
const targets = new Set<string>();
47+
for (const rel of model.relations) {
48+
targets.add(rel.target);
49+
}
50+
relationMap.set(model.name, targets);
51+
}
52+
53+
// Normalise to PascalCase for lookup
54+
const toPascal = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
55+
const expandedUsedModels = new Set<string>(usedModels);
56+
let changed = true;
57+
while (changed) {
58+
changed = false;
59+
for (const [modelName, targets] of relationMap) {
60+
const lc = modelName.charAt(0).toLowerCase() + modelName.slice(1);
61+
if (expandedUsedModels.has(modelName) || expandedUsedModels.has(lc)) {
62+
for (const target of targets) {
63+
if (!expandedUsedModels.has(target) && !expandedUsedModels.has(target.charAt(0).toLowerCase() + target.slice(1))) {
64+
expandedUsedModels.add(target);
65+
changed = true;
66+
}
67+
}
68+
}
69+
}
70+
}
71+
4072
// 1. Models used in code but not in schema
4173
for (const model of usedModels) {
4274
// Prisma uses lowercase model names in client (e.g., prisma.user)
4375
// Schema has PascalCase (e.g., User)
44-
const pascalCase = model.charAt(0).toUpperCase() + model.slice(1);
76+
const pascalCase = toPascal(model);
4577
if (!schemaModelNames.has(model) && !schemaModelNames.has(pascalCase)) {
4678
findings.push({
4779
agentId: 'schema-drift',
@@ -53,18 +85,18 @@ export const schemaDriftAgent: Agent = {
5385
}
5486
}
5587

56-
// 2. Schema models never referenced in code
88+
// 2. Schema models never referenced in code (direct OR via relations)
5789
for (const model of graph.schema.models) {
5890
const lowerName = model.name.charAt(0).toLowerCase() + model.name.slice(1);
59-
if (!usedModels.has(model.name) && !usedModels.has(lowerName)) {
91+
if (!expandedUsedModels.has(model.name) && !expandedUsedModels.has(lowerName)) {
6092
// Skip common framework models
6193
if (['Account', 'Session', 'VerificationToken'].includes(model.name)) continue;
6294

6395
findings.push({
6496
agentId: 'schema-drift',
6597
severity: 'info',
6698
title: 'Unused schema model',
67-
message: `Schema model \`${model.name}\` is never referenced in any API route or page data fetching`,
99+
message: `Schema model \`${model.name}\` is never referenced in any API route, page data fetching, or schema relation from a used model`,
68100
suggestion: 'Remove the model if no longer needed, or add code that uses it',
69101
});
70102
}

0 commit comments

Comments
 (0)