mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-10 02:30:39 +01:00
* Add a check that keeps tracker codes and internal jargon out of PR titles and branch names gstack-plain-title checks the readable front of a title, branch name, PR opening or changelog entry against the repository's .gstack/plain-titles.json; one trailing issue code stays allowed. A Plain title workflow runs it on every new pull request, and /ship's push refuses a never-pushed branch whose name fails it. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Move the PR body's machine-read lines into a closing Agent notes section The ship receipt, its runs block and the Plan line now live in one collapsed section at the end of the body, and every reader looks there first while still accepting the old receipt-at-top layout. gstack-post refuses to post a title the plain-title check refuses, and land uses the checked PR title as main's commit title. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Teach /ship to write plain titles, PR openings, commit messages and changelog entries The README now lists what gstack is for, so a PR opening can name the goal it serves. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Keep tests, size budgets and generated files in step with plain titles and Agent notes Copy lists that install result-code modules now include the plain-title codes, the ship goldens and size ledger are refreshed, the new workflow is classified, and the pinned prose tests read the new title and body layout. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Record the test-profile list change as non-behavioral for the shared test harness Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Keep text people add below the PR notes, and fix three smaller review findings The notes section now ends at its own closing tag, so a collapsible added after it survives the next post. The opening check splits the body the same way the notes reader does. Wave PR titles no longer name the area, which the plain-title check could refuse. The Capy line in the PR-body steps now puts Approvals in the notes. Review-Finding: p1/specialist-batched#1 reader=testing Review-Finding: p1/red-team#1 reader=red-team Review-Finding: p1/red-team#2 reader=red-team Review-Finding: p1/red-team#3 reader=red-team Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Mask credentials in refused-title lines and keep older PRs' plan lines readable A refused title's printed match is masked by the secret scanner first. The Plan line reader falls back to the whole body when the Agent notes section has none, so a body written before this change keeps its plan when its first receipt lands. The notes section also ends at its own closing tag when its opener is split or marked open, or when a collapsible inside it is left unclosed. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Print a fixed placeholder when a credential in a refused title cannot be masked Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Keep a plain-title policy edit from scheduling every paid eval Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Record the paid-eval list change as non-behavioral for the shared test harness Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Recheck the PR title on every edit so a base change cannot turn a refused title green Review-Finding: s11/targeted#1 reader=targeted Review-Finding: s11/adversarial-native#1 reader=adversarial-native 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>
641 lines
43 KiB
TypeScript
641 lines
43 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',
|
|
'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-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, '..');
|
|
/** gstack's own changelog_fragments.dir (.gstack/ship-policy.json): release notes the merge step folds into CHANGELOG.md, read by no paid case (GST-400). */
|
|
const CHANGELOG_FRAGMENTS_DIR = ((): string | null => {
|
|
try {
|
|
const dir = JSON.parse(fs.readFileSync(path.join(ROOT, '.gstack', 'ship-policy.json'), 'utf8'))?.changelog_fragments?.dir;
|
|
return typeof dir === 'string' && dir.trim() ? dir.replace(/^(\.\/)+/, '').replace(/\/+$/, '') : null;
|
|
} catch { return null; }
|
|
})();
|
|
|
|
/** 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 }>;
|
|
/** Narrow screens: derived files whose closure reaches every gate case, left to the train gate's full census. */
|
|
hubFiles?: string[];
|
|
/** 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.
|
|
'scripts/lib/shard-jobs.ts', // Free runner's --ci-run --jobs sub-shards; ship-measure loads it through test-free-shards.ts, so its touchfiles list it.
|
|
'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/**',
|
|
'careful/bin/hook-extract.sh', // The careful/freeze hooks' path extractor: free test/hook-scripts.test.ts covers it; no paid case reads it since the /investigate cases retired (GST-1717).
|
|
'qm/**', // The QM edition (scripts/gen-qm-edition.ts): shipped to QM as a deployment layer, read by no paid case; free test/qm-edition.test.ts covers it (GST-1492).
|
|
'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.
|
|
'USING_GBRAIN_WITH_GSTACK.md', // The /setup-gbrain guide: README, docs/gbrain-quick-tour.md and free tests read it; no skill prompt or paid fixture does (GST-1027).
|
|
'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.
|
|
'.gstack/plain-titles.json', // gstack's own plain-title policy: gstack-plain-title reads it from the repo it runs in and the Plain title check from the base branch; paid cases run in fixture repos.
|
|
'.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.
|
|
'careful/bin/mask-commit-message.awk', // Loaded only by check-careful.sh (above): covered by test/hook-scripts.test.ts.
|
|
// 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/plain-title.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/release-gate.yml', // The 8-hour release gate on main: it dispatches evals.yml on main's head; 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-scoped-tests.yml',
|
|
'.github/workflows/windows-setup-e2e.yml',
|
|
'.github/workflows/windows-smoke.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;
|
|
|
|
/**
|
|
* Full-gate inputs a narrow screen derives like any other file. No paid run executes ./setup (evals.yml only
|
|
* builds) and scripts/pr-dependencies.ts maps it to the cases that invoke it; pull_request runs, broad screens
|
|
* and --profile full still restore the full gate for it, and the train gate's full census runs every case.
|
|
*/
|
|
export const NARROW_DERIVED_FULL_GATE_FILES = ['setup'] 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))
|
|
|| (CHANGELOG_FRAGMENTS_DIR !== null && path.posix.dirname(file) === CHANGELOG_FRAGMENTS_DIR && file.endsWith('.md'))
|
|
|| (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;
|
|
/**
|
|
* narrow (screens and eval:bg:pr): a derived file whose closure reaches every gate case selects nothing by itself; those
|
|
* cases run in the train gate's full census. broad (the default here, and `ready --screen --broad`): every dependent runs.
|
|
*/
|
|
screenPolicy?: 'narrow' | 'broad';
|
|
}): 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 gateIds = Object.keys(maps.e2eTouchfiles).filter(id => maps.tiers[id] === 'gate');
|
|
const hubFiles = options.screenPolicy === 'narrow' && gateIds.length > 0
|
|
? [...consumers.keys()].filter(file => derived?.tracked.has(file) && gateIds.every(id => consumers.get(file)!.e2e.has(id))) : [];
|
|
const derivedFiles = [...consumers.keys()].filter(file => !hubFiles.includes(file));
|
|
const unknownFiles = unmapped.filter(file => !derivedGlobal.includes(file) && !consumers.has(file) && !noConsumerFiles.includes(file));
|
|
const fullGateFiles = options.screenPolicy === 'narrow'
|
|
? PR_FULL_GATE_FILES.filter(file => !(NARROW_DERIVED_FULL_GATE_FILES as readonly string[]).includes(file)) : PR_FULL_GATE_FILES;
|
|
const sharedInputs = files.filter(file => depends(file, fullGateFiles));
|
|
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) && (!derived || fs.existsSync(path.join(ROOT, 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 (hubFiles.length && !fallback && !everything) reasons.push(`Narrow screen: derived references reach every gate case only through the static closure, so these files select no case here and the train gate's full census runs them: ${hubFiles.join(', ')} (EVALS_SCREEN_POLICY=broad restores them)`);
|
|
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 })), hubFiles, 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' | 'hubFiles' | '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)`);
|
|
}
|
|
if (coverage?.hubFiles?.length) lines.push(`- Left to the train gate's full census (narrow screen): ${coverage.hubFiles.slice(0, 40).map(file => `\`${file}\``).join(', ')}`);
|
|
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'));
|
|
}
|