Skip to content

fix(feed): dedup rule-driven counterparty/series rows on the timeline#1920

Merged
canalesb93 merged 1 commit into
mainfrom
claude/awesome-lovelace-5v7ar1
Jun 24, 2026
Merged

fix(feed): dedup rule-driven counterparty/series rows on the timeline#1920
canalesb93 merged 1 commit into
mainfrom
claude/awesome-lovelace-5v7ar1

Conversation

@canalesb93

Copy link
Copy Markdown
Owner

What & why

Fixes #1915 — on the home Feed, a single retroactive (or sync-time) assign_counterparty / assign_series rule showed up as two events: the canonical rule_applied row and a duplicate membership row attributed to the system actor "Breadbox":

Netflix Counterparty ran a rule on 16 transactions with rule Netflix Counterparty
Breadbox assigned to a counterparty 16 transactions to Netflix

Root cause

When a rule fires assign_counterparty / assign_series, two annotations are written inside the same transaction:

  1. rule_applied — attributed to the rule (the canonical audit record), and
  2. a structural side-effect row (counterparty_assigned / series_assigned) — attributed to SystemActor() ("Breadbox").

The rule-source dedup in EnrichAnnotations (isRuleSourceDuplicate) already collapses this pair for tag/category kinds by dropping the side-effect row when its payload carries source: "rule". The membership kinds were added after that dedup landed and were never wired in — the membership write path didn't stamp source: "rule", and the dedup switch didn't list those kinds. So the side-effect row escaped dedup and rendered as a second feed event with the wrong-looking "Breadbox" actor.

The fix (mirrors the existing tag/category pattern)

  • The rule-write paths (AssignCounterpartyFromRuleTx / AssignSeriesFromRuleTx) now stamp source: "rule" into the membership payload via a threaded source argument. User- and agent-authored assignments pass "" and continue to surface as first-class events.
  • isRuleSourceDuplicate now drops counterparty_assigned / counterparty_unlinked / series_assigned / series_unlinked rows carrying source: "rule".
  • formatRuleSummary phrases the surviving rule_applied row for the counterparty / series action fields (instead of the generic "applied") so the per-transaction timeline stays readable.

This fixes both surfaces that share EnrichAnnotations — the home Feed and the per-transaction activity timeline.

Evidence

This is a deduplication-logic bug, so the proof is a deterministic before/after of the same end-to-end path the screenshot exercises (seed 3 Netflix charges → create an assign_counterparty rule → apply retroactively → read ListFeedEvents).

Before the fix (dedup switch reverted to its old tag/category-only form) — the exact two rows from the issue:

--- FAIL: TestApplyRuleRetroactively_AssignCounterparty_FeedSingleEvent
    bulk_action kind="rule_applied"          actor="Netflix Counterparty"
    bulk_action kind="counterparty_assigned" actor="Breadbox"
    bulk_action events = 2, want 1 (rule_applied only; membership side-effect deduped)
FAIL

After the fix — collapses to one event:

--- PASS: TestApplyRuleRetroactively_AssignCounterparty_FeedSingleEvent (0.62s)
PASS
ok  	breadbox/internal/service

New regression tests added:

  • TestApplyRuleRetroactively_AssignCounterparty_FeedSingleEvent (integration) — the end-to-end feed assertion above.
  • TestEnrichAnnotations_DropsRuleSourcedMembership (unit) — rule-source membership rows are deduped; surviving summary reads Rule "Netflix Counterparty" set the counterparty retroactively.
  • TestEnrichAnnotations_KeepsUserAuthoredMembership (unit) — user/agent-authored membership rows still survive.

Validation run locally:

  • go build ./..., go vet ./... — clean.
  • go test ./... (unit) — green for touched packages (the unrelated internal/agent process-reaping test fails identically on a clean checkout — pre-existing/env-specific).
  • go test -tags integration -p 1 ./internal/service/ ./internal/sync/ — green.

Notes

  • Docs: docs/activity-timeline.md dedup contract updated to list the membership kinds and to note "stamp source: "rule" and add the kind to isRuleSourceDuplicate" when introducing a new rule-write path.
  • Attribution doctrine is unchanged: a rule firing is still attributed to the rule (rule_applied). Collapsing the duplicate removes the stray system-actor "Breadbox" row, which is what made the attribution look off in the issue.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CvAru8AVLa95DGfZg4M1wj


Generated by Claude Code

A retroactive (or sync-time) assign_counterparty / assign_series rule
wrote both a `rule_applied` row (attributed to the rule) and a
membership side-effect row (`counterparty_assigned` / `series_assigned`,
attributed to the SystemActor "Breadbox"). The rule-source dedup in
EnrichAnnotations only covered tag/category kinds, so the membership row
escaped it and the same application surfaced as two adjacent feed events
("Netflix Counterparty ran a rule …" + "Breadbox assigned … to Netflix").

Mirror the category/tag pattern for membership kinds:

- The rule-write paths (AssignCounterpartyFromRuleTx / AssignSeriesFromRuleTx)
  now stamp `source: "rule"` into the membership payload via a threaded
  `source` arg; user- and agent-authored assignments pass "" and survive.
- isRuleSourceDuplicate drops counterparty_assigned/_unlinked and
  series_assigned/_unlinked rows carrying source="rule".
- formatRuleSummary phrases the surviving rule_applied row for the
  counterparty / series action fields instead of falling back to "applied".

The result: one rule_applied event, no duplicate "Breadbox …" row.

Fixes #1915

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvAru8AVLa95DGfZg4M1wj
@canalesb93 canalesb93 force-pushed the claude/awesome-lovelace-5v7ar1 branch from 08f52d9 to 91c4268 Compare June 24, 2026 06:43
@canalesb93 canalesb93 merged commit 76cfe39 into main Jun 24, 2026
8 checks passed
@canalesb93 canalesb93 deleted the claude/awesome-lovelace-5v7ar1 branch June 24, 2026 06:53
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.

[Bug] Some actions I took are shown as if the rules took them on the Feed timeline?

2 participants