Skip to content

Ticket #2539 :: Task: Track documentation contributions and derive the Documenter achievement from them - #2641

Open
herzog0 wants to merge 24 commits into
teo/badges-docsfrom
teo/2539-source-documentation
Open

Ticket #2539 :: Task: Track documentation contributions and derive the Documenter achievement from them #2641
herzog0 wants to merge 24 commits into
teo/badges-docsfrom
teo/2539-source-documentation

Conversation

@herzog0

@herzog0 herzog0 commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Issue: #2539

⚠️ Base branch is teo/badges-docs

Summary & Context

Makes Documenter an automatic achievement. Nothing in the site recorded who wrote
documentation, so the badge could only be granted by hand. The commit importer already clones
every library repo, so asking git for per-file statistics in the same call costs one flag and no
extra API traffic. Each commit now carries a count of the documentation files it touched, and the
achievement counts commits with a non-zero count.

Getting there needed one fix first. An automatic grant identified its evidence by the row it
pointed at, and commit row ids are not stable: the importer deletes and re-creates them, and the
same commit is stored once per release range covering it. So grants were being orphaned and counts
inflated. Grants now carry the source's own id instead (a commit sha, a library key, a review
fingerprint). That part is invisible on screen but is the reason the new source can be trusted.

  • Figma link: n/a
  • Link to components/page: n/a

Changes

  • Automatic grants gain a dedup_info column and are matched on it instead of on a row id.
  • Every source iterator now names its evidence; the key is required, not optional.
  • Review can produce its own dedup fingerprint, which the import command used to keep private.
  • The commit importer reads per-file statistics and stores a doc-file count on each commit.
  • Documentation path rules live in one new module, with the classifier and rename handling.
  • A destructive commit re-import now discards the grants it would otherwise orphan.
  • The Documenter source is wired and selectable wherever a source can be named.
  • Two migrations: the grant column, and the count on Commit.

‼️ Risks & Considerations ‼️

  • Existing environments need clearing, not migrating. Grants written before this branch carry
    no key, so a reconcile replaces them rather than adopting them. Nothing is in production; staging
    and QA should have their badge tables emptied and rebuilt. I (Teo) will be doing that.
  • Counts will drop where they were inflated. That is the correct number arriving, but it reads
    like a regression.
  • The key format is now a contract. Changing how a source names its evidence later invalidates
    every grant it fed. Cheap to change now, expensive after launch.
  • Old commits read low until re-imported. The count only lands when a library is imported
    again, so doc work predating this shows as zero until then.
  • "Update Commits" still deletes and reinserts the whole table. Discarding grants first makes
    it safe for achievements; whether the button should stop doing that is an open question on the
    ticket.
  • The Documenter description still talks about Antora and BoostLook while the classifier counts any
    doc path. Those two should be settled together before a production backfill.

Screenshots

n/a - no UI. The badge already renders wherever badges render.

Peer-review testing steps

Setup: just load_production_data, just migrate, docker compose up.

  1. Updating commit authors: run the following first:
docker compose exec -T celery-worker python manage.py shell -c "
from libraries.tasks import update_commit_authors_users
update_commit_authors_users()
"
  1. The rules. In just manage shell:

    from libraries.doc_paths import is_doc_path
    [is_doc_path(p) for p in ["doc/index.adoc", "README.md", "doc/html/index.html", "meta/libraries.json"]]
    # [True, False, False, False]
  2. A real import. just manage import_commits --key mp11, then:

    -- docker compose exec db psql -U postgres postgres
    SELECT count(*) FILTER (WHERE docs_files_changed > 0) AS doc_commits,
           count(*)                                       AS total
    FROM libraries_commit c
    JOIN libraries_libraryversion lv ON lv.id = c.library_version_id
    JOIN libraries_library l ON l.id = lv.library_id
    WHERE l.key = 'mp11';
    
    -- Merges never count.
    SELECT count(*) FROM libraries_commit WHERE is_merge AND docs_files_changed > 0;  -- 0
    
    -- The stats must not have leaked into the message.
    SELECT count(*) FROM libraries_commit WHERE message LIKE '%' || chr(9) || '%';    -- 0

    Spot-check one sha against the repo: git show --numstat <sha> and count the doc paths yourself.

  3. Backfill. just manage backfill_achievements --source documentation, then run it a
    second time. The second run must report added 0.

    SELECT count(*), count(DISTINCT dedup_info)
    FROM badges_userachievement ua
    JOIN badges_achievement a ON a.id = ua.achievement_id
    WHERE a.slug = 'documentation';   -- equal
  4. Pk churn is a non-event. Note the code-commits and documentation grant counts, then
    just manage import_commits --key mp11 --clean and backfill both sources again. Counts
    unchanged, and no grant left pointing at a deleted row:

    SELECT count(*) FROM badges_userachievement ua
    JOIN django_content_type ct ON ct.id = ua.source_content_type_id
    LEFT JOIN libraries_commit c ON c.id = ua.source_object_id
    WHERE ct.app_label = 'libraries' AND ct.model = 'commit' AND c.id IS NULL;  -- 0
  5. Reconcile. just manage reconcile_achievements --dry-run. Over freshly backfilled
    data every source reports added 0, removed 0.

