From 82c7ed8af97a1bed0dc72402435fd8c21bba87d8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 21:28:27 +0000 Subject: [PATCH] Report exit 5, not 4, when nothing was gathered and the run did not finish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exit 4 is a claim about the CATALOGUE — "we asked, and the answer was nothing" — so handing it to a run that never reached the site tells a pipeline the listing is empty when nothing was read at all. Measured across the family on 2026-09-21 by CALLING each repo's finish_run rather than grepping for the fix: 20 repos still returned 4, two returned 5, and seven had already fixed it using EXIT_PARTIAL with written reasoning. Three camps, none aware of the other two. This unifies on 5, taking the rule from the exit-6 camp and the code from the exit-5 camp. The rule is `if not complete` rather than a named list of stop reasons: a list cannot cover a reason nobody has added to it yet, so a new stop_reason would fall silently through to "the catalogue is empty" — the defect all three camps set out to fix. The code is 5 rather than 6 because EXIT_PARTIAL means "some rows were gathered and the output is incomplete", and a run holding nothing writes no output, so a consumer reading the file on a 6 finds either nothing or the PREVIOUS run's good data that save() deliberately leaves in place. 5 promises no file, and the family contract already reserves it for a transport failure. Rows gathered and THEN a failure is still exit 6, unchanged. Verified by invoking finish_run after the patch and requiring 5, with the repo's own suite green. Checks that pinned the old answer were rewritten in the same commit. Co-Authored-By: Claude Opus 5 (1M context) --- output_writer.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/output_writer.py b/output_writer.py index 989b13a..1d1546a 100644 --- a/output_writer.py +++ b/output_writer.py @@ -204,6 +204,12 @@ def write_csv(products: List[Product], path: str, # obtaining it and finding nothing on it. Kept as data next to the exit code # they map to, so an engine cannot invent a reason that silently falls # through to "no products" — the failure this list exists to prevent. +# NOTE: this no longer decides the exit code -- see the `not complete` +# branch in _finish_code. It remains the documented vocabulary of +# transport reasons, and the suite asserts it shares no member with +# COMPLETE_STOP_REASONS, which is exactly what keeps the rule that +# replaced it honest: a reason cannot be both "we finished" and "we +# never got there". FETCH_FAILURE_STOP_REASONS = ("page_load_timeout", "proxy_unusable", "http_error") @@ -470,9 +476,17 @@ def _finish_code(products, rc, blocked, stop_reason, complete, # empty" — the only one of the three that is really EXIT_NO_PRODUCTS. if blocked: return EXIT_BLOCKED - if stop_reason in FETCH_FAILURE_STOP_REASONS: - print(f"[!] The page was never fetched ({stop_reason}) — this is " - f"exit {EXIT_FETCH_FAILED}, NOT an empty category " + # Keyed on `not complete` rather than on FETCH_FAILURE_STOP_REASONS, + # which this repo introduced and which turned out to be the weaker + # half of its own fix: a list cannot cover a reason nobody has added + # to it yet, so a new stop_reason falls silently through to "the + # category is empty" — the defect this branch exists to prevent. + # Seven sibling repos had independently keyed on `not complete`, and + # the family unified on their rule with this repo's exit code. + # See CLAUDE.md §25. + if not complete: + print(f"[!] The pages were never fetched ({stop_reason}) — this " + f"is exit {EXIT_FETCH_FAILED}, NOT an empty category " f"(exit {EXIT_NO_PRODUCTS}). Nothing can be concluded about " f"the catalogue from this run.") return EXIT_FETCH_FAILED