Skip to content

Method call static analysis - #137

Merged
benh merged 136 commits into
mainfrom
caller-static-analysis-3
Sep 1, 2026
Merged

benh merged 136 commits into
mainfrom
caller-static-analysis-3

Conversation

@benh

@benh benh commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

For every method of a servicer, statically analyze it to determine what method calls it makes.

@benh
benh force-pushed the caller-static-analysis-3 branch 7 times, most recently from bf4cb0c to b731827 Compare August 20, 2026 03:17
@rileysdev
rileysdev force-pushed the riley/reboot-inspect branch from 0a7c6b1 to 2d5d1b6 Compare August 20, 2026 07:11
Comment thread reboot/dashboard/pyright.py Outdated
Comment thread reboot/dashboard/pyright.py Outdated
Comment thread reboot/dashboard/pyright.py Outdated
Comment thread reboot/dashboard/pyright.py Outdated
Comment thread reboot/dashboard/pyright.py Outdated
Comment thread reboot/dashboard/pyright.py Outdated
Comment thread reboot/dashboard/pyright.py Outdated
Comment thread reboot/dashboard/backend/pyright.py
Comment thread reboot/dashboard/pyright.py Outdated
Comment thread reboot/dashboard/pyright.py Outdated
@benh
benh force-pushed the caller-static-analysis-3 branch 3 times, most recently from 477ea26 to 515b226 Compare August 20, 2026 20:50
@rileysdev
rileysdev force-pushed the riley/reboot-inspect branch 2 times, most recently from 69596bb to 60d3d63 Compare August 21, 2026 01:16
@benh
benh force-pushed the caller-static-analysis-3 branch 2 times, most recently from 36daa6c to 06ab732 Compare August 22, 2026 19:22
@rileysdev
rileysdev force-pushed the riley/reboot-inspect branch from 60d3d63 to 8d42a32 Compare August 24, 2026 19:14
@benh
benh force-pushed the caller-static-analysis-3 branch 2 times, most recently from 17a9e99 to f4b6885 Compare August 24, 2026 23:08
@rileysdev
rileysdev force-pushed the riley/reboot-inspect branch 4 times, most recently from eedbe2f to c13f688 Compare August 25, 2026 00:48
@benh benh changed the title WIP Method call static analysis Aug 25, 2026
@benh
benh requested a review from rileysdev August 25, 2026 00:49
@benh benh self-assigned this Aug 25, 2026
@benh
benh marked this pull request as ready for review August 25, 2026 00:50
@github-actions

Copy link
Copy Markdown
Contributor

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

@aviator-app
aviator-app Bot force-pushed the riley/reboot-inspect branch from c13f688 to 7359802 Compare August 25, 2026 01:47
benh and others added 28 commits September 1, 2026 15:49
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
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
@aviator-app

aviator-app Bot commented Sep 1, 2026

Copy link
Copy Markdown

This pull request failed to merge: some required checks failed. After you have resolved the problem, you should remove the blocked pull request label from this PR and then try to re-queue the PR.

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
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.

3 participants