Skip to content

Commit 53261ab

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
Merge branch 'codex/design-diff-engine' into codex/design-diff-benchmark-v3
2 parents af45e00 + 691769f commit 53261ab

6 files changed

Lines changed: 193 additions & 25 deletions

File tree

scripts/design-diff/README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ scripts/design-diff/
102102
infrastructure.ts Rendering lockfile dependency closure
103103
report.ts Value previews and bounded JSON serialization
104104
benchmark.ts Immutable-engine historical replay
105+
process.ts Native Bun process status and bounded diagnostics
105106
benchmark/comparisons.json Frozen original/holdout comparison manifest
106107
memory.ts Bun parser-batch garbage collection
107108
tailwind.ts Pinned compiler and trusted merge convention
@@ -149,7 +150,9 @@ does not establish equivalence of arbitrary interactive behavior.
149150

150151
The resolver supports immutable constants, object properties, arrays, primitive template
151152
strings, simple arithmetic, conditional branches, static imports/re-exports, namespace
152-
imports, workspace exports and project `paths` aliases. It records CVA bases, variants,
153+
imports, workspace exports and project `paths` aliases. Static array selections and awaited
154+
`Promise.all` results trace the selected value independently; arbitrary promise failure and
155+
scheduling effects are not modeled. It records CVA bases, variants,
153156
defaults, compound variants and selections; runtime selections remain symbolic. Recognized
154157
`cn`/`clsx` helpers are interpreted as data. The trusted EMCN `cn` merge convention includes
155158
the repository's custom font-size groups. A helper with an unrecognized origin is not trusted
@@ -339,6 +342,8 @@ bun --no-env-file scripts/design-diff/benchmark.ts \
339342
Fetch manifest commit objects beforehand; missing history fails explicitly. The runner verifies
340343
the frozen comparison commits and GitHub file sets. Cache identity includes engine SHA, trusted
341344
config, lockfile, runtime and comparison commits, with report-content verification before reuse.
342-
It records per-comparison elapsed time and peak RSS separately from deterministic reports.
345+
It awaits native Bun process exit status and records per-comparison elapsed time and peak RSS
346+
separately from deterministic reports. Failed runs retain bounded stderr diagnostics in a
347+
separate file, without printing source findings to logs.
343348
`/usr/bin/time` is required (macOS or Linux); source findings are not printed. Review original
344349
and holdout rates separately, and inspect every disagreement against the source label.

scripts/design-diff/benchmark.ts

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { execFileSync, spawn } from 'node:child_process'
1+
import { execFileSync } from 'node:child_process'
22
import { createHash } from 'node:crypto'
33
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
44
import path from 'node:path'
55
import { parseArgs } from 'node:util'
6+
import { runProcess } from '#design-diff/process'
67
import type { Report } from '#design-diff/types'
78

