Skip to content

Reshape CLDK front end: per-language factories + typed backend-config objects #164

Closed

Description

@rahlk

Abstract

This proposal recommends restructuring the CLDK front end, replacing the single
stringly-typed entry point CLDK(language).analysis(...) with a set of
per-language factory methods that accept typed backend-configuration objects
(referred to herein as "Option 2"). The motivation is that the public interface
currently conflates two orthogonal concerns within a single flat parameter
namespace, which produces validation logic that scales poorly and a backend
selection mechanism that is implicit rather than explicit.

This issue body has been reconciled to fold in the subsequent decisions recorded
in the comments: deprecation and removal of the Java source_code input,
consolidation of on-disk persistence into a single language-keyed cache_dir,
and removal of analysis_json_path and analysis_backend_path from the public
interface.

Problem Statement

The system exposes two orthogonal axes of variation:

  1. Language: java, python, typescript, c.
  2. Backend: the in-memory codeanalyzer backend versus the read-only
    Neo4j/Cypher backend.

The present facade collapses both axes into one method (core.py:101-258),
comprising approximately twelve keyword arguments dispatched by an
if/elif self.language construct. Three consequences follow.

First, validation is expressed negatively and scales as O(languages x knobs).
Representative cases appear at core.py:223-247, where analysis_backend_path
is rejected for Python and cache_dir/use_ray are rejected for non-Python
languages.

Second, backend selection is implicit: it is determined by the truthiness of
neo4j_config, and the selection logic is duplicated across
python_analysis.py:171-192 and typescript_analysis.py:84-104.

Third, Neo4jConnectionConfig is duplicated verbatim across
python/neo4j/config.py and typescript/neo4j/config.py. The docstring of the
former already identifies hoisting as the intended remediation.

Notably, the backend abstract base classes (java/backend.py,
python/backend.py, typescript/backend.py) already presuppose the two-axis
structure. The front end should be brought into correspondence with it.

Proposed Design

The governing principle is a separation of the two axes: the language is
selected by the factory method invoked, and the backend is selected by the type
of the configuration object supplied. The configuration object is a Parameter
Object; the per-language union of admissible configuration types forms a
discriminated union over which backend construction dispatches.

Cross-cutting query options (project_path, analysis_level, target_files,
eager) remain explicit parameters on the factory methods. Backend-specific
options are carried by a backend= configuration object whose type acts as the
discriminator.

Configuration objects

On-disk persistence is consolidated into a single cache_dir, defaulting to
<project>/.codeanalyzer and keyed by language to prevent collisions (see
"Cache layout"). The backend binary is sourced solely from the packaged
dependencies, so neither analysis_json_path nor analysis_backend_path
survives on the public interface. The result is a minimal base object that only
Python extends.

# cldk/analysis/commons/backend_config.py  (shared)
@dataclass
class CodeAnalyzerConfig:
    cache_dir: str | Path | None = None     # default: <project>/.codeanalyzer (language-keyed)

@dataclass
class PyCodeAnalyzerConfig(CodeAnalyzerConfig):
    use_codeql: bool = True
    use_ray: bool = False

# Java and TypeScript introduce no backend-specific options; they use
# CodeAnalyzerConfig directly. A named empty subclass may be introduced later
# only if nominal dispatch or an extension anchor becomes necessary.

@dataclass
class Neo4jConnectionConfig:        # hoisted from the two duplicate copies
    uri: str
    username: str = "neo4j"
    password: str = "neo4j"
    database: str | None = None
    application_name: str | None = None

PyBackend   = PyCodeAnalyzerConfig | Neo4jConnectionConfig
TSBackend   = CodeAnalyzerConfig | Neo4jConnectionConfig
JavaBackend = CodeAnalyzerConfig    # no neo4j backend at present

Factory methods

The Java factory is project-path only; the source_code (single-file) input is
deprecated and removed (a DeprecationWarning is already emitted at the current
entry point), bringing Java into line with the Python and TypeScript factories.

# core.py
class CLDK:
    @staticmethod
    def python(
        project_path: str | Path | None = None,
        *,
        analysis_level: str = AnalysisLevel.symbol_table,
        target_files: list[str] | None = None,
        eager: bool = False,
        backend: PyBackend = None,        # defaults to PyCodeAnalyzerConfig()
    ) -> PythonAnalysis: ...

    @staticmethod
    def java(project_path=None, *, analysis_level=..., target_files=None,
             eager=False, backend: JavaBackend = None) -> JavaAnalysis: ...

    @staticmethod
    def typescript(project_path=None, *, analysis_level=..., target_files=None,
                   eager=False, backend: TSBackend = None) -> TypeScriptAnalysis: ...

    @staticmethod
    def c(project_path) -> CAnalysis: ...

