Skip to content

[NRT-771] Post-flag-flip cleanup: drop AUTO_PROTECT_FEATURE_FLAG scaffolding + thread NoteDiff#1305

Draft
RisingOrange wants to merge 5 commits into
mainfrom
worktree-nrt-771-flag-cleanup
Draft

[NRT-771] Post-flag-flip cleanup: drop AUTO_PROTECT_FEATURE_FLAG scaffolding + thread NoteDiff#1305
RisingOrange wants to merge 5 commits into
mainfrom
worktree-nrt-771-flag-cleanup

Conversation

@RisingOrange

@RisingOrange RisingOrange commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

Related issues

Proposed changes

auto_protect_fields_when_edited is now permanent for all users, so this removes the feature-flag scaffolding NRT-749 added. It also lands a related performance cleanup that was deliberately deferred until the flag was gone — reusing the per-note diff computed when the suggestion dialog opens instead of recomputing it on submit (see §3 for why the flag blocked it).

1. Remove AUTO_PROTECT_FEATURE_FLAG scaffolding

  • Delete the constant + its now-unused config import in main/suggestions.py.
  • Inline the 4 gate sites so the gated code always runs:
    • editor.py — auto-protect-on-unfocus hook (drops the early-return flag check).
    • suggestion_dialog.py — the bulk empty-state gate and the _setup_ui field-selection-widget gate.
    • decks_dialog.py — the "Automatically protect fields when edited" box is always shown.
  • Both suggestion builders now pass include_protected_fields=True unconditionally.

2. Tighten BulkSuggestionFilters

  • fields_to_include_by_mid is now required (drops Optional); for_mid no longer has a None "ship-everything" branch. The dialog's widget always renders and produces a concrete per-mid selection, so None was unreachable legacy. Empty-construction sites pass {}.

3. Reuse the note-diff on submit instead of recomputing it ("thread NoteDiff to the submit path")

Plain version: When you edit a note and open the suggestion dialog, the add-on computes a NoteDiff per note — what changed versus the version AnkiHub has on record (the changed fields, added/removed tags, new media). That diff is what powers the "Include in suggestion" picker. Previously it was thrown away once the dialog opened: when you hit submit, the builders re-derived the exact same information from scratch — re-converting each note, re-reading the AnkiHub DB, re-comparing fields and tags — plus an extra batch query. This change threads (passes along) that already-computed diff from the dialog through to the submit path, and the submit builders now just apply your selection on top of the diff instead of recomputing it: the diff is computed once and is the single source of truth, and a duplicate AnkiHubNote.filter(...) query is dropped.

Why it was deferred: while the flag existed, the open-time conversion always included protected fields but the submit-time conversion only did so when the flag was on — so reusing the open-time diff could have shipped the wrong content. With the flag gone, both sides behave identically, so reuse is now safe. (Hence folding it into this ticket rather than NRT-749.)

