Skip to content

refactor: reshape analysis levels as a fluent AnalysisPipeline#110

Open
rahlk wants to merge 14 commits into
mainfrom
refactor/analysis-pipeline-fluent-passes
Open

refactor: reshape analysis levels as a fluent AnalysisPipeline#110
rahlk wants to merge 14 commits into
mainfrom
refactor/analysis-pipeline-fluent-passes

Conversation

@rahlk

@rahlk rahlk commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

What

Reshape the analysis levels: refactor Codeanalyzer.analyze() — a ~130-line procedural method with if self.analysis_level >= N gates threaded throughout — into a fluent AnalysisPipeline of four subsystem-grouped passes over a shared AnalysisContext.

analysis = (
    AnalysisPipeline(ctx)
        .with_symbol_table()        # syntactic_analysis/   (L1 tree)
        .with_call_graph()          # semantic_analysis/ + identity (L1 + L2)
        .with_intraproc_dataflow()  # dataflow/  L3
        .with_interproc_dataflow()  # dataflow/  L4
        .build()                    # -> Analysis
)

This is a behavior-preserving refactor — no schema change, no CLI change, no output change. schema_version stays 2.0.0.

Why

The old analyze() was hard to read as "the four levels," the per-level work wasn't independently testable, adding/reordering a step meant editing a monolith, and per-step observability was hand-rolled. The pipeline makes each level a self-contained, testable context -> context pass with uniform timing/gate logging.

Design

  • Passes self-gate on an intrinsic min_level (1, 1, 3, 4) via one shared _run gate — no skip argument to pass wrong at a call site. Skips are observable (⏭️ intraproc_dataflow: skipped (level 2 < 3)); the two internal L2 gates (PyCG augmentation, backfill_callees) stay inside _pass_call_graph.
  • AnalysisContext carries the working state threaded through the chain: inputs (options, project_dir, virtualenv, analysis_level, app_name, cached_symbol_table) and produced artifacts (symbol_tableapp/sig_to_idinfosir). The L4 pass reuses the L3 PDGs (infos) rather than recomputing.
  • Cache and venv lifecycle stay on Codeanalyzer (not in the pipeline), so every pass is a pure context -> context function testable against a hand-built context. Each pass asserts its preconditions, so a misordered chain fails loudly.
  • New codeanalyzer/pipeline/ package (context.py, symbol_table.py, passes.py, pipeline.py) — imports only downward (never from core; core imports from pipeline). Three helpers (build_symbol_table, pycg_call_graph_edges, home_external_symbols) moved out of Codeanalyzer as free functions.

Full rationale in docs/superpowers/specs/2026-07-22-analysis-pipeline-fluent-passes-design.md; task-by-task plan in docs/superpowers/plans/2026-07-22-analysis-pipeline-fluent-passes.md.

Behavior-preservation evidence

  • Characterization golden gate (test/test_pipeline_equivalence.py): analysis.json for four fixtures × levels was captured from the old analyze(), committed, and now matches the new pipeline byte-for-byte (L1–L3). Volatile fields (analyzer.version, repository, absolute file_path/path/last_modified) are normalized so the gate is deterministic across runs.
  • Degradation semantics preserved verbatim (PyCG failure → Jedi-only; python-scalpel absent → type-based alias fallback).
  • Monotonicity (L1 ⊆ L2 ⊆ L3 ⊆ L4) and determinism (PYTHONHASHSEED=0 + sorted call graph) unchanged.

Test status

  • The behavior-preservation suites are green: the equivalence gate, test_pipeline.py (pass/pipeline units, no mocks), and the test_v2_* / test_dataflow_* / test_neo4j_schema / test_env_interpreter suites.
  • Pre-existing, unrelated: test/test_cli.py has ~6 failures (PyCG edge-count assertions + one --version test) that fail on main as well (a PyCG/env issue on Python 3.14). This branch introduces zero new failures.

L4 coverage caveat

The -a 4 equivalence goldens require the optional python-scalpel dependency; when it is absent those cases skip, so the byte-identical gate enforces L1–L3 in a scalpel-free environment. L4 behavior-preservation is instead covered by the interproc pass being a verbatim relocation of the old L4 block plus the structural L4 suites (test_v2_l4, test_v2_l4_summary, test_dataflow_sdg) and the L4 ordering test in test_pipeline.py. Follow-up suggestion: add a scalpel-installed CI lane to capture and commit the a4 goldens, making "byte-identical at every level" literally enforced.

rahlk added 14 commits July 22, 2026 10:33
Design for refactoring Codeanalyzer.analyze() into a fluent AnalysisPipeline
of four subsystem-grouped passes over a shared AnalysisContext. Passes
self-gate on an intrinsic min_level (no explicit skip arg); caching/venv stay
at the edge; three helpers move off Codeanalyzer. Behavior-preserving:
byte-identical output at every level, monotonicity preserved. Includes the
four-layer testing plan.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant