Method call static analysis - #137
Merged
Merged
Conversation
benh
force-pushed
the
caller-static-analysis-3
branch
7 times, most recently
from
August 20, 2026 03:17
bf4cb0c to
b731827
Compare
rileysdev
force-pushed
the
riley/reboot-inspect
branch
from
August 20, 2026 07:11
0a7c6b1 to
2d5d1b6
Compare
benh
commented
Aug 20, 2026
benh
force-pushed
the
caller-static-analysis-3
branch
3 times, most recently
from
August 20, 2026 20:50
477ea26 to
515b226
Compare
rileysdev
force-pushed
the
riley/reboot-inspect
branch
2 times, most recently
from
August 21, 2026 01:16
69596bb to
60d3d63
Compare
benh
force-pushed
the
caller-static-analysis-3
branch
2 times, most recently
from
August 22, 2026 19:22
36daa6c to
06ab732
Compare
rileysdev
force-pushed
the
riley/reboot-inspect
branch
from
August 24, 2026 19:14
60d3d63 to
8d42a32
Compare
benh
force-pushed
the
caller-static-analysis-3
branch
2 times, most recently
from
August 24, 2026 23:08
17a9e99 to
f4b6885
Compare
rileysdev
force-pushed
the
riley/reboot-inspect
branch
4 times, most recently
from
August 25, 2026 00:48
eedbe2f to
c13f688
Compare
benh
marked this pull request as ready for review
August 25, 2026 00:50
Contributor
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. |
rileysdev
approved these changes
Aug 25, 2026
aviator-app
Bot
force-pushed
the
riley/reboot-inspect
branch
from
August 25, 2026 01:47
c13f688 to
7359802
Compare
The public entry: `api_of(api, filename=...)` returns the `rbt.v1alpha1.pydantic.API` for one file. A test reads the dashboard's shop fixture and checks each part of the grammar against the file: package and module, the state type and its description, each method's kind, factory, request, response, errors, description and MCP tool, the data types, and that every model has a schema. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
`proto_text_from_api` takes an `rbt.v1alpha1.pydantic.API` and only prints: the package and module come off it, each state type's options, UIs and methods off its message, every model's message off `schemas` by reference name, and a method's kind off the arm of its `kind`. `generate_proto_file_from_api` reads the file with `api_of` first. So `rbt generate` and the dashboard now read a file the same way, and the generated proto is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
`API.filename` is the file relative to the API directory, e.g. `shop/v1/shop.py`: what `package` and `module` are derived from, and the key `API.files` and `API.generated` use for the same file. The path as the developer would open it, the API directory as they spelled it plus this, is the watcher's to add where it is shown. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
…per file The dashboard's `API` state now records `apis`, a map from each file, relative to the API directory, to what that file declares, as `api_of` read it, plus the `api_directory` itself. The dashboard's own `StateType`, `Method`, `DataType` and `Declarations` messages are gone: the reader returns the grammar's `API` (or `None` for a file declaring no `api`), the watcher keeps one per file, and the changelog diffs maps of them. Every type's id is the name the rest of the system gives it: a state type is named as the runtime names it, its package then its name, e.g. `shop.v1.Shop`, and a data type by its reference name, its module then its name, e.g. `shop.v1.shop.Item`. The page still groups types by package. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
`api_digest` is the SHA-256 of `rbt.v1alpha1.pydantic.API` serialized deterministically: `rbt generate` records it in each module it writes, and the dashboard's reader computes it from the same `api_of` read it describes the file with, so neither side prints proto to compare. `API.generated` maps each module `rbt generate` would write, keyed the way `Implementation.generated` is (`shop/v1/shop_rbt.py`), to that digest; only files declaring an `api` have an entry, which is what the map's domain says, and `File` keeps describing every file. With the same key on both sides the graph page compares digests: equal means the module came from the file as it is, unequal means the file `changed` since; only a module recording no digest, generated before digests were, falls back to comparing modification times. A module generated before this carries a digest of the proto text instead and so reads as `changed` until regenerated, which is true. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
The watcher recorded its state through a workflow `write`; an `Update` transaction on `ImplementationMethods` now records it, the way `APIMethods.Update` records what the API files declare. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
A servicer added or removed is recorded in the same changelog the API watcher writes, and so is each method whose implementation does something different: its body, by the digest of its syntax, or only the Reboot calls it reaches, when a function it calls into changed. Recorded in the `Update` transaction beside the state, so the changelog never says something the state does not. The API changelog is what records a method appearing or disappearing; the code's methods are read off the state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
`_walk_and_analyze` is what one iteration reads off the disk, memoized with `at_least_once`, so a workflow restarted mid-iteration records the same `Update` it was recording, which is what the idempotency of the write needs. The memoization pickles the result, which cannot serialize a `MappingProxyType`, so each analyzed file's mappings are returned plain. And a restart no longer waits for a save: the first iteration of a restarted watch analyzes the application at once, so a change made while the dashboard was down is recorded, the way the API watcher records one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
The Python moves to `reboot/dashboard/backend` and the page to `reboot/dashboard/web`, the way an application splits into `backend` and a directory for its page. The proto stays under `rbt/dashboard/v1`, where every framework proto lives, since the generated code must be importable by its package. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
The state records everything the dashboard shows, not only what the API files declare, so it is named for the dashboard. Everything spelled from the state's name follows: `DashboardMethods`, `DashboardGetResponse`, `DashboardServicer`, `DASHBOARD_ID`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
The `Implementation` state folds into `Dashboard`: one state records everything the dashboard shows, with `api_files` and `code_files` telling the two walks' files apart. Two workflows keep it current, `WatchApi` reading the developer's API files and `WatchCode` analyzing their application, each recording through its own transaction, `UpdateApi` and `UpdateCode`, so neither write touches the other's fields. One `Get` serves the page, which drops its second subscription. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
The join of what each API file declares against what its generated module records moves off the page and into the backend, which holds both sides on the one `Dashboard` state: `Get` derives a `ReasonToGenerate` per read, so every update re-serves a fresh verdict and nothing derived is stored. The page consumes the enum and drops its copy of the join. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
The nav's counts were reported upward by each page as it rendered, so a count read zero until its page was opened once. `Overview` now derives the call count from the joined state types it already holds, through `drawnCallCount`, and reads the changelog itself, passing the page what to show; each page stops reporting through `onCount`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
The badge marks a factory method, and `factory` is the word the rest of the dashboard and the API use for it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
The same `favicon.svg` an application's own home page shows, copied from the root page's assets and served beside the dashboard's page. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
Each method's header is a toggle with its own caret, sliding just that method's detail open or closed, and the `methods` eyebrow toggles every method at once, replacing the card's `Expand details` button. What is open is one preference: `expanded_methods`, the set of open methods, written through `SetMethodsExpanded` whether one method was toggled or all of them. The eyebrow's caret is open only when every method is, so closing any single method closes it and clicking it then opens everything again. The closed list's aligned columns and the pill slide follow the per-method state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
The pane, not the window, is what scrolls, so the browser's own scroll restoration never applied to it: a back landed wherever the freshly rendered page put it. The pane's scroll is now remembered for each history entry as it is left and restored on back or forward, only once the entry's content is on screen, so the offset is applied against the heights the developer was looking at. The effect that scrolls to the element the URL names fires only when the developer navigated to it, or on the page load itself; and the first paint waits for the preferences, so nothing opens after the offset is applied. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
The page said `field` for what `rbt.v1alpha1.pydantic.Schema` declares as a `Property`, so the reader met two words for one thing. The page, its types and its file names now say property. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
A method the graph knows only from a call reads `unknown`, in italics in the legend, and its dot is the open ring: nothing declares it, so there is nothing to paint it in with. A workflow's dot is painted in like every declared kind's; its dashed edges are what say it runs past the call that started it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
A name wider than the sidebar pushed the whole grid past the panel's edge, hiding every count behind a scroll nothing pointed at. The name column now shrinks, ellipsizing the name, and the row's `title` carries the full name; the counts stay at the right edge whatever the names are. Dragging the sidebar wider still shows more of the names. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
Opening a method answers what it takes and returns before the author's prose about it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
Clicking the name, underlined on hover, opens that method's detail on the state page: the method is expanded through the preferences and the URL names it, so the page scrolls to it. Each method's row carries the id the URL names, scoped by its state type. And the graph's view is remembered for each history entry, its viewport, chosen method and collapsed boxes, so coming back lands on the graph exactly as it was left. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
The chosen method's id moves into the URL, `/graph/<method id>`: each choice pushes a history entry, so clicking from one method to another and pressing back steps to the one chosen before, and a graph URL with a method deep-links to it chosen. The viewport does not move on these steps, since the page stays mounted; only leaving the graph and coming back restores it from the remembered view. A choice let go by closing its box replaces the entry rather than pushing one, since the reader clicked the box, not the choice. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
The grammar of what an API file declares was packaged as `rbt.v1alpha1.pydantic`, which stops being true the day a `.proto` reader produces it too. It moves to `rbt.v1alpha1.api`, its own package because the base `rbt.v1alpha1` already declares `Transaction` and `UI`, which the grammar also names. Serialized bytes carry field numbers, not names, so recorded state survives the rename. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
The changelog's entries are immutable history, and a data type's package was derived from its name's spelling, which depends on what declared the type: a Pydantic model's name carries its module, a proto message's its package. Each data type entry now records the package it was written with, so entries stay readable whatever spelling later declarers use. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
… tests The old test_coordinator_down_participant_prepared_still_aborts raced the coordinator's durable write of its participants against server_stop: if the write won, recovery correctly re-prepared and committed the transaction and the test's abort assertion failed. Pin each side of that race as its own test: - test_coordinator_down_before_recording_participants_aborts blocks the coordinator's DatabaseClient.transaction_coordinator_prepare so nothing persists before the stop; recovery knows nothing of the transaction, the prepared participant aborts, and the account can be signed up fresh. - test_coordinator_down_after_recording_participants_commits waits for that write, then fails DatabaseClient.transaction_coordinator_cleanup while the server stops, simulating a crash rather than a clean abort (the dying coordinator's cleanup can still run after server_stop returns, and the failing cleanup also keeps the abort from reaching the participant); recovery re-prepares the recorded participants, every one is durably prepared, and the transaction commits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
|
This pull request failed to merge: some required checks failed. After you have resolved the problem, you should remove the Failed checks: Build public repo for Linux x86_64 / GitHub-hosted Runner |
The coordinator's durable record of its participants was written unmocked, and the test only passed because stopping the server is a graceful shutdown whose abort deletes that record; that cleanup runs as a detached task that can land after server_start has begun, racing recovery. Had recovery won, it would have re-prepared, and the mock's only-refuse-the-first-time branch would have really prepared the participant, committing the transaction and failing the final SignUp with StateAlreadyConstructed (reproduced by pinning the record: the test then failed exactly that way). Pin both sides. Fail DatabaseClient.transaction_coordinator_cleanup while the server stops, waiting for the dying coordinator's one failed attempt, so the record provably survives until recovery reads it. And refuse prepare by transaction id rather than only on the first call, so the recovery re-prepare gets the participant's definitive refusal again and the transaction aborts, while later transactions prepare normally. Also reword the docstring, which claimed the coordinator keeps nothing durable before the prepare phase completes; it durably records its participants, and recovery re-prepares from that record. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
For every method of a servicer, statically analyze it to determine what method calls it makes.