Facts port: who acted and with what access, every label event, every review with its dismissal, and the acts since a writer (#191)
Part of #191: the facts port's second pull request, the timeline. It builds on the transport and adapters/pull.py, which #264 landed.
What it adds: adapters/timeline.py.
- Accounts. An actor's kind comes from GitHub's account type and the configuration the caller gives, by account id, never from a name's spelling (decision 0005, L-57).
machine_useris the User named as the approver, and no other.lensis a Bot named as a Lens's App, read as that Lens's name.- Any other Bot is
app, and any other User ishuman. - A User's write access, the machine user's among them, is read from the collaborator permission.
admin,maintainandwritecount;triagedoes not, as the Maintainer ruled on #222. It is read once a login for the snapshot. An App is never asked. - These are refused, never guessed: a User whose login GitHub would not give,
[bot]among them; a permission GitHub does not give; and a 404 on the permission.
- The timeline and the reviews. Both are read page by page, following the
linkheader. A list is whole only when its last page names no next page, never by a page's length (§3.3). An item that is not an object is refused. label_events. As the timeline lists them, with who made each and when.reviews.- Each review comes with its commit and that commit's content id: the sha256 of the compare diff against the base's head. P-29 shows that this is the pull request's own content.
- A commit too large to diff, or one no longer there, takes the commit's own id, as
adapters/pull.pydoes for the head. - From the timeline: who dismissed each review and what it was before.
- No text of GitHub's is kept: no body, no dismissal message, no comment.
since_writer.- It gives when a person with write access last acted, and how many acts the Factory made since: the machine user's approvals, a dismissed one included; and the machine user's or a Factory App's labels and queue moves (decision 0003).
- A mention or a subscription is nobody's act.
- An action an App took with a person's token is not that person's.
- An act in the same second as a writer's action is counted, the reading that stops work.
- Tests:
tests/test_adapters.py, classTimeline. They replay P-7's reviews, dismissals and timeline, P-29's compare, and P-28. A stand-in answers the permission and compare reads no fixture holds yet; the permission's trial is the one #222 plans.
Not here: threads. They need GraphQL for their resolved state, and their category join reads the Lens's applied artifact, which hits the same redirect as #192's outcome read. The four thread rows and 0038's join wait on adapters/threads.py, which is now in adapters/AGENTS.md's plan.
Rows.
- 0003's acts row, 0005's account row and L-57 name the tests that hold them.
- 0005's row says a thread's accounts wait on
adapters/threads.py. - L-57's row says what each credential is shown is recorded by #222's trial.
Mutants. 31 on adapters/timeline.py, each naming its tests, each checked to compile and killed by hand. An equivalent one was dropped: a 404's body names no permission, so the permission check refuses it anyway.
Reviews, one fix round on 2ff5469: a fresh-context Claude review, a security pass, and AGY on Flash, which approved with one finding: a mutant that broke the syntax instead of the rule. "Muse: not run".
Taken:
- The machine user was read as unable to write (Claude; blocking).
kernel/derive.py's standing approval needscan_write, so its approval could never stand. It is now read like a person's, and matched by id (both). - A dismissed approval fell out of the act count (both; blocking). The timeline gives a review's state now, so the re-approval loop the ceiling bounds went uncounted.
- A list was whole at a short page (Claude; blocking). Section 3.3 forbids that; GitHub's
linkheader names the next page. - Any App's label or queue move counted as the Factory's (both).
- An act in the writer's second was not counted (both).
- A review's 406 or lost commit refused the whole snapshot (Claude).
- An item that is not an object was skipped (Claude).
- A 404 on the permission read as
none(security). That fails open for a renamed writer. - An App's action with a person's token counted as that person's (security).
- A login with dots or a slash reached the path (security).
- Three mutants were killed for unrelated reasons, and one broke the syntax (Claude, AGY).
- 0005's and L-57's rows claimed more than their tests hold (Claude).
Not taken:
- That the queue events' names are ungrounded (security). No fixture holds
added_to_merge_queueon a REST timeline. P-2 (#185) records the queue, and its fixture then grounds the names or changes them.
🤖 Generated with Claude Code
Summary by CodeRabbit
-
New Features
- Added pull-request timeline and review processing.
- Added validation for actor identities, permissions, review states, labels, timestamps, and timeline data.
- Added support for pagination, review dismissals, commit comparisons, writer activity, and machine-user approval tracking.
- Write access now depends on authenticated collaborator permissions.
-
Tests
- Expanded coverage for timeline processing, reviews, permissions, pagination, actor classification, and invalid responses.
- Added mutation testing coverage for key validation and attribution rules.
-
Documentation
- Updated implementation references and decision records to reflect the new timeline support.