adopt-knowledge 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,246 @@
1
+ """`adopt ingest` / `harvest` / `bind` / `review` / `gaps` -- Build 2.
2
+
3
+ The store's first knowledge corpus, bound **honestly** to the identities Build 1
4
+ mapped, and the coverage gap made visible.
5
+
6
+ **The invariant this package exists to hold** (v6.1 §6 H2/D9, critical semantic
7
+ invariant #2): *no binding row exists that a structural match or a human did not
8
+ justify.* Everything else here is in service of it.
9
+
10
+ Why that one, out of everything Build 2 could have been careful about: a false
11
+ binding is the only failure in this build that is both **silent** and
12
+ **self-reinforcing**. `recompute_coverage` counts the falsely-bound identity as
13
+ covered, so `adopt gaps` stops asking for the knowledge that is actually
14
+ missing; and every later change to that identity stales a document that never
15
+ described it, until the reviewer learns the queue is noise. Neither symptom
16
+ points back at the matcher. The two-tier rule -- structural evidence binds,
17
+ names are proposed to a person -- is what makes the failure unrepresentable
18
+ rather than merely unlikely.
19
+
20
+ Three further postures, each inherited rather than invented:
21
+
22
+ * **No model call anywhere.** v6.1 §4 R3; the optional summarization pass is
23
+ Build 4's generation module and is not built here.
24
+ * **Offline.** Harvest mines what is locally present (F7) through the system
25
+ `git` binary, confined to `gitlog`. Forge enrichment is a `--allow-network`
26
+ option that is **declared and refused**, so an operator who read the
27
+ architecture gets a sentence naming the deferral rather than an
28
+ unknown-option error.
29
+ * **Mined and authored never merge.** `artifact_observed` is a claim about
30
+ where something was read from, and nothing a human or a model writes can
31
+ acquire it after the fact.
32
+ """
33
+
34
+ from adopt_knowledge.changes import (
35
+ ACTION_CONFIRM_CURRENT,
36
+ ACTION_REBIND,
37
+ ACTION_RETIRE,
38
+ ACTIONS,
39
+ SOURCE_RULED_CLASSES,
40
+ ChangedBinding,
41
+ ChangeOutcome,
42
+ confirm_current_item,
43
+ rebind_item,
44
+ retire_item,
45
+ still_stale_after_confirm,
46
+ )
47
+ from adopt_knowledge.documents import (
48
+ AUDIENCES,
49
+ DEFAULT_AUDIENCE,
50
+ DEFAULT_KIND,
51
+ Document,
52
+ body_digest,
53
+ discover,
54
+ read_document,
55
+ split_frontmatter,
56
+ )
57
+ from adopt_knowledge.drafting import (
58
+ DRAFT_ACTOR,
59
+ DRAFT_AUTHORITY,
60
+ DRAFT_KIND,
61
+ DRAFT_PROMPT_REF,
62
+ DRAFT_PROVENANCE_PREFIX,
63
+ DRAFT_SOURCE,
64
+ DRAFT_VERIFICATION,
65
+ Draft,
66
+ DraftOutcome,
67
+ DraftReport,
68
+ DraftTarget,
69
+ Fact,
70
+ draft_one,
71
+ ground,
72
+ render_body,
73
+ run_drafting,
74
+ title_for,
75
+ )
76
+ from adopt_knowledge.gaps import (
77
+ GAP_KEY_SEPARATOR,
78
+ OPEN_DISPOSITION,
79
+ ConflictRow,
80
+ CoverageEntry,
81
+ Gap,
82
+ OpenConflict,
83
+ gap_key_for,
84
+ rank_conflicts,
85
+ rank_gaps,
86
+ )
87
+ from adopt_knowledge.gitlog import Commit, head_sha, read_commits
88
+ from adopt_knowledge.harvest import (
89
+ HARVEST_EXTRACTOR,
90
+ HARVEST_EXTRACTOR_VERSION,
91
+ Candidate,
92
+ HarvestReport,
93
+ Signal,
94
+ batch_key,
95
+ decision_record_titles,
96
+ mine,
97
+ run_harvest,
98
+ )
99
+ from adopt_knowledge.ingest import (
100
+ CREATED,
101
+ INGEST_EXTRACTOR_VERSION,
102
+ UNCHANGED,
103
+ UPDATED,
104
+ DocumentOutcome,
105
+ IngestReport,
106
+ StoredDocument,
107
+ run_ingest,
108
+ )
109
+ from adopt_knowledge.matchers import (
110
+ NAME_TIER,
111
+ STRUCTURAL_TIERS,
112
+ IdentityView,
113
+ Match,
114
+ MatchOutcome,
115
+ match_document,
116
+ name_matches,
117
+ path_matches,
118
+ structural_matches,
119
+ )
120
+ from adopt_knowledge.ports import (
121
+ BindingFreshener,
122
+ BindingSuperseder,
123
+ BindingWriter,
124
+ DraftStore,
125
+ ItemRetirer,
126
+ KnowledgeWriter,
127
+ ReviewWriter,
128
+ )
129
+ from adopt_knowledge.review import (
130
+ CHANGE_POPULATIONS,
131
+ SOURCE_DRAFT,
132
+ SOURCE_HARVEST,
133
+ SOURCE_INGEST,
134
+ SOURCE_REFRESH,
135
+ SOURCE_SENSE,
136
+ ChangeCause,
137
+ ChangedItem,
138
+ Outcome,
139
+ PendingItem,
140
+ coalesce_changes,
141
+ confirm,
142
+ derive_suggestions,
143
+ edit,
144
+ reject,
145
+ source_of,
146
+ )
147
+
148
+ __all__ = [
149
+ "ACTIONS",
150
+ "ACTION_CONFIRM_CURRENT",
151
+ "ACTION_REBIND",
152
+ "ACTION_RETIRE",
153
+ "AUDIENCES",
154
+ "CHANGE_POPULATIONS",
155
+ "CREATED",
156
+ "DEFAULT_AUDIENCE",
157
+ "DEFAULT_KIND",
158
+ "DRAFT_ACTOR",
159
+ "DRAFT_AUTHORITY",
160
+ "DRAFT_KIND",
161
+ "DRAFT_PROMPT_REF",
162
+ "DRAFT_PROVENANCE_PREFIX",
163
+ "DRAFT_SOURCE",
164
+ "DRAFT_VERIFICATION",
165
+ "GAP_KEY_SEPARATOR",
166
+ "HARVEST_EXTRACTOR",
167
+ "HARVEST_EXTRACTOR_VERSION",
168
+ "INGEST_EXTRACTOR_VERSION",
169
+ "NAME_TIER",
170
+ "OPEN_DISPOSITION",
171
+ "SOURCE_DRAFT",
172
+ "SOURCE_HARVEST",
173
+ "SOURCE_INGEST",
174
+ "SOURCE_REFRESH",
175
+ "SOURCE_RULED_CLASSES",
176
+ "SOURCE_SENSE",
177
+ "STRUCTURAL_TIERS",
178
+ "UNCHANGED",
179
+ "UPDATED",
180
+ "BindingFreshener",
181
+ "BindingSuperseder",
182
+ "BindingWriter",
183
+ "Candidate",
184
+ "ChangeCause",
185
+ "ChangeOutcome",
186
+ "ChangedBinding",
187
+ "ChangedItem",
188
+ "Commit",
189
+ "ConflictRow",
190
+ "CoverageEntry",
191
+ "Document",
192
+ "DocumentOutcome",
193
+ "Draft",
194
+ "DraftOutcome",
195
+ "DraftReport",
196
+ "DraftStore",
197
+ "DraftTarget",
198
+ "Fact",
199
+ "Gap",
200
+ "HarvestReport",
201
+ "IdentityView",
202
+ "IngestReport",
203
+ "ItemRetirer",
204
+ "KnowledgeWriter",
205
+ "Match",
206
+ "MatchOutcome",
207
+ "OpenConflict",
208
+ "Outcome",
209
+ "PendingItem",
210
+ "ReviewWriter",
211
+ "Signal",
212
+ "StoredDocument",
213
+ "batch_key",
214
+ "body_digest",
215
+ "coalesce_changes",
216
+ "confirm",
217
+ "confirm_current_item",
218
+ "decision_record_titles",
219
+ "derive_suggestions",
220
+ "discover",
221
+ "draft_one",
222
+ "edit",
223
+ "gap_key_for",
224
+ "ground",
225
+ "head_sha",
226
+ "match_document",
227
+ "mine",
228
+ "name_matches",
229
+ "path_matches",
230
+ "rank_conflicts",
231
+ "rank_gaps",
232
+ "read_commits",
233
+ "read_document",
234
+ "rebind_item",
235
+ "reject",
236
+ "render_body",
237
+ "retire_item",
238
+ "run_drafting",
239
+ "run_harvest",
240
+ "run_ingest",
241
+ "source_of",
242
+ "split_frontmatter",
243
+ "still_stale_after_confirm",
244
+ "structural_matches",
245
+ "title_for",
246
+ ]
@@ -0,0 +1,378 @@
1
+ """The three review actions Build 6's change population takes (v6.1 §6).
2
+
3
+ `refresh` puts an item in front of a human because something it describes
4
+ changed. This module is what the human's answer *does*, and there are exactly
5
+ three answers:
6
+
7
+ | Action | What the reviewer is saying | The store-state consequence |
8
+ |---|---|---|
9
+ | **retire** | "this note is obsolete" | a terminal knowledge revision; the item resolves `retired` |
10
+ | **rebind** | "it followed the referent that replaced it" | the old link appends `moved`; a new binding to the successor |
11
+ | **confirm-current** | "it is still true as written" | a `human_confirmed`/`verified` revision; the staled links go `fresh` |
12
+
13
+ **The resolution enum is the disposition; the store state is the record.**
14
+ `confirm-current` stamps `confirmed` and the other two stamp `corrected` (plan
15
+ decision D12), which is all three values `review_resolution` has room for -- and
16
+ it is deliberately *not* how a reader tells the actions apart. Two of them share
17
+ a value, so the honest place to look is what the store now says: an item that is
18
+ `retired`, a chain of bindings whose head is `moved` beside a new one, or a
19
+ revision a person put their name on. Inventing a fourth enum value to make the
20
+ disposition self-describing would have been a schema change (§8 budgets none)
21
+ buying a worse record than the one the writes already leave.
22
+
23
+ **Recorded first, acted second -- the same ordering `review.confirm` uses and
24
+ for the same reason.** `resolve` refuses an item that is already resolved, so it
25
+ is the guard that makes a double resolution impossible. Acting first would
26
+ retire the item, or supersede its bindings, and only *then* discover the entry
27
+ had been answered an hour ago by somebody else.
28
+
29
+ **What `confirm-current` cannot do, it says rather than appears to do.** For a
30
+ DEAD or MOVED cause the item stays STALE after the confirmation, because
31
+ `resolve_freshness`' *source* rules read the identity's own head status and no
32
+ binding-level write reaches them. That is correct -- the referent really is gone
33
+ -- and the reviewer is told so by name, with the two actions that can help. A
34
+ `confirm-current` that silently left the item stale would look like a broken
35
+ button; one that forced it fresh would be the product lying about a deleted
36
+ endpoint.
37
+ """
38
+
39
+ from collections.abc import Sequence
40
+ from dataclasses import dataclass
41
+ from typing import Final
42
+
43
+ from adopt_knowledge.ingest import EXTRACTOR_NAME_CONFIRMED, INGEST_EXTRACTOR_VERSION
44
+ from adopt_knowledge.ports import (
45
+ BindingFreshener,
46
+ BindingSuperseder,
47
+ ItemRetirer,
48
+ KnowledgeWriter,
49
+ ReviewWriter,
50
+ UnitOfWork,
51
+ )
52
+ from adopt_knowledge.review import (
53
+ CHANGE_POPULATIONS,
54
+ CONFIRMED,
55
+ CORRECTED,
56
+ PendingItem,
57
+ _append_human_revision,
58
+ )
59
+ from adopt_model._enums import ImpactClass, ReviewResolution
60
+ from adopt_obs import AdoptError, ErrorCode, get_logger
61
+
62
+ __all__ = [
63
+ "ACTIONS",
64
+ "ACTION_CONFIRM_CURRENT",
65
+ "ACTION_REBIND",
66
+ "ACTION_RETIRE",
67
+ "SOURCE_RULED_CLASSES",
68
+ "ChangeOutcome",
69
+ "ChangedBinding",
70
+ "confirm_current_item",
71
+ "rebind_item",
72
+ "retire_item",
73
+ "still_stale_after_confirm",
74
+ ]
75
+
76
+ _log = get_logger("adopt_knowledge")
77
+
78
+ #: The `--action` vocabulary, v6.1 §6 Build 6's demo line, spelled once.
79
+ ACTION_RETIRE: Final[str] = "retire"
80
+ ACTION_REBIND: Final[str] = "rebind"
81
+ ACTION_CONFIRM_CURRENT: Final[str] = "confirm-current"
82
+
83
+ ACTIONS: Final[tuple[str, ...]] = (ACTION_RETIRE, ACTION_REBIND, ACTION_CONFIRM_CURRENT)
84
+
85
+ #: The classes `resolve_freshness` stales from the **identity's** head status
86
+ #: rather than from the binding row -- so no binding write clears them. Typed
87
+ #: from the generated enum, which is the machine-gated spelling authority.
88
+ _DEAD: Final[ImpactClass] = "BINDING_DEAD"
89
+ _MOVED: Final[ImpactClass] = "BINDING_MOVED"
90
+ _SEMANTICS: Final[ImpactClass] = "BINDING_INTACT_SEMANTICS_CHANGED"
91
+
92
+ SOURCE_RULED_CLASSES: Final[frozenset[str]] = frozenset({_DEAD, _MOVED})
93
+
94
+ #: What a retirement records as its reason when the reviewer supplied none. The
95
+ #: revision needs a body -- the knowledge family carries its terminal state on
96
+ #: the parent and its reason in the text -- and "resolved from the queue" is the
97
+ #: truthful minimum rather than an empty string that reads as a lost value.
98
+ DEFAULT_RETIRE_REASON: Final[str] = "retired from the refresh review queue"
99
+
100
+
101
+ @dataclass(frozen=True, slots=True)
102
+ class ChangedBinding:
103
+ """One (item <-> changed identity) link a resolution acts on.
104
+
105
+ Assembled by the caller, which is the half that may read a store -- the same
106
+ split `PendingItem` uses, and for the same reason: what the reviewer was
107
+ shown and what the action operates on are provably the same tuple rather
108
+ than two queries run a moment apart.
109
+ """
110
+
111
+ binding_id: str
112
+ item_id: str
113
+ identity_id: str
114
+ identity_uri: str
115
+ impact_class: str
116
+ is_load_bearing: bool
117
+
118
+ @property
119
+ def is_source_ruled(self) -> bool:
120
+ """Whether staleness comes from the identity rather than from this row."""
121
+ return self.impact_class in SOURCE_RULED_CLASSES
122
+
123
+
124
+ @dataclass(frozen=True, slots=True)
125
+ class ChangeOutcome:
126
+ """What resolving one change item did -- every field a store consequence.
127
+
128
+ A separate value from `review.Outcome` rather than four more optional fields
129
+ on it: the other three populations answer "is this proposal right?" and
130
+ carry bindings and a revision, while this one answers "what happened to the
131
+ system, and what should the note do about it?". One dataclass covering both
132
+ would have every field optional, and a payload whose meaning depends on
133
+ which half is populated is two contracts wearing one name.
134
+ """
135
+
136
+ action: str
137
+ resolution: ReviewResolution
138
+ revision_id: str | None = None
139
+ provenance_ids: tuple[str, ...] = ()
140
+ superseded_bindings: tuple[str, ...] = ()
141
+ new_binding_id: str | None = None
142
+ freshened_bindings: tuple[str, ...] = ()
143
+ #: Referents whose staleness this action could not clear, by URI. Populated
144
+ #: only by `confirm-current`, and reported rather than suppressed: an item
145
+ #: that stays STALE after a confirmation needs the reason on screen.
146
+ still_stale: tuple[str, ...] = ()
147
+
148
+
149
+ def still_stale_after_confirm(affected: Sequence[ChangedBinding]) -> tuple[str, ...]:
150
+ """The referent URIs a `confirm-current` leaves stale, sorted.
151
+
152
+ Exposed rather than kept private because the CLI has to say the sentence
153
+ *before* the reviewer commits to the action as well as after it, and two
154
+ implementations of "which causes are source-ruled" would eventually disagree
155
+ about the one thing this build is careful to be honest about.
156
+ """
157
+ return tuple(sorted({link.identity_uri for link in affected if link.is_source_ruled}))
158
+
159
+
160
+ def retire_item(
161
+ item: PendingItem,
162
+ *,
163
+ reviews: ReviewWriter,
164
+ knowledge: ItemRetirer,
165
+ unit: UnitOfWork,
166
+ reason: str = DEFAULT_RETIRE_REASON,
167
+ actor_id: str | None = None,
168
+ ) -> ChangeOutcome:
169
+ """End the item: append its terminal revision, and stamp the queue `corrected`.
170
+
171
+ `corrected` rather than `rejected`, because the reviewer is not saying the
172
+ entry was wrong to appear -- the change was real and the queue was right to
173
+ ask. They are saying the *note* is finished, which is a correction to the
174
+ knowledge (D12).
175
+
176
+ **Nothing is deleted, here or anywhere.** The item keeps every revision it
177
+ ever had and stays readable: coverage provenance depends on it (PRD F6.7),
178
+ and `resolve_freshness` reports `retired` -- which is what lets `adopt ask`
179
+ answer "that was withdrawn" instead of falling silent.
180
+
181
+ **One transaction, added by T1.3/T1.4** (B2-03). The ordering below is
182
+ unchanged and still deliberate; what was missing was the boundary. A
183
+ resolution that committed and a write that then failed left the queue
184
+ entry stamped and the work undone, and `_resolve` refuses the retry -- so
185
+ the store recorded a human decision that never took effect and nothing
186
+ could correct it.
187
+ """
188
+ _require_change_item(item, ACTION_RETIRE)
189
+ with unit.transaction():
190
+ _resolve(reviews, item, CORRECTED)
191
+ revision_id = knowledge.retire(item_id=item.item_id, reason=reason, actor_id=actor_id)
192
+ _log.info(
193
+ "change.resolved",
194
+ action=ACTION_RETIRE,
195
+ review_item=item.review_item_id,
196
+ batch=item.review_batch_id,
197
+ revision=revision_id,
198
+ )
199
+ return ChangeOutcome(action=ACTION_RETIRE, resolution=CORRECTED, revision_id=revision_id)
200
+
201
+
202
+ def rebind_item(
203
+ item: PendingItem,
204
+ *,
205
+ reviews: ReviewWriter,
206
+ bindings: BindingSuperseder,
207
+ unit: UnitOfWork,
208
+ affected: Sequence[ChangedBinding],
209
+ target_identity_id: str,
210
+ target_uri: str,
211
+ actor_id: str | None = None,
212
+ ) -> ChangeOutcome:
213
+ """Re-point the item at the referent that replaced the changed one.
214
+
215
+ Two writes, in this order and for this reason: every load-bearing link to
216
+ the changed referent appends `moved` -- *replaced*, not withdrawn -- and one
217
+ new binding is created to the target. Superseding first means there is never
218
+ an instant in which the item is anchored to two live links, which is what a
219
+ coverage recount happening between the writes would otherwise see.
220
+
221
+ The new binding is **load-bearing**, on `review.confirm`'s precedent: only
222
+ load-bearing links are superseded, and a human who named the successor has
223
+ supplied the same standard of evidence a structural match does.
224
+
225
+ Raises:
226
+ AdoptError: ``REVIEW_ITEM_NOT_FOUND`` when the entry is not a change
227
+ item, and ``BIND_TARGET_NOT_FOUND`` when no load-bearing link to a
228
+ changed referent exists to re-point. The second refusal matters:
229
+ without it a rebind on an unaffected item would quietly add a
230
+ binding to a successor and supersede nothing, leaving the item bound
231
+ to both.
232
+ """
233
+ _require_change_item(item, ACTION_REBIND)
234
+ superseded_links = tuple(
235
+ link for link in affected if link.is_load_bearing and link.item_id == item.item_id
236
+ )
237
+ if not superseded_links:
238
+ raise AdoptError(
239
+ ErrorCode.BIND_TARGET_NOT_FOUND,
240
+ message=f"review item {item.review_item_id} has no load-bearing link to a "
241
+ "changed referent, so there is nothing to re-point",
242
+ hint="Rebind replaces a link that a change made wrong. An item whose bindings "
243
+ "are all intact is confirmed or retired, not rebound -- adding the new "
244
+ "binding alone would leave it bound to both referents.",
245
+ )
246
+
247
+ superseded: list[str] = []
248
+ with unit.transaction():
249
+ _resolve(reviews, item, CORRECTED)
250
+
251
+ for link in sorted(superseded_links, key=lambda row: row.binding_id):
252
+ bindings.supersede(binding_id=link.binding_id, actor_id=actor_id)
253
+ superseded.append(link.binding_id)
254
+
255
+ new_binding_id, _ = bindings.bind(
256
+ item_id=item.item_id,
257
+ identity_id=target_identity_id,
258
+ is_load_bearing=True,
259
+ extractor=EXTRACTOR_NAME_CONFIRMED,
260
+ extractor_version=INGEST_EXTRACTOR_VERSION,
261
+ actor_id=actor_id,
262
+ )
263
+
264
+ _log.info(
265
+ "change.resolved",
266
+ action=ACTION_REBIND,
267
+ review_item=item.review_item_id,
268
+ batch=item.review_batch_id,
269
+ superseded=len(superseded),
270
+ )
271
+ return ChangeOutcome(
272
+ action=ACTION_REBIND,
273
+ resolution=CORRECTED,
274
+ superseded_bindings=tuple(superseded),
275
+ new_binding_id=new_binding_id,
276
+ )
277
+
278
+
279
+ def confirm_current_item(
280
+ item: PendingItem,
281
+ *,
282
+ reviews: ReviewWriter,
283
+ knowledge: KnowledgeWriter,
284
+ freshener: BindingFreshener,
285
+ unit: UnitOfWork,
286
+ affected: Sequence[ChangedBinding],
287
+ actor_id: str | None = None,
288
+ ) -> ChangeOutcome:
289
+ """Re-affirm the note as written, and return the links it re-affirmed to `fresh`.
290
+
291
+ The revision carries the **current head body unchanged** and is
292
+ `human_confirmed` / `verified` with `human` provenance -- the same append
293
+ `review.confirm` makes for a candidate, through the same helper. There is no
294
+ argument that makes it `artifact_observed`: nothing was re-read from an
295
+ artifact, a person read what we already had and said it still holds.
296
+
297
+ Only links whose cause is SEMANTICS-CHANGED are freshened. A DEAD or MOVED
298
+ cause is source-ruled -- `resolve_freshness` reads the identity's own head
299
+ status, which no binding write reaches -- so those URIs come back on the
300
+ outcome as `still_stale` for the caller to name. That is the honest answer
301
+ and not a defect: the referent is gone, and the actions that help are
302
+ `retire` and `rebind`.
303
+ """
304
+ _require_change_item(item, ACTION_CONFIRM_CURRENT)
305
+ with unit.transaction():
306
+ _resolve(reviews, item, CONFIRMED)
307
+
308
+ revision_id, provenance_ids = _append_human_revision(
309
+ item,
310
+ knowledge=knowledge,
311
+ body_md=item.body_md,
312
+ source_ref=item.review_item_id,
313
+ actor_id=actor_id,
314
+ )
315
+
316
+ reaffirmed = [
317
+ link.binding_id
318
+ for link in affected
319
+ if link.item_id == item.item_id
320
+ and link.is_load_bearing
321
+ and link.impact_class == _SEMANTICS
322
+ ]
323
+ freshened = freshener.freshen_bindings(reaffirmed)
324
+
325
+ _log.info(
326
+ "change.resolved",
327
+ action=ACTION_CONFIRM_CURRENT,
328
+ review_item=item.review_item_id,
329
+ batch=item.review_batch_id,
330
+ revision=revision_id,
331
+ freshened=len(freshened),
332
+ )
333
+ return ChangeOutcome(
334
+ action=ACTION_CONFIRM_CURRENT,
335
+ resolution=CONFIRMED,
336
+ revision_id=revision_id,
337
+ provenance_ids=provenance_ids,
338
+ freshened_bindings=freshened,
339
+ still_stale=still_stale_after_confirm(
340
+ [link for link in affected if link.item_id == item.item_id]
341
+ ),
342
+ )
343
+
344
+
345
+ def _require_change_item(item: PendingItem, action: str) -> None:
346
+ """Refuse an action on a population it does not belong to.
347
+
348
+ The message names what the item **is** rather than reporting it absent,
349
+ which is `_targets`' own rule (CR-38's precedent): the id was perfectly
350
+ correct and sending its operator to hunt for a typo would be the second
351
+ time this queue made that mistake.
352
+
353
+ Raises:
354
+ AdoptError: ``REVIEW_ITEM_NOT_FOUND``, the registered usage code for a
355
+ queue entry an invocation cannot act on. Build 6 adds no error code
356
+ (plan decision D14).
357
+ """
358
+ if item.source not in CHANGE_POPULATIONS:
359
+ raise AdoptError(
360
+ ErrorCode.REVIEW_ITEM_NOT_FOUND,
361
+ message=f"review item {item.review_item_id} belongs to the {item.source!r} "
362
+ f"population, and --action {action} resolves change items",
363
+ hint="A suggestion or a candidate is answered with --confirm, --reject or "
364
+ "--edit. The three change actions describe what happened to a *referent*, "
365
+ "which is not what those items are about.",
366
+ )
367
+
368
+
369
+ def _resolve(reviews: ReviewWriter, item: PendingItem, resolution: ReviewResolution) -> None:
370
+ """Stamp the disposition first, so a second resolution is refused.
371
+
372
+ The returned row is deliberately dropped: everything an action operates on
373
+ travels on the `PendingItem` and the `ChangedBinding`s the caller assembled,
374
+ so re-reading it here would introduce a second, later view of the same
375
+ subject -- exactly what `PendingItem`'s docstring says the value exists to
376
+ prevent.
377
+ """
378
+ reviews.resolve(review_item_id=item.review_item_id, resolution=resolution)