Backend dispatch

The truthiness-based selection in each facade is replaced by a single explicit
dispatch on the configuration type, ideally extracted into a shared resolver:

match backend:
    case Neo4jConnectionConfig() as cfg:
        ...PyNeo4jBackend(cfg.uri, cfg.username, cfg.password, cfg.database, app)
    case CodeAnalyzerConfig() as cfg:        # PyCodeAnalyzerConfig is a subtype
        ...PyCodeanalyzer(..., cache_dir=cfg.cache_dir, use_codeql=cfg.use_codeql, use_ray=cfg.use_ray)

Cache layout

A single cache_dir must be keyed by language so that a polyglot repository
analyzed under more than one language does not have its backends overwrite a
shared analysis.json. Each backend joins its own canonical language key under
the shared root; callers pass only the root.

<cache_dir>/
  java/        analysis.json
  python/      analysis.json, venv/, codeql/, analysis_cache.json
  typescript/  analysis.json

Representative call sites

CLDK.python("/repo")
CLDK.python("/repo", backend=PyCodeAnalyzerConfig(use_ray=True, use_codeql=False))
CLDK.python("/repo", backend=Neo4jConnectionConfig(uri="bolt://localhost:7687"))
CLDK.java("/repo", backend=CodeAnalyzerConfig(cache_dir="/tmp/ca"))

Expected Benefits

  1. Each per-language signature exposes only the parameters that are applicable,
    so static type checkers and editor completion present an honest interface and
    the negative-validation clauses are eliminated.
  2. Backend selection becomes explicit and type-driven rather than dependent on
    argument truthiness. Introducing a third backend requires one additional
    dataclass and one additional match arm, rather than threading new keyword
    arguments through every factory.
  3. Parameters are grouped according to the component that owns them (query
    versus backend); Neo4jConnectionConfig is consolidated into a single
    shared module; and on-disk persistence collapses from two knobs
    (cache_dir plus analysis_json_path) to one language-keyed cache_dir.
  4. Discoverability improves for the LLM-driven skills (poe-with-cldk,
    triage-and-pov) that introspect the public interface.

Scope of Work

  • Introduce cldk/analysis/commons/backend_config.py containing the
    configuration dataclasses and the per-language *Backend unions.
  • Hoist Neo4jConnectionConfig to a single shared location and re-export it
    from the existing python/neo4j and typescript/neo4j paths for backward
    compatibility.
  • Add the CLDK.java, CLDK.python, CLDK.typescript, and CLDK.c
    factory methods; the Java factory is project-path only.
  • Replace each facade's truthiness-based backend selection with a dispatch
    on the configuration type, extracting a shared resolver where appropriate.
  • Add a single cache_dir parameter; default it to <project>/.codeanalyzer
    and key the on-disk layout by language (<cache_dir>/<language>/).
  • Remove analysis_json_path from the public interface; route Java/TS
    analysis.json output and reuse through the language-keyed cache.
  • Remove analysis_backend_path from the public interface entirely; source
    the backend binary solely from the packaged dependency (Java/TS), with
    $CODEANALYZER_TS_BIN remaining as the only out-of-band override.
  • Remove the Java source_code input after its deprecation window: delete
    the parameter from JavaAnalysis.__init__, the compatibility shim, and
    JCodeanalyzer.
  • Migrate the Python backend's existing cache_dir/analysis_json_path
    split onto the unified, language-keyed layout.
  • Retain CLDK(language).analysis(...) as a thin deprecated shim that
    forwards to the new factories for one or two release cycles.
  • Update the documentation and the skills that construct CLDK(...).
  • Add tests covering backend selection by configuration type, the
    language-keyed cache layout, and the deprecation shims.

Compatibility Considerations

The change introduces a new preferred entry point while retaining the existing
one as a deprecation shim, so the migration is staged rather than abrupt. The
work may proceed incrementally: the factory methods may be introduced first,
after which the typed backend= configurations may be added alongside the
existing keyword arguments under a deprecation window.

The language-keyed cache layout relocates analysis.json from
<cache_dir>/analysis.json to <cache_dir>/<language>/analysis.json. Existing
caches will not be found at the new path, so the first run after the change
recomputes the analysis. This is benign and should be noted in the changelog.

