fix(ingestion): keep the file-name saver anchor out of the ladder (#390)

The #390 ruling queues a file that only its saver's file name resolves for
Needs review, never linking it. inspection_resolution gains name_anchor;
the ladder passes False, so such a file falls to the legacy floor in the
scan (and to Needs review) and is never stored by the monitor, while the
recorded saver still decides. The inspector keeps the name anchor.
This commit is contained in:
Vito Sansevero
2026-10-28 06:29:23 -07:00
parent 4a90a5dd83
commit 4b4fa7f8ff
6 changed files with 71 additions and 57 deletions
+3 -1
View File
@@ -5050,7 +5050,9 @@ workflows without a PromptManager node. It does no file or database I/O.
pair, A1111, sampler-less). Only a graph it leaves ambiguous or unresolved
reaches the inspector's resolve (`inspection_resolution` in
`utils/prompt_inspector.py`, the inspection profile, then the recorded saver
anchor of #386), and only that positive's `storage_text` is stored (never
anchor of #386; never the file-name saver anchor: a file only its saver's
file name resolves is queued for Needs review, never linked, the #390 ruling,
`name_anchor=False`), and only that positive's `storage_text` is stored (never
prepend or append text, never `recorded_texts`). Then the #170 browser parser
(`graph_prompt_text`), the legacy floor, then the recorded run's stored
prompt. History is asked once per file and only at that step; the scan's
+26 -3
View File
@@ -180,9 +180,32 @@ class TestUndecidedGraph(unittest.TestCase):
decision = _decide(
_graph(graph), lookup=_Lookup("43"), file_name="s45_branch_2_00001_.png"
)
# The file name alone names branch 2; history's branch 4 is not used.
self.assertEqual(decision.source, "inspector")
self.assertEqual(decision.text, shapes.BRANCH_TEXTS[1])
# The name contradicts history's branch 4: no recorded anchor.
self.assertEqual(decision.source, "legacy-parser")
def test_step_8_1_the_file_name_anchor_alone_never_decides(self):
"""#390 ruling: a file only its saver's file name resolves is queued
for Needs review by the scan, never linked; the monitor leaves it."""
name = "s45_branch_2_00001_.png"
scan = _decide(_graph(shapes.branch_bench()), lookup=_Lookup(), file_name=name)
self.assertEqual((scan.kind, scan.source), ("positive", "legacy-parser"))
self.assertEqual(scan.review, "ambiguous")
monitor = _decide(
_graph(shapes.branch_bench()),
policy=Policy.MONITOR,
lookup=_Lookup(),
file_name=name,
)
self.assertEqual(monitor.kind, "never")
recorded = _decide(
_graph(shapes.branch_bench()), lookup=_Lookup("43"), file_name=name
)
# The recorded saver still decides: history says branch 4, but the
# file name names branch 2, which contradicts it.
self.assertNotEqual(recorded.source, "recorded-run")
recorded = _decide(_graph(shapes.branch_bench()), lookup=_Lookup("43"))
self.assertEqual(recorded.source, "recorded-run")
self.assertEqual(recorded.text, shapes.BRANCH_TEXTS[3])
def test_step_8_2_the_monitor_waits_for_history_inside_the_output_folder(self):
decision = _decide(_graph(shapes.shared_prefix()), policy=Policy.MONITOR)
+24
View File
@@ -434,6 +434,30 @@ class TestInspectionResolution(unittest.TestCase):
self.assertFalse(result.malformed)
self.assertEqual(result.resolution["state"], "absent")
def test_without_the_name_anchor_a_file_name_names_no_branch(self):
from utils.prompt_inspector import inspection_resolution
graph = {"prompt": json.dumps(shapes.branch_bench())}
name = "s45_branch_2_00001_.png"
named = inspection_resolution(graph, name, None)
self.assertEqual(_positive(named.resolution), shapes.BRANCH_TEXTS[1])
blind = inspection_resolution(graph, name, None, name_anchor=False)
self.assertEqual(blind.resolution["state"], "ambiguous")
# The recorded saver still decides, and the name still contradicts it.
recorded = inspection_resolution(graph, name, _saver(2), name_anchor=False)
self.assertTrue(recorded.by_record)
shared = {"prompt": json.dumps(shapes.shared_prefix())}
bench = shapes.branch_bench()
for index in (1, 3, 4):
bench[_saver(index)]["inputs"]["filename_prefix"] = "bench/shared"
contradicted = inspection_resolution(
{"prompt": json.dumps(bench)}, name, _saver(4), name_anchor=False
)
self.assertFalse(contradicted.by_record)
self.assertTrue(
inspection_resolution(shared, name, _saver(4), name_anchor=False).by_record
)
if __name__ == "__main__":
unittest.main()
+3 -50
View File
@@ -10,13 +10,6 @@ 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.
#247 (D-Q1) lets the scan's ladder store what the inspector's resolve names,
the saver anchor included, so the scan now links such a file instead of
queuing it (``TestScanLinksTheFileItsSaverNames``). The queue and apply
classes below pin the ladder to the file name ignored (``_name_blind``), the
rule items in existing libraries were queued under, so their Needs review
behavior stays covered.
"""
import json
@@ -39,7 +32,7 @@ except ImportError: # discovered with tests/ as the top-level directory
qwen_edit_graph,
)
from utils import file_prompt, review_capture
from utils import review_capture
def branch_graph(*prompts, prefixes=None):
@@ -89,23 +82,6 @@ def _without_name(real):
return unnamed
class _NameBlind:
"""Run the scan's ladder with the file name ignored, as before #247."""
def setUp(self):
super().setUp()
real = file_prompt.inspection_resolution
def name_blind(metadata, file_name, recorded_saver_id, **options):
return real(metadata, None, recorded_saver_id, **options)
patcher = patch.object(
file_prompt, "inspection_resolution", side_effect=name_blind
)
patcher.start()
self.addCleanup(patcher.stop)
def _only_candidate(test, item):
"""The positive text of an anchored item's one candidate."""
evidence = json.loads(item["evidence"])
@@ -115,7 +91,7 @@ def _only_candidate(test, item):
return candidate["positive"]["text"]
class TestInspectionItemsAnchor(_NameBlind, InspectionTestCase):
class TestInspectionItemsAnchor(InspectionTestCase):
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))
@@ -223,7 +199,7 @@ class TestInspectionItemsAnchor(_NameBlind, InspectionTestCase):
self.assertIsNone(self._linked_text(path))
class TestDefaultItemsAnchor(_NameBlind, InspectionTestCase):
class TestDefaultItemsAnchor(InspectionTestCase):
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
@@ -346,29 +322,6 @@ class TestDefaultItemsAnchor(_NameBlind, InspectionTestCase):
self.assertEqual(self._linked_text(path), "cat")
class TestScanLinksTheFileItsSaverNames(InspectionTestCase):
"""#247: the saver anchor names one prompt, so the scan links it."""
async def test_an_inspection_item_file_is_linked_not_queued(self):
self._roots()
path = self._write("branch_1_00001_.png", graph=branch_graph(*PROMPTS))
await self._scan()
self.assertEqual(self._items(), [])
self.assertEqual(self._linked_text(path), "add a red hat")
async def test_a_default_item_file_is_linked_not_queued(self):
self._roots()
path = self.output_dir / "side_0_00001_.png"
write_png(path, prompt=saved_unlinkable_graph("cat", "dog"))
await self._scan()
self.assertEqual(self._items(), [])
self.assertEqual(self._linked_text(path), "cat")
class TestSaverAnchored(InspectionTestCase):
def test_only_the_anchor_counts(self):
anchored = {"prompt": saved_unlinkable_graph("cat", "dog")}
+5 -2
View File
@@ -9,7 +9,8 @@ stores the ``Decision``.
The default resolver profile decides first, as ingestion always has. Only
for a file it leaves ambiguous or unresolved does the ladder ask the
inspector's resolve (``inspection_resolution``: the inspection profile, then
the recorded saver anchor), then the #170 browser parser (the legacy floor).
the recorded saver anchor; never the file-name saver anchor, whose files go
to Needs review by the #390 ruling), then the #170 browser parser (the legacy floor).
The policies differ in one place: for a file the inspector calls ambiguous,
the scan keeps the legacy floor and the monitor stores nothing. Junk text
(``junk_text``) is never stored, whichever step found it.
@@ -153,7 +154,9 @@ def _decide(found: _File) -> Decision:
def _undecided_graph(found: _File) -> Decision:
recorded = found.lookup() if found.lookup is not None else None
saver = recorded.saver_id if recorded is not None else None
inspected = inspection_resolution(found.metadata, found.file_name, saver)
inspected = inspection_resolution(
found.metadata, found.file_name, saver, name_anchor=False
)
text = _inspected_text(inspected.resolution)
if text is not None:
source = "recorded-run" if inspected.by_record else "inspector"
+10 -1
View File
@@ -616,6 +616,7 @@ def inspection_resolution(
recorded_saver_id: str | None,
*,
profile: ResolverProfile = INSPECTION_PROFILE,
name_anchor: bool = True,
) -> InspectionResult:
"""Resolve raw file metadata as the inspector does.
@@ -624,6 +625,12 @@ def inspection_resolution(
Resolves with ``profile``; when that leaves the prompt open and
``recorded_saver_id`` is given, resolves again anchored on that saver
and keeps the anchored result only when it names the positive.
``name_anchor=False`` (ingestion, #247) resolves the unanchored pass
without ``file_name``, so the saver its file name names anchors nothing:
such a file goes to Needs review, never linked (#390 ruling). The
recorded-saver pass still gets ``file_name``, only to refuse a saver the
name contradicts.
"""
workflow = metadata.get("workflow")
canonical = {
@@ -645,7 +652,9 @@ def inspection_resolution(
if workflow is not None and "prompt" in canonical:
# Only to recognize overwritten PromptManager text (R1); never a role.
canonical["workflow"] = workflow
resolved = resolve_prompt_pair(canonical, profile=profile, file_name=file_name)
resolved = resolve_prompt_pair(
canonical, profile=profile, file_name=file_name if name_anchor else None
)
if resolved["state"] not in _UNDECIDED or recorded_saver_id is None:
return InspectionResult(resolved)
anchored = resolve_prompt_pair(