Make the profile the source of truth for author credit - #2093
Conversation
| end | ||
| end | ||
|
|
||
| context 'via search_by_params (the user-facing path)' do |
There was a problem hiding this comment.
🤖 From Claude: Behavior change worth knowing: an author name can no longer be one term of an AND full-text query (search("John technology") used to work). Honoring the credit preference needs per-person branching that a flat SearchCop index cannot express, so person-name search moved entirely to by_credited_person_name, OR-ed in by search_by_params. Single-term author search still works.
| <%= link_to divergence_record_title(record), polymorphic_path(record), | ||
| target: "_blank", rel: "noopener", | ||
| title: "Opens in a new tab", | ||
| class: "text-blue-700 hover:underline" %> |
There was a problem hiding this comment.
🤖 From Claude: Deliberate deviation from the eyebrow convention. These link to 8 different destinations, none of which has a return_to mechanism today (they use hardcoded utility-link rows), so retrofitting all eight was out of scope here — using target="_blank" instead. Happy to do the eyebrow work as a follow-up if you would rather link in-page.
| end | ||
| end | ||
|
|
||
| context 'via search_by_params (the user-facing path)' do |
There was a problem hiding this comment.
🤖 From Claude: Behavior change worth knowing: an author name can no longer be one term of an AND full-text query (search("John technology") used to work). Honoring the credit preference needs per-person branching a flat SearchCop index cannot express, so person-name search moved entirely to by_credited_person_name, OR-ed in by search_by_params. Single-term author search still works.
| <%= link_to divergence_record_title(record), polymorphic_path(record), | ||
| target: "_blank", rel: "noopener", | ||
| title: "Opens in a new tab", | ||
| class: "text-blue-700 hover:underline" %> |
There was a problem hiding this comment.
🤖 From Claude: Deliberate deviation from the eyebrow convention. These rows link to 8 different destinations, none of which has a return_to mechanism today (they use hardcoded utility-link rows), so retrofitting all eight was out of scope — using target="_blank" instead. Happy to do the eyebrow work as a follow-up if you would rather link in-page.
| <%= link_to divergence_record_title(record), polymorphic_path(record), | ||
| target: "_blank", rel: "noopener", | ||
| title: "Opens in a new tab", | ||
| class: "text-blue-700 hover:underline" %> |
There was a problem hiding this comment.
🤖 From Claude: Deliberate deviation from the eyebrow convention. These rows link to 8 different destinations, none of which has a return_to mechanism today (they use hardcoded utility-link rows), so retrofitting all eight was out of scope — using target="_blank" instead. Happy to do the eyebrow work as a follow-up if you would rather link in-page.
bdcfd5d to
c1d2886
Compare
Name display was asked twice with two value sets: once on the person profile (people index) and once per item (every credit), so the same facilitator could render three different ways with no single place to fix it. Point credits at the profile instead. Anonymity stays a per-item latch — a person may want four stories credited and the fifth not, and nothing should be able to de-anonymize an item that was submitted anonymously. Co-Authored-By: Claude <noreply@anthropic.com>
Strip the per-item credit select from all 8 forms and show a warning only where the stored snapshot disagrees with the profile. The stored column stays as the consent record, editable in one place. Search now honors the preference too: an anonymous credit matches nothing, first_name_only isn't findable by last name, and first_name_last_initial matches only the initial. Dropped the person_first/person_last SearchCop attributes that bypassed this. Co-Authored-By: Claude <noreply@anthropic.com>
Main introduced shared/_badge while this branch was in flight; the reconciled pill was hand-rolled. Co-Authored-By: Claude <noreply@anthropic.com>
Sort each person's suggested preference into the worklist and name it inline (e.g. "Suggested: First name only — most restrictive across Maria Johnson's content") so the hint is specific to the person rather than a generic line. Add a person name-search picker to the filters and order groups by first then last name. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Keeps the admin worklist open while checking a profile, matching the content links that already open in a new tab. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Clearing a single column is what makes that column safe to drop, so each one gets its own subsection and its own all-clear note naming the field. Guess who each free-text name refers to so an admin confirms rather than looks every one up, resolved in one query for the page. Co-Authored-By: Claude <noreply@anthropic.com>
Every fix on the author credit divergences page (Save stored consent, Apply to profile, Credit to a person) did a full-page redirect. Respond with a Turbo Stream that re-renders just the results frame and flash, so the worklist updates in place with no page flip. Non-Turbo requests still redirect. Also widen the remote-select left padding so the search icon never overlaps the placeholder or selected value. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- author_credit returns "Anonymous" for a stored-anonymous item even when it carries a legacy free-text name — that name belongs to no profile, so nothing else would suppress it. - Add a credited_openly scope so a NULL snapshot (which means "follow the profile") stays visible; a bare where.not would drop it, since NULL never compares unequal. Use it for a person's public authored content. - Build the divergences result only on the frame request, so the full page load stays cheap. - Under an active filter, an empty section is the filter's doing, not a milestone, so section-clear withholds the congratulations and cleanup note. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The divergences page couldn't finish its own job. An item whose stored consent was "anonymous" offered no way back to "follow the profile", so the only exit was picking a name format — re-recording a consent nobody gave. Per-item anonymity is the legacy state this page exists to drain, and the profile is the source of truth, so clearing it is now allowed on every row. The author picker fell back to the creator, so a workshop credited to the legacy name "Lisa Cohen" showed "Umberto User" in its own edit form — and the next save would have written that creator into author_id, destroying the legacy credit. The picker now reflects only the record's own author (still defaulting to the creator on new records) and names the legacy credit standing in when there isn't one. Sorting COALESCEd author, creator, legacy while display used author, legacy, creator, so legacy-credited rows sorted under a name they never show. A note under each author picker flags when the credited person's profile suppresses credits, since nothing on the record itself reveals that. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…the creator The column reads better as the state it is than as a predicate on a collection, and it now matches the "Anonymous contributions" label already used in the profile form and the factory trait. The migration is unmerged, so it changes in place rather than stacking a rename on top of itself. A legacy free-text name follows nobody's profile, so a legacy-credited row now reports no governing person. It was being grouped on the divergences page under whoever happened to enter it, asserting that person's profile had drifted from a consent snapshot that never described them. Those rows have to be matched to a real person by hand against the legacy text — the creator is not a fallback for them. Community news author is optional on the model, so the form offers a blank option rather than forcing whoever edits one of the legacy authorless rows to credit somebody before they can save anything else. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
a13d917 to
11fc630
Compare
🤖 suggested review level: 5 Inspect 🔬 changes how every author credit renders app-wide, narrows search results, and adds an admin page
Name display was asked in two unrelated places with two different value sets —
people.display_name_preference(people index) and<item>.author_credit_preference(every credit, on 8 forms).AuthorCreditabledeliberately calledperson.full_namerather thanperson.name, so the per-item answer won and the profile was ignored. One facilitator could render three different ways with no single place to fix it.Now: the profile formats every credit. The 8 per-item selects come off. The stored column is retained as the consent record, human-editable only on a new admin reconciliation page.
Two axes on the profile
display_name_preference(4 values)Person#nameand author creditsanonymous_contributions(new)The one asymmetry
A stored
author_credit_preferenceno longer drives display — exceptanonymous, which is always honored while it's set. Anonymity is true if the profile says so or the item says so; neither strips the other's flag. Three reasons it has to work this way:anonymouswould start rendering its author's real name on deploy.Search now honors the preference
Anonymous → matches nothing.
first_name_only→ not findable by last name.first_name_last_initial→ matches the initial, not the whole last name. This also meant dropping theperson_first/person_lastSearchCop attributes onStory/CommunityNews, which bypassed the gate entirely.Reconciliation page (
/author_credit_divergences, linked from admin home)Content whose snapshot disagrees with its author's profile, grouped by person, filterable by
person_id/type/preference/include_reconciled. Resolvable in both directions: apply one preference to the person's profile (suggested = most restrictive across their content), or edit a single item's stored value.Worklist UX (low-tech admin): filters in a card (incl. a person remote lookup) apply live with no Filter button; per-section counts up top with jump links; each card shows the action after the supporting data; and every fix (Save / Apply to profile / Credit) posts over Turbo, re-rendering just the results frame + flash instead of reloading. Any stored value — including
anonymous— can be cleared to None (follow profile) (normalizes tonil); that is how a legacy per-item flag gets drained, and it hands the item back to the profile. A section that's empty because of a filter says so rather than congratulating you and suggesting a column drop.Legacy credits are matched by hand, never inherited
A legacy free-text name follows nobody's profile, so a legacy-credited row has no governing person. It used to be grouped on the divergences page under whoever entered it, claiming that person's profile had drifted from a snapshot that never described them.
credit_governing_personnow returnsnilfor those, and they're resolved only by matching a real person against the legacy text (section 2 already suggests one by name).Author pickers
The picker fell back to
author_person, which includes the creator — so a workshop credited to the legacy name "Lisa Cohen" rendered "Umberto User" in its own edit form, and the next save would have written that creator intoauthor_idand destroyed the legacy credit. It now reflects only the record's ownauthor(still defaulting to the creator on new records), and a shared note under each of the 5 pickers says when the credited person's profile suppresses credits, or when no author is set and a legacy free-text name is standing in. Community newsauthor_idis optional on the model, so that form gained the blank option it was missing.Also swept
Four places where a real name escaped past an anonymized credit: the variation-ideas index link, the story form's subtitle,
Workshop#author_name, and two deadResourceDecoratormethods. Plus two phantom strong params (profile_show_name_preference,promoted_to_story) that referenced columns that don't exist.Three more consistency edges: a stored
anonymousoutranks a legacy free-text name (that name belongs to no profile, so nothing else would suppress it); a person's public authored content goes through acredited_openlyscope so a NULL snapshot (which just means "follow the profile") stays visible — a barewhere.notwould drop it, since NULL never compares unequal in SQL; andorder_by_authorCOALESCEd author → creator → legacy while display used author → legacy → creator, so legacy-credited rows sorted under a name they never show.Verified
Full suite green (5969 examples, 0 failures), rubocop clean, brakeman 0 warnings, bundler-audit clean. Drove it in the running app: each format change propagates to the index and every credit at once; both anonymity paths suppress the credit and the link; search gating confirmed against real data; both warning variants render; the reconciliation page lists, filters, and saves.
🤖 Generated with Claude Code