Cascade

A code-to-column change-impact knowledge graph for AI coding agents.

View the Project on GitHub alexsoft-hq/Cascade

The CLI — every command and every flag

cascade is a thin shell over src/core and src/mcp. Run it as node bin/cascade.mjs <command> from the engine root (or as cascade once the bin entry is on your PATH).

Running it with no command prints the usage text. That text is the source of truth for this page: test/docs.test.mjs runs the binary, reads the commands and flags out of what it printed, and fails if any of them is missing here.

Exit codes

Code Meaning
0 the command did what it was asked
1 doctor only — a required prerequisite is missing
2 bad usage, or an input the command refuses (this is what die() exits with)
3 analyze only — the calibration gate judged the run a regression. The pack is written to <packDir>-rejected/ and the certified pack is left untouched
4 verify only — a digest, the engine identity, or the expiry disagreed

Finding the project

Four commands (analyze, mcp, view, impact, and also verify, estimate, golden) locate a project the same way, in this order:

--pack <dir>      an explicit pack directory
--project <id>    looked up in ~/.cascade/registry.json (written by `init`)
--root <dir>      that directory's .cascade/
(nothing)         the current directory's .cascade/

An unknown --project lists the ids that are registered rather than guessing one.


cascade setup

cascade setup [--force] [--home]

Build the SQL lane’s Python and install its pinned requirements, in one command, so reading a setup page is not what stands between you and your first answer. It finds a python3, creates a virtual environment where a run will look for one, installs adapters/sql/requirements.txt, and then proves it by importing sqlglot with the interpreter a run will actually use rather than trusting that pip said ok. Nothing outside that environment is touched.

Where it builds:

you are in it builds
a checkout of this repository (there is a .git) <engine>/.venv, the path the setup pages and CI already name
anything else, or --home <cascade home>/venv, which survives an upgrade that replaces the package directory

A run looks for the interpreter in three places, in this order: CASCADE_PYTHON when you set it, then the checkout’s own .venv, then the tool home. Whichever is found first is the one every command uses, and cascade doctor prints which one that is. If you already have an interpreter with sqlglot in it, point CASCADE_PYTHON at it and skip this command entirely.

The Java lane’s JDK is not something this command can supply: a JDK is not a Python package. cascade doctor names the directories it looked in.

cascade doctor

cascade doctor [--json]

Pre-flight every prerequisite at once instead of discovering them one error at a time: Node’s version, git, the SQL lane’s venv and sqlglot, a JDK — naming which candidate directory won and why each of the others did not — the three optional DB drivers, Docker, the registry file and the cache directory. Each line carries a status (ok / warn / missing) and, when it is not ok, the remedy.

Exit 0 only when every required prerequisite is ok. The optional lines (DB drivers, Docker) are reported and never fatal.

cascade init

cascade init [--root <dir>] [--project <id>] [--force] [--json]

Discover what is in a tree and write the project’s own state: .cascade/manifest.json (repositories pinned to full commits), .cascade/profile.json (the reading convention), .cascade/.gitignore (which ignores pack/ and catalog/, because they carry your SQL and your column comments), and an entry in ~/.cascade/registry.json.

It reports what it found — java/mapper/DDL counts, build tool, package prefixes, the mapper directories and Java source roots the lanes will read — and what it has no lane for: Kotlin sources and frontend packages come back as UNSUPPORTED_TECHNOLOGY diagnostics rather than being silently ignored.

cascade agent

cascade agent [--client claude-code|cursor|codex|all] [--write]
              [--project <id>] [--root <dir>]

Put the MCP server and the rule that gets it used into a project, in one command. The configuration is the easy half: an absolute path to bin/cascade.mjs, the project id out of the registry, one JSON shape per client. The rule is the half that gets forgotten, and it is the one that matters. An agent with the server attached and nothing telling it when to ask edits a mapper without a question, because nothing in its context says a question is due.

The project is the one registered for --root (default: the current directory), found by comparing the real path of <root>/.cascade against the registry, or the one --project <id> names, whose root then comes from its manifest. A root nothing is registered for exits 2 and names cascade init. No pack is needed: wiring the agent up before the first analyze is a normal thing to do.

What each client gets:

--client MCP config rules
claude-code (default) <root>/.mcp.json, key mcpServers.cascade <root>/CLAUDE.md, a managed block
cursor <root>/.cursor/mcp.json, key mcpServers.cascade <root>/.cursor/rules/cascade.mdc, the whole file, alwaysApply: true
codex not written: a [mcp_servers.cascade] TOML block is printed for ~/.codex/config.toml <root>/AGENTS.md, a managed block
all the three above  

The command in the config is always absolute: command is the running node binary and the first argument is the real path of bin/cascade.mjs, resolved through the symlink a global install leaves behind. A client starts the server from a working directory you do not control, and a relative path there is the single most common reason a client reports the server as failed.

How the merge works. A JSON config is read, parsed, and given the one key: every other key and every other server stays where it was, and the file is written back with a two-space indent and a trailing newline. A file that exists and does not parse is not touched at all. The command exits 2 naming it, with nothing written, because a tool that overwrites what it could not read has thrown away work it never looked at.

The rules block sits between <!-- cascade:begin --> and <!-- cascade:end --> on their own lines. A file that has the block gets it replaced in place, so your own text above and below it survives. A file without it gets the block after one blank line, and a file that does not exist is created holding only the block. Running the command twice is byte-identical the second time, and the unchanged line says so.

With --write, each file is reported as created, updated or unchanged.

Claude Code needs one approval. A server that arrives in a project .mcp.json sits at Pending approval until you run claude in that directory once and approve cascade when it asks, and claude mcp get cascade prints that state.

cascade analyze

cascade analyze [--root <repo>] [--out <dir>] [--profile <f>]
                [--cold | --incremental] [--accept-baseline]
                [--ddl <schema.sql|glob>... | --no-ddl]
                [--mappers <dir>... | --no-mappers]
                [--java-src <dir>... | --no-java]
                [--web-src <dir>... | --no-web]
                [--openapi <file>... | --no-openapi]
                [--har <file>...]

Run the lanes end to end and write a content-addressed pack (<packDir>/pack.json). With no lane flag the inputs come from the project itself — the DDL from the profile’s catalog.connectionFrom (one path or an ordered list) or, failing that, from the classification below; the mapper directories and Java source roots from discovery — and the run prints which lane got what, and from where.

Every lane input is optional and a missing axis is declared, not fatal: the pack records meta.axes with shipped / degraded / not-shipped and why.

A run is incremental whenever a previous facts-index.json and its shards both apply; otherwise it is cold and says why.

Each --no-<lane> overrides whatever the manifest, profile or discovery would otherwise have supplied, so “run without this” is always expressible.

cascade estimate

cascade estimate [--root <dir>] [--project <id>] [--json]

What this tree will ship, degrade, or not ship — before you analyze it. Once a pack exists it also reports the measured share of questions that can be answered EXACT.

Which tree it reads is decided exactly as analyze decides it, and the banner says which rule won: --root always, then the registered project’s own source (its manifest’s single repository, or the workspace holding several), then the current directory when nothing resolved. So estimate --project mall from anywhere describes mall, not the directory your shell happens to be in.

estimate for /path/to/mall (the registered project's manifest), project mall

cascade verify

cascade verify [--pack <dir> | --project <id> | --root <dir>] [--json]

Recompute every digest in .cascade/receipt.json from the files on disk, check the running engine against the one that signed the receipt, and refuse an expired receipt. Exit 4 on any disagreement — never a partial pass.

cascade golden

cascade golden <propose|approve|seal|check> [--pack <dir> | --project <id> | --root <dir>]

The project’s golden corpus. The split is the point: the tool proposes, a human approves.

Flags: --ids <id>…, --all, plus --pack / --project / --root.

cascade catalog discover

cascade catalog discover [--root <dir>] [--json]

List the datasource configuration a tree carries — host, port, database, dialect, and whether a password is there. No password value is read, printed or stored, and nothing is connected to.

cascade catalog fetch

cascade catalog fetch [--project <id> | --root <dir>]
                      [--candidate <n> | --url <jdbc url> --user <u>
                       | --dialect <d> --host <h> [--port <p>] --database <db> --user <u>]
                      [--password-env NAME] [--schema NAME] [--stamp-schema NAME] [--yes]

Pin a read-only catalog snapshot into .cascade/catalog/. It prints the exact target and refuses to connect without --yes: the connection info came out of the analyzed repository, which is untrusted input. The password is read only from the named environment variable — never from the command line, where ps would show it — and is never written anywhere.

Analysis itself never connects; it reads the pinned snapshot, so a pack stays reproducible against a database that keeps moving.

cascade pack

cascade pack --catalog <f> --lineage <f> --out <dir> [--project NAME]

Build a pack directly from SQL-lane outputs (catalog + lineage JSONL) — the low level under analyze, useful when you have run the workers yourself.

cascade mcp

cascade mcp [--pack <dir> | --project <id> ... | --root <dir>] [--memory-budget <MB>]

Serve the tool catalog over stdio as an MCP server. With no --pack/--root/--project it serves every registered project, lazily: a pack is parsed on the first call that needs it, and the loaded ones are held in an LRU under the memory budget.

Each tool takes an optional project argument; on a multi-project server a call without one is answered ambiguous, listing the ids. Nothing is guessed. See mcp.md.

cascade impact

cascade impact [--pack <dir> | --project <id> | --root <dir>] [--file <path>...]
               [--verbose] [--mode strict|conservative|heuristic|base-only]

The edit loop, from the shell: what did my uncommitted edits touch? By default the dirty files (the working-tree diff against the commit the pack was built from, plus untracked ones) are re-parsed on every call, so the answer describes the bytes on disk rather than the last analysis. Nothing is written — not the pack, not the fact cache.

A row marked PROVISIONAL exists only in the overlay — no certified run has seen it. After a commit the overlay is discarded and the answer is behind, naming cascade analyze as the cure.

cascade view

cascade view [--pack <dir> | --project <id> ... | --root <dir>] [--port 4319] [--memory-budget <MB>]

Serve the local web viewer over HTTP on 127.0.0.1. Same project selection as mcp; the page shows one project at a time — open it with ?project=<id> when the server serves several. See viewer.md.