Skip to content

refactor(flow): runtime.core behind an import contract, first action executors (PR 2) - #1535

Draft
abrichr wants to merge 9 commits into
mainfrom
refactor/runtime-core
Draft

abrichr wants to merge 9 commits into
mainfrom
refactor/runtime-core

Conversation

@abrichr

@abrichr abrichr commented Oct 25, 2026 •

Copy link
Copy Markdown
Member

Summary

PR 2 of the decomposition plan (packages/flow/docs/design/DECOMPOSITION.md, section 4.3): the replayer's step pipeline and strategies. Draft; it grows by chunk. replayer.py is down from 18,300 to 10,926 lines, with behavior unchanged.

1. runtime.core, behind import contracts

  • runtime/core/actions.py: ActionContext carries the step, the backend, and two gates.
    • authorize() runs delivery authorization, then the qualification-environment check, in the replayer's original order.
    • deliver() is the replayer's own delivery bookkeeping.
    • KEY, HOTKEY and WAIT now run here.
  • runtime/core/render.py: render scale and layout drift. Moved; its host reads no governance state.
  • packages/flow/.importlinter, checked by lint-imports in flow core lint:
    • Direct contract: core imports no runner, hosted, approval_mode, jit, replayer or qualification* module. Kept with no exceptions.
    • Indirect contract: the same through every import chain, except 45 listed edges. A core module that imports only ir, backend and the resolver reaches 249 of 562 modules today (section 2.4).
  • tests/test_import_boundaries.py keeps the forbidden list complete and proves every ActionKind has exactly one executor.

2. Move-only extraction into runtime/replay/ (mixins)

  • _shared.py: the 49 module-level constants, helpers and small types. replayer.py re-exports them, so imports such as _DURABLE_RESUME_AUTHORITY and _ProgramHalt get the same objects.
  • One mixin module per concern: qualification faults, consequential delivery, effect checks and read-back, the API tier, acting by kind, settling, confirming, surfaces and overlay, assets and healing.
  • Each module comes from scripts/extract_mixin.py. Method bodies are cut verbatim. Each mixin gets a host class that declares, for mypy only, what the code reads from self, using the replayer's own types.
  • A plain class, not a Protocol, and why, is in DECOMPOSITION.md section 4.1.
  • scripts/verify_moved_code.py (from test: safety net for decomposing the largest files (PR 1) #1510) checks every move commit: 185 units moved, no edits, no new logic.

Not moved yet, because open PRs change them

Test changes

Two tests patched module globals that moved code now reads from its new module. They now patch them there as well:

  • resolve, in replay.acting and replay.settling;
  • _DIALOG_WAIT_S, in replay.acting.

The verifier now flags that case (#1510).

Checks (local, at most 2 workers)

  • tests/test_core_key_actions.py, committed before the first move: real Playwright; key event and submit, HOTKEY modifiers, refusal of a key step with no key, WAIT sends nothing.
  • 3,149 tests across the 146 files that import the replayer, plus test_record_compile_replay, test_perturbation and test_replayer (144).
    • One environment-only failure on macOS: test_layout_drift.py::test_real_render[font_size_changed] fails identically on unmodified code (font rendering).
  • Strict mypy on runtime/core, runtime/replay and replayer.py; package-wide mypy (575 files); both import contracts; ruff.
  • Golden runs (Linux) come with test: safety net for decomposing the largest files (PR 1) #1510. CI checks this PR against them once both are on main.

🤖 Generated with Claude Code

abrichr and others added 9 commits October 24, 2026 23:26
Pins today's behavior before these executors move into runtime.core:
the key event and form submit, the modifier string, the refusal of a key
step with no key, and a WAIT that sends no input.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…an import contract

runtime.core carries out steps through a backend; governance reaches it
only as gates. KEY, HOTKEY and WAIT now run there: ActionContext.authorize
runs delivery authorization and then the qualification-environment check
(Replayer._authorize_delivery), exactly the order the replayer used, and
deliver is the replayer's own delivery bookkeeping. Behavior is unchanged:
the characterization tests from the previous commit and the keyboard,
guarded-keyboard and refusal suites pass.

packages/flow/.importlinter, run by lint-imports in flow core lint:
- runtime.core imports no runner, hosted, approval_mode, jit, replayer or
  qualification* module (kept with no exceptions);
- the same through every import chain, except 45 listed edges by which
  the schema, effects, policy, repair, fleet and durable modules reach
  governance today. New governance imports in the core's dependencies fail.
tests/test_import_boundaries.py keeps the forbidden list complete and
proves every ActionKind has exactly one executor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Section 2.4 measures the boundary (a core module reaches 249 of 562
modules through 45 governance edges). Section 10 inventories duplicate
implementations (strength scales, outcomes, admissions, certify paths,
canonical JSON, runtimes) with the canonical one and the PR for each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…(move only)

Seven Replayer methods move verbatim into runtime/core/render.py as the
RenderScaleAndLayoutDrift mixin; scripts/verify_moved_code.py finds 7
moved units and no new logic. scripts/extract_mixin.py, added here, does
such moves: it cuts methods verbatim, imports what they use from where
the replayer got it, and writes a host class that declares for mypy only
what the code reads from self, with the replayer's own annotations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…me/replay (move only)

SurfacesAndOverlay (10 methods) and AssetsHealingAndEvidence (3) move
verbatim; verify_moved_code finds 13 moved units and no new logic. They
read governed-run state, so they go to runtime/replay, not runtime.core.
The extractor now copies only signature decorators onto host stubs and
ignores docstrings when it looks for referenced names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s stay put

Records why a mixin's host is a plain class (protocol attributes without a
value are abstract and get re-inferred; implicitly abstract protocol stubs
can shadow real methods), the rule for what goes into runtime.core, and two
invariants: resolver.py and program_predicates.py source bytes are part of
the predicate evaluator contract, and the CLI split keeps command identities.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…/replay/_shared (move only)

49 constants, helpers and small types move verbatim, adjacent definitions
as one block so shared comments stay with them; replayer.py re-exports
every name, so imports such as _DURABLE_RESUME_AUTHORITY, secret_env_var
and _ProgramHalt keep working (same objects). verify_moved_code: 58 units.
The extractor no longer adds module imports for names a moved body imports
itself: ruff would otherwise delete the body's own import.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e only)

Qualification fault injection, consequential delivery, effect checks and
read-back, the API tier, acting by kind, settling and confirming move
verbatim into mixins (107 units; verify_moved_code finds no edits).
replayer.py goes from 18,300 to 10,926 lines.

Left in place because open pull requests change them: identity checks
(#1518, #1504), _resolve_step (#1504), postconditions and step evidence
(#1516, #1514), the starting-state settle and qualification boundary
(#1449), and the run and step drivers. Two methods that name Replayer
stay too.

Two tests patched module globals the moved code now reads from its new
module, and patch them there as well: resolve (scroll readiness is in
replay.acting, interstitials in replay.settling) and _DIALOG_WAIT_S
(replay.acting).

Checked: strict mypy on runtime/core, runtime/replay and replayer.py;
package-wide mypy; both import contracts; 3,149 tests across the 146
files that import the replayer plus the real-browser e2e files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…replay

The moved replayer code keeps the strict type check and safety-path review
it had in replayer.py.

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

This branch has not been deployed

No deployments
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