@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.
Maintainers
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
- Code context for AI coding
- What it answers
- Change impact in practice
- Supported stacks
- Install
- Ten minutes on your own project
- Set it up for your AI agent
- The edit loop
- Several projects, one server
- Honesty, verify and doctor
- The viewer
- Profile and framework packs
- How it is measured
- Layout of the repository
- Contributing, security, conduct
- License
A coding session with Cascade 0.9
- Start with the project's saved structure.
session_briefingsummarizes the indexed scope, recorded entry points, freshness and limits.architectureshows dependencies between directory groups with source evidence. - Find the code that matters. Use
code_searchandfile_outlineto locate declarations,code_traceto follow recorded callers and callees, andcode_snippetto read the relevant source. - Save an edit and query again. Source-backed MCP and viewer sessions check
saved inputs and wait for any required refresh.
index_statusreports the generation and refresh state, including failures. - Check the change's reach. Use
changed_impactfor edited files andflowor 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 16384Connect 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.
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.

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 missingOr 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 # WindowsPython 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 setupIt 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 doctorok 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-web1. init: what is in this tree
node bin/cascade.mjs init --root ../target-examples/mall --project mallproject 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: noneIt 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/malllanes [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 99141d55e969The 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/srcWeb 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=shippedAdding 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 7994e1a5fd22The 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/mallMEASURED. 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 mallcascade viewer at http://127.0.0.1:4319/ serving 1 project(s) [mall], budget 512 MB of pack JSONThe page is described under The viewer.
5. mcp: serve it to an agent
node bin/cascade.mjs mcp --project mallcascade mcp: serving 1 project(s) [mall]: packs load on first use, budget 512 MB of pack JSONThat 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 fileRun 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 mallClaude 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.
basisis what the answer is anchored to: which project, which pack digest, when it was built, and a freshness verdict ofcurrent,behind,provisional-overlayorunknown.unknownnever reads ascurrent.trustis a computed level, never a typed-in one, and there are four of them:UNCERTIFIED(nothing was scored, which is whatUNCERTIFIEDhere 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) andGOLDEN_PASS.knownGapsnames each axis that is degraded or was never built.limitsis 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".truncatedsays, 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 --verboseoverlay 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: 15The 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 heldPacks 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 mallverified ../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 -> GREENIt 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-baselineThat 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-shippedColumn 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

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

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.

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.

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

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


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


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
