adopt-cli 0.4.0__tar.gz → 0.4.2__tar.gz
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_cli-0.4.0 → adopt_cli-0.4.2}/PKG-INFO +1 -1
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/pyproject.toml +1 -1
- adopt_cli-0.4.2/src/adopt_cli/_build_info.py +6 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/_ask_support.py +44 -3
- adopt_cli-0.4.2/src/adopt_cli/commands/_conflict_support.py +52 -0
- adopt_cli-0.4.2/src/adopt_cli/commands/_coverage_support.py +69 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/_handover_support.py +87 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/_pack_support.py +4 -3
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/answer.py +38 -15
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/boundary.py +2 -2
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/coverage.py +45 -6
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/handover.py +85 -7
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/interchange.py +5 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/knowledge.py +90 -80
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/policy.py +2 -2
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/refresh.py +13 -8
- adopt_cli-0.4.0/src/adopt_cli/_build_info.py +0 -6
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/.gitignore +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/LICENSE +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/NOTICE +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/__init__.py +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/__init__.py +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/_ci_sense_probes.py +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/_ci_sense_support.py +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/_draft_support.py +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/_drafting.py +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/_knowledge_support.py +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/_map_support.py +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/_probe_support.py +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/_refresh_support.py +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/_remote_support.py +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/_review_remote.py +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/agent.py +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/ask.py +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/ci_sense.py +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/detect.py +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/doctor.py +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/draft.py +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/freshness.py +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/identity.py +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/init.py +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/map_command.py +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/pack.py +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/probe.py +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/pull.py +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/serve.py +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/store.py +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/commands/version.py +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/config.py +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/json_out.py +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/main.py +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/py.typed +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/remote.py +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/replica.py +0 -0
- {adopt_cli-0.4.0 → adopt_cli-0.4.2}/src/adopt_cli/store_option.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: adopt-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: The adopt command surface: config resolution, JSON mode, exit codes.
|
|
5
5
|
Project-URL: Homepage, https://github.com/onboardux/onboard-core
|
|
6
6
|
Project-URL: Source, https://github.com/onboardux/onboard-core
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Generated by scripts/embed_build_info.py; do not edit in a release build."""
|
|
2
|
+
|
|
3
|
+
from typing import Final
|
|
4
|
+
|
|
5
|
+
SBOM_SHA256: Final[str | None] = 'd9c82e3f0197e2d81b8ef92be4bdbb2f9f38710ee54d8db9ca3baa099047fda8'
|
|
6
|
+
BUILD_ID: Final[str | None] = 'github:onboardux/onboard-core:37286165801:1:b50f2ec2a85b4271c0d445d8432b105574a8978a'
|
|
@@ -114,7 +114,7 @@ def answer_question(
|
|
|
114
114
|
"""
|
|
115
115
|
from adopt_ask import compose, guard, json_payload, render, retrieve
|
|
116
116
|
from adopt_ask.branch import Resolved
|
|
117
|
-
from adopt_ask.escalate import consented, escalate, may_escalate
|
|
117
|
+
from adopt_ask.escalate import consented, escalate, may_dispute, may_escalate
|
|
118
118
|
from adopt_ask.questionlog import log_question, should_log
|
|
119
119
|
from adopt_ask.synthesis import SYNTHESIS_PROMPT_REF, render_with
|
|
120
120
|
|
|
@@ -134,11 +134,22 @@ def answer_question(
|
|
|
134
134
|
[candidate.passage.revision_id for candidate in candidates]
|
|
135
135
|
)
|
|
136
136
|
|
|
137
|
+
from adopt_cli.commands._conflict_support import contradicted_now
|
|
138
|
+
|
|
137
139
|
freshness_records = handle.freshness_records()
|
|
140
|
+
# A revision a drifted probe currently contradicts is served STALE, whatever
|
|
141
|
+
# its bindings say, so `ask` agrees with the pack's "Contradicted by
|
|
142
|
+
# observation" from the moment `adopt probe run` records the conflict rather
|
|
143
|
+
# than from the next `refresh` (independence transcript T3a).
|
|
144
|
+
contradicted = contradicted_now(handle) if candidates else frozenset()
|
|
138
145
|
resolved = tuple(
|
|
139
146
|
Resolved(
|
|
140
147
|
candidate=candidate,
|
|
141
|
-
freshness=
|
|
148
|
+
freshness=_unless_contradicted(
|
|
149
|
+
resolve_freshness(freshness_records, candidate.passage.item_id, clock=clock),
|
|
150
|
+
candidate.passage.revision_id,
|
|
151
|
+
contradicted,
|
|
152
|
+
),
|
|
142
153
|
)
|
|
143
154
|
for candidate in candidates
|
|
144
155
|
)
|
|
@@ -224,10 +235,21 @@ def answer_question(
|
|
|
224
235
|
payload["escalation_id"] = escalation_id
|
|
225
236
|
payload["routed_to_plane"] = routed_to_plane
|
|
226
237
|
where = "on the control plane" if routed_to_plane else "in this store"
|
|
227
|
-
|
|
238
|
+
if may_dispute(answer):
|
|
239
|
+
human += f"\n\nReported as wrong: open question {escalation_id} {where}."
|
|
240
|
+
else:
|
|
241
|
+
human += f"\n\nRecorded as open question {escalation_id} {where}."
|
|
228
242
|
human += f'\nAnswer it with: adopt answer {escalation_id} --text "..."'
|
|
229
243
|
if routed_to_plane:
|
|
230
244
|
human += "\nIts owner has been routed a draft reply."
|
|
245
|
+
elif may_dispute(answer) and escalate_flag:
|
|
246
|
+
# Remote mode only: the plane composes its own answer, and opens no
|
|
247
|
+
# question for one it answered KNOWN. Said out loud, because the silent
|
|
248
|
+
# version of this (exit 0, nothing recorded) is the defect being fixed.
|
|
249
|
+
human += (
|
|
250
|
+
"\n\nThe control plane answered this KNOWN and recorded no dispute. "
|
|
251
|
+
"Tell the system's owner directly, or correct the cited item with `adopt review`."
|
|
252
|
+
)
|
|
231
253
|
elif may_escalate(answer) and not escalate_flag:
|
|
232
254
|
human += "\n\nRecord it as an open question with `--escalate`."
|
|
233
255
|
|
|
@@ -236,6 +258,25 @@ def answer_question(
|
|
|
236
258
|
)
|
|
237
259
|
|
|
238
260
|
|
|
261
|
+
def _unless_contradicted(resolution: Any, revision_id: str, contradicted: frozenset[str]) -> Any:
|
|
262
|
+
"""`resolution`, or STALE with `contradicted_by_observation` if a probe disagrees.
|
|
263
|
+
|
|
264
|
+
Only a resolution that would otherwise serve as KNOWN is replaced: an item
|
|
265
|
+
already stale keeps the rule that staled it, because that rule is the more
|
|
266
|
+
specific statement of what went wrong.
|
|
267
|
+
"""
|
|
268
|
+
from adopt_freshness import RULE_CONTRADICTED_BY_OBSERVATION, FreshnessResolution
|
|
269
|
+
|
|
270
|
+
if revision_id not in contradicted or resolution.state not in ("fresh", "unverified"):
|
|
271
|
+
return resolution
|
|
272
|
+
return FreshnessResolution(
|
|
273
|
+
item_id=resolution.item_id,
|
|
274
|
+
state="stale",
|
|
275
|
+
level="knowledge_revision",
|
|
276
|
+
deciding_rule=RULE_CONTRADICTED_BY_OBSERVATION,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
|
|
239
280
|
def _synthesize(answer: Any, *, question: str, config: Mapping[str, str | None]) -> Any:
|
|
240
281
|
"""One optional model call. `None` for every reason not to have made it.
|
|
241
282
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""The two reads every surface needs to agree about a probe contradiction.
|
|
2
|
+
|
|
3
|
+
`adopt gaps`, `adopt pack`, `adopt handover` and `adopt ask` all report on Bet 4's
|
|
4
|
+
`conflict` rows, and until 2026-10-05 they disagreed: the pack listed a runbook
|
|
5
|
+
as "Contradicted by observation" while `ask` served it KNOWN and fresh
|
|
6
|
+
(independence transcript T3a). Each now applies the same rule
|
|
7
|
+
(`adopt_knowledge.rank_conflicts` / `contradicted_revisions`), fed by these two
|
|
8
|
+
reads.
|
|
9
|
+
|
|
10
|
+
Both are `table_rows` reads -- Build 4's report pattern -- so no query path is
|
|
11
|
+
added to any realized port and the plane's escape-coverage denominator does not
|
|
12
|
+
move.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
__all__ = ["conflict_rows", "contradicted_now", "superseded_revisions"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def conflict_rows(handle: Any) -> tuple[Any, ...]:
|
|
21
|
+
"""Every `conflict` row in the store."""
|
|
22
|
+
from adopt_model import Conflict
|
|
23
|
+
|
|
24
|
+
return tuple(handle.export_records().table_rows("conflict", Conflict))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def superseded_revisions(handle: Any) -> frozenset[str]:
|
|
28
|
+
"""Knowledge revision ids that are no longer their item's head.
|
|
29
|
+
|
|
30
|
+
A conflict against one of these contradicts a claim nobody is making any
|
|
31
|
+
more: the item was re-confirmed or rewritten after the probe drifted.
|
|
32
|
+
"""
|
|
33
|
+
from adopt_model import KnowledgeItem, KnowledgeRevision
|
|
34
|
+
|
|
35
|
+
records = handle.export_records()
|
|
36
|
+
heads = {
|
|
37
|
+
item.current_revision_id
|
|
38
|
+
for item in records.table_rows("knowledge_item", KnowledgeItem)
|
|
39
|
+
if item.current_revision_id is not None
|
|
40
|
+
}
|
|
41
|
+
return frozenset(
|
|
42
|
+
revision.id
|
|
43
|
+
for revision in records.table_rows("knowledge_revision", KnowledgeRevision)
|
|
44
|
+
if revision.id not in heads
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def contradicted_now(handle: Any) -> frozenset[str]:
|
|
49
|
+
"""Head revisions a drifted probe currently contradicts."""
|
|
50
|
+
from adopt_knowledge import contradicted_revisions
|
|
51
|
+
|
|
52
|
+
return contradicted_revisions(conflict_rows(handle), superseded=superseded_revisions(handle))
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Keeping the coverage cache current after a command that itself changed coverage.
|
|
2
|
+
|
|
3
|
+
**The alarm was firing on the product's own writes.** `identity.covered_cache`
|
|
4
|
+
is rebuilt *from* `recompute_coverage()` and never the reverse, and any
|
|
5
|
+
disagreement between the two alarms (`COVERAGE_CACHE_DISAGREEMENT`) rather than
|
|
6
|
+
self-healing. But only `adopt coverage recompute --rebuild` ever wrote the
|
|
7
|
+
cache, so every `ingest`, `bind`, `answer`, review resolution and `refresh`
|
|
8
|
+
left it disagreeing, and the next `adopt gaps` paged about a change the product
|
|
9
|
+
had just made on purpose. An alarm that fires on every ordinary write is one an
|
|
10
|
+
operator learns to ignore, which is exactly when the real one -- a cache some
|
|
11
|
+
defect wrote -- would go unread.
|
|
12
|
+
|
|
13
|
+
**The rule that keeps the alarm meaningful: refresh only a cache that agreed.**
|
|
14
|
+
The command recomputes first. If the cache already disagreed, something other
|
|
15
|
+
than this command wrote it, and `store.py`'s incident rule applies -- rebuilding
|
|
16
|
+
a disagreeing cache destroys the evidence of whatever wrote it -- so the cache is
|
|
17
|
+
left alone and the alarm keeps firing. Only when it agreed beforehand does the
|
|
18
|
+
command rebuild it after its own writes, from a recompute that does not compare,
|
|
19
|
+
because the comparison would only report the change this command intended.
|
|
20
|
+
|
|
21
|
+
The rebuild goes through `adopt_coverage.rebuild_cache`, so `no-covered-cache-write`
|
|
22
|
+
still holds: the CLI decides *when*, and `adopt_coverage` remains the only writer.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from collections.abc import Iterator
|
|
26
|
+
from contextlib import contextmanager
|
|
27
|
+
from typing import Protocol
|
|
28
|
+
|
|
29
|
+
from adopt_coverage import CacheWriter, CoverageRecords
|
|
30
|
+
|
|
31
|
+
__all__ = ["CoverageStore", "coverage_cache_kept_current"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CoverageStore(Protocol):
|
|
35
|
+
"""The two things this module needs from a store handle, structurally.
|
|
36
|
+
|
|
37
|
+
Declared rather than importing the SQLite handle, so `no-raw-sqlite` holds:
|
|
38
|
+
`adopt_coverage`'s own ports are the whole surface.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def backend(self) -> CacheWriter: ...
|
|
43
|
+
def coverage_records(self) -> CoverageRecords: ...
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@contextmanager
|
|
47
|
+
def coverage_cache_kept_current(handle: CoverageStore, system_id: str | None) -> Iterator[None]:
|
|
48
|
+
"""Wrap a command body that changes coverage inputs for `system_id`.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
handle: An open, writable store handle.
|
|
52
|
+
system_id: The system whose coverage the body may change. `None` (a
|
|
53
|
+
command that resolved no system) wraps nothing.
|
|
54
|
+
|
|
55
|
+
The post-write rebuild runs only when the body returns normally: a body that
|
|
56
|
+
raised has rolled its own transaction back, so there is nothing to follow.
|
|
57
|
+
"""
|
|
58
|
+
if system_id is None:
|
|
59
|
+
yield
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
from adopt_coverage import rebuild_cache, recompute_coverage
|
|
63
|
+
|
|
64
|
+
before = recompute_coverage(handle.coverage_records(), system_id)
|
|
65
|
+
yield
|
|
66
|
+
if before.disagreements:
|
|
67
|
+
return
|
|
68
|
+
after = recompute_coverage(handle.coverage_records(), system_id, compare_cache=False)
|
|
69
|
+
rebuild_cache(handle.backend, after)
|
|
@@ -45,11 +45,13 @@ __all__ = [
|
|
|
45
45
|
"ResolvedSystem",
|
|
46
46
|
"close_handover",
|
|
47
47
|
"current_owner_of",
|
|
48
|
+
"independence_of",
|
|
48
49
|
"open_conflicts",
|
|
49
50
|
"open_gaps",
|
|
50
51
|
"open_questions",
|
|
51
52
|
"opening_position",
|
|
52
53
|
"records_for",
|
|
54
|
+
"referent_facts",
|
|
53
55
|
"refuse_if_replica",
|
|
54
56
|
"require_open",
|
|
55
57
|
"resolve_system",
|
|
@@ -345,6 +347,91 @@ def open_conflicts(
|
|
|
345
347
|
return build_conflicts(handle, uris=uris)
|
|
346
348
|
|
|
347
349
|
|
|
350
|
+
def referent_facts(handle: Any, *, system: ResolvedSystem, uris: set[str]) -> dict[str, Any]:
|
|
351
|
+
"""What is true now of each drilled referent -- the input `validity.assess` judges.
|
|
352
|
+
|
|
353
|
+
Every read is a report read (`table_rows`, or a coverage port method that
|
|
354
|
+
already exists), so independence status adds no query path to any realized
|
|
355
|
+
port. The URI is looked up **exactly**, not through `resolve_identity`'s alias
|
|
356
|
+
walk: following a moved identity to its successor would report the successor
|
|
357
|
+
`active` and hide the very move that invalidates the drill.
|
|
358
|
+
"""
|
|
359
|
+
from adopt_handover.validity import ReferentFacts
|
|
360
|
+
|
|
361
|
+
from adopt_cli.commands._conflict_support import conflict_rows, superseded_revisions
|
|
362
|
+
from adopt_freshness import resolve_freshness
|
|
363
|
+
from adopt_model import Classification, Identity
|
|
364
|
+
|
|
365
|
+
if not uris:
|
|
366
|
+
return {}
|
|
367
|
+
records = handle.export_records()
|
|
368
|
+
by_uri = {row.uri: row for row in records.table_rows("identity", Identity) if row.uri in uris}
|
|
369
|
+
coverage = handle.coverage_records()
|
|
370
|
+
statuses = coverage.head_identity_statuses(system_id=system.system_id, environment_id=None)
|
|
371
|
+
wanted = {row.id for row in by_uri.values()}
|
|
372
|
+
|
|
373
|
+
changes: dict[str, list[tuple[str, _dt.datetime]]] = {}
|
|
374
|
+
for row in records.table_rows("classification", Classification):
|
|
375
|
+
if row.identity_id in wanted:
|
|
376
|
+
changes.setdefault(row.identity_id, []).append((str(row.class_), row.created_at))
|
|
377
|
+
|
|
378
|
+
binding_statuses = coverage.head_binding_statuses(
|
|
379
|
+
system_id=system.system_id, environment_id=None
|
|
380
|
+
)
|
|
381
|
+
verifications = coverage.head_item_verifications(system_id=system.system_id)
|
|
382
|
+
items_by_identity: dict[str, set[str]] = {}
|
|
383
|
+
for binding in coverage.bindings_in_scope(system_id=system.system_id, environment_id=None):
|
|
384
|
+
if (
|
|
385
|
+
binding.identity_id in wanted
|
|
386
|
+
and binding.is_load_bearing
|
|
387
|
+
and binding_statuses.get(binding.id) == "active"
|
|
388
|
+
and verifications.get(binding.item_id) == "verified"
|
|
389
|
+
):
|
|
390
|
+
items_by_identity.setdefault(binding.identity_id, set()).add(binding.item_id)
|
|
391
|
+
|
|
392
|
+
superseded = superseded_revisions(handle)
|
|
393
|
+
contradicted_identities = {
|
|
394
|
+
row.identity_id
|
|
395
|
+
for row in conflict_rows(handle)
|
|
396
|
+
if str(row.disposition) == "open"
|
|
397
|
+
and row.intent_revision_id is not None
|
|
398
|
+
and row.intent_revision_id not in superseded
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
freshness_records = handle.freshness_records()
|
|
402
|
+
facts: dict[str, Any] = {}
|
|
403
|
+
for uri in uris:
|
|
404
|
+
identity = by_uri.get(uri)
|
|
405
|
+
if identity is None:
|
|
406
|
+
facts[uri] = ReferentFacts(found=False)
|
|
407
|
+
continue
|
|
408
|
+
stale_rule: str | None = None
|
|
409
|
+
degraded_rule: str | None = None
|
|
410
|
+
for item_id in sorted(items_by_identity.get(identity.id, ())):
|
|
411
|
+
resolution = resolve_freshness(freshness_records, item_id)
|
|
412
|
+
if resolution.state == "stale" and stale_rule is None:
|
|
413
|
+
stale_rule = resolution.deciding_rule
|
|
414
|
+
elif resolution.state == "observation_stale" and degraded_rule is None:
|
|
415
|
+
degraded_rule = resolution.deciding_rule
|
|
416
|
+
facts[uri] = ReferentFacts(
|
|
417
|
+
found=True,
|
|
418
|
+
status=statuses.get(identity.id),
|
|
419
|
+
changes=tuple(changes.get(identity.id, ())),
|
|
420
|
+
contradicted=identity.id in contradicted_identities,
|
|
421
|
+
stale_rule=stale_rule,
|
|
422
|
+
degraded_rule=degraded_rule,
|
|
423
|
+
)
|
|
424
|
+
return facts
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def independence_of(handle: Any, *, system: ResolvedSystem, record: Any) -> Any:
|
|
428
|
+
"""Each passed exit test's validity now (`adopt_handover.validity`)."""
|
|
429
|
+
from adopt_handover.validity import assess, passed_tasks
|
|
430
|
+
|
|
431
|
+
uris = {task.uri for task in passed_tasks(record) if task.uri is not None}
|
|
432
|
+
return assess(record, referent_facts(handle, system=system, uris=uris))
|
|
433
|
+
|
|
434
|
+
|
|
348
435
|
def open_questions(handle: Any, *, system_id: str) -> tuple[OpenQuestion, ...]:
|
|
349
436
|
"""Unanswered questions for this system, newest first."""
|
|
350
437
|
return tuple(
|
|
@@ -203,9 +203,8 @@ def build_conflicts(handle: Any, *, uris: dict[str, str]) -> tuple[PackConflict,
|
|
|
203
203
|
"""
|
|
204
204
|
from adopt_knowledge import rank_conflicts
|
|
205
205
|
|
|
206
|
-
from
|
|
206
|
+
from adopt_cli.commands._conflict_support import conflict_rows, superseded_revisions
|
|
207
207
|
|
|
208
|
-
rows = handle.export_records().table_rows("conflict", Conflict)
|
|
209
208
|
return tuple(
|
|
210
209
|
PackConflict(
|
|
211
210
|
uri=conflict.uri,
|
|
@@ -213,7 +212,9 @@ def build_conflicts(handle: Any, *, uris: dict[str, str]) -> tuple[PackConflict,
|
|
|
213
212
|
intent_revision_id=conflict.intent_revision_id,
|
|
214
213
|
detected_at=conflict.detected_at,
|
|
215
214
|
)
|
|
216
|
-
for conflict in rank_conflicts(
|
|
215
|
+
for conflict in rank_conflicts(
|
|
216
|
+
conflict_rows(handle), uris, superseded=superseded_revisions(handle)
|
|
217
|
+
)
|
|
217
218
|
)
|
|
218
219
|
|
|
219
220
|
|
|
@@ -63,6 +63,8 @@ class _Items(Protocol):
|
|
|
63
63
|
self, *, revision_id: str, source_type: SourceType, source_ref: str
|
|
64
64
|
) -> str: ...
|
|
65
65
|
|
|
66
|
+
def tag_audience(self, *, item_id: str, audience: str) -> bool: ...
|
|
67
|
+
|
|
66
68
|
|
|
67
69
|
class _Bindings(Protocol):
|
|
68
70
|
def create(
|
|
@@ -113,6 +115,14 @@ ScopeOption = Annotated[
|
|
|
113
115
|
str | None,
|
|
114
116
|
typer.Option("--scope", help="firm/engagement/system/environment. Defaults to the store's."),
|
|
115
117
|
]
|
|
118
|
+
AudienceOption = Annotated[
|
|
119
|
+
list[str] | None,
|
|
120
|
+
typer.Option(
|
|
121
|
+
"--audience",
|
|
122
|
+
help="Who the answer is for. Repeatable. Defaults to the same audience "
|
|
123
|
+
"`adopt ingest` gives an untagged document.",
|
|
124
|
+
),
|
|
125
|
+
]
|
|
116
126
|
ActorOption = Annotated[str | None, typer.Option("--actor", help="Who answered.")]
|
|
117
127
|
JsonOption = Annotated[bool, typer.Option("--json", help="Emit the strict JSON envelope only.")]
|
|
118
128
|
|
|
@@ -124,11 +134,12 @@ def answer(
|
|
|
124
134
|
title: TitleOption = None,
|
|
125
135
|
scope: ScopeOption = None,
|
|
126
136
|
store: StoreOption = None,
|
|
137
|
+
audience: AudienceOption = None,
|
|
127
138
|
actor: ActorOption = None,
|
|
128
139
|
json_output: JsonOption = False,
|
|
129
140
|
) -> None:
|
|
130
141
|
"""Bank a human's answer as confirmed knowledge and resolve the question."""
|
|
131
|
-
from adopt_ask.capture import capture_answer, identities_to_bind
|
|
142
|
+
from adopt_ask.capture import DEFAULT_AUDIENCES, capture_answer, identities_to_bind
|
|
132
143
|
|
|
133
144
|
from adopt_cli.commands._remote_support import configured_remote
|
|
134
145
|
|
|
@@ -158,20 +169,29 @@ def answer(
|
|
|
158
169
|
identity_ids, unmatched = identities_to_bind(
|
|
159
170
|
text, identity_views(handle, resolved_scope), explicit_uris=uri or []
|
|
160
171
|
)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
172
|
+
from adopt_cli.commands._coverage_support import coverage_cache_kept_current
|
|
173
|
+
|
|
174
|
+
system_id = resolved_scope.system.id if resolved_scope.system is not None else None
|
|
175
|
+
with coverage_cache_kept_current(handle, system_id):
|
|
176
|
+
result = capture_answer(
|
|
177
|
+
_StoreAdapter(handle),
|
|
178
|
+
escalation_id=escalation_id,
|
|
179
|
+
scope=resolved_scope,
|
|
180
|
+
# The question is the title, so the captured row is findable by
|
|
181
|
+
# the words somebody actually asked. Falling back to the answer's
|
|
182
|
+
# first line would title it with whatever the human happened to
|
|
183
|
+
# type first, which is not what the next asker will search for.
|
|
184
|
+
title=title or existing.question or f"Answer to {escalation_id}",
|
|
185
|
+
body_md=text,
|
|
186
|
+
identity_ids=identity_ids,
|
|
187
|
+
# `DEFAULT_AUDIENCES` rather than a literal here: the default
|
|
188
|
+
# lives in `adopt_ask.capture` beside the rule it protects, so the
|
|
189
|
+
# CLI and the plane cannot drift into tagging captured answers
|
|
190
|
+
# differently.
|
|
191
|
+
audiences=tuple(audience) if audience else DEFAULT_AUDIENCES,
|
|
192
|
+
actor_id=actor,
|
|
193
|
+
unmatched_uris=unmatched,
|
|
194
|
+
)
|
|
175
195
|
|
|
176
196
|
payload = {
|
|
177
197
|
"escalation_id": result.escalation_id,
|
|
@@ -248,6 +268,9 @@ class _StoreAdapter:
|
|
|
248
268
|
source_ref=source_ref,
|
|
249
269
|
)
|
|
250
270
|
|
|
271
|
+
def tag_audience(self, *, item_id: str, audience: str) -> object:
|
|
272
|
+
return self._handle.items().tag_audience(item_id=item_id, audience=audience)
|
|
273
|
+
|
|
251
274
|
def bind(self, *, item_id: str, identity_id: str, is_load_bearing: bool) -> str:
|
|
252
275
|
binding_id, _revision_id = self._handle.bindings().create(
|
|
253
276
|
item_id=item_id, identity_id=identity_id, is_load_bearing=is_load_bearing
|
|
@@ -57,8 +57,8 @@ ScopeOption = Annotated[
|
|
|
57
57
|
str | None,
|
|
58
58
|
typer.Option(
|
|
59
59
|
"--scope",
|
|
60
|
-
help="firm/engagement/system
|
|
61
|
-
"the store; omitted, it is computed and reported without writing.",
|
|
60
|
+
help="firm/engagement/system, optionally with /environment. Given, the boundary is "
|
|
61
|
+
"written to the store; omitted, it is computed and reported without writing.",
|
|
62
62
|
),
|
|
63
63
|
]
|
|
64
64
|
ArchetypeOption = Annotated[
|
|
@@ -30,12 +30,23 @@ app = typer.Typer(
|
|
|
30
30
|
)
|
|
31
31
|
|
|
32
32
|
SystemOption = Annotated[
|
|
33
|
-
str,
|
|
34
|
-
typer.Option("--system", help="
|
|
33
|
+
str | None,
|
|
34
|
+
typer.Option("--system", help="System id or slug to evaluate.", show_default=False),
|
|
35
|
+
]
|
|
36
|
+
ScopeOption = Annotated[
|
|
37
|
+
str | None,
|
|
38
|
+
typer.Option(
|
|
39
|
+
"--scope",
|
|
40
|
+
help="firm/engagement/system, optionally with /environment. The alternative to "
|
|
41
|
+
"--system, and the string `adopt init` already took.",
|
|
42
|
+
),
|
|
35
43
|
]
|
|
36
44
|
EnvironmentOption = Annotated[
|
|
37
45
|
str | None,
|
|
38
|
-
typer.Option(
|
|
46
|
+
typer.Option(
|
|
47
|
+
"--environment",
|
|
48
|
+
help="One environment id. Omit for every environment, or name one in --scope.",
|
|
49
|
+
),
|
|
39
50
|
]
|
|
40
51
|
StoreOption = Annotated[
|
|
41
52
|
Path | None,
|
|
@@ -53,13 +64,27 @@ JsonOption = Annotated[bool, typer.Option("--json", help="Emit the strict JSON e
|
|
|
53
64
|
|
|
54
65
|
@app.command()
|
|
55
66
|
def recompute(
|
|
56
|
-
system: SystemOption,
|
|
67
|
+
system: SystemOption = None,
|
|
68
|
+
scope: ScopeOption = None,
|
|
57
69
|
environment: EnvironmentOption = None,
|
|
58
70
|
store: StoreOption = None,
|
|
59
71
|
rebuild: RebuildOption = False,
|
|
60
72
|
json_output: JsonOption = False,
|
|
61
73
|
) -> None:
|
|
62
|
-
"""Evaluate the six inputs of contracts §6 for every identity in scope.
|
|
74
|
+
"""Evaluate the six inputs of contracts §6 for every identity in scope.
|
|
75
|
+
|
|
76
|
+
**`--scope` exists because this command is the remedy for an alarm, and the
|
|
77
|
+
remedy has to be reachable.** `COVERAGE_CACHE_DISAGREEMENT` fires on every
|
|
78
|
+
`adopt gaps` and `adopt pack` once a binding is confirmed, and clearing it
|
|
79
|
+
needs this verb -- which took a system **id** that no verb printed. `store
|
|
80
|
+
info` reports counts, `store doctor` reports findings, and the only command
|
|
81
|
+
that ever surfaced a system id was `adopt handover start`, which opens a
|
|
82
|
+
handover event as a side effect. An operator was left reading the SQLite
|
|
83
|
+
file by hand to silence an alarm the product raised at them, so the scope
|
|
84
|
+
string they typed into `adopt init` is now accepted here too.
|
|
85
|
+
"""
|
|
86
|
+
from adopt_cli.commands._handover_support import resolve_system
|
|
87
|
+
|
|
63
88
|
with open_configured_store(
|
|
64
89
|
store,
|
|
65
90
|
read_only=not rebuild,
|
|
@@ -69,7 +94,21 @@ def recompute(
|
|
|
69
94
|
# nothing a later `adopt pull` overwrites was canon.
|
|
70
95
|
non_canon_reason="--rebuild writes only the coverage cache, never canon",
|
|
71
96
|
) as handle:
|
|
72
|
-
|
|
97
|
+
# `resolve_system` rather than a second resolver: it already accepts an
|
|
98
|
+
# id, a slug or a scope path, refuses both at once, and with neither
|
|
99
|
+
# falls through to `resolve_scope`, which returns the store's only
|
|
100
|
+
# environment and **refuses rather than guessing** when there are
|
|
101
|
+
# several. A second implementation would be a second answer to "which
|
|
102
|
+
# system is this", which is how two verbs come to disagree about a store.
|
|
103
|
+
target = resolve_system(handle, system=system, scope=scope)
|
|
104
|
+
result = recompute_coverage(
|
|
105
|
+
handle.coverage_records(),
|
|
106
|
+
target.system_id,
|
|
107
|
+
# An explicit `--environment` wins over one named in `--scope`: it is
|
|
108
|
+
# the narrower statement of the two, and it names an id rather than a
|
|
109
|
+
# slug, so it cannot be the vaguer thing the operator meant.
|
|
110
|
+
environment or target.environment_id,
|
|
111
|
+
)
|
|
73
112
|
rebuilt = rebuild_cache(handle.backend, result) if rebuild else 0
|
|
74
113
|
|
|
75
114
|
payload = {
|
|
@@ -47,7 +47,8 @@ SystemOption = Annotated[
|
|
|
47
47
|
ScopeOption = Annotated[
|
|
48
48
|
str | None,
|
|
49
49
|
typer.Option(
|
|
50
|
-
"--scope",
|
|
50
|
+
"--scope",
|
|
51
|
+
help="firm/engagement/system, optionally with /environment. Narrows to one environment.",
|
|
51
52
|
),
|
|
52
53
|
]
|
|
53
54
|
OwnerOption = Annotated[
|
|
@@ -63,6 +64,14 @@ OutOption = Annotated[
|
|
|
63
64
|
]
|
|
64
65
|
StoreOption = Annotated[Path | None, typer.Option("--store", help="Path to the store.")]
|
|
65
66
|
JsonOption = Annotated[bool, typer.Option("--json", help="Machine-readable output.")]
|
|
67
|
+
StrictOption = Annotated[
|
|
68
|
+
bool,
|
|
69
|
+
typer.Option(
|
|
70
|
+
"--strict",
|
|
71
|
+
help="Exit 4 when any exit test the receiving team passed has since been invalidated "
|
|
72
|
+
"by a change. For a pipeline that gates on independence; off by default.",
|
|
73
|
+
),
|
|
74
|
+
]
|
|
66
75
|
AudienceOption = Annotated[
|
|
67
76
|
list[str] | None,
|
|
68
77
|
typer.Option("--audience", help="Emit this audience. Repeatable; defaults to all four."),
|
|
@@ -396,7 +405,11 @@ def verify(
|
|
|
396
405
|
raise AdoptError(
|
|
397
406
|
ErrorCode.HANDOVER_CHECKLIST_INVALID,
|
|
398
407
|
message=f"{checklist} is not valid YAML: {error}",
|
|
399
|
-
hint="
|
|
408
|
+
hint="A checklist is a mapping with a `tasks:` list, each task carrying `id`, "
|
|
409
|
+
"`task` and `outcome` (pass | fail | skipped). The annotated example is "
|
|
410
|
+
"docs/handover-checklist.example.yaml in the source tree, or "
|
|
411
|
+
"https://github.com/onboardux/onboard-core/blob/main/"
|
|
412
|
+
"docs/handover-checklist.example.yaml.",
|
|
400
413
|
) from error
|
|
401
414
|
if not isinstance(raw, dict):
|
|
402
415
|
raise AdoptError(
|
|
@@ -428,8 +441,18 @@ def verify(
|
|
|
428
441
|
question=question_for(task),
|
|
429
442
|
)
|
|
430
443
|
opened.append({"task_id": task.id, "escalation_id": escalation_id})
|
|
444
|
+
# `uri` is what lets the result go stale: it names the referent the
|
|
445
|
+
# task exercised, so a later change to it invalidates this pass
|
|
446
|
+
# (`adopt_handover.validity`). Until 2026-10-05 it was parsed and
|
|
447
|
+
# then dropped, and a passed task could never be invalidated.
|
|
431
448
|
outcomes.append(
|
|
432
|
-
{
|
|
449
|
+
{
|
|
450
|
+
"id": task.id,
|
|
451
|
+
"outcome": task.outcome,
|
|
452
|
+
"escalation_id": escalation_id,
|
|
453
|
+
"uri": task.uri,
|
|
454
|
+
"performed_by": task.performed_by,
|
|
455
|
+
}
|
|
433
456
|
)
|
|
434
457
|
|
|
435
458
|
detail = {
|
|
@@ -639,14 +662,21 @@ def close(
|
|
|
639
662
|
def status(
|
|
640
663
|
system: SystemOption = None,
|
|
641
664
|
scope: ScopeOption = None,
|
|
665
|
+
strict: StrictOption = False,
|
|
642
666
|
store: StoreOption = None,
|
|
643
667
|
json_output: JsonOption = False,
|
|
644
668
|
) -> None:
|
|
645
|
-
"""Every step recorded, what comes next,
|
|
669
|
+
"""Every step recorded, what comes next, what is still open -- and what still holds.
|
|
646
670
|
|
|
647
671
|
Reads only, and answers for a closed handover as readily as an open one --
|
|
648
672
|
the record is what both parties keep, and it must still describe itself a
|
|
649
673
|
year later.
|
|
674
|
+
|
|
675
|
+
**Independence leads.** Each exit test the receiving team passed is judged
|
|
676
|
+
against what has changed since (`adopt_handover.validity`): `valid`,
|
|
677
|
+
`invalidated` with its cause, `degraded`, or `unanchored` when the task named
|
|
678
|
+
no `uri`. A drill result that could not go stale would be a certificate;
|
|
679
|
+
this is the proof that says when it stopped being true.
|
|
650
680
|
"""
|
|
651
681
|
from adopt_handover import VERB_FOR, record_payload
|
|
652
682
|
|
|
@@ -695,12 +725,18 @@ def status(
|
|
|
695
725
|
system_owner=owner,
|
|
696
726
|
written_by=writer_identity(),
|
|
697
727
|
),
|
|
728
|
+
"independence": _independence_payload(
|
|
729
|
+
support.independence_of(handle, system=resolved, record=record)
|
|
730
|
+
),
|
|
698
731
|
}
|
|
699
732
|
finally:
|
|
700
733
|
handle.close()
|
|
701
734
|
|
|
735
|
+
invalidated = bool(payload.get("independence", {}).get("invalidated"))
|
|
702
736
|
if json_output:
|
|
703
737
|
emit(payload, as_json=True)
|
|
738
|
+
if strict and invalidated:
|
|
739
|
+
raise typer.Exit(4) # const-sync: ok -- exit code, not a version.
|
|
704
740
|
return
|
|
705
741
|
if payload["state"] == "none":
|
|
706
742
|
typer.echo(
|
|
@@ -708,12 +744,22 @@ def status(
|
|
|
708
744
|
"`adopt handover start --receiving-owner <group>` opens one."
|
|
709
745
|
)
|
|
710
746
|
return
|
|
747
|
+
independence = payload["independence"]
|
|
711
748
|
lines = [
|
|
712
749
|
f"Handover {payload['handover_id']} [{payload['state']}] {payload['scope']}",
|
|
713
|
-
f"
|
|
714
|
-
f" owner now: {payload['current_owner'] or 'nobody'}",
|
|
715
|
-
"",
|
|
750
|
+
f" independence: {independence['summary']}",
|
|
716
751
|
]
|
|
752
|
+
for task in independence["tasks"]:
|
|
753
|
+
if task["status"] != "valid":
|
|
754
|
+
since = f" since {task['since']}" if task["since"] else ""
|
|
755
|
+
lines.append(f" {task['status']}: {task['task_id']} ({task['cause']}){since}")
|
|
756
|
+
lines.extend(
|
|
757
|
+
[
|
|
758
|
+
f" receiving owner: {payload['receiving_owner']}",
|
|
759
|
+
f" owner now: {payload['current_owner'] or 'nobody'}",
|
|
760
|
+
"",
|
|
761
|
+
]
|
|
762
|
+
)
|
|
717
763
|
for step in payload["steps"]:
|
|
718
764
|
mark = "x" if step["done"] else " "
|
|
719
765
|
when = f" {step['at']}" if step["at"] else ""
|
|
@@ -735,6 +781,38 @@ def status(
|
|
|
735
781
|
"unreadable detail; the trail was edited outside this product."
|
|
736
782
|
)
|
|
737
783
|
typer.echo("\n".join(lines))
|
|
784
|
+
if strict and invalidated:
|
|
785
|
+
raise typer.Exit(4) # const-sync: ok -- exit code, not a version.
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
def _independence_payload(independence: Any) -> dict[str, Any]:
|
|
789
|
+
"""The `independence` block of `adopt handover status --json`."""
|
|
790
|
+
from adopt_handover.validity import DEGRADED, INVALIDATED, UNANCHORED, VALID
|
|
791
|
+
|
|
792
|
+
from adopt_obs import format_timestamp
|
|
793
|
+
|
|
794
|
+
def stamp(value: Any) -> str | None:
|
|
795
|
+
return None if value is None else format_timestamp(value)
|
|
796
|
+
|
|
797
|
+
return {
|
|
798
|
+
"summary": independence.summary,
|
|
799
|
+
"passed": independence.passed,
|
|
800
|
+
"valid": independence.count(VALID),
|
|
801
|
+
"invalidated": independence.count(INVALIDATED),
|
|
802
|
+
"degraded": independence.count(DEGRADED),
|
|
803
|
+
"unanchored": independence.count(UNANCHORED),
|
|
804
|
+
"tasks": [
|
|
805
|
+
{
|
|
806
|
+
"task_id": task.task_id,
|
|
807
|
+
"uri": task.uri,
|
|
808
|
+
"status": task.status,
|
|
809
|
+
"cause": task.cause,
|
|
810
|
+
"verified_at": stamp(task.verified_at),
|
|
811
|
+
"since": stamp(task.since),
|
|
812
|
+
}
|
|
813
|
+
for task in independence.tasks
|
|
814
|
+
],
|
|
815
|
+
}
|
|
738
816
|
|
|
739
817
|
|
|
740
818
|
def _write_acceptance(
|
|
@@ -87,6 +87,11 @@ def import_(
|
|
|
87
87
|
json_output: JsonOption = False,
|
|
88
88
|
) -> None:
|
|
89
89
|
"""Verify a bundle whole, then restore it into an empty store."""
|
|
90
|
+
# `--into` is "created empty if absent", and that has to include its
|
|
91
|
+
# directory: the restore is often a beneficiary's first command on their own
|
|
92
|
+
# machine, and SQLite refuses a path whose parent does not exist with a raw
|
|
93
|
+
# `OperationalError` rather than anything an operator can act on.
|
|
94
|
+
into.parent.mkdir(parents=True, exist_ok=True)
|
|
90
95
|
with open_named_store(into, migrate=True) as handle:
|
|
91
96
|
manifest = apply_bundle(handle.import_records(), directory)
|
|
92
97
|
emit(_payload(directory, manifest), as_json=json_output, title="adopt import")
|
|
@@ -60,6 +60,7 @@ def ingest(
|
|
|
60
60
|
"""Turn documents into knowledge, bound to the identities they refer to."""
|
|
61
61
|
from adopt_knowledge import discover, run_ingest
|
|
62
62
|
|
|
63
|
+
from adopt_cli.commands._coverage_support import coverage_cache_kept_current
|
|
63
64
|
from adopt_cli.commands._knowledge_support import (
|
|
64
65
|
StoreUnitOfWork,
|
|
65
66
|
bound_pairs,
|
|
@@ -73,19 +74,21 @@ def ingest(
|
|
|
73
74
|
try:
|
|
74
75
|
resolved = resolve_scope(handle, scope)
|
|
75
76
|
documents = discover(paths, root=Path.cwd(), audience=audience)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
77
|
+
system_id = resolved.system.id if resolved.system is not None else None
|
|
78
|
+
with coverage_cache_kept_current(handle, system_id):
|
|
79
|
+
report = run_ingest(
|
|
80
|
+
documents,
|
|
81
|
+
scope=resolved,
|
|
82
|
+
identities=identity_views(handle, resolved),
|
|
83
|
+
stored=stored_documents(handle, resolved),
|
|
84
|
+
knowledge=handle.items(),
|
|
85
|
+
bindings=handle.bindings(),
|
|
86
|
+
reviews=handle.governance(),
|
|
87
|
+
unit=StoreUnitOfWork(handle),
|
|
88
|
+
bound_pairs=bound_pairs(handle),
|
|
89
|
+
presented_revisions=presented_revisions(handle),
|
|
90
|
+
actor_id=actor,
|
|
91
|
+
)
|
|
89
92
|
payload = _ingest_payload(report)
|
|
90
93
|
finally:
|
|
91
94
|
handle.close()
|
|
@@ -307,14 +310,17 @@ def bind(
|
|
|
307
310
|
hint="Run `adopt map` first. A moved identity's old URI still resolves, so "
|
|
308
311
|
"this is genuine absence rather than a stale address.",
|
|
309
312
|
)
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
313
|
+
from adopt_cli.commands._coverage_support import coverage_cache_kept_current
|
|
314
|
+
|
|
315
|
+
with coverage_cache_kept_current(handle, identity.system_id):
|
|
316
|
+
binding_id, revision_id = handle.bindings().bind(
|
|
317
|
+
item_id=knowledge_id,
|
|
318
|
+
identity_id=identity.id,
|
|
319
|
+
is_load_bearing=not not_load_bearing,
|
|
320
|
+
extractor=EXTRACTOR_MANUAL,
|
|
321
|
+
extractor_version=INGEST_EXTRACTOR_VERSION,
|
|
322
|
+
actor_id=actor,
|
|
323
|
+
)
|
|
318
324
|
payload = {
|
|
319
325
|
"binding": binding_id,
|
|
320
326
|
"revision": revision_id,
|
|
@@ -499,12 +505,13 @@ def _conflicts_payload(handle: Any, result: Any, ranker: Any) -> list[dict[str,
|
|
|
499
505
|
coverage just evaluated, so a conflict recorded against another system cannot
|
|
500
506
|
appear in this scope's queue.
|
|
501
507
|
"""
|
|
502
|
-
from
|
|
508
|
+
from adopt_cli.commands._conflict_support import conflict_rows, superseded_revisions
|
|
503
509
|
|
|
504
510
|
if result is None:
|
|
505
511
|
return []
|
|
506
512
|
uris = {row.identity_id: row.uri for row in result.identities}
|
|
507
|
-
rows = handle
|
|
513
|
+
rows = conflict_rows(handle)
|
|
514
|
+
superseded = superseded_revisions(handle)
|
|
508
515
|
return [
|
|
509
516
|
{
|
|
510
517
|
"uri": conflict.uri,
|
|
@@ -516,7 +523,7 @@ def _conflicts_payload(handle: Any, result: Any, ranker: Any) -> list[dict[str,
|
|
|
516
523
|
# exists, and so a reader of the JSON never has to assume.
|
|
517
524
|
"disposition": "open",
|
|
518
525
|
}
|
|
519
|
-
for conflict in ranker(rows, uris)
|
|
526
|
+
for conflict in ranker(rows, uris, superseded=superseded)
|
|
520
527
|
]
|
|
521
528
|
|
|
522
529
|
|
|
@@ -684,68 +691,71 @@ def review(
|
|
|
684
691
|
title="adopt review",
|
|
685
692
|
)
|
|
686
693
|
return
|
|
694
|
+
from adopt_cli.commands._coverage_support import coverage_cache_kept_current
|
|
695
|
+
|
|
687
696
|
handle = open_configured_store(store, read_only=not writing, verb="review")
|
|
688
697
|
try:
|
|
689
698
|
resolved = resolve_scope(handle, scope)
|
|
690
699
|
identities = identity_views(handle, resolved)
|
|
691
700
|
pending = pending_items(handle, resolved, identities)
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
resolutions: list[dict[str, Any]] = []
|
|
716
|
-
for item, action in targets:
|
|
717
|
-
if action == REJECT:
|
|
718
|
-
outcome = reject_pending(item, reviews=handle.governance())
|
|
719
|
-
elif action == CORRECT:
|
|
720
|
-
outcome = edit_pending(
|
|
721
|
-
item,
|
|
722
|
-
reviews=handle.governance(),
|
|
723
|
-
knowledge=handle.items(),
|
|
724
|
-
unit=StoreUnitOfWork(handle),
|
|
725
|
-
body_md=body_md,
|
|
726
|
-
source_ref=str(file),
|
|
727
|
-
actor_id=actor,
|
|
728
|
-
)
|
|
729
|
-
else:
|
|
730
|
-
outcome = confirm_pending(
|
|
731
|
-
item,
|
|
732
|
-
reviews=handle.governance(),
|
|
733
|
-
bindings=handle.bindings(),
|
|
734
|
-
unit=StoreUnitOfWork(handle),
|
|
735
|
-
knowledge=handle.items(),
|
|
736
|
-
bound_pairs=bound_pairs(handle),
|
|
737
|
-
actor_id=actor,
|
|
738
|
-
)
|
|
739
|
-
resolutions.append(
|
|
740
|
-
{
|
|
741
|
-
"review_item": item.review_item_id,
|
|
742
|
-
"action": outcome.resolution,
|
|
743
|
-
"source": item.source,
|
|
744
|
-
"bindings": len(outcome.bindings),
|
|
745
|
-
"revision": outcome.revision_id,
|
|
746
|
-
}
|
|
701
|
+
system_id = resolved.system.id if writing and resolved.system is not None else None
|
|
702
|
+
with coverage_cache_kept_current(handle, system_id):
|
|
703
|
+
if resolve_item:
|
|
704
|
+
payload = _resolve_change(
|
|
705
|
+
handle,
|
|
706
|
+
pending,
|
|
707
|
+
known_review_items(handle),
|
|
708
|
+
review_item_id=resolve_item,
|
|
709
|
+
action=action or "",
|
|
710
|
+
to_uri=to_uri,
|
|
711
|
+
actor=actor,
|
|
712
|
+
)
|
|
713
|
+
elif not writing:
|
|
714
|
+
payload = _queue_payload(pending)
|
|
715
|
+
payload.update(_refresh_payload(refresh_population(handle, resolved, pending)))
|
|
716
|
+
else:
|
|
717
|
+
targets = _targets(
|
|
718
|
+
pending,
|
|
719
|
+
known_review_items(handle),
|
|
720
|
+
confirm_item,
|
|
721
|
+
reject_item,
|
|
722
|
+
edit_item,
|
|
723
|
+
confirm_batch,
|
|
747
724
|
)
|
|
748
|
-
|
|
725
|
+
resolutions: list[dict[str, Any]] = []
|
|
726
|
+
for item, action in targets:
|
|
727
|
+
if action == REJECT:
|
|
728
|
+
outcome = reject_pending(item, reviews=handle.governance())
|
|
729
|
+
elif action == CORRECT:
|
|
730
|
+
outcome = edit_pending(
|
|
731
|
+
item,
|
|
732
|
+
reviews=handle.governance(),
|
|
733
|
+
knowledge=handle.items(),
|
|
734
|
+
unit=StoreUnitOfWork(handle),
|
|
735
|
+
body_md=body_md,
|
|
736
|
+
source_ref=str(file),
|
|
737
|
+
actor_id=actor,
|
|
738
|
+
)
|
|
739
|
+
else:
|
|
740
|
+
outcome = confirm_pending(
|
|
741
|
+
item,
|
|
742
|
+
reviews=handle.governance(),
|
|
743
|
+
bindings=handle.bindings(),
|
|
744
|
+
unit=StoreUnitOfWork(handle),
|
|
745
|
+
knowledge=handle.items(),
|
|
746
|
+
bound_pairs=bound_pairs(handle),
|
|
747
|
+
actor_id=actor,
|
|
748
|
+
)
|
|
749
|
+
resolutions.append(
|
|
750
|
+
{
|
|
751
|
+
"review_item": item.review_item_id,
|
|
752
|
+
"action": outcome.resolution,
|
|
753
|
+
"source": item.source,
|
|
754
|
+
"bindings": len(outcome.bindings),
|
|
755
|
+
"revision": outcome.revision_id,
|
|
756
|
+
}
|
|
757
|
+
)
|
|
758
|
+
payload = {"resolved": len(resolutions), "resolutions": resolutions}
|
|
749
759
|
finally:
|
|
750
760
|
handle.close()
|
|
751
761
|
|
|
@@ -39,8 +39,8 @@ ScopeOption = Annotated[
|
|
|
39
39
|
str | None,
|
|
40
40
|
typer.Option(
|
|
41
41
|
"--scope",
|
|
42
|
-
help="firm/engagement/system
|
|
43
|
-
"declared for that scope. Omitted, the strictest default boundary is used.",
|
|
42
|
+
help="firm/engagement/system, optionally with /environment, to validate against the "
|
|
43
|
+
"boundary declared for that scope. Omitted, the strictest default boundary is used.",
|
|
44
44
|
),
|
|
45
45
|
]
|
|
46
46
|
StoreOption = Annotated[Path | None, typer.Option("--store", help="Store path override.")]
|
|
@@ -152,14 +152,19 @@ def refresh(
|
|
|
152
152
|
allow_network=allow_network,
|
|
153
153
|
)
|
|
154
154
|
)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
155
|
+
from adopt_cli.commands._coverage_support import coverage_cache_kept_current
|
|
156
|
+
|
|
157
|
+
with coverage_cache_kept_current(
|
|
158
|
+
handle, resolved.system.id if resolved.system is not None else None
|
|
159
|
+
):
|
|
160
|
+
outcome = record_refresh(
|
|
161
|
+
handle,
|
|
162
|
+
scope=resolved,
|
|
163
|
+
diff=diff,
|
|
164
|
+
batch_key=batch_key,
|
|
165
|
+
probes=probes,
|
|
166
|
+
actor_id=actor,
|
|
167
|
+
)
|
|
163
168
|
outcome.files_hashed = files_hashed
|
|
164
169
|
# After the canonical write commits, never before: a snapshot saved
|
|
165
170
|
# first would let a crash between the two leave the store claiming this
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
"""Generated by scripts/embed_build_info.py; do not edit in a release build."""
|
|
2
|
-
|
|
3
|
-
from typing import Final
|
|
4
|
-
|
|
5
|
-
SBOM_SHA256: Final[str | None] = '86636f02cad8578ffdc6c4b39fe311c0a8191cafd5fcb323ddb732447dc6b0c8'
|
|
6
|
-
BUILD_ID: Final[str | None] = 'github:onboardux/onboard-core:34144382673:1:a1b32208f0a296bc708e283d860023ce7fdf4609'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|