Files
gstack/scripts/test-pr-profile.ts
T
Garry Tanandcapy-ai[bot] b3e94a3f8d v1.91.279.0 eval:select exemptions and ETHOS.md registration (#3499), below-floor review rulings (#3504, #3485), Windows curation, GSTA-121 follow-ups (#3515)
* Eval selection exemptions, review-floor rulings and the GSTA-121 follow-ups

Release metadata (NOTICE.md, LICENSE, licenses/**) no longer restores the
full paid gate and ETHOS.md is registered under the two prompts that read
it (#3499). docs/review-policy.md records the six finding classes #3485
and #3504 ruled below the floor and the review army's lead judgment
applies the table; the floor itself is unchanged. The Windows curation
moves test/pregate-remote-stamp-only.test.ts to KNOWN_WINDOWS_INCOMPATIBLE
(#3485). From GSTA-121: Blast radius in docs/skills.md and the merge-ask
template, the 11-digit run-id exemption from pii.phone.e164 after a run
word, relative string literals as import-graph edges, and a ubi-runner
pack that never tars the live .git.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes

Red Team pass 1 (all below the floor): an 11-digit span with a leading 1
after a run word is a US or Chinese national number, not a run id, and
reports again; ubi-runner pack removes its temporary repository when it
fails and replays branch.* with remote.* so a branch's upstream resolves
on the VM; the conservative-refusal example in docs/review-policy.md
reads in the refusing direction; the touchfiles header names why ETHOS.md
sits only under the two prompts that quote it. Step 6's selection found
captain/templates/merge-ask.md as the one unknown dependency of this PR
and registered captain/templates/** in FREE_ONLY_PR_FILES (#3499).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes (pass 2)

The cmd_pack EXIT trap bakes the temp path in: a set -e failure unwinds
the function before the trap runs and a local $tmp is unset there, so
the old form leaked the snapshot. The leak test creates TMPDIR where the
pack reads it and asserts the failure is the missing index. CHANGELOG
examples no longer spell a full national number.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: adversarial review fixes

The pack snapshot carries the index's staged blobs as a pack of their own,
so a VM write-tree (gstack-wtree) can read a staged file the way the old
.git tar allowed; git init runs with --template= and gc.auto=0 and a
valueless remote/branch key replays as true. scripts/harness-version.json
re-pins scripts/test-pr-profile.ts after the captain/templates/** entry.
The run-id exemption reads the same way in the scanner and gstack-post:
an 11-digit span after a run word is a run id (the leading-1 exclusion
from the Red Team pass contradicted gstack-post's tested contract and the
ruling that a run id is not a phone number), with the residual named in
the comment, the docs and the CHANGELOG. The #3499 pins move to their own
test so the boundary test stays under its default budget. Docs audit:
TESTING_INTERNALS names RELEASE_METADATA_FILES and captain/templates/**,
docs/captain.md names Blast radius.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes (pass 4)

The pack's staged-blob list comes from the copied index, holds only blobs
staged against HEAD (not every tracked blob), skips gitlinks whose commit
the superproject lacks, and dies with its own message when pack-objects
fails. The live-checkout test covers a gitlink entry and a valueless
remote key replayed as true. CHANGELOG names the staged-blob carry.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes (pass 5)

The pack's blob list is the copied index's blobs that HEAD's tree lacks,
so an intent-to-add entry's empty blob and the stages of an unmerged entry
travel too (the HEAD-only diff list dropped both); gitlinks still skip.
The live-checkout test adds an intent-to-add entry, which the VM fsck
assertion covers.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-10-27 17:20:55 +00:00

605 lines
39 KiB
TypeScript

/** Fast PR policy. Cadence changes here never remove cases from the broad census. */
import * as fs from 'node:fs';
import * as path from 'node:path';
import { E2E_TOUCHFILES, E2E_TIERS, GLOBAL_TOUCHFILES, LLM_JUDGE_TOUCHFILES } from '../test/helpers/touchfiles-data';
import { matchGlob, TOUCHFILES_DATA_PATH } from '../test/helpers/test-selection';
import { isPaidTestFile } from '../test/helpers/paid-test-set';
import { FREE_FIXTURES } from '../test/helpers/free-fixtures-data';
import {
checkoutExercisedConsumers, derivedDependencies, EXERCISED_MAP_PATH, type DerivedDependencies, type ExercisedConsumers,
} from './pr-dependencies';
/** Existing short behavioral probes; intersect with changed-input selection. */
export const PR_PROFILE_CASE_IDS = [
'hermetic-canary', 'hermetic-sentinel',
'browse-basic', 'browse-snapshot', 'skillmd-setup-discovery',
'qa-bootstrap', 'review-sql-injection', 'review-gate-integrity', 'review-coverage-audit',
'qa-functional-cli-report', 'qa-functional-webhook-report',
'qa-functional-cli-fix', 'qa-functional-webhook-fix',
'review-exploratory-small-cli', 'ship-exploratory-small-cli', 'ship-exploratory-unavailable',
'ship-exploratory-plan-checks', 'ship-exploratory-late-input',
'plan-eng-coverage-audit', 'plan-review-report',
'auq-format-gate', 'plan-design-review-no-ui-scope',
'tpa-present', 'tpa-absent-linux',
'ship-local-workflow', 'ship-coverage-audit', 'docsync-spawned',
'ship-docsync-completion', 'ship-docsync-current', 'ship-docsync-failure', 'ship-docsync-store',
'ship-docsync-missing-marker', 'ship-docsync-missing-asset', 'ship-docsync-launch-failure',
'ship-docsync-timeout-unsettled', 'ship-docsync-late-result', 'ship-docsync-stale-before',
'ship-docsync-stale-after', 'ship-docsync-recovery',
'ship-managed-hook-refresh', 'ship-unmanaged-hook-consent', 'ship-local-hook-preservation',
'setup-deploy-workflow', 'context-restore-loads-latest', 'plan-tune-inspect',
'skillify-provenance-refusal', 'diagram-triplet', 'learnings-show',
'gstack-upgrade-happy-path',
'investigate-owned-completion', 'investigate-owned-abort', 'investigate-owned-ending-error',
'ship-coverage-value', 'review-test-value', 'test-audit-report-only',
'office-hours-auto-mode', 'plan-ceo-review-plan-mode',
] as const;
/** Audited ownership: unknown/direct-describe files remain broad coverage. */
export const PR_PROFILE_FILES: Record<string, readonly string[]> = {
'test/skill-e2e-office-hours-auto-mode.test.ts': ['office-hours-auto-mode'],
'test/skill-e2e-plan-ceo-plan-mode.test.ts': ['plan-ceo-review-plan-mode'],
'test/skill-e2e-investigate-owned-completion.test.ts': ['investigate-owned-completion'],
'test/skill-e2e-investigate-owned-termination.test.ts': ['investigate-owned-abort', 'investigate-owned-ending-error'],
'test/skill-e2e-hermetic-canary.test.ts': ['hermetic-canary', 'hermetic-sentinel'],
'test/skill-e2e-bws.test.ts': ['browse-basic', 'browse-snapshot', 'skillmd-setup-discovery'],
'test/skill-e2e-qa-workflow.test.ts': ['qa-bootstrap'],
'test/skill-e2e-qa-functional.test.ts': ['qa-functional-cli-report', 'qa-functional-webhook-report'],
'test/skill-e2e-qa-functional-fix.test.ts': ['qa-functional-cli-fix', 'qa-functional-webhook-fix'],
'test/skill-e2e-qa-callers.test.ts': ['review-exploratory-small-cli', 'ship-exploratory-small-cli', 'ship-exploratory-unavailable', 'ship-exploratory-plan-checks', 'ship-exploratory-late-input'],
'test/skill-e2e-review.test.ts': ['review-sql-injection'],
'test/skill-e2e-gate-integrity.test.ts': ['review-gate-integrity'],
'test/skill-e2e-coverage-audit.test.ts': ['review-coverage-audit', 'plan-eng-coverage-audit'],
'test/skill-e2e-test-value.test.ts': ['ship-coverage-value', 'review-test-value', 'test-audit-report-only'],
'test/skill-e2e-plan.test.ts': ['plan-review-report'],
'test/skill-e2e-ask-user-question-format-compliance.test.ts': ['auq-format-gate'],
'test/skill-e2e-design.test.ts': ['plan-design-review-no-ui-scope'],
'test/skill-e2e-third-party-actions.test.ts': ['tpa-present', 'tpa-absent-linux'],
'test/skill-e2e-workflow.test.ts': ['ship-local-workflow', 'ship-coverage-audit', 'gstack-upgrade-happy-path'],
'test/skill-e2e-ship-hook-refresh.test.ts': ['ship-managed-hook-refresh'],
'test/skill-e2e-ship-hook-consent.test.ts': ['ship-unmanaged-hook-consent', 'ship-local-hook-preservation'],
'test/skill-e2e-docsync-spawned.test.ts': ['docsync-spawned'],
'test/skill-e2e-ship-docsync.test.ts': ['ship-docsync-completion', 'ship-docsync-current', 'ship-docsync-failure', 'ship-docsync-store', 'ship-docsync-missing-marker', 'ship-docsync-missing-asset', 'ship-docsync-launch-failure', 'ship-docsync-timeout-unsettled', 'ship-docsync-late-result', 'ship-docsync-stale-before', 'ship-docsync-stale-after', 'ship-docsync-recovery'],
'test/skill-e2e-deploy.test.ts': ['setup-deploy-workflow'],
'test/skill-e2e-session-intelligence.test.ts': ['context-restore-loads-latest'],
'test/skill-e2e-plan-tune.test.ts': ['plan-tune-inspect'],
'test/skill-e2e-skillify.test.ts': ['skillify-provenance-refusal'],
'test/skill-e2e-diagram.test.ts': ['diagram-triplet'],
'test/skill-e2e-learnings.test.ts': ['learnings-show'],
};
const ROOT = path.resolve(import.meta.dir, '..');
/** The one paid test file whose source registers a case (its touchfiles name exactly one), else null. */
export function caseOwnerFile(id: string, maps: Pick<PrProfileMaps, 'e2eTouchfiles'> = PR_PROFILE_MAPS): string | null {
const owners = (maps.e2eTouchfiles[id] ?? []).filter(dep => /^test\/[^/]+\.test\.ts$/.test(dep) && isPaidTestFile(dep));
return owners.length === 1 ? owners[0]! : null;
}
/**
* DX-11: a gate case outside the audited profile can run in the PR lane only
* when the runner can select it by name: its owner registers a Bun test whose
* name is the case id (testIfSelected / testConcurrentIfSelected / test with
* the literal id), which is what the PR lane's case-name pattern matches.
*/
export function caseNameAddressable(id: string, source: string): boolean {
const quoted = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const code = source.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
return new RegExp(`(?:^|[\\s;{(])(?:test(?:Concurrent)?IfSelected|test(?:\\.concurrent)?)\\(\\s*(['"\`])${quoted}\\1`, 'm').test(code);
}
const sourceCache = new Map<string, string>();
const readRepoSource = (file: string): string => {
if (!sourceCache.has(file)) {
try { sourceCache.set(file, fs.readFileSync(path.join(ROOT, file), 'utf8')); } catch { sourceCache.set(file, ''); }
}
return sourceCache.get(file)!;
};
/** Selectable outside the profile: a gate case with one owning paid file that names it as a test. */
export function prProfileDirectCase(id: string, maps: PrProfileMaps = PR_PROFILE_MAPS, readSource = readRepoSource): string | null {
if (maps.tiers[id] !== 'gate') return null;
const owner = caseOwnerFile(id, maps);
return owner && caseNameAddressable(id, readSource(owner)) ? owner : null;
}
/** Whether a manifest may carry this case in the PR lane: audited profile, or a recorded name-addressable direct case. */
export function prProfileCaseAllowed(id: string, directCases: readonly string[] = []): boolean {
return (PR_PROFILE_CASE_IDS as readonly string[]).includes(id) || (directCases.includes(id) && prProfileDirectCase(id) !== null);
}
/** File -> PR-lane case ids: the audited map plus the selected direct cases each file owns (DX-11). */
export function prProfileFileMap(selected: readonly string[] | null): Record<string, readonly string[]> {
const map: Record<string, string[]> = Object.fromEntries(Object.entries(PR_PROFILE_FILES).map(([file, ids]) => [file, [...ids]]));
for (const id of selected ?? []) {
if ((PR_PROFILE_CASE_IDS as readonly string[]).includes(id)) continue;
const owner = prProfileDirectCase(id);
if (owner) (map[owner] ??= []).push(id);
}
return map;
}
/** The PR-lane case ids one test file owns under `selected`. */
export function prProfileFileCases(file: string, selected: readonly string[] | null): readonly string[] {
return prProfileFileMap(selected)[file] ?? [];
}
export interface PrProfileMaps {
e2eTouchfiles: Record<string, string[]>;
judgeTouchfiles: Record<string, string[]>;
tiers: Record<string, 'gate' | 'periodic' | 'marathon'>;
globalTouchfiles: readonly string[];
}
export const PR_PROFILE_MAPS: PrProfileMaps = {
e2eTouchfiles: E2E_TOUCHFILES, judgeTouchfiles: LLM_JUDGE_TOUCHFILES,
tiers: E2E_TIERS, globalTouchfiles: GLOBAL_TOUCHFILES,
};
export interface PrProfileSelection {
/**
* pr: the fast profile on mapped inputs; dependents: every gate case whose
* derived reference closure holds a changed file (no profile cut);
* full-fallback: a global build/runtime input or an underivable file.
*/
mode: 'pr' | 'dependents' | 'full-fallback';
e2e: string[];
judges: string[];
deferred: Array<{ id: string; tier: 'gate' | 'periodic' | 'marathon'; reason: string }>;
unknownFiles: string[];
/** One label and fix per unknown file (CEO-13/DX-6). */
unknownFileLabels: Array<{ file: string; label: string; fix: string }>;
/** DX-11: gate cases outside the profile selected because their own paid test file changed. */
directCases: string[];
/** Unmapped changed files placed by derived references (scripts/pr-dependencies.ts), with their consumer counts. */
derivedFiles: Array<{ file: string; e2e: number; judges: number }>;
/** Unmapped changed files under derivable directories that no paid case's reference closure reaches. */
noConsumerFiles: string[];
deferredPromptFiles: string[];
missingCoverage: string[];
needsFullValidation: boolean;
reasons: string[];
/** What the exercised-path map would select (shadow mode: never changes e2e or judges). */
shadow?: ShadowSelection;
}
/** The exercised-path map's selection beside the real one (docs/TESTING_INTERNALS.md#exercised-path-map). */
export interface ShadowSelection {
/** Gate cases the map would select. */
e2e: string[];
/** Selected gate cases the map would not select. */
wouldDrop: string[];
/** Gate cases the map would select that today's selection does not. */
wouldAdd: string[];
/** Per changed file with exercised consumers: how many gate cases it reaches, and through which edge. */
provenance: Array<{ file: string; summary: string }>;
/** Mean known cost of the dropped cases minus the added ones (the map's per-case costUsd; an unpriced case counts $0): a lower bound. */
predictedSavingUsd: number;
unpricedCases: number;
/** Share of observed case runs whose trials carry no known cost (PTY trials); their spend is missing from every dollar figure. */
unknownCostShare: number;
/** Why no shadow could be computed (the real selection is unaffected). */
error?: string;
}
/** Reject stale profile registrations and deferred cases with no scheduled home. */
export function validatePrProfileInventory(
maps: PrProfileMaps = PR_PROFILE_MAPS,
profile: readonly string[] = PR_PROFILE_CASE_IDS,
): void {
if (new Set(profile).size !== profile.length) throw new Error('Duplicate PR profile case');
for (const id of profile) {
if (!Object.hasOwn(maps.e2eTouchfiles, id) || maps.tiers[id] !== 'gate') {
throw new Error(`PR profile case must exist in the broad gate census: ${id}`);
}
}
for (const id of Object.keys(maps.e2eTouchfiles)) {
if (maps.tiers[id] !== 'gate' && maps.tiers[id] !== 'periodic' && maps.tiers[id] !== 'marathon') {
throw new Error(`E2E case has no broad gate/periodic/marathon census: ${id}`);
}
}
}
function expandSelection(selected: readonly string[] | null, inventory: Record<string, unknown>): string[] {
const ids = [...new Set(selected ?? Object.keys(inventory))].sort();
for (const id of ids) {
if (!Object.hasOwn(inventory, id)) throw new Error(`Unregistered selected case: ${id}`);
}
return ids;
}
function matches(file: string, patterns: readonly string[]): boolean {
return patterns.some(pattern => matchGlob(file, pattern));
}
/** Workflows that define or feed the paid eval lanes; every other workflow is free-lane only. */
export const PAID_WORKFLOW_FILES = [
'.github/workflows/evals.yml',
'.github/workflows/evals-periodic.yml',
'.github/workflows/evals-marathon.yml',
] as const;
/**
* Files no paid case consumes. Entries may be globs. Scheduler inputs change
* packing and derived job timeouts, never which cases run or how they are
* judged; the listed workflows never run a paid case (test/free-fixtures.test.ts
* fails when a workflow is in neither this list nor PAID_WORKFLOW_FILES).
*/
export const FREE_ONLY_PR_FILES = [
'scripts/test-free-shards.ts',
'scripts/lib/free-home-guard.ts', // Imported only by the free shard runner.
'test/helpers/auq-parallel-worker.ts',
'test/helpers/stored-zip.ts', // Read only by the free test-health report tests.
'scripts/free-test-durations.json',
'scripts/free-test-seed-allowlist.json', // Read only by the free seed ratchet (test/free-seed-ratchet.test.ts).
'scripts/paid-test-durations.json',
'scripts/ubicloud/**',
'capy/skill/**', // The vendored Capy skill (Drive bootstrapper): run by Capy agents, read by no paid case; test/capy-skill-wrapper.test.ts covers it.
'captain/templates/**', // The /captain crew templates (CREW.md, merge-ask.md, slot-call.md, …): copied into a Captain's drive by people and Captains, read by no paid case; free test/captain-skill.test.ts covers them (#3499).
'tsconfig.test.json', // Read only by `tsc -p tsconfig.test.json` (typecheck:test); Bun's runtime reads tsconfig.json.
'.gitignore', // Changes which untracked files git reports, never tracked content a case reads.
'.gstack/generated.json', // gstack's own regen registry: gstack-regen/gstack-pregate read it from origin/<base> of the repo they run in; paid cases run in fixture repos.
'.gstack/pregate.json', // gstack's own pre-gate policy: same trust boundary, same reason.
'.gstack/ship-policy.json', // gstack's own ship policy: gstack-ship-policy/gstack-restamp read it from origin/<base> of the repo they run in; same reason.
'.gstack/ship-policy.example.json', // The example beside it: only .gitignore and docs/ship-policy.md name it.
'.github/dependabot.yml', // Dependabot's own config: read by GitHub, and by free test/compare-pr-version.test.ts; dependabot runs skip the paid lane.
'.osv-scanner.toml', // Read by .github/workflows/osv-scanner.yml and the free OSV tests (test/osv-*.test.ts).
'setup.ps1', // The Windows installer: run only by .github/workflows/windows-setup-e2e.yml; the paid lane is Linux.
'.gitlab-ci.yml', // gstack's own GitLab config: skills probe the fixture repo's .gitlab-ci.yml, never this one.
'.github/ISSUE_TEMPLATE/**', // Issue templates: read by GitHub and free test/gstack-doctor.test.ts.
'scripts/retired-command.ts', // One-release stubs for retired package scripts; no paid case imports it.
'careful/bin/check-careful.sh', // /careful PreToolUse hook: covered by test/hook-scripts.test.ts; no paid fixture copies or runs it.
// Reporting and launch tools: they read CI history or start a lane, never run inside a paid case.
'scripts/test-health-report.ts',
'scripts/bump-harness-version.ts',
'scripts/eval-bg.ts',
'.github/workflows/actionlint.yml',
'.github/workflows/arm-setup-smoke.yml',
'.github/workflows/ci-image.yml',
'.github/workflows/cso-runtime-images.yml',
'.github/workflows/cso-runtime-promote.yml',
'.github/workflows/cso-runtime-qualification.yml',
'.github/workflows/cso-scanner-images.yml',
'.github/workflows/dependency-review.yml',
'.github/workflows/free-durations-record.yml',
'.github/workflows/free-tests.yml',
'.github/workflows/make-pdf-gate.yml',
'.github/workflows/model-policy-freshness.yml',
'.github/workflows/measure-journal-read.yml',
'.github/workflows/native-qualification.yml',
'.github/workflows/osv-scanner.yml',
'.github/workflows/platform-qualification.yml',
'.github/workflows/pr-title-sync.yml',
'.github/workflows/quality-gate.yml',
'.github/workflows/scorecard.yml',
'.github/workflows/skill-docs.yml',
'.github/workflows/test-health.yml',
'.github/workflows/eval-sweep.yml', // The weekly off-ship sweep on main; no PR-gate case reads it, and it never gates a PR.
'.github/workflows/evals-main-backstop.yml', // The merge-train backstop on main: it dispatches evals.yml after a push to main; no PR-gate case reads it, and it never gates a PR.
'.github/workflows/weekly-release.yml', // The Monday release cut (tag, stable, GitHub Release): reads check runs on main; no paid case reads it, and it never gates a PR.
'scripts/release-cut.ts', // The release cut's logic, run only by weekly-release.yml; covered by the free test/release-cut.test.ts.
'.github/actionlint.yaml', // Runner labels for actionlint only.
'scripts/ship-measure-sweep.ts', // The weekly sweep CLI; it launches paid cases, never runs inside one.
'.github/workflows/version-gate.yml',
'.github/workflows/windows-free-tests.yml',
'.github/workflows/windows-pregate-e2e.yml',
'.github/workflows/windows-setup-e2e.yml',
] as const;
/**
* Release metadata no paid case reads: the third-party notices and licence
* texts, and the root LICENSE. A courtesy entry or a licence line never
* widens the PR gate (#3499); CHANGELOG.md and VERSION, which the train stamp
* rewrites on every slot, are already exempt in knownNonBehaviorFile.
*/
export const RELEASE_METADATA_FILES = ['NOTICE.md', 'LICENSE', 'licenses/**'] as const;
/**
* Inputs every paid case consumes that must always restore the full gate
* (fail closed, even when a touchfile or a derived reference also names
* them): dependencies and the CI image, setup and the build, the PR
* workflow and its setup actions, the Bun test preload and compiler config,
* line-ending policy, the skill generator with the preamble every skill
* embeds, and the eval policy every case is planned under.
*/
export const PR_FULL_GATE_FILES = [
'package.json', 'bun.lock', '.github/docker/Dockerfile.ci',
'scripts/host-config.ts', 'scripts/discover-skills.ts', 'hosts/index.ts',
'setup', 'scripts/build.sh', '.github/workflows/evals.yml', '.github/actions/**',
'test-setup.ts', 'bunfig.toml', 'tsconfig.json', '.gitattributes',
'scripts/gen-skill-docs.ts', 'scripts/resolvers/index.ts', 'scripts/resolvers/types.ts',
'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble/**', 'scripts/resolvers/runtime-root.ts',
'test/helpers/periodic-exclude-data.ts',
] as const;
/** Periodic- and marathon-lane workflows: the PR gate never runs a case under them; their branch dispatch validates them. */
export const PERIODIC_ONLY_PR_FILES = ['.github/workflows/evals-periodic.yml', '.github/workflows/evals-marathon.yml'] as const;
/** Directories the reference derivation covers: an unmapped file there with no consumer is consumed by no paid case. */
export const DERIVABLE_PREFIXES = ['bin/', 'lib/', 'scripts/', 'browse/', 'design/', 'make-pdf/', 'hosts/', 'extension/', 'model-overlays/', 'agents/', 'test/helpers/', 'capy/'] as const;
export function knownNonBehaviorFile(file: string): boolean {
// A mapped dependency still wins over these exemptions. New helper,
// runtime, dependency, or paid-workflow files are deliberately not exempted.
return /^(?:docs\/|(?:README|CONTRIBUTING|ARCHITECTURE|CHANGELOG|TODOS)\.md$|VERSION$)/.test(file)
// Hermetic skill views exclude checkout instructions; these are maintained
// by free doc/generation checks and are not copied into paid fixtures.
|| ['AGENTS.md', 'CLAUDE.md', 'agents-digest/gstack-AGENTS.md'].includes(file)
|| FREE_ONLY_PR_FILES.some(pattern => matchGlob(file, pattern))
|| RELEASE_METADATA_FILES.some(pattern => matchGlob(file, pattern))
|| (PERIODIC_ONLY_PR_FILES as readonly string[]).includes(file)
|| (file.startsWith('test/fixtures/') && Object.keys(FREE_FIXTURES).some(pattern => matchGlob(file, pattern)))
// Free test files anywhere (test/, browse/test/, design/test/, make-pdf/test/): no paid case runs or reads a free test.
|| (/\.test\.tsx?$/.test(file) && !isPaidTestFile(file));
}
export const FALLBACK_FIX_ANCHOR = 'docs/TESTING_INTERNALS.md#pr-paid-lane-fallback';
export const EXERCISED_MAP_ANCHOR = 'docs/TESTING_INTERNALS.md#exercised-path-map';
/** Why one changed file restored the full gate, and the edit that narrows it next time (DX-6). */
export function unknownFileLabel(file: string): { label: string; fix: string } {
if (matches(file, PR_FULL_GATE_FILES)) {
return { label: 'full-gate input', fix: `${file} is in PR_FULL_GATE_FILES (scripts/test-pr-profile.ts): every paid case consumes it, so the full gate is intended and no mapping narrows it` };
}
if (isPaidTestFile(file) || file.startsWith('test/helpers/')) {
return { label: 'needs touchfile entry', fix: `register ${file} under the cases that consume it in test/helpers/touchfiles-data.ts` };
}
if (file.startsWith('test/fixtures/')) {
return { label: 'needs touchfile entry', fix: `register ${file} in the touchfiles of the paid cases that read it, or add it with its free consumers to FREE_FIXTURES in test/helpers/free-fixtures-data.ts` };
}
if (/^\.github\/workflows\/[^/]+\.ya?ml$/.test(file) && !(PAID_WORKFLOW_FILES as readonly string[]).includes(file)) {
return { label: 'add to FREE_ONLY_PR_FILES', fix: `add ${file} to FREE_ONLY_PR_FILES in scripts/test-pr-profile.ts` };
}
return { label: 'real unknown dependency', fix: `register ${file} under the cases that consume it in test/helpers/touchfiles-data.ts, add it to FREE_ONLY_PR_FILES in scripts/test-pr-profile.ts if no paid case reads it, or add its directory to DERIVABLE_PREFIXES so scripts/pr-dependencies.ts derives its consumers` };
}
/** Only the release version may be ignored; dependency/script changes still matter. */
export function packageChangeOnlyVersion(before: string, after: string): boolean {
try {
const old = JSON.parse(before), current = JSON.parse(after);
if (typeof old?.version !== 'string' || typeof current?.version !== 'string') return false;
delete old.version; delete current.version;
return JSON.stringify(old) === JSON.stringify(current);
} catch { return false; }
}
function isPromptFile(file: string): boolean {
if (FREE_ONLY_PR_FILES.some(pattern => matchGlob(file, pattern))) return false;
return file.endsWith('.tmpl') || /(?:^|\/)SKILL\.md$/.test(file)
|| /^[^/]+\/sections\/.+\.md$/.test(file);
}
/**
* Diff selection → fast case intersection → explicit deferred coverage.
* Unknown dependencies restore the full gate, while periodic work stays visible.
* A changed prompt without a relevant retained check requires full validation.
*/
export function selectPrProfile(options: {
selectedE2E: readonly string[] | null;
selectedJudges: readonly string[] | null;
changedFiles: readonly string[];
maps?: PrProfileMaps;
profile?: readonly string[];
/** Generated artifacts may inherit the identity of their verified source template. */
sourceAliases?: Readonly<Record<string, string>>;
/** Test-file source reader for the DX-11 addressability check (defaults to the checkout). */
readSource?: (file: string) => string;
/** Derived references; default: derived from the checkout for the real maps, none for fixture maps. */
derived?: DerivedDependencies | null;
/** Exercised-path consumers for the shadow block; default: the checkout's committed map for the real maps, none for fixture maps. */
exercised?: ExercisedConsumers | null;
}): PrProfileSelection {
const maps = options.maps ?? PR_PROFILE_MAPS;
const profile = options.profile ?? PR_PROFILE_CASE_IDS;
validatePrProfileInventory(maps, profile);
const selectedE2E = expandSelection(options.selectedE2E, maps.e2eTouchfiles);
const selectedJudges = expandSelection(options.selectedJudges, maps.judgeTouchfiles);
const files = [...new Set(options.changedFiles.map(file => file.replace(/\\/g, '/')))].sort();
const depends = (file: string, patterns: readonly string[]) => matches(file, patterns)
|| (!!options.sourceAliases?.[file] && matches(options.sourceAliases[file], patterns));
const dependencyPatterns = [...new Set([
...Object.values(maps.e2eTouchfiles).flat(), ...Object.values(maps.judgeTouchfiles).flat(),
...maps.globalTouchfiles,
])];
const unmapped = files.filter(file => file !== TOUCHFILES_DATA_PATH && file !== EXERCISED_MAP_PATH
&& !depends(file, dependencyPatterns) && !knownNonBehaviorFile(file));
// Unmapped files are placed by derived references where the derivation covers them; the rest restore the full gate.
const derived = options.derived !== undefined ? options.derived : maps === PR_PROFILE_MAPS ? derivedDependencies(maps, ROOT) : null;
// A path absent from the head tree was deleted by the diff: its consumers are the live files that still reference it.
const consumers = new Map<string, { e2e: Set<string>; judges: Set<string> }>();
const derivedGlobal: string[] = [];
const noConsumerFiles: string[] = [];
const deletedLabels = new Map<string, { label: string; fix: string }>();
for (const file of derived ? unmapped : []) {
if (derived!.tracked.has(file)) {
if (derived!.global.has(file)) { derivedGlobal.push(file); continue; }
const found = { e2e: derived!.e2e.get(file) ?? new Set<string>(), judges: derived!.judges.get(file) ?? new Set<string>() };
if (found.e2e.size + found.judges.size > 0) consumers.set(file, found);
else if (DERIVABLE_PREFIXES.some(prefix => file.startsWith(prefix))) noConsumerFiles.push(file);
continue;
}
const referencers = derived!.referencers(file);
const fullGate = referencers.find(referencer => matches(referencer, PR_FULL_GATE_FILES));
if (fullGate) {
deletedLabels.set(file, { label: 'deleted but still referenced', fix: `${file} is deleted but ${fullGate} (a full-gate input) still references it; remove the reference or restore the file` });
continue;
}
if (referencers.some(referencer => derived!.global.has(referencer))) { derivedGlobal.push(file); continue; }
const found = { e2e: new Set(referencers.flatMap(referencer => [...derived!.e2e.get(referencer) ?? []])),
judges: new Set(referencers.flatMap(referencer => [...derived!.judges.get(referencer) ?? []])) };
if (found.e2e.size + found.judges.size > 0) consumers.set(file, found);
else noConsumerFiles.push(file);
}
// A failed import scan leaves some closure unknown: every unmapped file restores the full gate (fail closed).
const scanFailed = derived && unmapped.length ? [...derived.scanErrors.keys()].sort() : [];
if (scanFailed.length) {
consumers.clear(); derivedGlobal.length = 0; noConsumerFiles.length = 0;
for (const file of unmapped) deletedLabels.set(file, { label: 'import scan failed', fix: `scripts/pr-dependencies.ts could not scan the imports of ${scanFailed.slice(0, 3).join(', ')}${scanFailed.length > 3 ? ` and ${scanFailed.length - 3} more` : ''}; fix the parse error so ${file}'s consumers can be derived` });
}
const derivedFiles = [...consumers.keys()];
const unknownFiles = unmapped.filter(file => !derivedGlobal.includes(file) && !consumers.has(file) && !noConsumerFiles.includes(file));
const sharedInputs = files.filter(file => depends(file, PR_FULL_GATE_FILES));
const fallback = unknownFiles.length > 0 || sharedInputs.length > 0;
const dependentsMode = !fallback && derivedFiles.length > 0;
const derivedE2E = derivedFiles.flatMap(file => [...consumers.get(file)!.e2e]);
const derivedJudges = derivedFiles.flatMap(file => [...consumers.get(file)!.judges]);
// A file in the global touchfiles' import closure is as broad as a global touchfile.
const everything = derivedGlobal.length > 0;
const candidates = fallback || everything ? Object.keys(maps.e2eTouchfiles).sort() : [...new Set([...selectedE2E, ...derivedE2E])].sort();
// DX-11: an edited paid test file runs its own gate cases even outside the profile, when the runner can select them by name.
const editedPaid = new Set(files.filter(file => isPaidTestFile(file)));
const owned = candidates.filter(id => maps.tiers[id] === 'gate' && !profile.includes(id) && editedPaid.has(caseOwnerFile(id, maps) ?? ''));
const directCases = fallback || dependentsMode ? [] : owned.filter(id => prProfileDirectCase(id, maps, options.readSource) !== null);
const unaddressed = fallback || dependentsMode ? [] : owned.filter(id => !directCases.includes(id));
const e2e = candidates.filter(id => maps.tiers[id] === 'gate' && (fallback || dependentsMode || profile.includes(id) || directCases.includes(id)));
const judges = fallback || everything ? Object.keys(maps.judgeTouchfiles).sort() : [...new Set([...selectedJudges, ...derivedJudges])].sort();
const kept = new Set(e2e);
const deferred = candidates.filter(id => !kept.has(id)).map(id => ({
id, tier: maps.tiers[id],
reason: maps.tiers[id] === 'marathon'
? 'Full end-to-end marathon coverage; non-blocking lane, not executed by the PR gate'
: maps.tiers[id] === 'periodic'
? 'Broad periodic/release coverage; not executed by the PR gate'
: unaddressed.includes(id)
? `Its test file changed, but the PR lane cannot select it by name; name its Bun test '${id}' (testIfSelected) to run it on PRs`
: 'Broad gate census/release coverage; outside the fast PR profile',
}));
const noQuickCoverage = files.filter(file => isPromptFile(file)
&& !(depends(file, maps.globalTouchfiles) && (e2e.length > 0 || judges.length > 0))
&& !e2e.some(id => depends(file, maps.e2eTouchfiles[id]))
&& !judges.some(id => depends(file, maps.judgeTouchfiles[id])));
const hasQuickDependency = (file: string) => profile.some(id => depends(file, maps.e2eTouchfiles[id]))
|| Object.values(maps.judgeTouchfiles).some(patterns => depends(file, patterns));
const deferredPromptFiles = noQuickCoverage.filter(file => !hasQuickDependency(file)
&& Object.values(maps.e2eTouchfiles).some(patterns => depends(file, patterns)));
const missingCoverage = noQuickCoverage.filter(file => !deferredPromptFiles.includes(file));
const reasons: string[] = [];
if (unknownFiles.length) reasons.push(`Unknown dependencies restore every gate case and judge: ${unknownFiles.map(file => `${file} (${(deletedLabels.get(file) ?? unknownFileLabel(file)).label})`).join(', ')}`);
if (sharedInputs.length) reasons.push(`Shared runtime/build inputs restore every gate case and judge: ${sharedInputs.join(', ')}`);
if (dependentsMode) reasons.push(`Derived references select every dependent gate case: ${derivedFiles.map(file => `${file} (${consumers.get(file)!.e2e.size} cases)`).join(', ')}`);
else if (!fallback) reasons.push('Changed-input selection intersected with the fast PR profile; selected judges retained');
if (files.includes(EXERCISED_MAP_PATH)) reasons.push(`${EXERCISED_MAP_PATH} is shadow-only data (it changes no selection yet), so it selects no paid case (${EXERCISED_MAP_ANCHOR})`);
if (derivedGlobal.length) reasons.push(`Paid-runner inputs (global touchfile import closure) select every case: ${derivedGlobal.join(', ')}`);
if (noConsumerFiles.length) reasons.push(`No paid case's reference closure reaches (or, deleted, still references): ${noConsumerFiles.join(', ')}`);
if (directCases.length) reasons.push(`Edited paid test files select their own gate cases: ${directCases.join(', ')}`);
if (deferred.length) reasons.push(`${deferred.length} selected behaviors remain scheduled/release coverage, not PR passes`);
if (deferredPromptFiles.length) reasons.push(`No quick live coverage; known broad prompt checks deferred: ${deferredPromptFiles.join(', ')}`);
if (missingCoverage.length) reasons.push(`Full validation required for prompts without a relevant PR check: ${missingCoverage.join(', ')}`);
const exercised = options.exercised !== undefined ? options.exercised : maps === PR_PROFILE_MAPS ? checkoutExercised() : null;
const shadow = exercised instanceof Error ? emptyShadow(e2e, exercised.message)
: exercised ? shadowSelection({ ...options, maps, profile }, { e2e, fallback, derived, exercised, files }) : undefined;
return {
mode: fallback ? 'full-fallback' : dependentsMode ? 'dependents' : 'pr', e2e, judges, deferred,
unknownFiles, unknownFileLabels: unknownFiles.map(file => ({ file, ...(deletedLabels.get(file) ?? unknownFileLabel(file)) })), directCases,
derivedFiles: derivedFiles.map(file => ({ file, e2e: consumers.get(file)!.e2e.size, judges: consumers.get(file)!.judges.size })), noConsumerFiles,
deferredPromptFiles, missingCoverage, needsFullValidation: missingCoverage.length > 0, reasons, ...(shadow ? { shadow } : {}),
};
}
function checkoutExercised(): ExercisedConsumers | Error {
try { return checkoutExercisedConsumers(PR_PROFILE_MAPS, ROOT, PR_FULL_GATE_FILES); }
catch (error) { return error instanceof Error ? error : new Error(String(error)); }
}
function emptyShadow(e2e: string[], error: string): ShadowSelection {
return { e2e, wouldDrop: [], wouldAdd: [], provenance: [], predictedSavingUsd: 0, unpricedCases: 0, unknownCostShare: 0, error };
}
/**
* Shadow selection (variant cu of the measured prototype): the same policy
* with the exercised map in place of the derived closure, unioned with the
* exercised consumers of every changed file that is not global or full-gate.
* A full-gate fallback stays full.
*/
function shadowSelection(options: Parameters<typeof selectPrProfile>[0] & { maps: PrProfileMaps },
today: { e2e: string[]; fallback: boolean; derived: DerivedDependencies | null; exercised: ExercisedConsumers; files: string[] }): ShadowSelection {
const { maps } = options;
const { exercised } = today;
const observedCases = Object.values(exercised.map?.cases ?? {});
const observations = observedCases.reduce((sum, entry) => sum + entry.observations, 0);
const unknownCostShare = observations ? Math.round(1000 * (1 - observedCases.reduce((sum, entry) => sum + (entry.costRuns ?? 0), 0) / observations)) / 1000 : 0;
if (today.fallback) return { e2e: today.e2e, wouldDrop: [], wouldAdd: [], provenance: [], predictedSavingUsd: 0, unpricedCases: 0, unknownCostShare };
const gate = (ids: Iterable<string>) => [...ids].filter(id => maps.tiers[id] === 'gate');
const reach = new Map<string, string[]>();
const inner = selectPrProfile({ ...options, derived: today.derived ? { ...today.derived, e2e: exercised.e2e } : null, exercised: null });
const sources = [...new Set([...today.files, ...Object.values(options.sourceAliases ?? {})])];
const shadowSet = new Set(inner.e2e);
for (const file of sources) {
if (today.derived?.global.has(file) || matches(file, PR_FULL_GATE_FILES) || knownNonBehaviorFile(file)) continue;
const via = !today.derived || today.derived.tracked.has(file) ? [file] : today.derived.referencers(file);
const ids = gate(new Set(via.flatMap(source => [...exercised.e2e.get(source) ?? []])));
if (ids.length) reach.set(file, ids);
for (const id of ids) shadowSet.add(id);
}
const e2e = [...shadowSet].sort();
const wouldDrop = today.e2e.filter(id => !shadowSet.has(id));
const wouldAdd = e2e.filter(id => !today.e2e.includes(id));
const cost = (ids: string[]) => ids.reduce((sum, id) => sum + (exercised.map?.cases[id]?.costUsd ?? 0), 0);
const provenance = [...reach].map(([file, ids]) => {
const groups = new Map<string, number>();
for (const id of ids) {
const label = exercised.closure(id).get(file) ?? 'referencer';
groups.set(label, (groups.get(label) ?? 0) + 1);
}
return { file, summary: `selects ${ids.length} case(s): ${[...groups].sort((a, b) => b[1] - a[1]).slice(0, 6).map(([label, n]) => `${label} (${n})`).join(', ')}` };
});
return { e2e, wouldDrop, wouldAdd, provenance, predictedSavingUsd: Math.round((cost(wouldDrop) - cost(wouldAdd)) * 100) / 100,
unpricedCases: [...wouldDrop, ...wouldAdd].filter(id => exercised.map?.cases[id]?.costUsd === undefined).length, unknownCostShare };
}
/** The manifest fields the coverage summary reads (a PaidRunManifest subset). */
export interface CoverageSummaryInput {
profile?: string;
selection?: { e2e: string[] | null; judges: string[] | null };
prCoverage?: Pick<PrProfileSelection, 'mode' | 'deferred' | 'unknownFiles'> & Partial<Pick<PrProfileSelection, 'unknownFileLabels' | 'derivedFiles' | 'noConsumerFiles' | 'shadow'>>;
}
/**
* Job-summary block for one PR plan (CEO-13): the profile mode, selected case
* counts, reused records once the report has them, and on fallback every
* file that restored the full gate with its fix.
*/
export function formatPrCoverageSummary(manifest: CoverageSummaryInput, totals?: { total: number; reused: number }): string[] {
const coverage = manifest.prCoverage;
const count = (ids: string[] | null | undefined) => ids === null || ids === undefined ? 'all' : String(ids.length);
const lines = [
'### PR paid lane coverage',
`- Mode: \`${coverage?.mode ?? manifest.profile ?? 'full'}\``,
`- Selected: ${count(manifest.selection?.e2e)} E2E case(s), ${count(manifest.selection?.judges)} judge(s); ${coverage?.deferred.length ?? 0} deferred to scheduled/release coverage`,
];
if (totals) lines.push(`- Reused: ${totals.reused} of ${totals.total} rule/judge record(s) came from verified receipts`);
if (coverage?.derivedFiles?.length) {
lines.push(`- Derived dependents (scripts/pr-dependencies.ts) of ${coverage.derivedFiles.length} unmapped file(s):`);
for (const item of coverage.derivedFiles.slice(0, 40)) lines.push(` - \`${item.file}\`: ${item.e2e} case(s), ${item.judges} judge(s)`);
}
const shadow = coverage?.shadow;
if (shadow) lines.push(shadow.error ? `- Shadow exercised-path map: unavailable (${shadow.error}); selection unaffected`
: `- Shadow exercised-path map (not active): would select ${shadow.e2e.length} gate case(s), dropping ${shadow.wouldDrop.length} and adding ${shadow.wouldAdd.length}; predicted saving $${shadow.predictedSavingUsd.toFixed(2)}${shadow.unpricedCases ? ` (${shadow.unpricedCases} unpriced)` : ''}, a lower bound: known cost excludes the ${Math.round(100 * (shadow.unknownCostShare ?? 0))}% of observed case runs with unknown cost (PTY) (${EXERCISED_MAP_ANCHOR})`);
if (coverage?.noConsumerFiles?.length) lines.push(`- No paid case reaches: ${coverage.noConsumerFiles.slice(0, 40).map(file => `\`${file}\``).join(', ')}`);
if (coverage?.mode === 'full-fallback' && coverage.unknownFiles.length) {
lines.push(`- Full gate restored by ${coverage.unknownFiles.length} file(s) (fix: ${FALLBACK_FIX_ANCHOR}):`);
for (const file of coverage.unknownFiles) {
const labeled = coverage.unknownFileLabels?.find(entry => entry.file === file) ?? { file, ...unknownFileLabel(file) };
lines.push(` - \`${file}\` (${labeled.label}): ${labeled.fix}`);
}
}
return lines;
}
if (import.meta.main) {
const [command, manifestPath, outcomesPath] = process.argv.slice(2);
if (command !== 'summary' || !manifestPath) {
console.error('usage: bun run scripts/test-pr-profile.ts summary <manifest.json> [collector-outcomes.json]');
process.exit(2);
}
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as CoverageSummaryInput;
let totals: { total: number; reused: number } | undefined;
if (outcomesPath) {
try {
const parsed = JSON.parse(fs.readFileSync(outcomesPath, 'utf8'));
if (Number.isSafeInteger(parsed?.totals?.total) && Number.isSafeInteger(parsed?.totals?.reused)) totals = parsed.totals;
} catch { /* No verified report: the summary omits the reuse line. */ }
}
console.log(formatPrCoverageSummary(manifest, totals).join('\n'));
}