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(