Activity

  1. rahlk commented on Jun 22, 2026

    @rahlk
    CollaboratorAuthor

    Addendum: Deprecation of Java source_code input

    The Java source_code (single-file) input is deprecated and slated for removal.
    A DeprecationWarning is now emitted at the public entry point when it is
    supplied (core.py, Java branch), and the corresponding parameter
    documentation has been annotated accordingly.

    Consequences for this redesign:

    • JavaCodeAnalyzerConfig should NOT carry a source_code field. The Java
      factory (CLDK.java) is therefore project-path only, bringing Java into line
      with the Python and TypeScript factories, which already reject single-file
      mode.
    • The deprecation shim retained for CLDK(language).analysis(...) should
      continue to forward source_code (with the warning) until the removal lands,
      after which the parameter is dropped from both the shim and the underlying
      JavaAnalysis constructor.

    Additional task

    • Remove the Java source_code input after the deprecation window:
      delete the parameter from JavaAnalysis.__init__, the
      CLDK(language).analysis(...) shim, and JCodeanalyzer; omit it from
      JavaCodeAnalyzerConfig.
  2. rahlk commented on Jun 22, 2026

    @rahlk
    CollaboratorAuthor

    Addendum: Unified cache directory with per-language keying

    Decision: consolidate the on-disk persistence knobs into a single cache_dir
    parameter shared by all languages, and remove analysis_json_path from the
    public interface. analysis_backend_path is removed entirely from the
    public interface; the backend binary is sourced exclusively from the
    pyproject.toml dependencies (codeanalyzer-java, codeanalyzer-typescript,
    codeanalyzer-python).

    Rationale

    analysis_json_path is not a backend locator; it is the on-disk cache/output
    location for analysis.json, and it governs caching behavior. For Java and
    TypeScript a value of None forces pipe mode, in which the analyzer recomputes
    on every instantiation; a non-None value enables reuse via the existing
    compatibility check. It is therefore the Java/TypeScript analogue of the Python
    cache_dir. Deleting it outright would regress Java/TypeScript to uncached
    operation, so it is folded into cache_dir rather than dropped.

    analysis_backend_path is removed outright rather than retained as an override.
    The packaged binary is now the single source of the backend executable: Java
    invokes python -m codeanalyzer_java, and TypeScript resolves
    codeanalyzer_typescript.bin_path() from the installed package. The
    $CODEANALYZER_TS_BIN environment variable remains available as an
    out-of-band escape hatch for TypeScript, so no public parameter is required for
    custom or locally built binaries.

    Caveat: subdirectories must be keyed by language

    A single cache_dir introduces a collision hazard: a polyglot repository
    analyzed under more than one language would otherwise have each backend write
    analysis.json to the same location, overwriting the others. The cache layout
    must therefore be keyed by language. Proposed layout, with the default
    cache_dir resolving to <project>/.codeanalyzer:

    <cache_dir>/
      java/        analysis.json
      python/      analysis.json, venv/, codeql/, analysis_cache.json
      typescript/  analysis.json
    

    Each backend is responsible for joining its own language key under cache_dir;
    callers pass a single root and never the per-language subdirectory. The key
    should be the canonical language identifier (java, python, typescript,
    c) so the partitioning is stable and predictable.

    Effect on the configuration objects

    CodeAnalyzerConfig is revised to carry a unified cache_dir only; it no
    longer carries analysis_json_path or analysis_backend_path:

    @dataclass
    class CodeAnalyzerConfig:
        cache_dir: str | Path | None = None          # default: <project>/.codeanalyzer
    
    @dataclass
    class PyCodeAnalyzerConfig(CodeAnalyzerConfig):
        use_codeql: bool = True
        use_ray: bool = False

    The default-on caching means the former persistence knobs disappear from a
    typical call; cache_dir need only be supplied to relocate the cache root.

    Additional tasks

    • Add a single cache_dir parameter to every factory; default it to
      <project>/.codeanalyzer.
    • Key the cache layout by language; each backend joins its own subdirectory
      (<cache_dir>/<language>/) under the shared root.
    • Remove analysis_json_path from the public interface; route Java/TS
      analysis.json output and reuse through <cache_dir>/<language>/.
    • Remove analysis_backend_path from the public interface entirely; source
      the backend binary solely from the packaged dependency (Java/TS), with
      $CODEANALYZER_TS_BIN remaining as the only out-of-band override.
    • Migrate the Python backend's existing cache_dir/analysis_json_path
      split onto the unified, language-keyed layout.
  3. self-assigned this
    on Jun 22, 2026
  4. added a commit that references this issue on Jun 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions