Skip to content

TypeScript backend does not invalidate cached analysis.json on source change — stale results unless eager=True #269

Description

@rahlk

PROBLEM

The TypeScript backend reuses a cached analysis.json whenever one exists and eager=False — with NO check that the source is unchanged. cldk/analysis/typescript/codeanalyzer/codeanalyzer.py:168:

needs_run = self.eager_analysis or not analysis_json_file.exists() or bool(self.target_files)

So after any edit to the analyzed project, a re-analysis with the default eager=False silently returns the PRIOR analysis.json — stale symbol table, call graph, and (schema v2) L3/L4 body/edge data. The analyzer subprocess never runs, so cants' own content-hash cache never gets a chance to detect the change. The only escape is passing eager=True on every call, which defeats caching entirely.

Contrast: the Python backend (codeanalyzer.py:239) always invokes analyzer.analyze() and delegates freshness to codeanalyzer-python's content-hash cache — so Python invalidates correctly on source change. TS is the odd one out.

WHY IT MATTERS NOW (schema v2 / rc.1)

Under schema v2, sub-callable vertices (statements, CFG/DDG nodes) are addressed by ORDINAL ids (@line:col) that are deliberately NOT durable across edits. Any L3/L4 consumption API that seeds queries by source location (slice_backward("src/util.ts:42"), flows-to, def-use) relies on cache invalidation to keep line:col honest. With this bug, a stale cache returns vertices pointing at the OLD line numbers — a silent wrong-answer, the worst failure mode for slice/flow evidence. This blocks the "staleness is handled by cache invalidation" assumption the L3/L4 API design rests on.

RELATED

#177 (cache not invalidated when tsc_only/resolver mode changes) is a SPECIAL CASE of this same root cause (no freshness check at :168) — it should close with this fix or be folded in.

FIX DIRECTION

  • Gate needs_run on a content signal, not mere file existence: compare a hash/mtime of the project's TS sources (or the top-level content_hash cants already emits per module) against what produced the cached analysis.json; re-run when they differ. Or: always invoke the analyzer and let cants' content-hash cache decide (parity with the Python path) — measure the cold-vs-warm cost first, since that removes the SDK-level short-circuit.
  • Either way: editing a source file and re-analyzing with eager=False must yield fresh results.

DEFINITION OF DONE

Blocks the schema-v2 L3/L4 consumption API design (epic #238, F6 #244); precondition for location-seeded slice/flow queries.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions