fix(review): queue an anchored file with its saver's pair as the choice (#390)
Maintainer ruling: a file never falls through both the link and the queue. When only the saver anchor resolves a file the default profile and the parser floor leave unlinked, the scan queues it with the anchor's pair as its one candidate (evidence anchor: saver), never linking it. Files already linked or resolved by the default path are unchanged. Apply accepts the anchored pair when the bytes now resolve to it, and an anchored item is a choice the untraced settle keeps.
This commit is contained in:
@@ -235,7 +235,9 @@ def evidence_offers_choice(evidence: Any) -> bool:
|
||||
candidates = evidence.get("candidates")
|
||||
if not isinstance(candidates, list):
|
||||
return False
|
||||
return sum(1 for c in candidates if choosable(c)) >= MIN_CHOOSABLE
|
||||
# The saver anchor's one pair (#390) is a suggestion to confirm.
|
||||
needed = 1 if evidence.get("anchor") == "saver" else MIN_CHOOSABLE
|
||||
return sum(1 for c in candidates if choosable(c)) >= needed
|
||||
|
||||
|
||||
def evidence_digest(text: str) -> str:
|
||||
|
||||
+9
-7
@@ -1594,8 +1594,7 @@ class AdminRoutesMixin:
|
||||
whose final outcome is ambiguous and unlinked (the resolver found it
|
||||
ambiguous and the fallback did not link it) is recorded for Needs
|
||||
review (best effort, counted in ``review`` when given); one a saved
|
||||
row already links is reported ``unchanged`` and never queued, and one
|
||||
whose saver anchors its branch (#390) ``unresolved``. An
|
||||
row already links is reported ``unchanged`` and never queued. An
|
||||
ambiguous file with nothing to choose is reported ``unresolved`` and
|
||||
its pending items without a choice leave the queue.
|
||||
"""
|
||||
@@ -1630,12 +1629,15 @@ class AdminRoutesMixin:
|
||||
if ambiguous and outcome == "ambiguous":
|
||||
if self._review_file_linked(media_file, roots):
|
||||
outcome = "unchanged" # already linked: not a review case
|
||||
elif self._saver_anchored_review(
|
||||
media_file, roots, records.get("canonical"), review
|
||||
):
|
||||
outcome = "unresolved" # its saver names its branch (#390)
|
||||
else:
|
||||
self._record_scanned_review(media_file, roots, review)
|
||||
# The records name the item versions that count; the
|
||||
# saver anchor (#390) is checked on the captured bytes.
|
||||
self._record_scanned_review(
|
||||
media_file,
|
||||
roots,
|
||||
review,
|
||||
scanned=(records.get("canonical"), None),
|
||||
)
|
||||
elif untraced and outcome == "unresolved":
|
||||
# Untraced by the default profile: the inspection profile may
|
||||
# still offer a choice (queued for review, never linked).
|
||||
|
||||
@@ -730,6 +730,9 @@ def _candidate_still_offered(resolution: dict, choice: str) -> bool:
|
||||
that rewords the top-level reason or reorders candidates leaves a
|
||||
pending item applicable.
|
||||
"""
|
||||
if isinstance(resolution, dict) and resolution.get("state") == "resolved":
|
||||
# The bytes now name one pair (the saver anchor, #390): only it.
|
||||
resolution = {"state": "ambiguous", "candidates": [resolution]}
|
||||
if not isinstance(resolution, dict) or resolution.get("state") != "ambiguous":
|
||||
return False
|
||||
evidence, _count, limited = review_evidence(resolution)
|
||||
|
||||
@@ -88,7 +88,8 @@ class ReviewRecordingMixin:
|
||||
def _record_scanned_review(self, media_file, roots, review=None, scanned=None):
|
||||
"""Record one ambiguous, unlinked scanned file (logged and counted).
|
||||
|
||||
``scanned`` is the scan's ``(metadata, inspection_resolution)``.
|
||||
``scanned`` is the scan's ``(metadata, inspection_resolution)``; the
|
||||
resolution is None when the scan did not run the inspection profile.
|
||||
"""
|
||||
try:
|
||||
outcome = self._record_review_sync(
|
||||
@@ -210,74 +211,6 @@ class ReviewRecordingMixin:
|
||||
del memory[next(iter(memory))]
|
||||
return None
|
||||
|
||||
def _saver_anchored_review(self, media_file, roots, canonical, review=None):
|
||||
"""True when the saver that wrote a default-ambiguous file resolves
|
||||
its branch (#390), so the file is not queued (blocking).
|
||||
|
||||
Its current bytes' pending, undecided items then leave the queue
|
||||
(counted in ``review["untraced"]``: nothing to choose). A failure is
|
||||
logged and counted, and the file is queued as before. A file state
|
||||
(size, ``mtime_ns``, item version) found unanchored in this process
|
||||
is not inspected again, and bytes carrying a decision are not
|
||||
inspected at all (a decision is never settled).
|
||||
"""
|
||||
located = self._review_location(media_file, roots)
|
||||
if canonical is None or located is None:
|
||||
return False
|
||||
locator = os.path.normcase(located[3])
|
||||
try:
|
||||
facts = os.lstat(located[3])
|
||||
except OSError:
|
||||
return False # gone: the capture decides as before
|
||||
if self.db.review_decided_at(locator, facts.st_size, facts.st_mtime_ns):
|
||||
return False
|
||||
version = review_capture.inspection_review_version(canonical)
|
||||
if version is None:
|
||||
return False
|
||||
state = _digest(f"anchor:{facts.st_size}:{facts.st_mtime_ns}:{version}")
|
||||
memory = self._unanchored_memory()
|
||||
key = _memory_key(locator)
|
||||
if memory.get(key) == state:
|
||||
return False
|
||||
try:
|
||||
anchored = review_capture.saver_anchored(canonical, media_file.name)
|
||||
except Exception:
|
||||
self.logger.warning(
|
||||
f"{media_file.name} could not be inspected for Needs review",
|
||||
exc_info=True,
|
||||
)
|
||||
if review is not None:
|
||||
review["errors"] += 1
|
||||
return False
|
||||
if not anchored:
|
||||
memory.pop(key, None) # re-inserted last: the newest state
|
||||
memory[key] = state
|
||||
while len(memory) > MAX_UNTRACED_MEMORY:
|
||||
del memory[next(iter(memory))]
|
||||
return False
|
||||
try:
|
||||
# No version is current for these bytes: any pending item goes.
|
||||
removed = self.db.settle_stale_review_items(
|
||||
locator, facts.st_size, facts.st_mtime_ns, ()
|
||||
)
|
||||
except Exception:
|
||||
self.logger.warning(
|
||||
f"Needs review items of {media_file.name} could not be removed",
|
||||
exc_info=True,
|
||||
)
|
||||
if review is not None:
|
||||
review["errors"] += 1
|
||||
return True
|
||||
if review is not None:
|
||||
review["untraced"] += removed
|
||||
return True
|
||||
|
||||
def _unanchored_memory(self):
|
||||
memory = getattr(self, "_unanchored_inspections", None)
|
||||
if memory is None:
|
||||
memory = self._unanchored_inspections = {}
|
||||
return memory
|
||||
|
||||
def _settle_stale_reviews(self, media_file, roots, canonical, review=None):
|
||||
"""Remove the pending, undecided items of an untraced file's current
|
||||
bytes recorded under another version (best effort, counted in
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
The workspace inspector (since #383) passes the file's name to the resolver,
|
||||
which anchors the file on the one saver whose ``filename_prefix`` names it
|
||||
(``_GraphResolver.saver_stages``). The scan and Needs review use the same
|
||||
rule: a file whose branch that anchor resolves is never queued, and its
|
||||
pending, undecided items leave the queue on the next scan. No match, several
|
||||
matches or an unfinished walk anchor nothing, so such a file stays queued as
|
||||
before. Apply re-resolves an inspection item with the file's name, so a
|
||||
choice the anchor now contradicts is refused as stale evidence.
|
||||
rule. Maintainer ruling: a file never falls through both the link and the
|
||||
queue, so when only that anchor resolves a file the default profile and the
|
||||
parser floor do not link, it is queued with the anchor's pair as its one
|
||||
choice (never linked automatically). No match, several matches or an
|
||||
unfinished walk anchor nothing, so such a file is queued as before. Apply
|
||||
re-resolves with the file's name, so a choice the anchor contradicts is
|
||||
refused as stale evidence.
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -17,7 +19,6 @@ try:
|
||||
from tests.test_review import unlinkable_graph, write_png
|
||||
from tests.test_review_inspection import (
|
||||
PROMPTS,
|
||||
_counted,
|
||||
InspectionTestCase,
|
||||
qwen_edit_graph,
|
||||
)
|
||||
@@ -26,7 +27,6 @@ except ImportError: # discovered with tests/ as the top-level directory
|
||||
from test_review_inspection import (
|
||||
PROMPTS,
|
||||
InspectionTestCase,
|
||||
_counted,
|
||||
qwen_edit_graph,
|
||||
)
|
||||
|
||||
@@ -80,20 +80,47 @@ def _without_name(real):
|
||||
return unnamed
|
||||
|
||||
|
||||
def _only_candidate(test, item):
|
||||
"""The positive text of an anchored item's one candidate."""
|
||||
evidence = json.loads(item["evidence"])
|
||||
test.assertEqual(evidence.get("anchor"), "saver")
|
||||
test.assertEqual(evidence.get("profile"), "inspection")
|
||||
(candidate,) = evidence["candidates"]
|
||||
return candidate["positive"]["text"]
|
||||
|
||||
|
||||
class TestInspectionItemsAnchor(InspectionTestCase):
|
||||
async def test_file_its_saver_names_is_not_queued(self):
|
||||
async def test_file_its_saver_names_is_queued_with_the_anchor_choice(self):
|
||||
self._roots()
|
||||
path = self._write("branch_1_00001_.png", graph=branch_graph(*PROMPTS))
|
||||
|
||||
job = await self._scan()
|
||||
|
||||
self.assertEqual(self._items(), [])
|
||||
(item,) = self._items()
|
||||
self.assertEqual(_only_candidate(self, item), "add a red hat")
|
||||
self.assertEqual(item["candidate_count"], 1)
|
||||
self.assertEqual(
|
||||
item["resolver_version"],
|
||||
review_capture.inspection_review_version(
|
||||
{"prompt": branch_graph(*PROMPTS)}
|
||||
),
|
||||
)
|
||||
(row,) = job["outcomes"]["review"]
|
||||
self.assertEqual(row["path"], "branch_1_00001_.png")
|
||||
self.assertNotIn("more than one prompt could match", row["detail"])
|
||||
# Never queued is not linked: ingestion stays on the default profile.
|
||||
# Queued, never linked: ingestion stays on the default profile.
|
||||
self.assertIsNone(self._linked_text(path))
|
||||
|
||||
async def test_anchored_item_stays_on_rescan(self):
|
||||
self._roots()
|
||||
self._write("branch_1_00001_.png", graph=branch_graph(*PROMPTS))
|
||||
await self._scan()
|
||||
(item,) = self._items()
|
||||
|
||||
job = await self._scan("all")
|
||||
|
||||
self.assertEqual([i["id"] for i in self._items()], [item["id"]])
|
||||
self.assertEqual(job["scope"]["review_untraced"], 0)
|
||||
|
||||
async def test_file_no_saver_names_stays_queued(self):
|
||||
self._roots()
|
||||
self._write("elsewhere_00001_.png", graph=branch_graph(*PROMPTS))
|
||||
@@ -102,6 +129,7 @@ class TestInspectionItemsAnchor(InspectionTestCase):
|
||||
|
||||
(item,) = self._items()
|
||||
self.assertEqual(item["candidate_count"], len(PROMPTS))
|
||||
self.assertNotIn("anchor", json.loads(item["evidence"]))
|
||||
|
||||
async def test_file_several_savers_name_stays_queued(self):
|
||||
self._roots()
|
||||
@@ -113,7 +141,7 @@ class TestInspectionItemsAnchor(InspectionTestCase):
|
||||
(item,) = self._items()
|
||||
self.assertEqual(item["candidate_count"], len(PROMPTS))
|
||||
|
||||
async def test_pending_item_of_an_anchored_file_leaves_on_rescan(self):
|
||||
async def test_old_rule_item_is_replaced_by_the_anchor_choice(self):
|
||||
self._roots()
|
||||
self._write("branch_1_00001_.png", graph=branch_graph(*PROMPTS))
|
||||
real = review_capture.resolve_inspection
|
||||
@@ -124,14 +152,27 @@ class TestInspectionItemsAnchor(InspectionTestCase):
|
||||
),
|
||||
):
|
||||
await self._scan()
|
||||
self.assertEqual(len(self._items()), 1) # queued under the old rule
|
||||
(old,) = self._items() # queued under the old rule with every branch
|
||||
self.assertEqual(old["candidate_count"], len(PROMPTS))
|
||||
|
||||
job = await self._scan("all")
|
||||
await self._scan("all")
|
||||
|
||||
self.assertEqual(self._items(), [])
|
||||
self.assertEqual(job["scope"]["review_untraced"], 1)
|
||||
(item,) = self._items()
|
||||
self.assertNotEqual(item["id"], old["id"])
|
||||
self.assertEqual(_only_candidate(self, item), "add a red hat")
|
||||
|
||||
async def test_apply_refuses_a_choice_the_anchor_contradicts(self):
|
||||
async def test_apply_links_the_anchor_choice(self):
|
||||
self._require_routes()
|
||||
self._roots()
|
||||
path = self._write("branch_1_00001_.png", graph=branch_graph(*PROMPTS))
|
||||
await self._scan()
|
||||
|
||||
_item, (status, data) = await self._apply("add a red hat")
|
||||
|
||||
self.assertEqual(status, 200, data)
|
||||
self.assertEqual(self._linked_text(path), "add a red hat")
|
||||
|
||||
async def test_apply_refuses_an_old_choice_the_anchor_contradicts(self):
|
||||
self._require_routes()
|
||||
self._roots()
|
||||
path = self._write("branch_1_00001_.png", graph=branch_graph(*PROMPTS))
|
||||
@@ -157,16 +198,24 @@ class TestInspectionItemsAnchor(InspectionTestCase):
|
||||
|
||||
|
||||
class TestDefaultItemsAnchor(InspectionTestCase):
|
||||
async def test_default_ambiguous_file_its_saver_names_is_not_queued(self):
|
||||
async def test_anchor_resolves_and_nothing_links_so_it_is_queued(self):
|
||||
"""Maintainer ruling: never unlinked and unqueued. The anchor resolves
|
||||
the file, the default profile and the parser floor link nothing, so
|
||||
it is queued with the anchor's pair as its choice."""
|
||||
self._roots()
|
||||
path = self.output_dir / "side_0_00001_.png"
|
||||
write_png(path, prompt=saved_unlinkable_graph("cat", "dog"))
|
||||
graph = saved_unlinkable_graph("cat", "dog")
|
||||
write_png(path, prompt=graph)
|
||||
self.assertEqual(
|
||||
review_capture.resolve_prompt_pair({"prompt": graph})["state"], "ambiguous"
|
||||
)
|
||||
|
||||
job = await self._scan()
|
||||
|
||||
self.assertEqual(self._items(), [])
|
||||
(item,) = self._items()
|
||||
self.assertEqual(_only_candidate(self, item), "cat")
|
||||
(row,) = job["outcomes"]["review"]
|
||||
self.assertNotIn("more than one prompt could match", row["detail"])
|
||||
self.assertIn("more than one prompt could match", row["detail"])
|
||||
self.assertIsNone(self._linked_text(path))
|
||||
|
||||
async def test_default_ambiguous_file_no_saver_names_stays_queued(self):
|
||||
@@ -181,31 +230,29 @@ class TestDefaultItemsAnchor(InspectionTestCase):
|
||||
(item,) = self._items()
|
||||
evidence = json.loads(item["evidence"])
|
||||
self.assertNotIn("profile", evidence)
|
||||
self.assertNotIn("anchor", evidence)
|
||||
self.assertEqual(item["resolver_version"], review_capture.RESOLVER_VERSION)
|
||||
|
||||
async def test_pending_default_item_of_an_anchored_file_leaves_on_rescan(self):
|
||||
async def test_file_the_parser_floor_links_is_not_queued(self):
|
||||
"""Already linked by the default path: unchanged by the anchor."""
|
||||
self._roots()
|
||||
write_png(
|
||||
self.output_dir / "side_0_00001_.png",
|
||||
prompt=saved_unlinkable_graph("cat", "dog"),
|
||||
)
|
||||
with patch.object(review_capture, "saver_anchored", return_value=False):
|
||||
await self._scan()
|
||||
self.assertEqual(len(self._items()), 1) # queued under the old rule
|
||||
self.api._unanchored_inspections = {} # a restart onto the new rule
|
||||
graph = saved_unlinkable_graph("cat", "dog")
|
||||
del graph["3"] # the parser floor now reads a real sampler
|
||||
path = self.output_dir / "side_0_00001_.png"
|
||||
write_png(path, prompt=graph)
|
||||
|
||||
job = await self._scan("all")
|
||||
await self._scan()
|
||||
|
||||
self.assertEqual(self._items(), [])
|
||||
self.assertEqual(job["scope"]["review_untraced"], 1)
|
||||
self.assertIsNotNone(self._linked_text(path))
|
||||
|
||||
async def test_apply_refuses_a_default_choice_the_anchor_contradicts(self):
|
||||
async def test_apply_refuses_an_old_default_choice_the_anchor_contradicts(self):
|
||||
self._require_routes()
|
||||
self._roots()
|
||||
path = self.output_dir / "side_0_00001_.png"
|
||||
write_png(path, prompt=saved_unlinkable_graph("cat", "dog"))
|
||||
with patch.object(review_capture, "saver_anchored", return_value=False):
|
||||
await self._scan()
|
||||
with patch.object(review_capture, "_captured_inspection", return_value=None):
|
||||
await self._scan() # queued from the default profile, as before #390
|
||||
item, detail, observed = await self._open()
|
||||
# side_0 is the saver of "cat"; choose the other branch.
|
||||
wrong = self._candidate(detail, "dog")
|
||||
@@ -218,42 +265,18 @@ class TestDefaultItemsAnchor(InspectionTestCase):
|
||||
self.assertEqual(data["code"], "review-conflict", data)
|
||||
self.assertIsNone(self._linked_text(path))
|
||||
|
||||
async def test_unanchored_file_is_not_inspected_again_until_it_changes(self):
|
||||
self._roots()
|
||||
path = self.output_dir / "unsaved_00001_.png"
|
||||
write_png(path, prompt=saved_unlinkable_graph("cat", "dog"))
|
||||
with _counted(review_capture, "saver_anchored") as first:
|
||||
await self._scan()
|
||||
self.assertEqual(len(first), 1)
|
||||
|
||||
with _counted(review_capture, "saver_anchored") as again:
|
||||
await self._scan("all")
|
||||
self.assertEqual(again, [])
|
||||
|
||||
write_png(path, prompt=saved_unlinkable_graph("cat", "dog"), color=(0, 9, 0))
|
||||
with _counted(review_capture, "saver_anchored") as changed:
|
||||
await self._scan("all")
|
||||
self.assertEqual(len(changed), 1)
|
||||
|
||||
async def test_decided_default_item_of_an_anchored_file_is_kept(self):
|
||||
async def test_apply_accepts_an_old_default_choice_the_anchor_names(self):
|
||||
self._require_routes()
|
||||
self._roots()
|
||||
write_png(
|
||||
self.output_dir / "side_0_00001_.png",
|
||||
prompt=saved_unlinkable_graph("cat", "dog"),
|
||||
)
|
||||
with patch.object(review_capture, "saver_anchored", return_value=False):
|
||||
path = self.output_dir / "side_0_00001_.png"
|
||||
write_png(path, prompt=saved_unlinkable_graph("cat", "dog"))
|
||||
with patch.object(review_capture, "_captured_inspection", return_value=None):
|
||||
await self._scan()
|
||||
item, detail, observed = await self._open()
|
||||
status, data = await self._decide(
|
||||
item["id"], self._body(detail, observed, "unresolved")
|
||||
)
|
||||
|
||||
_item, (status, data) = await self._apply("cat")
|
||||
|
||||
self.assertEqual(status, 200, data)
|
||||
before = (self._items(), self._decisions())
|
||||
|
||||
await self._scan("all")
|
||||
|
||||
self.assertEqual((self._items(), self._decisions()), before)
|
||||
self.assertEqual(self._linked_text(path), "cat")
|
||||
|
||||
|
||||
class TestSaverAnchored(InspectionTestCase):
|
||||
|
||||
+96
-45
@@ -43,13 +43,16 @@ links stay on the default profile, which never loads the table (D1).
|
||||
|
||||
The saver anchor (#390): the inspection profile resolves a file with its
|
||||
name, as the workspace inspector does since #383, so the one saver whose
|
||||
``filename_prefix`` names the file anchors its branch. A file whose branch
|
||||
that anchor resolves is never queued, from either profile
|
||||
(``saver_anchored`` for a default item); no match, several matches or an
|
||||
unfinished walk anchor nothing and the file stays queued as before. Apply
|
||||
re-resolves an inspection item with the file's name too.
|
||||
``REVIEW_RULE_VERSION`` is folded into inspection item versions, so items
|
||||
recorded before the anchor are re-evaluated by the next scan.
|
||||
``filename_prefix`` names the file anchors its branch. When that anchor alone
|
||||
resolves a file the default profile left ambiguous and nothing links
|
||||
(``saver_anchored``), the scan queues it with the anchor's pair as its one
|
||||
candidate (evidence ``anchor: "saver"``), never linking it: a file never
|
||||
falls through both the link and the queue (maintainer ruling). No match,
|
||||
several matches or an unfinished walk anchor nothing and the file is queued
|
||||
as before. Apply re-resolves with the file's name, so only the anchored
|
||||
branch can be applied. ``REVIEW_RULE_VERSION`` is folded into inspection
|
||||
item versions, so inspection items recorded before the anchor are recorded
|
||||
again by the next scan.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -114,6 +117,9 @@ MAX_CANDIDATE_DEPTH = 8
|
||||
# The resolver profile an item's evidence was built with (``profile``).
|
||||
DEFAULT = "default"
|
||||
INSPECTION = "inspection"
|
||||
# The ``anchor`` an item recorded from the saver anchor names (#390).
|
||||
SAVER_ANCHOR = "saver"
|
||||
ANCHOR_REASON = "The saver that wrote this file names its branch"
|
||||
# Inspection item versions: above every plausible ``RESOLVER_VERSION`` and,
|
||||
# with 40 bits of the folded digest, below 2**53 (exact as a JSON number).
|
||||
INSPECTION_VERSION_BASE = 1 << 40
|
||||
@@ -195,38 +201,54 @@ def resolve_inspection(metadata: dict, file_name: str | None = None) -> dict:
|
||||
|
||||
|
||||
def inspection_choice(metadata: dict, file_name: str | None = None) -> dict | None:
|
||||
"""The inspection profile's ambiguous result when it offers a choice.
|
||||
"""The inspection profile's result when the file is a review case.
|
||||
|
||||
The scan asks only for a file the default profile found ambiguous
|
||||
without a choice that nothing links. ``file_name`` anchors as in
|
||||
``resolve_inspection``: a file whose branch its saver resolves offers
|
||||
no choice.
|
||||
without a choice that nothing links. The result is ambiguous with a
|
||||
choice, or ``resolved`` when ``file_name`` anchors the file on its saver
|
||||
and only that anchor resolves it (``saver_anchored``): the item then
|
||||
offers the anchor's pair. None otherwise.
|
||||
"""
|
||||
if inspection_review_version(metadata) is None:
|
||||
return None
|
||||
resolution = resolve_inspection(metadata, file_name)
|
||||
if resolution.get("state") == "resolved":
|
||||
return resolution if _anchor_alone(metadata, file_name) else None
|
||||
if resolution.get("state") != "ambiguous" or not reviewable(resolution):
|
||||
return None
|
||||
return resolution
|
||||
|
||||
|
||||
def _anchor_alone(metadata: Any, file_name: str | None) -> bool:
|
||||
"""True when ``metadata`` does not resolve without ``file_name``."""
|
||||
if not file_name:
|
||||
return False
|
||||
return resolve_inspection(metadata).get("state") != "resolved"
|
||||
|
||||
|
||||
def saver_anchored(metadata: Any, file_name: str | None) -> bool:
|
||||
"""True when the saver that wrote ``file_name`` resolves its branch (#390).
|
||||
|
||||
The inspection profile resolves ``metadata`` with the file's name, and
|
||||
without it the file does not resolve: the anchor alone decides it. A file
|
||||
the default profile found ambiguous with a choice is then not queued. No
|
||||
name, an unusable table, no matching saver, several, or an unfinished
|
||||
walk anchor nothing (False). The profile without the name runs only when
|
||||
the named run resolved.
|
||||
the default profile left ambiguous is then queued with the anchor's pair
|
||||
as its one candidate. No name, an unusable table, no matching saver,
|
||||
several, or an unfinished walk anchor nothing (False). The profile
|
||||
without the name runs only when the named run resolved.
|
||||
"""
|
||||
return _anchored(metadata, file_name) is not None
|
||||
|
||||
|
||||
def _anchored(metadata: Any, file_name: str | None) -> dict | None:
|
||||
"""The anchored result ``saver_anchored`` describes, else None."""
|
||||
if not file_name or not isinstance(metadata, dict):
|
||||
return False
|
||||
return None
|
||||
if inspection_review_version(metadata) is None:
|
||||
return False
|
||||
if resolve_inspection(metadata, file_name).get("state") != "resolved":
|
||||
return False
|
||||
return resolve_inspection(metadata).get("state") != "resolved"
|
||||
return None
|
||||
resolution = resolve_inspection(metadata, file_name)
|
||||
if resolution.get("state") != "resolved":
|
||||
return None
|
||||
return resolution if _anchor_alone(metadata, file_name) else None
|
||||
|
||||
|
||||
def item_profile(evidence: Any) -> str:
|
||||
@@ -315,24 +337,24 @@ def record_ambiguous_file(
|
||||
if resolution is None or resolution.get("state") != "ambiguous":
|
||||
return "skipped"
|
||||
profile, version = DEFAULT, RESOLVER_VERSION
|
||||
if not reviewable(resolution):
|
||||
version = inspection_review_version(captured[0]) if inspection else None
|
||||
if version is None:
|
||||
named = _captured_inspection(captured[0], relative, scanned) if inspection else None
|
||||
anchored = named is not None and named[0].get("state") == "resolved"
|
||||
if anchored:
|
||||
# Only the saver anchor resolves it (#390): offer that one pair.
|
||||
profile, version = INSPECTION, named[1]
|
||||
resolution = {
|
||||
"state": "ambiguous",
|
||||
"reason": ANCHOR_REASON,
|
||||
"candidates": [named[0]],
|
||||
}
|
||||
elif not reviewable(resolution):
|
||||
if named is None:
|
||||
return "untraced"
|
||||
profile = INSPECTION
|
||||
if (
|
||||
scanned is not None
|
||||
and scanned[1] is not None
|
||||
and _same_records(captured[0], scanned[0])
|
||||
):
|
||||
resolution = scanned[1] # the same records: no second run
|
||||
else:
|
||||
# Heavier, so only for a file ambiguous with nothing to choose.
|
||||
resolution = resolve_inspection(captured[0], _file_name(relative))
|
||||
profile, (resolution, version) = INSPECTION, named
|
||||
if resolution.get("state") != "ambiguous" or not reviewable(resolution):
|
||||
return "untraced"
|
||||
evidence, count, limited = review_evidence(
|
||||
resolution, profile, version, captured[0]
|
||||
resolution, profile, version, captured[0], anchored=anchored
|
||||
)
|
||||
try:
|
||||
inserted = db.record_review_item(
|
||||
@@ -356,6 +378,33 @@ def record_ambiguous_file(
|
||||
return "limited" if limited else "recorded"
|
||||
|
||||
|
||||
def _captured_inspection(metadata: dict, relative: str, scanned) -> tuple | None:
|
||||
"""``(resolution, item_version)`` of the captured records under the
|
||||
inspection profile, anchored on the file's name; None when the table is
|
||||
unusable.
|
||||
|
||||
A ``resolved`` result means the saver anchor alone resolves the file
|
||||
(#390); a result that also resolves without the name is returned as
|
||||
``unresolved`` (no review case, as before). The scan's result is reused
|
||||
when the capture holds the same records (``inspection_choice`` applies
|
||||
the same rule).
|
||||
"""
|
||||
version = inspection_review_version(metadata)
|
||||
if version is None:
|
||||
return None
|
||||
if (
|
||||
scanned is not None
|
||||
and scanned[1] is not None
|
||||
and _same_records(metadata, scanned[0])
|
||||
):
|
||||
return scanned[1], version # the same records: no second run
|
||||
name = _file_name(relative)
|
||||
resolution = resolve_inspection(metadata, name)
|
||||
if resolution.get("state") == "resolved" and not _anchor_alone(metadata, name):
|
||||
resolution = {**resolution, "state": "unresolved"}
|
||||
return resolution, version
|
||||
|
||||
|
||||
def _file_name(relative: str) -> str:
|
||||
"""The last part of a ``/``-separated relative path (the saved name)."""
|
||||
return relative.rsplit("/", 1)[-1]
|
||||
@@ -412,9 +461,9 @@ def captured_review(
|
||||
capture. ``resolution`` comes from ``profile`` (``DEFAULT`` or
|
||||
``INSPECTION``; the latter anchored on ``file_name``, the file's own
|
||||
name, as the item was recorded). With ``file_name`` (Apply), a default
|
||||
result its saver anchor resolves (``saver_anchored``) is that anchored
|
||||
result, so no candidate is offered any more. None when the capture is
|
||||
gone or carries no prompt metadata.
|
||||
result its saver anchor alone resolves (``saver_anchored``) is that
|
||||
anchored result, so only the anchored branch is still offered. None
|
||||
when the capture is gone or carries no prompt metadata.
|
||||
"""
|
||||
if store.acquire(capture.token) is None:
|
||||
return None
|
||||
@@ -444,13 +493,11 @@ def captured_review(
|
||||
if profile == INSPECTION:
|
||||
return canonical, resolve_inspection(canonical, file_name)
|
||||
resolution = resolve_prompt_pair(canonical)
|
||||
if (
|
||||
file_name is not None
|
||||
and resolution.get("state") == "ambiguous"
|
||||
and saver_anchored(canonical, file_name)
|
||||
):
|
||||
# Its saver names its branch (#390): no choice is left to apply.
|
||||
return canonical, resolve_inspection(canonical, file_name)
|
||||
if file_name is not None and resolution.get("state") == "ambiguous":
|
||||
anchored = _anchored(canonical, file_name)
|
||||
if anchored is not None:
|
||||
# Its saver names its branch (#390): only that one is offered.
|
||||
return canonical, anchored
|
||||
return canonical, resolution
|
||||
|
||||
|
||||
@@ -477,12 +524,14 @@ def review_evidence(
|
||||
profile: str = DEFAULT,
|
||||
version: int | None = None,
|
||||
metadata: Any = None,
|
||||
anchored: bool = False,
|
||||
) -> tuple[dict, int, bool]:
|
||||
"""``(evidence, candidate_count, limited)`` for one ambiguous result.
|
||||
|
||||
Inspection evidence also names its ``profile`` and ``semantics``, the
|
||||
table fingerprint of ``metadata``'s graph it was resolved with;
|
||||
``version`` is the item's version.
|
||||
``version`` is the item's version. ``anchored`` evidence (the saver
|
||||
anchor's one pair, #390) also names ``anchor: "saver"``.
|
||||
"""
|
||||
base = {
|
||||
"resolver_version": RESOLVER_VERSION if version is None else version,
|
||||
@@ -491,6 +540,8 @@ def review_evidence(
|
||||
if profile == INSPECTION:
|
||||
base["profile"] = INSPECTION
|
||||
base["semantics"] = semantics_version(metadata)
|
||||
if anchored:
|
||||
base["anchor"] = SAVER_ANCHOR
|
||||
candidates: list[dict] = []
|
||||
seen: set[str] = set()
|
||||
total = 0
|
||||
|
||||
Reference in New Issue
Block a user