1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
//! PR → ticket linkage: extract the governing ticket-id from a pull request.
//!
//! Why: the BACK gate starts from a PR, not a ticket-id, so the ISR must
//! resolve *which* ticket governs the change. trusty-review's existing linkage
//! is fuzzy keyword search (spec §2.1); the ISR replaces it with explicit
//! parsing. The regexes are **lifted from tga** (`collect/ticket.rs`) so both
//! gates parse linkage without pulling tga's git2/rusqlite stack (spec §6.3).
//! What: `is_ticketed` / `extract_ticket_id` (lifted verbatim in behaviour),
//! plus `extract_pr_ticket` which applies the spec's source-precedence order
//! (PR body trailers → commit trailers → branch name).
//! Test: `super::tests::linkage_*` (AC-5).
use OnceLock;
use Regex;
/// Compiled regexes that qualify text as "ticketed" (lifted from tga).
///
/// Why: a bare `#N` is too noisy to qualify on its own (tga issue #445); only
/// JIRA/Linear, GitHub action-keyword refs, and ADO refs qualify.
/// What: the three qualifying patterns. `gh_bare` lives in [`ExtractPatterns`].
/// Test: `super::tests::linkage_is_ticketed`.
/// Lazily-initialised qualifying pattern set.
///
/// Why: compile the regexes exactly once across all resolver calls.
/// What: returns a `'static` reference to the compiled `TicketPatterns`.
/// Test: exercised by every `linkage_*` test.
/// Compiled extraction patterns, most-specific first (lifted from tga).
///
/// Why: when several ref styles appear, the highest-fidelity match wins.
/// What: ADO, then JIRA/Linear, then a bare `#N` last-resort.
/// Test: `super::tests::linkage_extract_*`.
/// Lazily-initialised extraction pattern set.
///
/// Why: compile once; reuse across all resolver calls.
/// What: returns a `'static` reference to the compiled `ExtractPatterns`.
/// Test: exercised by every `linkage_extract_*` test.
/// Return `true` if `message` contains a *qualifying* ticket reference.
///
/// Why: a bare `#N` is too noisy to count as linkage on its own (tga #445);
/// only action-keyword GitHub refs, JIRA/Linear ids, and ADO refs qualify.
/// What: lifted verbatim from `tga::collect::ticket::is_ticketed`.
/// Test: `super::tests::linkage_is_ticketed`.
/// Extract the first recognisable ticket id from arbitrary text.
///
/// Why: populate the linkage as a last-resort identifier even when no action
/// keyword is present (e.g. a bare `#N`).
/// What: lifted verbatim from `tga::collect::ticket::extract_ticket_id` —
/// tries ADO, then JIRA/Linear, then bare `#N`, in that priority order.
/// Test: `super::tests::linkage_extract_*`.
/// Extract a `#N` ticket id from a branch name (`fix/1325-x` → `#1325`).
///
/// Why: a branch name is the spec's third linkage source (§6.3) and does not
/// carry the `#` sigil that [`extract_ticket_id`]'s bare pattern needs.
/// What: matches the first run of digits in a `kind/NNNN-slug` branch and
/// returns it as a `#N` GitHub-style id; returns `None` for branches with no
/// leading numeric segment.
/// Test: `super::tests::linkage_branch_*`.
/// Resolve the governing ticket-id from a PR, applying source precedence.
///
/// Why: the BACK gate must pick *one* ticket deterministically from several
/// possible linkage sources; the spec fixes the order (§6.3).
/// What: tries, in order — (a) the PR **body**, which is free-text prose and so
/// must carry a *qualifying* reference ([`is_ticketed`]: an action-keyword
/// GitHub ref, a JIRA/Linear id, or an ADO ref) before a ticket-id is taken
/// from it; (b) each commit message (a bare `#N` here is acceptable — commit
/// trailers are conventionally terse); then (c) the branch name via
/// [`extract_branch_ticket`]. Returns the first match; `None` when the PR
/// carries no linkage at all.
///
/// The body is gated on `is_ticketed` specifically to avoid the tga #445
/// false-positive: a PR body like `"See discussion in #42 for background"`
/// mentions `#42` only in passing and must NOT resolve to ticket 42, whereas
/// `"Closes #42"` (an action keyword) qualifies. Commit messages and branch
/// names are NOT free-text discussion, so they retain the bare-`#N`
/// last-resort fallback (spec §6.3).
/// Test: `super::tests::linkage_pr_*`, `super::tests::ac5_pr_body_bare_ref_*`
/// (AC-5).