import type { GateEvaluator, GateTier, GateVerdict, GhRunner } from './gate-types.js'; import { safeExec } from './sys/exec.js'; /** * merge-ready (mmnto-ai/totem#2800): a PreToolUse gate over the Bash and * PowerShell tools that reads the five-point deterministic pre-merge floor off * GitHub before `gh pr merge` runs. * * ZERO LLM calls, zero filesystem writes. The only outside state is what `gh` * answers, and `gh` reaches this module through the injected {@link GhRunner} * seam so a test replaces the network with a checked-in capture (R3, R4). * * The five predicates, evaluated IN ORDER (the first failure is the reason): * 1. `checks` — the head commit's status-check rollup is green * 2. `unresolved-bot-threads`— no unresolved, non-outdated review thread whose * ROOT comment is one of the known review bots * 3. `changes-requested` — no un-superseded CHANGES_REQUESTED review * 4. `high-severity-inline` — no HIGH/Major bot inline that CURRENTLY applies * to the head commit (`comment.commit.oid`) and * is not DISCHARGED through the disposition path * (mmnto-ai/totem#2861): its thread RESOLVED plus * a disposition NAMING IT — a non-bot reply * after the root inside the thread, or a * non-bot PR-level comment created after the * root carrying a `disposition: ` line for this thread. A bare * resolve, or a round disposition that did not * name the thread, still applies at every tier * (fail-closed) * 5. `merge-state` — GitHub's own `mergeStateStatus` is mergeable * * THE TIER SPLIT (R1): a predicate that FAILS is `deny` at every tier (the * wrapper's tier map decides whether that blocks). A predicate whose INPUT * could not be derived — gh missing, an API error, an incomplete page, a head * that moved mid-read, `mergeStateStatus: UNKNOWN` — is the UNEVALUABLE class: * `deny` under strict, `warn` under pilot, and NEVER `allow`. Every unevaluable * path is named on stderr under BOTH tiers; only the exit code differs. * * THE OVERRIDE: `TOTEM_MERGE_GATE_OVERRIDE=1` yields `allow` plus one stderr * audit line naming the repo, the PR, the head sha and the predicates that * would have denied. It is read per evaluation (no module-level state). */ /** The gate's registry event name. */ export declare const MERGE_READY_EVENT = "merge-ready"; /** The module label every verdict cites as its `provenance.source` (never a path). */ export declare const MERGE_READY_SOURCE = "merge-ready gh graphql read"; /** The audited bypass. Set to exactly `1`; any other value is not an override. */ export declare const MERGE_READY_OVERRIDE_ENV = "TOTEM_MERGE_GATE_OVERRIDE"; /** Prefix every stderr line this gate emits carries, so a reader can grep one gate out of a hook log. */ export declare const MERGE_READY_NOTICE_PREFIX = "[totem merge-ready]"; /** `provenance.matched` (and every quoted evidence fragment) is bounded to this many characters. */ export declare const MERGE_READY_EVIDENCE_MAX = 160; /** The predicates, in evaluation order. */ export type MergeReadyPredicate = 'checks' | 'unresolved-bot-threads' | 'changes-requested' | 'high-severity-inline' | 'merge-state'; /** The payload the wrapper projects from a `gh pr merge` command (or a hand caller passes). */ export interface MergeReadyPayload { /** `owner/name`. */ repo: string; /** The PR number, or null when the command named no PR (then `branch` carries the target). */ pr: number | null; /** The branch the PR is resolved from when `pr` is null. */ branch?: string; /** The local head sha the caller believes it is merging, when it could read one. */ headSha?: string; /** * A `gh pr merge` argument the projection could not turn into a PR — an * UNEXPANDED shell variable (`gh pr merge $PR`), whose value the wrapper * cannot know (mmnto-ai/totem#2800 fold F13). Carried instead of guessed: a * literal `"$PR"` read as a branch name would resolve nothing and the gate * would judge the wrong PR, or none. Present ⇒ the evaluation is UNEVALUABLE * before any read, so strict denies and pilot warns (R1). */ unresolvedTarget?: string; } /** * The rich evidence every merge-ready verdict carries in `provenance.detail`. * A type alias, not an interface, so it satisfies the verdict's * `Record` detail slot without a cast. */ export type MergeReadyProvenanceDetail = { repo: string; pr: number | null; /** The PR head sha as GitHub answered it, or null when the read never got that far. */ headSha: string | null; /** * Predicate 1's count, per CHECK after the latest-run judgment * (mmnto-ai/totem#2879): a check is a name under one producer (the app, * and the workflow for Actions), so `total` counts checks the way * `gh pr checks` lists them — one row per name, one per producer when two * producers share a name; `superseded` counts the check runs a later run of * the same check replaced (a concurrency group's cancelled duplicates), * which are read and disclosed but never judged. */ checks: { total: number; success: number; pending: number; failing: number; superseded: number; }; threads: { unresolvedBot: number; pagesRead: number; complete: boolean; }; changesRequestedBy: string[]; /** HIGH/Major bot inlines on the head commit that still APPLY after the discharge read — predicate 4's count. */ highInline: number; /** * HIGH/Major bot inlines on the head commit DISCHARGED through the * disposition path (mmnto-ai/totem#2861): thread resolved, and a * disposition on record after the root. Carried so the audit record shows * what the predicate released, not only what it kept. */ dischargedHigh: number; /** The discharges split by the arm that carried each: an in-thread non-bot reply, or a PR-level disposition line naming the thread. */ dischargedBy: { inThreadReply: number; prLevelDisposition: number; }; mergeStateStatus: string | null; evaluatedAt: string; /** `gh `, read from the same binary that answered the query. */ evaluatedBy: string; /** Present and true only when the audited override produced the verdict. */ override?: boolean; }; /** What one evaluation produced: the verdict, its evidence, and the lines the host must print. */ export interface MergeReadyEvaluation { verdict: GateVerdict; detail: MergeReadyProvenanceDetail; /** stderr lines, already prefixed. Every unevaluable path and R5's zero-check fact is here. */ notices: readonly string[]; } /** Options for the pure {@link evaluateMergeReady} entry point. */ export interface MergeReadyOptions { /** The gh seam. Production passes {@link makeGhRunner}; tests pass fixtures. */ runner: GhRunner; /** Enforcement tier for the UNEVALUABLE class only (default `strict`). */ tier?: GateTier; /** Environment the override is read from (default `process.env`). */ env?: NodeJS.ProcessEnv; /** Clock seam for `checkedAt` / `evaluatedAt` (default `Date`). */ now?: () => Date; } /** The read the evaluator sends when the payload names a PR number. Fixtures are captured with THIS string. */ export declare const MERGE_READY_QUERY = "query TotemMergeReady($number: Int!, $owner: String!, $name: String!, $reviewsAfter: String, $threadsAfter: String, $checksAfter: String, $commentsAfter: String) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) { ...MergeReadyPr }\n }\n}\n\nfragment MergeReadyPr on PullRequest {\n number\n isDraft\n mergeStateStatus\n headRefOid\n headRefName\n baseRefName\n commits(last: 1) {\n nodes {\n commit {\n oid\n statusCheckRollup {\n state\n contexts(first: 100, after: $checksAfter) {\n totalCount\n pageInfo { hasNextPage endCursor }\n nodes {\n __typename\n ... on CheckRun { name status conclusion databaseId checkSuite { databaseId app { slug } workflowRun { workflow { databaseId name } } } }\n ... on StatusContext { context state }\n }\n }\n }\n }\n }\n }\n reviews(first: 100, after: $reviewsAfter) {\n pageInfo { hasNextPage endCursor }\n nodes {\n author { login }\n state\n submittedAt\n }\n }\n reviewThreads(first: 100, after: $threadsAfter) {\n pageInfo { hasNextPage endCursor }\n nodes {\n isResolved\n isOutdated\n comments(first: 10) {\n pageInfo { hasNextPage }\n nodes {\n databaseId\n author { __typename login }\n body\n createdAt\n commit { oid }\n originalCommit { oid }\n }\n }\n }\n }\n comments(first: 100, after: $commentsAfter) {\n pageInfo { hasNextPage endCursor }\n nodes {\n author { __typename login }\n body\n createdAt\n }\n }\n}"; /** * The read the evaluator sends when the payload carries a branch and no number * (`gh pr merge` with no argument). Same fragment, so the predicates read the * same fields; the first page resolves the number, and every later page uses * {@link MERGE_READY_QUERY} against that fixed number. */ export declare const MERGE_READY_BRANCH_QUERY = "query TotemMergeReadyByBranch($branch: String!, $owner: String!, $name: String!, $reviewsAfter: String, $threadsAfter: String, $checksAfter: String, $commentsAfter: String) {\n repository(owner: $owner, name: $name) {\n pullRequests(headRefName: $branch, first: 1, states: OPEN, orderBy: { field: CREATED_AT, direction: DESC }) {\n nodes { ...MergeReadyPr }\n }\n }\n}\n\nfragment MergeReadyPr on PullRequest {\n number\n isDraft\n mergeStateStatus\n headRefOid\n headRefName\n baseRefName\n commits(last: 1) {\n nodes {\n commit {\n oid\n statusCheckRollup {\n state\n contexts(first: 100, after: $checksAfter) {\n totalCount\n pageInfo { hasNextPage endCursor }\n nodes {\n __typename\n ... on CheckRun { name status conclusion databaseId checkSuite { databaseId app { slug } workflowRun { workflow { databaseId name } } } }\n ... on StatusContext { context state }\n }\n }\n }\n }\n }\n }\n reviews(first: 100, after: $reviewsAfter) {\n pageInfo { hasNextPage endCursor }\n nodes {\n author { login }\n state\n submittedAt\n }\n }\n reviewThreads(first: 100, after: $threadsAfter) {\n pageInfo { hasNextPage endCursor }\n nodes {\n isResolved\n isOutdated\n comments(first: 10) {\n pageInfo { hasNextPage }\n nodes {\n databaseId\n author { __typename login }\n body\n createdAt\n commit { oid }\n originalCommit { oid }\n }\n }\n }\n }\n comments(first: 100, after: $commentsAfter) {\n pageInfo { hasNextPage endCursor }\n nodes {\n author { __typename login }\n body\n createdAt\n }\n }\n}"; /** Parse + validate the payload. Throws `GATE_INVALID` (the wrapper's fail-closed arm) on anything else. */ export declare function parseMergeReadyPayload(payload: unknown): MergeReadyPayload; /** * The root comment ids a PR-level comment body names on disposition lines, * in first-seen order, without duplicates. Exported for the tests, so they * assert the shipped predicate and not a copy of it. */ export declare function dispositionedRootIds(body: string): number[]; /** * Whether a comment body carries a bot's OWN structured high-severity label — * one of {@link HIGH_SEVERITY_MARKERS}, read against the body with every code * form replaced first ({@link withoutCode}), so a quoted marker never reads * as a label and prose is never matched. `true` is "this finding's author * ranked it HIGH/Major/P0/P1"; `false` is "no such label in the prose" — which * includes a bot that emits no structured label at all (the disclosed miss * direction above). */ export declare function hasHighSeverityMarker(body: string): boolean; /** * Evaluate the merge-ready floor. Pure with respect to state: it reads GitHub * through the injected runner and returns a verdict, its evidence, and the * stderr lines the host must print. It writes nothing. */ export declare function evaluateMergeReady(payload: unknown, options: MergeReadyOptions): MergeReadyEvaluation; /** * The production gh seam: spawn `gh` with an argv (never a shell string) and * report `{ stdout, exitCode }`. A spawn failure (gh absent) and a non-zero * exit are the SAME shape here — both are "the read did not answer", which the * evaluator turns into the named UNEVALUABLE class. * * When gh writes its diagnosis to stderr and nothing to stdout (the missing * binary, an auth failure), the stderr text is returned as `stdout` so the * reason can name what gh said; the exit code is what decides. */ export declare function makeGhRunner(timeoutMs?: number, execute?: typeof safeExec): GhRunner; /** * The registry evaluator. Builds the production runner lazily (so no gh is * spawned unless this gate actually runs), evaluates, and prints the gate's * stderr lines through the injected sink. */ export declare const mergeReadyEvaluator: GateEvaluator; //# sourceMappingURL=merge-ready.d.ts.map