Skip to content

Commit 520f5d1

Browse files
thymikeeclaude
andcommitted
fix(depgraph): tighten report assertions and empty-flag handling
Review findings on #3285: - model.test.ts asserted the SCC/modularity summary lines with shape-only regexes that accepted any count or score, so a CLI line inconsistent with the JSON it was derived from still passed. Compare against the exact payload values instead (planted a wrong modularity value to confirm the test now fails without the fix). - flagValue returned an empty string for `--out ""` / `--dominator-entry ""` verbatim, which bypassed the `?? <default>` fallback (only nullish values trigger it) and resolved --out to the current directory, crashing the write with EISDIR instead of falling back like a missing value does. Fixed at the shared helper, with a regression test for both flags (confirmed both fail against the prior flagValue). - README overstated dominatorSummary.bottlenecks as covering "every reachable file"; the entry itself is excluded by construction. - Clarified the ALL_EDGES doc comment: computeZoneSccSummary builds its own value-only zone graph and never reads this constant, only computeCohesionSummary does. Co-Authored-By: Claude Sonnet 5 <[email protected]>
1 parent 8368e7b commit 520f5d1

4 files changed

Lines changed: 46 additions & 12 deletions

File tree

‎scripts/depgraph/README.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,10 @@ permanent home in the tooling, not a gate.
134134

135135
- **`dominatorSummary`** — the dominator tree of one entry's eager-load closure over VALUE edges
136136
only (dynamic imports are a deliberate cold-start seam, so they never join the closure).
137-
`reachableFiles` is the eager closure size; `bottlenecks` ranks every reachable file by how many
138-
files become unreachable if it disappeared — the size of the branch moving it behind `import()`
139-
would cut. Defaults to `src/daemon.ts`; override with `pnpm depgraph --dominator-entry <path>`.
137+
`reachableFiles` is the eager closure size; `bottlenecks` ranks every reachable non-entry file
138+
by how many files become unreachable if it disappeared — the size of the branch moving it
139+
behind `import()` would cut. Defaults to `src/daemon.ts`; override with
140+
`pnpm depgraph --dominator-entry <path>`.
140141
- **`zoneSccSummary`** — strongly connected components of the zone graph, built from VALUE zone
141142
pairs only (`zoneEdges` entries with `valueCount > 0`), the same edge kind R4 keeps acyclic at
142143
file level. R4 is file-level; nothing stops a loop from closing once files collapse into zones —

‎scripts/depgraph/build.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,10 +187,16 @@ function formatSummary(payload: Payload, jsonPath: string): string {
187187
);
188188
}
189189

190-
/** A flag's value, or `undefined` when the flag is absent or trailing with nothing after it. */
190+
/**
191+
* A flag's value, or `undefined` when the flag is absent, trailing with nothing after it, or
192+
* given an empty string — the last case matters because `?? <default>` only catches nullish
193+
* values, and an empty `--out ""` must fall back the same way a missing one does, not resolve to
194+
* the current directory and crash the subsequent write with EISDIR.
195+
*/
191196
function flagValue(argv: readonly string[], flag: string): string | undefined {
192197
const index = argv.indexOf(flag);
193-
return index >= 0 ? argv[index + 1] : undefined;
198+
const value = index >= 0 ? argv[index + 1] : undefined;
199+
return value ? value : undefined;
194200
}
195201

196202
async function main(argv: readonly string[]): Promise<number> {

‎scripts/depgraph/import-graph.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ export const VALUE_EDGES: ReadonlySet<EdgeKind> = new Set(['value']);
1616
export const EXECUTABLE_EDGES: ReadonlySet<EdgeKind> = new Set(['value', 'dynamic']);
1717

1818
/**
19-
* Every collapsed edge kind, for the structural questions ("does anything reference this file
20-
* at all") that community detection and zone-level cycle reporting ask. Community algorithms
21-
* treat the graph as undirected regardless of kind, so mixing kinds in is the right input, not
22-
* a loosening of a rule — there is no gate riding on this set.
19+
* Every collapsed edge kind, for the structural question ("does anything reference this file at
20+
* all") community detection asks (`computeCohesionSummary`). Community algorithms treat the graph
21+
* as undirected regardless of kind, so mixing kinds in is the right input, not a loosening of a
22+
* rule — there is no gate riding on this set. Zone-level cycle reporting (`computeZoneSccSummary`)
23+
* does not use this set: it builds its own zone graph from VALUE zone pairs only, to mirror R4.
2324
*/
2425
export const ALL_EDGES: ReadonlySet<EdgeKind> = new Set(['value', 'type', 'dynamic']);
2526

‎scripts/depgraph/model.test.ts‎

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -504,10 +504,21 @@ test('build.ts writes the default path and a summary consistent with the JSON',
504504
`dominator summary for src/daemon\\.ts: ${payload.dominatorSummary.reachableFiles} of ${payload.dominatorSummary.totalFiles} files loaded eagerly`,
505505
),
506506
);
507-
assert.match(stdout, /zone-level SCCs: \d+ cyclic group\(s\) across \d+ zones/);
508-
assert.match(
507+
// Tied to the exact payload numbers (not a shape-only `\d+` regex) so a CLI summary that
508+
// drifts from the JSON it was derived from — wrong count, wrong rounding — fails this test.
509+
assert.ok(
510+
stdout.includes(
511+
`zone-level SCCs: ${payload.zoneSccSummary.components.length} cyclic group(s) across ` +
512+
`${payload.zoneSccSummary.zones} zones`,
513+
),
514+
stdout,
515+
);
516+
assert.ok(
517+
stdout.includes(
518+
`modularity: declared zones ${payload.cohesionSummary.modularity.declaredZones.toFixed(3)}, ` +
519+
`detected communities ${payload.cohesionSummary.modularity.detectedCommunities.toFixed(3)}`,
520+
),
509521
stdout,
510-
/modularity: declared zones -?\d+\.\d{3}, detected communities -?\d+\.\d{3}/,
511522
);
512523
});
513524

@@ -543,3 +554,18 @@ test('build.ts falls back to the default path when --out has no value', () => {
543554
assert.equal(status, 0, stdout);
544555
assert.ok(stdout.includes('.tmp/depgraph/graph.json'), stdout);
545556
});
557+
558+
test('build.ts falls back to the default path when --out is given an empty string', () => {
559+
// `--out ""` must fall back the same way a missing value does. `flagValue` returning `''`
560+
// instead of `undefined` here would skip the `??` default and resolve to the current
561+
// directory, crashing the write with EISDIR instead of writing the default report.
562+
const { status, stdout } = runBuild(['--out', '']);
563+
assert.equal(status, 0, stdout);
564+
assert.ok(stdout.includes('.tmp/depgraph/graph.json'), stdout);
565+
});
566+
567+
test('build.ts falls back to the default dominator entry when --dominator-entry is given an empty string', () => {
568+
const { status, stdout } = runBuild(['--dominator-entry', '']);
569+
assert.equal(status, 0, stdout);
570+
assert.ok(stdout.includes('dominator summary for src/daemon.ts:'), stdout);
571+
});

0 commit comments

Comments
 (0)