QA: Documenter badge

The Documenter badge could only ever be given by hand, because nothing in the site recorded who
wrote documentation. It is now worked out automatically from the commits: each commit is checked
for documentation files, and the badge counts the commits that touched some.


‼️ Read this first - the environment has to be prepared ‼️

Two things must be true before any of this will show results, and neither is something you can do
yourself
:

  1. The commit history has to be re-imported. The documentation count is worked out when a
    commit is read in, so every commit already in the database reads zero until a full re-import has
    run. On a database copied from production that is all of them.
  2. The badge tables have to be emptied and rebuilt. Achievements created before this change do
    not carry the new identifier and will be replaced rather than reused.

Teo does both of these. Confirm with him that they have been done before you start. If you run
a backfill first, you will correctly get zero grants and it will look like the feature does not
work.

Related: after that re-import, counts on some existing badges will go down. That is the correct
number arriving after a bug that inflated them - not a regression.


What this change does

  • Documenter becomes an automatic badge, alongside the others.
  • Every achievement now records what earned it in a readable form - a commit id, a library name,
    a review fingerprint - instead of pointing at a database row that can be replaced. There is a new
    read-only Dedup info field showing it.
  • Because of that, re-importing commits or reviews no longer breaks the link between a badge and
    the thing that earned it.

What it does not do

  • Nothing changes on the public website beyond the badge itself appearing, which is tested
    separately.
  • Only Regular and Publisher remain hand-granted after this.

Before you start

  1. Open /admin/badges/achievement/
    or /admin/badges/badge/ and find
    Documenter.
  2. Its Automatic column must now say yes. It used to say no.
  3. Only Regular and Publisher should still say no.

About the buttons: a status line appears under the button and updates by itself - Queued,
Running, Finished. Stuck on Queued forever means the background job system is not running
in QA; tell Teo.


Test 1: Documentation is a source you can pick

  1. Open /admin/badges/userachievement/.
  2. Open the Source dropdown next to the buttons.

Expected: Documentation is in the list. Pick it and press Backfill achievements.

Expected: the status line finishes in place, without a reload.

If you get zero grants, do not report it straight away. There are two ordinary reasons, and
both are listed at the top of this document: the commit history has not been re-imported, or the
commits belong to people whose email is not linked to a member account. Check with Teo before
treating it as a failure.

Test 2: the achievements say where they came from

  1. Still on /admin/badges/userachievement/,
    filter Achievement to Documentation.
  2. Click a row's Source link.

Expected: it opens the commit that earned it, and the commit message reads like documentation
work.

  1. Go back and open the achievement row itself.

Expected:

  • There is a read-only Dedup info field showing the commit's id from git.
  • Nothing on the page is editable except Grant notes. There is no delete button.

Test 3: you can search by commit id

  1. Copy the Dedup info value from the row you opened in Test 2.
  2. Paste it into the search box on
    /admin/badges/userachievement/.

Expected: it finds that achievement. This did not work before this change.

Test 4: the badge was awarded

  1. Open /admin/badges/userbadge/.

Expected: the members from Test 2 hold Documenter at whatever level their count reaches.

  1. Click a member's name to open their per-member page.

Expected: it shows how many valid achievements they have and how many more they need for the
next level, in plain words.

Test 5: running it twice changes nothing (the most important check)

  1. Press Backfill achievements on Documentation again.
  2. Open /admin/badges/achievementsyncrun/.

Expected: two documentation rows, the second one with Added 0, both naming you under
Triggered by.

This is what stops the weekly job doubling everybody's count every time it runs. If the second
run adds anything at all, stop and report it immediately with both run numbers.

