Skip to content

Commit ed4c9f5

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix: narrow dependency propagation and deduplicate visual evidence
1 parent 9df1584 commit ed4c9f5

10 files changed

Lines changed: 643 additions & 101 deletions

File tree

scripts/design-diff/README.md

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ references do not count. Dynamic/ambiguous imports are not presented as exact us
6161
Zero means no references were enumerated, not proof of no consumers. Overrides, inactive
6262
variants and runtime conditions can prevent a referenced component from changing visually.
6363

64+
Changed sources are analyzed before unchanged consumers. Once a source has flagged evidence,
65+
repeated downstream expansion can be omitted while its resolved usage inventory is retained.
66+
The report records the number of unchanged files omitted; categories describe retained evidence.
67+
Changed files, configured documentation inputs and infrastructure are still inspected. CSS/token
68+
sources retain a changed consumer example before further repeated expansion is omitted.
69+
6470
Schema 3 readers must handle either literal values or a summary object containing
6571
`$truncated`, `preview`, `sha256`, `hashAlgorithm`, `originalBytes`, `previewBytes` and `omittedBytes`.
6672
`hashAlgorithm: sha256-merkle-v1` hashes the complete semantic tree, including order and types,
@@ -102,6 +108,7 @@ scripts/design-diff/
102108
refactors.ts Supported literal/refactor normalization
103109
mutations.ts Referenced collection/property writes
104110
finite.ts Static finite keys for computed environment reads
111+
environment.ts Literal createEnv schema field comparison
105112
resolve.ts Bounded expression and import resolution
106113
inputs.ts Configured file-loaded documentation inputs
107114
infrastructure.ts Rendering lockfile dependency closure
@@ -167,12 +174,16 @@ because its name happens to be `cn` or `clsx`.
167174
Dependency propagation resolves named/default imports, aliases and static namespace members
168175
through named/star re-exports and import-then-export indexes to the defining module. A
169176
Button edit does not implicate a file merely because it imports an unrelated Icon from the
170-
same index. Changed top-level bindings and local dependents narrow the first propagation
171-
step and usage counts within a multi-export file. Both revisions are considered, including
177+
same index. Changed top-level bindings and local dependents narrow the propagation
178+
steps and usage counts within a multi-export file. Unresolved imports keep immediate module
179+
edges instead of bypassing intermediate binding checks with every transitive dependency.
180+
Literal `createEnv` schemas can narrow the first hop to changed keys, including keys from
181+
static feature-definition loops. Changed helper implementations, options, computed schemas
182+
and escaping collections retain conservative propagation. Both revisions are considered, including
172183
redirected re-exports. The module graph identifies candidates only. Actual findings require
173184
changed values, guards, referenced implementations or an explicitly unresolved imported input.
174185
Unrelated imports and dead re-exports cannot flag an unchanged expression. Object properties,
175-
destructured parameter defaults, and helper return paths are traced separately. Small immutable
186+
destructured parameter defaults, and selected helper return properties/guards are traced separately. Small immutable
176187
local literals are normalized before expression budgets, preserving supported constant hoists.
177188
The exact `Object.entries(...).reduce` record-map idiom is normalized to `Object.fromEntries`
178189
only with an empty accumulator, unchanged key and no accumulator reads in the mapped value. Unknown
@@ -286,7 +297,7 @@ bun run check:design-diff-types
286297
bun run check:api-validation
287298
```
288299

289-
Root script-test discovery includes every suite in `tests/`. The fixtures exercise visual
300+
Script-test discovery through `scripts/vitest.config.ts` includes every suite in `tests/`. The fixtures exercise visual
290301
categories, noops, movement, shared imports/themes, source order, documents/native rendering,
291302
Git divergence/renames/deletions/binaries/unusual names/missing history, bounded evaluation,
292303
deterministic output, CLI failure status and non-execution of proposed code/plugins.
@@ -357,7 +368,10 @@ Fetch manifest commit objects beforehand; missing history fails explicitly. The
357368
the frozen comparison commits and GitHub file sets. Cache identity includes engine SHA, trusted
358369
config, lockfile, runtime and comparison commits, with report-content verification before reuse.
359370
It awaits native Bun process exit status and records per-comparison elapsed time and peak RSS
360-
separately from deterministic reports. Failed runs retain bounded stderr diagnostics in a
371+
separately from deterministic reports. The default comparison deadline is 900 seconds.
372+
For research, `--timeout-seconds` accepts 1–3600 seconds and enters the cache identity;
373+
it does not change the production workflow timeout. Report comparisons exceeding 900 seconds
374+
separately because they cannot fit that workflow budget. Failed runs retain bounded stderr diagnostics in a
361375
separate file, without printing source findings to logs.
362376
`/usr/bin/time` is required (macOS or Linux); source findings are not printed. Review original
363377
and holdout rates separately, and inspect every disagreement against the source label.

scripts/design-diff/analyze.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { cssValue, extractCss } from '#design-diff/extract/css'
55
import { extractDocument } from '#design-diff/extract/documents'
66
import { extractTsx } from '#design-diff/extract/tsx'
77
import { GitReader } from '#design-diff/git'
8-
import { groupFindings } from '#design-diff/group'
8+
import { causalSources, groupFindings } from '#design-diff/group'
99
import { renderingLock } from '#design-diff/infrastructure'
1010
import { fileLoadedInputs } from '#design-diff/inputs'
1111
import { reclaimMemory } from '#design-diff/memory'
@@ -191,10 +191,22 @@ export async function analyze(
191191
return []
192192
}
193193
let extracted = 0
194-
for (const file of [...affected].sort()) {
194+
let omittedConsumers = 0
195+
const covered = new Set<string>()
196+
const currentPath = (file: string) => [...renames].find(([, old]) => old === file)?.[0] ?? file
197+
const ordered = [...affected].sort(
198+
(a, b) => Number(changed.has(b)) - Number(changed.has(a)) || a.localeCompare(b, 'en')
199+
)
200+
for (const file of ordered) {
195201
if (++extracted % 32 === 0) reclaimMemory()
196202
if (!scoped(file, config) && !infrastructure(file, config)) continue
197203
if ([...renames.values()].includes(file) && !after.entries.has(file)) continue
204+
const roots = [...(causes.get(file) ?? [])].map(currentPath)
205+
if (!changed.has(file) && roots.length && roots.every((root) => covered.has(root))) {
206+
omittedConsumers++
207+
continue
208+
}
209+
const firstFinding = findings.length
198210
const oldFile = renames.get(file) ?? file
199211
const a = await extract(before, previousTailwind, oldFile)
200212
const b = await extract(after, nextTailwind, file)
@@ -235,7 +247,16 @@ export async function analyze(
235247
review(file, after.entries.get(file)?.oid ?? '', 'Unsupported rendering mechanism')
236248
)
237249
)
250+
for (const change of findings.slice(firstFinding))
251+
if (change.decision === 'flag')
252+
for (const source of causalSources(change, causes, renames))
253+
if (source !== currentPath(file) || /\.[jt]sx$/.test(source)) covered.add(source)
238254
}
255+
if (omittedConsumers)
256+
report.limitations = [
257+
...report.limitations,
258+
`Repeated downstream expansion omitted for ${omittedConsumers} unchanged files after all contributing changed sources already had flagged evidence. Categories describe retained evidence; usage counts remain partial resolved references.`,
259+
]
239260
for (const file of changed) {
240261
if (file !== 'bun.lock' && !file.endsWith('/package.json') && file !== 'package.json')
241262
continue

scripts/design-diff/benchmark.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ interface Result {
2626
flagged: boolean | null
2727
exitCode: number | null
2828
seconds: number
29+
timeoutSeconds: number
2930
peakMemoryBytes: number | null
3031
reportBytes: number
3132
reportSha256?: string
@@ -45,6 +46,7 @@ export async function benchmark(args = process.argv.slice(2)): Promise<void> {
4546
manifest: { type: 'string' },
4647
output: { type: 'string' },
4748
workers: { type: 'string', default: '3' },
49+
'timeout-seconds': { type: 'string', default: '900' },
4850
},
4951
strict: true,
5052
})
@@ -89,12 +91,16 @@ export async function benchmark(args = process.argv.slice(2)): Promise<void> {
8991
new Set(manifest.comparisons.map((item) => item.pr)).size !== manifest.comparisons.length
9092
)
9193
throw new Error('Invalid or duplicate comparison manifest')
94+
const timeoutSeconds = Number(values['timeout-seconds'])
95+
if (!Number.isInteger(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > 3600)
96+
throw new Error('Timeout seconds must be 1..3600')
9297
const identity = hash(
9398
JSON.stringify({
9499
sha: values.sha,
95100
config: hash(readFileSync(path.join(engine, 'design-diff.config.json'))),
96101
lock: git('rev-parse', 'HEAD:bun.lock'),
97102
runtime: bunVersion,
103+
timeoutSeconds,
98104
})
99105
)
100106
const workers = Number(values.workers)
@@ -119,6 +125,7 @@ export async function benchmark(args = process.argv.slice(2)): Promise<void> {
119125
flagged: null,
120126
exitCode: null,
121127
seconds: 0,
128+
timeoutSeconds,
122129
peakMemoryBytes: null,
123130
reportBytes: 0,
124131
categories: [],
@@ -168,7 +175,8 @@ export async function benchmark(args = process.argv.slice(2)): Promise<void> {
168175
reportFile,
169176
],
170177
engine,
171-
env
178+
env,
179+
timeoutSeconds * 1000
172180
)
173181
result.exitCode = execution.exitCode
174182
const metrics =

0 commit comments

Comments
 (0)