A code-to-column change-impact knowledge graph for AI coding agents.
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.
cascade <command> --help (or -h) prints just that command’s section and
exits 0, before any other flag on the line is read and before anything is
resolved, discovered or written.
cascade --version prints the installed package version and exits 0
without creating a registry, cache or project files. Whole-program help
(cascade --help, -h, help, or no arguments) prints usage to stderr and
intentionally exits 2. See installation and recovery.
| 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 |
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 codeQuery the same persistent code-structure tools served over MCP. Output is a JSON response including basis, trust, limits and pagination information.
cascade code <briefing|architecture|search|outline|trace|context|snippet|status> [--root <dir> | --project <id> | --pack <dir>]
| Action | Options |
|---|---|
briefing |
--depth N, --limit N, --max-bytes N |
architecture |
--depth N, --limit N, --offset N, --max-bytes N |
search |
--query <text>, --kind all\|file\|declaration, --limit N, --offset N |
outline |
--file <indexed-path>, --limit N, --offset N |
trace |
--symbol <declaration-id>, --direction inbound\|outbound\|both, --depth N, --limit N, --offset N, --max-bytes N |
context |
--query <text>, --max-bytes N, --limit N |
snippet |
--symbol <declaration-id> or --file <indexed-path>; --from-line N, --to-line N, --max-bytes N, --max-lines N |
status |
Show current index/refresh status without triggering analysis |
Source-backed queries automatically refresh saved changes before answering.
--no-watch reads the existing snapshot instead, with unknown freshness.
The same flag disables automatic analysis for cascade mcp and cascade view.
An unavailable source tree remains a disconnected snapshot. Failed refreshes
are errors rather than silently current answers. briefing, architecture and context budgets count UTF-8
bytes of answer JSON, and snippet budgets source-text bytes; neither is a
model-token count. See code intelligence for scope and
the difference between structure and graded impact.
cascade setupcascade 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 and checking its exact pinned version with the interpreter
a run will actually use rather than trusting that pip said ok.
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 |
--force — reinstall a managed environment even when its parser already matches
the pin. A missing, broken or mismatched parser is repaired without this flag.
Replacements retain a backup until verification succeeds and roll back on
failure; registry, cache and other data outside the venv are preserved.--home — build in the tool home even inside a checkout.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. When CASCADE_PYTHON is set, setup only validates that external
interpreter. It never installs into or rebuilds it, even with --force. An exact
parser pin succeeds; a missing or mismatched parser fails with installation
instructions, or guidance to unset the override and use a managed environment.
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 doctorcascade 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. The SQL parser check requires the exact version in
adapters/sql/requirements.txt; merely importing another version is not enough.
--json — the whole report as cascade:doctor:1 instead of the table.Exit 0 only when every required prerequisite is ok. The optional lines
(DB drivers, Docker) are reported and never fatal.
cascade initcascade 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.
It also reads what the project says about ITSELF, out of its own Spring
configuration under resources:
service name: edge-service (from src/main/resources/application.yml)
gateway routes: 4 read from src/main/resources/application.yml
/api/order -> / at orders-service
The name goes to profile.serviceNames (it is how two projects serving the same
path are told apart when one calls the other), the routes to
profile.gatewayRoutes. Both are written only into a profile that has none of
its own: a map you typed is yours, and a re-run with --force says how many
routes it found and did not apply. See
the web lane.
A frontend with no package.json gets a line of its own, and the way to
switch it off is in the same sentence:
frontend without a package: reading src/main/resources/static/scripts (22 file(s), router angular-router). Set webRoots to [] in the profile to stop
That is a directory of frontend sources with no manifest above them that the
tree says is served: under static, public, webapp, www or
resources/templates, or named by an index.html beside it. It goes to
profile.webRoots, which cascade analyze then reads with no flag. Like the
two keys above, a list already in the profile is yours, an empty one included.
See the web lane.
A server-rendered application gets one too. Where a view name becomes a page, with the engine that renders it and the suffix the resolver appends:
template roots: 1 (config) src/main/resources/templates freemarker .ftl
It goes to profile.templateRoots, which cascade analyze then reads with no
flag, and the same rule holds: a list already in the profile is yours, an empty
one included. analyze prints the roots it will read before any lane starts:
template roots 1 (profile): src/main/resources/templates freemarker .ftl
See the web lane.
A NestJS application is found by its bootstrap: a TypeScript file under a
package that depends on @nestjs/core and calls NestFactory.create. The one
it finds goes to profile.tsBackend.app with the nestjs framework pack, and
the database its schema.prisma names (provider = "postgresql") to
profile.sqlDialects.main. Two applications write none, and say which it
found: a pack reads one. See the TypeScript lane.
A path prefix Spring configuration code sets cannot be discovered, so a
pathPrefixes list you declared is kept when init runs again, --force
included. See the Java lane.
--root <dir> — the tree to discover (default: the current directory).--project <id> — the id to register it under (default: derived from the
directory name).--force — overwrite an existing manifest/profile. Without it, a second run
keeps your edits.--json — the whole discovery report as JSON.cascade agentcascade 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 trusted-project .codex/config.toml or ~/.codex/config.toml |
<root>/AGENTS.md, a managed block |
all |
the three above |
--client <name> — one of claude-code, cursor, codex, or all
(default: claude-code).--write — write the files. Without it the command prints every file it
would write, each under a --- <relative path> --- header, with the exact
content, and touches nothing.--project <id> — the registered project to configure, instead of the one at
--root.--root <dir> — the project directory to write into (default: the current
directory).The generated entry pins the selected registry directory in env.CASCADE_HOME,
so a client launched from another environment still finds the same project.
It does not copy other environment variables or credentials. Codex supports
project-scoped MCP configuration
for trusted projects; the command prints a block for you to merge.
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 analyzecascade 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]
[--ts-src <dir> | --no-ts]
[--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.
Beside the pack the run also writes routes.json, a small sorted index of
what this project serves and what it calls and does not serve, and prints one
line for it (routes index: 8 served, 0 outbound). A server that holds several
projects reads those sidecars to join one project’s outbound call to another
project’s route (see mcp.md); nothing else reads it, and it is derived
from the pack rather than an input to it, so the pack digest is unchanged by it.
A run is incremental whenever a previous facts-index.json and its shards
both apply; otherwise it is cold and says why.
--root <repo> — the tree to analyze (default: the current directory).--out <dir> — where the pack goes (default: the resolved .cascade/pack).--profile <f> — a profile file other than <dotCascade>/profile.json.--cold — recompute everything.--incremental — ask for an incremental run. One that cannot be incremental
still runs, and still says why it could not.--accept-baseline — re-seal the calibration baseline from this run. The
one override there is, and a human decision.--ddl <schema.sql|glob> — a DDL file the catalog is parsed from. Repeatable,
and each value may be a glob (* inside a path segment, ** across them):
a schema split one file per service is --ddl a.sql --ddl b.sql --ddl c.sql
or --ddl 'svc-*/db/mysql/schema.sql'. The files are applied in the order
given, so a base schema followed by its migrations reads as the history it
is: CREATE TABLE declares; ALTER TABLE ADD/DROP/MODIFY/CHANGE/RENAME
COLUMN, ALTER COLUMN SET/DROP NOT NULL and TYPE, adding or dropping a
primary key, and RENAME TABLE amend; and two files declaring the same table
are reported (DUPLICATE_TABLE_DECLARATION) with the first kept, never
merged. A CREATE TABLE the grammar cannot read is read again with what the
catalog does not hold set aside, or named (create_table_unreadable,
create_table_unread;
sql-lane.md). An
ALTER clause not applied is named (alter_clause_unsupported,
alter_unreadable). Each ALTER is read by the rules of the database
sqlDialects.main names, and with none declared by MySQL’s, each conclusion
that rests on one of them saying so (alter_rule_assumed); where that database
has no rule a statement depends on, the statement keeps what the catalog had
and says so (alter_primary_key_unknown, alter_modify_unsaid_unknown;
sql-lane.md).
What the reader said is in the pack, one diagnostic per kind with its count
(meta.laneStats.catalog), and the overview counts it into its catalog gaps.
With no --ddl at all, discovery classifies every .sql it found by
dialect (from the path — db/mysql/…, schema_h2.sql — and otherwise from
spellings only one database has) and by role (schema when it declares
tables, migration when it changes more than it declares or sits under a
flyway/liquibase/migration/upgrade/patch path). The default is every
schema file of the project’s dialect, in path order; migrations and files
under src/test/ are not applied. Every file is printed with the reason it
was applied or left out, and naming one with --ddl overrides the lot.
--no-ddl — run with no DB catalog at all (the column axis then declares
itself degraded). It overrides --ddl, the profile and the classification.--mappers <dir> — a MyBatis mapper directory; repeat for several.--no-mappers — no SQL statements (a schema with nothing over it).--java-src <dir> — a Java source root; repeat for several. An unflagged run
reads main sources only and prints the src/test roots it skipped;
passing one here includes it.--no-java — a SQL-only pack.--web-src <dir> — a frontend source root; repeat for several. An unflagged
run reads the roots discovery found, but only when the profile declares the
web framework pack. The lane traces every HTTP call to the client that sends
it, derives the prefix that client goes through, matches the URL against the
routes this pack knows, and adds a graded CALLS_HTTP edge from the frontend
function to the endpoint. The web axis says what had to be guessed. Its facts
are cached per file, so a second run re-reads only what changed. A frontend in
a repository of its own, beside the root or nested in it, is part of the pin:
its commit is in it, so a frontend at another commit is a REPIN, and its
uncommitted files are the pack’s dirty files (meta.base.dirtyFiles).
See the web lane setup page.--no-web — do not read the frontend even when the profile declares it.--ts-src <dir> — the root of a NestJS application. The TypeScript lane reads
the routes the controllers its modules register serve (under the global
prefix and URI version the bootstrap sets), the calls between its methods,
and every Prisma or TypeORM call as a statement of its own, against the
application’s schema.prisma or its TypeORM entities. It also reads the files
the application’s imports reach elsewhere in --root (a monorepo’s shared
library), never node_modules. One application per pack: a second root is
named and left out. An unflagged run reads the profile’s tsBackend.app when the profile
declares the nestjs framework pack, which cascade init writes when it
finds one application. A frontend root around it leaves its files to this
lane. See the TypeScript lane setup page.--no-ts — do not read the TypeScript backend even when the profile declares it.--openapi <file> — an OpenAPI 3 or Swagger 2 document, JSON or YAML; repeat
for several. Every (method, path) it declares becomes an endpoint with the
same id the Java lane would give it: a route the code also serves is
corroborated (the node gains declaredBy, and the operationId and
summary when the document carries them), and a route nothing here serves is
added with no handler edge, because a declaration says a route exists and
says nothing about what runs below it. The two drift lists — declared and not
served, served and not declared — are on meta.laneStats.openapi and in the
overview’s openapi-drift gap. Two readings give a declared route a handler
that no mapping annotation names: a Spring controller that implements the
interface openapi-generator writes from the document, paired by the
generator’s naming and graded HEURISTIC (rules.md), and a Spring
functional route whose prefix code elsewhere composes, placed where the
document declares the operation id it names and graded HEURISTIC too
(the Java lane). With no flag, the
documents come from the profile’s openapi.documents, and failing that from
discovery.--no-openapi — read no document even when the profile or discovery names one.--har <file> — a browser recording (HAR 1.2, what Chrome DevTools saves from
the Network panel); repeat for several. Every request in it whose path matches
a route this pack serves becomes a screen --CALLS_HTTP--> endpoint edge
graded RUNTIME_ONLY, which is below every query mode’s floor: it is
shown (observed: true on the screen, the route and every row that names
them) and never walked. A recording never raises a grade and never
replaces the static reading; where a page it saw matches no screen the source
declares, a screen is added with source: "har" and no RENDERS edge.
Requests that match no route are counted by path, static assets are counted
apart, and neither is dropped. With no flag the recordings come from the
profile’s runtimeEvidence.har (manifest-relative). There is no discovery
step: a recording is something you made on purpose, and picking one up
because it happens to be in the tree would let an unrelated capture decide what
this pack claims was observed. See the web lane setup page.--otel <file> — an OpenTelemetry trace export (OTLP/JSON, the resourceSpans
shape); repeat for several. A trace says which concrete implementation
handled a request and which statement it ran, which is the one thing no
reading of the source can decide. Where the trace confirms a hop this analysis
already had, that edge keeps its grade and gains observed: true with a count
beside it, so a candidate set the code could only grade SOUND_SET now names
the member that really ran without being promoted. A candidate the trace
did not visit is left exactly where it was: absence of observation is not
absence of the path. A hop the trace saw and no static rule explains becomes a
MAY_CALL edge graded RUNTIME_ONLY, which is below every query mode’s
floor, so it is shown and never walked. No SQL text and no bound parameter
enters the pack: a statement’s SQL is read for the table names it touched
and then dropped. With no flag the traces come from the profile’s
runtimeEvidence.otel (manifest-relative), and there is no discovery step,
for the same reason recordings have none. See
the runtime evidence page.Each --no-<lane> overrides whatever the manifest, profile or discovery would
otherwise have supplied, so “run without this” is always expressible.
--otel reads either an OTLP/JSON document or the application log the
OpenTelemetry Java agent writes with -Dotel.traces.exporter=logging-otlp: one
export per line, behind the logger’s own prefix. Which one it is is decided by
reading the file, never by its extension, and a line the reader cannot use is
skipped and counted rather than fatal. A log is reported as one:
Runtime evidence: app.log was read as an agent log, one export per line: 169 span(s) in it, 54 line(s) carried none
cascade otel-methodscascade otel-methods [--pack <dir> | --project <id> | --root <dir>] [--json]
Print the otel.instrumentation.methods.include value this pack needs, so the
OpenTelemetry Java agent emits the method spans the dispatch join reads.
Out of the box the agent writes HTTP server spans, repository spans and JDBC
spans, so a first capture observes routes and statements and reports dispatch
0: no controller and no service method has a span, so no method span ever
nests inside another one. The agent can add them, and it wants explicit
method names — pkg.Class[m1,m2]. A wildcard is not a name: pkg.Class[*]
matches nothing and the next capture is as empty as the first.
What goes on the list is read off the pack: every route handler (a HANDLES
target) and every symbol that reaches a statement (the method that
implements one, and everything with a MAY_CALL path down to it). A symbol
marked external is left off, because a library method is not yours to
instrument.
$ cascade otel-methods --project petclinic
org.springframework.samples.petclinic.owner.OwnerController[findOwner,findPaginatedForOwnersLastName,…];org.springframework.samples.petclinic.owner.OwnerRepository[findById,…];…
32 method(s) in 10 class(es): 17 route handler(s) and 24 method(s) that reach a statement. …
The value goes to stdout on its own, so it can be pasted or piped; the count and the instruction go to stderr, where they cannot get into a pipe. Classes are sorted and so are the methods inside each one, so the same pack prints the same line every time.
--pack / --project / --root — which pack (see Finding the project).--json — the same list as { "pkg.Class": ["m1", "m2"] }.The recipe this belongs to is on the runtime evidence page.
cascade estimatecascade 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
--root <dir> / --project <id> — which project (see Finding the project).--json — the raw estimate object.cascade verifycascade verify [--pack <dir> | --project <id> | --root <dir>] [--json]
Recompute every digest in .cascade/receipt.json from the files on disk, check
that the fact index names the pack beside it, 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.
--pack / --project / --root — which project.--json — the verification result as JSON.cascade goldencascade golden <propose|approve|seal|check> [--pack <dir> | --project <id> | --root <dir>]
[--per-relation N] [--from-otel <trace file>] [--ids <id>…] [--all] [--json]
The project’s golden corpus. The split is the point: the tool proposes, a human approves.
propose — suggest candidate cases from the current pack. Writes candidates
and nothing else.propose --from-otel <trace> — propose cases from an OpenTelemetry trace
instead. Repeatable, and the file may be an OTLP/JSON document or the Java
agent’s own log.approve — make candidates evidence. Requires --ids <id>… or an explicit
--all; the tool never approves itself.seal — hash the approved set, deciding which cases are held out.check — score the approved cases through the shipped MCP tools.Flags: --per-relation N, --from-otel <file>, --ids <id>…, --all,
--json, plus --pack / --project / --root.
A proposal sampled from the pack is right by construction: the engine wrote both
the question and the answer, so it proves nothing until somebody reads it. That
is why almost nobody ever does, and why almost every project is UNCERTIFIED.
--from-otel breaks that loop by taking the labels from somewhere else: the
running program. For every route the trace exercised, the case says the tables of
every statement that ran under that request; for every method that ran SQL, it
says the statement the pack keys by that method. A route or a method this pack
does not know stays out and is counted in one line, so nothing is invented.
Two things follow from what execution can and cannot witness, and both are on
the case itself (source: "runtime"):
absent ids and scores recall alone. It can show the answer
covered what ran; it can never show precision.PASS where the pack answers no table either (two independent sources
agreeing that this route reads nothing), and UNSCORABLE where the pack answers
tables the run never touched, because the request may not have taken that
branch. check prints how many cases of each relation assert nothing and how
many could not be scored, and only the scored ones count towards covering a
relation’s population.--all is the expected path here. A human still stamps the approval, but
what they are agreeing to is that the recording is a fair one, not that the
analyzer was right. Reading each case one at a time buys nothing, because the
engine did not write these labels.cascade otel-methods prints the instrument list a Java agent needs before a
trace can see method spans at all; docs/setup/runtime-evidence.md is the whole
capture story.
cascade catalog discovercascade catalog discover [--project <id>|--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.
--root <dir> — the tree to read.--json — the candidates as JSON.cascade catalog fetchcascade 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/, then write
catalog.source: "jdbc" into the profile so the next analyze reads it.
It prints the exact target first and asks y/N in a terminal; outside one
(a script, CI, a pipe) it refuses without --yes. The confirmation stays
because the connection info came out of the analyzed repository, which is
untrusted input, and a saved password for that host does not make the host
trustworthy.
The password is looked up in this order, and is never a command-line argument
(ps would show it):
--password-env NAME;CASCADE_DB_PASSWORD;$CASCADE_HOME/credentials
(see cascade catalog credentials);Analysis itself never connects; it reads the pinned snapshot, so a pack stays reproducible against a database that keeps moving.
--candidate <n> — one of the candidates catalog discover listed.--url <jdbc url> --user <u> — or name the target by URL.--dialect <d> --host <h> --port <p> --database <db> --user <u> — or
spell it out.--password-env NAME — the environment variable holding the password.--schema NAME — the schema to read.--stamp-schema NAME — the schema name to stamp on the records.--yes — confirm the exact target without being asked. Required outside a
terminal; nothing connects without it there.--project / --root — which project the snapshot belongs to.cascade catalog credentialscascade catalog credentials list
cascade catalog credentials set --url <jdbc url> --user <u> [--password-env NAME]
cascade catalog credentials remove --url <jdbc url> --user <u>
The passwords catalog fetch may use. They live in $CASCADE_HOME/credentials
(~/.cascade/credentials by default) as one JSON object per line, keyed by
server and user, at mode 0600 — the same shape and the same rule as
~/.pgpass. A file that group or others can read is refused, with the
chmod 600 to run: a password anyone on the machine can open is not a secret.
The file is never written under a project tree. If CASCADE_HOME resolves
inside the project being analyzed, the command refuses and says why.
list — the servers and users held. Never a password.set — store one. The password comes from --password-env NAME, else from
CASCADE_DB_PASSWORD, else from a hidden prompt in a terminal.remove — delete exactly one entry.--dialect <d> --host <h> --port <p> --database <db> --user <u> —
name the same target field by field instead of as a URL.cascade packcascade 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.
--catalog <f> — the catalog JSONL.--lineage <f> — the lineage JSONL.--out <dir> — where pack.json is written.--project NAME — the project name stamped in the pack meta.cascade mcpcascade 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.
--project <id> — repeat to narrow the served set.--pack <dir> / --root <dir> — serve one pack.--memory-budget <MB> — MB of pack JSON held in memory (default 512).cascade impactcascade 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, plus the files the pack itself read uncommitted, and, for a frontend in a repository of its own, the files that differ from that repository’s HEAD) 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.
--file <path> — restrict the question to these files; repeat for several.--verbose — also print the reused shard count, the dirty document hashes and
the files that were parsed.--mode strict|conservative|heuristic — which edge grades the walk may use.--mode base-only — answer from the pack alone: what these files touched as
they were last analyzed, labelled as such.--pack / --project / --root — which project.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.
With nothing to ask about, the command says why rather than “no changed
files” alone. When the overlay was not laid (declined, or its base commit is
stale), it prints overlay NOT applied (<state>): <reason> and how to ask
anyway (--file, or --mode base-only); on a clean tree it prints
no changed files. Pass --file <path> [--file …], or edit the repo the pack was built from.
Both go to stderr and exit 2. With files to ask about and an overlay that was
declined, the answer goes on from the base pack, under the same
overlay NOT applied line.
The overlay builds from the inputs the pack was built from, so a change that no diff of the analyzed root reports never reads as your edit. An input that moved since is declined, behind or said, never read again silently:
pathPrefixes, gatewayRoutes entry or SQL convention alike, on a
clean tree too;analyze records the commit it read it at, and one that moved on makes
the answer behind, as a moved HEAD does. That commit is in the pack’s pin, so
a rerun with the frontend at another commit is a REPIN for the calibration
gate, and the repository’s uncommitted files are the pack’s dirty files, which
the overlay always reads again: an edit you then undo is seen as undone. A
frontend outside the analyzed root that is in no repository is said in
limits;mappers.alternatives, tsBackend.prismaSchema and tsBackend.app are read
against the directory the run resolved them in (the project’s .cascade), not
the pack’s own.A long-running server keys its overlay on those inputs as they are now, so it never answers from one built over the old ones.
The overlay reads the OpenAPI documents the pack read, as they are on disk now,
so a document’s routes and the handlers a rule linked to them are still there;
an edited document is an edit to the routes it declares, and the answer names
the routes the edited files gave the pack that the overlay no longer has
(overlay.removedIds.endpoints), which the command line prints after its
timings line (routes the edit removed (<n>):, one id per line). It reads the catalog as analyze did (the
same DDL files with their dialect, or the snapshot), and declines when any of
those files is edited. It hands the web lane the frontend packages and server
ports the pack read, so with no edit it answers as the pack does. What it does
not read again it says in the answer: the table id generators a Spring XML
declares, when the pack bound any, an edited package.json near a frontend,
and, when the pack read ports, an edited Spring configuration, a file the pack
read a port from or named as the reason a port is unknown, any other
.properties, .yml or .yaml file outside the test tree that now states a
port, or a Java source that now loads configuration or sets the port in code: the overlay keeps the
packages and ports as the pack read them. A pack that reads a TypeScript backend is
overlaid too: the edited files are read again, the imports followed as they
are now, and the tsconfig, schema.prisma and package.json files read again
(see the TypeScript lane). The overlay line counts the
TypeScript files it read again beside the Java and frontend ones, and the
timings add ts. Only a fact index an older engine wrote declines with
ts-not-overlaid; --mode base-only still answers.
cascade viewcascade 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.
--port 4319 — the port to bind (default 4319).--project <id> — repeat to narrow the served set.--pack <dir> / --root <dir> — serve one pack.--memory-budget <MB> — MB of pack JSON held in memory (default 512).cascade exportcascade export [--pack <dir> | --project <id> | --root <dir>] [--tab flow|impact]
(--endpoint|--screen|--symbol|--table|--column|--statement) <name>
[--mode strict|conservative|heuristic] [--depth 1-8] [--limit <n>] [--lang en|ko] [--format html|svg|card] [--out <file>]
Write one Flow or Impact answer as one HTML file. The file is the viewer’s own page with the answer inside, so it opens in any browser with no server and no network, and it draws the same grades, limits and cut lists the live page does. The viewer’s Export button writes the same file for the same question. See viewer.md.
--tab flow|impact — which picture: flow walks down from the entry, impact
walks up to it (default flow). The viewer’s Trace saves a walk down as a Flow
file and a walk up as an Impact one.--endpoint, --screen, --symbol start a Flow picture; --table, --column,
--statement, --symbol and --endpoint start an Impact one. An endpoint
walked up answers “where is this API used”: the frontend functions that call
it and the screens that render them. Give exactly one.--mode strict|conservative|heuristic — which edge grades the walk may use
(default conservative).--depth 1-8 — how many calls deep. Left out (the default), the walk follows
every call, the one rule every walk uses, as Trace opens; a number narrows it.--limit <n> — rows per lane before the list is cut (default 40). A cut list
says so in the file.--format html|svg|card — html (default) writes the page with the answer inside;
svg writes one picture of the same answer for a slide or a document, with
every row, grade dash, cut and limit drawn in it. The viewer turns the same SVG
into a PNG with its PNG button. card writes the answer at a glance, as a
.card.svg: what was asked about at the top, then one layer per lane with its
whole total, a few names in the answer’s order, a badge per grade and a rail
dashed by its weakest row. A cut layer says its grades cover only the rows
shown, a walk that stopped early says so, and every limit is written out word
for word. The card carries no export time, so exporting it again from the same
pack writes the same bytes, which is what lets a README keep one.--lang en|ko — the language of the page’s own words. The engine’s words
(grades, limits, reasons) are never translated.--out <file> — where to write; default cascade-<project>-<tab>-<entry>.html
in the current directory.--pack / --project / --root — which project.The command prints the trust level and how many limits and cut lists the answer carries, because a file handed to someone else is read without the screen it came from.
cascade diffcascade diff (--base-commit <rev> | --base <pack>) [--pack <dir> | --project <id> | --root <dir>] [--head <pack>]
[--limit <n>] [--json]
What changed in one project since an earlier commit, for a pull request or a release:
the routes, screens, tables, columns, statements and symbols that appeared or
went away, the edges that appeared, went away or changed grade, and the
endpoints and screens above any of that. A frontend change is above no endpoint,
which is why the screens are listed too. Node ids are meanings (endpoint:GET /x,
statement:ns.id), so the two packs are compared by id with no guessing.
The report also separates an existing record whose content changed from one
whose source location alone moved. For nodes, content includes every recorded
nonidentity attribute other than file, line and declaredAt: a column type,
access-related attribute or @Transactional marker can therefore be reviewed
without pretending it was an addition. For edges, grade-only change remains a
regrade; changed evidence (such as access) is reported with its complete
base/head record group, including duplicate records, while source-location-only
evidence is a move. Each changed field prints its exact base and head values;
<missing> is different from null. Content changes, additions, removals and
regrades seed the static upstream walk on both packs. A location-only move does
not, so it cannot enlarge the affected endpoint or screen list. That walk follows
the recorded relation graph on each side, including candidate-grade edges: its
endpoints and screens are review candidates, not proof that a request ran or
that behaviour changed.
--base-commit <rev> — this project at another commit (main, HEAD~3, a
sha). When the project’s pack history holds a build at that commit that was
made from a clean checkout AND analyzed the same way as the current pack, it
is used as it is. Otherwise the commit is checked out in a temporary git
worktree, given this project’s manifest and the profile the current pack was
analyzed with, and analyzed with the very lane flags the current pack recorded
(--ddl, --mappers, --no-java and the rest; a pack records each flag as
the file it named, relative to the project when it is in the repository, so a
flag typed from another directory replays the same file), into a scratch directory:
nothing is registered or sealed, and the worktree is removed afterwards (a
removal that fails is an error that says what to clean). A path inside the
repository, in the profile or in a flag, is read at that commit; one outside it
(a frontend checked out beside the repository, a DB snapshot in
.cascade/catalog) has no older version, is read as it is today, and the
output lists every such path. A profile the current pack did not read is not
read at the base either, even when that commit tracks one. A current pack that does not record how it was
analyzed (built before this release) is refused: run cascade analyze once.
So is one whose inputs outside the repository have changed on disk since it
was analyzed, because a kept base would carry their old content and a rebuilt
one their new: run cascade analyze, then compare. The commit is looked up in
the checkout the .cascade sits in (a copied project uses its own), or in the
tree the current pack recorded when only that one is the repository the pack
was built from (analyze --root elsewhere). A shallow clone may not hold the commit, and the command says to fetch it.--base <pack> — a pack on disk instead: a pack.json, the directory holding
it, or a .cascade directory.--head <pack> — the pack with the change; default this project’s pack.--pack / --project / --root — which project.--limit <n> — how many ids each list prints (default 50). The counts are
always whole, and every cut list says how many of its own rows it left out.--json — the whole difference as JSON. The additive response schema remains
cascade:pack-diff:1; comparisonVersion: 2 identifies the expanded record
comparator, while clients that read the older fields can continue to do so.Packs are checked before comparison. diff verifies the pack schema, graph
records and content digest for the base and head before it prints any count. The
digest covers graph nodes and edges, not execution metadata, so changing
meta.builtAt does not make a graph change and is not authenticated by this
check.
Two different repositories are refused. Their difference is everything, and
a list of a thousand added routes reads like a review while meaning nothing. Each
pack records where it was built (meta.base): the commit its history starts
from (not for a shallow clone, whose oldest commit is only where the clone was
cut), the origin remote without credentials, the project’s folder inside the
repository, and the checkout path. The strongest evidence both packs carry
decides. Two projects in folders of one repository (a monorepo) share a root
commit and a remote, and are told apart by the folder. Remotes agree across
https, ssh and scp forms and a default port; another port is another
server, and the path keeps its letter case except on GitHub, GitLab and
Bitbucket. For packs built before these were recorded, two different project
ids are two projects; an equal id alone proves nothing, and the diff says the
repository is unknown.
The pack history. Every certified analyze copies the pack it replaces into
.cascade/history/, one directory per build, and keeps the five most recent. A
rebuild that changed nothing (same commit, same digest) keeps nothing. A kept
pack whose nodes and edges no longer match its digest is not used. The copy
is made under the project’s write lock, which a run takes before its gate reads
the sealed baseline and holds until its receipt is written; then the fact index and the route index
are renamed into place, each naming the digest of the pack it belongs to, then
the pack; the gate’s verdict, the baseline and the receipt follow the pack and
are written before the lock is released, so a pack that failed to publish is
never certified and two analyses of one project (a rejected one included) never
interleave. A reader that finds a sidecar written for another build refuses it
rather than reading it with the wrong pack, and a server reads a project again
when its pack, indexes, verdict, golden corpus, profile or manifest change. The
history is pruned after that, even when certifying failed, so the pack being
served is always the old one or the new one. The lock (.cascade/.write.lock,
one per project whichever --out a certified run writes) makes a second run
wait, saying for which process, for up to ten minutes. It is never broken
automatically, because two runs that both judged it abandoned would both
publish: a lock whose process is not running on this machine (an analyze that
was killed) is refused at once, with its process id, and is removed by hand. A build made with uncommitted edits is kept
but never chosen by its commit. The viewer’s Compare and the MCP tool
pack_diff { base_commit } choose their base from here, and a server notices a
pack republished under it and reads the new one.
The conditions come first. The same code read by a newer worker, under
another profile, with a lane or an axis missing, gives a different pack too. So
the two packs’ lanes, identity rule, axes, worker versions, profile digest,
engine, opt-out flags, lane flags, source roots, catalog snapshot, runtime
evidence files and the content of every input outside the repository (a
frontend checked out beside it changes with no commit, so its path alone says
nothing; its package configuration above the source root, .env* and the
rest, counts too) are compared before anything is counted (two packs that both
leave one of these unrecorded are not taken to agree on it),
and every one that differs is printed. A removed node whose axis changed between
the packs is marked, because an unread catalog and a dropped table look alike in
a list of ids. A pack built before 0.8.8 records no workers, profile or engine,
and the diff then says those conditions are unknown rather than assuming them
equal.
A renamed method is one removal and one addition. This is a comparison of what the two static graph packs record: it does not capture every source/body change, prove runtime behaviour or judge whether a change is safe. The Compare page shows the same report with print and Markdown-download actions; conditions and cut-list totals remain part of that report.
The MCP tool pack_diff answers the same question from the project’s pack
history (mcp.md).
cascade rulescascade rules list | show <rule id> | test [<rule id> | <pack>] | explain <type> [--json]
The rule packs the engine reads. Knowledge of frameworks, such as which word in a path names which database, is moving out of the engine’s code into packs a person can read, and every rule carries the examples that hold it. See rules.md for the format.
list — every pack and rule, with how many examples each carries.show <rule id> — one rule whole: what it means, why it is there, its params
and its examples.test [<rule id> | <pack>] — run the examples of every rule, or of one rule or
one pack. Exits 1 when an example does not hold, or when nothing is named by
the argument, and 2 when an example could not be run (a Java example needs a
JDK), so it can stand in a CI job.explain <type> — why a Java type of the project (by its full or simple name)
has a MyBatis-Plus role, or has none: each supertype its declaration names,
what the file means by that name, what each rule that names it concludes and
why, and the role the MyBatis-Plus bridge gives once it follows the whole
chain. It reads the pack’s fact cache (--root or --project picks the
pack), and a cache from another worker generation is refused with the cure.--json — the same answer for a program to read.