/** * Canonical PR-merge ticket-closer (TD-791). * * The ONE extractor every auto-close path imports — Guardian's post-merge * lifecycle (`packages/daemon`) and the app-side GitHub webhook routes * (`apps//.../webhooks/github`, `.../internal/github-webhook`). Before * this consolidation each path carried its own copy with a DIFFERENT body gate * (the classic "derive, don't copy" hazard): the daemon used a leading-directive * gate while the webhook used a looser keyword-anywhere-on-line gate. Same PR * could close different tickets depending on which closer won, and the webhook's * looser gate still false-closed (see the verified failures below). One imported * function, one rule, no drift. * * Three surfaces, two rules: * * Title — every ticket ID closes (the established `/ship` convention), EXCEPT * two relational/revert shapes that are contextual cites, not close-intent * (TD-873): a REVERT title contributes no IDs (it undoes work, and an * auto-generated revert title carries the original PR's ID), and an ID * inside PARENTHESES does not close (the closing ticket always sits outside * parens; a parenthesized ID is a relational cite like * `revert(…): … (#2097 masked PTQ-97)` or `feat: … (TD-870 follow-up)`). * Branch — every ticket ID closes. Branch names are auto-generated from the * ticket (e.g. `yourname/td-792-…`) and are high-signal. * Body — a ticket ID closes ONLY if its line is a closing DIRECTIVE: it * begins (after optional list/quote/emphasis markers) with a closing * keyword (Closes / Fixes / Resolves) that directly introduces an ID, * e.g. `Closes: TD-1`, `- Fixes TD-2`, `**Resolves:** TD-3, TD-4`. * Bare narrative mentions — a "Follow-ups" section, "Related work", or * prose like "complete except for TD-784" — do NOT close. Critically, * a keyword used MID-SENTENCE or in a NEGATION does NOT close either: * "does NOT close `TD-793`", "this closes the gap for TD-784", and * "the closed-loop system, see TD-X" are all prose, not directives. * * Before any gate the closers matched EVERY `PREFIX-\d+` in the title, branch, OR * body prose and closed all of them — so a ticket merely *named* in a PR body got * marked Done. Verified failures (2026-06-07): PR #1715's body said "the project * is complete except for TD-784" — the sentence stating TD-784 is NOT done is what * closed it; likewise #1709's "…the moment TD-785 wires claim into the orient * loop" closed TD-785. * * The first fix (TD-792 / PR #1720) narrowed "any prose mention" to "any line * containing a closing keyword as a word" — but that STILL false-closed, because a * keyword can appear mid-sentence on an ID's line. Verified regression: PR #1722 * (the webhook path) merged 13:37Z with `Closes: none` and a heading "⚠️ Inert until * wired — does NOT close `TD-793`"; the word "close" on that line closed TD-793 at * 13:37:31Z — the exact failure class #1720 set out to kill. This gate (the second * fix, TD-797 / PR #1728) requires the keyword to be a LEADING DIRECTIVE that * directly introduces an ID, so negations and gap-prose ("closes the gap for…") * never close. TD-791 then promoted this proven gate here so the webhook — which * was still on the looser keyword-anywhere rule — inherits it instead of carrying * its own divergent copy. The `/ship` Step-3.5 "Closes:" lint (PKT-543) honors the * same convention. */ /** * Extract the deduplicated, uppercase Linear ticket IDs a merged PR should close. * * @param title PR title — all IDs here close. * @param branch PR head branch — all IDs here close. * @param body PR body — only IDs introduced by a leading closing-keyword * directive close (see LEADING_CLOSING_KEYWORD_RE). * @param prefixes Configured team prefixes (e.g. ['TD', 'PKT', 'NTQ']). Each is * regex-escaped internally. * @returns e.g. ['TD-792', 'PKT-543'] — empty when no prefixes or no closable IDs. */ export declare function extractClosableTicketIds(title: string, branch: string, body: string, prefixes: string[]): string[]; //# sourceMappingURL=extract-ticket-ids.d.ts.map