Orchestrata Ultimate / Specs / 110-codemod-engine

110-codemod-engine: In-Memory Code Intelligence Platform

SPEC-110 defines a complete in-memory code intelligence platform that loads an entire Python codebase as a lossless Concrete Syntax Tree (CST) forest, builds a multihop-queryable Code Intelligence Graph (CIG), executes composable transform and review passes entirely in RAM, and commits changes atomically with full rollback capability. The platform observes all operations through an event-driven architecture, persists provenance to a graph database via the repository pattern, and...

At a glance

Item Value
Spec directory specs/110-codemod-engine/
Status line in the spec Complete
Requirement IDs defined 59
With a @traces citation in src/ 43 (72.9%)
With a @traces citation in tests/ 28 (47.5%)
Tracked files 20
RTM page 110-codemod-engine

Reading this spec

This spec defines a complete in-memory code intelligence platform for Python. It loads a whole codebase as a lossless Concrete Syntax Tree (CST) forest, builds a Code Intelligence Graph (CIG) that supports multihop queries, runs composable transform and review passes entirely in RAM, and commits changes atomically with full rollback. The spec says it is more than a codemod engine: every analysis, review, graph query and transform works on the same in-memory representation, and nothing touches disk until the final commit. The reason is the gw to slot incident of spec 109, where a text-based, file-by-file rename missed four Redis slot parsers and caused 54 test failures and more than a day of recovery. The project runs on free-threaded Python 3.14, where a measured 2.66x parallel speedup on 4 threads makes parallel parsing and per-file passes worthwhile.

Work happens in three phases. Load and index parses every .py file into a LibCST module with byte-perfect round trip, builds the CIG (modules, classes, functions, methods, parameters, tests and imports, linked by imports, calls, inheritance, definition, tested-by and override edges) and builds compressed sparse row adjacency arrays. Transform and review runs passes over the forest, then runs review passes over the mutated forest so verdicts come before bytes, emits events to observers, and aborts the commit on any error-severity finding. Commit snapshots the pre-commit bytes, writes atomically through temporary files and rename, batches directory fsyncs and re-parses to verify.

The user stories (US-110-001 onward) include a codebase-wide symbol rename, a vocabulary migration over 500 or more files, breaking-change detection, a 40-check in-memory review, composing transforms in one atomic commit, persisting provenance to the graph database, and a warm daemon. The 59 requirement IDs (REQ-110-NNN) cover loading and incremental index updates, the rule that a pass never writes to disk and write-back happens only through workspace.commit(), dry-run commits that produce a diff and review report (the CLI defaults to dry-run), five built-in observers, analysis passes for blast radius, breaking changes and complexity, the daemon with its JSON-RPC query methods, byte-splice edits that keep every byte outside an edit span identical, and quality gates with at least 90% coverage per file.

The implementation spans the codemod package, with the most citations in the workspace (17 requirements), followed by the transaction layer and the daemon. The requirements traceability matrix lists each ID. The spec's own status line reads Complete and it tracks 20 files. Measured coverage is 43 of 59 requirements cited by at least one @traces in src/ (72.9%), spread over 36 modules; the module citing the most of them is orchestrata_ultimate/refactor/codemod/workspace (17 requirements). In tests/, 28 requirements are cited by a test-side @traces, thinner than the code side (28 against 43). 16 requirements have no citing symbol; the behaviour may still exist in code that was never annotated.

Requirements

Checkbox is the state recorded in the requirements checklist; Traced by is the count of src/ symbols whose @traces names the ID.

ID Done Requirement Traced by
REQ-110-001 yes The CodemodWorkspace.load(root, include, parallel) method SHALL parse every .py file under the configured include roots into a libcst.Module tree, keyed by repo-relative path. 3
REQ-110-002 yes For every loaded module, module.code.encode('utf-8') SHALL equal the on-disk bytes at load time (lossless round-trip fidelity). A module that fails to parse SHALL be recorded in workspace.unparsable as a (path, error_message) tuple and excluded from the forest. 3
REQ-110-003 yes Loading the full orchestrata-ultimate tree (~2,500 files across src/scripts/tests, ~780K LOC) with parallel=True SHALL use a ThreadPoolExecutor with max_workers=os.cpu_count() and complete in <120 seconds on 12 cores. The original "<360 seconds single-threaded" clause is RETIRED (never a user mandate; production always loads parallel=True). Serial performance is informational only. 2
REQ-110-004 yes The workspace.stats() method SHALL report WorkspaceStats with file count, total bytes, RSS delta (via psutil.Process().memory_info().rss), parse time, and unparsable count. RSS delta SHALL remain <3.5 GiB for the full codebase (2,500 files, 780K LOC, 30 MB on disk), measured in parallel=True mode. 3
REQ-110-005 yes The workspace SHALL expose modules() (bidirectional mapping of dotted names ↔ paths), dependencies(module) (modules this module imports), and dependents(module) (modules that import this module), all derived from the CST forest's import statements. none
REQ-110-006 no The index SHALL update incrementally when a pass mutates imports. The _update_index(paths) method SHALL re-derive dependencies and dependents for the specified modules only, not the entire forest. NOT IMPLEMENTED (measured 2026-10-06 by the S1 round-1 review, flipped on that measurement): refs _update_index and symbols --pattern update_index return nothing, and after AddImportPass rewrote a file the workspace dependency index did not change (reviewer probe probe_index.py); run() replaces the forest entry only. Previously marked [x] in error. none
REQ-110-007 yes The symbol_table() method SHALL map top-level defined names (classes, functions, module-level variables) per module, sufficient for a pass to resolve "who defines X" without disk access. none
REQ-110-008 yes A transform pass SHALL be a class implementing TransformPass.apply(workspace) -> ChangeSet. The ChangeSet SHALL record, per module: the mutated CST tree, a human-readable description, and the pass name. 1
REQ-110-009 yes Passes SHALL compose: workspace.run(passes: list[TransformPass]) SHALL apply them in order over the same forest. Pass N SHALL see pass N-1's mutations. The composed ChangeSet union SHALL be what write-back serializes — one transaction for N passes. 1
REQ-110-010 yes The engine SHALL ship 5 built-in transform passes: 10
REQ-110-011 yes A transform pass SHALL NOT write to disk. The API SHALL give passes no file handles. Write-back SHALL be exclusively workspace.commit(). This SHALL be enforced by construction (no open(), Path.write_text(), or os.rename in pass code) and asserted by a test that greps the pass base-class surface. 1
REQ-110-012 yes A review pass SHALL be a class implementing ReviewPass.review(workspace, changes) -> list[Finding]. Review passes SHALL operate on the mutated forest (the ChangeSet entries' mutated_tree), not on diffs or disk files. 4
REQ-110-013 yes The workspace.commit(reviews=[...]) method SHALL run reviews FIRST. Any Finding with severity "error" SHALL abort the commit with the findings report — nothing is written to disk. This is the in-RAM analogue of SPEC-109's residue abort. 1
REQ-110-014 yes The commit() method SHALL write only mutated modules. Each file SHALL be written via: (1) write to tempfile in same directory, (2) os.fsync(tempfile_fd), (3) os.rename(tempfile_path, target_path). After ALL writes, every written file SHALL be re-parsed from disk (libcst.parse_module(written_bytes)). Any failure SHALL trigger rollback of every written file to its pre-commit bytes (kept in ChangeEntry.original_bytes). 9
REQ-110-015 yes The commit() method SHALL refuse to run if any target file's on-disk mtime or content hash (xxhash3_64) changed since load() (concurrent-edit guard). The refusal SHALL list the stale paths. The guard SHALL use O_NOFOLLOW when opening files to prevent symlink attacks, and fcntl.flock(LOCK_EX | LOCK_NB) for advisory locking during the write window. 7
REQ-110-016 yes A commit(dry_run=True) SHALL produce the full unified diff and the review report without writing. The CLI SHALL default to dry-run (house convention). 2
REQ-110-017 yes Every commit SHALL emit a machine-readable CommitReport (JSON): passes run, files written, diffstat, review findings, duration, correlation_id. The report path SHALL be printed on completion. 1
REQ-110-018 yes The scripts/codemod.py CLI SHALL provide subcommands: none
REQ-110-019 yes Exit codes SHALL be: 0 success/clean-dry-run, 1 review-abort or verification failure, 2 usage error. These SHALL be asserted by CLI subprocess tests. none
REQ-110-020 yes RenameSymbolPass SHALL consume WorkspaceEditPlan from orchestrata_ultimate.refactor.rename.models rather than reimplementing resolution. The engine SHALL add NO second rename resolver. 3
REQ-110-021 yes Write-back SHALL reuse the applier's safety invariants. If code reuse of EditApplier is impractical (CST writes whole modules, not spans), the INVARIANTS (atomicity, post-parse, rollback, path-escape guard) SHALL be reimplemented against whole-module writes and cross-tested with the same scenarios as tests/refactor/rename/test_applier.py. 6
REQ-110-022 yes The CodeIntelligenceGraph SHALL be built from the CST forest by _cig_builder.py. It SHALL contain nodes for Module, Class, Function, Method, Parameter, Test, Import, and edges for IMPORTS, CALLS, INHERITS, DEFINES, TESTED_BY, OVERRIDES. (Shipped as _cig_builder_ast.py over the ctok/AST LosslessModule forest — spec.md's "CST forest"/_cig_builder.py wording predates the ctok migration; see REQ-110-125/126.) 1
REQ-110-023 yes The CIG SHALL support multihop queries: 15
REQ-110-024 yes All CIG queries SHALL execute in <2ms for the target codebase (2,500 files, ~15,000 edges) via CSR adjacency arrays. 1
REQ-110-025 yes The workspace SHALL compose a SyncObservable[CodemodEvent] and emit events for all operations: workspace loaded, index built, pass started/completed, file mutated, review started/completed, finding emitted, commit started/completed/aborted, rollback triggered, error detected. COMPLETE — CodemodWorkspace owns one SyncObservable[CodemodEvent]; CodemodEventType has the 13 kinds; each is emitted at its lifecycle boundary with one correlation_id per operation. Proven by tests/refactor/codemod/test_observability_wiring.py. 7
REQ-110-026 yes The engine SHALL ship 5 built-in observers: 6
REQ-110-027 yes All observers SHALL be optional and configurable. The workspace.run() and workspace.commit() methods SHALL accept an observers parameter to override defaults. COMPLETE — run(), commit() and load() accept observers; the default set is empty; a per-call sequence is subscribed for the duration of the call only. Semantics: the per-call observers= list overrides the DEFAULT set (empty by design); workspace-level subscribe() registrations are explicit and additive, not defaults, and an observer reachable through both receives each event exactly once (deduplicated by identity). Production constructor for the built-ins: scripts/codemod.py run --observe attaches default_observers() (the five REQ-110-026 observers) to that run's run() and commit(); without the flag the default stays empty. Known limitation (S1 review round 2, M4): the identity dedupe lives in _Operation.__init__, but the core _SubscriptionRegistry.subscribe (core/patterns/observer.py, out of S1 scope) dedupes by equality (not in), so an observer that defines __eq__ (for example a dataclass) and compares equal to one already subscribed is silently dropped; all five built-ins use identity equality and are unaffected. Owed to the owner of core/patterns/observer.py: an identity check in the registry. Proven by tests/refactor/codemod/test_observability_wiring.py and tests/scripts/test_codemod_cli.py. 5
REQ-110-028 yes The CodemodGraphRepository Protocol SHALL define methods: save_run(), save_pass(), save_findings(), save_changes(), link_changes_to_code(), query_runs(), query_findings(), query_affected_code(). 2
REQ-110-029 yes The BoltSyncCodemodRepository SHALL implement the Protocol using SyncGraphRepository.unit_of_work() for transactional writes, writing CodemodRun, CodemodPass, CodemodFinding, CodeChange nodes with edges HAS_CODEMOD_RUN, HAS_PASS, HAS_FINDING, ABOUT, PRODUCED_BY, BREAKING_CHANGE, CHANGED_IN. 6
REQ-110-030 yes The ontology SHALL extend canonical_schema.py with new labels (CodemodRun, CodemodPass, CodemodFinding, CodeChange) and relationships. The CodeChange label SHALL reuse the existing Change label as a sub-label. none
REQ-110-031 yes The engine SHALL ship analysis passes that compute code intelligence metrics from the CIG: BlastRadiusAnalyzer, BreakingChangeDetector, ComplexityAnalyzer, TestCoverageMapper, AbstractionAnalyzer, CouplingAnalyzer, DeadCodeDetector. COMPLETE — one submodule each under refactor/codemod/analysis/, thin adapters over CodeIntelligenceGraph (the ast overlay for complexity and the signature diff), surfaced by scripts/codemod.py analyze --query breaking_changes|complexity|test_coverage|dead_code|all-passes and by blast_radius/coupling_score/abstraction_level through the analyzers. Reduced results where the graph cannot answer (module-granularity dead code, GAP-1/2/4/5) are documented in docs/codemod-analysis.md; the owed graph methods are backlog in plans/spec-110-completion-plan.md (S5 findings). Proof: tests/refactor/codemod/analysis/ (TST-110-015..018), tests/scripts/test_codemod_cli.py (TST-110-020). 42
REQ-110-032 yes Analysis passes SHALL be read-only (no mutations) and SHALL return structured results (dataclasses) suitable for CLI output and graph persistence. COMPLETE — every result is a frozen dataclass (FunctionComplexity, ComplexityReport, TestCoverageMap, DeadCodeReport added to _types.py; BlastRadius, BreakingChange, CouplingScore, AbstractionLevel reused) and AnalysisResultEncoder.encode makes each JSON-serialisable; TST-110-019 proves the CIG node/edge counts and every forest module's bytes are identical before and after all seven analyzers run. 29
REQ-110-033 yes The InMemoryReviewEngine SHALL execute 40 checks on the mutated CST forest, organized into 7 categories: Complexity (4), Style (6), Quality (4), Structure (6), Safety (10), Correctness (6), Test Isolation (4). COMPLETE (default baseline delta, ADR-0073) — InMemoryReviewEngine (refactor/codemod/review_engine.py) runs the 40-check catalogue (review_catalogue.py: Complexity 4, Style 6, Quality 4, Structure 6, Safety 10, Correctness 6, Test Isolation 4; 8 shared-implementation groups, so 30 distinct implementations) over the mutated forest, and CodemodWorkspace.commit() runs it first, before any write-back, so an error finding aborts with the disk untouched (ERR-110-002). The existing four passes (ParseIntegrity, BannedConstructs, DiffBudget, PrologSemanticReview) stay. Documented in docs/codemod-review-engine.md (alias reporting, check scope, the unspecified cyclomatic max and TI_GLOBAL_STATE rule). Proof: tests/refactor/codemod/test_review_catalogue.py (TST-110-021), test_review_checks.py (TST-110-022/023, 40 violating + 40 clean real modules), test_review_engine.py (TST-110-024/025), test_review_engine_commit_wiring.py (TST-110-026). 56
REQ-110-034 yes All 40 checks SHALL operate on the engine's in-memory lossless layer: the ctok token array (LosslessModule / TokenStream) and the CPython ast overlay the forest already holds (NodeIndex). No check SHALL re-parse source from disk, call subprocess.run, or invoke an external linter. Structural checks SHALL be implemented as ast.NodeVisitor subclasses over the MUTATED forest's ast overlay; lexical checks SHALL be implemented as scanners over the MUTATED forest's token array. A check SHALL reuse the matching verification/review/ check_* function where one exists. (Amended by ADR-0068, Accepted 2026-10-06; the original clause named LibCST CSTTransformer/CSTVisitor trees, which REQ-110-120..127 superseded.) COMPLETE (default baseline delta, ADR-0073) — twenty implementations wrap the mapped verification/review/check_* functions (ReusedCheck, with IssueAdapter) over the mutated module's ast tree and lines rebuilt from its token stream; ten are new classes (ast.NodeVisitor subclasses, or scanners over stream.tokens/stream.source). check_lint (subprocess) is deliberately unmapped. TST-110-023 proves no check opens a file or spawns a process (an audit hook around all 80 fixture runs), that the source holds no I/O or linter imports, and that lexical checks read the stream while structural checks read the tree (a module whose two layers disagree). 34
REQ-110-035 - The InMemoryReviewEngine SHALL execute checks in three phases: fast structural checks (parallel per-file), graph-dependent checks, and Prolog semantic review; all phases SHALL run on the mutated forest. PARTIAL (S3 fix round 1, 2026-10-06) — ReviewPhaseRunner runs phase 1 (30 ids incl. Safety, plan D5) per file in parallel, phase 2 (10 ids) after it, and InMemoryReviewEngine appends PrologSemanticReview as phase 3; checks read the mutated forest's ast overlay and token stream. baseline='delta' is the default (ADR-0073, which supersedes plan D6); 'absolute' is the opt-in. TST-110-024 proves concurrency (a barrier both phase-1 workers must reach), phase order, the FINDING_EMITTED order and that ctx.cig is ws.cig. Gap: phase 2 is not "graph-dependent on the mutated forest". ws.cig is built once at load() from the pre-mutation forest, and none of the five phase-2 implementations (CO_LOCAL_MODULE_EXISTS, CO_ENTRY_POINT, CO_THIRD_PARTY_DECLARED, TI_MODULE_FIXTURES, TI_GLOBAL_STATE) reads ctx.cig; they read the mutated module's ast/tokens and the forest's path set. Closing it means rebuilding (or incrementally updating) the CIG from the mutated forest and a phase-2 check that queries it: a design item, owner orchestrator. 17
REQ-110-133 yes The engine SHALL be deployed as a long-running daemon process that keeps the workspace in memory between invocations. The daemon SHALL parse the workspace once at startup (120s) and serve subsequent requests via Unix socket IPC (<100ms round-trip). 1
REQ-110-134 yes The daemon SHALL create a Unix socket at .codemod_daemon/socket in the repo root, using JSON-RPC 2.0 for client communication. The daemon SHALL write its PID to .codemod_daemon/pid and acquire a lock file at .codemod_daemon/lock to prevent multiple daemons per repo. 10
REQ-110-135 yes The daemon SHALL support lifecycle management via codemod daemon start|stop|status|restart subcommands. start SHALL fork to background, parse the workspace, create the Unix socket, and start an inotify watcher thread. stop SHALL send SIGTERM, wait 5s for graceful shutdown, and force kill if necessary. 17
REQ-110-136 yes The daemon SHALL watch for file changes using inotify. When a file is modified, created, deleted, or moved, the daemon SHALL trigger incremental re-parse of the affected file(s) and update the CIG and CSR index. Incremental reload SHALL complete in <500ms. 7
REQ-110-137 yes The daemon SHALL support concurrent clients. Multiple clients MAY connect simultaneously and execute read operations (queries) in parallel. Write operations (run_pass, commit) SHALL be serialized via mutex. Reload operations SHALL acquire an exclusive lock and block all read/write operations. 6
REQ-110-138 yes The daemon SHALL expose JSON-RPC methods: query_dependencies, query_dependents, query_blast_radius, query_breaking_changes, query_cycles, query_topological_order, query_affected_tests, query_coupling_score, query_abstraction_level, query_centrality, query_impact_score, run_pass, commit, reload, status. 18
REQ-110-139 yes The daemon SHALL provide a Python client library (CodemodClient) that connects to the Unix socket and exposes the JSON-RPC methods as Python functions. The client SHALL support the context manager protocol for automatic connection cleanup. 4
REQ-110-140 yes The CLI SHALL auto-connect to the daemon if running, or start the daemon if not running (with --auto-start flag). If the daemon is not running and --auto-start is not set, the CLI SHALL fall back to library mode (parse on every invocation) and emit a warning. COMPLETE (S4) — scripts/codemod.py run and analyze connect to &lt;repo>/.codemod_daemon/socket whenever a daemon serves the repo with the requested roots, and otherwise print one library-mode warning; --no-daemon is silent; load-bench and report never connect (proof: tests/scripts/test_codemod_cli_daemon.py; design: contract D29; docs/codemod-daemon.md). Qualifier: it connects whenever a daemon serves the repo with the requested roots; breaking_changes, complexity, test_coverage, dead_code, all-passes parse locally with a notice (S4 backlog); a root mismatch runs locally with a notice. 1
REQ-110-101 yes No mocks, stubs, fakes, monkeypatch, or test skips in tests. Real files in tmp_path, real git init repos, real containers. Function-scope fixtures only. none
REQ-110-102 yes Every public class and method SHALL carry @traces(spec="110", story="US-110-NNN", reqs=["REQ-110-NNN"]) citing SPEC-110 requirement IDs. none
REQ-110-103 yes The engine SHALL pass all gates: ruff, black, standards, check-deps, test-review, pyright_ratchet. none
REQ-110-104 yes No pytest-xdist. Tests SHALL run via the batched executor (scripts/run_tests.py) or streaming runner (scripts/run_tests_streaming.py). none
REQ-110-105 yes New package: src/orchestrata_ultimate/refactor/codemod/ (sibling of refactor/rename/). Tests in tests/refactor/codemod/. No edits to refactor/rename/*, agents/tool_registry.py, or refactor/__init__.py (wiring is a later step, same policy as SPEC-109). none
REQ-110-106 yes The C extension (scripts/_codemod_fast/) SHALL declare Py_mod_gil = Py_MOD_GIL_NOT_USED in its PyModuleDef. The tests/conftest.py GIL probe SHALL fast-fail if the GIL is re-enabled. none
REQ-110-107 - LibCST FT-compatibility: If LibCST 1.8.6's native Rust parser has GIL issues on 3.14t, the engine SHALL fall back to LIBCST_PARSER_TYPE=pure. SUPERSEDED by REQ-110-120..126 (contract plans/contracts/ctok-parser): the FT landmine is removed, not hedged — LibCST's Rust parser is replaced by a hand-written FT-safe C tokenizer (_ctok) + CPython ast, so no Rust runs in the parse path and no pure-Python fallback is needed. none
REQ-110-108 yes Coverage ≥90% per file. Measured via pytest --cov=orchestrata_ultimate.refactor.codemod --cov-report=term-missing -o addopts="". none
REQ-110-120 yes The C tokenizer SHALL produce a lossless byte-span token stream: for every .py file in the tree, TokenStream.reconstruct() SHALL equal the on-disk bytes (zero mismatches, whole-tree). 5
REQ-110-121 yes The _ctok extension SHALL declare Py_MOD_GIL_NOT_USED; importing it SHALL NOT re-enable the GIL (sys._is_gil_enabled() stays False), retiring REQ-110-107. 2
REQ-110-122 yes LosslessModule.code SHALL equal the on-disk bytes at load time (same guarantee as REQ-110-002, token-array mechanism). 4
REQ-110-123 yes NodeIndex SHALL map every ast NAME/Attribute/def-name to its exact token byte-span, so a rename target is locatable without LibCST. 8
REQ-110-124 yes EditBuffer SHALL apply span edits as byte splices; every byte outside an edit span SHALL be byte-identical to the original (project-wide edit safety). 5
REQ-110-125 yes CodemodWorkspace.load SHALL build a LosslessModule forest; the engine SHALL import no libcst; libcst SHALL be removed from pyproject.toml. none
REQ-110-126 yes All transform passes and review passes SHALL operate on the ast/token model; behaviour SHALL be unchanged, proven by the existing codemod + rename suites green before AND after (behaviour-preserving refactor, executed via the refactor engine). (P3-audit correction: the engine has 4 review passes, not "40 checks"; and workspace.py currently defaults cig_backend="cst" — P3 SHALL make the LibCST-free ast backend the default and delete _cig_builder.py, closing the CLI/daemon-reload-vs-boot inconsistency.) none
REQ-110-127 yes Edit safety (Black-derived, assert_equivalent/assert_stable): after any EditBuffer/pass edit, the engine SHALL re-parse the result and assert AST-equivalence to the intended change via a normalized stringify_ast-style comparison — a rename differs only in the renamed identifiers; a formatting-only edit is AST-identical; anything else is rejected. Transform passes SHALL additionally be idempotent (re-running yields byte-identical output). 6

Code that traces this spec

Module Symbols Requirements
orchestrata_ultimate/refactor/codemod/_cig 15 3
orchestrata_ultimate/refactor/codemod/_types 4 2
orchestrata_ultimate/refactor/codemod/analysis/abstraction 4 2
orchestrata_ultimate/refactor/codemod/analysis/blast_radius 4 2
orchestrata_ultimate/refactor/codemod/analysis/breaking_change 6 2
orchestrata_ultimate/refactor/codemod/analysis/complexity 4 2
orchestrata_ultimate/refactor/codemod/analysis/coupling 4 2
orchestrata_ultimate/refactor/codemod/analysis/dead_code 5 2
orchestrata_ultimate/refactor/codemod/analysis/encode 2 1
orchestrata_ultimate/refactor/codemod/analysis/metrics 4 2
orchestrata_ultimate/refactor/codemod/analysis/protocol 2 2
orchestrata_ultimate/refactor/codemod/analysis/test_coverage 5 2
orchestrata_ultimate/refactor/codemod/client 16 3
orchestrata_ultimate/refactor/codemod/daemon 21 5
orchestrata_ultimate/refactor/codemod/observability/debug_panel 1 1
orchestrata_ultimate/refactor/codemod/observability/error_detector 1 1
orchestrata_ultimate/refactor/codemod/observability/events 1 1
orchestrata_ultimate/refactor/codemod/observability/graph_writer 1 2
orchestrata_ultimate/refactor/codemod/observability/metrics 1 1
orchestrata_ultimate/refactor/codemod/observability/tracing 1 1
orchestrata_ultimate/refactor/codemod/passes 13 5
orchestrata_ultimate/refactor/codemod/persistence/repository 7 2
orchestrata_ultimate/refactor/codemod/review 4 2
orchestrata_ultimate/refactor/codemod/review_catalogue 10 3
orchestrata_ultimate/refactor/codemod/review_checks 35 2
orchestrata_ultimate/refactor/codemod/review_context 1 2
orchestrata_ultimate/refactor/codemod/review_engine 15 3
orchestrata_ultimate/refactor/codemod/server 13 3
orchestrata_ultimate/refactor/codemod/txn 14 5
orchestrata_ultimate/refactor/codemod/watcher 6 1
orchestrata_ultimate/refactor/codemod/workspace 14 17
orchestrata_ultimate/refactor/ctok/edit_buffer 6 2
orchestrata_ultimate/refactor/ctok/lossless_module 3 3
orchestrata_ultimate/refactor/ctok/node_index 7 1
orchestrata_ultimate/refactor/ctok/token_stream 2 2
orchestrata_ultimate/refactor/ctok/tokenizer 3 2

Checklists

checklists/constitutional.md

SPEC-110 Constitutional Compliance Checklist

This checklist verifies that SPEC-110: In-Memory Code Intelligence Platform complies with the project's 12 constitutional articles.

Status Legend:


Article 1: Human Autonomy

Principle: The system must preserve human control over critical decisions.

Compliance: ✅ Fully compliant


Article 2: Transparency

Principle: The system must make its operations visible and understandable.

Compliance: ✅ Fully compliant


Article 3: Harm Prevention

Principle: The system must prevent damage to code, data, and infrastructure.

Compliance: ✅ Fully compliant


Article 4: Privacy

Principle: The system must respect user privacy and data confidentiality.

Compliance: ✅ Fully compliant


Article 5: Fairness

Principle: The system must treat all inputs and users equitably.

Compliance: ✅ Fully compliant


Article 6: Accountability

Principle: The system must enable tracing of actions to their origins.

Compliance: ✅ Fully compliant


Article 7: Security

Principle: The system must protect against malicious attacks and unauthorized access.

Compliance: ✅ Fully compliant


Article 8: Sustainability

Principle: The system must use resources efficiently and minimize environmental impact.

Compliance: ✅ Fully compliant


Article 9: Dignity

Principle: The system must respect human dignity and avoid degrading interactions.

Compliance: ✅ Fully compliant (N/A for most criteria)


Article 10: Rights

Principle: The system must respect user rights and intellectual property.

Compliance: ✅ Fully compliant (N/A for most criteria)


Article 11: Continuous Improvement

Principle: The system must enable learning and improvement over time.

Compliance: ✅ Fully compliant


Article 12: Plan Fidelity

Principle: The system must adhere to its stated specifications and requirements.

Compliance: ✅ Fully compliant


Summary
Article Status Notes
1. Human Autonomy ✅ Dry-run default, review-before-write, rollback capability
2. Transparency ✅ CommitReport, observability, tracing, debug panel
3. Harm Prevention ✅ Atomic writes, rollback, concurrent-edit guard, symlink prevention
4. Privacy ✅ No telemetry without opt-in, local operation, no user data
5. Fairness ✅ No bias, deterministic behavior, fair resource allocation
6. Accountability ✅ Graph DB provenance, correlation ID, @traces decorator
7. Security ✅ Path confinement, symlink prevention, advisory locking, parameterized Cypher
8. Sustainability ✅ Demand-driven parse, parallel execution, batch fsync, CSR arrays
9. Dignity ✅ Respectful error messages, no manipulation (N/A for most)
10. Rights ✅ No user data, no surveillance (N/A for most)
11. Continuous Improvement ✅ Observability, metrics, debug panel, benchmark tests
12. Plan Fidelity ✅ @traces, 132 requirements, 10 acceptance tests, 81 BDD scenarios

Overall Compliance: ✅ 12/12 articles fully compliant


Verification Methods
Automated Verification
Manual Verification

Non-Compliance Resolution

No non-compliance issues identified.

If non-compliance is discovered during implementation or review:

  1. Document the issue: Add entry to this checklist with [!] status and description
  2. Assess severity: Determine if issue is blocker or can be deferred
  3. Propose fix: Update spec.md, design document, or implementation as needed
  4. Verify fix: Re-run verification methods to confirm compliance
  5. Update checklist: Change status to [x] and add resolution notes

References

checklists/requirements.md

The requirement list above comes from this file; 56 of 57 checkbox items are ticked.

checklists/testing.md

SPEC-110 Testing Checklist

This checklist maps test scenarios (TST-110-NNN) to Gherkin feature files and tracks implementation status.

Status Legend:


Acceptance Tests (TST-110-001 through TST-110-010)
TST-110-001: Round-Trip Fidelity

Feature File: workspace_load.feature
Scenario: "Round-trip fidelity — module.code equals on-disk bytes"
Requirements: REQ-110-002

Implementation Notes:


TST-110-002: Comment Preservation

Feature File: workspace_load.feature
Scenario: "Comment preservation — only the mutated line changes"
Requirements: REQ-110-002

Implementation Notes:


TST-110-003: Compose Multiple Passes

Feature File: transform_passes.feature
Scenario: "Compose — rename + sweep + add-import in one run"
Requirements: REQ-110-008, REQ-110-009

Implementation Notes:


TST-110-004: Review-Abort on Banned Construct

Feature File: review_before_write.feature
Scenario: "BannedConstructs — bare os.environ aborts commit"
Requirements: REQ-110-012, REQ-110-013

Implementation Notes:


TST-110-005: Rollback on Post-Write Parse Failure

Feature File: transactional_writeback.feature
Scenario: "Rollback — post-write parse failure restores all files"
Requirements: REQ-110-014

Implementation Notes:


TST-110-006: Concurrent-Edit Guard

Feature File: transactional_writeback.feature
Scenario: "Concurrent-edit guard — file changed after load"
Requirements: REQ-110-015

Implementation Notes:


TST-110-007: Incremental Index Update

Feature File: workspace_index.feature
Scenario: "Incremental index — add-import updates dependents without reload"
Requirements: REQ-110-006

Implementation Notes:


TST-110-008: Benchmark — Full-Repo Load

Feature File: workspace_load.feature
Scenario: "Parallel parse completes within time budget"
Requirements: REQ-110-003, REQ-110-004, REQ-110-005

Implementation Notes:


TST-110-009: CLI — Subprocess Dry-Run and Apply

Feature File: cli.feature
Scenario: "Dry-run produces diff and report without writing"
Requirements: REQ-110-018, REQ-110-019

Implementation Notes:


TST-110-010: SPEC-109 Interop

Feature File: spec109_interop.feature
Scenario: "RenameSymbolPass consumes WorkspaceEditPlan"
Requirements: REQ-110-020, REQ-110-021

Implementation Notes:


FR-013: Daemon Architecture
TST-110-011: Daemon Lifecycle

Feature File: daemon_lifecycle.feature Scenario: "Daemon start creates socket, writes PID, parses workspace; stop shuts down gracefully" Requirements: REQ-110-013

Implementation Notes:


TST-110-012: IPC Protocol

Feature File: ipc_protocol.feature Scenario: "JSON-RPC request via Unix socket returns correct response within latency budget" Requirements: REQ-110-013

Implementation Notes:


TST-110-013: File Watching

Feature File: file_watching.feature Scenario: "inotify triggers incremental reload within 500ms of file modification" Requirements: REQ-110-013

Implementation Notes:


TST-110-014: Concurrency

Feature File: concurrency.feature Scenario: "Parallel read requests succeed; write requests are serialized" Requirements: REQ-110-013

Implementation Notes:


Feature File Inventory
Feature File FR Coverage Scenarios Status
workspace_load.feature FR-001 7 [ ]
workspace_index.feature FR-002 6 [ ]
transform_passes.feature FR-003 8 [ ]
review_before_write.feature FR-004 8 [ ]
transactional_writeback.feature FR-005 6 [ ]
cli.feature FR-006 5 [ ]
spec109_interop.feature FR-007 4 [ ]
code_intelligence_graph.feature FR-008 10 [ ]
observability.feature FR-009 6 [ ]
graph_persistence.feature FR-010 5 [ ]
analysis_passes.feature FR-011 8 [ ]
in_memory_review.feature FR-012 8 [ ]
daemon_lifecycle.feature FR-013 9 [ ]
ipc_protocol.feature FR-013 16 [ ]
file_watching.feature FR-013 10 [ ]
concurrency.feature FR-013 10 [ ]

Total: 16 feature files, ~126 scenarios


Non-BDD Tests

Note: Daemon tests (TST-110-011 through TST-110-014) are BDD scenarios, not plain pytest. They live in the feature files above, not in this table.

Test File Type Tests Status
test_benchmark_load.py Plain pytest 3 (performance budgets) [ ]
test_csr_index.py Plain pytest 6 (data structure correctness) [ ]
test_c_extension.py Plain pytest 6 (C/Python parity) [ ]

Total: 15 plain pytest tests


Coverage Targets

REQ-110-132: Coverage ≥90% per file

pytest tests/refactor/codemod/ \
  --cov=orchestrata_ultimate.refactor.codemod \
  --cov-report=term-missing \
  -o addopts="" \
  -p orchestrata_ultimate.core.threading.pytest_live_observer

Coverage Status:


Gate Commands

All gates must pass before every commit:

.venv/bin/ruff check src tests scripts
.venv/bin/black --check src/orchestrata_ultimate/refactor/codemod tests/refactor/codemod scripts/codemod.py
PYTHONPATH=src .venv/bin/python scripts/serialize_codebase_ast.py standards
PYTHONPATH=src .venv/bin/python scripts/serialize_codebase_ast.py check-deps --include-tests
PYTHONPATH=src .venv/bin/python scripts/test-review.py tests/refactor/codemod/ --stdout
PYTHONPATH=src .venv/bin/python scripts/pyright_ratchet.py --max 283

Gate Status:


Test Infrastructure
BDD Configuration

Feature file location: specs/110-codemod-engine/features/

Step definitions: tests/refactor/codemod/conftest.py (shared) + per-test-file (specific)

Scenario binding: @scenario per scenario (explicit, with TST/REQ docstrings)

Tags: @REQ_110_NNN, @TST_110_NNN, @FR_NNN on scenarios (registered as pytest markers)

Fixtures
# tests/refactor/codemod/conftest.py

@pytest.fixture
def codemod_repo(tmp_path: Path) -> Path:
    """Real git repository under tmp_path with src/ layout."""
    subprocess.run(["git", "init", "-q"], cwd=tmp_path, check=True)
    subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True)
    subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
    return tmp_path

@pytest.fixture
def fixture_package(codemod_repo: Path) -> Callable[[Mapping[str, str]], Path]:
    """Factory: write src/-rooted files, return repo root."""
    def make(files: Mapping[str, str]) -> Path:
        src_root = codemod_repo / "src"
        for relative, text in files.items():
            target = src_root / relative
            target.parent.mkdir(parents=True, exist_ok=True)
            target.write_text(text, encoding="utf-8")
        return codemod_repo
    return make
Shared Step Definitions
# tests/refactor/codemod/conftest.py

from pytest_bdd import given, when, then, parsers

@given(parsers.parse("the fixture package contains {count:d} Python files"), target_fixture="repo_root")
def fixture_with_files(fixture_package, count):
    files = {}
    for i in range(count):
        files[f"pkg/mod_{i}.py"] = f"# Module {i}\nx_{i} = {i}\n"
    files["pkg/__init__.py"] = ""
    return fixture_package(files)

@when("I load the workspace", target_fixture="workspace")
def workspace_loaded(repo_root):
    return CodemodWorkspace.load(repo_root, include=("src",))

@then(parsers.parse("the forest contains exactly {count:d} modules"))
def forest_count(workspace, count):
    assert len(workspace.forest) == count

Test Execution
Running BDD Tests
# Run all BDD scenarios
pytest tests/refactor/codemod/ -v

# Run specific feature file
pytest tests/refactor/codemod/test_workspace_load.py -v

# Run scenarios with specific tag
pytest tests/refactor/codemod/ -m REQ_110_001 -v

# Run with Gherkin terminal reporter
pytest tests/refactor/codemod/ --gherkin-terminal-reporter
Running Non-BDD Tests
# Run benchmark tests
pytest tests/refactor/codemod/test_benchmark_load.py -v

# Run CSR index unit tests
pytest tests/refactor/codemod/test_csr_index.py -v

# Run C extension parity tests
pytest tests/refactor/codemod/test_c_extension.py -v
Running Full Suite
# Via batched executor (recommended)
python scripts/run_tests.py tests/refactor/codemod/ --skip-gates

# Via streaming runner (development)
python scripts/run_tests_streaming.py tests/refactor/codemod/ -j 8

Implementation Notes
Test Organization
Test Data
Test Isolation
Test Parallelism

Deferred Tests

Success Criteria

Other tracked files

Full spec text

Verbatim from specs/110-codemod-engine/spec.md; headings are pushed down two levels.

SPEC-110: In-Memory Code Intelligence Platform

Metadata

1. Overview

What

SPEC-110 defines a complete in-memory code intelligence platform that loads an entire Python codebase as a lossless Concrete Syntax Tree (CST) forest, builds a multihop-queryable Code Intelligence Graph (CIG), executes composable transform and review passes entirely in RAM, and commits changes atomically with full rollback capability. The platform observes all operations through an event-driven architecture, persists provenance to a graph database via the repository pattern, and integrates Prolog-based semantic review for dependency safety analysis.

This is not merely a codemod engine. It is a code intelligence substrate that happens to also transform code. Every analysis, every review, every graph query, every transform pass operates on the same in-memory representation. Nothing touches disk until the final atomic commit.

Why

The project's existing rename engine (SPEC-109) demonstrated that half-applied refactors are catastrophic: a gw→slot rename that missed 4 Redis slot parsers produced 54 test failures and required >1 day of manual recovery across 3 entangled waves. The root cause was text-based, file-by-file editing without a unified view of the codebase.

Existing tools fail to solve this:

The project runs on Python 3.14.6 free-threaded with the GIL genuinely disabled (measured 2.66x parallel speedup on 4 threads, 12 CPU cores available). This is an unfair advantage no other codemod engine possesses. SPEC-110 exploits this with parallel CST parsing, per-file parallel transform passes, and concurrent observer dispatch.

How

The platform operates in three phases:

Phase 1 — Load & Index (RAM-only)

  1. Parse all .py files under configured roots into LibCST Module trees (lossless, byte-perfect round-trip)
  2. Build the Code Intelligence Graph (CIG): nodes for Module/Class/Function/Method/Parameter/Test/Import, edges for IMPORTS/CALLS/INHERITS/DEFINES/TESTED_BY/OVERRIDES
  3. Construct CSR (Compressed Sparse Row) adjacency arrays for O(1) dependency queries
  4. Index symbols, imports, and call sites for multihop traversal

Phase 2 — Transform & Review (RAM-only)

  1. Execute composable transform passes over the CST forest (parallel per-file where safe)
  2. Execute review passes over the mutated forest (verdicts before bytes)
  3. Emit CodemodEvent observations to all subscribed observers
  4. If any review pass produces an error-severity finding, abort the commit

Phase 3 — Commit (disk I/O)

  1. Snapshot pre-commit bytes for all mutated files
  2. Write mutated trees to disk via atomic tempfile + os.rename
  3. Batch fsync directory file descriptors (not per-file)
  4. Post-write re-parse verification
  5. If verification fails, restore all files from snapshot (byte-exact rollback)
  6. Persist provenance to graph database via CodemodGraphRepository
Non-Goals

2. Terminology

Term Definition
CST (Concrete Syntax Tree) A parse tree that preserves all source text including comments, whitespace, and formatting. LibCST's Module is a CST node.
Forest A collection of CST trees, one per source file. The workspace holds the entire codebase as a forest.
Lossless round-trip The property that module.code (LibCST serialization) equals the original on-disk bytes decoded as UTF-8.
CIG (Code Intelligence Graph) An in-memory property graph built from the CST forest, with nodes for code entities and edges for relationships.
CSR (Compressed Sparse Row) A graph adjacency representation using contiguous array.array('i') buffers for O(1) neighbor lookups.
Transform Pass A composable code transformation that takes a workspace and returns a ChangeSet of mutated trees.
Review Pass A composable code analysis that takes a workspace and ChangeSet, returning Finding objects (error/warning).
ChangeSet An immutable collection of ChangeEntry objects, each containing a mutated CST tree and metadata.
Finding A review result with severity (error/warning), rule ID, file path, line number, and human-readable message.
CommitReport A machine-readable JSON record of a commit attempt: passes run, files written, findings, duration, dry-run flag.
CodemodEvent An observable event emitted during codemod operations (workspace loaded, pass started, finding emitted, file written, etc.).
Blast radius The set of all code entities transitively affected by a change, computed via multihop graph traversal.
Breaking change A code change that violates the API contract of a downstream consumer (removed parameter, changed return type, deleted public method).
Demand-driven parse Deprecated. The engine uses eager parse to load the full codebase into memory for immediate graph queries.
Batch fsync Writing all files, then fsync-ing directory file descriptors (not per-file), reducing I/O overhead.
Observer A subscriber to SyncObservable[CodemodEvent] that reacts to events (logging, metrics, graph writes, error detection).
Repository pattern A Protocol-based abstraction for graph database operations, enabling backend-agnostic persistence.

3. User Scenarios

US-110-001: Rename a symbol across the entire codebase with full safety

As a developer performing a large-scale rename
I want to rename a class, function, or variable and have every reference updated atomically
So that I never produce a half-applied rename that breaks tests

Acceptance Criteria:

US-110-002: Apply a vocabulary migration across 500+ files

As a developer migrating from neo4j to graph terminology
I want to replace all occurrences of "Neo4j" with "Graph" in code, comments, and docstrings
So that the codebase reflects the technology-agnostic naming policy

Acceptance Criteria:

US-110-003: Detect breaking API changes before committing

As a library maintainer changing a public function signature
I want to know which downstream consumers will break
So that I can update them in the same commit or defer the change

Acceptance Criteria:

US-110-004: Run 40-check in-memory code review without subprocess calls

As a CI pipeline running pre-commit checks
I want to execute all style, quality, safety, and correctness checks in-process
So that review completes in <500ms instead of 5s with subprocess overhead

Acceptance Criteria:

US-110-005: Debug a failed codemod run with full observability

As a developer investigating why a codemod failed
I want to see a structured trace of all operations, timings, and errors
So that I can identify the root cause without re-running the entire operation

Acceptance Criteria:

US-110-006: Query code intelligence without reloading the workspace

As a developer exploring the impact of a proposed change
I want to ask "what depends on this module?" or "what tests cover this function?"
So that I can make informed refactoring decisions

Acceptance Criteria:

US-110-007: Compose multiple transforms in a single atomic commit

As a developer performing a multi-step refactor (rename + add import + update docstrings)
I want to apply all transforms in sequence and commit once
So that the codebase is never in an inconsistent intermediate state

Acceptance Criteria:

US-110-008: Verify LibCST round-trip fidelity on a real codebase

As a platform maintainer validating the CST substrate
I want to prove that every file's module.code equals the on-disk bytes
So that I can guarantee no silent corruption during transforms

Acceptance Criteria:

US-110-009: Integrate with SPEC-109 rename engine without forking resolvers

As a developer using the rename engine's LSP-based resolution
I want to feed WorkspaceEditPlan objects into the codemod engine
So that I get transactional writes and observability without duplicating rename logic

Acceptance Criteria:

US-110-010: Persist codemod provenance to the operational graph database

As a project maintainer auditing code changes
I want to query the graph database for "what codemod runs touched this file?"
So that I can trace changes back to their originating operations

Acceptance Criteria:

US-110-013: Keep the workspace warm across invocations via a daemon

As a developer running repeated codemod queries against a large workspace I want to pay the 120s parse cost once rather than on every invocation So that interactive use is possible at all

Acceptance Criteria:

Provenance. The daemon capability and its requirements REQ-110-133..140 were specified in §11 from the outset, but no US-110-0NN heading was ever written for them — so the four modules implementing it (daemon.py, client.py, server.py, watcher.py) have carried story="US-110-013" against a story that did not exist. This heading backs the id those decorators already cite; the story number is 013 as shipped rather than renumbered, because renumbering would invalidate working citations to fix nothing. 011 and 012 are unused, as REQ-110 ids are likewise sparse (001-035, 101-108, 133-140).

4. Data Models

4.1 Core Data Types

The platform's data model is built on frozen dataclasses with slots=True for memory efficiency and immutability. All types are serializable to JSON for CommitReport persistence.

# refactor/codemod/report.py

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Literal, Any

import libcst


@dataclass(frozen=True, slots=True)
class ChangeEntry:
    """One mutated CST tree with metadata."""
    path: str                          # repo-relative path (e.g., "src/pkg/mod.py")
    mutated_tree: libcst.Module        # the modified CST
    original_bytes: bytes              # pre-mutation on-disk bytes (for rollback)
    description: str                   # human-readable (e.g., "Renamed Foo to Bar")
    pass_name: str                     # which pass produced this entry


@dataclass(frozen=True, slots=True)
class ChangeSet:
    """Immutable collection of ChangeEntry objects."""
    entries: tuple[ChangeEntry, ...]

    @property
    def file_count(self) -> int:
        return len(self.entries)

    @property
    def paths(self) -> frozenset[str]:
        return frozenset(e.path for e in self.entries)

    def merge(self, other: ChangeSet) -> ChangeSet:
        """Combine two ChangeSets (later entries override earlier for same path)."""
        merged = {e.path: e for e in self.entries}
        merged.update({e.path: e for e in other.entries})
        return ChangeSet(entries=tuple(merged.values()))


@dataclass(frozen=True, slots=True)
class Finding:
    """One review finding (error or warning)."""
    severity: Literal["error", "warning"]
    path: str
    line: int | None
    rule: str                          # e.g., "BARE_OS_ENVIRON", "IMPORT_CYCLE"
    message: str
    proof_tree: dict[str, Any] | None = None  # from Prolog explain:explain_whynot/2


@dataclass(frozen=True, slots=True)
class WorkspaceStats:
    """Workspace load statistics."""
    file_count: int
    total_bytes: int
    rss_delta_bytes: int
    parse_time_seconds: float
    unparsable_count: int


@dataclass(frozen=True, slots=True)
class CommitReport:
    """Machine-readable commit report (JSON-serializable)."""
    passes_run: tuple[str, ...]
    files_written: tuple[str, ...]
    diffstat: str                      # unified diff summary
    findings: tuple[Finding, ...]
    duration_seconds: float
    dry_run: bool
    correlation_id: str                # links all CodemodEvent observations

    def to_json(self) -> str:
        """Serialize to JSON string."""
        ...
4.2 Code Intelligence Graph (CIG) Node Types

The CIG is an in-memory property graph with the following node types:

# refactor/codemod/_cig.py

from __future__ import annotations

from dataclasses import dataclass


@dataclass(frozen=True, slots=True)
class CIGModule:
    node_id: int                       # CSR index
    path: str                          # repo-relative path
    qualified_name: str                # dotted module name
    line_count: int
    byte_count: int
    content_hash: str                  # xxhash3_64


@dataclass(frozen=True, slots=True)
class CIGClass:
    node_id: int
    name: str
    qualified_name: str
    module_path: str
    line: int
    is_abstract: bool
    is_dataclass: bool
    is_protocol: bool
    is_enum: bool
    bases: tuple[str, ...]             # base class qualified names


@dataclass(frozen=True, slots=True)
class CIGFunction:
    node_id: int
    name: str
    qualified_name: str
    module_path: str
    line: int
    is_async: bool
    return_type: str | None
    complexity: int                    # cyclomatic complexity
    nesting_depth: int
    line_count: int
    param_count: int


@dataclass(frozen=True, slots=True)
class CIGMethod:
    node_id: int
    name: str
    qualified_name: str
    class_name: str
    module_path: str
    line: int
    is_async: bool
    is_classmethod: bool
    is_staticmethod: bool
    is_property: bool
    is_abstract: bool
    return_type: str | None
    complexity: int
    nesting_depth: int


@dataclass(frozen=True, slots=True)
class CIGParameter:
    node_id: int
    name: str
    type_annotation: str | None
    default: str | None
    position: int                      # 0-based parameter index
    parent_id: int                     # CIGFunction or CIGMethod node_id


@dataclass(frozen=True, slots=True)
class CIGTest:
    node_id: int
    name: str
    qualified_name: str
    module_path: str
    line: int
    target_name: str                   # stripped "test_" prefix for TESTED_BY matching


@dataclass(frozen=True, slots=True)
class CIGImport:
    node_id: int
    module_path: str                   # importing module
    imported_module: str               # imported module (dotted name)
    imported_name: str | None          # specific name (from X import Y) or None (import X)
    alias: str | None                  # "as" alias
    is_relative: bool
    line: int
4.3 Code Intelligence Graph (CIG) Edge Types
# refactor/codemod/_cig.py (continued)

from enum import StrEnum


class CIGEdgeType(StrEnum):
    """Edge types in the Code Intelligence Graph."""
    IMPORTS = "IMPORTS"                # Module → Module
    CALLS = "CALLS"                    # Function/Method → Function/Method
    INHERITS = "INHERITS"              # Class → Class
    DEFINES = "DEFINES"                # Module → Class/Function; Class → Method
    HAS_PARAMETER = "HAS_PARAMETER"    # Function/Method → Parameter
    TESTED_BY = "TESTED_BY"            # Function/Method → Test
    OVERRIDES = "OVERRIDES"            # Method → Method (same name, different class)
4.4 CSR (Compressed Sparse Row) Index

The CSR index stores graph adjacency as contiguous array.array('i') buffers for O(1) neighbor lookups:

# refactor/codemod/_csr_index.py

from __future__ import annotations

import array


class _CSRIndex:
    """Compressed Sparse Row adjacency index.

    Stores a directed graph as two CSR matrices (forward and transpose)
    for O(1) neighbor lookups in both directions.

    Memory layout:
        _offsets[i] : start index in _neighbors for node i
        _offsets[i+1] : end index (exclusive)
        _neighbors[_offsets[i]:_offsets[i+1]] : neighbor node_ids

    Example:
        Node 0 has neighbors [2, 5, 7]
        _offsets[0] = 0, _offsets[1] = 3
        _neighbors[0:3] = [2, 5, 7]
    """

    __slots__ = ("_offsets", "_neighbors", "_transpose_offsets", "_transpose_neighbors", "_node_count")

    def __init__(self, node_count: int, edges: list[tuple[int, int]]) -> None:
        self._node_count = node_count
        self._offsets, self._neighbors = self._build_csr(node_count, edges)
        self._transpose_offsets, self._transpose_neighbors = self._build_csr(
            node_count, [(to, frm) for frm, to in edges]
        )

    @staticmethod
    def _build_csr(node_count: int, edges: list[tuple[int, int]]) -> tuple[array.array, array.array]:
        """Build CSR arrays from edge list."""
        # Count out-degree per node
        degree = [0] * node_count
        for frm, _ in edges:
            degree[frm] += 1

        # Compute offsets (prefix sum)
        offsets = array.array("i", [0] * (node_count + 1))
        for i in range(node_count):
            offsets[i + 1] = offsets[i] + degree[i]

        # Fill neighbors
        neighbors = array.array("i", [0] * len(edges))
        cursor = list(offsets)  # mutable copy for fill cursor
        for frm, to in edges:
            neighbors[cursor[frm]] = to
            cursor[frm] += 1

        return offsets, neighbors

    def dependencies(self, node_id: int) -> list[int]:
        """Forward neighbors (what this node depends on)."""
        start = self._offsets[node_id]
        end = self._offsets[node_id + 1]
        return list(self._neighbors[start:end])

    def dependents(self, node_id: int) -> list[int]:
        """Reverse neighbors (what depends on this node)."""
        start = self._transpose_offsets[node_id]
        end = self._transpose_offsets[node_id + 1]
        return list(self._transpose_neighbors[start:end])

    def transitive_closure(self, node_id: int, direction: str = "forward", max_depth: int = -1) -> frozenset[int]:
        """BFS transitive closure with optional depth limit."""
        ...

    def update(self, node_id: int, new_edges: list[tuple[int, int]]) -> None:
        """Incrementally update edges for one node (rebuild CSR)."""
        ...
4.5 Codemod Event Types
# refactor/codemod/observability/events.py

from __future__ import annotations

from dataclasses import dataclass
from enum import StrEnum
from typing import Any


class CodemodEventType(StrEnum):
    """Observable event types during codemod operations."""
    WORKSPACE_LOADED = "workspace_loaded"
    INDEX_BUILT = "index_built"
    PASS_STARTED = "pass_started"
    PASS_COMPLETED = "pass_completed"
    FILE_MUTATED = "file_mutated"
    REVIEW_STARTED = "review_started"
    FINDING_EMITTED = "finding_emitted"
    REVIEW_COMPLETED = "review_completed"
    COMMIT_STARTED = "commit_started"
    FILE_WRITTEN = "file_written"
    COMMIT_COMPLETED = "commit_completed"
    COMMIT_ABORTED = "commit_aborted"
    ROLLBACK_TRIGGERED = "rollback_triggered"
    ERROR_DETECTED = "error_detected"


@dataclass(frozen=True, slots=True)
class CodemodEvent:
    """One observable event during codemod operations."""
    event_type: CodemodEventType
    timestamp: float                   # time.monotonic()
    correlation_id: str                # UUID linking all events in one run
    payload: dict[str, Any]            # event-specific data
    span_name: str | None = None       # for TracingObserver (e.g., "CodemodWorkspace.load")
4.6 Graph Persistence Records
# refactor/codemod/persistence/repository.py

from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime


@dataclass(frozen=True, slots=True)
class CodemodRunRecord:
    """Graph database record of a codemod run."""
    id: str                            # "{project}:codemod_run:{correlation_id}"
    project: str
    started_at: datetime
    completed_at: datetime | None
    status: str                        # "success", "aborted", "rolled_back"
    dry_run: bool
    pass_count: int
    finding_count: int
    file_count: int
    duration_ms: int


@dataclass(frozen=True, slots=True)
class CodemodPassRecord:
    """Graph database record of a transform/review pass."""
    id: str                            # "{run_id}:pass:{pass_name}"
    run_id: str
    pass_name: str
    pass_type: str                     # "transform" or "review"
    file_count: int
    duration_ms: int


@dataclass(frozen=True, slots=True)
class CodemodFindingRecord:
    """Graph database record of a review finding."""
    id: str                            # "{run_id}:finding:{rule}:{path}:{line}"
    run_id: str
    severity: str                      # "error" or "warning"
    rule: str
    path: str
    line: int | None
    message: str
    module_id: str | None              # CIGModule node_id for ABOUT edge


@dataclass(frozen=True, slots=True)
class CodeChangeRecord:
    """Graph database record of a code change."""
    id: str                            # "{run_id}:change:{path}"
    run_id: str
    pass_name: str
    path: str
    description: str
    diff_hash: str                     # xxhash3_64 of unified diff
    entity_id: str | None              # CIGModule/Class/Function node_id for CHANGED_IN edge
4.7 Data Model Diagram

Diagram projects-orchestrata-ultimate-specs-110-codemod-engine-m0490b4da62

5. Functional Requirements

FR-001: Lossless CST Forest Load

REQ-110-001: The CodemodWorkspace.load(root, include, parallel) method SHALL parse every .py file under the configured include roots into a libcst.Module tree, keyed by repo-relative path.

REQ-110-002: For every loaded module, module.code.encode('utf-8') SHALL equal the on-disk bytes at load time (lossless round-trip fidelity). A module that fails to parse SHALL be recorded in workspace.unparsable as a (path, error_message) tuple and excluded from the forest.

REQ-110-003 (amended 2026-08-11): Loading the full orchestrata-ultimate tree (~2,500 files across src/scripts/tests, ~780K LOC) with parallel=True SHALL use a ThreadPoolExecutor with max_workers=os.cpu_count() and complete in <120 seconds on 12 cores. The original "<360 seconds single-threaded" clause is RETIRED: it was never a user mandate (introduced by the SPEC-110 implementation commit as spec completeness) and production (the codemod daemon, _daemon_entry.py) always loads with parallel=True. Serial remains the signature default for small/CLI loads; its performance is informational only.

REQ-110-004 (amended 2026-08-24): The workspace.stats() method SHALL report WorkspaceStats with file count, total bytes, RSS delta (via psutil.Process().memory_info().rss), parse time, and unparsable count. RSS delta SHALL remain <3.5 GiB for the full codebase (2,500 files, 780K LOC, 30 MB on disk), measured in parallel=True mode — the configuration production actually runs (see REQ-110-003 amendment).

FR-002: Module + Dependency Index

REQ-110-005: The workspace SHALL expose modules() (bidirectional mapping of dotted names ↔ paths), dependencies(module) (modules this module imports), and dependents(module) (modules that import this module), all derived from the CST forest's import statements.

REQ-110-006: The index SHALL update incrementally when a pass mutates imports. The _update_index(paths) method SHALL re-derive dependencies and dependents for the specified modules only, not the entire forest.

REQ-110-007: The symbol_table() method SHALL map top-level defined names (classes, functions, module-level variables) per module, sufficient for a pass to resolve "who defines X" without disk access.

FR-003: Transform Passes

REQ-110-008: A transform pass SHALL be a class implementing TransformPass.apply(workspace) -> ChangeSet. The ChangeSet SHALL record, per module: the mutated CST tree, a human-readable description, and the pass name.

REQ-110-009: Passes SHALL compose: workspace.run(passes: list[TransformPass]) SHALL apply them in order over the same forest. Pass N SHALL see pass N-1's mutations. The composed ChangeSet union SHALL be what write-back serializes — one transaction for N passes.

REQ-110-010: The engine SHALL ship 5 built-in transform passes:

REQ-110-011: A transform pass SHALL NOT write to disk. The API SHALL give passes no file handles. Write-back SHALL be exclusively workspace.commit(). This SHALL be enforced by construction (no open(), Path.write_text(), or os.rename in pass code) and asserted by a test that greps the pass base-class surface.

FR-004: Review-Before-Write

REQ-110-012: A review pass SHALL be a class implementing ReviewPass.review(workspace, changes) -> list[Finding]. Review passes SHALL operate on the mutated forest (the ChangeSet entries' mutated_tree), not on diffs or disk files.

The engine SHALL ship 4 built-in review passes:

REQ-110-013: The workspace.commit(reviews=[...]) method SHALL run reviews FIRST. Any Finding with severity "error" SHALL abort the commit with the findings report — nothing is written to disk. This is the in-RAM analogue of SPEC-109's residue abort.

FR-005: Transactional Write-Back

REQ-110-014: The commit() method SHALL write only mutated modules. Each file SHALL be written via: (1) write to tempfile in same directory, (2) os.fsync(tempfile_fd), (3) os.rename(tempfile_path, target_path). After ALL writes, every written file SHALL be re-parsed from disk (libcst.parse_module(written_bytes)). Any failure SHALL trigger rollback of every written file to its pre-commit bytes (kept in ChangeEntry.original_bytes).

REQ-110-015: The commit() method SHALL refuse to run if any target file's on-disk mtime or content hash (xxhash3_64) changed since load() (concurrent-edit guard). The refusal SHALL list the stale paths. The guard SHALL use O_NOFOLLOW when opening files to prevent symlink attacks, and fcntl.flock(LOCK_EX | LOCK_NB) for advisory locking during the write window.

REQ-110-016: A commit(dry_run=True) SHALL produce the full unified diff and the review report without writing. The CLI SHALL default to dry-run (house convention).

REQ-110-017: Every commit SHALL emit a machine-readable CommitReport (JSON): passes run, files written, diffstat, review findings, duration, correlation_id. The report path SHALL be printed on completion.

FR-006: CLI

REQ-110-018: The scripts/codemod.py CLI SHALL provide subcommands:

The CLI SHALL use os.environ.get only (no bare os.environ["X"]), absolute imports, and read nothing but the repo tree and its own args.

REQ-110-019: Exit codes SHALL be: 0 success/clean-dry-run, 1 review-abort or verification failure, 2 usage error. These SHALL be asserted by CLI subprocess tests.

FR-007: SPEC-109 Integration

REQ-110-020: The RenameSymbolPass SHALL consume SPEC-109 artifacts (WorkspaceEditPlan from orchestrata_ultimate.refactor.rename.models) rather than reimplementing resolution. The engine SHALL add NO second rename resolver.

REQ-110-021: Write-back SHALL reuse the applier's safety invariants. If code reuse of EditApplier is impractical (CST writes whole modules, not spans), the INVARIANTS (atomicity, post-parse, rollback, path-escape guard) SHALL be reimplemented against whole-module writes and cross-tested with the same scenarios as tests/refactor/rename/test_applier.py.

FR-008: Code Intelligence Graph

REQ-110-022: The CodeIntelligenceGraph SHALL be built from the CST forest by _cig_builder.py. It SHALL contain nodes for Module, Class, Function, Method, Parameter, Test, Import, and edges for IMPORTS, CALLS, INHERITS, DEFINES, TESTED_BY, OVERRIDES.

REQ-110-023: The CIG SHALL support multihop queries:

REQ-110-024: All CIG queries SHALL execute in <2ms for the target codebase (2,500 files, ~15,000 edges) via CSR adjacency arrays.

FR-009: Observability

REQ-110-025: The workspace SHALL compose a SyncObservable[CodemodEvent] and emit events for all operations: workspace loaded, index built, pass started/completed, file mutated, review started/completed, finding emitted, commit started/completed/aborted, rollback triggered, error detected.

REQ-110-026: The engine SHALL ship 5 built-in observers:

REQ-110-027: All observers SHALL be optional and configurable. The workspace.run() and workspace.commit() methods SHALL accept an observers parameter to override defaults.

FR-010: Graph Persistence

REQ-110-028: The CodemodGraphRepository Protocol SHALL define methods: save_run(), save_pass(), save_findings(), save_changes(), link_changes_to_code(), query_runs(), query_findings(), query_affected_code().

REQ-110-029: The BoltSyncCodemodRepository SHALL implement the Protocol using SyncGraphRepository.unit_of_work() for transactional writes. It SHALL write CodemodRun, CodemodPass, CodemodFinding, CodeChange nodes with edges: HAS_CODEMOD_RUN, HAS_PASS, HAS_FINDING, ABOUT, PRODUCED_BY, BREAKING_CHANGE, CHANGED_IN.

REQ-110-030: The ontology SHALL extend canonical_schema.py with new labels (CodemodRun, CodemodPass, CodemodFinding, CodeChange) and relationships. The CodeChange label SHALL reuse the existing Change label as a sub-label.

FR-011: Analysis Passes

REQ-110-031: The engine SHALL ship analysis passes that compute code intelligence metrics from the CIG:

REQ-110-032: Analysis passes SHALL be read-only (no mutations) and SHALL return structured results (dataclasses) suitable for CLI output and graph persistence.

FR-012: In-Memory Review (40 Checks)

REQ-110-033: The InMemoryReviewEngine SHALL execute 40 checks on the mutated CST forest, organized into 7 categories:

Complexity (4 checks):

Style (6 checks):

Quality (4 checks):

Structure (6 checks):

Safety (10 checks):

Correctness (6 checks):

Test Isolation (4 checks):

REQ-110-034: All 40 checks SHALL operate on the engine's in-memory lossless layer: the ctok token array (LosslessModule / TokenStream) and the CPython ast overlay the forest already holds (NodeIndex). No check SHALL re-parse source from disk, call subprocess.run, or invoke an external linter. Structural checks SHALL be implemented as ast.NodeVisitor subclasses over the MUTATED forest's ast overlay; lexical checks (line length, whitespace, tabs, final newline, comment and pattern scans) SHALL be implemented as scanners over the MUTATED forest's token array. A check SHALL reuse the matching verification/review/ check_* function where one exists rather than re-implement it. (Amended by ADR-0068, Accepted 2026-10-06; the original clause named LibCST CSTTransformer/CSTVisitor trees, which REQ-110-120..127 superseded.)

REQ-110-035: The InMemoryReviewEngine SHALL execute checks in three phases:

  1. Fast structural checks (parallel per-file): complexity, style, quality, structure
  2. Graph-dependent checks (require CIG): correctness, test isolation
  3. Prolog semantic review (require Prolog facts): PrologSemanticReview pass

All phases SHALL run on the mutated forest (changes applied in-memory).

6. Error Conditions

ID REQ Trigger Severity Handling
ERR-110-001 REQ-110-002 File fails to parse (syntax error, encoding error) ERROR Record in workspace.unparsable as (path, error_message), exclude from forest, continue loading other files
ERR-110-002 REQ-110-013 Review pass produces error-severity finding ERROR Abort commit, return CommitReport with findings, write nothing to disk. For the default engine an error-severity finding means one the change INTRODUCED (delta baseline, ADR-0073); baseline='absolute' restores the unconditional abort.
ERR-110-003 REQ-110-014 Post-write re-parse fails (written file is invalid Python) CRITICAL Rollback ALL written files to pre-commit bytes, raise PostWriteVerificationError
ERR-110-004 REQ-110-015 Concurrent-edit guard detects stale file (mtime/hash changed) ERROR Refuse commit, list stale paths, suggest --force-reload (explicit reload + re-apply)
ERR-110-005 REQ-110-015 Symlink attack detected (O_NOFOLLOW fails) CRITICAL Refuse commit, log security alert, raise SymlinkAttackError
ERR-110-006 REQ-110-015 Advisory lock fails (fcntl.flock returns EWOULDBLOCK) ERROR Refuse commit, list locked paths, suggest retry after lock release
ERR-110-007 REQ-110-011 Transform pass attempts disk I/O (open(), Path.write_text()) CRITICAL Raise PassIOError at construction time (enforced by AST grep on pass code)
ERR-110-008 REQ-110-020 RenameSymbolPass receives invalid WorkspaceEditPlan (missing fields) ERROR Raise InvalidPlanError with field-level validation message
ERR-110-009 REQ-110-022 CIG builder encounters unresolvable import (module not in forest) WARNING Record in cig.unresolved_imports, continue building graph with partial edges
ERR-110-010 REQ-110-023 CIG query exceeds max_depth (infinite loop protection) ERROR Raise QueryDepthExceededError with suggested depth limit
ERR-110-011 REQ-110-025 Observer raises exception during event dispatch WARNING Log exception, continue dispatching to other observers (log-and-continue policy)
ERR-110-012 REQ-110-029 Graph DB write fails (connection error, Cypher syntax error) WARNING Log error, drop event (non-blocking observer), continue codemod operation
ERR-110-013 REQ-110-003 Parallel parse exceeds time budget (>40s on 12 cores) WARNING Log performance degradation, suggest --serial flag or reduce include roots
ERR-110-014 REQ-110-004 RSS delta exceeds 4 GiB WARNING Log memory pressure, suggest demand-driven parse or reduce include roots
ERR-110-015 REQ-110-012 Prolog semantic review fails (janus-swi error, Prolog syntax error) WARNING Log error, skip Prolog checks, continue with other review passes

7. API Contracts

7.1 CodemodWorkspace
# refactor/codemod/workspace.py

from __future__ import annotations

from collections.abc import Mapping, Sequence
from pathlib import Path
from typing import ClassVar

import libcst

from orchestrata_ultimate.core.base.traces import traces
from orchestrata_ultimate.core.patterns.observer import SyncObservable
from orchestrata_ultimate.refactor.codemod.passes import TransformPass
from orchestrata_ultimate.refactor.codemod.review import ReviewPass
from orchestrata_ultimate.refactor.codemod.report import (
    ChangeSet,
    CommitReport,
    WorkspaceStats,
    CodemodEvent,
)
from orchestrata_ultimate.refactor.codemod._cig import CodeIntelligenceGraph


@traces(spec="110", story="US-110-001", reqs=["REQ-110-001", "REQ-110-002", "REQ-110-003", "REQ-110-004"])
class CodemodWorkspace:
    """In-memory CST forest with demand-driven parse and parallel load."""

    DEFAULT_INCLUDE: ClassVar[tuple[str, ...]] = ("src", "scripts", "tests")

    def __init__(self, root: Path, include: tuple[str, ...], parallel: bool) -> None:
        ...

    @classmethod
    def load(
        cls,
        root: Path,
        include: tuple[str, ...] = DEFAULT_INCLUDE,
        parallel: bool = False,
    ) -> CodemodWorkspace:
        """Parse all .py files under include roots into CST forest.

        Args:
            root: Repository root directory.
            include: Tuple of directory names to include (e.g., ("src", "tests")).
            parallel: If True, use ThreadPoolExecutor for parallel parse.

        Returns:
            CodemodWorkspace with populated forest and index.

        Raises:
            ERR-110-001: Files that fail to parse are recorded in unparsable, not raised.
        """
        ...

    @property
    def forest(self) -> Mapping[str, libcst.Module]:
        """Repo-relative path → CST Module (demand-driven parse)."""
        ...

    @property
    def unparsable(self) -> Sequence[tuple[str, str]]:
        """(path, error_message) for files that failed to parse."""
        ...

    def modules(self) -> Mapping[str, str]:
        """Bidirectional mapping: dotted name ↔ repo-relative path."""
        ...

    def dependencies(self, module: str) -> frozenset[str]:
        """Modules this module imports (direct only)."""
        ...

    def dependents(self, module: str) -> frozenset[str]:
        """Modules that import this module (direct only)."""
        ...

    def symbol_table(self) -> Mapping[str, frozenset[str]]:
        """Module path → top-level defined names (classes, functions, variables)."""
        ...

    def stats(self) -> WorkspaceStats:
        """Workspace load statistics."""
        ...

    @traces(spec="110", story="US-110-003", reqs=["REQ-110-008", "REQ-110-009"])
    def run(self, passes: Sequence[TransformPass]) -> ChangeSet:
        """Apply transform passes sequentially over the forest.

        Pass N sees pass N-1's mutations. Returns composed ChangeSet.

        Args:
            passes: Sequence of TransformPass instances.

        Returns:
            ChangeSet with all mutations from all passes.

        Raises:
            ERR-110-007: If a pass attempts disk I/O.
        """
        ...

    @traces(spec="110", story="US-110-005", reqs=["REQ-110-013", "REQ-110-014", "REQ-110-015", "REQ-110-016", "REQ-110-017"])
    def commit(
        self,
        changes: ChangeSet,
        reviews: Sequence[ReviewPass] | None = None,
        dry_run: bool = True,
        observers: Sequence[Observer[CodemodEvent]] | None = None,
    ) -> CommitReport:
        """Review mutated forest, then write changes atomically.

        Args:
            changes: ChangeSet from workspace.run().
            reviews: Review passes to run before write. Defaults to DEFAULT_REVIEWS.
            dry_run: If True, produce diff and report without writing.
            observers: Override default observers for this commit.

        Returns:
            CommitReport with passes run, files written, findings, duration.

        Raises:
            ERR-110-002: If any review produces error-severity finding (introduced by the change, ADR-0073; `absolute` baseline aborts on any).
            ERR-110-003: If post-write re-parse fails (triggers rollback).
            ERR-110-004: If concurrent-edit guard detects stale file.
            ERR-110-005: If symlink attack detected.
            ERR-110-006: If advisory lock fails.
        """
        ...

    def _build_index(self) -> None:
        """Build CIG and CSR index from forest (called by load())."""
        ...

    def _update_index(self, paths: Sequence[str]) -> None:
        """Incrementally update index for mutated modules."""
        ...
7.2 TransformPass
# refactor/codemod/passes.py

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import ClassVar, Literal

from orchestrata_ultimate.core.base.traces import traces
from orchestrata_ultimate.refactor.codemod.workspace import CodemodWorkspace
from orchestrata_ultimate.refactor.codemod.report import ChangeSet


@traces(spec="110", story="US-110-003", reqs=["REQ-110-008", "REQ-110-011"])
class TransformPass(ABC):
    """Abstract base for composable code transforms."""

    name: ClassVar[str]                # e.g., "ReplaceStringLiteralPass"
    parallelism: ClassVar[Literal["serial", "per_file"]] = "serial"
    max_files: ClassVar[int | None] = None  # for DiffBudget review
    max_lines: ClassVar[int | None] = None

    @abstractmethod
    def apply(self, ws: CodemodWorkspace) -> ChangeSet:
        """Apply transform to workspace, return ChangeSet.

        MUST NOT write to disk (enforced by ERR-110-007).
        """
        ...

    def apply_file(
        self,
        path: str,
        tree: libcst.Module,
        ws: CodemodWorkspace,
    ) -> ChangeEntry | None:
        """Per-file parallel dispatch (override for embarrassingly-parallel passes).

        Only called when parallelism == "per_file".
        Return None if file is unchanged.
        """
        raise NotImplementedError
7.3 ReviewPass
# refactor/codemod/review.py

from __future__ import annotations

from abc import ABC, abstractmethod

from orchestrata_ultimate.core.base.traces import traces
from orchestrata_ultimate.refactor.codemod.workspace import CodemodWorkspace
from orchestrata_ultimate.refactor.codemod.report import ChangeSet, Finding


@traces(spec="110", story="US-110-004", reqs=["REQ-110-012", "REQ-110-013"])
class ReviewPass(ABC):
    """Abstract base for composable code reviews."""

    name: ClassVar[str]                # e.g., "BannedConstructs"

    @abstractmethod
    def review(self, ws: CodemodWorkspace, changes: ChangeSet) -> list[Finding]:
        """Review mutated forest, return findings.

        Operates on ChangeSet entries' mutated_tree, not on disk files.
        """
        ...
7.4 CodeIntelligenceGraph
# refactor/codemod/_cig.py

from __future__ import annotations

from dataclasses import dataclass

from orchestrata_ultimate.core.base.traces import traces
from orchestrata_ultimate.refactor.codemod._csr_index import _CSRIndex


@dataclass(frozen=True, slots=True)
class BlastRadius:
    """Result of blast_radius() query."""
    affected_nodes: frozenset[int]
    affected_files: frozenset[str]
    impact_score: float                # 0.0 to 1.0


@dataclass(frozen=True, slots=True)
class BreakingChange:
    """Result of breaking_changes() query."""
    change_path: str
    consumer_path: str
    consumer_line: int
    incompatibility: str               # e.g., "removed parameter 'x'"


@dataclass(frozen=True, slots=True)
class CouplingScore:
    """Result of coupling_score() query."""
    efferent: int                      # outgoing dependencies
    afferent: int                      # incoming dependencies
    instability: float                 # efferent / (efferent + afferent)


@dataclass(frozen=True, slots=True)
class AbstractionLevel:
    """Result of abstraction_level() query."""
    abstract_count: int                # Protocol/ABC classes
    concrete_count: int
    ratio: float                       # abstract / (abstract + concrete)


@traces(spec="110", story="US-110-006", reqs=["REQ-110-022", "REQ-110-023", "REQ-110-024"])
class CodeIntelligenceGraph:
    """In-memory property graph with multihop traversal."""

    def __init__(self, csr: _CSRIndex, nodes: dict[int, CIGModule | CIGClass | CIGFunction | ...]) -> None:
        ...

    def dependencies(self, node_id: int) -> list[int]:
        """Direct imports (O(1) via CSR)."""
        ...

    def dependents(self, node_id: int) -> list[int]:
        """Direct reverse imports (O(1) via CSR transpose)."""
        ...

    def blast_radius(self, node_id: int, hops: int = 3) -> BlastRadius:
        """BFS transitive closure over CALLS + INHERITS + IMPORTS edges."""
        ...

    def breaking_changes(self, change: CodeChange) -> list[BreakingChange]:
        """Detect API-breaking changes via signature diff + downstream consumers."""
        ...

    def dependency_path(self, from_id: int, to_id: int) -> list[int] | None:
        """Shortest dependency path (BFS)."""
        ...

    def cycles(self) -> list[list[int]]:
        """All import cycles via iterative Tarjan's SCC."""
        ...

    def topological_order(self) -> list[int]:
        """Kahn's algorithm — modules in dependency order."""
        ...

    def affected_tests(self, changed_functions: frozenset[int]) -> frozenset[int]:
        """Tests covering changed functions via TESTED_BY reverse edges."""
        ...

    def coupling_score(self, node_id: int) -> CouplingScore:
        """Efferent/afferent coupling + instability metric."""
        ...

    def abstraction_level(self, node_id: int) -> AbstractionLevel:
        """Ratio of abstract definitions (Protocol/ABC) to concrete."""
        ...

    def centrality(self, top_k: int = 20) -> list[tuple[int, float]]:
        """Betweenness centrality — bottleneck modules."""
        ...

    def impact_score(self, node_id: int) -> float:
        """PageRank-style importance based on incoming edges."""
        ...
7.5 CodemodGraphRepository
# refactor/codemod/persistence/repository.py

from __future__ import annotations

from collections.abc import Sequence
from typing import Protocol, runtime_checkable

from orchestrata_ultimate.core.base.traces import traces
from orchestrata_ultimate.refactor.codemod.persistence.records import (
    CodemodRunRecord,
    CodemodPassRecord,
    CodemodFindingRecord,
    CodeChangeRecord,
)


@traces(spec="110", story="US-110-010", reqs=["REQ-110-028"])
@runtime_checkable
class CodemodGraphRepository(Protocol):
    """Repository for codemod provenance in the operational graph DB."""

    def save_run(self, run: CodemodRunRecord) -> None:
        """MERGE CodemodRun node + HAS_CODEMOD_RUN edge to Project."""
        ...

    def save_pass(self, run_id: str, pass_record: CodemodPassRecord) -> None:
        """MERGE CodemodPass node + HAS_PASS edge to CodemodRun."""
        ...

    def save_findings(self, run_id: str, findings: Sequence[CodemodFindingRecord]) -> None:
        """UNWIND batch MERGE CodemodFinding nodes + HAS_FINDING + ABOUT edges."""
        ...

    def save_changes(self, run_id: str, changes: Sequence[CodeChangeRecord]) -> None:
        """UNWIND batch MERGE CodeChange nodes + PRODUCED_BY + CHANGED_IN edges."""
        ...

    def link_changes_to_code(self, changes: Sequence[CodeChangeRecord]) -> None:
        """MERGE BREAKING_CHANGE edges for detected breaking changes."""
        ...

    def query_runs(self, project: str, limit: int = 10) -> list[CodemodRunRecord]:
        """Query recent codemod runs for a project."""
        ...

    def query_findings(self, run_id: str) -> list[CodemodFindingRecord]:
        """Query all findings for a codemod run."""
        ...

    def query_affected_code(self, run_id: str) -> list[str]:
        """Query all code entity IDs affected by a codemod run."""
        ...

8. Non-Functional Requirements

REQ-110-101: No mocks, stubs, fakes, monkeypatch, or test skips in tests. Real files in tmp_path, real git init repos, real containers. Function-scope fixtures only.

REQ-110-102: Every public class and method SHALL carry @traces(spec="110", story="US-110-NNN", reqs=["REQ-110-NNN"]) citing SPEC-110 requirement IDs.

REQ-110-103: The engine SHALL pass all gates: ruff check, black --check, serialize_codebase_ast.py standards, check-deps --include-tests, test-review.py --stdout (0 issues), pyright_ratchet.py --max 283.

REQ-110-104: No pytest-xdist. Tests SHALL run via the batched executor (scripts/run_tests.py) or streaming runner (scripts/run_tests_streaming.py).

REQ-110-105: New package: src/orchestrata_ultimate/refactor/codemod/ (sibling of refactor/rename/). Tests in tests/refactor/codemod/. No edits to refactor/rename/*, agents/tool_registry.py, or refactor/__init__.py (wiring is a later step, same policy as SPEC-109).

REQ-110-106: Free-threading compatibility. The C extension (_codemod_fast) SHALL declare Py_mod_gil = Py_MOD_GIL_NOT_USED in its PyModuleDef. The tests/conftest.py GIL probe SHALL fast-fail if the GIL is re-enabled.

REQ-110-107: LibCST FT-compatibility. If LibCST 1.8.6's native Rust parser has GIL issues on 3.14t, the engine SHALL fall back to LIBCST_PARSER_TYPE=pure (pure-Python parser) and document which mode is active in benchmark output.

REQ-110-108: Coverage ≥90% per file. Measured via pytest --cov=orchestrata_ultimate.refactor.codemod --cov-report=term-missing -o addopts="".

9. Technical Design

9.1 System Architecture

Diagram projects-orchestrata-ultimate-specs-110-codemod-engine-m13e5baae9c

9.2 CST Forest Structure

The CST forest is a dictionary mapping repo-relative paths to LibCST Module trees. Each Module is a lossless representation of the source file, preserving:

Demand-driven parse: The forest is initialized with None sentinels for all discovered files. When a pass or query accesses workspace.forest[path], the file is parsed on-demand. This reduces RSS by ~80% for typical passes that touch <20% of files.

Parallel parse: When parallel=True, the workspace uses ThreadPoolExecutor(max_workers=os.cpu_count()) to parse files concurrently. On Python 3.14t with GIL disabled, this achieves ~6x speedup on 12 cores (measured 2.66x on 4 threads, extrapolated linearly).

9.3 Code Intelligence Graph

The CIG is built by _cig_builder.py in a single pass over the CST forest:

  1. Node creation: Walk each Module.body to extract ClassDef, FunctionDef, Import, ImportFrom nodes. Assign sequential node_id integers.
  2. Edge extraction: For each import, resolve the target module path and create an IMPORTS edge. For each call expression, resolve the callee and create a CALLS edge. For each class base, create an INHERITS edge.
  3. CSR construction: Pass the edge list to _CSRIndex, which builds forward and transpose adjacency arrays.

Multihop queries use BFS over the CSR arrays:

def blast_radius(self, node_id: int, hops: int = 3) -> BlastRadius:
    """BFS with depth limit over CALLS + INHERITS + IMPORTS edges."""
    visited = {node_id}
    frontier = [node_id]
    for _ in range(hops):
        next_frontier = []
        for nid in frontier:
            # Forward edges (CALLS, IMPORTS)
            next_frontier.extend(self._csr.dependencies(nid))
            # Reverse edges (INHERITS — subclasses affected by base changes)
            next_frontier.extend(self._csr.dependents(nid))
        next_frontier = [n for n in next_frontier if n not in visited]
        visited.update(next_frontier)
        frontier = next_frontier
    affected_files = frozenset(self._nodes[nid].module_path for nid in visited if isinstance(self._nodes[nid], (CIGFunction, CIGMethod)))
    return BlastRadius(affected_nodes=frozenset(visited), affected_files=affected_files, impact_score=len(visited) / len(self._nodes))
9.4 Transform Pass Execution

Transform passes execute in two modes:

Serial mode (parallelism == "serial"): The pass's apply(workspace) method is called once. The pass iterates over the forest internally. Used for RenameSymbolPass (cross-file dependencies require global view).

Per-file parallel mode (parallelism == "per_file"): The workspace dispatches apply_file(path, tree, workspace) to a ThreadPoolExecutor. Each thread processes one file independently. Used for ReplaceStringLiteralPass, AddImportPass, RemoveUnusedImportPass, DocstringSweepPass (embarrassingly parallel).

def run(self, passes: Sequence[TransformPass]) -> ChangeSet:
    composed = ChangeSet(entries=())
    for pass_ in passes:
        if pass_.parallelism == "per_file":
            with ThreadPoolExecutor(max_workers=os.cpu_count()) as pool:
                futures = [
                    pool.submit(pass_.apply_file, path, tree, self)
                    for path, tree in self.forest.items()
                ]
                entries = [f.result() for f in futures if f.result() is not None]
        else:
            entries = pass_.apply(self).entries
        composed = composed.merge(ChangeSet(entries=tuple(entries)))
        self._update_index(composed.paths)  # incremental index update
    return composed
9.5 Review Pipeline

The review pipeline executes in three phases:

Phase 1 — Fast structural checks (parallel per-file): Complexity, style, quality, structure checks. Each file is reviewed independently via ThreadPoolExecutor. These checks operate on individual CST trees without cross-file context.

Phase 2 — Graph-dependent checks (require CIG): Correctness, test isolation checks. These checks query the CIG for cross-file relationships (e.g., "does this import resolve to a file in the forest?").

Phase 3 — Prolog semantic review (require Prolog facts): The PrologSemanticReview pass asserts ChangeSet mutations as Prolog facts, then queries codemod_review.pl predicates. Uses explain:explain_whynot/2 for proof trees when a safety check fails.

def review(self, ws: CodemodWorkspace, changes: ChangeSet) -> list[Finding]:
    findings = []

    # Phase 1: Fast structural (parallel)
    with ThreadPoolExecutor(max_workers=os.cpu_count()) as pool:
        futures = [
            pool.submit(self._review_file, entry.mutated_tree, entry.path)
            for entry in changes.entries
        ]
        for f in futures:
            findings.extend(f.result())

    # Phase 2: Graph-dependent
    findings.extend(self._review_graph(ws, changes))

    # Phase 3: Prolog semantic
    findings.extend(self._review_prolog(changes))

    return findings
9.6 Transactional Write-Back

The commit process follows a strict sequence to ensure atomicity:

Diagram projects-orchestrata-ultimate-specs-110-codemod-engine-m5855a5cccc

Batch fsync optimization: Instead of fsync-ing each file individually (100 fsyncs for 100 files), the engine opens directory file descriptors and fsyncs each directory once. This reduces I/O overhead by ~20x (5 directories instead of 100 files).

def _commit_atomic(self, changes: ChangeSet, repo_root: Path, snapshot: dict[str, bytes]) -> None:
    dir_fds = {}
    written = []
    try:
        for entry in changes.entries:
            path = repo_root / entry.path
            parent = path.parent
            if parent not in dir_fds:
                dir_fds[parent] = os.open(parent, os.O_RDONLY | os.O_DIRECTORY)
            tmp = path.with_suffix('.codemod_tmp')
            tmp.write_bytes(entry.mutated_tree.code.encode('utf-8'))
            os.fsync(tmp.open('rb').fileno())
            os.rename(tmp, path)
            written.append(path)

        for fd in dir_fds.values():
            os.fsync(fd)

        # Post-write verification
        for path in written:
            libcst.parse_module(path.read_text(encoding='utf-8'))
    except Exception:
        self._rollback(snapshot, repo_root)
        raise
    finally:
        for fd in dir_fds.values():
            os.close(fd)
9.7 Observer Event Flow

The observer architecture uses SyncObservable[CodemodEvent] from core.patterns.observer. Events are emitted synchronously during operations, and observers react in subscription order.

Diagram projects-orchestrata-ultimate-specs-110-codemod-engine-mf2fe525bae

Log-and-continue policy: If an observer raises an exception, the observable logs the exception and continues dispatching to other observers. This prevents a failing observer from aborting the codemod operation.

9.8 Graph Persistence Model

The BoltSyncCodemodRepository writes provenance to the operational graph database using Cypher MERGE patterns:

// CodemodRun node + HAS_CODEMOD_RUN edge
MERGE (r:CodemodRun {id: $id})
SET r.started_at = datetime($started_at),
    r.completed_at = datetime($completed_at),
    r.status = $status,
    r.dry_run = $dry_run,
    r.pass_count = $pass_count,
    r.finding_count = $finding_count,
    r.file_count = $file_count,
    r.duration_ms = $duration_ms
WITH r
MATCH (p:Project {slug: $project})
MERGE (p)-[:HAS_CODEMOD_RUN]->(r)

// CodemodPass nodes + HAS_PASS edges (batched)
UNWIND $passes AS pass
MERGE (cp:CodemodPass {id: pass.id})
SET cp.pass_name = pass.pass_name,
    cp.pass_type = pass.pass_type,
    cp.file_count = pass.file_count,
    cp.duration_ms = pass.duration_ms
WITH cp, pass
MATCH (r:CodemodRun {id: pass.run_id})
MERGE (r)-[:HAS_PASS]->(cp)

// CodemodFinding nodes + HAS_FINDING + ABOUT edges (batched)
UNWIND $findings AS f
MERGE (cf:CodemodFinding {id: f.id})
SET cf.severity = f.severity,
    cf.rule = f.rule,
    cf.path = f.path,
    cf.line = f.line,
    cf.message = f.message
WITH cf, f
MATCH (r:CodemodRun {id: f.run_id})
MERGE (r)-[:HAS_FINDING]->(cf)
WITH cf, f
OPTIONAL MATCH (m:Module {id: f.module_id})
FOREACH (_ IN CASE WHEN m IS NOT NULL THEN [1] ELSE [] END |
    MERGE (cf)-[:ABOUT]->(m))

// CodeChange nodes + PRODUCED_BY + CHANGED_IN edges (batched)
UNWIND $changes AS c
MERGE (cc:CodeChange {id: c.id})
SET cc.pass_name = c.pass_name,
    cc.description = c.description,
    cc.diff_hash = c.diff_hash
WITH cc, c
MATCH (r:CodemodRun {id: c.run_id})
MERGE (cc)-[:PRODUCED_BY]->(r)
WITH cc, c
MATCH (entity {id: c.entity_id})
MERGE (entity)-[:CHANGED_IN]->(cc)
9.9 Prolog Integration

The PrologSemanticReview pass integrates with the project's Prolog infrastructure via janus-swi:

  1. Assert mutations as facts: For each ChangeEntry, assert codemod_mutated(ModulePath, PassName) and codemod_affected(Symbol, ModulePath) facts.
  2. Query safety predicates: Call codemod_review:pass_safe(PassType, Context, Safe) to check if the pass is safe given the current mutations.
  3. Explain failures: If a safety check fails, call explain:explain_whynot(codemod_review:pass_safe(...), ProofTree) to get a proof tree explaining why.
% src/orchestrata_ultimate/prolog/codemod_review.pl

:- module(codemod_review, [
    pass_safe/3,
    blast_radius_ok/3,
    import_cycle_free/2,
    rename_safe/3,
    string_replace_safe/3
]).

:- use_module(library(lists)).

% Dynamic facts asserted by Python
:- dynamic codemod_affected/2.
:- dynamic codemod_imports/2.
:- dynamic codemod_mutated/2.
:- dynamic codemod_string_use/3.

% A pass is safe if all its mutations pass safety checks
pass_safe(PassName, Module, Safe) :-
    codemod_mutated(Module, PassName),
    \+ unsafe_mutation(PassName, Module),
    Safe = true.
pass_safe(_, _, false).

% Blast radius: affected files must not exceed limit
blast_radius_ok(Symbol, MaxFiles, Ok) :-
    findall(A, codemod_affected(Symbol, A), Affected),
    length(Affected, Count),
    (Count =< MaxFiles -> Ok = true ; Ok = false).

% Import cycle detection
import_cycle_free(TargetModule, ImportedModule) :-
    \+ transitive_depends(ImportedModule, TargetModule).

transitive_depends(A, B) :- codemod_imports(A, B).
transitive_depends(A, B) :- codemod_imports(A, C), transitive_depends(C, B).

% Rename safety: no affected symbol in a different package
rename_safe(Symbol, _NewName, Safe) :-
    findall(A, codemod_affected(Symbol, A), Affected),
    forall(member(M, Affected), same_package(Symbol, M)),
    Safe = true.
rename_safe(_, _, false).

% String replacement safety
string_replace_safe(Module, Old, Safe) :-
    \+ codemod_string_use(Module, Old, fstring),
    \+ codemod_string_use(Module, Old, format_call),
    Safe = true.
string_replace_safe(_, _, false).
9.10 C Extension Design

The _codemod_fast C extension follows the janus-swi vendored-fork pattern:

Sub-package structure:

scripts/_codemod_fast/
  pyproject.toml          # setuptools build-backend
  setup.py                # Extension('codemod_fast._native', ...)
  codemod_fast/
    __init__.py           # Python fallback: try import _native, fall back to pure Python
    csr_index.c           # CSR build + BFS in C (10x faster than Python array.array)
    content_hash.c        # xxhash3 wrapper (5 GB/s vs SHA-256 500 MB/s)
    mmap_loader.c         # mmap file loading (zero-copy parse source)
    module.c              # PyModuleDef with Py_mod_gil = Py_MOD_GIL_NOT_USED

Critical: The module.c MUST declare Py_MOD_GIL_NOT_USED or the entire process loses free-threading. This is enforced by tests/conftest.py's GIL probe.

Python fallback pattern:

# codemod_fast/__init__.py

try:
    from codemod_fast._native import csr_build, csr_bfs, xxhash3_64, mmap_load
    _HAS_NATIVE = True
except ImportError:
    _HAS_NATIVE = False
    # Pure Python implementations below
    def csr_build(node_count, edges):
        ...
    def csr_bfs(offsets, neighbors, start, max_depth):
        ...
    def xxhash3_64(data):
        import hashlib
        return hashlib.sha256(data).hexdigest()[:16]
    def mmap_load(path):
        return Path(path).read_bytes()

Wiring in root pyproject.toml:

[tool.uv.sources]
codemod-fast = { path = "scripts/_codemod_fast" }

[tool.uv]
no-build-isolation-package = ["janus-swi", "codemod-fast"]
9.11 BDD Test Strategy

The test suite uses pytest-bdd 8.1.0 for behavioral specification:

Feature file location: specs/110-codemod-engine/features/ (co-located with spec.md)

Step definitions: tests/refactor/codemod/conftest.py (shared) + per-test-file (specific)

Scenario binding: @scenario per scenario (explicit, with TST/REQ docstrings)

Tags: @REQ_110_NNN, @TST_110_NNN, @FR_NNN on scenarios (registered as pytest markers)

Non-BDD tests: Benchmark, CSR unit, C extension parity tests stay as plain pytest (performance and data-structure tests don't benefit from Given/When/Then)

Example feature file:

@FR_001
Feature: Lossless CST Forest Load
  As a developer running a codemod
  I want the entire repository loaded as a lossless CST forest in RAM
  So that every subsequent operation works on a single, consistent, byte-perfect tree

  Background:
    Given a fixture package with Python source files

  @REQ_110_001 @TST_110_001
  Scenario: Load parses every .py file under included roots
    Given the fixture package contains 5 Python files across 2 directories
    When I load the workspace with include roots "src" and "tests"
    Then the forest contains exactly 5 modules
    And the unparsable list is empty

  @REQ_110_002 @TST_110_001
  Scenario: Round-trip fidelity — module.code equals on-disk bytes
    Given the fixture package contains a file with comments, blank lines, and odd quoting
    When I load the workspace
    Then for every loaded module, module.code equals the on-disk bytes decoded as UTF-8

Example step definitions:

# tests/refactor/codemod/conftest.py

from pytest_bdd import given, when, then, parsers

@given(
    parsers.parse("the fixture package contains {count:d} Python files"),
    target_fixture="repo_root",
)
def fixture_with_files(fixture_package, count):
    files = {}
    for i in range(count):
        files[f"pkg/mod_{i}.py"] = f"# Module {i}\nx_{i} = {i}\n"
    files["pkg/__init__.py"] = ""
    return fixture_package(files)

@when("I load the workspace", target_fixture="workspace")
def workspace_loaded(repo_root):
    return CodemodWorkspace.load(repo_root, include=("src",))

@then(parsers.parse("the forest contains exactly {count:d} modules"))
def forest_count(workspace, count):
    assert len(workspace.forest) == count

10. Examples

10.1 CLI Usage
# Measure workspace load performance
python scripts/codemod.py load-bench
# Output:
# Load time (serial): 28.3s
# Load time (parallel): 4.7s (12 cores)
# Files loaded: 797
# Total bytes: 9,437,184
# RSS delta: 1.2 GiB
# Unparsable: 0

# Dry-run a string replacement (default)
python scripts/codemod.py run --pass replace-string --arg old=Neo4j --arg new=Graph
# Output:
# Pass: ReplaceStringLiteralPass
# Files mutated: 152
# Findings: 0 errors, 3 warnings
# Diff:
# --- a/src/orchestrata_ultimate/knowledge_graph/neo4j_client.py
# +++ b/src/orchestrata_ultimate/knowledge_graph/neo4j_client.py
# @@ -42,7 +42,7 @@
# -class Neo4jClient:
# +class GraphClient:
# ...
# Commit report: reports/codemod-2026-07-27T14-32-01.json

# Apply the changes (write to disk)
python scripts/codemod.py run --pass replace-string --arg old=Neo4j --arg new=Graph --apply
# Output:
# Pass: ReplaceStringLiteralPass
# Files written: 152
# Findings: 0 errors, 3 warnings
# Duration: 2.3s
# Commit report: reports/codemod-2026-07-27T14-32-01.json

# Query code intelligence
python scripts/codemod.py analyze --query dependencies --module orchestrata_ultimate.knowledge_graph.sync
# Output:
# Dependencies of orchestrata_ultimate.knowledge_graph.sync:
#   - orchestrata_ultimate.knowledge_graph.canonical_schema
#   - orchestrata_ultimate.knowledge_graph.schema
#   - orchestrata_ultimate.core.graph.interfaces
#   - orchestrata_ultimate.core.neo4j_factory

# Query blast radius
python scripts/codemod.py analyze --query blast_radius --module orchestrata_ultimate.core.base.traces --hops 3
# Output:
# Blast radius of orchestrata_ultimate.core.base.traces (3 hops):
#   Affected nodes: 342
#   Affected files: 187
#   Impact score: 0.43
10.2 Python API Usage
from pathlib import Path
from orchestrata_ultimate.refactor.codemod.workspace import CodemodWorkspace
from orchestrata_ultimate.refactor.codemod.passes import (
    ReplaceStringLiteralPass,
    AddImportPass,
    DocstringSweepPass,
)
from orchestrata_ultimate.refactor.codemod.review import DEFAULT_REVIEWS

# Load workspace
ws = CodemodWorkspace.load(Path("."), include=("src", "tests"), parallel=True)
print(f"Loaded {ws.stats().file_count} files in {ws.stats().parse_time_seconds:.1f}s")

# Compose multiple passes
passes = [
    ReplaceStringLiteralPass(old="Neo4j", new="Graph", regex=False),
    DocstringSweepPass(replacements={"neo4j": "graph", "Neo4j": "Graph"}),
    AddImportPass(module="__future__", name="annotations"),
]
changes = ws.run(passes)
print(f"Mutated {changes.file_count} files")

# Review and commit (dry-run by default)
report = ws.commit(changes, reviews=DEFAULT_REVIEWS, dry_run=True)
print(f"Findings: {len([f for f in report.findings if f.severity == 'error'])} errors, "
      f"{len([f for f in report.findings if f.severity == 'warning'])} warnings")

# Apply changes (write to disk)
if not any(f.severity == "error" for f in report.findings):
    report = ws.commit(changes, reviews=DEFAULT_REVIEWS, dry_run=False)
    print(f"Written {len(report.files_written)} files in {report.duration_seconds:.1f}s")
    print(f"Commit report: {report.to_json()}")
10.3 CIG Query Usage
from orchestrata_ultimate.refactor.codemod.workspace import CodemodWorkspace

ws = CodemodWorkspace.load(Path("."), include=("src",))
cig = ws._cig  # CodeIntelligenceGraph instance

# Query dependencies
module_id = cig._node_ids["orchestrata_ultimate.knowledge_graph.sync"]
deps = cig.dependencies(module_id)
print(f"Dependencies: {[cig._nodes[d].qualified_name for d in deps]}")

# Query blast radius
blast = cig.blast_radius(module_id, hops=3)
print(f"Blast radius: {len(blast.affected_nodes)} nodes, {len(blast.affected_files)} files")
print(f"Impact score: {blast.impact_score:.2f}")

# Query breaking changes
from orchestrata_ultimate.refactor.codemod.report import CodeChange
change = CodeChange(path="src/pkg/api.py", pass_name="RenameSymbolPass", description="Removed parameter 'x' from foo()", diff_hash="abc123", entity_id=module_id)
breaking = cig.breaking_changes(change)
for bc in breaking:
    print(f"Breaking change: {bc.consumer_path}:{bc.consumer_line} — {bc.incompatibility}")

# Query import cycles
cycles = cig.cycles()
for cycle in cycles:
    print(f"Cycle: {' -> '.join(cig._nodes[nid].qualified_name for nid in cycle)}")

# Query topological order
order = cig.topological_order()
print(f"Topological order: {[cig._nodes[nid].qualified_name for nid in order[:10]]}...")

11. Daemon Architecture

11.1 Problem: Cold Start Cost

The engine parses 2,500 files (120s, 3 GiB RSS) on every invocation. This is unacceptable for:

11.2 Solution: Long-Running Daemon

REQ-110-133: The engine SHALL be deployed as a long-running daemon process that keeps the workspace in memory between invocations. The daemon SHALL parse the workspace once at startup (120s) and serve subsequent requests via Unix socket IPC (<100ms round-trip).

REQ-110-134: The daemon SHALL create a Unix socket at .codemod_daemon/socket in the repo root. The socket SHALL use JSON-RPC 2.0 protocol for client communication. The daemon SHALL write its PID to .codemod_daemon/pid and acquire a lock file at .codemod_daemon/lock to prevent multiple daemons per repo.

REQ-110-135: The daemon SHALL support lifecycle management via codemod daemon start|stop|status|restart subcommands. The start command SHALL fork to background, parse the workspace, create the Unix socket, and start an inotify watcher thread. The stop command SHALL send SIGTERM, wait 5s for graceful shutdown, and force kill if necessary.

REQ-110-136: The daemon SHALL watch for file changes using inotify. When a file is modified, created, deleted, or moved, the daemon SHALL trigger incremental re-parse of the affected file(s) and update the CIG and CSR index. Incremental reload SHALL complete in <500ms.

REQ-110-137: The daemon SHALL support concurrent clients. Multiple clients MAY connect simultaneously and execute read operations (queries) in parallel. Write operations (run_pass, commit) SHALL be serialized via mutex. Reload operations SHALL acquire exclusive lock and block all read/write operations.

REQ-110-138: The daemon SHALL expose the following JSON-RPC methods:

REQ-110-139: The daemon SHALL provide a Python client library (CodemodClient) that connects to the Unix socket and exposes the JSON-RPC methods as Python functions. The client SHALL support context manager protocol for automatic connection cleanup.

REQ-110-140: The CLI SHALL auto-connect to the daemon if running, or start the daemon if not running (with --auto-start flag). If the daemon is not running and --auto-start is not set, the CLI SHALL fall back to library mode (parse on every invocation) and emit a warning.

11.3 Performance Targets (Updated)
Metric Target Measurement
Daemon startup <125s 120s parse + 5s socket setup
IPC round-trip (query) <100ms Unix socket + JSON-RPC
IPC round-trip (transform) <10s Unix socket + pass execution
File change detection <500ms inotify event + incremental re-parse
Concurrent clients 10+ Parallel read operations
11.4 Example Usage

Daemon lifecycle:

# Start daemon (one-time cost: 120s)
codemod daemon start

# Subsequent CLI invocations (fast: <100ms)
codemod query dependencies pkg.module.Class
codemod query blast-radius pkg.module.Class --hops 3
codemod run --pass replace-string --arg old=foo --arg new=bar

# Stop daemon
codemod daemon stop

Python client:

from pathlib import Path
from orchestrata_ultimate.refactor.codemod.client import CodemodClient

with CodemodClient.connect(Path(".")) as client:
    # Query operations (fast, <100ms)
    deps = client.query_dependencies("pkg.module.Class")
    blast = client.query_blast_radius("pkg.module.Class", hops=3)
    
    # Transform operations (slower, 1-10s)
    changes = client.run_pass("ReplaceStringLiteralPass", {"old": "foo", "new": "bar"})
    report = client.commit(changes, dry_run=True)
11.5 Alternative Considered: Library with Disk Caching

Rejected because:

When library mode would be appropriate:

These use cases are supported via --no-daemon flag (falls back to library mode).

11.6 Path Confinement (SR-110-001)

Every file path SHALL be resolved and confined to the declared repo_root before any I/O. The _resolve_in_repo(path) method SHALL raise PathEscapeError if the path resolves outside repo_root. This SHALL run before snapshots, before writes, and before post-write verification.

def _resolve_in_repo(self, path: Path) -> Path:
    resolved = Path(path).resolve()
    try:
        resolved.relative_to(self._repo_root)
    except ValueError as exc:
        raise PathEscapeError(
            f"{path} resolves to {resolved}, outside repo_root {self._repo_root}"
        ) from exc
    return resolved

The concurrent-edit guard SHALL use O_NOFOLLOW when opening files to prevent symlink attacks. If open() fails with ELOOP (symlink detected), the commit SHALL be refused and a security alert logged.

import os

def _check_concurrent_edits(self, changes: ChangeSet, repo_root: Path) -> None:
    for entry in changes.entries:
        path = repo_root / entry.path
        # O_NOFOLLOW prevents following symlinks
        fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW)
        try:
            stat = os.fstat(fd)
            if stat.st_mtime_ns != self._snapshots[entry.path].mtime_ns:
                raise ConcurrentEditError(f"File {path} modified since load")
            content = os.read(fd, stat.st_size)
            if xxhash3_64(content) != self._snapshots[entry.path].content_hash:
                raise ConcurrentEditError(f"File {path} content changed since load")
        finally:
            os.close(fd)
11.8 Advisory File Locking (SR-110-003)

The commit process SHALL acquire advisory locks (fcntl.flock(LOCK_EX | LOCK_NB)) on all target files before writing. If a lock fails with EWOULDBLOCK, the commit SHALL be refused and the locked paths listed.

import fcntl

def _acquire_locks(self, changes: ChangeSet, repo_root: Path) -> list[int]:
    fds = []
    for entry in changes.entries:
        path = repo_root / entry.path
        fd = os.open(path, os.O_RDWR | os.O_NOFOLLOW)
        try:
            fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
            fds.append(fd)
        except OSError as exc:
            if exc.errno == 11:  # EWOULDBLOCK
                for fd in fds:
                    os.close(fd)
                raise ConcurrentEditError(f"File {path} locked by another process")
            raise
    return fds
11.9 No Disk I/O in Passes (SR-110-004)

Transform passes SHALL NOT write to disk. This SHALL be enforced by construction (the TransformPass API provides no file handles) and asserted by a test that greps the pass base-class surface for open(), Path.write_text(), os.rename, etc.

def test_pass_cannot_write_to_disk():
    """REQ-110-011: TransformPass API has no disk I/O capability."""
    import ast
    source = inspect.getsource(TransformPass)
    tree = ast.parse(source)
    for node in ast.walk(tree):
        if isinstance(node, ast.Call):
            if isinstance(node.func, ast.Name) and node.func.id in ("open", "write_text", "rename"):
                raise AssertionError(f"TransformPass contains disk I/O: {node.func.id}")
11.10 Threat Model
Threat Mitigation Severity
Path traversal attack _resolve_in_repo() with Path.resolve() + relative_to() check HIGH
Symlink attack O_NOFOLLOW flag on all file opens HIGH
Race condition (TOCTOU) Advisory locking + mtime/hash check MEDIUM
Malicious transform pass AST grep for disk I/O in pass code LOW
Graph DB injection Parameterized Cypher queries (no string interpolation) LOW
Prolog fact injection Facts asserted from trusted ChangeSet, not user input LOW

12. Governance

12.1 Prolog Semantic Review

The PrologSemanticReview pass integrates with the project's governance infrastructure:

12.2 Constitutional Compliance

The engine SHALL comply with the project's 12 constitutional articles:

Article Compliance
Human Autonomy Dry-run default; human approves --apply
Transparency Full CommitReport with diff and findings
Harm Prevention Rollback on any failure; no partial writes
Privacy No telemetry without opt-in
Fairness No bias in pass execution order
Accountability Graph DB provenance for all operations
Security Path confinement, symlink prevention, advisory locking
Sustainability Demand-driven parse reduces RSS by 80%
Dignity N/A (no human-facing UI)
Rights N/A (no user data)
Continuous Improvement Observability enables performance tuning
Plan Fidelity @traces on every public class/method

13. Dependencies

13.1 Required Dependencies
Package Version Purpose
libcst >=1.8.6 Lossless CST parsing (native Rust parser with cp314t wheels)
xxhash >=3.0.0 Fast content fingerprinting (5 GB/s)
psutil >=7.2.2 RSS measurement for memory guard
janus-swi >=1.5.3 Prolog integration (vendored free-threaded fork)
13.2 Reused Project Infrastructure
Component Source Purpose
core.base.traces.traces src/orchestrata_ultimate/core/base/traces.py @traces decorator
core.patterns.observer.SyncObservable src/orchestrata_ultimate/core/patterns/observer.py Event dispatch
core.tracing.Tracer src/orchestrata_ultimate/core/tracing/tracer.py Span timing
core.logging.StructuredFormatter src/orchestrata_ultimate/core/logging.py JSON logging
core.graph.interfaces.SyncGraphRepository src/orchestrata_ultimate/core/graph/interfaces.py Graph DB Protocol
core.neo4j_factory.create_neo4j src/orchestrata_ultimate/core/neo4j_factory.py Graph DB driver
refactor.rename.models.WorkspaceEditPlan src/orchestrata_ultimate/refactor/rename/models.py SPEC-109 interop
13.3 Rejected Alternatives
Alternative Reason for Rejection
ast (stdlib) Loses comments/formatting; ast.unparse destroys engineering comments
tree-sitter Read-only; no lossless write-back capability
parso Slower than LibCST; no native Rust parser
networkx (for CIG) Dict-of-sets overhead; CSR is 10x faster for adjacency queries
pytest-xdist Banned repo-wide (deadlocks ~78% of the time)
behave (BDD) Separate test runner; pytest-bdd integrates with existing infrastructure

14. Integration Points

14.1 BDD Test Strategy

The test suite uses pytest-bdd 8.1.0 for behavioral specification. Feature files live in specs/110-codemod-engine/features/ (co-located with spec.md). Step definitions live in tests/refactor/codemod/conftest.py (shared) and per-test-file (specific).

Feature file inventory (12 files, ~81 scenarios):

Feature File FR Coverage Scenarios
workspace_load.feature FR-001 7
workspace_index.feature FR-002 6
transform_passes.feature FR-003 8
review_before_write.feature FR-004 8
transactional_writeback.feature FR-005 6
cli.feature FR-006 5
spec109_interop.feature FR-007 4
code_intelligence_graph.feature FR-008 10
observability.feature FR-009 6
graph_persistence.feature FR-010 5
analysis_passes.feature FR-011 8
in_memory_review.feature FR-012 8
14.2 Non-BDD Tests
Test File Type Tests
test_benchmark_load.py Plain pytest 3 (performance budgets)
test_csr_index.py Plain pytest 6 (data structure correctness)
test_c_extension.py Plain pytest 6 (C/Python parity)
14.3 Coverage Targets

REQ-110-108: Coverage ≥90% per file. Measured via:

pytest tests/refactor/codemod/ \
  --cov=orchestrata_ultimate.refactor.codemod \
  --cov-report=term-missing \
  -o addopts="" \
  -p orchestrata_ultimate.core.threading.pytest_live_observer
14.4 Gate Commands

All gates SHALL pass before every commit:

.venv/bin/ruff check src tests scripts
.venv/bin/black --check src/orchestrata_ultimate/refactor/codemod tests/refactor/codemod scripts/codemod.py
PYTHONPATH=src .venv/bin/python scripts/serialize_codebase_ast.py standards
PYTHONPATH=src .venv/bin/python scripts/serialize_codebase_ast.py check-deps --include-tests
PYTHONPATH=src .venv/bin/python scripts/test-review.py tests/refactor/codemod/ --stdout
PYTHONPATH=src .venv/bin/python scripts/pyright_ratchet.py --max 283

15. Observability

15.1 Event Types

The CodemodEventType enum defines 14 observable events:

Event Type When Emitted Payload
WORKSPACE_LOADED After CodemodWorkspace.load() completes file_count, parse_time_seconds, rss_delta_bytes
INDEX_BUILT After CIG and CSR construction node_count, edge_count, build_time_seconds
PASS_STARTED Before TransformPass.apply() or ReviewPass.review() pass_name, pass_type
PASS_COMPLETED After pass completes pass_name, file_count, duration_seconds
FILE_MUTATED After a file's CST tree is mutated path, pass_name, description
REVIEW_STARTED Before review phase review_count
FINDING_EMITTED After a review pass produces a finding severity, rule, path, line, message, legacy (bool, true on a legacy finding under the default engine; ADR-0073)
REVIEW_COMPLETED After all review passes complete finding_count, error_count, warning_count, legacy_count (ADR-0073)
COMMIT_STARTED Before write-back phase file_count, dry_run
FILE_WRITTEN After a file is written to disk path, bytes_written
COMMIT_COMPLETED After successful commit files_written, duration_seconds
COMMIT_ABORTED After review abort error_findings
ROLLBACK_TRIGGERED After rollback due to verification failure files_rolled_back, failure_reason
ERROR_DETECTED After ErrorDetector pattern match pattern, severity, message
15.2 Observer Configuration

Observers are configured per-commit via the observers parameter:

from orchestrata_ultimate.refactor.codemod.observability import (
    TracingObserver,
    GraphWriterObserver,
    ErrorDetector,
    MetricsObserver,
    DebugPanelObserver,
)

observers = [
    TracingObserver(),
    GraphWriterObserver(repository=graph_repo, batch_size=100),
    ErrorDetector(patterns=["3_consecutive_parse_failures"]),
    MetricsObserver(),
    DebugPanelObserver(output_dir=Path("pipeline_output/traces")),
]

report = ws.commit(changes, reviews=DEFAULT_REVIEWS, dry_run=False, observers=observers)
15.3 Tracing Integration

The TracingObserver writes structured JSON spans via core.tracing.Tracer:

{
  "timestamp": "2026-07-27T14:32:01.123Z",
  "correlation_id": "codemod-2026-07-27T14-32-01",
  "span_name": "CodemodWorkspace.load",
  "duration_ms": 4723,
  "event_type": "WORKSPACE_LOADED",
  "payload": {
    "file_count": 797,
    "parse_time_seconds": 4.7,
    "rss_delta_bytes": 1288490188
  }
}
15.4 Metrics

The MetricsObserver records counters and histograms:

Counters:

Histograms:

15.5 Debug Panel

The DebugPanelObserver writes NDJSON trace files to pipeline_output/traces/{correlation_id}.ndjson:

{"timestamp": 1722089521.123, "event_type": "WORKSPACE_LOADED", "payload": {"file_count": 797}}
{"timestamp": 1722089521.456, "event_type": "INDEX_BUILT", "payload": {"node_count": 5234}}
{"timestamp": 1722089522.789, "event_type": "PASS_STARTED", "payload": {"pass_name": "ReplaceStringLiteralPass"}}
{"timestamp": 1722089523.012, "event_type": "FILE_MUTATED", "payload": {"path": "src/pkg/mod.py"}}
{"timestamp": 1722089523.234, "event_type": "PASS_COMPLETED", "payload": {"file_count": 152}}
{"timestamp": 1722089523.567, "event_type": "FINDING_EMITTED", "payload": {"severity": "warning", "rule": "TODO_COMMENT"}}
{"timestamp": 1722089524.890, "event_type": "COMMIT_COMPLETED", "payload": {"files_written": 152}}

16. Integration Points

16.1 SPEC-109 Rename Engine

The RenameSymbolPass consumes WorkspaceEditPlan from orchestrata_ultimate.refactor.rename.models. LSP text edits are converted to LibCST mutations via libcst.metadata.PositionProvider:

class RenameSymbolPass(TransformPass):
    name = "RenameSymbolPass"
    parallelism = "serial"  # cross-file dependencies

    def __init__(self, plan: WorkspaceEditPlan) -> None:
        self._plan = plan

    def apply(self, ws: CodemodWorkspace) -> ChangeSet:
        entries = []
        for file_edit in self._plan.files:
            path = str(file_edit.path.relative_to(ws._root))
            tree = ws.forest[path]
            # Convert LSP text edits to LibCST mutations
            wrapper = libcst.MetadataWrapper(tree)
            transformer = _LSPToCSTTransformer(file_edit.edits, wrapper)
            mutated_tree = wrapper.visit(transformer)
            entries.append(ChangeEntry(
                path=path,
                mutated_tree=mutated_tree,
                original_bytes=tree.code.encode('utf-8'),
                description=f"Applied {len(file_edit.edits)} LSP edits",
                pass_name=self.name,
            ))
        return ChangeSet(entries=tuple(entries))
16.2 Knowledge Graph Ontology

The ontology extends canonical_schema.py with new labels and relationships:

New labels:

New relationships:

16.3 Prolog Infrastructure

The PrologSemanticReview pass integrates with:

16.4 Event Bus

The ErrorDetector observer emits alerts via kernel.event_bus.EventBus:

class ErrorDetector:
    def on_event(self, event: CodemodEvent) -> None:
        if event.event_type == CodemodEventType.ERROR_DETECTED:
            self._event_bus.emit(SystemEvent(
                id=f"codemod-error-{event.correlation_id}",
                type="codemod.error",
                source="codemod_engine",
                timestamp=event.timestamp,
                payload=event.payload,
                correlation_id=event.correlation_id,
                severity="high",
            ))
16.5 Logging

The TracingObserver uses core.logging.StructuredFormatter for JSON logging:

class TracingObserver:
    def on_event(self, event: CodemodEvent) -> None:
        self._logger.log(
            level=logging.INFO,
            msg=event.event_type.value,
            extra={
                "correlation_id": event.correlation_id,
                "event_type": event.event_type.value,
                "payload": event.payload,
                "duration_ms": event.payload.get("duration_seconds", 0) * 1000,
            },
        )

17. Definition of Done

18. Revision History

Version Date Author Changes
1.0.0 2026-07-27 Mohamed Initial spec (handoff commit bfd0073a)
2.0.0 2026-07-27 Claude Opus 4.6 (orchestration) Complete rewrite: 18-section spec, 10-layer architecture, BDD test strategy, graph persistence, Prolog integration, C extension plan, GPT-4.1 adversarial review triage