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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions