fix: count the POSTS a working name spans, not the files (4392)
CI and images / lint (push) Failing after 3s
CI and images / extension-version (push) Successful in 3s
CI and images / frontend-build (push) Successful in 23s
CI and images / backend-lint-and-test (push) Successful in 32s
CI and images / integration (push) Successful in 2m19s
CI and images / sign-extension (push) Skipped
CI and images / build-web (push) Skipped
CI and images / smoke-web (push) Skipped
CI and images / promote (push) Skipped
CI and images / build-agent (push) Skipped

Counting files punished a piece for the one thing a working name is
guaranteed to do — have several exports. Measured across the operator's four
dual-platform artists: knuxy carries `p217` on four files spanning exactly two
posts, the Patreon post and the Discord drop, and ~200 comic-page tokens have
that shape. Every one scored half strength. Counting posts scores them 1.00
and still catches the habits, which span many posts rather than many files:
tamadaheijun's `comic2` spans 8, conto's `seth2` 5, `maid` 4. Knuxy's
ambiguous band drops from 268 tokens to 55.

Three false positives found the same way — by running the module over the real
library rather than reading it:

  * The screenshot guard matches from the start of the stem, so it never fired
    on the legacy `<post id>_media_<media id>_` era. `Screenshot 2025-07-27
    182450ab` sailed through and contributed `2025-07-27` — the same-day date
    collision this module's docstring exists to refuse.
  * tamadaheijun names screenshots in Japanese. A guard that knows only the
    English word is a guard for one artist.
  * `timeline 3-0002` spans three unrelated posts. Keeping hyphens inside
    tokens for `0-k`'s sake let the number survive whole, so an identity token
    now has to contain a letter — which is the property behind the old
    bare-year and all-digits rules anyway, and leaves `0-k`, `680lc` and `p59`
    untouched.

Also stopwords for `the`, `gif`, `main`, `patreon`, `capture`, `timeline`,
each measured carrying a false match inside the admitted band.

