From 4b4fa7f8ffa73e33fab49605501676b2a4f3c300 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Wed, 28 Oct 2026 06:29:23 -0700 Subject: [PATCH] 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. --- TESTING.md | 4 ++- tests/test_file_prompt.py | 29 ++++++++++++++-- tests/test_recorded_saver_anchor.py | 24 +++++++++++++ tests/test_review_saver_anchor.py | 53 ++--------------------------- utils/file_prompt.py | 7 ++-- utils/prompt_inspector.py | 11 +++++- 6 files changed, 71 insertions(+), 57 deletions(-) diff --git a/TESTING.md b/TESTING.md index fa9406d5..acdb15e6 100644 --- a/TESTING.md +++ b/TESTING.md @@ -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 diff --git a/tests/test_file_prompt.py b/tests/test_file_prompt.py index 9ae150b1..79c944f0 100644 --- a/tests/test_file_prompt.py +++ b/tests/test_file_prompt.py @@ -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) diff --git a/tests/test_recorded_saver_anchor.py b/tests/test_recorded_saver_anchor.py index 580f2c45..b8df3731 100644 --- a/tests/test_recorded_saver_anchor.py +++ b/tests/test_recorded_saver_anchor.py @@ -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() diff --git a/tests/test_review_saver_anchor.py b/tests/test_review_saver_anchor.py index ee4a45d2..c61d8112 100644 --- a/tests/test_review_saver_anchor.py +++ b/tests/test_review_saver_anchor.py @@ -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")} diff --git a/utils/file_prompt.py b/utils/file_prompt.py index d9fabd67..b0b6b81d 100644 --- a/utils/file_prompt.py +++ b/utils/file_prompt.py @@ -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" diff --git a/utils/prompt_inspector.py b/utils/prompt_inspector.py index 05a0b76b..6211f408 100644 --- a/utils/prompt_inspector.py +++ b/utils/prompt_inspector.py @@ -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(