89
interface Comparison {
@@ -150,10 +151,11 @@ export async function benchmark(args = process.argv.slice(2)): Promise<void> {
150151
const started = performance.now()
151152
const env = { ...process.env }
152153
for (const key of ['DESIGN_DIFF_PR', 'DESIGN_DIFF_ENGINE_SHA', 'HEAD_SHA']) delete env[key]
153-
const timeArgs = process.platform === 'darwin' ? ['-l'] : ['-v']
154-
const proc = spawn(
155-
'/usr/bin/time',
154+
const metricsFile = `${stem}.time.txt`
155+
const timeArgs = process.platform === 'darwin' ? ['-l'] : ['-v', '-o', metricsFile]
156+
const execution = await runProcess(
156157
[
158+
'/usr/bin/time',
157159
...timeArgs,
158160
process.execPath,
159161
'--no-env-file',
@@ -165,22 +167,20 @@ export async function benchmark(args = process.argv.slice(2)): Promise<void> {
165167
'--output',
166168
reportFile,
167169
],
168-
{ cwd: engine, env, stdio: ['ignore', 'ignore', 'pipe'], detached: true }
170+
engine,
171+
env
169172
)
170-
let metrics = ''
171-
proc.stderr.on('data', (chunk: Buffer) => {
172-
if (metrics.length < 65536) metrics += chunk.toString()
173-
})
174-
const timeout = setTimeout(
175-
() => {
176-
if (proc.pid) process.kill(-proc.pid, 'SIGKILL')
177-
},
178-
15 * 60 * 1000
179-
)
180-
result.exitCode = await new Promise<number | null>((resolve, reject) => {
181-
proc.on('error', reject)
182-
proc.on('close', resolve)
183-
}).finally(() => clearTimeout(timeout))
173+
result.exitCode = execution.exitCode
174+
const metrics =
175+
process.platform === 'linux' && existsSync(metricsFile)
176+
? readFileSync(metricsFile, 'utf8')
177+
: execution.stderr
178+
if (execution.exitCode !== 0 || execution.timedOut)
179+
writeFileSync(
180+
`${stem}.stderr.txt`,
181+
execution.stderr + (execution.truncated ? '\n[stderr truncated at 65536 bytes]\n' : ''),
182+
{ mode: 0o600 }
183+
)
184184
result.seconds = Math.round((performance.now() - started) / 10) / 100
185185
const rss =
186186
process.platform === 'darwin'
@@ -189,6 +189,7 @@ export async function benchmark(args = process.argv.slice(2)): Promise<void> {
189189
result.peakMemoryBytes = rss
190190
? Number(rss[1]) * (process.platform === 'darwin' ? 1 : 1024)
191191
: null
192+
if (execution.timedOut) throw new Error('Analysis deadline exceeded')
192193
if (!existsSync(reportFile)) throw new Error('Engine did not produce a report')
193194
const bytes = readFileSync(reportFile)
194195
const report = JSON.parse(bytes.toString()) as Report

scripts/design-diff/process.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/** Native Bun subprocess contract used by the benchmark, independent of Node stream events. */
2+
interface Runtime {
3+
spawn(
4+
args: string[],
5+
options: {
6+
cwd: string
7+
env: NodeJS.ProcessEnv
8+
stdin: 'ignore'
9+
stdout: 'ignore'
10+
stderr: 'pipe'
11+
detached: true
12+
}
13+
): { pid: number; exited: Promise<number>; stderr: ReadableStream<Uint8Array> }
14+
}
15+
16+
/** Await the process exit status itself and drain bounded diagnostics separately. */
17+
export async function runProcess(
18+
args: string[],
19+
cwd: string,
20+
env: NodeJS.ProcessEnv,
21+
milliseconds = 900000
22+
) {
23+
const runtime = (globalThis as typeof globalThis & { Bun?: Runtime }).Bun
24+
if (!runtime) throw new Error('Benchmark subprocesses require Bun')
25+
const proc = runtime.spawn(args, {
26+
cwd,
27+
env,
28+
stdin: 'ignore',
29+
stdout: 'ignore',
30+
stderr: 'pipe',
31+
detached: true,
32+
})
33+
const reader = proc.stderr.getReader()
34+
const chunks: Uint8Array[] = []
35+
let bytes = 0
36+
let truncated = false
37+
const drain = (async () => {
38+
for (;;) {
39+
const { done, value } = await reader.read()
40+
if (done) break
41+
const remaining = Math.max(0, 65536 - bytes)
42+
if (value.length > remaining) truncated = true
43+
if (remaining) {
44+
chunks.push(value.subarray(0, remaining))
45+
bytes += Math.min(remaining, value.length)
46+
}
47+
}
48+
return Buffer.concat(chunks).toString('utf8')
49+
})()
50+
let timedOut = false
51+
const timeout = setTimeout(() => {
52+
timedOut = true
53+
try {
54+
process.kill(-proc.pid, 'SIGKILL')
55+
} catch (error) {
56+
if ((error as NodeJS.ErrnoException).code !== 'ESRCH') throw error
57+
}
58+
}, milliseconds)
59+
try {
60+
const [exitCode, stderr] = await Promise.all([proc.exited, drain])
61+
return { exitCode, stderr, truncated, timedOut }
62+
} finally {
63+
clearTimeout(timeout)
64+
}
65+
}

scripts/design-diff/resolve.ts

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,13 @@ export class Resolver {
332332
}
333333
return initial
334334
}
335+
if (binding?.path.isImportNamespaceSpecifier()) {
336+
const declaration = binding.path.parentPath
337+
if (declaration.isImportDeclaration()) {
338+
const target = this.tree.resolve(file, declaration.node.source.value)
339+
if (target) return this.exported(target, key, depth + 1)
340+
}
341+
}
335342
if (binding?.path.isImportSpecifier() || binding?.path.isImportDefaultSpecifier()) {
336343
const declaration = binding.path.parentPath
337344
if (declaration.isImportDeclaration()) {
@@ -348,6 +355,31 @@ export class Resolver {
348355
}
349356
}
350357
}
358+
if (path.isAwaitExpression()) {
359+
const awaited = child(path, 'argument')
360+
if (awaited.isCallExpression()) {
361+
const callee = child(awaited, 'callee')
362+
if (
363+
callee.isMemberExpression() &&
364+
!callee.node.computed &&
365+
t.isIdentifier(callee.node.object, { name: 'Promise' }) &&
366+
!callee.scope.getBinding('Promise') &&
367+
t.isIdentifier(callee.node.property, { name: 'all' })
368+
) {
369+
const array = children(awaited, 'arguments')[0]
370+
if (array?.isArrayExpression() && !array.node.elements.some(t.isSpreadElement)) {
371+
const selected = this.selected(array, key, file, depth + 1, seen)
372+
if (selected !== undefined) return { $await: selected }
373+
}
374+
}
375+
}
376+
}
377+
if (path.isArrayExpression() && /^(?:0|[1-9]\d*)$/.test(key)) {
378+
const elements = children(path, 'elements')
379+
const index = Number(key)
380+
if (index < elements.length && !elements.slice(0, index + 1).some((p) => p.isSpreadElement()))
381+
return this.value(elements[index], file, depth + 1)
382+
}
351383
if (path.isObjectExpression()) {
352384
for (const prop of children(path, 'properties').reverse()) {
353385
if (prop.isObjectMethod() && !prop.node.computed && propertyName(prop.node.key) === key)
@@ -357,7 +389,8 @@ export class Resolver {
357389
if (prop.isSpreadElement()) {
358390
const selected = this.selected(child(prop, 'argument'), key, file, depth + 1, seen)
359391
if (selected !== undefined) return selected
360-
return undefined // An unknown later spread can override an earlier property.
392+
/** An unknown later spread can override an earlier property. */
393+
return undefined
361394
}
362395
}
363396
}
@@ -465,7 +498,10 @@ export class Resolver {
465498
if (!selection) return undefined
466499
const init = child(binding, 'init')
467500
const [first, ...rest] = selection.keys
468-
let value = typeof first === 'string' ? this.selected(init, first, file, depth + 1) : undefined
501+
let value =
502+
typeof first === 'string' || typeof first === 'number'
503+
? this.selected(init, String(first), file, depth + 1)
504+
: undefined
469505
const keys = value === undefined ? selection.keys : rest
470506
if (value === undefined) value = this.value(init, file, depth + 1)
471507
for (const key of keys) {
@@ -916,8 +952,8 @@ export class Resolver {
916952
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
917953
const key = node.computed ? read('property') : propertyName(node.property)
918954
const selected =
919-
typeof key === 'string'
920-
? this.selected(child(path, 'object'), key, file, depth + 1)
955+
typeof key === 'string' || typeof key === 'number'
956+
? this.selected(child(path, 'object'), String(key), file, depth + 1)
921957
: undefined
922958
if (selected !== undefined) return selected
923959
const base = read('object')

scripts/design-diff/tests/precision.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,3 +344,42 @@ it('isolates unrelated mutable object fields such as telemetry warmup state', as
344344
)
345345
expect(report.flagged).toBe(false)
346346
})
347+
348+
it.each(['[live, unrelated]', 'await Promise.all([live, unrelated])'])(
349+
'projects an individual array result from %s',
350+
async (expression) => {
351+
const source = (colour: string, telemetry: number) =>
352+
`export const live='${colour}';export const unrelated=${telemetry}`
353+
const files = {
354+
[data]: source('red', 1),
355+
[view]: `import {live,unrelated} from './data';export async function Page(){const [colour]=${expression};return <span style={{color:colour}}/>}`,
356+
}
357+
expect((await compareFiles(files, { [data]: source('red', 2) }, settings)).flagged).toBe(false)
358+
expect((await compareFiles(files, { [data]: source('blue', 1) }, settings)).flagged).toBe(true)
359+
}
360+
)
361+
362+
it('keeps shadowed Promise.all conservative', async () => {
363+
const source = (n: number) => `export const unrelated=${n}`
364+
const report = await compareFiles(
365+
{
366+
[data]: source(1),
367+
[view]: `import {unrelated} from './data';export async function Page({Promise}){const [colour]=await Promise.all(['red',unrelated]);return <span style={{color:colour}}/>}`,
368+
},
369+
{ [data]: source(2) },
370+
settings
371+
)
372+
expect(report.flagged).toBe(true)
373+
})
374+
375+
it('projects namespace members even when an outer expression reaches its resolution limit', async () => {
376+
const source = (colour: string, unused: number) =>
377+
`export const colour='${colour}';export const unused=${unused}`
378+
const files = {
379+
[data]: source('red', 1),
380+
[view]: `import * as palette from './data';export const Page=()=> <span style={{color:unknown(unknown(unknown(palette.colour)))}}/>`,
381+
}
382+
const bounded = { ...settings, limits: { ...settings.limits, resolutionDepth: 2 } }
383+
expect((await compareFiles(files, { [data]: source('red', 2) }, bounded)).flagged).toBe(false)
384+
expect((await compareFiles(files, { [data]: source('blue', 1) }, bounded)).flagged).toBe(true)
385+
})
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { execFileSync } from 'node:child_process'
2+
import { fileURLToPath } from 'node:url'
3+
import { expect, it } from 'vitest'
4+
5+
const runner = fileURLToPath(new URL('../process.ts', import.meta.url))
6+
7+
it.each([
8+
['exit status', 'process.stderr.write("metric");process.exit(7)', 5000, 7, false, false, 6],
9+
['bounded diagnostics', 'process.stderr.write("x".repeat(100000))', 5000, 0, false, true, 65536],
10+
['deadline', 'await new Promise(resolve=>setTimeout(resolve,30000))', 50, null, true, false, 0],
11+
])(
12+
'measures native Bun subprocess %s',
13+
(_name, source, deadline, code, timedOut, truncated, bytes) => {
14+
const script = `import {runProcess} from ${JSON.stringify(runner)};const result=await runProcess([process.execPath,'--no-env-file','-e',${JSON.stringify(source)}],process.cwd(),process.env,${deadline});process.stdout.write(JSON.stringify({...result,stderr:result.stderr.length}));`
15+
const result = JSON.parse(
16+
execFileSync('bun', ['--no-env-file', '-e', script], { encoding: 'utf8', timeout: 10000 })
17+
)
18+
expect(result).toMatchObject({ timedOut, truncated, stderr: bytes })
19+
if (code === null) expect(result.exitCode).not.toBe(0)
20+
else expect(result.exitCode).toBe(code)
21+
}
22+
)

0 commit comments

Comments
 (0)