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.

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.

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 code

Query 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 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 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

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 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. The SQL parser check requires the exact version in adapters/sql/requirements.txt; merely importing another version is not enough.

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.

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.

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 trusted-project .codex/config.toml or ~/.codex/config.toml <root>/AGENTS.md, a managed block
all the three above  

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 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]
                [--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.

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-methods

cascade 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.

The recipe this belongs to is on the runtime evidence page.

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 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.

cascade golden

cascade 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.

Flags: --per-relation N, --from-otel <file>, --ids <id>…, --all, --json, plus --pack / --project / --root.

Cases from a run

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"):

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 discover

cascade 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.

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/, 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):

  1. the variable named by --password-env NAME;
  2. the environment variable CASCADE_DB_PASSWORD;
  3. the entry for this server and user in $CASCADE_HOME/credentials (see cascade catalog credentials);
  4. a hidden prompt, when a terminal is attached, which offers once to save what you typed (default No).

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

cascade catalog credentials

cascade 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.

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, 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.

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:

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 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.

cascade export

cascade 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.

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 diff

cascade 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.

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 rules

cascade 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.