Skip to content

Make the non-BISAC nonfiction reset re-runnable (PP-5129) - #3736

Closed
dbernstein wants to merge 4 commits into
bugfix/unrecognized-bisac-codes-vote-nonfictionfrom
bugfix/rerun-non-bisac-nonfiction-reset
Closed

dbernstein wants to merge 4 commits into
bugfix/unrecognized-bisac-codes-vote-nonfictionfrom
bugfix/rerun-non-bisac-nonfiction-reset

Conversation

@dbernstein

@dbernstein dbernstein commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Description

Carries the whole data repair for PP-4849, as a startup task rather than a migration. The migration in #3726 was removed since it would unnecessarily duplicate the work done here in the startup task.

Three layers, mirroring reclassify_null_audience_works, which repairs the sibling audience defect:

  • reset_non_bisac_nonfiction_subjects — a Celery task that re-applies the reset.
  • ResetNonBisacNonfictionSubjectsScript — queues the task.
  • bin/work_reset_non_bisac_nonfiction_subjects — the wrapper an operator runs.

The selection lives on BISACClassifier as contradicts_stored_fiction, with exactly one implementation — the Celery task — that all callers go through.

Note

Stacked on #3726 and based on its branch, since the migration this repairs does not exist on main yet. GitHub will retarget this to main when #3726 merges. Only the single commit here is new.

Motivation and Context

Raised by @tdilauro in review on #3726.

The migration only sets checked=false; the re-scoring happens later, in classify_unchecked_subjects. That leaves a window. If old code reaches those subjects first — a still-running scripts server, or a host that redeploys itself via watchtower or ECS/Fargate auto_restart — it re-scores them under the superseded rules and re-stamps checked=true.

The failure is quiet, which is what makes it worth addressing. The reset is consumed rather than lost, so nothing errors, nothing retries, and no later run revisits those subjects. The repair did nothing, having paid for a full reindex to do it. It will not happen on a normal migrate deployment.

This cannot be prevented from inside the migration, so the aim is to make it recoverable instead: run the reset again.

The task resets only

Re-scoring stays with classify_unchecked_subjects, which picks these subjects up on its next nightly run and can be triggered immediately through bin/work_classify_unchecked_subjects. One task, one job — and no large reindex fired the moment someone runs the repair.

How Has This Been Tested?

New tests:

  • TestBISACClassifier::test_contradicts_stored_fiction — seven parametrized cases in both directions: a vendor code, a shape-valid but non-existent code, and a real FIC* code all read as stale when stored nonfiction; a real nonfiction code and agreeing values read as current; (None, None) is not stale.
  • test_reset_non_bisac_nonfiction_subjects — four subjects establishing the selection boundary. The stale one is reset; a real nonfiction code, a subject already scored fiction, and a tag-typed subject carrying the same identifier are all left checked.
  • test_reset_non_bisac_nonfiction_subjects_is_idempotent — a second run leaves the reset in place and finds nothing to do.
  • TestResetNonBisacNonfictionSubjectsScript::test_run — the script queues the task, mirroring the null-audience script's test.

Full suite locally against Postgres + Valkey: 6,276 passed, 0 failed. 104 errors, all the OpenSearch container not running locally (test_search.py); CI covers those. tests/migration passes with the migration routed through the shared classmethod. mypy clean across 1,166 files; all pre-commit hooks pass.

Checklist

  • I have updated the documentation accordingly.
  • All new and existing tests passed.

🤖 Generated with Claude Code


Update: the migration is gone

An earlier revision of this stack did the reset in a migration (52d1bbdd4671) and kept the Celery task only for re-runs. That meant two implementations of one algorithm — select BISAC subjects stored as nonfiction, filter through contradicts_stored_fiction, reset checked, log the counts — written once in raw SQL and once through the ORM, sharing only the per-row predicate.

