mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-02 09:29:49 +02:00
Main shipped v1.91.9.0, so this wave becomes v1.91.10.0. Conflicts kept this branch's planner-derived assertions. Main's new test-value eval gets rule kinds for its three gate cases and its measured 332 s duration from #2998's CI; census counts and the PR fallback floor follow the new file. The packing test now weighs each tier's recorded durations the way the planner does.
219 lines
13 KiB
TypeScript
219 lines
13 KiB
TypeScript
/** Fast PR policy. Cadence changes here never remove cases from the broad census. */
|
|
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';
|
|
|
|
/** 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-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-ceo-review-benefits', 'plan-eng-coverage-audit', 'plan-review-report',
|
|
'auq-format-gate', 'plan-design-review-no-ui-scope', 'office-hours-spec-review',
|
|
'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',
|
|
] as const;
|
|
|
|
/** Audited ownership: unknown/direct-describe files remain broad coverage. */
|
|
export const PR_PROFILE_FILES: Record<string, readonly string[]> = {
|
|
'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-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-ceo-review-benefits', 'plan-review-report', 'office-hours-spec-review'],
|
|
'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'],
|
|
};
|
|
|
|
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 {
|
|
mode: 'pr' | 'full-fallback';
|
|
e2e: string[];
|
|
judges: string[];
|
|
deferred: Array<{ id: string; tier: 'gate' | 'periodic' | 'marathon'; reason: string }>;
|
|
unknownFiles: string[];
|
|
deferredPromptFiles: string[];
|
|
missingCoverage: string[];
|
|
needsFullValidation: boolean;
|
|
reasons: 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));
|
|
}
|
|
|
|
export const FREE_ONLY_PR_FILES = [
|
|
'scripts/test-free-shards.ts',
|
|
'test/helpers/auq-parallel-worker.ts',
|
|
// Read only by free tests (context-budget ratchet, host-config goldens), never by a paid case.
|
|
'test/fixtures/context-budget.json',
|
|
'test/fixtures/golden/claude-ship-SKILL.md',
|
|
'test/fixtures/golden/codex-ship-SKILL.md',
|
|
'test/fixtures/golden/factory-ship-SKILL.md',
|
|
] as const;
|
|
|
|
const FULL_GATE_PR_FILES = [
|
|
'package.json', 'bun.lock', '.github/docker/Dockerfile.ci',
|
|
'scripts/host-config.ts', 'scripts/discover-skills.ts', 'hosts/index.ts',
|
|
] as const;
|
|
|
|
function knownNonBehaviorFile(file: string): boolean {
|
|
// A mapped dependency still wins over these exemptions. New helper/fixture,
|
|
// runtime, dependency, or 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(freeOnly => freeOnly === file)
|
|
|| (file.startsWith('test/') && /\.test\.tsx?$/.test(file) && !isPaidTestFile(file));
|
|
}
|
|
|
|
/** 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 {
|
|
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>>;
|
|
}): 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 unknownFiles = files.filter(file => file !== TOUCHFILES_DATA_PATH
|
|
&& !depends(file, dependencyPatterns) && !knownNonBehaviorFile(file));
|
|
const sharedInputs = files.filter(file => depends(file, FULL_GATE_PR_FILES));
|
|
const fallback = unknownFiles.length > 0 || sharedInputs.length > 0;
|
|
const candidates = fallback ? Object.keys(maps.e2eTouchfiles).sort() : selectedE2E;
|
|
const e2e = candidates.filter(id => maps.tiers[id] === 'gate' && (fallback || profile.includes(id)));
|
|
const judges = fallback ? Object.keys(maps.judgeTouchfiles).sort() : selectedJudges;
|
|
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'
|
|
: '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.join(', ')}`);
|
|
if (sharedInputs.length) reasons.push(`Shared runtime/build inputs restore every gate case and judge: ${sharedInputs.join(', ')}`);
|
|
if (!fallback) reasons.push('Changed-input selection intersected with the fast PR profile; selected judges retained');
|
|
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(', ')}`);
|
|
return {
|
|
mode: fallback ? 'full-fallback' : 'pr', e2e, judges, deferred,
|
|
unknownFiles, deferredPromptFiles, missingCoverage, needsFullValidation: missingCoverage.length > 0, reasons,
|
|
};
|
|
}
|