/** * When a scoped `testsPassed` stops counting (owner decision D11150). * * A scoped run (`tool:test-affected`, a scope-aware `tool:test` that chose the * affected packages, or a targeted `test-run:` report) proves what it ran * before merge. Once the change has merged, only merged CI (`ci:`) or a * full `tool:test` proves `testsPassed` — unless the scoped result is pinned * to a tree whose copy of every file the change touched equals the merge * commit's, so the merged change is the one it tested (T13495). A tree-bound `test-run:` also stops * counting as soon as the tree it ran on moves (T12965). This module * is the single place both rules live: `cleo done` planning and * `cleo complete` ask {@link testsPassedSupersededReason}, and they and a * scope-aware `tool:test` all judge the merge — and the PR whose CI proves * it — through {@link taskChangeMergeState} (T12959, T12656 AC2). * * @task T12656 * @task T12959 * @task T12960 * @task T12965 */ import type { ChangeSetMergeState, EvidenceAtom, TaskChangeSet } from '@cleocode/contracts'; import type { ChangeSetDeps, ChangeSetTask, PatchEquivalenceUnknown } from './change-set.js'; /** Whether the task's change has merged to the default branch. */ export type ChangeMergeState = ChangeSetMergeState; /** A merge state and, when a lookup it needed failed, which one (T12959 review). */ export interface MergeVerdict { /** Merge state of the task's latest implementation. */ state: ChangeMergeState; /** * Why a lookup failed — `gh` unreachable, or commits missing from the * local object store. With `state: 'unknown'` it is why the state is * unknown; with `state: 'merged'` and no PR named, why the PR that carries * the change is unknown. */ lookupFailed?: string; } /** * Recorded `testsPassed` evidence whose only verification results are scoped: * affected-scope tool runs and targeted `test-run:` reports (no `ci:` and no * full `tool:` result). * * @param atoms - The gate's recorded atoms. * @returns True when every result atom is scoped. * @task T12656 * @task T12965 */ export declare function isScopedOnly(atoms: ReadonlyArray): boolean; /** * The merge state a derived change set implies. What the PR lookup found * decides (`mergeState`), never `source` alone: a merged PR whose `pr:` * check was refused falls back to the task branch, which outlives a squash * merge (T12656 review HIGH). Otherwise a merged-PR change set is merged, a * failed merged-PR lookup unknown, anything else unmerged. * * @param changeSet - Derived change set. * @returns The merge state. * @task T12656 */ export declare function mergeStateOfChangeSet(changeSet: Pick): ChangeMergeState; /** * Why a scoped-only `testsPassed` no longer stands, or null when it does. * Fails closed: a scoped result whose merge state is unknown does not stand * either, since a scoped run counts before merge ONLY. * * @param atoms - Recorded `testsPassed` atoms. * @param state - Merge state of the task's change. * @param lookupFailed - With `state: 'unknown'`, why (defaults to `gh` unreachable). * @param notCarried - Why the scoped result did not carry across the merge. * @param ciRemedy - With `state: 'merged'`, the `evidence.ciSatisfies` block a * project with workflows but no opt-in should add (T13096). * @returns The reason, or null. * @task T12656 * @task T13096 */ export declare function scopedRunSupersededReason(atoms: ReadonlyArray, state: ChangeMergeState, lookupFailed?: string, notCarried?: string, ciRemedy?: string | null): string | null; /** * Why a recorded `testsPassed` resting on a tree-bound `test-run:` no longer * stands, or null when it does (T12965). A targeted report proves the tree it * ran on; once that content moved, it proves nothing about the code * being completed. A `ci:` or a full tool run in the same gate carries the * gate on its own, and atoms recorded before T12965 (no `treeHash`) are not * judged. * * @param atoms - Recorded `testsPassed` atoms. * @param current - The current tree hash of the execution root, or null when * it cannot be computed (not a git checkout, or git failed). * @param scope - The root and current change to compare over (T13784): a * moved report still stands while the change's files, the test files it * ran and their test inputs are byte-identical; without it any moved * tree refuses. * @returns The reason, or null. * @task T12965 * @task T13784 */ export declare function testRunTreeMismatchReason(atoms: ReadonlyArray, current: string | null, scope?: QaRunCarryScope): string | null; /** What an earlier-tree qa-run receipt is compared over (T13644). */ export interface QaRunCarryScope { /** The execution root both trees are compared in. */ root: string; /** The task's current changed and deleted files. */ changed: readonly string[]; } /** * Why a recorded `qaPassed` resting on a tree-bound `qa-run:` receipt no * longer stands, or null when it does (T13427): a receipt proves the tree it * ran on. A `ci:` in the gate carries it on its own; atoms with no * `treeHash` (no git checkout at verify time) are not judged. Given a carry * scope, a receipt recorded on an earlier tree carries when its checked * `roots`, the task's current changed files and the shared tool inputs are * all byte-identical at the current tree (T13644): a file the change gained * after the receipt differs, so coverage cannot silently lapse. Otherwise the * refusal names the paths that differ. * * @param atoms - Recorded `qaPassed` atoms. * @param current - The current tree hash, or null when it cannot be computed. * @param scope - The root and current change to compare over; without it any * moved tree refuses. * @returns The reason, or null. * @task T13427 * @task T13644 */ export declare function qaRunTreeMismatchReason(atoms: ReadonlyArray, current: string | null, scope?: QaRunCarryScope): string | null; /** * Whether recorded `qaPassed` atoms carry a tree-bound `qa-run:`, so callers * compute the current tree hash only when it matters. * * @param atoms - Recorded `qaPassed` atoms. * @returns True when a tree-bound qa-run is present. * @task T13427 */ export declare function hasTreeBoundQaRun(atoms: ReadonlyArray): boolean; /** * Why a test-run or qa-run no longer stands because a file it set aside as * untracked and outside the change is now tracked (T14000), or null. The tree * hash includes untracked files, so committing one moves nothing the tree * check sees: without this, a run that never covered the file would carry it * (the review of #2302 hazard). * * The tracked list is read once (`git ls-files -z`, no pathspecs, so no argv * limit and no glob matching of names like `routes/[id]/+page.svelte`) and * compared literally. It fails closed: when git cannot answer, the run is * refused as stale. * * @param atoms - Recorded gate atoms. * @param root - The bound checkout, lazily; null result when absent. * @returns The reason, or null. * @task T14000 */ export declare function setAsideJoinedReason(atoms: ReadonlyArray, root: (() => string) | undefined): string | null; /** * The one rule for whether a recorded `qaPassed` still stands — shared by * `cleo done` planning and `cleo complete`, like * {@link testsPassedSupersededReason}. A moved tree under a qa-run receipt * (T13427) is checked first; a receipt whose checked roots, current changed * files and shared tool configs are byte-identical at the current tree * carries (T13644). Then, once the change has merged, a gate resting * on qa-run receipts alone (no `ci:`, no whole-project `tool:` run) stands * only when the receipts carry across the merge byte for byte (T13495), or * were recorded on the default branch after the merge, on the current tree, * covering the change (T13644, extending the T13673 owner decision): the * stand-in holds whatever the merge's own CI concluded. * * @param atoms - Recorded `qaPassed` atoms. * @param probe - Lazy merge state and current tree hash, as for testsPassed. * @returns The reason it no longer stands, or null. * @task T13427 * @task T13644 */ export declare function qaPassedSupersededReason(atoms: ReadonlyArray, probe: Parameters[1]): Promise; /** * Whether the recorded `testsPassed` atoms carry a tree-bound `test-run:` * that {@link testRunTreeMismatchReason} would judge, so callers compute the * current tree hash only when it matters. * * @param atoms - Recorded `testsPassed` atoms. * @returns True when a tree-bound test-run is present. * @task T12965 */ export declare function hasTreeBoundTestRun(atoms: ReadonlyArray): boolean; /** * The one rule for whether a recorded `testsPassed` still stands — shared by * `cleo done` planning and `cleo complete` (T12656 AC2). A moved tree under a * targeted test-run (T12965) is checked first, then a scoped-only result * after (or with unknown) merge (D11150). Each input is resolved lazily, only * when the recorded atoms make it relevant. * * @param atoms - Recorded `testsPassed` atoms. * @param probe - Lazy merge state (with why a lookup failed, when known) and * current tree hash. * @returns The reason it no longer stands, or null. * @task T12656 * @task T12965 */ export declare function testsPassedSupersededReason(atoms: ReadonlyArray, probe: { mergeState: () => Promise | ChangeMergeState | MergedVerdict; currentTree: () => Promise | string | null; /** * Whether a commit has landed on origin's default branch (the later-main * stand-in, T13673); defaults to `isLandedOnOriginDefault`. */ landed?: (root: string, sha: string) => boolean; /** * The bound checkout, for the set-aside check (T14000): a run recorded * while a file was untracked and outside the change stops standing once * that file is tracked. Skipped when absent. */ root?: () => string; /** * The `evidence.ciSatisfies` remedy named when a merged scoped run does not * stand (T13096); defaults to `ciSatisfiesRemedy` of the bound checkout. */ ciRemedy?: () => Promise; }): Promise; /** * Whether every scoped result in `atoms` was recorded on a tree whose copy of * the result's whole scope equals the merge commit's (T13495, narrowed by * D11205 / T14071): the change's files and deleted files, plus what the result * depends on — a test-run's test files and their test inputs (root configs, * lockfile, runner configs and setup files), a qa-run's roots and shared tool * configs, a `tool:test-affected` run's tested package directories. The same * per-result scope as the earlier-tree carry (T13784). Then the merged code is * the code the run tested, so its result still stands. Fails closed — no merge * commit, no changed paths, a result with no tree pin, a capped test-file list, * or a git failure each say why the result does not carry; a differing scope * names its first changed file and the routes that still prove the change. * * @param atoms - Recorded `testsPassed` atoms, or `qaPassed` qa-run receipts (T13644). * @param changeSet - The merged change set (its merge commit, files and root). * @returns `ok`, or why the scoped result does not carry across the merge. * @task T13495 */ export declare function scopedRunCarriedAcrossMerge(atoms: ReadonlyArray, changeSet: Pick | null): { ok: true; } | { ok: false; why: string; }; /** * Whether a targeted `test-run:` recorded on the default branch after the * change merged stands in for merged CI that concluded without proving the * change — red, or its required jobs skipped (T13623). The run tested the * merged code as it is now, so it speaks for the change when CI on the merge * commit cannot. Fails closed, and only `test-run:` reports qualify, because * only they name the test files they ran: * - the merge commit is known, and the change lists files; * - every report carries the HEAD and tree it was bound to, that HEAD * descends from the merge commit AND has landed on origin's default * branch (a branch that merged main and added its own fix proves nothing * about main, T13627), and the tree is still the current one (a report * that predates a later edit of any file is refused); * - the reports together cover the change's own files the way a * pre-merge report must cover a branch ({@link testRunCoversChange}), and * ran every test file the change added or edited that still exists. * * @param atoms - Recorded `testsPassed` atoms, or `qaPassed` atoms with `gate`. * @param changeSet - The merged change set (its merge commit, files and root). * @param current - The current tree hash of that root, or null. * @param landed - Whether a commit has landed on origin's default branch * (`isLandedOnOriginDefault`; passed in so this module keeps loading * change-set lazily). * @param gate - `qaPassed` judges `qa-run:` receipts instead (T13644): a * typecheck and a lint, each bound like a test-run, whose roots together * cover the change by the verify-time coverage rule ({@link qaRunCoverage}). * @returns `ok`, or why the recorded run does not stand in. * Since T13673 (owner decision 2026-10-13) the same stand-in applies to every * merged change, whatever its CI concluded: {@link testsPassedSupersededReason} * asks it whenever byte-identity carry fails, so `cleo done --plan` and * `cleo complete` accept it alike. * * @task T13623 * @task T13627 * @task T13673 * @task T13644 */ export declare function scopedRunStandsAfterRedCi(atoms: ReadonlyArray, changeSet: (Pick & Partial>) | null, current: string | null, landed: (root: string, sha: string) => boolean, gate?: 'testsPassed' | 'qaPassed'): { ok: true; } | { ok: false; why: string; }; /** * A merge verdict, with the merged change set when the caller derived one * ({@link TaskMergeInfo} carries it), so a scoped result can be compared with * the merge commit (T13495). */ export type MergedVerdict = MergeVerdict & { changeSet?: TaskChangeSet | null; landing?: LandedCarryTarget | null; }; /** * What a scoped result is compared with when the change landed on the default * branch with no merge commit (fast-forward or direct push, T13608): the newest * landed implementing commit, and every file the implementing commits changed. */ export type LandedCarryTarget = Pick; /** * The carry target of commits that reached the default branch themselves. * Fails closed: no single newest commit, or a git failure, gives `null`. * * @param root - Repository the commits live in. * @param commits - The task's implementing commits, all landed. * @param contains - Whether `ancestor` is contained in `descendant`. * @returns The newest commit and the files the commits changed, or null. * @task T13608 */ export declare function landedCarryTarget(root: string, commits: readonly string[], contains: (root: string, ancestor: string, descendant: string) => boolean): LandedCarryTarget | null; /** Git and `gh` probes {@link taskChangeMergeState} uses; injectable for tests. */ export interface MergeProbeDeps { /** * Change-set I/O for the merged-PR lookup (tests inject `gh`). Its `viewPr` * also lists a PR's own commits for the containment check, and its * `listPrsForCommit` finds the PRs GitHub associates with a commit. */ changeSet?: ChangeSetDeps; /** A change set already derived for this task (`cleo done` planning), so it is not derived twice. */ derived?: TaskChangeSet; /** * Repository the implementation commits live in; defaults to the task's * change-set root (its worktree when one is registered, T12965 review). */ executionRoot?: string; /** * Whether `sha` is on origin's default branch; defaults to * `git merge-base --is-ancestor`. A positive signal only: a local * `origin/` can be stale, and a squash or rebase merge never makes * the recorded SHA an ancestor. */ isLanded?: (root: string, sha: string) => boolean; /** Whether `ancestor` is contained in `descendant`; defaults to `git merge-base --is-ancestor`. */ contains?: (root: string, ancestor: string, descendant: string) => boolean; /** * Whether `sha`'s patch is among `candidates`: null when git failed, * `{ missing }` when commits it needs are not local; defaults to `git patch-id`. */ equivalent?: (root: string, sha: string, candidates: readonly string[]) => boolean | null | PatchEquivalenceUnknown; } /** Where a task's change stands, and the PR whose CI would prove it. */ export interface TaskMergeInfo extends MergeVerdict { /** The derived change set, when one was derived. */ changeSet: TaskChangeSet | null; /** * The merged PR that carries the latest implementation (`` or * `@`), or null when none is known. Never a PR that merely * cites the task: its CI must have run the recorded commits. */ prRef: string | null; /** * With `state: 'merged'`, no {@link prRef} and no `lookupFailed`: why no * merged PR is named — every lookup answered, and none carries the change. */ unproven?: string; /** * The carry target when no derived change set gives one: the commits landed * with no merge commit (T13608), or a recorded `pr:` atom named the merge * (T13634). */ landing?: LandedCarryTarget | null; } /** * The carry target of a merge commit: the commit and every file it changed * against its first parent, which for a squash or merge commit is exactly the * PR's change (T13636). Fails closed: an unknown commit, a root commit, a git * failure or an empty diff gives null. * * @param root - Repository the merge commit lives in. * @param merge - The merge (or squash) commit. * @returns The commit and the files it changed and deleted, or null. * @task T13636 * @task T13637 */ export declare function mergeCarryTarget(root: string, merge: string | null | undefined): LandedCarryTarget | null; /** * Whether a task's LATEST implementation has merged, and through which PR — * the one function `cleo done` planning, `cleo complete` and a scope-aware * `tool:test` all ask (T12656 AC2). * * With recorded `commit:` atoms, the commits decide, never "any merged PR * that cites the task": the PR named is one that carries every commit (see * {@link carriesAll}) — a recorded `pr:` atom first (newest), then the PR the * change-set derivation found, then a merged PR into the default branch that * GitHub associates with the commits. Landing on the local `origin/` * is a positive signal only (merged, but no PR proves it); its absence proves * nothing, since the ref may be stale and squash and rebase merges rewrite * SHAs. With no sign of a merge, the state is unmerged — or unknown when a * lookup it needed failed. A landed change with no carrier and a failed lookup * names that lookup (`lookupFailed`): the carrier is unknown, which is not * "no PR carries it" (`unproven`). Without commit atoms, the newest recorded * `pr:` atom, else the derivation, decides. * * @param task - The task whose change is examined. * @param storeRoot - CLEO store root. * @param deps - Change-set I/O and git/`gh` probes. * @returns The merge state, the PR, the change set when one was derived, and * any lookup that failed. * @task T12959 * @task T12960 */ export declare function taskChangeMergeState(task: ChangeSetTask, storeRoot: string, deps?: MergeProbeDeps): Promise; //# sourceMappingURL=affected-scope.d.ts.map