Skip to content

docs(design): attribution facts as a stream, not a keyspace - #283

Merged
sunib merged 2 commits into
mainfrom
feat/attribution-fact-stream-design
Jul 28, 2026
Merged

docs(design): attribution facts as a stream, not a keyspace#283
sunib merged 2 commits into
mainfrom
feat/attribution-fact-stream-design

Conversation

@sunib

@sunib sunib commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

What

Three design records. No code changes.

attribution-fact-stream.md — the decision

Attribution facts stop being a keyspace the watch side reads and become a log it follows. The audit receiver appends one batched entry per type to a per-(route, group/resource) Redis stream, every process follows only the types it watches, and facts live in one bounded, TTL'd in-memory index. SET, GET, and the three fact key shapes are deleted.

It also:

  • deletes the deletecollection expander. One collection fact that every removal in its (type, namespace, selector, window) scope joins, ranked below every per-object fact, with an opportunistic uid set when the apiserver sent a body. That removes the response-body parsing and starts resolving the aggregated-API and metadata-only cases that degrade to committer today.
  • puts the transport behind a two-method seam, so a single-pod install can run attribution on an in-memory ring instead of Redis. Redis is already optional everywhere except attribution and the admission webhook.
  • is chosen to move toward HA. Under replicas the audit POST and the watch shard land on different processes by construction, and a resumable stream survives the rollouts an HA install spends its life in.

Keeps: the blocking grace window, commit order, attribution being optional, and one-fact-serves-every-GitTarget.

attribution-wait-poll-vs-push.md — the reasoning trail

Marked superseded, kept for the argument. Six options priced against the measurement that decided between them: audit delivery is batched by the apiserver, so the watch event arrives first by roughly --audit-webhook-batch-max-wait and the first lookup is a near-guaranteed miss. The poll loop is the normal path, not the exceptional one.

Both measurements already existed in this repo. reportAttributionStats in the e2e suite prints the wait histogram (0.5–2s with batch-max-wait=1s), and the mutation-capture corpus records which scenarios produce no audit event at all.

open-asks-priority.md

Merges the three open backlogs into one ordered queue.

Why not plain publish and subscribe

It is at-most-once and reaches only current subscribers. That is tolerable today because a missed message costs latency: the fact is still under a key. Once the keys are gone, a message lost during a reconnect is gone, and those commits are authored unknown (attribution unresolved) silently and permanently. Three routine cases make it not hypothetical: every process restart, every reconnect, and every new watch on a type all start with an empty index and no way to backfill. A stream replays from the TTL horizon and makes loss detectable.

Validation

Docs-only, so task lint-docs is the whole gate per AGENTS.md. It passes: doccheck resolves every reference, markdownlint is clean, Vale reports 0 errors.

Follow-up

Implementation lands separately on feat/attribution-fact-stream, split per the sequence in the design record.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Documentation
    • Added design guidance for streaming attribution facts, including Redis and in-memory transport options, retention, recovery, and resolution flow.
    • Documented alternatives to polling for attribution waits, including measurement approach and push-style considerations.
    • Added a prioritized queue for open design asks, including proposed removal of sibling inference and associated follow-ups.
    • Updated the design index to reflect an expanded set of currently open items.

Three design records land together.

attribution-fact-stream.md is the chosen replacement for the attribution
keyspace. The audit receiver appends one batched entry per type to a
per-(route, group/resource) Redis stream, every process follows only the
types it watches, and the facts live in one bounded, TTL'd in-memory
index. That deletes the per-key SET/GET lookup, the 150ms poll loop, and
the deletecollection expander, while keeping the blocking grace window,
commit order, and the one-fact-serves-every-GitTarget property. The
transport sits behind a two-method seam, so a single-pod install can run
attribution on an in-memory ring instead of Redis, and the same shape
works unchanged once GitTargets are distributed across replicas.

attribution-wait-poll-vs-push.md is the reasoning trail behind it, kept
as a superseded record: six options priced against the measurement that
decided between them. Audit delivery is batched by the apiserver, so the
watch event arrives first by roughly the batch window and the first
lookup is a near-guaranteed miss. Both the e2e wait histogram and the
mutation-capture corpus already recorded this.

open-asks-priority.md merges the three open backlogs into one ordered
queue.

Docs-only, so task lint-docs is the whole gate per AGENTS.md. It passes.

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

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR updates the design index and adds design records for prioritized open asks, stream-based attribution facts, and alternatives to polling during attribution waits.

Changes

Attribution design documentation

Layer / File(s) Summary
Prioritized open-asks queue
docs/design/open-asks-priority.md, docs/INDEX.md
Defines prioritization rules, removes sibling inference, organizes queued and declined work, and updates the index of open design items.
Attribution wait strategy
docs/design/attribution-wait-poll-vs-push.md
Marks polling guidance as superseded and compares measurement, circuit-breaking, push notifications, and ordered buffering options.
Stream-based attribution flow
docs/design/attribution-fact-stream.md
Defines grouped Redis Stream facts, cursor-based following, an in-memory fact index, resolver waiters, and collection-delete facts.
Collection facts and transport operation
docs/design/attribution-fact-stream.md
Specifies replay and trim-gap handling, transport alternatives, rollout behavior, operational constraints, and open questions.

