npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@alexsoft-hq/cascade

v0.9.1

Published

Persistent code structure and code-to-column change impact for AI coding agents, with automatic source refresh over MCP.

Readme

English | 한국어

Cascade

Understand the code. Find where to work. Check what a change reaches.

Cascade 0.9 gives AI coding agents a stored view of project structure they can query across sessions. Start from a project briefing, find the relevant functions and source, then trace a proposed change through services, APIs and database columns. When you save a local edit, Cascade checks for changes and waits for any required analysis before answering.

Available over MCP, in the CLI, and in a local web viewer. Analysis uses the selected Java, TypeScript and frontend inputs. Apache-2.0.

A coding session with Cascade 0.9

  1. Start with the project's saved structure. session_briefing summarizes the indexed scope, recorded entry points, freshness and limits. architecture shows dependencies between directory groups with source evidence.
  2. Find the code that matters. Use code_search and file_outline to locate declarations, code_trace to follow recorded callers and callees, and code_snippet to read the relevant source.
  3. Save an edit and query again. Source-backed MCP and viewer sessions check saved inputs and wait for any required refresh. index_status reports the generation and refresh state, including failures.
  4. Check the change's reach. Use changed_impact for edited files and flow or the column-impact tools for related services, APIs, tables and screens. Read the evidence grades and coverage limits with each answer.

The saved analysis is reusable project context. It does not store an agent's conversation or previous reasoning. See the 0.9 walkthrough for a step-by-step check on your own project.

Code context for AI coding

The structure index preserves files, declarations and recorded relationships from an analysis so the next session can query them again. The agent can use that map to choose which source to inspect for a task. code_context collects name/path matches and nearby relationships within an answer-byte budget; code_snippet checks the source hash before returning a declaration's exact parser-backed span. Java overloads and constructors have distinct identities.

The viewer's Code workspace brings briefing copy, module dependencies, declaration search, file outlines and verified source into one place.

cascade init --root /path/to/project
cascade analyze --root /path/to/project
cascade code briefing --root /path/to/project
cascade code architecture --root /path/to/project
cascade code context --root /path/to/project --query OrderService --max-bytes 16384

Connect your AI agent to use these tools over MCP, or run cascade view --root /path/to/project to explore the same analysis.

Source-backed MCP and viewer sessions check saved changes and refresh the analysis automatically. Queries wait for the required refresh; a failed build does not silently become a current answer. --no-watch serves the existing snapshot. Unsaved editor buffers are not inputs, and disconnected packs retain unknown freshness. Existing calibration gates still apply; automatic refresh never accepts a new baseline on your behalf.

Structure comes from the selected Java, TypeScript and frontend analysis facts, not an all-language index. Missing relationships remain unknown; directory groups are structural summaries, not inferred business intent. Structure has its own digest and coverage and does not change the impact graph's edge grades. code_trace distinguishes local, imported and immutable-alias calls from callback references, which do not prove invocation. Compact flow responses share repeated impact evidence and report omitted rows and continuation; inspect those limits before treating a shortened response as the full result. Context budgets count UTF-8 bytes, not model tokens. See code intelligence and freshness for the contract and the 0.9 walkthrough to try each feature. The supported-language evaluation records checks on Java/JavaScript examples, Ghostfolio and Cascade itself.

What it answers

Use structure tools to locate the work and impact tools to inspect what a change can reach. tools/list is the full surface.

| Question | Tool | |---|---| | I am starting a session. What has been indexed, and is it current? | session_briefing, index_status | | Where is this behavior defined, and which recorded functions call it? | code_search, file_outline, code_trace | | What source and nearby relationships should I inspect for this task? | code_context, code_snippet | | I am about to change this column. Which HTTP endpoints are affected? | endpoint_impact | | Which screens does that column reach, through the frontend's own calls? | screen_impact | | Which SQL statements read or write it, and which of those write? | column_impact | | I am about to change this endpoint or open this screen. What does it run through, down to the tables? | flow | | I have edited these files but not committed. What is the blast radius right now? | changed_impact | | Which two API groups share data through the database without calling each other? | coupling |

Change impact in practice

Once you have found the code to edit, inspect the data-facing paths it can affect. The card below shows Cascade's existing column-to-screen analysis.

One column and everything above it, drawn by Cascade from litemall:
litemall_admin.username is used by 10 SQL statements, 11 service methods, 6 API
endpoints including POST /admin/auth/login, 12 frontend functions and 4 screens,
each layer with its grades, and the answer's four limits written out
below

That card is Cascade's own answer, not a drawing: cascade export --project litemall --tab impact --column litemall_admin.username --format card wrote it, on litemall at a1ef964a718b. Change that one column and this is what sits above it, from the SQL up to the screens, with how sure each layer is. A solid line is seen in the code; a dashed one is a call that may happen but that no static read can confirm, which is as far as a parser honestly gets through Spring's dependency injection.

A tour of the Cascade viewer: how much of litemall is wired end to end, then
a column traced up to the SQL statements and HTTP endpoints it touches, the
source of one statement, the whole-project graph, and the schema recovered from
the joins the SQL makes on mall

That is one column changed and the answer read in both directions, on two real open-source projects. The still shots below walk the same views one at a time.

For data-facing changes, follow a column up to SQL statements, service methods, HTTP endpoints and screens, or follow a screen back down to the data it uses. The impact tools return these paths over MCP; the viewer draws them for people. Each edge carries its own evidence grade so recorded facts, candidate sets and heuristics remain distinguishable.

Where it fits

Use Cascade when an AI coding task needs both a place to start in the source and evidence about the consequences of an edit. Its structure tools support navigation; its impact tools connect application code to database usage. Read the reported scope and unresolved links alongside the answer, and inspect source where the selected analysis cannot establish a relationship.

Supported stacks