Details: NoteDiff carries local_note: NoteInfo (the local Anki note, paired with ah_note) and changed_fields: List[Field] — the fields a suggestion would ship (changed-vs-AH for existing notes, non-empty for new notes); changed_field_names is a derived property over it. Both the bulk and single-suggestion dialogs thread their open-time compute_note_diffs map to the submit path: _suggestions_for_notes classifies notes from the diff (exists_in_ah_db / is_deleted_on_remote, dropping the duplicate AnkiHubNote.filter(anki_note_id__in=...) batch query) and hands each note's diff to _change_note_suggestion / _new_note_suggestion. Those builders just apply the user's allowlist to changed_fields / added_tags / removed_tags — the field/tag comparison (_fields_that_changed, _added_and_removed_tags) lives only in compute_note_diffs. A builder self-computes the diff when one isn't supplied (standalone callers / tests). BulkSuggestionFilters.fields_to_include_by_mid is required, and within an explicit filter a missing per-mid entry ships none of that note type's fields (the dialog's per-section selection). Passing no filter at all (filters=None) means "no filter — ship everything the diff detected", matching the single-note path; the dialog always passes an explicit selection, so None is a programmatic convenience.

Tests

  • Deleted the two flag-off legacy tests; dropped the set_feature_flags({"auto_protect_fields_when_edited": ...}) enabling lines (the per-deck set_auto_protect_fields_when_edited setting stays).
  • Gave bulk-path tests explicit fields_to_include_by_mid (the old None "ship-everything" default now ships nothing), and updated the shared mock_suggestion_dialog fixture to build its allowlist from the diffs — mirroring the real widget's default-all-checked selection.

Gating note for reviewer: this ticket is gated on the auto_protect_fields_when_edited flag being permanently flipped on for all users. Worth a final confirmation that the rollout is retired server-side before merge.

How to reproduce

N/a — internal refactor, no user-facing behavior change (given the flag is on for everyone). Covered by tests.

Further comments

Deliberately left PerNoteFilters / suggest_note_update / suggest_new_note filters=None "ship-everything" default in place: unlike the BulkSuggestionFilters map (only ever populated by the dialog widget), that's a sensible default for the general-purpose single-note suggest entry points, not flag residue.

…read NoteDiff

The auto_protect_fields_when_edited rollout is permanent, so remove the
feature-flag scaffolding added by NRT-749:

- Delete AUTO_PROTECT_FEATURE_FLAG and inline its 4 gate sites (editor
  auto-protect hook, bulk empty-state gate, dialog widget gate, deck-options
  auto-protect box). include_protected_fields is now unconditionally True.
- Make BulkSuggestionFilters.fields_to_include_by_mid required (drop the
  legacy None "no-filter" fallthrough in for_mid); the dialog widget always
  renders and produces a concrete per-mid selection now.
- Thread the open-time NoteDiff map through suggest_notes_in_bulk ->
  _suggestions_for_notes so the submit path reuses cur/ah_note instead of
  re-deriving them, and drop the duplicate AnkiHubNote.filter batch query.

Tests updated for the simplified contracts: removed the flag-off legacy tests,
gave bulk-path tests explicit field allowlists, and updated the shared
suggestion-dialog mock to mirror the widget's default selection.
… cleanup

- Fix: `_new_note_suggestion` now drops empty fields before applying the
  allowlist. With `diff.cur` (include_empty_fields=True) and the widget's
  per-mid aggregated selection, a field empty on one new note but allowlisted
  via a sibling note of the same type would otherwise ship as an empty field;
  the pre-threading path excluded it (include_empty_fields=False). Adds a
  regression test.
- Extract the duplicated "build per-mid allowlist from diffs" loop into a
  shared `bulk_filters_from_diffs` test helper.
- Reword the now-misleading "legacy" docstrings/comments around the
  PerNoteFilters None path and `for_mid`.
Threading NoteDiff only reused the conversions (local_note/ah_note); the
builders still re-ran _fields_that_changed / _added_and_removed_tags. Finish
the job:

- NoteDiff now carries `changed_fields: List[Field]` (the fields a suggestion
  would ship — changed-vs-AH for existing notes, non-empty for new notes);
  `edited_fields` becomes a derived property over it.
- `_change_note_suggestion` / `_new_note_suggestion` consume the diff directly
  (apply the user's allowlist to `changed_fields` / `added_tags` / `removed_tags`)
  instead of recomputing it. `_fields_that_changed` / `_added_and_removed_tags`
  now live only in `compute_note_diffs`.
- The new-note empty-field guard moves into `compute_note_diffs`, so the rule
  holds for every consumer of the diff in one place.
- Drop the per-builder `diff-or-to_note_data` branching: a single line ensures a
  diff exists (self-computed for callers that don't supply one), then the body
  reads it uniformly.

Also: rename `NoteDiff.cur` -> `local_note` (pairs with `ah_note`), and rewrite
the added comments to describe the code as it stands rather than narrate the PR.
…iff, polish

- Make `filters` required on `suggest_notes_in_bulk` / `_suggestions_for_notes`
  (drop the silent `filters or {}` default). An empty/absent per-mid map ships
  no fields for that note type; there's no implicit "ship everything" in the
  bulk path, so a caller omitting filters now fails fast instead of silently
  shipping nothing. Fix the `BulkSuggestionFilters` docstring to say so.
- Thread the open-time `NoteDiff` through the single-suggestion path too
  (`suggest_note_update` / `suggest_new_note` gain an optional `diff`, fed from
  the dialog's open-time map), mirroring the bulk path so submit no longer
  recomputes the diff. Self-computed when omitted.
- Rename the `NoteDiff.edited_fields` property to `changed_field_names`
  (pairs with `changed_fields`; the `_names` suffix makes the List[str] honest).
- Comment/docstring cleanup: trim the `compute_note_diffs` docstring (was
  leaking `to_note_data` internals) and the `changed_fields` field comment;
  drop change-narrating phrasing.
…all)

Reconsidered the "make filters required" call from the prior review pass.
`suggest_notes_in_bulk(filters=None)` now means "no filter — ship everything
each note's diff detected", matching the single-note path's `filters=None`
and giving an ergonomic "just suggest these notes" call. This is distinct
from passing an explicit `BulkSuggestionFilters` with a `{}`/partial map,
where a missing per-mid entry still ships no fields for that note type (the
dialog's explicit-selection semantics, unchanged). The dialog always passes
an explicit filter, so None is purely a programmatic convenience.

Restored the natural no-filter form in the basic bulk tests so the
None = ship-all path is covered.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant