Orchestrata Ultimate › Plans › refactor-convergence-design
Refactor lanes convergence — LSP + interpreter functions and our AST (design)
Source: plans/refactor-convergence-design.md at 8a2cb42ad · last changed 2026-10-20 · first committed 2026-10-20 · 139 lines.
Prose pending.
Plan text
Refactor lanes convergence — LSP + interpreter functions and our AST (design)
Status: PROPOSED, partially ruled (ADR-0117, Mohamed 2026-10-20 "A": the SPEC-109 engine MOVES to codetools — P-A rescoped to MOVE-109, §6 decision 1 ratified. Decisions 2-3 and the codemod/ctok location remain open. RUL-022: no dispatch executes the open parts until ruled; MOVE-109 carries ADR-0117 as its contract.) Author: coordinator/architect seat, 2026-10-20. All numbers measured this session; §8 is the reproduction ledger. Origin: Mohamed, 2026-10-20, verbatim: "consider the functions of an LSP and the python interpreter for refactoring, can we extract or use such functions with our ast?" — approved scope "a + b": deciding measurements + this design, folded into the queued packets.
1. The short answer
The LSP and interpreter functions are ALREADY extracted — across five surfaces in this repo, two
of them real engines (SPEC-109's basedpyright JSON-RPC client, SPEC-110's transactional codemod
workspace), one of them the interpreter's own compiler (symtable, inside the toolkit rename).
The defect is not extraction; it is convergence and precision: three rename engines with no
shared discovery vocabulary, inferred types that are consumed and discarded instead of resolving
the KB's fail-closed cases, and two measured resolver-precision gaps (from-import asnames lost;
same-bare-name classes silently zero-matching). This design assigns lanes, names the packets that
close the gaps by REUSE, and lists what we deliberately do not build.
2. Measured inventory (2026-10-20)
| Surface | Spec | What it gives | Engine | Wiring status (measured) |
|---|---|---|---|---|
tools/lsp_tool.py |
SPEC-065 | definition / references / hover / document_symbols / diagnostics | hand-rolled ast.walk name-matching for the first four (hover's own docstring: "no live type inference (no jedi/pyright-langserver session)"); pyright CLI for diagnostics (__init__(command="pyright") :610) |
WIRED: agents/tool_registry.py:2092 registers LSPTool |
refactor/rename/ |
SPEC-109 | prepareRename/rename/definition/open_document over stdio JSON-RPC; hard no-name-matching timeout policy (REQ-109-103); scanner→plan→executor→applier, module_move, string_refs, residue, report |
REAL LSP: basedpyright-langserver (MIT, bundled Node); design ratified by live spike 2026-07-27 (plans/rename-engine-design.md) |
CLI seam SHIPPED (scripts/rename_engine.py, 10.3KB); volk-tool registration DECLARED-DEFERRED (design §5: "~6-line follow-up"); refactor.rename.tool has 0 production importers |
refactor/codemod/ |
SPEC-110 | transactional workspace: TransformPass family (incl. RenameSymbolPass), daemon/server/RPC client, AnalysisPass protocol, Bolt + in-memory persistence, review |
own CIG (CodeIntelligenceGraph) + ast overlay | CLI seam exists (scripts/codemod.py {load-bench,run,report,analyze}); codemod.daemon 0 importers; observability layer unwired (no CodemodEvent producer — backlog); AnalysisPass implementations ABSENT (REQ-110-031/032) |
scripts/serialize_codebase_ast.py rename/move-symbol/extract-class |
— | KB-native refactoring: token-precise, scope-aware via symtable (the interpreter's own compiler symbol table), type-scoped method rename, fail-closed on undeterminable receivers |
repo AST KB + symtable | WIRED: the coordinator's daily lane; ast_api.py exposes read-only ops over HTTP |
scripts/check_async_await.py |
— | async-correctness gate, "resolve-by-type via Pyright" | pyright as resolver library | WIRED (gate) |
code_intel.py / rts.py / KB |
— | beyond-LSP: affected-symbols transitive closure (tabled Prolog, resolves 7 import cycles), literals, callers/callees type-resolved, body_hash/residue_hash change visibility, freshness |
Prolog KB over the serialized snapshot | WIRED (RUL-028/029 doctrine; P-RUL040 rides it) |
cst_lsp (pipx, user-level) |
— | LibCST 1.8.5 + lsprotocol server (format-preserving CST) | external | NOT a repo dependency; codetools-adjacent; stays external |
Dependency graph already carries: pyright>=1.1.411, basedpyright>=1.39.9 (pyproject dev group);
pyright 1.1.411 + basedpyright + node all runnable in this worktree (measured).
3. Deciding measurements
- M1 — the KB-native typed rename refuses real work.
rename --old begin --new begin_probe --kind method --class SessionController(dry-run): resolves the DEF site only, reports 1 BLOCKED receiver (session/event_emitting_session_controller.py:78, undeterminable type), rewrites 0 call sites, refuses--apply(fail-closed, by design). The refusal class is exactly what pyright's inference resolves — SPEC-109 exists because of it (founding incident: the half-appliedgw→slotrename, 4 missed Redis slot parsers, 54 test failures). - M2 — from-import asnames are lost. Live probe (
from pathlib import Path as P): serialized as{"names":["Path"],"alias":null}; plainimport json as jkeeps"alias":"j". Every aliased from-import use is invisible to refs/rename resolution → systematic undercount. Confirms the standing backlog task with a fresh probe. - M3 — same-bare-name classes silently zero-match. Two
PrologBridgeclasses exist (cxp/prolog_bridge.py:122,infra/prolog_bridge.py:73).rename --kind method --class PrologBridgeanswered "Would change 0 edit(s)" — no ambiguity error, no disambiguation prompt. Silent-zero is the failure shape SPEC-109's whole design exists to make impossible ("a rename that silently under-applies is indistinguishable from a rename that succeeded"). - M4 — the engines are islands. Wired: lsp_tool (AST approximation), toolkit rename, code_intel. Unwired/deferred: SPEC-109 volk tool (declared), codemod daemon (CLI-only), codemod observability (no producer), AnalysisPass implementations (absent). No shared finding/evidence vocabulary (standing backlog item) — the three rename engines each carry their own refs discovery.
- M5 — SPEC-109 green-proof is owed, not skipped. 13 test modules exist
(
tests/refactor/rename/: transport, executor, applier, scanner, symbol_rename ×12, tool ×7, ...). The sanctioned-runner proof (run_tests.py tests/refactor/rename/*.py --skip-gates; bare multi-file pytest correctly refused by the RUL-038 cap guard — measured) runs the momentscripts/run_tests.pyleaves the T17 run-3 fence. Recording it as owed beats re-running an unrecorded green (RUL-032). - M6 — inference is available now. pyright 1.1.411 answers in-worktree; the repo already consumes it two ways (LSP server via SPEC-109 client; resolver via check_async_await).
4. Lane architecture (the convergence)
| Lane | Owner surface | Function | Rule |
|---|---|---|---|
| ANALYSIS — structure, closure, change-visibility | AST KB: serialize_codebase_ast.py + code_intel.py + rts.py hashes (+ symtable for scopes) |
refs/who-imports/affected-symbols/unused/literals/branches/freshness | the only lane that answers "what exists / what does this touch"; everything else consumes it |
| INFERENCE — types | Pyright/basedpyright, consumed as (a) LSP server through SPEC-109's lsp_client — moving to codetools per ADR-0117, consumed as the deployed CLI, (b) resolver as in check_async_await |
receiver-type resolution, hover types, definition-by-type | queried ON DEMAND at refactor time; NOT persisted into the KB by default (§6 decision 3) |
| EXECUTION — writes | SPEC-110 codemod txn = the single transactional multi-pass write path; SPEC-109 = the type-resolved rename PLANNER producing workspace edits; toolkit rename = KB-native fast path |
applying changes atomically, reviewably, revertibly | lane split for rename: name/func/class/module kinds → toolkit (KB-precise, symtable-scoped); method/type-scoped kinds → SPEC-109 (its founding class); a rename riding inside a larger codemod → RenameSymbolPass in the txn. All three consume ONE refs vocabulary (§6 decision 2) |
| GROUND TRUTH — runtime | interpreter side: BDD Phase 3 EXECUTED grade (runtime provenance, designed not built), sanctioned-runner green proofs, per-file coverage | "this code state actually executed" vs "should cover" | no refactor packet is done without a sanctioned green proof + affected-set verdict (RUL-029/032) |
5. Packets (fold into the queued lane — approval "b")
| id | Packet | Folds | Shape |
|---|---|---|---|
| P-A MOVE-109 (rescoped by ADR-0117, Mohamed ruling "A" 2026-10-20) | SPEC-109 engine + CLI + 13 test modules + spec bookkeeping MOVE to codetools (its refactor lane — measured: import-level primitives only, zero pyright/LSP there); decouple the 5 external orchestrata imports (traces/tracer/tools.base/lsp_tool-helpers/registry); rule the module_move overlap in-packet; deploy /opt/codetools/bin; orchestrata consumes the deployed CLI — contracts seam ONLY on a concrete volk consumer (named trigger); two-copy parity: the M5 sanctioned green proof runs BEFORE the move (its oracle), byte-faithful move, codetools green AFTER; orchestrata-side RTM/spec disposition records the move (REQ-109 never dangles). Absorbs the standing "persistent LSP session backend (065)" item — the moved engine IS the persistent-session machinery. codemod/ctok location: open, own ruling (ADR-0117 decision 6). |
backlog 065 item; M1's refusal class; ADR-0117 | cross-repo packet (orchestrata + codetools); contract = SPEC-109 design + ADR-0117 |
| P-B CIG-PASSES | Implement SPEC-110's absent AnalysisPasses (REQ-110-031/032) by DELEGATION to code_intel/KB: BlastRadius ← affected-symbols, DeadCode ← unused, Complexity ← complexity, Coupling/Cycles ← cycles+call-graph, BreakingChange ← refs+closure. No new analysis code; the pass = adapter + frozen dataclass per the protocol |
backlog SPEC-110 gaps item (REQ-110-031/032/033-035 split: 031/032 close here; 033-035 stay a spec-amendment decision) | worker packet; contract = analysis/protocol.py |
| P-C ASNAME | Capture from-import asnames in orchestrata_ast (standing task, M2 re-confirmed): names entry gains the alias; consumers (refs, rename lookup, sync) audited for the shape change | existing backlog task | worker packet; model-shape ripple = its design note |
| P-D DISAMBIG | Rename/symbols lookup on an ambiguous bare class name must ERROR with the candidate list (file:line each), never silent 0-edit (M3); same for code_intel callers qname misses |
new, from M3 | small worker packet, toolkit fence |
| P-E TYPEFACTS (deferred) | Persist pyright-inferred type facts into the KB so refs/rename resolve receivers without a live server. DEFERRED behind §6 decision 3: staleness cost (every edit invalidates types) vs on-demand query; decide from P-A's measured refusal-residual, not now |
— | decision gate, not a packet |
Sequencing: P-C + P-D are precision prerequisites (small); P-A is the headline (turns measured refusals into completed renames); P-B closes a spec gap by reuse. None blocks the current queue (P-RUL039/040, build-order packets, INT-02+) — they slot into the debts/hardening lane.
6. Decisions owed to Mohamed (one recommendation each)
- Lane split vs single engine. Keep three rename surfaces with the §4 lane rule, or converge on SPEC-109 as the only rename engine (toolkit rename delegates everything)? RULED 2026-10-20 (ADR-0117): lane split stands — and the SPEC-109 lane lives in codetools (engine moves; toolkit KB-native rename stays; codemod txn stays pending its own location ruling).
- Shared refs vocabulary. The three engines each carry refs discovery; the standing "no shared finding/evidence vocabulary in core/" item is the same defect. Fold a common discovery/evidence protocol into that item's resolution, with ANALYSIS (KB) as the single source? Recommendation: yes, as the vocabulary item's design input — no third parallel model (Phase B unification of ast_models is the precedent).
- Type-fact persistence (P-E). Persist inferred types into the KB, or keep inference on-demand? Recommendation: defer — measure the refusal residual after P-A lands; persist only if on-demand latency or offline demand proves real. Stale type facts in a KB whose freshness discipline is file-hash-based would be a new lie class.
7. Explicitly NOT doing
- No new refactoring engine, no jedi (basedpyright satisfies the open-source ruling; two consumption modes already exist in-tree).
- No cst_lsp/LibCST dependency in this repo (external codetools surface; format-preservation needs, if they arise, are a codetools lane question).
- No rebuild of SPEC-110 analysis (P-B delegates) and no wiring of the codemod observability producer inside these packets (that stays its own backlog item — SPEC-110 §15 feature, RUL-022 design-first).
- No volk-facing rename exposure beyond P-A's registered tool: high-blast-radius ops keep the coordinator/human review seam the founder incident demands.
8. Evidence ledger (reproduce)
# M1 refusal
.venv/bin/python scripts/serialize_codebase_ast.py rename --old begin --new begin_probe \
--kind method --class SessionController --source src/orchestrata_ultimate # dry-run
# M2 asname probe: /tmp/opencode/asname-probe/probe_mod.py + serialize + jq .imports
# M3 ambiguity
.venv/bin/python scripts/serialize_codebase_ast.py symbols --pattern "^PrologBridge$" --kind class
.venv/bin/python scripts/serialize_codebase_ast.py rename --old assertz --new assertz_probe \
--kind method --class PrologBridge --source src/orchestrata_ultimate # "0 edit(s)", silent
# M4 wiring: who-imports tools.lsp_tool | refactor.codemod.{server,daemon} | refactor.rename.tool
# M5: pytest-live tests/refactor/rename/ → RUL-038 guard names the sanctioned 13-file invocation
# M6: .venv/bin/pyright --version → 1.1.411; pyproject dev group
Cross-references: plans/rename-engine-design.md (SPEC-109, ratified+spiked), specs/109-rename-engine/,
specs/110-codemod-engine/, specs/065-lsp-tool/, backlog items: asname capture, shared
finding/evidence vocabulary, SPEC-110 gaps, tool-hardening batch (065), Neo4jSyncer name-index
collision (M3's graph-side twin).