| Stack | What is read | Grade ceiling | What is not read | |---|---|---|---| | Java Spring MVC controllers | @RestController / @Controller mapping annotations, and the routes a method returning a RouterFunction builds with calls, parsed with the JDK's own compiler in parse-only mode. No Gradle, no Maven, no dependency classpath. A path prefix configuration code sets is applied once the profile declares it (pathPrefixes), and a controller implementing an interface openapi-generator writes handles the routes its OpenAPI document declares | EXACT for a mapping on a concrete controller method, and for a functional route whose handler the source names; SOUND_SET for a mapping on an interface the implementer serves, and for a functional route reached through a declared type; HEURISTIC for a handler paired with a document's route only by openapi-generator's naming, for a functional route placed only where a document declares its operation id (until the profile says the document is in step with the code), for a functional route through a type the tree cannot close, and for a JPA key the tree does not declare (the assumed id) | a controller assembled at run time; a handler registered by any other call; a functional route whose path is in a variable, or whose handler does more than call one method, which is kept with no handler and said; a path prefix set in code, which is said (SETTING_IN_CODE) until the profile declares it | | MyBatis XML and annotations | <mapper namespace> files, <include refid> fragments resolved through a global index, and SQL written in @Select / @Insert / @Update / @Delete | EXACT: a statement id is the mapper interface FQN plus the method | a ${} substitution, which is recorded as a diagnostic rather than guessed at | | MyBatis-Plus | @TableName, @TableField, @TableId, @TableLogic, the BaseMapper / IService / ServiceImpl built-ins, and condition wrappers down to the method references and literals they carry | EXACT where the source names the table or column; HEURISTIC where a naming rule had to be assumed | a wrapper whose conditions come from an HTTP query string: the table stays a fact, the columns are marked decided at run time | | JPA and Spring Data | Entity and column mappings, embedded and composite/shared keys, association joins, inherited attributes, derived queries, JPQL, native SQL, and reached repository built-ins | Physical naming applies to written and derived names. EXACT where the supported mapping and naming rules settle the result; HEURISTIC where a physical or implicit rule is assumed or unmodelled. Declare jpa.namingStrategy and jpa.implicitNamingStrategy separately; configuration is weighed against the factory | Secondary tables, unsupported class/association overrides, converters, element collections and named queries; see the Java lane for reported gaps | | SQL DDL catalogs | CREATE TABLE and ALTER TABLE with types, nullability, primary keys and comments, per dialect: MySQL and MariaDB, PostgreSQL, Oracle, and H2 and HSQLDB through the ANSI parser. Each dialect brings its own identifier-case rule | EXACT | a dialect this engine cannot route, which is refused rather than parsed as MySQL | | Live catalog fetch | one read-only connection that reads tables, columns, comments and primary keys and writes a pinned snapshot | EXACT | anything but metadata: no table data is ever selected, and analysis itself never connects | | NestJS, Prisma and TypeORM | the controllers a module the application loads registers, under the global prefix and URI version its bootstrap sets; the calls between methods through this, injected fields, imported functions and static methods, and through an abstract class or an interface to the classes that answer it; the files of a monorepo's shared library the imports reach; every Prisma or TypeORM call as a statement of its own, with the relations a Prisma call names, read against schema.prisma or the TypeORM entities, which are the catalog | EXACT for a route whose address the bootstrap states, for a Prisma call on PrismaClient or a class extending it, and for a TypeORM name whose naming strategy, table prefix and schema are known; SOUND_SET for a call between methods, a call through an abstract type whose set the module bindings settle, the nestjs-prisma client, and a column a select may leave out; HEURISTIC for a route an exclude built at run time may move, a TypeORM name that depends on a strategy, prefix or schema the source leaves to run time, and a call through an abstract type whose set may be short | a prefix or a module list the source builds at run time, which makes no route and says so; raw SQL; Prisma's fluent relation API; TypeORM embedded entities, table inheritance and Active Record calls; guards and interceptors; Mongoose | | Frontends | .js, .mjs, .cjs, .jsx, .ts, .tsx and the <script> blocks of .vue single-file components; axios and fetch and XMLHttpRequest, Angular's HttpClient known by the type a field is declared with, and the project's own wrappers, an object's methods included, traced to whichever of them sends the request; a base URL read per build from the .env files the build tool reads; vue-router, react-router and Angular Routes declarations composed into screens; OpenAPI 3 and Swagger 2 documents as declared routes; HAR recordings as runtime evidence | SOUND_SET for a call traced to a client that sends it; EXACT for a RENDERS edge onto the file a route declares; HEURISTIC for a call whose base URL rests on a default no .env file sets, or only on hosts that are not this machine, for a call to this machine on a port no file states, and for a call through a wrapper step whose URL, method or base URL the code does not settle | which of an imported component's functions really runs, which is a run-time question and stays SOUND_SET |

NestJS prefix exclusions can also be read from imported literal constants, templates, string substring, ordered array spreads and pure one-parameter map expressions. The engine does not execute the application. Mutation, unsafe escapes or dynamic code such as eval and Function can prevent it from settling the list; those routes stay HEURISTIC. A verified exclusion list can be declared as tsBackend.globalPrefixExclude, while a prefix read from runtime configuration needs tsBackend.globalPrefix. See the TypeScript lane for the supported expressions and limits.

Not supported, and the engine says so rather than guessing. Kotlin sources. Any backend this engine has no lane for, unless it publishes an OpenAPI document, in which case the routes exist and nothing below them is walked. Svelte routers. Runtime wiring, reflection and AOP proxies. GraphQL. WebSocket. A menu the server sends the frontend when the app starts, beyond the routes the source itself declares.

Install

npm install -g @alexsoft-hq/cascade     # the command is `cascade`
cascade --version                       # installed package version
cascade setup                           # prepare the SQL parser
cascade doctor                          # says what else, if anything, is missing

Or run it without installing anything: npx @alexsoft-hq/cascade doctor. From a clone of this repository the same commands are node bin/cascade.mjs …, and the rest of this page is written that way so it works before you have installed anything.

Node 20 or newer for the engine itself, and nothing else. The engine and its servers have no npm dependencies of their own, so nothing is fetched beyond the package. Execution is supported on macOS, Linux and Windows. CI checks specific OS, architecture and runtime combinations; that is not a claim that every combination has been measured. See installation, updates and recovery for the CI matrix and package verification commands.

On Windows, cascade setup uses Scripts\python.exe inside the selected virtual environment (<cascade home>/venv for an installed package, .venv in a checkout); the JDK is whatever javac is on PATH or under JAVA_HOME.

A JDK 17 or newer for the Java lane. The lane uses the JDK's own compiler Tree API in parse-only mode, so javac and java are all it wants:

brew install openjdk                          # macOS
export JAVA_HOME=/opt/homebrew/opt/openjdk
sudo apt-get install default-jdk              # Debian or Ubuntu
winget install EclipseAdoptium.Temurin.21.JDK # Windows

Python 3 for the SQL lane, and one command to wire it up. The sqlglot version is pinned in the requirements file rather than chosen at install time, because a different one parses some statements differently and would move the pack digest, so let the tool install the pinned set:

node bin/cascade.mjs setup

It finds a python3, builds the virtual environment where a run looks for one, installs the pinned requirements, and verifies the exact sqlglot version with the interpreter a run will actually use. A managed environment with a mismatched version is repaired by setup. An interpreter selected by CASCADE_PYTHON is read-only to Cascade: install the exact pin there yourself, or unset the override and run setup.

Nothing at all for the web lane. Its parser is vendored under adapters/web/vendor/, so reading a frontend needs no install and makes no network call.

Then let the tool tell you what is missing:

node bin/cascade.mjs doctor
ok      node >= 20                                      v24.20.0
ok      git                                             git version 2.50.1 (Apple Git-155)
ok      python venv (SQL lane)                          .venv/bin/python: Python 3.9.6
ok      sqlglot importable (SQL lane)                   sqlglot 30.17.0
ok      JDK 17+ (java lane)                             javac 26.0.2.1 via homebrew keg (arm64)
ok      web lane parser (vendored)                      adapters/web/vendor/babel-parser.cjs loads and parses
missing mysql driver (optional, catalog fetch)          ModuleNotFoundError: No module named 'pymysql'
                                                        -> .venv/bin/pip install pymysql. This is only needed for `cascade catalog fetch` against mysql
missing postgres driver (optional, catalog fetch)       ModuleNotFoundError: No module named 'psycopg'
                                                        -> .venv/bin/pip install psycopg[binary]. This is only needed for `cascade catalog fetch` against postgres
missing oracle driver (optional, catalog fetch)         ModuleNotFoundError: No module named 'oracledb'
                                                        -> .venv/bin/pip install oracledb. This is only needed for `cascade catalog fetch` against oracle
ok      docker (optional, live-catalog container test)  server 29.7.2
ok      project registry readable                       ~/.cascade/registry.json: 12 project(s)
ok      cache directory writable                        ~/.cache/cascade/doctor-probe

all 7 required prerequisite(s) ok (9 ok, 0 warn, 3 missing)

One editorial note that holds for every output excerpt on this page: an absolute path the engine printed is written here as the relative or ~-prefixed path you would type. Nothing else in any excerpt is edited.

doctor exits 0 only when every required prerequisite is ok. The optional lines, the three database drivers and Docker, are reported and never fatal: the analysis path never connects to a database, so a missing driver costs you cascade catalog fetch and nothing else.

Ten minutes on your own project

The run below is the one that produced every number on this page. It reads macrozheng/mall (Apache-2.0, Spring Boot with MyBatis XML mappers and a MySQL dump in the repository) at commit 0504e86b, together with its Vue admin frontend macrozheng/mall-admin-web at 81fc17e5. Point the same commands at your own tree instead.

git clone https://github.com/macrozheng/mall ../target-examples/mall
git clone https://github.com/macrozheng/mall-admin-web ../target-examples/mall-admin-web

1. init: what is in this tree

node bin/cascade.mjs init --root ../target-examples/mall --project mall
project mall at ../target-examples/mall
repositories (1): .@0504e86b
files scanned 717: 524 java (48 spring handlers, 0 JPA entities), 104 mybatis mapper xml, 1 DDL, 0 kotlin, 0 frontend package.json
build tool maven; package prefixes [com.macro.mall]; lanes [sql,java]
wrote ../target-examples/mall/.cascade/manifest.json
wrote ../target-examples/mall/.cascade/profile.json
registered mall -> ../target-examples/mall/.cascade in ~/.cascade/registry.json
diagnostics: none

It writes three things and nothing else: manifest.json (each repository pinned to a full commit), profile.json (the reading convention, which you may edit), and a .cascade/.gitignore that ignores pack/ and catalog/ because those carry your SQL text and your column comments. It also adds one line to the home registry, so later commands can say --project mall instead of a path.

A technology the engine has no lane for comes back as an UNSUPPORTED_TECHNOLOGY diagnostic rather than being quietly skipped. A second run keeps your edits unless you pass --force, and --json prints the whole discovery report.

2. analyze: the lanes, and one content-addressed pack

With no lane flag the inputs come from the project itself: the DDL from the profile, the mapper directories and Java source roots from discovery. The run prints which lane got what, and from where.

node bin/cascade.mjs analyze --root ../target-examples/mall
lanes [sql,java]: ddl 1 file(s) (profile): ../target-examples/mall/document/sql/mall.sql; mappers 4 dir(s) (discovery); java-src 7 root(s) (discovery; 4 test root(s) excluded (the standard src/test layout; pass --java-src to include): mall-admin/src/test, mall-demo/src/test/java, mall-portal/src/test/java, mall-search/src/test/java); web none; openapi none; har none
SQL lane: lineage (dialect mysql, identifiers fold-lower) over 904 statement(s)…
Java lane: 246 endpoints, 9876 calls, 251 dispatch, 904 stmt-bindings (358 unresolved, 439 external, 0 mapper method(s) with no statement in this pack)
wrote ../target-examples/mall/.cascade/pack/pack.json: 12674 nodes, 19269 edges, lanes [sql,java], digest 99141d55e969
axes: catalog=shipped statements=shipped jpa=not-shipped mybatisPlus=not-shipped column=shipped code=shipped web=not-shipped screen=not-shipped
cold (no previous facts-index.json beside the pack, so there is nothing to reuse): parsed 519 java file(s), 904 lineage shard(s) over 906 statement(s), pack digest 99141d55e969

The lane line is the one to read. It names every input, where it came from, and what was left out: here, the four src/test roots an unflagged run does not read. The axes line under it is the pack's own declaration of what it can answer, per axis, before anybody asks a question.

mall's frontend lives in a repository of its own, so one flag adds it:

node bin/cascade.mjs analyze --root ../target-examples/mall \
  --web-src ../target-examples/mall-admin-web/src
Web lane: 128 file(s) (83 .vue, 45 .ts/.tsx, 0 .js/.jsx), 0 parse error(s); 155 call site(s) carry a URL (89 literal, 59 template, 2 constant, 5 unresolved), 54 route declaration(s), 1 alias(es), 0 proxy rule(s)
Web lane: 153 call site(s), 145 resolved (145 sound, 0 heuristic), 8 unresolved (expression 4, noMatch 3, parameter 1), 3 outside-pack; prefix ../mall-admin-web: (none) (derived)
Web lane: 54 screen(s) from 54 route declaration(s), 54 with a component (0 unresolved), 121 exact and 64 candidate RENDERS edge(s); 301 frontend function node(s) (148 send a request, 153 lead to one), 174 CALLS edge(s) (174 exact, 0 sound, 0 heuristic; 0 of them a function handed over as a value)
wrote ../target-examples/mall/.cascade/pack/pack.json: 13032 nodes, 19797 edges, lanes [sql,java,web], digest 7994e1a5fd22
axes: catalog=shipped statements=shipped jpa=not-shipped mybatisPlus=not-shipped column=shipped code=shipped web=shipped screen=shipped

Adding a lane widened the pack, and the calibration gate stopped the run the first time because a ratio moved: see Honesty, verify and doctor below for the exact refusal and the one override.

A second analyze reuses what did not change. On this tree, with nothing edited:

incremental: reparsed 0 java files (519 reused, 0 dropped), reparsed 0 web file(s) (127 reused, 0 dropped), lineage recomputed 0 statements (904 reused), mapper statements reused, catalog reused, pack digest 7994e1a5fd22

The digest is the same as the cold run's, which is the point: an incremental pack must equal a cold pack of the same state byte for byte, and a test holds that against randomly mutated projects.

3. estimate: what it will answer, and what it will not

node bin/cascade.mjs estimate --root ../target-examples/mall
MEASURED. What the pack that exists actually answers:
  pack 7994e1a5fd22 built 2026-09-06T19:32:15.901Z lanes [sql,java,web]
  statementsWithColumnFacts       753 / 906    83.1%
  statementsWithStringSubst       396 / 906    43.7%
  endpointsReachingAStatement     205 / 242    84.7%
  webCallsResolved                145 / 153    94.8%
  mapperMethodsBound              904 / 904    100.0%
  callsResolved                  9437 / 10234  92.2%
  exactAnswerable                 509 / 906    56.2%

Run it before you analyze and it answers from discovery alone: what this tree will ship, degrade or not ship, axis by axis, with the reason on each line. Run it after and it adds the measured half above.

4. view: look at it yourself

node bin/cascade.mjs view --project mall
cascade viewer at http://127.0.0.1:4319/  serving 1 project(s) [mall], budget 512 MB of pack JSON

The page is described under The viewer.

5. mcp: serve it to an agent

node bin/cascade.mjs mcp --project mall
cascade mcp: serving 1 project(s) [mall]: packs load on first use, budget 512 MB of pack JSON

That is a normal MCP server speaking JSON-RPC over stdio: initialize, then tools/list, then tools/call. The next section wires it into a client.

Set it up for your AI agent

cascade agent --write                     # Claude Code: .mcp.json + CLAUDE.md
cascade agent --client cursor --write     # Cursor: .cursor/mcp.json + a rule file

Run it in the project. It writes two things: the MCP server configuration, with the absolute path and the project id filled in from the registry, and a short block that tells the agent when to ask (before it edits a mapper, an entity, a controller or a screen), which tool to ask with, and how to read the trust, limits and grade fields that come back. That block is the half people forget, and it is the half the value sits on: a server with no rule attached is a server the model never calls, because nothing in its context says a question is due.

The block goes into CLAUDE.md (or AGENTS.md for Codex) between two markers, so a second run replaces it in place and your own text around it survives. Without --write the command prints every file it would write instead of writing one. Claude Code then needs one approval: a server that arrives in a project .mcp.json sits at Pending approval until you run claude there once and approve cascade.

What the command writes, if you would rather do it by hand

Every configuration below runs the same command, node <path>/bin/cascade.mjs mcp --project <id>. Use an absolute path to bin/cascade.mjs: an MCP client starts the server from a working directory you do not control.

Claude Code, from a shell:

claude mcp add cascade -- node /path/to/cascade/bin/cascade.mjs mcp --project mall

Claude Desktop, in claude_desktop_config.json:

{
  "mcpServers": {
    "cascade": {
      "command": "node",
      "args": ["/path/to/cascade/bin/cascade.mjs", "mcp", "--project", "mall"]
    }
  }
}

Drop --project mall and the server serves every project in your registry, and each tool then takes a project argument. Name several instead and it serves exactly those: "args": ["/path/to/cascade/bin/cascade.mjs", "mcp", "--project", "mall", "--project", "shop"].

Cursor and a generic stdio client take the same three fields. docs/setup/agents.md has each client in full, plus what to check when a client reports the server as failed.

A worked session

An agent that has just been asked to add a discount field starts by asking what the pack even is, then narrows. These are real answers from the run above, abridged to the fields under discussion.

1. What is in here?

{"name": "overview", "arguments": {}}
{ "answer": {
    "pack": { "project": "mall", "digest": "7994e1a5fd22", "lanes": ["sql", "java", "web"] },
    "nodes": [ {"kind": "symbol", "count": 11085}, {"kind": "statement", "count": 906},
               {"kind": "column", "count": 669}, {"kind": "endpoint", "count": 242},
               {"kind": "table", "count": 76}, {"kind": "screen", "count": 54} ],
    "reach": { "endpoints": 239, "statementsReached": 208, "tablesReached": 49,
               "columnsReached": 461, "endpointsWithoutStatement": 34 } },
  "basis": { "project": "mall", "buildDigest": "7994e1a5fd22",
             "builtAt": "2026-09-06T19:32:15.901Z", "freshness": { "verdict": "unknown" } },
  "trust": { "trustLevel": "UNCERTIFIED", "axes": ["overview"],
             "knownGaps": ["no-project-golden", "jpa-axis-not-shipped", "mybatisPlus-axis-not-shipped"] },
  "limits": [ { "scope": "axis:jpa",
                "reason": "the jpa axis of this pack is not-shipped: the JPA bridge did not run, because there is no Java lane or the profile declares no jpa pack. Persistence declared by @Entity or Spring Data is absent, not empty" } ],
  "truncated": { "any": true, "fields": [ { "field": "nodes", "shown": 6, "total": 6, "nextOffset": null } ] } }

Read the four fields at the bottom before the answer at the top, because they are what makes the answer usable.

  • basis is what the answer is anchored to: which project, which pack digest, when it was built, and a freshness verdict of current, behind, provisional-overlay or unknown. unknown never reads as current.
  • trust is a computed level, never a typed-in one, and there are four of them: UNCERTIFIED (nothing was scored, which is what UNCERTIFIED here means: this project has no approved golden corpus, stated rather than hidden), GOLDEN_FAIL (a check got something wrong), RUNTIME_PASS (the checks that were scored passed, and at least one of them was labelled by a recording of the program running, so the answer covered what actually ran and precision is not covered) and GOLDEN_PASS. knownGaps names each axis that is degraded or was never built.
  • limits is what the engine could not see, in sentences, each scoped. The one above says the JPA axis is absent rather than empty, which is a different claim from "this project uses no JPA".
  • truncated says, per list, how many rows were shown out of how many exist, in what order, and where to continue. A truncated list that called itself complete is the failure this field exists to prevent.

There is a fifth thing to read, and it is the shape of an empty list. empty answers not-shipped (the axis was never built), degraded (built without something it needed) or none (looked, found nothing). Those are three different answers and they never collapse into [].

2. Which statements touch the column?

{"name": "column_impact", "arguments": {"column": "pms_product.price"}}
{ "column": "pms_product.price", "type": "DECIMAL(10, 2)",
  "statements": [ { "id": "com.macro.mall.mapper.PmsProductMapper.insert", "access": "write", "grade": "EXACT" },
                  { "id": "com.macro.mall.mapper.PmsProductMapper.updateByExample", "access": "write", "grade": "EXACT" } ] }

