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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
//! The ISR entry point: `resolve(IntentQuery) -> ResolvedIntent`.
//!
//! Why: both conformance gates call one resolver so ticket+spec resolution and
//! the precedence rule (ticket > spec) are implemented **once**, centrally
//! (spec §6.1, §6.5, G4). This module wires the pieces — linkage, ticket fetch,
//! spec resolution, method extraction — and applies precedence exactly once.
//! What: the public async `resolve`, the pluggable `TicketFetcher` /
//! `IntentTokenResolver` seams (§7.4), and `apply_precedence` (the normative
//! four-case rule). Every failure path is **fail-open**: it returns
//! `ResolvedIntent::unresolved{reason}`, never an `Err` (spec §4.2).
//! Test: `super::tests` (AC-1..AC-7).
use async_trait;
use ;
use extract_pr_ticket;
use ;
use ;
/// Pluggable token resolver for GitHub App / JWT auth (spec §7.4).
///
/// Why: trusty-review's serve mode uses richer GitHub App/JWT auth than the
/// `tickets` backend's PAT-only path. The ISR must NOT hard-wire a PAT; it
/// exposes this seam (mirroring review's `IssueTokenResolver`) so it works in
/// webhook/serve mode too (spec §7.4).
/// What: one async method returning a bearer token for an owner/repo, or an
/// error when none is available.
/// Test: `super::tests::token_resolver_*`.
/// Default token resolver: reads the `GITHUB_TOKEN` environment variable.
///
/// Why: the common (CLI / PAT) case needs no custom resolver; this preserves
/// the existing `tickets` backend behaviour as the zero-config default
/// (spec §7.4) while keeping the App/JWT path pluggable.
/// What: returns `$GITHUB_TOKEN`, or `IsrError::NoToken` when it is unset.
/// Test: `super::tests::token_resolver_env_*` (serial, env-mutating).
;
/// Pluggable ticket fetcher (the seam over `tickets::Backend::get_issue`).
///
/// Why: the resolver must fetch a ticket body without the production GitHub
/// client in tests, and without coupling `resolve` to one backend. This trait
/// is the seam both the default GitHub fetcher and test mocks implement
/// (spec §6.6 reuses `Backend::get_issue` behind it).
/// What: one async method mapping `(owner, repo, ticket_id)` to a `TicketData`,
/// or `IsrError` on failure.
/// Test: `super::tests` uses a `MockFetcher` implementing this trait.
/// The minimal ticket data the resolver needs (backend-agnostic).
///
/// Why: decouples the resolver from the full `tickets::Issue` shape so a mock
/// (and a future non-GitHub backend) need only supply these fields.
/// What: id, title, body, optional URL, and the backend tag — exactly the
/// inputs to `TicketRef` + method extraction (spec §6.1, §6.2).
/// Test: `super::tests` constructs `TicketData` directly.
/// Resolve intent from a query, applying precedence (ticket > spec).
///
/// Why: the one entry point both gates call; centralising resolution +
/// precedence guarantees FRONT and BACK can never disagree (spec §6.1, G4).
/// What: extracts the ticket-id (for `Pr`), fetches the ticket via `fetcher`,
/// extracts the ticket method via `extractor`, resolves the spec section + spec
/// method from `changed_files`, then applies [`apply_precedence`]. Any
/// fetch/parse failure degrades to `ResolvedIntent::unresolved` (fail-open,
/// spec §4.2) — this fn never returns `Err`. Non-ticketed `Pr` input →
/// `ResolvedIntent::none()`.
/// Test: `super::tests` (AC-1..AC-7).
pub async
/// Pull `(owner, repo, ticket_id, changed_files)` out of a query.
///
/// Why: `Pr` must derive the ticket-id from linkage while `Ticket` already has
/// it; isolating that branch keeps `resolve` linear (spec §6.1, §6.3).
/// What: for `Ticket`, returns its fields directly; for `Pr`, runs
/// [`extract_pr_ticket`] and returns `None` when the PR has no linkage.
/// Test: `super::tests::linkage_pr_*` (AC-5), `ticket_query_*`.
/// Resolve the spec section + spec method (+ revision drift) from changed files.
///
/// Why: the spec axis is greenfield and must never fabricate linkage — a file
/// with no SLD ref yields no spec method (spec §6.4). C4 (#1361) additionally
/// surfaces revision drift so a `~v1` ref to a `~v2` section still resolves and
/// is flagged `stale_spec`-adjacent without blocking (§6.4, OQ-6).
/// What: parses SLD refs from each changed file (first match wins), looks up
/// the spec markdown via `spec_lookup`, and resolves the governed section via
/// [`resolve_spec_section`] — returning the `SpecRef`, the extracted method, and
/// whether the referenced revision drifted from the section's. Returns
/// `(None, None, false)` when no SLD ref is declared.
///
/// KNOWN LIMITATION (spec §6.4 "first match wins"): the loop returns the FIRST
/// changed file that declares an SLD ref and silently ignores SLD refs in later
/// files. A PR touching multiple files each governed by a different spec
/// section is out of scope; multi-file/multi-section reconciliation is not part
/// of C4.
/// Test: `super::tests::spec_resolve_*` (AC-6).
/// Pluggable spec-markdown loader.
///
/// Why: the spec-resolution leg needs the *text* of a `docs/specs/*.md` file to
/// extract the spec method, but the resolver must not assume a filesystem
/// layout (review's serve mode may load specs differently). This seam keeps the
/// resolver testable offline (spec §6.4, mirrors the token/fetcher seams).
/// What: one method mapping a `docs/specs/*.md` path to its text, or `None`
/// when the spec cannot be loaded (a gap, never an error).
/// Test: `super::tests::spec_resolve_*` uses an in-memory `MapSpecLookup`.
/// Assemble a `ResolvedIntent` and apply precedence to it.
///
/// Why: keeps `resolve` readable by separating wiring from the normative
/// precedence computation (spec §6.1). C4 also folds the SLD revision-drift
/// signal into `stale_spec`-adjacent metadata here (spec §6.4).
/// What: builds the struct from the resolved axes, runs [`apply_precedence`] to
/// set `precedence_winner`/`conflict`/`stale_spec`, then, when the spec was
/// resolved via a drifted revision (`spec_drift`), raises `stale_spec` WITHOUT
/// setting `conflict` — a non-blocking advisory marker (OUTDATED enforcement is
/// out of scope, §1.3). Drift never lowers a precedence winner.
/// Test: `super::tests::precedence_*` (AC-1..AC-4), `spec_resolve_drift_*`.
/// Apply the NORMATIVE precedence rule (ticket > spec) to an intent.
///
/// Why: the precedence rule is the heart of the contract and must be applied
/// exactly once, centrally, so no caller re-derives it (spec §6.1, G4).
/// What: implements the four cases verbatim from spec §6.1:
/// 1. both present + agree → winner `Ticket`, `conflict=false`.
/// 2. both present + disagree → winner `Ticket`, `conflict=true`,
/// `stale_spec=true`.
/// 3. ticket only → `Ticket`; spec only → `Spec`.
/// 4. neither → `None` (gap).
///
/// Method equality is by normalised `text` (case/whitespace-insensitive).
///
/// Test: `super::tests::precedence_*` (AC-1..AC-4).
/// Whether a ticket method and a spec method agree.
///
/// Why: case 1 vs. case 2 turns on whether the two methods say the same thing
/// (spec §6.1). A normalised text compare is the conservative C1 rule; richer
/// semantic equivalence is out of C1 scope.
/// What: compares the two methods' `text` after lowercasing and collapsing
/// whitespace; returns `true` when they match.
/// Test: `super::tests::precedence_agree_*` / `precedence_conflict_*`.
/// Lowercase + collapse internal whitespace for method-text comparison.
///
/// Why: trivial formatting differences must not register as a conflict.
/// What: lowercases, splits on whitespace, and rejoins single-spaced.
/// Test: covered by `precedence_agree_*`.
/// Convenience wrapper: resolve with the default heuristic extractor.
///
/// Why: the common case wants the OQ-1 default (heuristic, no network) without
/// constructing an extractor by hand (spec §6.2 OQ-1 default path).
/// What: calls [`resolve`] with a [`HeuristicMethodExtractor`].
/// Test: `super::tests` use this wrapper for AC-1..AC-7.
pub async