Test 6: re-importing commits does not disturb anybody's badges

This is the whole reason for the new Dedup info field, and it is worth doing carefully.

  1. Pick a member holding Documenter and a member holding the code commits badge. For each,
    write down: how many achievements they have, what level they hold, and the Awarded at date on
    the badge.
  2. Ask Teo to re-import the commits for one library.
  3. Without pressing any badge button, check those same members again.

Expected, all four:

  • Their achievement counts are unchanged.
  • No badge was revoked.
  • No Awarded at date has moved.
  • Every achievement's Source link still opens a real commit - none of them are broken links.

A badge that disappeared, or an award date that jumped to today, is a serious bug here. Report
it with the member's email and both sets of numbers.

Test 7: hand-given achievements are left alone

  1. On /admin/badges/userachievement/
    Add, grant Documentation to any member by hand, with a note.
  2. Press Backfill achievements on Documentation again.

Expected: the hand-given achievement is still there, still shows your note, and still names you
as the person who granted it. Automatic runs never overwrite a manual decision.

Test 8: the badge shows on the profile

  1. Find a member holding Documenter and open their public profile on
    https://www.cppal-dev.boost.org/.

Expected: the Documenter badge appears in their badges list along with the rest.

  1. Check the same page in dark mode.
  2. In the admin, open that member at
    /admin/users/user/, tick Hide badges,
    save, and open their profile again in a logged-out window.

Expected: no badges at all on their public profile. Untick it afterwards.

Test 9: commit messages got longer

A side effect worth knowing about, because it is visible and it is not a bug.

Commit messages were previously being cut short in one specific case. After a re-import, roughly
1,500 commits will show their full message where they used to show a truncated one.

Expected: longer, complete commit messages on
/admin/libraries/commit/. What would be
a bug is the opposite: a message with stray numbers, tabs or file names stuck on the end of it.
Report that if you see it.


Things that look wrong but are meant to be that way

  • Zero Documenter grants until the commit history has been fully re-imported.
  • Some existing badge counts going down after that re-import.
  • Members whose commit email is not linked to an account earn nothing.
  • The Documenter description mentions specific documentation tools while the badge in fact counts
    any documentation file. That wording is still being settled - do not report it.
  • Only Regular and Publisher are still hand-granted.

Reporting anything you find

Please include:

  1. The run number from /admin/badges/achievementsyncrun/.
  2. The Dedup info value from the achievement, if one is involved - it identifies the exact
    commit.
  3. The member's email, plus a screenshot of their per-member badge page.
  4. Which test above you were on, and whether the commit re-import had been done.

Summary by CodeRabbit

  • New Features

    • Documentation contributions can now earn badges automatically.
    • Commit records track documentation files changed.
    • Automatic achievements use stable evidence identifiers to prevent duplicate awards and support cleanup of outdated grants.
    • Review imports now use consistent duplicate detection.
  • Bug Fixes

    • Improved handling of re-imported commits, renamed libraries, duplicate legacy achievements, and concurrent updates.
    • Documentation detection now excludes generated, ignored, and non-documentation files.
  • Documentation

    • Updated badge administration guidance and manual-grant information.

@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 77f3a40a-d7e1-401e-8175-d15ac313c1ec

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ff843da4-80b9-480c-ad16-3ec66b585cda

📥 Commits

Reviewing files that changed from the base of the PR and between 13f8a1f and 3ca5e13.

📒 Files selected for processing (20)
  • badges/admin.py
  • badges/migrations/0004_userachievement_dedup_info.py
  • badges/models.py
  • badges/services.py
  • badges/sources.py
  • badges/tests/fixtures.py
  • badges/tests/test_admin_badge_config.py
  • badges/tests/test_dedup_keys.py
  • badges/tests/test_sources.py
  • badges/tests/test_sync_log.py
  • docs/badges-admin.md
  • libraries/doc_paths.py
  • libraries/github.py
  • libraries/migrations/0044_commit_docs_files_changed.py
  • libraries/models.py
  • libraries/tests/test_doc_paths.py
  • libraries/tests/test_github.py
  • versions/management/commands/import_reviews.py
  • versions/models.py
  • versions/review_keys.py

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The pull request adds stable deduplication keys for automatic achievements, tracks documentation changes during Git imports, wires documentation commits to the Documenter badge, and centralizes review fingerprint generation.

Changes

Badge deduplication and documentation sources