The migration has been dropped (see the commit on #3726). The repair now runs from a startup task, which:

  • executes in the same init container moments after migrations, so the reset lands at the same point in the deploy;
  • chains reset_non_bisac_nonfiction_subjects into classify_unchecked_subjects (the second signature immutable, so the chain does not pass a return value into a task that takes none), closing the reset-to-re-score gap to seconds;
  • shares one implementation with the bin/ wrapper and with Re-apply the non-BISAC nonfiction reset a release later (PP-5129) #3737's next-release re-apply.

It also puts the repair where the README says post-deployment work depending on new code belongs, and removes a frozen migration whose selection depended on the live classifier.

The trade, for the record: a migration runs regardless of Celery's health, where a dispatched chain is lost if the broker is down at deploy. #3737 is what makes that acceptable.

@dbernstein dbernstein added the bug Something isn't working label Sep 14, 2026
@dbernstein
dbernstein force-pushed the bugfix/rerun-non-bisac-nonfiction-reset branch from 79b2cb7 to af7c1b7 Compare September 14, 2026 18:23
@claude

claude Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @dbernstein's task in 6m 56s —— View job


Code review

  • Gather context (diff vs bugfix/unrecognized-bisac-codes-vote-nonfiction)
  • Review the Celery task and classifier helper
  • Review the startup task, script, and bin/ wrapper
  • Review test coverage
  • Post review

Summary

The design holds up under checking. contradicts_stored_fiction reproduces the exact path Classifier.classify takes (scrub_identifier_and_name → is_fiction, and BISACClassifier does not override classify), so the rows it selects are precisely the rows assign_to_genre would now write differently. The idempotency claim is real rather than asserted: assign_to_genre assigns self.fiction unconditionally, so a re-scored subject lands on None or True and drops out of the task's fiction == False filter on the next run. chain(...) is a Signature subclass, so run_startup_tasks dispatches it and logs the id, and a broker failure raises inside the transaction, leaving the task unrecorded so it retries on the next boot. The bulk update is safe to do outside the ORM — Subject has no flush listeners — and since Subject.lookup keys on (type, identifier), the candidate set is bounded by distinct BISAC identifiers, so loading it into memory is fine. Greptile's P1 about the missing later re-apply is the stacked draft #3737, per the two-PR workflow in CLAUDE.md. Everything below is documentation/consistency.

Details

Minor: tests/manager/core/classifiers/test_bisac.py:594-595

The docstring still says "Both the migration and the re-run task ask this", but this stack dropped migration 52d1bbdd4671, so the Celery task is the only caller today (#3737 adds the second). Commit 5769c9a cleaned up the same stale reference in tests/manager/celery/tasks/test_work.py and in the script docstring; this one was missed, and it is the one that explains why the helper exists on the classifier rather than in the task.

that reset it need one definition of which rows are affected. Both the
migration and the re-run task ask this.

Minor: src/palace/manager/celery/tasks/work.py:44

The one-time repair task has no removal TODO, so nothing anchors its cleanup. Its sibling reclassify_null_audience_works carries one, and both callers defer to the task rather than marking it: the script says "Remove this script when the reset_non_bisac_nonfiction_subjects Celery task is removed", and the startup task's TODO refers to the startup file. Worth adding to the docstring:

TODO: Remove this task, ResetNonBisacNonfictionSubjectsScript and its bin
wrapper once the reset has run on all deployments (PP-5129).

def reset_non_bisac_nonfiction_subjects(task: Task) -> None:

Nit: src/palace/manager/celery/tasks/work.py:71-77

The literal False restates the Subject.fiction == False filter twenty lines away, so broadening the query later (to include fiction IS NULL, say, for subjects an interrupted re-score left behind) would silently ask the wrong question instead of failing. Selecting the column and passing it keeps the two in step:

session.query(Subject.id, Subject.identifier, Subject.name, Subject.fiction)
...
if BISACClassifier.contradicts_stored_fiction(row.identifier, row.name, row.fiction)

stale_ids = [
row.id
for row in candidates
if BISACClassifier.contradicts_stored_fiction(
row.identifier, row.name, False
)
]

· branch bugfix/rerun-non-bisac-nonfiction-reset

@greptile-apps

greptile-apps Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 4/5

The PR is not yet safe to merge because the previously reported missing later reset remains a blocking correctness issue, and explicit repository requirements remain unsatisfied.

Findings

  1. P1 Later reset is missing ▶
  2. P2 New code in deprecated package ▶
  3. P2 Task identity is discarded ▶
  4. P2 New code in deprecated core ▶
  5. P2 Public function lacks docstring ▶

Summary

This PR adds a repeatable repair workflow for BISAC subjects whose stored nonfiction classification is stale.

  • Adds an idempotent Celery task that resets affected subjects to checked=False.
  • Adds a startup task chaining the reset with reclassification.
  • Adds an operator-facing wrapper for manually dispatching the reset.
  • Centralizes stale-fiction detection in BISACClassifier.
  • Adds classifier, task, idempotency, and script-dispatch tests.
Diagram
sequenceDiagram
    participant Startup as Startup registry
    participant Broker as Celery broker
    participant Reset as Reset task
    participant DB as Database
    participant Classify as Classification task

    Startup->>Broker: Dispatch reset → classify chain
    Broker->>Reset: Run reset
    Reset->>DB: Select checked BISAC nonfiction subjects
    Reset->>DB: "Set stale subjects checked=False"
    Reset-->>Broker: Complete
    Broker->>Classify: Run immutable follow-up
    Classify->>DB: Reclassify unchecked subjects and affected works
Loading

Reviews (6) · Last reviewed commit: "Drop the stale migration reference from ..."

Comment on lines +256 to +267
class ResetNonBisacNonfictionSubjectsScript(Script):
"""Manually dispatch the ``reset_non_bisac_nonfiction_subjects`` Celery task.

The work itself happens in the Celery task; this script just queues it. It
exists so the repair in migration 52d1bbdd4671 can be applied again, in
case its reset was consumed by old code before the new classifier was live.

TODO: Remove this script when the ``reset_non_bisac_nonfiction_subjects``
Celery task is removed.
"""

def do_run(self, *args: Any, **kwargs: Any) -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 New code in deprecated package

This adds ResetNonBisacNonfictionSubjectsScript under src/palace/manager/scripts, but the repository explicitly marks this legacy CLI package as deprecated and prohibits new code there. Move this operational entry point to the supported command framework. This repository requirement must be satisfied before merging.

Context Used: CLAUDE.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +268 to +272
reset_non_bisac_nonfiction_subjects.delay()
self.log.info(
'The "reset_non_bisac_nonfiction_subjects" task has been queued for '
"execution. See the celery logs for details about task execution."
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Task identity is discarded

The command discards the AsyncResult returned by .delay() and logs only the static task name. An operator therefore cannot correlate this repair invocation with worker logs or distinguish it from another run. Retain and log the task ID to make execution of this repair verifiable.

Suggested change
reset_non_bisac_nonfiction_subjects.delay()
self.log.info(
'The "reset_non_bisac_nonfiction_subjects" task has been queued for '
"execution. See the celery logs for details about task execution."
)
result = reset_non_bisac_nonfiction_subjects.delay()
self.log.info(
'The "reset_non_bisac_nonfiction_subjects" task has been queued for '
f"execution with task ID {result.id}. See the celery logs for details "
"about task execution."
)

Knowledge Base Used:

@codecov

codecov Bot commented Sep 14, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.71%. Comparing base (3325a89) to head (5769c9a).

Additional details and impacted files
@@                                 Coverage Diff                                 @@
##           bugfix/unrecognized-bisac-codes-vote-nonfiction    #3736      +/-   ##
===================================================================================
- Coverage                                            93.71%   93.71%   -0.01%     
===================================================================================
  Files                                                  510      510              
  Lines                                                46493    46452      -41     
  Branches                                              6311     6300      -11     
===================================================================================
- Hits                                                 43573    43533      -40     
  Misses                                                1886     1886              
+ Partials                                              1034     1033       -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@dbernstein
dbernstein force-pushed the bugfix/unrecognized-bisac-codes-vote-nonfiction branch from 46d57e9 to ef9e2dc Compare September 14, 2026 18:39
@dbernstein
dbernstein force-pushed the bugfix/rerun-non-bisac-nonfiction-reset branch from af7c1b7 to 153af5c Compare September 14, 2026 18:41
Comment on lines +693 to +722
@classmethod
def contradicts_stored_fiction(
cls,
identifier: str | None,
name: str | None,
stored_fiction: bool | None,
) -> bool:
"""Does this classifier disagree with a subject's stored fiction status?

Subjects are only re-examined when `checked` is false, so a value
scored under superseded rules persists indefinitely. Repairs that
reset `checked` need to identify those rows, and they need to agree
with each other about which rows they are. Expressing the question
here keeps that definition in one place: a subject is stale when the
classifier, run now, does not return what is stored.

:param identifier: The subject's identifier, as stored.
:param name: The subject's name, as stored.
:param stored_fiction: The subject's current `fiction` value.
:return: True when the classifier no longer agrees with `stored_fiction`.
"""
if not identifier and not name:
# Nothing to classify. Subject.lookup will not create such a row,
# but both columns are nullable, so do not assume.
return False
scrubbed_identifier, scrubbed_name = cls.scrub_identifier_and_name(
identifier, name
)
return cls.is_fiction(scrubbed_identifier, scrubbed_name) is not stored_fiction

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 New code in deprecated core

This adds the public BISACClassifier.contradicts_stored_fiction helper under src/palace/manager/core, but the repository explicitly marks core as deprecated and prohibits new code there. Move the shared stale-classification logic to a supported package and have the classifier, migration, and task call it there. This repository requirement must be satisfied before merging.

Context Used: CLAUDE.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@dbernstein
dbernstein force-pushed the bugfix/unrecognized-bisac-codes-vote-nonfiction branch from ef9e2dc to f3535a5 Compare September 14, 2026 20:33


def run(services: Services, session: Session, log: logging.Logger) -> Signature | None:
return classify_unchecked_subjects.s()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Later reset is missing

The startup task queues only classify_unchecked_subjects, even though old web containers can still consume the migration's reset afterward. The docstring says a later startup task will reapply the reset after those containers are recycled, but no such task exists. If old code restores a stale subject to checked=True, classification will no longer select it, leaving the incorrect nonfiction value in place.

Knowledge Base Used:

@dbernstein
dbernstein force-pushed the bugfix/unrecognized-bisac-codes-vote-nonfiction branch from f3535a5 to 5c1cc0b Compare September 15, 2026 00:09
@dbernstein
dbernstein force-pushed the bugfix/rerun-non-bisac-nonfiction-reset branch from f99d214 to 76f13e8 Compare September 15, 2026 00:14
dbernstein added a commit that referenced this pull request Sep 15, 2026
Hold in draft. Merge only after the release containing #3726 and #3736 has
gone out.

The release N repair -- migration 52d1bbdd4671 plus the startup task that
dispatches the re-score -- is exposed for as long as any old code is running.
Anything reaching Subject.assign_to_genre before the re-score lands consumes
the reset, and code on the superseded rules re-stamps checked=True with the
same wrong value.

helpers/migrate.yml stops the scripts container, where every Celery worker
runs, before migrating, so the worker path is safe. The web containers are not:
they are recycled after the migration and can reach assign_to_genre through a
presentation recalculation. Fargate deployments are not governed by that
playbook at all.

Running the reset again a release later closes both, because by then no old
code is live anywhere. The task is idempotent, so if the release N repair took,
this is a no-op.

Re-scoring is left to the nightly classify_unchecked_subjects: with no old code
to lose the reset to, dispatching it here would buy nothing.

Same shape as the null-audience repair, where startup task 2026_06_17 re-ran
what 2026_05_12 had dispatched a release earlier.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment on lines +50 to +51
def run(services: Services, session: Session, log: logging.Logger) -> Signature | None:
return chain(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Public function lacks docstring

The new public run function has no function-level reStructuredText docstring. This violates the repository requirement that all public functions be documented, so the requirement must be satisfied before merging.

Suggested change
def run(services: Services, session: Session, log: logging.Logger) -> Signature | None:
return chain(
def run(services: Services, session: Session, log: logging.Logger) -> Signature | None:
"""Queue the subject reset and reclassification tasks.
:param services: Application services supplied by the startup task runner.
:param session: Database session supplied by the startup task runner.
:param log: Logger supplied by the startup task runner.
:return: A Celery chain that resets and reclassifies affected subjects.
"""
return chain(

Context Used: CLAUDE.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@dbernstein
dbernstein force-pushed the bugfix/unrecognized-bisac-codes-vote-nonfiction branch from 5c1cc0b to a19c2c7 Compare September 15, 2026 14:29
dbernstein added a commit that referenced this pull request Sep 15, 2026
Hold in draft. Merge only after the release containing #3726 and #3736 has
gone out.

The release N repair -- migration 52d1bbdd4671 plus the startup task that
dispatches the re-score -- is exposed for as long as any old code is running.
Anything reaching Subject.assign_to_genre before the re-score lands consumes
the reset, and code on the superseded rules re-stamps checked=True with the
same wrong value.

helpers/migrate.yml stops the scripts container, where every Celery worker
runs, before migrating, so the worker path is safe. The web containers are not:
they are recycled after the migration and can reach assign_to_genre through a
presentation recalculation. Fargate deployments are not governed by that
playbook at all.

Running the reset again a release later closes both, because by then no old
code is live anywhere. The task is idempotent, so if the release N repair took,
this is a no-op.

Re-scoring is left to the nightly classify_unchecked_subjects: with no old code
to lose the reset to, dispatching it here would buy nothing.

Same shape as the null-audience repair, where startup task 2026_06_17 re-ran
what 2026_05_12 had dispatched a release earlier.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dbernstein
dbernstein force-pushed the bugfix/unrecognized-bisac-codes-vote-nonfiction branch from a19c2c7 to 3325a89 Compare September 16, 2026 20:58
dbernstein and others added 4 commits September 16, 2026 13:58
Migration 52d1bbdd4671 repairs subjects that were stored as nonfiction
because an unresolvable BISAC code fell through the ruleset catch-all.
It only sets checked=false; the re-scoring happens later, in
classify_unchecked_subjects.

That leaves a window. If old code reaches those subjects first -- a
still-running scripts server, or a host that redeploys itself via
watchtower or ECS/Fargate auto_restart -- it re-scores them under the
superseded rules and re-stamps checked=true. The reset is consumed
rather than lost, so nothing errors, nothing retries, and no later run
revisits them. The repair quietly did nothing, having paid for a full
reindex. Raised in review on #3726; it is not preventable from inside
the migration, so make it recoverable instead.

Adds reset_non_bisac_nonfiction_subjects, a Celery task that re-applies
the reset, with ResetNonBisacNonfictionSubjectsScript and
bin/work_reset_non_bisac_nonfiction_subjects to queue it -- the same
three layers as reclassify_null_audience_works, which repairs the
sibling audience defect.

The task resets only. Re-scoring stays with classify_unchecked_subjects,
which picks these subjects up on its next nightly run and can be
triggered immediately through bin/work_classify_unchecked_subjects. One
task, one job, and no large reindex fired the moment someone runs the
repair.

The selection lives on BISACClassifier as contradicts_stored_fiction, so
the migration and the task cannot disagree about which rows are
affected. Both already import the classifier, so this adds no coupling
that was not there, and it puts the definition with the code that owns
the answer. The migration is updated to use it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Migration 52d1bbdd4671 only resets checked=False. classify_unchecked_subjects
is what re-scores those subjects and recalculates their works, and left to
itself that does not happen until the nightly run.

The gap between the two is where the repair is exposed: anything reaching
Subject.assign_to_genre in the meantime consumes the reset, and if it is
running the superseded rules it re-stamps checked=True with the same wrong
value. Nothing errors and nothing revisits the subject afterwards.

Adds a startup task that dispatches the re-score immediately after the
migration, shrinking that gap from about a day to seconds. This is the pattern
the null-audience repair already used -- migration d856ff4dbefb makes the data
change, startup task 2026_05_12 dispatches the follow-up.

The timing works out: helpers/migrate.yml stops the scripts container -- which
is where every Celery worker and beat run -- before running the migration, and
starts it again afterwards from the new image. So no worker is alive when this
dispatches, and the one that picks the task up is necessarily new code.

Web containers are the remaining exposure. The deploy recycles them after the
migration, and they can reach assign_to_genre through a presentation
recalculation. A second startup task in the next release re-applies the reset
once no old code is running anywhere.

This moves roughly 4,877 works' worth of recalculation and reindexing from
overnight to deploy time. That is about 2% of the nightly volume behind
PP-4472, so it should be unremarkable, but it is a deliberate choice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follow-on from dropping the migration. The reset and the re-score are now
chained from one place: reset_non_bisac_nonfiction_subjects marks the stale
subjects unchecked, classify_unchecked_subjects re-scores them. The second
signature is immutable so the chain does not pass the first task's return value
into a task that takes no arguments.

There is now one implementation of the selection instead of two. The task is
the only thing that knows how to find these subjects, and all three callers --
this startup task, the next-release re-apply, and the bin wrapper -- go through
it.

Chaining rather than dispatching the reset alone is what keeps the repair from
being exposed. The reset on its own can be consumed by anything reaching
Subject.assign_to_genre first, and code on the superseded rules re-stamps
checked=True with the same wrong value, silently. Running the re-score straight
after closes that to seconds instead of waiting for the nightly.

Docstrings that referred to migration 52d1bbdd4671 now describe the condition
they repair rather than pointing at a file that no longer exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The migration this named was removed when the repair moved to a startup task.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dbernstein
dbernstein force-pushed the bugfix/rerun-non-bisac-nonfiction-reset branch from 1a85d67 to 5769c9a Compare September 16, 2026 20:58
dbernstein added a commit that referenced this pull request Sep 16, 2026
Hold in draft. Merge only after the release containing #3726 and #3736 has
gone out.

The release N repair -- migration 52d1bbdd4671 plus the startup task that
dispatches the re-score -- is exposed for as long as any old code is running.
Anything reaching Subject.assign_to_genre before the re-score lands consumes
the reset, and code on the superseded rules re-stamps checked=True with the
same wrong value.

helpers/migrate.yml stops the scripts container, where every Celery worker
runs, before migrating, so the worker path is safe. The web containers are not:
they are recycled after the migration and can reach assign_to_genre through a
presentation recalculation. Fargate deployments are not governed by that
playbook at all.

Running the reset again a release later closes both, because by then no old
code is live anywhere. The task is idempotent, so if the release N repair took,
this is a no-op.

Re-scoring is left to the nightly classify_unchecked_subjects: with no old code
to lose the reset to, dispatching it here would buy nothing.

Same shape as the null-audience repair, where startup task 2026_06_17 re-ran
what 2026_05_12 had dispatched a release earlier.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dbernstein added a commit that referenced this pull request Sep 17, 2026
Hold in draft. Merge only after the release containing #3726 and #3736 has
gone out.

The release N repair -- migration 52d1bbdd4671 plus the startup task that
dispatches the re-score -- is exposed for as long as any old code is running.
Anything reaching Subject.assign_to_genre before the re-score lands consumes
the reset, and code on the superseded rules re-stamps checked=True with the
same wrong value.

helpers/migrate.yml stops the scripts container, where every Celery worker
runs, before migrating, so the worker path is safe. The web containers are not:
they are recycled after the migration and can reach assign_to_genre through a
presentation recalculation. Fargate deployments are not governed by that
playbook at all.

Running the reset again a release later closes both, because by then no old
code is live anywhere. The task is idempotent, so if the release N repair took,
this is a no-op.

Re-scoring is left to the nightly classify_unchecked_subjects: with no old code
to lose the reset to, dispatching it here would buy nothing.

Same shape as the null-audience repair, where startup task 2026_06_17 re-ran
what 2026_05_12 had dispatched a release earlier.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dbernstein

Copy link
Copy Markdown
Contributor Author

Closing: these commits have been rolled into #3726.

The classifier fix and the data repair really have to ship together — run on its own, the reset would just be re-scored by the old rules and re-stamped checked=true, paying for a reindex that changes nothing. Keeping them in separate PRs made that a review convention rather than a fact about the code, so they are now one PR.

Nothing was dropped. All four commits (d02999e, 61ae9b6, 1f3cab4, 5769c9a) are in #3726 unchanged, and #3737 has been rebased onto it and retargeted.

@dbernstein dbernstein closed this Sep 17, 2026
dbernstein added a commit that referenced this pull request Sep 18, 2026
Hold in draft. Merge only after the release containing #3726 and #3736 has
gone out.

The release N repair -- migration 52d1bbdd4671 plus the startup task that
dispatches the re-score -- is exposed for as long as any old code is running.
Anything reaching Subject.assign_to_genre before the re-score lands consumes
the reset, and code on the superseded rules re-stamps checked=True with the
same wrong value.

helpers/migrate.yml stops the scripts container, where every Celery worker
runs, before migrating, so the worker path is safe. The web containers are not:
they are recycled after the migration and can reach assign_to_genre through a
presentation recalculation. Fargate deployments are not governed by that
playbook at all.

Running the reset again a release later closes both, because by then no old
code is live anywhere. The task is idempotent, so if the release N repair took,
this is a no-op.

Re-scoring is left to the nightly classify_unchecked_subjects: with no old code
to lose the reset to, dispatching it here would buy nothing.

Same shape as the null-audience repair, where startup task 2026_06_17 re-ran
what 2026_05_12 had dispatched a release earlier.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dbernstein added a commit that referenced this pull request Sep 18, 2026
Hold in draft. Merge only after the release containing #3726 and #3736 has
gone out.

The release N repair -- migration 52d1bbdd4671 plus the startup task that
dispatches the re-score -- is exposed for as long as any old code is running.
Anything reaching Subject.assign_to_genre before the re-score lands consumes
the reset, and code on the superseded rules re-stamps checked=True with the
same wrong value.

helpers/migrate.yml stops the scripts container, where every Celery worker
runs, before migrating, so the worker path is safe. The web containers are not:
they are recycled after the migration and can reach assign_to_genre through a
presentation recalculation. Fargate deployments are not governed by that
playbook at all.

Running the reset again a release later closes both, because by then no old
code is live anywhere. The task is idempotent, so if the release N repair took,
this is a no-op.

Re-scoring is left to the nightly classify_unchecked_subjects: with no old code
to lose the reset to, dispatching it here would buy nothing.

Same shape as the null-audience repair, where startup task 2026_06_17 re-ran
what 2026_05_12 had dispatched a release earlier.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dbernstein added a commit that referenced this pull request Oct 2, 2026
Hold in draft. Merge only after the release containing #3726 and #3736 has
gone out.

The release N repair -- migration 52d1bbdd4671 plus the startup task that
dispatches the re-score -- is exposed for as long as any old code is running.
Anything reaching Subject.assign_to_genre before the re-score lands consumes
the reset, and code on the superseded rules re-stamps checked=True with the
same wrong value.

helpers/migrate.yml stops the scripts container, where every Celery worker
runs, before migrating, so the worker path is safe. The web containers are not:
they are recycled after the migration and can reach assign_to_genre through a
presentation recalculation. Fargate deployments are not governed by that
playbook at all.

Running the reset again a release later closes both, because by then no old
code is live anywhere. The task is idempotent, so if the release N repair took,
this is a no-op.

Re-scoring is left to the nightly classify_unchecked_subjects: with no old code
to lose the reset to, dispatching it here would buy nothing.

Same shape as the null-audience repair, where startup task 2026_06_17 re-ran
what 2026_05_12 had dispatched a release earlier.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant