/** * `pr:` evidence-atom validator. * * Resolves merge identity, checks and changed-file provenance via the `gh` * CLI. The task evidence service separately validates task scope and criteria; * this resolver does not establish implementation, testing or review. * Closes the release-verb dogfood gap (T9764): tasks that ship via the * standard PR + admin-merge flow previously had no zero-friction way to record * evidence retroactively — * `tool:test` re-runs the entire monorepo suite and `note:` is rejected for * hard gates on critical verifications. * * A PR atom counts as evidence when: * 1. `state === 'MERGED'` — the PR was actually shipped to main. * 2. `mergedAt` is non-null — defends against API races. * 3. Every required-workflow check has `conclusion === 'SUCCESS'` (or * `'SKIPPED'`) — there are zero `FAILURE` checks among the * required workflows configured by branch protection. * * Required-workflow list precedence (gh#1192 / T12104): * 1. `CLEO_PR_REQUIRED_WORKFLOWS` env var (operator override). * 2. `.cleo/project-context.json` `release.prRequiredWorkflows` (an explicit * empty array means NO required workflows — gh#1104). * 3. The TARGET repo's branch-protection required status checks, queried * via `gh api repos/{owner}/{repo}/branches/{branch}/protection/ * required_status_checks` and cached for 1h. * 4. {@link PR_REQUIRED_WORKFLOWS} built-in default (cleocode's own gates). * * Results are cached under `/.cleo/cache/evidence/pr--.json`, * keyed on `(repo, prNumber, mergedAt)` so re-verifies skip the network round * trip and two repositories' PR #N never collide (T13912). * The branch-protection tier is cached separately at * `/.cleo/cache/evidence/pr-required-checks-.json` (1h TTL, per repository). * * @task T9764 * @task T12104 * @epic T9762 * @saga T9758 */ import type { GhPrViewPayload } from '@cleocode/contracts'; import type { EvidenceRoots } from '../tasks/evidence.js'; /** * Result of resolving a `pr:` atom. * * On success carries the data needed to materialise an `EvidenceAtom` of * `kind: 'pr'`. On failure carries a human-readable reason plus the same * `codeName` vocabulary used by the rest of the evidence pipeline so error * presentation stays uniform. * * @task T9764 */ export type PrAtomResolution = { ok: true; prNumber: number; mergeCommitSha: string; mergedAt: string; successCount: number; totalChecks: number; cacheHit: boolean; /** PR title and body are historical assertions used for explicit task linkage. */ title: string; body: string; headRefName: string; /** * Final PR head commit, when known (T12634: its checks count for `ci:` * when its tree equals the merge commit's). Absent on cache entries * captured before T12634. */ headRefOid?: string; /** Complete changed-file inventory; count mismatch is reported by contextual validation. */ changedPaths: string[]; changedFileCount: number; /** * Why the changed-file inventory could not be completed, when paginating * the PR files API failed (T12358). Present only alongside an incomplete * `changedPaths`; contextual validation refuses such a result. */ changedFilesError?: string; } | { ok: false; reason: string; codeName: 'E_EVIDENCE_INVALID' | 'E_EVIDENCE_INSUFFICIENT' | 'E_EVIDENCE_TESTS_FAILED' | 'E_EVIDENCE_TOOL_FAILED' | 'E_EVIDENCE_GIT_ROOT'; }; /** * The repository a PR number belongs to, for the PR cache (T13912): the * execution root's `origin` as a lowercased `owner/repo` slug, else * `path:` for a checkout with no GitHub * origin. Two repositories' PR #N never share a cache entry. * * @param executionRoot - The checkout `gh` runs in. * @returns The repository identity. * @task T13912 */ export declare function prRepoIdentity(executionRoot: string): string; /** * The `owner/repo` slug of a checkout's `origin` remote on github.com (https, * `ssh://` or `git@github.com:` form), lowercased, or null when there is none * or it is another host. Ambient GIT_* is ignored. * * @param root - The checkout. * @task T13912 */ export declare function originRepoSlug(root: string): string | null; /** * Filesystem location for the cached PR validation result, per repository * (T13912): `/.cleo/cache/evidence/pr--.json`. * Entries written before T13912 (`pr-.json`, no repository) are never * read, so they are refetched, never trusted. * * @param projectRoot - The store root holding the cache. * @param prNumber - The PR number. * @param repo - {@link prRepoIdentity} of the checkout the PR belongs to. * @task T9764 * @task T13912 */ export declare function prCacheEntryPath(projectRoot: string, prNumber: number, repo: string): string; /** * Function signature for fetching a GitHub PR's metadata. * * Returning `null` signals a fetch failure (binary missing, auth error, * malformed JSON). Returning a payload that fails {@link ghPrViewSchema} * also yields a `E_EVIDENCE_TOOL_FAILED` outcome. * * A failure MAY carry `E_EVIDENCE_GIT_ROOT` instead, which is not a tool * failure at all: the working directory is not a git work tree, so `gh` could * never have discovered the repository it was asked about (gh#1462). * * @task T9764 */ export type FetchGhPrPayload = (prNumber: number, cwd: string, /** * CLEO store root, for failure text only (T12308). Optional so existing * injected fetchers — which never shell out and never report a layout * failure — keep their two-parameter shape. */ storeRoot?: string) => Promise<{ ok: true; payload: unknown; } | { ok: false; reason: string; codeName?: 'E_EVIDENCE_GIT_ROOT'; }>; /** * Default `gh pr view` invocation. Calls `gh pr view --json * state,mergedAt,mergeable,headRefOid,statusCheckRollup` via `execFileSync` * for parity with the rest of the release module's `gh` usage. * * @task T9764 */ export declare const defaultFetchGhPrPayload: FetchGhPrPayload; /** * Page size requested from GitHub's PR files API — its documented maximum. * * `gh pr view --json files` returns at most this many entries, which is why a * PR with more changed files needs the REST endpoint paginated (T12358). * * @task T12358 */ export declare const PR_FILES_PAGE_SIZE = 100; /** * Upper bound on pages read from the PR files API. GitHub serves at most * 3000 files for one PR (30 pages of {@link PR_FILES_PAGE_SIZE}); a PR beyond * that cannot be fully inventoried and stays refused as incomplete. * * @task T12358 */ export declare const PR_FILES_MAX_PAGES = 30; /** * Function signature for fetching ONE page of a PR's changed files. * * `page` is 1-based. A successful page returns the changed paths it holds; a * page shorter than {@link PR_FILES_PAGE_SIZE} is the last one. A failure * carries a human-readable reason and leaves the inventory incomplete. * * @task T12358 */ export type FetchGhPrFilesPage = (prNumber: number, page: number, cwd: string) => Promise<{ ok: true; paths: string[]; } | { ok: false; reason: string; }>; /** * Default {@link FetchGhPrFilesPage}: `gh api * repos/{owner}/{repo}/pulls//files?per_page=100&page=`, run in * `cwd` so `gh` resolves the repository the same way `gh pr view` does. * * @task T12358 */ export declare const defaultFetchGhPrFilesPage: FetchGhPrFilesPage; /** * Read a PR's COMPLETE changed-file inventory by paginating the files API. * * Stops at the first short page, once `expectedCount` paths are collected, or * after {@link PR_FILES_MAX_PAGES}. The result is `ok` only when exactly * `expectedCount` distinct paths were retrieved; a page error or a short total * returns `ok: false` with the paths gathered so far, so the caller keeps the * inventory visibly incomplete instead of treating a partial list as the diff. * * @param prNumber - PR number. * @param expectedCount - GitHub's reported `changedFiles` count. * @param cwd - Working directory for the page fetcher. * @param fetchPage - Page fetcher; defaults to {@link defaultFetchGhPrFilesPage}. * @returns The full path list, or the partial list plus the failure reason. * * @task T12358 */ export declare function collectPrChangedFiles(prNumber: number, expectedCount: number, cwd: string, fetchPage?: FetchGhPrFilesPage): Promise<{ ok: true; paths: string[]; } | { ok: false; paths: string[]; reason: string; }>; /** The app (slug or id) and workflow file a required check must come from. */ export interface RequiredCheckPinSpec { /** GitHub App slug or numeric id. */ readonly app?: string | number; /** Workflow file path, for workflow-run checks. */ readonly workflow?: string; } /** * Pins declared by object entries of `release.prRequiredWorkflows` * (`{ name, app?, workflow? }`), keyed by check name (T12634). * * @param projectContext - Parsed `.cleo/project-context.json`, or null. * @returns Name → pin, for entries that declare an app or workflow. * @task T12634 */ export declare function readRequiredCheckPins(projectContext?: Record | null): Record; /** * Resolve the list of required-workflow names for the current project. * * SYNC tiers only (most specific wins): * 1. `CLEO_PR_REQUIRED_WORKFLOWS` env var (comma-separated, or a JSON array; * `[]` means none, T13744) — CI/operator override. * 2. `.cleo/project-context.json` `release.prRequiredWorkflows` * ({@link PR_REQUIRED_WORKFLOWS_CONTEXT_KEY}) — committed per-project config. * An explicit empty array means NO required workflows (gh#1104). * 3. {@link PR_REQUIRED_WORKFLOWS} default (the cleocode contract-repo gates). * * The branch-protection tier (gh#1192) requires a `gh api` round trip and * therefore lives in the async {@link resolveRequiredWorkflowsDetailed}; this * pure function is kept for callers that cannot await. * * @throws Error when the env var starts with `[` but is not a JSON array of * named entries (T13744): an unreadable list is never an empty one. * @task T9764 * @task T12014 (gh#1104) * @task T12104 (gh#1192) */ export declare function resolveRequiredWorkflows(env?: NodeJS.ProcessEnv, projectContext?: Record | null): string[]; /** * Which tier produced the required-workflow list. Surfaced in rejection * messages (gh#1198) so the reader can tell immediately whether the list is * their own configuration or a CLEO-side assumption. * * @task T12104 (gh#1192, gh#1198) */ export type RequiredWorkflowsSource = { readonly tier: 'env'; } | { readonly tier: 'project-context'; } | { readonly tier: 'branch-protection'; readonly repo: string; readonly branch: string; } | { readonly tier: 'default'; } /** * No tier could name this repository's required checks (gh#1323). * * This is a DISTINCT outcome from "no checks are required". The previous * behaviour fell through to {@link PR_REQUIRED_WORKFLOWS} — *cleocode's own* * gate names — and evaluated a foreign repository against them, producing a * confident refusal sourced from the wrong project. A surface that takes a * target must be able to say it cannot answer. */ | { readonly tier: 'unknown'; readonly reason: string; }; /** * The resolved required-workflow list plus the tier that produced it. * * @task T12104 (gh#1192, gh#1198) */ export interface RequiredWorkflowsResolution { readonly workflows: string[]; readonly source: RequiredWorkflowsSource; /** * App pins from branch protection's `checks[].app_id` (T12634), keyed by * check name. Present only for the branch-protection tier. */ readonly pins?: Record; } /** * Human-readable description of a {@link RequiredWorkflowsSource}, embedded * verbatim in rejection messages as `(source: …)`. * * @task T12104 (gh#1198) */ export declare function describeRequiredWorkflowsSource(source: RequiredWorkflowsSource): string; /** * Function signature for querying the TARGET repo's branch-protection * required status checks via the `gh` CLI. Injectable for tests, mirroring * {@link FetchGhPrPayload}. * * Returning `ok: false` NEVER fails the atom — the caller falls back to the * {@link PR_REQUIRED_WORKFLOWS} built-in default. An ok result carries the * repo (`owner/name`) and branch the contexts were read from so the source * can be named in rejection messages. * * @task T12104 (gh#1192) */ export type FetchGhBranchProtection = (cwd: string) => Promise<{ ok: true; contexts: string[]; repo: string; branch: string; /** `checks[].app_id` per context, when branch protection names the app (T12634). */ appIds?: Record; } | { ok: false; reason: string; }>; /** * Default {@link FetchGhBranchProtection}: reads the repo identity and * default branch via `gh repo view --json nameWithOwner,defaultBranchRef` * (falling back to `main` when the default branch cannot be determined), * then queries `gh api repos/{owner}/{repo}/branches/{branch}/protection/ * required_status_checks`. Both the classic `.contexts[]` entries and the * newer `.checks[].context` entries are collected and de-duplicated. * * A lookup that yields ZERO contexts (protection exists but requires no * checks) is treated as a lookup failure and falls back to the built-in * default — the explicit "no required workflows" lever remains the empty * `release.prRequiredWorkflows` array from gh#1104. * * @task T12104 (gh#1192) */ export declare const defaultFetchGhBranchProtection: FetchGhBranchProtection; /** * Filesystem location for the cached branch-protection required-checks list, * per repository (T13912): `/.cleo/cache/evidence/pr-required-checks-.json`. * * @param projectRoot - The store root holding the cache. * @param repo - {@link prRepoIdentity} of the checkout `gh` runs in. * @task T12104 (gh#1192) * @task T13912 */ export declare function branchProtectionCachePath(projectRoot: string, repo: string): string; /** * TTL for the branch-protection cache: 1 hour. Branch protection changes * rarely and verify runs cluster in time, so a short TTL keeps repeated * verifies off the API without going stale for long. The entry also records * the repo+branch it was read from, purely for diagnostics. * * @task T12104 (gh#1192) */ export declare const BRANCH_PROTECTION_CACHE_TTL_MS: number; /** * Full async required-workflow resolution (gh#1192). Precedence: * 1. env var → 2. project context → 3. branch protection (cached, 1h TTL) * → 4. built-in default. * * The branch-protection tier is a READ-ONLY hint: any lookup failure (offline, * no `gh`, no branch protection, not a GitHub repo) silently falls through to * the built-in default — the atom never fails because the protection lookup * failed. * * @task T12104 (gh#1192) */ export declare function resolveRequiredWorkflowsDetailed(roots: EvidenceRoots, opts?: { /** Env (defaults to `process.env`). */ readonly env?: NodeJS.ProcessEnv; /** Parsed `.cleo/project-context.json` (or `null`). */ readonly projectContext?: Record | null; /** Mock branch-protection fetcher for tests. */ readonly fetchGhBranchProtection?: FetchGhBranchProtection; /** When `true`, skip the branch-protection cache read (writes still happen). */ readonly bypassProtectionCache?: boolean; /** When `true`, never write the branch-protection cache (T12623 read-only planner). */ readonly readOnly?: boolean; }): Promise; /** * Collapse re-runs of one check to the run that decides it (T13506). * * The rollup lists every run of a check on the PR's head, so a check that * failed and was re-run green appears twice. Counting both let the stale * FAILURE veto the green re-run. Runs are grouped by workflow and job name; * the latest COMPLETED run (by `completedAt`, else `startedAt`) decides, and * pending runs count only when the check has no completed run. Entries without * a name or any timestamp are kept as-is, which is the behaviour before T13506. * * @param rollup - The PR's `statusCheckRollup`. * @returns The rollup with each re-run check reduced to its deciding run. * @task T13506 */ export declare function latestCompletedRuns(rollup: GhPrViewPayload['statusCheckRollup']): GhPrViewPayload['statusCheckRollup']; /** * Inspect the `statusCheckRollup` and decide whether the required checks * are green. * * Required-check names from {@link PR_REQUIRED_WORKFLOWS} match against * EITHER the rollup entry's `workflowName` (top-level workflow like * `"Lockfile Check"`) OR its `name` (individual job, like `"Contracts * Dep Lint"` which lives under `workflowName === "CI"`). GitHub branch * protection lists required contexts by job name OR workflow name, and * the rollup may surface them at either granularity. * * ## Per-required-entry success rule * * For each entry in {@link PR_REQUIRED_WORKFLOWS}: * 1. At least one rollup check must match by name OR workflowName. * 2. At least one matching check must have `conclusion === 'SUCCESS'`. * 3. The match's status must be `COMPLETED` (not pending). * * Workflow-level matches (e.g. `"CI"` matches every job under CI) are * intentionally LENIENT: when ANY job under the workflow is `SUCCESS`, * the workflow counts as satisfied even if other (non-required-by-name) * jobs inside the same workflow are `FAILURE`. This mirrors GitHub's * branch-protection behavior, which gates merge on the * `required_status_checks[contexts]` list — administrators may merge * past non-required job failures, and `pr:` evidence accepts that * reality. To enforce stricter rules, narrow the required list via the * {@link PR_REQUIRED_WORKFLOWS_ENV_VAR} env var to specific job names. * * @internal * @task T9764 * @task T12104 (gh#1198) */ export declare function evaluateRollup(rollup: GhPrViewPayload['statusCheckRollup'], requiredWorkflows: readonly string[], opts?: { /** * Human-readable source of the required list (see * {@link describeRequiredWorkflowsSource}), named in the missing-workflows * rejection so the reader knows which tier produced it (gh#1198). */ readonly requiredSource?: string; /** PR number, embedded in the missing-workflows rejection header. */ readonly prNumber?: number; }): { ok: true; successCount: number; totalChecks: number; } | { ok: false; reason: string; }; /** * Fetches the check runs recorded on a merge commit (its default-branch push * run), in `statusCheckRollup` shape. Injectable for tests. * * @task T13611 */ export type FetchGhCommitCheckRuns = (sha: string, cwd: string) => Promise<{ ok: true; checks: GhPrViewPayload['statusCheckRollup']; } | { ok: false; reason: string; }>; /** * Default {@link FetchGhCommitCheckRuns}: `gh api * repos/{owner}/{repo}/commits//check-runs`, run in `cwd` so `gh` * resolves the repository the same way `gh pr view` does. * * @task T13611 */ export declare const defaultFetchGhCommitCheckRuns: FetchGhCommitCheckRuns; /** * Options for {@link resolvePrEvidenceAtom}. Allows test injection of a * mock `gh` wrapper plus an env override so tests do not have to mutate * `process.env`. * * @task T9764 */ export interface ResolvePrEvidenceAtomOptions { /** Mock `gh` wrapper for tests; defaults to {@link defaultFetchGhPrPayload}. */ readonly fetchGhPrPayload?: FetchGhPrPayload; /** Env (defaults to `process.env`). */ readonly env?: NodeJS.ProcessEnv; /** When `true`, bypass cache reads (PR-result cache and branch-protection cache). Cache writes always happen. */ readonly bypassCache?: boolean; /** * Parsed `.cleo/project-context.json` (or `null`). Tier between env and the * built-in default for required-workflow resolution (gh#1104). @task T12014 */ readonly projectContext?: Record | null; /** * Mock branch-protection fetcher for tests; defaults to * {@link defaultFetchGhBranchProtection}. Only consulted when neither the * env var nor the project context declares a required-workflow list * (gh#1192). @task T12104 */ readonly fetchGhBranchProtection?: FetchGhBranchProtection; /** * Mock PR files page fetcher for tests; defaults to * {@link defaultFetchGhPrFilesPage}. Consulted only when `gh pr view` * returned fewer files than the PR's `changedFiles` count. @task T12358 */ readonly fetchGhPrFilesPage?: FetchGhPrFilesPage; /** * When `true`, persist nothing: neither the PR-result cache nor the * branch-protection cache is written. Reads still happen. Used by the * read-only `cleo done --plan` planner, which must leave `.cleo/` untouched. * @task T12623 */ readonly readOnly?: boolean; /** * Mock merge-commit check-run fetcher for tests; defaults to * {@link defaultFetchGhCommitCheckRuns}. Consulted only when the PR's own * rollup is not green. @task T13611 */ readonly fetchGhCommitCheckRuns?: FetchGhCommitCheckRuns; } /** * Validate a `pr:` atom end-to-end. * * Steps: * 1. Cache lookup — return immediately on a fresh hit. * 2. Fetch `gh pr view --json …`. * 3. Schema-validate the payload. * 4. Reject non-merged PRs (`state !== 'MERGED'` or null `mergedAt`). * 5. Resolve the required-workflow list (env → project context → branch * protection → built-in default; gh#1192). * 6. Evaluate the status-check rollup against required workflows. * 7. When `gh pr view` truncated the file list (>100 files), paginate the * PR files API for the complete inventory (T12358). * 8. Persist a complete result to cache and return. * * @param prNumber - PR number (positive integer). * @param projectRoot - Absolute path to the project root (for cache + cwd). * @param opts - Optional overrides for testing. * @returns Validation result envelope. * * @task T9764 * @task T12104 */ export declare function resolvePrEvidenceAtom(prNumber: number, roots: EvidenceRoots, opts?: ResolvePrEvidenceAtomOptions): Promise; //# sourceMappingURL=pr-evidence.d.ts.map