Layer / File(s) Summary
Automatic grant identity contract
badges/models.py, badges/migrations/0004_userachievement_dedup_info.py, badges/admin.py
UserAchievement stores dedup_info. Automatic grants use conditional uniqueness on user, achievement, and deduplication data. The admin exposes the field for search and display.
Documentation change import
libraries/doc_paths.py, libraries/github.py, libraries/models.py, libraries/migrations/0044_commit_docs_files_changed.py, libraries/tests/*
Git numstat output is parsed and documentation paths are classified. Commits store docs_files_changed. Reimports update this field and remove grants tied to deleted commits.
Source keys and grant synchronization
badges/sources.py, badges/services.py, badges/tests/*, docs/badges-admin.md
Source iterators yield stable keys. _sync_source uses those keys to match, insert, deduplicate, and remove automatic grants. Documentation commits now provide the Documenter source.

Review identity reuse

Layer / File(s) Summary
Shared review fingerprint
versions/review_keys.py, versions/models.py, versions/management/commands/import_reviews.py
Review normalization and fingerprinting move to versions.review_keys. Review.dedup_key and review imports use the shared implementation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Merge Risk: ⚪ Minimal · up to 3ca5e

This change adds automatic documentation achievement tracking and related persistence updates; no actionable merge-blocking risk remains, so it is merge-ready after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant GitRepository
  participant libraries.github
  participant libraries.doc_paths
  participant Commit
  participant badges.sources
  participant badges.services
  participant UserAchievement

  GitRepository->>libraries.github: Return git log --numstat output
  libraries.github->>libraries.doc_paths: Count documentation files
  libraries.doc_paths-->>libraries.github: Return documentation-file count
  libraries.github->>Commit: Store docs_files_changed
  badges.sources->>badges.services: Yield user, commit, and commit SHA
  badges.services->>UserAchievement: Create or update keyed grant
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 80.77% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the main change: making the Documenter achievement automatic by tracking documentation contributions. It is specific and related to the changeset.
Description check ✅ Passed The description is comprehensive and follows the required structure. It explains the purpose, changes, risks, testing steps, and QA expectations. It omits the template's self-review checklist, but the…
Full details: Description check

Explanation

The description is comprehensive and follows the required structure. It explains the purpose, changes, risks, testing steps, and QA expectations. It omits the template's self-review checklist, but the detailed testing information makes this non-critical.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch teo/2539-source-documentation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@herzog0
herzog0 force-pushed the teo/2539-source-documentation branch from 27ed0df to 657de79 Compare August 21, 2026 14:48
@herzog0
herzog0 marked this pull request as ready for review August 21, 2026 15:59
@herzog0
herzog0 force-pushed the teo/2539-source-documentation branch from 657de79 to 8451733 Compare August 24, 2026 13:40
@herzog0
herzog0 force-pushed the teo/2539-source-documentation branch from 8451733 to 82dd126 Compare August 24, 2026 19:22
@herzog0
herzog0 force-pushed the teo/2539-source-documentation branch from 82dd126 to 5fe652c Compare August 25, 2026 14:16
@herzog0
herzog0 force-pushed the teo/2539-source-documentation branch from 5fe652c to 221fe98 Compare August 26, 2026 17:49
@herzog0
herzog0 force-pushed the teo/2539-source-documentation branch from 221fe98 to 67de1b6 Compare August 26, 2026 17:51
@jlchilders11
jlchilders11 self-requested a review August 26, 2026 18:36
Comment thread libraries/github.py
Comment on lines +591 to +598
relink_source_achievements(
Commit,
dict(
Commit.objects.filter(
library_version__library=library
).values_list("sha", "pk")
),
)

@javiercoronadonarvaez javiercoronadonarvaez Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
relink_source_achievements(
Commit,
dict(
Commit.objects.filter(
library_version__library=library
).values_list("sha", "pk")
),
)
relink_source_achievements(
Commit,
dict(
Commit.objects.filter(
library_version__library=library
).values_list("sha", "pk")
),
doomed_ids,
)

Hey Teo, I found this bug:

Some commits are stored more than once, under different libraries. The code says so itself, in badges/sources.py. The same commit is stored "once per library version covering it, and once per library sharing the repository."

One commit name then, can appear in two rows belonging to two different libraries. The re-pointing step looks grants up purely by commit name. It never checks whether the grant was one of the broken ones. It also grabs grants that were pointing at a healthy row in a completely different library, and moves those too.

Proposed fix:
When the code re-points grants, it already knows the list of slots it just deleted (doomed_ids). It should require both conditions: matching commit name and pointing at one of the deleted slots, thus only repairing grants that actually broke and leaving grants belonging to other libraries alone.

This fix is probably not just constrained to this file, but I believe it's the main one.

Bellow is a concrete example that Claude created to help me understand:

Walkthrough

Commit a1b2c3 is stored twice:

slot library sha
row 500 numeric/conversion a1b2c3
row 900 numeric/interval a1b2c3

Alice has one badge grant, and it points at row 900 — the copy under
numeric/interval.

Step 1 — an admin re-imports numeric/conversion. Row 500 is deleted and
comes back as row 1500. Row 900 is untouched; a different library was
re-imported, so nothing happened to it.

Step 2 — the re-pointing runs. It searches for grants whose sha is a1b2c3
and finds Alice's. It does not notice that her grant was pointing at row 900,
which is fine and still there. It moves her grant to row 1500.

Nothing visibly breaks yet. But Alice's grant is now attached to the
numeric/conversion copy of the commit, when it should still be attached to the
numeric/interval copy.

Step 3 — months later, numeric/conversion is re-imported again, and this
time its version range no longer includes commit a1b2c3. So that commit does
not come back under numeric/conversion.

Step 4 — the cleanup step runs. It deletes grants still pointing at slots
that did not come back. Alice's grant points at row 1500, which did not come
back, so her grant is deleted.

But row 900 is still sitting there under numeric/interval, still recording
that Alice wrote that commit. The evidence never went away — the grant was just
moved onto the wrong copy of it back in step 2.

The result for Alice: her commit count drops by one for no real reason. If
that tips her below a threshold, the system takes her badge away and writes a
permanent "revoked" entry in the audit trail.

Comment thread badges/services.py Outdated

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm sorry to bring this up, because it's not part of your changes, but I think it makes sense to at least flag it because the discard_source_achievements function is called in libraries/github.py:601 .

The bug:
discard_source_achievements sends every row id it was given in a single database statement, with no batching.

Proposed fix:
Simply chunk all calls in SYNC_BATCH_SIZE slices. Only 'discard_source_achievements' is lagging. The relink_source_achievements function, which sits up above the former in github.py incorporates batching, for example:

    if moved:
        UserAchievement.objects.bulk_update(
            moved, ["source_object_id"], batch_size=SYNC_BATCH_SIZE
        )

Comment thread libraries/github.py Outdated
)
# Whatever still points into the deleted ids is evidence that did
# not come back, so those grants really are stale.
discard_source_achievements(Commit, doomed_ids)

@javiercoronadonarvaez javiercoronadonarvaez Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

update_commits decides which versions to rebuild using min_version, but decides which rows to delete without it. The delete is therefore wider than the rebuild.

Before this PR, and in reference to discard_source_achievements , a clean import with a floor deleted commit rows below the floor and left the grants pointing at nothing. That was untidy, but recoverable. A later full import brought the commits back and the counts still held.

Now, however, the grants pointing into those deleted ids are deleted too, and recalculate_badges runs on every affected member. The commit rows are gone from the table, so no later import restores them.

One great example from Claude:

Say someone runs update_commits.delay(clean=True, min_version="boost-1.85.0").
Measured against your local database, here is what that does to one library:

Geometry commit rows
stored today 9,309
covered by versions ≥ boost-1.85.0 291
deleted and never rebuilt 9,018

Step 1 — the delete. doomed selects all 9,309 rows, ignoring the floor.
Their ids go into doomed_ids. All 9,309 are deleted.

Step 2 — the rebuild. Only versions at or above boost-1.85.0 are in
library_versions, so only 291 rows are re-created. The other 9,018 commits are
simply gone from the table.

Step 3 — the relink. It can only re-point grants whose commit came back —
291 rows' worth. Every grant derived from the other 9,018 still points at a
deleted id.

Step 4 — the discard. discard_source_achievements(Commit, doomed_ids)
deletes all of those grants and recalculates the badges they supported.

The result for a contributor. Bob's Commits Master count for this library
falls from 300 to a handful. That drops him below his tier threshold, so
recalculate_badges revokes his Gold badge and writes a permanent revocation
row in the audit trail.

Fortunately, 'update_commits' is not run with both the clean and min_version arguments in our codebase, so it would require a perhaps relatively simple fix: scope the delete to the versions actually being rebuilt.

doomed = Commit.objects.filter(library_version__in=library_versions.values())

@javiercoronadonarvaez javiercoronadonarvaez left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hey @herzog0 all testing was performed accurately, but left a few comments.

Let me know what you think. Overall, it's pretty much ready, so great work on this one.

@herzog0
herzog0 force-pushed the teo/2539-source-documentation branch from 67de1b6 to f8e174c Compare August 27, 2026 12:52

@jlchilders11 jlchilders11 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Pre approving pending Javiers findings, but I did not find any additional errors, and this seems sound from a design perspective.

update_commits picked the versions to rebuild with min_version but deleted
without it, so the delete was always wider than the rebuild. A run with a floor
emptied the whole library and re-created only the versions at or above it: for
geometry that is 9,309 rows deleted and 291 rebuilt.

That used to leave grants pointing at nothing, which a later full import
repaired. It no longer does. discard_source_achievements now deletes those
grants and recalculates the badges they justified, and neither half comes back
on its own - a backfill can only credit evidence that exists, and the commit
rows are gone from the table until someone runs an import with no floor.

No caller passes both arguments today: the admin button passes clean only,
import_commits has no floor option, and release_tasks passes a floor with clean
defaulting to false. The combination is reachable by hand-invoking the task,
which is enough for the delete and the rebuild to disagree.

Scoped with the same predicate that builds library_versions rather than with
that dict's values, so it stays identical to current behaviour when min_version
is empty and does not depend on LibraryVersion having one row per version name,
which nothing enforces.
This branch moved the identity of an automatic grant off the source row and onto
the source's own name for the evidence: sources.py says the key "is the source's
own name for the evidence rather than a row id", the constraint moved from
(user, achievement, content_type, object_id) to (user, achievement, dedup_info),
and _library_key falls back to a pk only because nothing re-creates Library rows.

discard_source_achievements was the one place left deciding a deletion from the
old identity. It deleted every grant pointing into the row ids a caller was
removing, which is only the same question where a key has exactly one row. A
commit is stored once per library version covering it and once per library
sharing the repository, so a sha routinely has several rows and a grant points at
whichever one the sweep happened to see first. A clean re-import of one library
that no longer reports that sha therefore deleted a grant whose evidence was
still sitting in the table under another library, revoked the badge it justified,
and left it to be earned again dated later by the next backfill - the exact churn
relink_source_achievements exists to prevent.

Callers whose keys map to one row keep the old behaviour by passing nothing, so
import_reviews is unchanged: its fingerprints are collapsed to one row per key
during the import. Survivors are read excluding the ids being discarded, so the
check is also correct for a caller that discards before deleting.

The relink is deliberately left matching on the key alone. Under this identity
the foreign key is provenance, not identity - only the admin's source column
reads it, never the count - so moving it between two rows carrying the same sha
changes nothing that is counted or displayed, and its breadth is what lets a full
clean run pull a pointer back onto a live row. Narrowing it to the ids just
deleted would harden a field the model deliberately stopped treating as identity,
and would strand pointers orphaned by earlier runs.

Also corrects the docstring on the discard test, which argued that a reconcile
could not clean up an abandoned grant. It can - an unyielded key reads as stale.
The reason the importer has to discard inline is that nothing runs a reconcile:
release_tasks runs backfill_achievements, which only adds, and the admin's clean
re-import runs no sweep at all.
discard_source_achievements sent every row id in one statement while
_sync_source, three functions below it, honours SYNC_BATCH_SIZE for exactly this
shape of work. The constant's own comment describes what this function was
doing: "Rows per DELETE ... WHERE pk IN (...)".

Nothing breaks at today's sizes - the largest library holds 9,309 commit rows -
but the cost is not only the parameter list. post_delete is connected to
UserAchievement, so the queryset delete cannot take Django's fast path: it
materialises every matching row and dispatches a signal per row, and the pair
scan above it reads the same rows again. That is precisely why the stale delete
in _sync_source batches.

The pairs are accumulated across chunks and recalculated after the last delete
rather than per chunk. Recalculating per chunk would give a member with grants in
two chunks two identical answers, which is the batching this module exists to
hold in place.

The survivor subquery is deliberately not chunked. A key whose only other row
sits in a later chunk is not a survivor, so narrowing that read to the chunk
would spare grants whose evidence is on its way out.
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.

Task: Track documentation contributions and derive the Documenter achievement from them

3 participants