The CLDK TypeScript SDK (typescript-sdk/, published as @codellm-devkit/cldk,
built and tested with Bun) is the user-facing API for Node/TS consumers:
CLDK.for("<lang>").analysis({ projectPath }). Adding a language mirrors the Python SDK
move — a models tree, an analysis facade, and a dispatch branch — but in TypeScript.
Do it on a git branch (add-<lang>-support), confirming a clean tree first.
Design the surface once, in design mode. This file is the encoding mechanics for the TS side. The facade's query surface is designed with the user in
skills/designing-cldk-changes/references/sdk-facade-design-loop.md, and that one approved vocabulary (from.claude/FACADE_DECISIONS.md) is shared by both SDKs — so the TS<Lang>Analysismirrors the Python<Lang>Analysismethod-for-method, just transposed tocamelCaseand TS types. Don't re-decide the surface here.
One invocation model only. The TS SDK has no in-process backend pattern — there is no TS equivalent of importing a pip package. Every language is invoked as a subprocess (Java shells out to a bundled JAR). So even a Python-authored analyzer is reached through its CLI binary, never imported. Copy the Java facade (
src/analysis/java/,src/models/java/) as the one worked example. (Verify the exact paths against the currenttypescript-sdklayout.)
cd typescript-sdk
git checkout -b add-<lang>-support
The schema is one node-tree modeled once (schema-contract.md), so the TS side
mirrors the Python two-layer approach:
src/models/cpg/schema.ts(shared, build once) — the envelopeAnalysisPayload(schema_version,language,max_level,k_limit?,application) and the canonical typesApplication,Module,Node(single interface,kindstring, kind-specific fields optional),Edge({ src, dst, kind?, var?, prov[], weight }),Span(withbytesfor slicing). Property names match the JSON keys so a parsedanalysis.jsonsatisfies the type. Language extras are additive optional fields onNode+ an opentags.src/models/cpg/views.ts— thin view classes (CallableView,TypeView,ModuleView,CallsiteView) exposing the old field names (.code = module.source.slice(...bytes),.callSites=bodycallnodes,ModuleView.classes= kind-filter over onetypes).src/models/<lang>/index.ts— aliases only:export type TSCallable = CallableViewetc. (the remap table inschema-contract.md), plus that language's addedNodefields /kindstrings. No per-language schema tree.- Don't mirror the old per-language rich-edge split. Edges are identity-only
{ src, dst }with id endpoints; there is no<L>CallEdge/<L>Callsitetype (a call is abodycallnode; the list name is the edge type). index.ts— re-export the public model names.
TypeScript is structurally typed and erases types at runtime, so a parsed
analysis.json satisfies an interface by shape alone and an extra JSON key is silently
tolerated. Two consequences:
- Define every field you intend to read — an unmodeled field simply isn't visible;
there is no Pydantic-style validation error to catch drift, and no Go-style
nullcollection problem to coerce (JSONnulltypechecks against an optional field). - To get an actual runtime check (the TS counterpart of Pydantic validation), the
existing
src/models/java/uses a strict base withextra: "forbid"-style parsing to catch drift rather than tolerate it. Follow the repo's established convention — a runtime schema guard (e.g. a validator overschema.ts) if the Java models use one, or plain interfaces if they don't. Verify which the current repo does per SDK and match it; the goal is that a real fixture round-trips and unmodeled drift is caught.
<Lang>Analysis.ts— mirroringsrc/analysis/java/JavaAnalysis.ts. Constructor takes{ projectDir, analysisLevel, ... }; it builds the CLI args (the analyzer's documented CLI /--help), runs the binary (spawnSync/bun), reads the emittedanalysis.json, parses it into the sharedApplication, and exposes the read-only query vocabulary (Tier A verbatim; leaf accessors named for the language, incamelCase).- Binary resolution — the TS SDK can't consume the
codeanalyzer-<lang>PyPI wheel, so it resolves the self-contained binary in order:analysisBackendPath→$CODEANALYZER_<LANG>_BIN→ the GitHub Release asset at the pinned tag (downloaded to a cache on first use, or vendored undersrc/analysis/<lang>/bin/and listed inpackage.jsonfiles). This is why the analyzer'srelease.ymlpublishes raw binaries alongside the wheels. A native binary needs no runtime guard; Java needsmakeSureJavaIsInstalledonly because it ships a JAR. index.ts— export<Lang>Analysis.
Mirror the Java branch in CLDK.analysis(...):
- Import at the top:
import { <Lang>Analysis } from "./analysis/<lang>";. - Widen the return type from
JavaAnalysisto the union (JavaAnalysis | <Lang>Analysis) so the new facade is a legal return. - Dispatch branch before the
else throw:} else if (this.language === "<lang>") { // toolchain guard ONLY if the binary needs a runtime (a native binary needs none) return new <Lang>Analysis({ projectDir: projectPath, analysisLevel }); }
- The binary is the analyzer's GitHub Release asset, not a bundle the SDK builds.
Pin the release tag (download-on-first-use into a cache, or vendor under
src/analysis/<lang>/bin/and add tofiles). Either way the binary isrelease.ymloutput, not committed source. - Keep that pinned tag in lockstep with the Python SDK's
codeanalyzer-<lang>pin — both reference the same release the codeanalyzer-backend skill published. - Builds with
bun build; tests withbun test.
<lang>Analysis.test.ts— mirror the Java test. Mock the backend (stub the binary invocation to return a fixtureanalysis.json) so tests don't require the binary, then assert the symbol table is non-empty and the call graph builds with no dangling endpoints (id-keyed). Add a fixtureanalysis.jsonalongside the Java fixtures.- Full tiering (mocked / E2E / backend-contract) and what each tier may touch:
sdk-testing.md.
Same structural fact as the Python SDK: there is no shared base class. JavaAnalysis
(and your <Lang>Analysis) are independent classes mirroring each other's method names
by convention; CLDK.analysis() returns the union and callers duck-type. Reproduce the
shared vocabulary deliberately and match names/signatures to the Java facade — drift
won't be caught by the compiler across the union. A facade is a thin, read-only query
layer over the parsed Application: it runs the binary once, parses analysis.json,
and answers queries by indexing into that object or building derived graph views. Don't
stub framework/semantic methods the analyzer can't populate yet.
CLDK.for("<lang>").analysis({ projectPath: <fixture> })returns a facade whose symbol table is non-empty and whose call graph has no dangling nodes (every edge endpoint a real node id).- The public API is unchanged — every accessor keeps its name, signature, and return
type; the
<L>*types are the shared views. bun testpasses with the backend mocked.- The pinned release tag matches the Python SDK's
codeanalyzer-<lang>pin. - All changes sit on the
add-<lang>-supportbranch; summarize the diff.