IDENTITY_FLOOR names what a shared name must reach to propose a link with no
corroboration. At 0.75, 13 of artist 8's 15 name-sharing pairs clear it,
including the operator's own example. The two that do not are real pairs this
signal will not carry alone — the stated cost of refusing the four-post band,
where conto's `illustration9` and `maid` also sit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
This commit is contained in:
2026-09-24 08:00:36 -04:00
co-authored by Claude Opus 5
parent ecbfd83029
commit c67a8c313e
2 changed files with 170 additions and 37 deletions
+85 -11
View File
@@ -17,8 +17,9 @@ from collections import Counter
import pytest
from backend.app.services.post_naming import (
MAX_MARKER_FREQUENCY,
MAX_TOKEN_FREQUENCY,
IDENTITY_FLOOR,
MAX_MARKER_POSTS,
MAX_TOKEN_POSTS,
marker_frequencies,
marker_overlap,
shared_identity,
@@ -74,6 +75,42 @@ def test_a_work_in_progress_matches_the_piece_it_became():
)
@pytest.mark.parametrize(
"path, why",
[
(
"109078417_media_334848471_Screenshot 2025-07-27 182450ab.png",
"the guard matches from the START of the stem, so while the legacy "
"prefix was left on, a legacy screenshot never looked like one — "
"and contributed `2025-07-27`, the exact same-day date collision "
"this module refuses",
),
(
"136070668_media_513155924_\u30b9\u30af\u30ea\u30fc\u30f3\u30b7\u30e7\u30c3\u30c8 2025-07-27 9.31.png",
"tamadaheijun's screenshots are named in Japanese; a guard that "
"only knows the English word is a guard for one artist",
),
(
"129421439_media_469882823_timeline 3-0002.jpg",
"keeping hyphens inside tokens for `0-k`'s sake let `3-0002` "
"survive whole, and it was MEASURED spanning three unrelated posts",
),
],
)
def test_measured_false_positives_contribute_nothing(path, why):
"""Each of these was found by running the module against the operator's
real library, not by reading it — which is the only way any of them would
have been found."""
assert working_name_tokens(path) == set(), why
def test_an_identity_token_must_contain_a_letter():
"""The property behind refusing bare years, bare numbers and date
fragments, stated once. `0-k`, `680lc` and `p59` all keep a letter."""
assert working_name_tokens("01_2025-07-27.jpg") == set()
assert working_name_tokens("01_0-k.jpg") == {"0-k"}
def test_the_literal_string_none_is_not_a_name():
"""#3999 rendered `{user[name]}` as "None" for ~1,600 files, which made it
the single most common "name" in the library and an identity for nothing."""
@@ -132,7 +169,7 @@ def test_a_token_at_the_cap_names_nothing():
"""It decays to zero, and reporting it anyway would hand the review queue a
reason with no weight behind it — "matched on loislanetb2", with nothing
there. A token is named only while it is doing work."""
freqs = Counter({"tok": MAX_TOKEN_FREQUENCY})
freqs = Counter({"tok": MAX_TOKEN_POSTS})
assert shared_identity({"tok"}, {"tok"}, freqs) == (0.0, None)
@@ -152,9 +189,10 @@ def test_the_rarest_shared_token_decides_not_the_count_of_them():
def test_frequencies_are_counted_per_artist_not_per_library():
"""A working name belongs to the person who chose it; the same string is
one creator's piece and another's boilerplate."""
counts = token_frequencies(
["01_ConnFront.jpg", "20230222_1078078245695664148_01_ConnFront.jpg"]
)
counts = token_frequencies([
["01_ConnFront.jpg"],
["20230222_1078078245695664148_01_ConnFront.jpg"],
])
assert counts["connfront"] == 2
@@ -219,11 +257,47 @@ def test_marker_frequencies_count_posts_not_occurrences():
def test_the_marker_gate_is_tighter_than_the_filename_gate():
"""Stated as a property because the two caps count different things and the
difference is deliberate: the filename cap counts a working name across a
piece's EXPORTS, the marker cap counts a public decoration across POSTS. A
marker tying an announcement to its drop lands on two posts — the two."""
assert MAX_MARKER_FREQUENCY < MAX_TOKEN_FREQUENCY
"""Both caps count POSTS, so they are directly comparable and the gap is a
claim: a working name is the creator's private label for one piece and may
honestly recur as they revisit it, while a marker is public decoration and
stops being evidence the moment it is reused."""
assert MAX_MARKER_POSTS < MAX_TOKEN_POSTS
def test_a_name_must_clear_the_floor_to_link_on_its_own():
"""The floor is the whole two-route design in one number: identity may
propose alone, circumstance never may. Pinned against the threshold it
guards so the two cannot drift apart silently."""
assert 0.0 < IDENTITY_FLOOR <= 1.0
assert IDENTITY_FLOOR > 0.60 # the matcher's default threshold
# --- the count is of POSTS, which is what makes the cap mean anything --------
def test_a_piece_with_many_exports_is_not_penalised_for_having_them():
"""Counting FILES punishes a piece for the one thing a working name is
guaranteed to do. Measured: knuxy carries `p217` on four files across
exactly two posts — the Patreon post and the Discord drop — and roughly
two hundred comic-page tokens have that shape. File-counting scored every
one of them at half strength."""
counts = token_frequencies([
["p217.jpg", "p217-clean.jpg"], # the Patreon post
["20240101_123456789_01_p217.jpg", "..._02_p217-clean.jpg"], # the drop
])
assert counts["p217"] == 2
assert shared_identity({"p217"}, {"p217"}, counts) == (1.0, "p217")
def test_a_name_reused_across_many_posts_is_still_caught():
"""The other half of the same property — the cap has to keep working once
the unit changes. Measured habits: tamadaheijun's `comic2` spans 8 posts,
conto's `seth2` 5."""
counts = token_frequencies([["comic2_%02d.jpg" % i] for i in range(8)])
assert counts["comic2"] == 8
assert shared_identity({"comic2"}, {"comic2"}, counts) == (0.0, None)
def test_marker_overlap_cannot_be_called_without_the_frequencies():