Summary
The cached analysis.json is not invalidated when tsc_only changes. The flag materially changes analyzer output (the tsc resolver produces a different call graph and an empty synthesized_callables, vs. the Jelly default), but it is not part of the needs_run cache-bust predicate. Toggling tsc_only against the same project silently returns the stale graph recorded under the previous mode.
Found while reviewing the tsc_only work on fix/issue-174 (#174).
Where
cldk/analysis/typescript/codeanalyzer/codeanalyzer.py:168:
needs_run = self.eager_analysis or not analysis_json_file.exists() or bool(self.target_files)
bool(self.target_files) is in the predicate precisely because it changes what the analyzer produces; self.tsc_only has the same property but is not included. The cache file is a fixed analysis.json with no encoding of the resolver mode it was produced under (no sidecar manifest, no fingerprint). This is not an opt-in path — typescript_analysis.py resolves a default cache dir (<project>/.codeanalyzer/typescript) even when cache_dir=None, so every TypeScript run hits it.
Reproduce
CLDK.typescript(project_path=P, backend=TSCodeAnalyzerConfig(tsc_only=False)) → first run, no cache, binary runs with the Jelly resolver, writes analysis.json with anonymous-callback edges and a populated synthesized_callables.
CLDK.typescript(project_path=P, backend=TSCodeAnalyzerConfig(tsc_only=True)) against the same project → needs_run evaluates to False, the binary is never invoked, and the stale Jelly-produced analysis.json is re-hydrated. The caller asked to suppress Jelly resolution but gets Jelly edges and a non-empty synthesized_callables anyway.
The reverse direction is symmetric: a tsc-only cache served to a caller who asked for the default Jelly resolution. No error, no warning.
Fix
Any of:
- Include the resolver mode in the predicate via a recorded prior value (e.g. an
analysis.json.manifest sidecar of the output-affecting flags; force a re-run on mismatch).
- Namespace the cache file by mode (
analysis.json for Jelly, analysis.tsc_only.json for tsc-only) so the two modes cache independently.
- Hash the output-affecting args into the cache file name.
Related
The pre-existing analysis_level parameter has the same gap — it's in the CLI args but not in needs_run. Worth addressing together; the existing inconsistency is what makes adding another mode knob a footgun.
Summary
The cached
analysis.jsonis not invalidated whentsc_onlychanges. The flag materially changes analyzer output (the tsc resolver produces a different call graph and an emptysynthesized_callables, vs. the Jelly default), but it is not part of theneeds_runcache-bust predicate. Togglingtsc_onlyagainst the same project silently returns the stale graph recorded under the previous mode.Found while reviewing the
tsc_onlywork onfix/issue-174(#174).Where
cldk/analysis/typescript/codeanalyzer/codeanalyzer.py:168:bool(self.target_files)is in the predicate precisely because it changes what the analyzer produces;self.tsc_onlyhas the same property but is not included. The cache file is a fixedanalysis.jsonwith no encoding of the resolver mode it was produced under (no sidecar manifest, no fingerprint). This is not an opt-in path —typescript_analysis.pyresolves a default cache dir (<project>/.codeanalyzer/typescript) even whencache_dir=None, so every TypeScript run hits it.Reproduce
CLDK.typescript(project_path=P, backend=TSCodeAnalyzerConfig(tsc_only=False))→ first run, no cache, binary runs with the Jelly resolver, writesanalysis.jsonwith anonymous-callback edges and a populatedsynthesized_callables.CLDK.typescript(project_path=P, backend=TSCodeAnalyzerConfig(tsc_only=True))against the same project →needs_runevaluates toFalse, the binary is never invoked, and the stale Jelly-producedanalysis.jsonis re-hydrated. The caller asked to suppress Jelly resolution but gets Jelly edges and a non-emptysynthesized_callablesanyway.The reverse direction is symmetric: a tsc-only cache served to a caller who asked for the default Jelly resolution. No error, no warning.
Fix
Any of:
analysis.json.manifestsidecar of the output-affecting flags; force a re-run on mismatch).analysis.jsonfor Jelly,analysis.tsc_only.jsonfor tsc-only) so the two modes cache independently.Related
The pre-existing
analysis_levelparameter has the same gap — it's in the CLI args but not inneeds_run. Worth addressing together; the existing inconsistency is what makes adding another mode knob a footgun.