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.
- adopt_knowledge/__init__.py +246 -0
- adopt_knowledge/changes.py +378 -0
- adopt_knowledge/documents.py +321 -0
- adopt_knowledge/drafting.py +615 -0
- adopt_knowledge/gaps.py +219 -0
- adopt_knowledge/gitlog.py +294 -0
- adopt_knowledge/harvest.py +454 -0
- adopt_knowledge/ingest.py +334 -0
- adopt_knowledge/matchers.py +326 -0
- adopt_knowledge/ports.py +204 -0
- adopt_knowledge/py.typed +0 -0
- adopt_knowledge/review.py +535 -0
- adopt_knowledge-0.4.0.dist-info/METADATA +20 -0
- adopt_knowledge-0.4.0.dist-info/RECORD +17 -0
- adopt_knowledge-0.4.0.dist-info/WHEEL +4 -0
- adopt_knowledge-0.4.0.dist-info/licenses/LICENSE +201 -0
- adopt_knowledge-0.4.0.dist-info/licenses/NOTICE +39 -0
|
@@ -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)
|