mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-02 17:40:02 +02:00
refactor(evals): share the import-closure walker and add the E2E shard reuse identity
sourceDependencyClosure moves from the workflow-judge adapter into scripts/eval-input-cache.ts unchanged, so judge keys stay byte-identical. scripts/e2e-shard-reuse.ts builds the consumed-input identity of one PR-lane E2E shard (test import closure, every registered case's touchfiles, globals, runner/workflow/setup actions, child env pins, CI image, Claude CLI) and fails closed on anything unknown. Marathon joins the always-fresh purposes.
This commit is contained in:
1 parent
f29d2ee015
commit
acb7bc02b1
3 files changed
+233
-38
No files matched your search
@@ -16,9 +16,50 @@
|
||||
*/
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { isBuiltin } from 'node:module';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { atomicWriteSync } from '../lib/fs-atomic';
|
||||
|
||||
/**
|
||||
* Follow literal module imports from `entries` (repo-relative) without
|
||||
* executing them, including installed package bytes and the package.json that
|
||||
* governs each resolved module; root bunfig/tsconfig/jsconfig are included
|
||||
* when present. The root package.json is left to the caller, which hashes its
|
||||
* semantic fields without the release version label.
|
||||
*/
|
||||
export function sourceDependencyClosure(root: string, entries: string[]): string[] {
|
||||
const seen = new Set<string>();
|
||||
const scan = new Bun.Transpiler({ loader: 'tsx' });
|
||||
const visit = (file: string) => {
|
||||
file = path.resolve(file);
|
||||
const relative = path.relative(root, file).split(path.sep).join('/');
|
||||
if (relative.startsWith('../') || path.isAbsolute(relative)) throw new Error('Dependency outside checkout');
|
||||
if (relative === 'package.json') return;
|
||||
if (seen.has(relative)) return;
|
||||
seen.add(relative);
|
||||
const source = fs.readFileSync(file, 'utf8');
|
||||
if (!/\.[cm]?[jt]sx?$/.test(file)) return;
|
||||
// Entrypoint scripts carry hashbangs, which scanImports does not accept.
|
||||
// Strip only for parsing; buildEvalInputIdentity still hashes the full file.
|
||||
for (const entry of scan.scanImports(source.replace(/^#![^\n]*(?:\n|$)/, '\n'))) {
|
||||
if (isBuiltin(entry.path) || entry.path.startsWith('bun:')) continue;
|
||||
const resolved = Bun.resolveSync(entry.path, path.dirname(file));
|
||||
visit(resolved);
|
||||
// Package export maps/defaults affect resolution independently of code.
|
||||
let directory = path.dirname(resolved);
|
||||
while (directory !== root && directory.startsWith(root + path.sep)) {
|
||||
const manifest = path.join(directory, 'package.json');
|
||||
if (fs.existsSync(manifest)) { visit(manifest); break; }
|
||||
directory = path.dirname(directory);
|
||||
}
|
||||
}
|
||||
};
|
||||
for (const file of entries) visit(path.join(root, file));
|
||||
for (const file of ['bunfig.toml', 'tsconfig.json', 'jsconfig.json'])
|
||||
if (fs.existsSync(path.join(root, file))) visit(path.join(root, file));
|
||||
return [...seen].sort();
|
||||
}
|
||||
|
||||
export const EVAL_CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
||||
export const EVAL_CACHE_RESULT_MAX_BYTES = 16 * 1024;
|
||||
const SCHEMA = 1;
|
||||
@@ -49,7 +90,7 @@ export type EvalInputIdentityResult =
|
||||
| { status: 'eligible'; identity: EvalInputIdentity }
|
||||
| { status: 'ineligible'; reason: string };
|
||||
export interface EvalCachePolicy {
|
||||
purpose: 'gate' | 'periodic' | 'release';
|
||||
purpose: 'gate' | 'periodic' | 'release' | 'marathon';
|
||||
fresh?: boolean;
|
||||
now?: number;
|
||||
maxAgeMs?: number;
|
||||
@@ -146,7 +187,7 @@ function validIdentity(value: unknown): value is EvalInputIdentity {
|
||||
&& canonical(value.caseIds) === canonical(sorted(value.caseIds));
|
||||
}
|
||||
function bypass(policy: EvalCachePolicy): string | null {
|
||||
if (policy.purpose !== 'gate') return 'Periodic and release validation must execute fresh';
|
||||
if (policy.purpose !== 'gate') return 'Periodic, marathon and release validation must execute fresh';
|
||||
if (policy.fresh) return 'Fresh validation requested';
|
||||
if (!positive(policy.now ?? Date.now()) || !positive(policy.maxAgeMs ?? EVAL_CACHE_MAX_AGE_MS)) return 'Invalid cache age policy';
|
||||
return null;
|
||||
|
||||
Reference in new issue
Block a user