fix(cli): scope dependency-drift reach to the projects that disagree · deepnote/deepnote@30b099d · GitHub
Skip to content

Commit 30b099d

Browse files
jamesbhobbsclaude
andcommitted
fix(cli): scope dependency-drift reach to the projects that disagree
Drift scored its blast radius by counting live projects across the whole workspace and clamping to the finding's own count — the shape that was just removed from credential-shared. It produced the right number only because drift files against live projects already, so the clamp always saturated: correct by accident, through a filter three hundred lines away. The holder count was wrong for a nearer reason. It counted every live consumer of the package, including one declaring it with no version at all, which consumes pandas but is not party to the dispute. Reach for the fixture drops from 5 projects to 4. Both now come from one list of holders, through the shared helper, so this stays right if the maintained-only filter ever loosens. Also: --governance stopped being "SQL correctness and hardcoded credentials" when this PR added the dependency checks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent a8303f4 commit 30b099d

4 files changed

Lines changed: 56 additions & 12 deletions

File tree

‎packages/cli/src/cli.ts‎

Lines changed: 5 additions & 2 deletions

‎packages/cli/src/utils/governance/audit.test.ts‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1289,6 +1289,45 @@ describe('auditWorkspace — dependencies', () => {
12891289
)
12901290
})
12911291

1292+
it('measures drift reach over the projects that disagree, not every live consumer', () => {
1293+
// A live project declaring pandas without a version consumes it but is not party to the
1294+
// dispute, and a workspace-wide live count is not the reach of this finding either. Both come
1295+
// from one list now: the projects that pin a version and disagree.
1296+
const audit = auditWorkspace(
1297+
workspace([
1298+
depProject('p1', 'Alpha', { packages: { pandas: '2.0.1' } }),
1299+
depProject('p2', 'Beta', { packages: { pandas: '1.5.3' } }),
1300+
depProject('p3', 'Bare', { requirements: ['pandas'] }),
1301+
depProject('p4', 'Elsewhere', { packages: { numpy: '1.26.0' } }),
1302+
]),
1303+
{ now: NOW }
1304+
)
1305+
1306+
const drift = issuesOf(audit, 'dependency-drift')
1307+
expect(drift).toHaveLength(2)
1308+
expect(drift[0].details?.projectIds).toEqual(['p1', 'p2'])
1309+
expect(drift[0].details?.projectCount).toBe(2)
1310+
})
1311+
1312+
it('does not count an abandoned holder as live reach', () => {
1313+
// Drift only files against maintained projects today, which is what kept a workspace-wide
1314+
// live count from showing up as a wrong number. Reach is measured among the holders either
1315+
// way, so it stays right if that filter ever loosens.
1316+
const audit = auditWorkspace(
1317+
workspace([
1318+
...['a', 'b', 'c'].map(id => depProject(id, id, { packages: { pandas: '2.0.1' } })),
1319+
depProject('d', 'd', { packages: { pandas: '1.5.3' } }),
1320+
// Five live projects elsewhere, which are not this finding's reach.
1321+
...['x1', 'x2', 'x3', 'x4', 'x5'].map(id => depProject(id, id, { packages: { numpy: '1.26.0' } })),
1322+
]),
1323+
{ now: NOW }
1324+
)
1325+
1326+
const drift = issuesOf(audit, 'dependency-drift')[0]
1327+
expect(drift.details?.projectCount).toBe(4)
1328+
expect((drift.details?.projectIds as string[]).sort()).toEqual(['a', 'b', 'c', 'd'])
1329+
})
1330+
12921331
it('ranks a bare name above a bounded range', () => {
12931332
const audit = auditWorkspace(
12941333
workspace([

‎packages/cli/src/utils/governance/audit.ts‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -784,8 +784,11 @@ export function auditWorkspace(workspace: LoadedWorkspace, options: AuditOptions
784784
continue
785785
}
786786
// Filed against each project that pins a version, because there is no one project at fault —
787-
// the finding is that they disagree.
788-
for (const consumer of usage.projects.filter(entry => entry.live && entry.version !== undefined)) {
787+
// the finding is that they disagree. Those projects are also the finding's holders: a live
788+
// consumer that declares the package without a version is not part of the disagreement, and
789+
// nothing outside the list is part of its reach.
790+
const holders = usage.projects.filter(entry => entry.live && entry.version !== undefined)
791+
for (const consumer of holders) {
789792
issues.push({
790793
severity: SEVERITY['dependency-drift'],
791794
code: 'dependency-drift',
@@ -801,7 +804,8 @@ export function auditWorkspace(workspace: LoadedWorkspace, options: AuditOptions
801804
versions: usage.maintainedVersions,
802805
allVersions: usage.versions,
803806
version: consumer.version,
804-
projectCount: usage.projects.filter(entry => entry.live).length,
807+
projectIds: holders.map(entry => entry.projectId),
808+
projectCount: holders.length,
805809
},
806810
})
807811
}
@@ -1119,12 +1123,9 @@ function scoreIssue(issue: PendingAuditIssue, context: ScoreIssueContext): Sever
11191123
// A bare name will install anything; a range at least bounds what can arrive.
11201124
return scoreFinding(issue.code, { age, signal: issue.details?.pin === 'ranged' ? 0.6 : undefined })
11211125

1122-
case 'dependency-drift': {
1126+
case 'dependency-drift':
11231127
// Its reach is the projects that disagree, weighted by how many of them are still live.
1124-
const projectCount = typeof issue.details?.projectCount === 'number' ? issue.details.projectCount : 1
1125-
const live = [...context.projectAges.values()].filter(projectAge => projectAge.liveness === 'live').length
1126-
return scoreFinding(issue.code, { age, reach: { live: Math.min(live, projectCount), total: projectCount } })
1127-
}
1128+
return scoreFinding(issue.code, { age, reach: holderReach(issue.details, context.projectAges) })
11281129

11291130
case 'ingress-integration-orphan':
11301131
// No live consumer by definition — that is the finding. Its reach is the projects still

‎packages/cli/src/utils/governance/index.ts‎

Lines changed: 3 additions & 2 deletions

0 commit comments

Comments
 (0)