Declare local_phash's skip reasons at source (authorised protected-core exception) - #574
Declare local_phash's skip reasons at source (authorised protected-core exception)#574WilfordGrimley wants to merge 1 commit into
Conversation
724c513 to
3df1322
Compare
459d18a to
08da4ad
Compare
Rebased onto the rebased #567Rebased from the old #567 head ( Checked that #567's rebase edits and this PR's edits to the same two files still cohere:
Verification
|
`local_phash.find_best_match` returned "no-hashable-candidates" and "no-clear-winner" as bare inline literals. The roster tether added in #567 derives the skip-reason roster from module-level `*_SKIP_REASON = "<literal>"` declarations, and it cannot enumerate literals it cannot see - so a NEW literal added inside `find_best_match` would have reached `CardScanLog.skip_reason` (~2.7M rows, no `choices` list, no FK) with nothing to catch it. #567 could not close that: `local_phash.py` is PROTECTED CORE (docs/upstreaming/license-provenance.md §2). It mirrored the two constants in the consuming module instead and documented the residual gap. The owner granted a narrow exception on 2026-07-29 to close it. - declare PHASH_NO_HASHABLE_CANDIDATES_SKIP_REASON and PHASH_NO_CLEAR_WINNER_SKIP_REASON in `local_phash.py`, export them, and return them by name - delete the mirror in `local_identify_printing_tags.py`, which now imports the one constant it uses; one declaration per value - record the exception, its reasoning and - the part that matters - its LIMITS in license-provenance.md §2.1, a new exception log. It permits declaring skip-reason constants in this one file. It is not a licence to edit protected core; the next such change needs its own ruling. `local_phash.py` stays on the protected list. - two guards in test_skip_reason_roster.py for the regressions the tether is structurally blind to: the mirror coming back, and a bare literal returning at the origin Naming-only. The string VALUES are untouched - and so are the constant NAMES, so the roster's pinning test and the doc's Constant column needed no edit at all. A `CardScanLog` row written after this is byte-identical to one written before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013NhYmT1PxCcyemA16dFDxN
08da4ad to
25a24f5
Compare
…e exception) `local_fallback.run_fallback_for_card` set `FallbackOutcome.skip_reason` from three bare inline literals - "no-evidence", "eliminated", "ambiguous". The roster tether added in #567 derives the skip-reason roster from module-level `*_SKIP_REASON = "<literal>"` declarations, and it cannot enumerate literals it cannot see, so a FOURTH literal added beside them would have joined three invisible siblings on the way to `CardScanLog.skip_reason` (~2.7M rows, no `choices` list, no FK). Latent, not live: this module's own write branch was retired by #560 and its one non-test caller (`local_residual_classify.recover_frame_mismatch_printing_via_fallback_refetch`) reads `outcome.printing_pk` and discards `skip_reason`. Nothing persists these today. That is an argument for closing the hole now, while the change is provably inert, not for leaving it: the invisibility is a property of the literals, and it goes live the moment anything persists the outcome, with no lint failure to mark the moment. `local_fallback.py` is PROTECTED CORE (docs/upstreaming/license-provenance.md section 2). The owner granted a SECOND narrow exception on 2026-07-29 - separate from #574's, which names this file explicitly as one it does NOT cover. - declare LOCAL_FALLBACK_NO_EVIDENCE_SKIP_REASON / LOCAL_FALLBACK_ELIMINATED_SKIP_REASON / LOCAL_FALLBACK_AMBIGUOUS_SKIP_REASON in `local_fallback.py`, export them, and pass them by name. The `LOCAL_FALLBACK_` prefix (not `FALLBACK_`) keeps them distinct from the SEPARATE `stage-d-fallback-v1` calculator's `FALLBACK_*` family in `local_calculate_verdicts.py` - no mirror was deleted, because none existed: unlike the phash pair these values were never re-declared for this engine anywhere, since its one caller never reads them. The Stage D family was checked and left alone - a parallel calculator's own vocabulary, not a mirror of these - completeness verified by AST scan of the whole file (every `skip_reason=` kwarg, every `.skip_reason` assignment, every lowercase/hyphenated string constant): three is the complete set, not just the three I was handed - record the exception, its reasoning, its proof of nil effect and - the part that matters - its LIMITS as the second entry in license-provenance.md section 2.1. `local_fallback.py` stays on the protected list in section 2, now annotated - document the three in docs/reference/skip-reasons.md under a new Local-fallback pilot engine section, marked Latent; correct that doc's reference to a function named `compute_fallback_outcome`, which does not exist (it is `run_fallback_for_card`) - three guards in test_skip_reason_roster.py, all mutation-checked. The mirror guard is deliberately narrower than #574's: `no-evidence`, `eliminated` and `ambiguous` are shared vocabulary several calculators legitimately declare under their own prefixes, so a flat value ban would forbid the roster's own design Naming-only. The string VALUES are untouched: the after-source with the constants inlined back to literals parses to an AST identical to the before-source (docstrings normalised - they are the only other difference), and the sequence run_fallback_for_card can produce is ['no-evidence', 'eliminated', 'ambiguous'] before and after. A `CardScanLog` row written after this is byte-identical to one written before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013NhYmT1PxCcyemA16dFDxN
…otected-core exception) (#584) * Declare local_phash's skip reasons at source (protected-core exception) `local_phash.find_best_match` returned "no-hashable-candidates" and "no-clear-winner" as bare inline literals. The roster tether added in #567 derives the skip-reason roster from module-level `*_SKIP_REASON = "<literal>"` declarations, and it cannot enumerate literals it cannot see - so a NEW literal added inside `find_best_match` would have reached `CardScanLog.skip_reason` (~2.7M rows, no `choices` list, no FK) with nothing to catch it. #567 could not close that: `local_phash.py` is PROTECTED CORE (docs/upstreaming/license-provenance.md §2). It mirrored the two constants in the consuming module instead and documented the residual gap. The owner granted a narrow exception on 2026-07-29 to close it. - declare PHASH_NO_HASHABLE_CANDIDATES_SKIP_REASON and PHASH_NO_CLEAR_WINNER_SKIP_REASON in `local_phash.py`, export them, and return them by name - delete the mirror in `local_identify_printing_tags.py`, which now imports the one constant it uses; one declaration per value - record the exception, its reasoning and - the part that matters - its LIMITS in license-provenance.md §2.1, a new exception log. It permits declaring skip-reason constants in this one file. It is not a licence to edit protected core; the next such change needs its own ruling. `local_phash.py` stays on the protected list. - two guards in test_skip_reason_roster.py for the regressions the tether is structurally blind to: the mirror coming back, and a bare literal returning at the origin Naming-only. The string VALUES are untouched - and so are the constant NAMES, so the roster's pinning test and the doc's Constant column needed no edit at all. A `CardScanLog` row written after this is byte-identical to one written before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013NhYmT1PxCcyemA16dFDxN * Declare local_fallback's skip reasons at source (second protected-core exception) `local_fallback.run_fallback_for_card` set `FallbackOutcome.skip_reason` from three bare inline literals - "no-evidence", "eliminated", "ambiguous". The roster tether added in #567 derives the skip-reason roster from module-level `*_SKIP_REASON = "<literal>"` declarations, and it cannot enumerate literals it cannot see, so a FOURTH literal added beside them would have joined three invisible siblings on the way to `CardScanLog.skip_reason` (~2.7M rows, no `choices` list, no FK). Latent, not live: this module's own write branch was retired by #560 and its one non-test caller (`local_residual_classify.recover_frame_mismatch_printing_via_fallback_refetch`) reads `outcome.printing_pk` and discards `skip_reason`. Nothing persists these today. That is an argument for closing the hole now, while the change is provably inert, not for leaving it: the invisibility is a property of the literals, and it goes live the moment anything persists the outcome, with no lint failure to mark the moment. `local_fallback.py` is PROTECTED CORE (docs/upstreaming/license-provenance.md section 2). The owner granted a SECOND narrow exception on 2026-07-29 - separate from #574's, which names this file explicitly as one it does NOT cover. - declare LOCAL_FALLBACK_NO_EVIDENCE_SKIP_REASON / LOCAL_FALLBACK_ELIMINATED_SKIP_REASON / LOCAL_FALLBACK_AMBIGUOUS_SKIP_REASON in `local_fallback.py`, export them, and pass them by name. The `LOCAL_FALLBACK_` prefix (not `FALLBACK_`) keeps them distinct from the SEPARATE `stage-d-fallback-v1` calculator's `FALLBACK_*` family in `local_calculate_verdicts.py` - no mirror was deleted, because none existed: unlike the phash pair these values were never re-declared for this engine anywhere, since its one caller never reads them. The Stage D family was checked and left alone - a parallel calculator's own vocabulary, not a mirror of these - completeness verified by AST scan of the whole file (every `skip_reason=` kwarg, every `.skip_reason` assignment, every lowercase/hyphenated string constant): three is the complete set, not just the three I was handed - record the exception, its reasoning, its proof of nil effect and - the part that matters - its LIMITS as the second entry in license-provenance.md section 2.1. `local_fallback.py` stays on the protected list in section 2, now annotated - document the three in docs/reference/skip-reasons.md under a new Local-fallback pilot engine section, marked Latent; correct that doc's reference to a function named `compute_fallback_outcome`, which does not exist (it is `run_fallback_for_card`) - three guards in test_skip_reason_roster.py, all mutation-checked. The mirror guard is deliberately narrower than #574's: `no-evidence`, `eliminated` and `ambiguous` are shared vocabulary several calculators legitimately declare under their own prefixes, so a flat value ban would forbid the roster's own design Naming-only. The string VALUES are untouched: the after-source with the constants inlined back to literals parses to an AST identical to the before-source (docstrings normalised - they are the only other difference), and the sequence run_fallback_for_card can produce is ['no-evidence', 'eliminated', 'ambiguous'] before and after. A `CardScanLog` row written after this is byte-identical to one written before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013NhYmT1PxCcyemA16dFDxN --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Closing as fully subsumed by #584. #584 carried both commits and merged alone, so this branch's content reached
Merging would at best be a no-op and at worst reintroduce the smaller |
Stacked on #567. Base is
feat/skip-reason-declaration-convention, notmaster— see "Why this is stacked" below. Review/merge after #567.The gap
local_phash.find_best_matchreturned two skip reasons as bare inline string literals:"no-hashable-candidates"and"no-clear-winner". #567's roster tether derives the roster by scanning for module-level*_SKIP_REASON = "<literal>"declarations, and it cannot enumerate literals it cannot see — so a NEW literal added insidefind_best_matchwould reachCardScanLog.skip_reason(~2.7M rows, nochoiceslist, no FK) with no lint failure anywhere.#567 could not close that, because
MPCAutofill/cardpicker/local_phash.pyis PROTECTED CORE (docs/upstreaming/license-provenance.md§2). It mirrored the constants in the consuming module and documented the residual gap instead.Owner ruling 2026-07-29: exception granted, with the added requirement that it "hold up to audit".
What this does
PHASH_NO_HASHABLE_CANDIDATES_SKIP_REASON/PHASH_NO_CLEAR_WINNER_SKIP_REASONare declared inlocal_phash.py, exported in__all__, and returned by name.local_identify_printing_tags.pyare gone; that module imports the one constant it uses. One declaration per value, nothing that can drift. Verified nothing else mirrored them._declared_skip_reasons()globscardpicker/*.py, so it picks the new site up with no linter change. Both values were already documented; the doc entries that named the mirror's location are corrected to the real one.§2.1 Authorised exceptions — the loginlicense-provenance.md, with the date, the change, the reasoning, the authorisation, the proof of zero effect, and an explicit scope statement: it permits declaring skip-reason constants in this one file. It is not a licence to edit protected core, not a licence for the other listed files, and not a standing "lint-driven refactors are exempt" rule. The next change of any shape needs its own ruling and its own entry.local_phash.pystays on the list in §2 and inPROTECTED_CORE_FILES.test_skip_reason_roster.pygains two tests: one fails if any othercardpickermodule re-declares either value (mirror returns), one fails iffind_best_matchreturns a bare literal again. Both mutation-checked.Verification
Byte-identical, proved not asserted. Three independent static proofs:
__all__entries, and inlining every load of those names back to its string literal produces a parse tree identical to the before-source. (Docstrings stripped from both: the docstring text deliberately changed to name the constants; it is prose, not executable data.)find_best_matchcan return, resolved statically through the module's constant table, is unchanged:['no-hashable-candidates', 'no-clear-winner', 'no-clear-winner', '']before and after.local_phashwith the same string.Proof A also holds against
origin/masterdirectly —local_phash.pyis byte-identical on master and on #567's branch, so the pre-change baseline is the same file either way.The constant NAMES were kept identical too, not just the values. That is why
test_skip_reason_roster.py's hand-writtenEXPECTED_SKIP_REASONSpin and the doc's Constant column needed no edit at all — nothing about the roster moved except the declaration site.Roster tether — passes, and proved it can still fail. With the doc entry for each value stripped in turn, the tether errors and names the new site:
Restored → clean. Full
docs_lint.py:docs-lint: clean.check_protected_core_license.py:protected-core-license: clean (9 files checked).Tests:
cardpicker/tests/— 2984 passed, 9 skipped.test_catalog_stats.pyis excluded from that count: 5 of its tests fail withValueError: Missing staticfiles manifest entry for 'cardpicker/favicon.ico', identically on the unmodified baseline (verified by stashing) — a localcollectstaticgap, not this change.Also clean:
ruff,isort,black,mypy(all pre-commit hooks passed on commit).Item 5 — same defect elsewhere in protected core? Reported only, not fixed
Each of these would need its own ruling; that is the point of the policy.
vote_consensus.pyDEDUCTIVE_BACKFILL_ANONYMOUS_ID.printing_consensus.pytag_consensus.pyartist_consensus.pyfederation-hash-tool/hash_my_cards.pylocal_fallback.pyrun_fallback_for_cardreturnsFallbackOutcome(skip_reason=...)with bare literals"no-evidence"(L687),"eliminated"(L690),"ambiguous"(L694).On
local_fallback.py: none of the three reachesCardScanLogtoday. Its only non-test caller islocal_residual_classify.recover_frame_mismatch_printing_via_fallback_refetch, which readsoutcome.printing_pkand discardsskip_reason; the module's own write branch was retired on 2026-07-29 and thelocal-fallback-v1rows in production are historical. So this is a latent gap, not a live one — but it is the same shape as the one this PR closes, and it would become live the moment anything persists that outcome. Recommend a separate ruling rather than folding it in here. (ItsFALLBACK_ANONYMOUS_IDis properly declared; the calculator-identity side of protected core is clean across all six files.)Incidental, out of scope, not protected core:
local_residual_classify.py:135filtersCardScanLog.objects.filter(skip_reason="frame-mismatch")on a bare literal — a read-side coupling to a value declared elsewhere, not a roster-tether gap.Why this is stacked on #567 rather than based on master
The instruction said to base on
masterif #567 had not landed. It has not (open as of writing), and basing onmasterwould have made the work unverifiable and items 2–3 vacuous: the mirror to remove, the tether to satisfy, and the doc entries to correct all originate in #567 and none exist onmaster. Basing there would have produced a PR that declares constants with nothing to check them against. Stacking keeps the deliverable reviewable as one coherent change; the diff against #567 is exactly the five files listed. If #567 is reworked, this rebases onto it.🤖 Generated with Claude Code
https://claude.ai/code/session_013NhYmT1PxCcyemA16dFDxN