18 statements: 8 write it, 10 read it, all EXACT. This axis is exact because a MyBatis statement id is the mapper interface plus the method name, and the column lineage comes from a real SQL parse against a real catalog.

3. Which endpoints does that reach?

{"name": "endpoint_impact", "arguments": {"column": "pms_product.price"}}

27 endpoints, 12 of them under /product/*, the rest spread over /home/*, /member/*, /brand/*, /esProduct/*, /cart/*, /productCategory/* and /flashProductRelation/*. Every row is SOUND_SET, not EXACT, and that is the honest grade: a chain is graded by its weakest link, and a parse-only lane resolving a call by name produces a candidate set the real target is guaranteed to be inside, not a proof.

4. And which screens?

{"name": "screen_impact", "arguments": {"column": "pms_product.price"}}
{ "screen": "/pms/updateProduct", "grade": "SOUND_SET", "observed": false,
  "endpoints": ["GET /product/updateInfo/{id}", "POST /product/create",
                "POST /product/update/deleteStatus", "POST /product/update/{id}"] }

12 screens. observed says whether a browser recording confirmed the call, and false here means no recording was supplied, not that the call does not happen.

5. Then walk one screen down to the tables.

{"name": "flow", "arguments": {"screen": "/pms/product", "direction": "down"}}

The walk crosses six lanes and stops at the tables: 20 frontend functions, then 10 endpoints, 35 service methods, 7 mapper statements and 5 tables, out of 79 links followed, 38 of them EXACT and 41 SOUND_SET. walk.cut reports whatever the depth cap or the mode floor stopped, so a short answer says why it is short.

The edit loop

You have edited a file and not committed it. Ask what you may have touched:

node bin/cascade.mjs impact --project mall --verbose
overlay 4ed662d103d9 (fresh): re-parsed 1 java + 0 frontend + 0 TypeScript file(s), dropped 0, provisional 2 node(s) / 2 edge(s)
timings ms: load-base 27 + java 133 + web 39 + sql 31 + ts 0 + graph 81 = 311
  reused 645 cached java shard(s); dirty documents: mall-admin/src/main/java/com/macro/mall/controller/PmsProductController.java@0478df64
changed files: 1  (matched 1, unmatched 0)
touched: 10 symbols, 0 statements, 11 endpoints

upstream endpoints affected (11):
  GET /product/list  [EXACT]
  GET /product/priceCheck/{id}  [EXACT]  PROVISIONAL (only in the overlay)
  GET /product/simpleList  [EXACT]
  ...

downstream columns affected (88):
  ...

(provisional-overlay) provisional overlay: the dirty files were RE-PARSED and this answer describes the bytes on disk. Rows marked provisional exist only in the overlay (no certified run has seen them); nothing here is published and the pack digest is unchanged.

The same question over MCP is changed_impact, and its answer carries the same machinery in fields:

{ "overlay": { "applied": true, "state": "fresh",
    "overlaySessionId": "4ed662d103d9ef080bd477fe9e5cde08c2222ac135497970bc66c4747c38be06",
    "baseCommit": "0504e86b…", "headCommit": "0504e86b…",
    "docVersions": { "mall-admin/…/PmsProductController.java": "0478df64…" },
    "provisionalIds": { "symbols": ["symbol:com.macro.mall.controller.PmsProductController#priceCheck"],
                        "endpoints": ["endpoint:GET /product/priceCheck/{id}"], "statements": [],
                        "tables": [], "columns": [] },
    "timingsMs": { "loadBase": 25, "java": 132, "web": 40, "sql": 26, "ts": 0, "build": 78, "total": 301 } },
  "basis": { "freshness": { "verdict": "provisional-overlay", "overlaySessionId": "4ed662d103d9…" } } }

provisional is a marker, not a grade. It says this node or edge exists only in the overlay and no certified run has ever seen it. The grade lattice is untouched by it: the new endpoint above is EXACT because a mapping annotation on a concrete controller method is its handler, and it is also PROVISIONAL because nothing certified has seen that method yet.

overlaySessionId is the sha256 identity of this particular overlay: the base commit plus the sha256 of every dirty document. Two answers with the same session id describe the same bytes on disk. Two with different ones do not, even one second apart, and an agent that caches answers should key on it.

behind is what you get after you commit. The overlay is discarded rather than laid onto a base that has moved:

overlay NOT applied (stale-commit): the pack was built at 0504e86b1f1b but HEAD is now 9477dd387ecc, so the overlay is discarded rather than laid onto a base that has moved
limit [overlay]: HEAD moved past the pack's base commit; the answer below is the BASE pack's, not the working tree's. Run `cascade analyze` (it is incremental) to certify the new commit
(behind) provisional: computed from the base pack (files as last analyzed). Edited regions may add or remove connections. Re-run `cascade analyze` for a certified result.

The one second rule. This loop is only useful if it fits inside the pause between an edit and the next question, so the overlay has a one-second budget and the run above spent 311 ms of it on a 519-file backend plus a 128-file frontend. Nothing is written while it runs: not the pack, not the fact cache. The overlay may over-approximate; it may not omit, and that is a test rather than a promise, comparing it against a full re-analysis of the same bytes.

Since a commit

The overlay answers for edits you have not committed. For the edits you have, cascade diff --base-commit <rev> compares the project with itself as it was at that commit: the routes, screens, tables, columns and statements that appeared or went away, the edges that changed grade, and the endpoints above any of that. The base comes from the project's own history when a certified analyze kept that build, and is otherwise analyzed now in a temporary git worktree, the way the current pack was, with nothing registered or sealed.

It also shows changed node attributes and edge evidence with exact before/after values, while keeping file/line-only moves apart so they do not inflate static affected APIs or screens. This is what the two graph packs record, not every source/body change or a runtime result. spring-petclinic, forty commits back:

base: commit b5a630b1994b, built now in a temporary worktree, the way the current pack was analyzed
base  60eb0629ec1b  petclinic  commit b5a630b199  built 2026-09-14T10:22:23.266Z
head  2e3d4b9bd778  petclinic  commit 818c4136ea  built 2026-09-14T10:22:02.018Z
conditions: the same (lanes, identity rule, axes, workers, profile, engine, flags, roots)
nodes: +4 -0  (statement +1, symbol +3)
edges: +25 -2 regraded 20  (EXECUTES +3/~1, IMPLEMENTS_STMT +1, MAY_CALL +6/-2/~1, READS ~11, WRITES +15/~7)
endpoints above the change: 15

The conditions come first, because the same code read by another worker or with an axis missing gives a different pack too. Two different projects are refused: their difference is everything, and a list of a thousand added routes reads like a review while meaning nothing.

Several projects, one server

cascade init writes one line per project into ~/.cascade/registry.json, and that file holds addresses only: {id, dotCascadePath, source, stack, lastCertifiedAt}. The analysis itself never leaves the project's own .cascade/.

With no flag, cascade mcp and cascade view serve every registered project:

node bin/cascade.mjs mcp                                 # every registered project
node bin/cascade.mjs mcp --project mall --project shop   # just these two
node bin/cascade.mjs mcp --pack .cascade/pack            # one pack directly
node bin/cascade.mjs mcp --memory-budget 256             # MB of pack JSON held

Packs are lazy. Starting the server reads the registry and nothing else:

{"name": "projects", "arguments": {}}
{ "answer": {
    "projects": [ { "id": "jpetstore", "stack": ["sql", "java"], "loaded": false, "bytes": null,
                    "federation": { "index": "present", "serves": 22, "calls": 0 } },
                  { "id": "mall", "stack": ["sql", "java", "web"], "loaded": false, "bytes": null,
                    "federation": { "index": "present", "serves": 239, "calls": 0 } } ],
    "cache": { "loaded": 0, "bytes": 0, "budgetBytes": 536870912, "evictions": 0, "hits": 0, "misses": 0 } },
  "basis": { "project": "*", "scope": "server", "buildDigest": null } }

Every other tool takes an optional project argument. One served project answers without being named. Several, with no project, is refused:

error [ambiguous]: several projects are registered: jpetstore, mall. Pass "project"

There is no "pick the first one" fallback, because a confident answer about the wrong project is exactly the failure this tool exists to prevent.

Several projects, one answer. A microservice repository analyzes into one pack per service, and a pack alone can only say that its code sends GET /owners/{ownerId} somewhere. When another registered project serves that route, flow and endpoint_impact keep walking there: the rows from the other pack carry project, basis.siblings names every pack the answer walked with its own digest, and answer.federation lists each crossing, each call that no registered project serves (the remedy is to register it), and each project skipped for having no route index. The join is made when the question is asked, from the small routes.json that analyze writes beside each pack; the packs and their digests do not change. A crossing is SOUND_SET at best, HEURISTIC when several projects serve the same route, and never invented: a call nobody serves stays "leaves the pack". federate: false asks one pack alone. The viewer's whole-project views follow the same rule: Analysis status lists the connected projects and the routes that go to each, the Graph draws what a request reaches in a sibling as a cluster beside this project's map, and Table links frames each sibling's tables in their own cluster, joined to this project by a dashed HTTP line and never by a key. Measured on spring-petclinic-microservices split into five projects, the gateway's GET /api/gateway/owners/{ownerId} reaches owners in customers-service and visits in visits-service, and the gateway's own /owners screen, an AngularJS app served from static/ with no package.json, reaches owners in customers-service through the route table discovery read from the gateway's application.yml.

The cache budget. --memory-budget <MB> bounds the pack JSON held in memory, 512 MB by default, and eviction is least-recently-used. That number is a proxy rather than a heap measurement, because Node cannot price a live object graph and measuring the real thing would mean loading the pack the budget is meant to refuse. The ratio of resident graph to proxy that two independent tools measured, and the two scripts that re-measure it on your own pack, are in docs/mcp.md. A pack that does not fit the budget alone is refused outright, naming both numbers, rather than half-loaded.

The viewer's header carries a project selector for the same set, and the choice rides in the URL hash.

Honesty, verify and doctor

The grade lattice

Every edge carries one of five grades, and nothing ever moves up the lattice.

| Grade | What it means | Example | |---|---|---| | EXACT | proved unique by syntax, symbols and constants | a MyBatis statement id, which is the mapper interface plus the method | | SOUND_SET | a conservative candidate set the real target is guaranteed to be inside | the implementations behind an interface dispatch | | HEURISTIC | plausible from a project convention, or a recovered or partial binding | a column name derived from a field name with no declared naming strategy | | RUNTIME_ONLY | not statically decidable, and needs runtime evidence | a request seen in a browser recording | | UNRESOLVED | the analysis failed, or the shape is unsupported | a URL that resolved to a route nothing here serves |

A candidate set of one is never promoted to EXACT. Narrowing is not proving. The lattice is computed in one file, src/core/policy.mjs, and the workers emit evidence rather than grades. A walk is graded by its weakest link, so one SOUND_SET hop makes the whole chain SOUND_SET. Query modes pick a floor: strict uses confirmed edges only, conservative adds candidate calls, heuristic also admits guessed rules, and RUNTIME_ONLY sits below all three, which is why a recorded call is shown and never walked.

The four fields every answer carries

basis, trust, limits and truncated, described in the worked session above. They are stamped with a private Symbol in the one file allowed to build a response, so an object that merely has the right shape is refused before it is serialised. Over HTTP that refusal is a 500 contract-violation, never a 200. The long version is in docs/concepts.md.

verify

node bin/cascade.mjs verify --project mall
verified ../target-examples/mall/.cascade: 7 check(s) agreed: pack, fact index and gate state match the receipt, the running engine is the one that signed it, and it is valid until 2026-10-06T19:32:05.381Z
gate NO_CHANGE -> GREEN

It recomputes every digest in the receipt from the files on disk, checks the running engine against the one that signed the receipt, and refuses an expired receipt. Exit 4 on any disagreement, never a partial pass.

doctor

Shown under Install. It exists so that a missing prerequisite is one report rather than five failures discovered one command at a time.

Calibration: every run judged against the last certified one

Each project keeps a sealed baseline in .cascade/calibration/, and every analyze is compared against it. The gate first asks why this run differs: NO_SEAL (no baseline yet, so this run becomes it), NO_CHANGE (same engine, same pins), ENGINE_MOVED (an engine upgrade), REPIN (the analyzed commits moved), or BOTH_MOVED.

Here is the gate refusing a real run: the same tree, the same engine, with the web lane added.

gate: NO_CHANGE -> RED - endpointsReachingAStatement dropped 1.28% (>0%)
  [error] endpointsReachingAStatement: same engine and same pin, but endpointsReachingAStatement moved from 85.8% (205/239) to 84.7% (205/242). Identical inputs must produce identical measurements
  [error] node:endpoint: same engine and same pin, but node:endpoint moved from 239 to 242
REJECTED: the pack was written to ../target-examples/mall/.cascade/pack-rejected/pack.json and the certified pack at ../target-examples/mall/.cascade/pack/pack.json was NOT touched
  a regression is not a new snapshot: fix it. If this drop is the intended new normal, re-run with `--accept-baseline`,
  which re-seals the baseline from THIS run. That is the only override, and it is a human decision.

Read that carefully, because it is the gate working rather than misfiring. The numerator did not move: 205 endpoints still reach a statement. The denominator grew from 239 to 242, because the web lane found three routes the frontend calls that nothing here serves. The ratio therefore fell while nothing got worse. The gate is comparative, not absolute, so it reports the finding and leaves the judgement to a person:

node bin/cascade.mjs analyze --root <repo> --web-src <front/src> --accept-baseline

That re-seals the baseline from that run, so tomorrow's comparison is against today. It is the only override there is. A RED run is never silently discarded: its pack goes to <packDir>-rejected/, the certified pack is left exactly where it was, and the command exits 3.

The edge counts the gate compares are per type at each grade or stronger (edge:READS/HEURISTIC+ is every READS edge graded HEURISTIC, SOUND_SET or EXACT). So an edge whose grade rises, say because the project's naming strategy was declared, shrinks no row and is not a drop; an edge that is gone still shrinks every row it was in, and a grade that fell shrinks the stronger rows. A baseline sealed before 0.8.10 is summed into the same rows before it is compared, so an upgrade re-seals nothing on trust.

Alongside the gate, cascade golden keeps the project's own labelled corpus. The tool proposes cases and a human approves them, a hash decides which are held out, and check scores the approved ones through the shipped MCP tools. The tool never approves itself, which is why the trust level on every answer can mean something.

A missing axis is declared, not fatal

Every lane input is optional, and every lane can be switched off by name: --no-ddl, --no-mappers, --no-java, --no-web, --no-openapi. The run still produces a valid pack, and the pack records meta.axes per axis. Here is mall with no database catalog:

node bin/cascade.mjs analyze --root ../target-examples/mall --no-ddl \
  --out ../mall-no-ddl-pack
{"code":"summary","columnFacts":1248,"defaultSchema":null,"diagnostics":2,"dialect":"mysql","identifierCase":"fold-lower","identifierCollisions":0,"joinFacts":0,"level":"info","statements":904,"tableFacts":948,"unresolvedColumns":5135,"unresolvedJoins":46,"unresolvedRate":0.8045,"version":"lineage/2"}
wrote ../mall-no-ddl-pack/pack.json: 12598 nodes, 13479 edges, lanes [sql,java], digest f0ba9c2b4d78
axes: catalog=not-shipped statements=shipped jpa=not-shipped mybatisPlus=not-shipped column=degraded code=shipped web=not-shipped screen=not-shipped

Column facts fall from 6342 to 1248 and unresolved column references rise from 396 to 5135. Nothing is dropped to make the answer look clean: what cannot be attributed without a catalog is recorded as unresolved, the column axis declares itself degraded, that declaration lands in trust.knownGaps on every answer, and an empty list then says degraded rather than none.

The viewer

cascade view starts one local web app over the same tool catalog the MCP server uses. The page never reimplements a query: every number on it arrives in a contract-valid answer from the engine, so the page and a model asking over MCP can never be told different things. It binds 127.0.0.1 and authenticates nothing, because it is not meant to be reachable from anywhere else.

The masthead is one line: the dateline (which project answered and which lanes built its pack), counts and build, which opens the chain with a count on each step and the digest and the commit, chips for freshness, trust and the project's limit count, a project selector, a language toggle and a theme toggle. Two themes sit on one set of tokens: dark is the signal room and the default, light is the engineering drawing that still reads when printed in greyscale.

Under it are five places, each a question a reader comes with: Start (where do I begin), Trace (one target, read either way), Structure (the whole pack as drawings), Compare (what changed since an earlier build) and Analysis status (what the analysis could and could not see). A link written for the tabs they replaced still lands where it did.

Start's rankings and Trace's browse counts follow the selected evidence mode, so changing between strict, conservative and heuristic updates both the lists and the answer they lead to.

Start

The landing page in the light theme: how much of the chain is wired, and a
map of the whole project, drawn as ink on paper

Start asks first. Where no API reaches SQL in the census mode, its first line says why, with the engine's fix. One box finds any API route, screen, table, column, SQL statement or method, and three questions follow: what breaks if I change it, how far it reaches, and what the analysis did not see. A question the target cannot answer says why instead of going grey. Under them are what this analysis can see (the lanes, the census mode, each axis and where it was read from) and the gaps that change an answer, each with What to do: the one fix the engine knows, a profile key with an example, a flag, a command or a mode. Then the map: the whole pack in about ten boxes a side, routes in code areas (or declared modules, or API groups) on the left and table families on the right. A box opens in place, and a route or a table inside it shows only the paths through it. Last come the shares from the overview answer's own reach: how many endpoints reach SQL, how many statements, tables and columns are reached, and how many screens reach a table, each saying the mode it was counted in. A share that has to be read with a limit carries it on its card, because "tables reached 100%" on a project whose schema was never read is not the whole database.

Trace

The details of one screen: the component file it mounts, the fourteen
frontend functions it runs, and the seven API routes those
reach

Pick one target, then read it one of three ways. What it uses follows it down: the code a route or a screen runs through, the SQL it sends and the tables at the end. Where it is used follows it up: the SQL, the code, the routes and the screens that reach it, which is what a change there would touch; up from a route it is the frontend functions and screens that call it. Details says what it is, as lists: the card above is a screen, with the .vue file the route declares, the functions on it, each marked leads to or sends, and the routes they reach with the grade on each. A target offers only the directions it has, and a missing one is said in a sentence. Over the picture, Trace answers in one sentence ("Change this and it reaches 31 APIs and 12 screens."), with the mode and depth it counted in and the first names of the ends. What my uncommitted edits touch, under Options, asks the same questions about your uncommitted changes.

A screen traced down: the answer first, then frontend functions, endpoints and
service methods in labelled hop columns; the mapper statements and tables are
named in the lane strip above the picture

The picture is one lane per step, and every node in it is named the way a developer reads it (Class.method, Mapper.id, a route with its verb). A solid connector is a call the engine can prove, a dashed one a candidate set it could not narrow, a dash-dot one a guess. by hop groups the same rows one step at a time with a census per hop. Beside the picture, one panel says what limited this answer and another what the mode left out, grade by grade.

A column traced up: the answer first, then mapper statements, service methods
and endpoints walked backwards; the frontend functions and screens are named in
the lane strip above the picture

Trace opens on a list, not on an empty search box: the kinds you can pick with their counts, a filter, a sort, and the rows with the numbers you would pick by. Every row is one browse answer, so the page counts nothing itself; typing filters the rows it already holds and sends no request. A table has a caret that opens it into its own columns, so you can walk from a table down to the column you are about to change. / puts the cursor in the filter, the arrow keys move the highlight, Enter picks. Under 1100px the list becomes a drawer behind a Browse button.

Structure

The whole pack as four drawings: the Graph, Table links, Coupling and Transactions.

Coupling: a writer-by-reader matrix of API groups with the shared column
counts in the cells, and the ranked list of coupled pairs
beside it

Coupling. Two API groups can depend on each other without ever calling each other: one writes a column, the other reads it. The matrix shows those pairs, writer down the side and reader across the top. On mall that is 63 pairs across 32 groups, 267 coupled columns, and 93 columns only one group touches. Click a cell for the columns the two share and the statements that carry them.

The Graph: the whole project as one map, API groups at the centre with
their endpoints and the tables those reach around
them

Graph. The whole pack as one picture. At rest it draws API groups and tables only, with each group-to-table line standing for every endpoint in that group that touches the table; click a group and its endpoints unfold as satellites. A node's radius follows the square root of its degree, line colour is what the endpoint does to the table, and thickness is how many statements carry it. The map is flat and still; 3D and the moving dots sit behind one Advanced view switch, off by default. Double-click a node for Around <node>: that node in the middle, what touches it on ring 1, what touches those on ring 2.

The graph follows the selected evidence mode. In conservative, a route whose address is still HEURISTIC does not connect to its handler or downstream tables. Analysis status shows the diagnostic and any supported profile setting that can settle it. A project analyzed with an explicit declaration, such as a verified NestJS exclusion list, depends on that declaration; the result does not imply that the same settings were inferred automatically.

Table links, by SQL joins: the whole schema laid out by the joins the mapper
SQL makes, with the hub tables ranked beside it

Table links, by SQL joins. The whole schema, laid out by the joins the mapper SQL makes between tables. It used to be called the ERD; foreign keys are never read, so a relationship here is a join some statement actually makes. On mall that is 27 relationships over 76 tables, joining 32 of them; the other 44 sit in a strip under the map, named as such rather than dropped.

Selecting a table opens a docked inspector. Drag its left edge or use the keyboard to resize it; Widen and Reset width provide quick controls, and the preferred width is remembered. Columns use one row each for name, type and comment, with a filter across all three. Long names and types stay intact and scroll horizontally. Relationship evidence starts folded above the columns. Resizing preserves the selection and graph layout, and narrow screens place the inspector below the drawing. Connected-project tables use the same inspector. See the viewer guide for the controls.

Ghostfolio's SymbolProfile table in the widened inspector: searchable column
rows keep dataGatheringFrequency and its type on one line

Transactions: each @Transactional method with its write count, read
count and the number of tables one commit can
touch

Transactions. Every @Transactional method, and what one commit can touch through it. mall has 35 of them, and the largest reaches 15 tables.

Compare

What changed in this project since an earlier build of it. Every certified analyze keeps the pack it replaces in .cascade/history/, the five most recent, and the place appears once the project has one. It never offers another project as a base, because two codebases differ in everything. The first panel says whether the two builds were analyzed the same way; read it before the lists, because a difference is a code change only when the analysis did not change. For a commit the history does not hold, cascade diff --base-commit builds the base from the repository. The same report separates attribute and evidence changes from location-only moves, and can be printed or downloaded as Markdown with its base, conditions and cut-list totals.

Analysis status

What this analysis could and could not see, in one place: how fresh the pack is, what the trust level rests on, the mode the census was walked in, every axis with where it was read from and what it changes, every blind spot as a row that says its cause, what it touches and what to do, and the analyzers' warnings, one row per kind, each titled in the reader's language with its code beside it. Every limit carries a short title in the page's words, with the engine's sentence one click under it. Its second view is Rules: the rule packs the engine ran, starting with the ones that gave something in this project, each with the links it gave.

The source pane

The source pane docked to the right of a traced chain, showing the component
file on disk with its own line numbers and the answer's lines
marked

The picture is the claim; the source is the evidence. One pane shows it, docked to the right edge, and every row that makes a claim can open it. It reads the file on disk through a local route, so it shows what is there right now rather than a copy baked into the pack. It carries the file's own line numbers, marks the lines the answer is about, and Open in editor hands the file and the line to VS Code or IntelliJ.

Language

The interface language toggle switches the chrome only. Grades, trust, limits, empty reasons and every tool's own message stay exactly as the engine wrote them, because a translated grade is a grade this project invented and no reader could check it against the engine's own answer.

The landing page with the interface language set to Korean, showing that the
grades, trust level and limit names stay in the engine's own
words

A traced chain in Korean: the page's own labels and the hop labels are
translated, the node ids and grades are not

Full detail, including how to add a language: docs/viewer.md.

Profile and framework packs

cascade init writes .cascade/profile.json, and you edit it. It is the reading convention: what this project's code means, in the places a parser cannot tell. Every key in it is either CONSUMED, meaning it changes what the engine does, or RECORDED and diagnosed the moment you set it. There are no dead keys, and a test holds that.

{
  "build": { "tool": "maven", "javaRelease": null, "profiles": [] },
  "packagePrefixes": ["com.macro.mall"],
  "schema": { "default": null, "propertyNames": [], "rewriteLayer": null },
  "sqlDialects": { "main": "mysql" },
  "sqlIdentifierCase": null,
  "gatewayRoutes": { "/dev-api": "" },
  "screenAxis": {
    "enabled": null,
    "nameSource": "route-meta",
    "pathRule": "last-segment",
    "codeRegex": "([A-Z]{2}\\d{4})"
  },
  "moduleAttribution": { "packageDepth": null, "codeLength": 2 },
  "frameworkPacks": ["spring-mvc", "mybatis-xml", "jpa", "mybatis-plus", "web", "vue-router", "react-router"],
  "jpa": { "namingStrategy": "spring-snake-case" },
  "mybatisPlus": { "namingStrategy": "underscore", "tablePrefix": null,
                   "logicDeleteValue": "1", "logicNotDeleteValue": "0" },
  "openapi": { "documents": ["api/openapi.yaml"] },
  "runtimeEvidence": { "har": ["evidence/admin-session.har"],
                       "otel": ["evidence/checkout-smoke.json"] },
  "catalog": { "source": "file", "connectionFrom": "../document/sql/mall.sql" },
  "calibration": { "firstRun": "bootstrap", "maxRelativeDrop": 0.05,
                   "maxRelativeDropOnRepin": 0.25, "receiptTtlDays": 30 }
}

frameworkPacks turns lanes on. spring-mvc reads mapping annotations, mybatis-xml reads mapper XML, jpa maps entities and repositories, mybatis-plus reads generic CRUD and condition wrappers, web reads the frontend, and vue-router and react-router say which router declarations to recognise. cascade init writes the ones it can see: an @Entity file gets jpa, an extends BaseMapper< or a @TableName gets mybatis-plus, a package.json depending on Vue or React gets web and its router.

gatewayRoutes maps the prefix the frontend writes to the prefix the backend serves. {"/dev-api": ""} says the dev server strips it, and "*" applies to every call in the project. Declaring it mov