Estimated code review effort: 3 (Moderate) | ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description has useful detail, but it does not follow the required template sections and omits Type of Change, Testing, Checklist, and related fields. Reformat the PR body to match the template and add the missing sections: Type of Change, Testing, Checklist, Related Issues, Screenshots, and Additional Notes.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: moving attribution facts to a stream-based design.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/attribution-fact-stream-design

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/design/attribution-fact-stream.md`:
- Around line 603-612: Update the Redis requirement documentation and
corresponding flag/validation/configuration descriptions so empty --redis-addr
is allowed for attribution when the selected transport is in-memory, while Redis
remains required for the Redis transport and admission webhook; keep all three
surfaces consistent.
- Around line 690-693: Update the fact-schema retention section around the
ephemeral-facts explanation to accurately identify the stored author, display
name, and email fields, and describe their retention in Redis streams and
in-memory indexes under the configured attribution TTL. Replace the “no user
data” and automatic-expiry-only claims with the applicable privacy and access
controls, keeping the migration behavior description consistent.
- Around line 695-696: Update the mixed-version rollout guidance around the
restart behavior to explicitly define compatibility when switching from keys to
streams. Require dual-read/dual-write, an atomic cutover, or a drain-based
rollout so old and new writers/readers cannot ignore each other’s events, and
reconcile the stated rolling-update safety claim with this requirement.
- Around line 191-202: Update the attribution index key construction and
matching around LookupAuthorResolution to include route alongside each existing
key shape: exact, latest, rv-only, and collection. Propagate route through
waiter candidate keys and collection-delete matching so facts from different
routes cannot collide, and add a test proving identical resource, UID, and RV
values remain isolated across routes.
- Around line 486-498: The “Starting up and catching up” section must define a
concrete Redis stream ID derived from now - factTTL, such as <unix-ms>-0, for
the initial per-stream XREAD position. Separate this catch-up backfill contract
from the XTRIM MINID trimming-gap detection, and remove wording that could imply
XREAD accepts MINID.
- Around line 137-149: Update the batching design around serveEventListRequest,
processEvent, and RecordFact to define a stable audit-event or request-batch
identity and deduplicate facts before each grouped XADD. Alternatively, use a
supported idempotent XADD mechanism that returns the existing stream entry for
retries; ensure retried audit POSTs do not create duplicate stream entries.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 34c5a9e9-5336-428c-8abf-995016871ac8

📥 Commits

Reviewing files that changed from the base of the PR and between b7ee21b and 662192c.

📒 Files selected for processing (4)
  • docs/INDEX.md
  • docs/design/attribution-fact-stream.md
  • docs/design/attribution-wait-poll-vs-push.md
  • docs/design/open-asks-priority.md

Comment thread docs/design/attribution-fact-stream.md
Comment thread docs/design/attribution-fact-stream.md
Comment thread docs/design/attribution-fact-stream.md
Comment thread docs/design/attribution-fact-stream.md
Comment thread docs/design/attribution-fact-stream.md Outdated
Comment thread docs/design/attribution-fact-stream.md
@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Six findings from the review, three of them corrections and three that the
record left implicit.

The index keys now carry the audit route. The streams are partitioned by
(route, group/resource) and the whole cross-cluster argument rests on that
dimension, but the four in-memory structures were keyed on the type alone,
which would pool two clusters' facts in one process-wide map and let a watch
event on cluster B take an author from cluster A. The rv-only hatch is where
that bites hardest, since a resourceVersion is not unique across clusters. The
route travels through the waiter keys and the collection scope match too, and
a cross-route test is named as the thing that proves it.

"No user data is stored in them" was wrong: a fact names an actor and carries
the username, display name, and email. There is now a section saying what is
held, for how long, where, and who can read it.

XREAD takes a concrete position per stream, so the catch-up section says how
the TTL horizon becomes one — "<unix-millis>-0" — instead of implying an XREAD
MINID that does not exist. MINID stays where it belongs, on the trim.

The three that were implicit: a retried audit POST may append the same batch
twice and that is safe, because a fact is keyed data rather than a position in
a sequence, so no XADD idempotency mode is needed; the startup validation has
to require Redis only for the Redis transport and the admission webhook, or
the in-memory mode is unreachable by construction; and a mixed-version rollout
is one process replacing one process, because the chart refuses a second
replica, with the dual-write answer noted for when HA makes the window real.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/design/attribution-fact-stream.md`:
- Around line 754-766: Update the HA rollout discussion near “what this buys for
high availability” to make compatibility an explicit rollout requirement:
prevent reader upgrades until old writers are drained or replaced, or require
dual-read support or dual-writing before deploying new readers. Clarify that
upgraded writers alone do not protect the old-writer/new-reader phase, and
remove the implication that compatibility can be deferred until later.
- Around line 165-177: Update the “A retried audit POST may append twice, and
that is safe” section to acknowledge that duplicate batches consume MAXLEN
capacity and can evict unrelated facts before factTTL expires. Document
retention as bounded by both factTTL and the stream entry cap, including
cap-sizing guidance and early-eviction behavior, and remove or qualify claims
that duplicates cost only one entry’s worth and are harmless.
- Around line 514-519: Update the reader startup/reconnect behavior description
to use the in-memory last-seen cursor for reconnects, falling back to the
factTTL retention horizon only after a process restart with no cursor. Clarify
that XREAD with id-x returns only IDs greater than id-x, so the horizon rendered
as <now - factTTL in unix milliseconds>-0 skips an entry exactly at the
boundary, while MINID retains entries at or above its threshold.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c84c1f1f-3be4-46df-8c69-c3150d2b4e54

📥 Commits

Reviewing files that changed from the base of the PR and between 662192c and 98cb282.

📒 Files selected for processing (1)
  • docs/design/attribution-fact-stream.md

Comment thread docs/design/attribution-fact-stream.md
Comment thread docs/design/attribution-fact-stream.md
Comment thread docs/design/attribution-fact-stream.md
@sunib
sunib merged commit 3753575 into main Jul 28, 2026
19 checks passed
@sunib
sunib deleted the feat/attribution-fact-stream-design branch July 28, 2026 11:51
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