A code-to-column change-impact knowledge graph for AI coding agents.
The web lane reads the screen side of the round trip. The rest of the engine
starts at the HTTP endpoint and goes down (endpoint → service → mapper → SQL →
table → column); this lane reads the code above the endpoint: the frontend that
calls it, and the routes that decide which screen a user is on.
Read this first, because it is the whole shape of this version: the lane
attaches a frontend call to the endpoint this pack serves, as a graded
CALLS_HTTP edge, and it turns the router’s own declarations into screens,
joined to the functions of the component each one mounts. So a column-impact
answer reaches up past the controller, through the api function that calls the
route and the view that calls that function, to the screen a user is looking at.
A browser recording (--har) can be laid over the same edges as runtime
evidence, shown and never walked.
Every edge it writes says what it rested on, and the web axis is shipped
only when nothing about the frontend had to be guessed. A prefix this engine
worked out by counting matches, or a path alias it assumed, makes the axis
degraded and names what to declare.
Node, and nothing else. The parser is vendored
(adapters/web/vendor/babel-parser.cjs, @babel/parser 7.29.8, MIT), so there
is no npm install, no lockfile, and no network at analysis time. The
repository NOTICE credits it and
adapters/web/vendor/README.md documents
the exact bytes, the sha256 they are pinned to, and how to update them.
cascade doctor has a web lane parser (vendored) line that loads the parser
and parses one statement with it, so a truncated checkout is a named failure
rather than “0 frontend calls”.
Under each source root, recursively: .js, .mjs, .cjs, .jsx, .ts,
.tsx, and the <script> blocks of .vue single-file components. A Vue file’s
line numbers are the lines in the .vue file, template included, so a fact
points where you would put your cursor.
It skips:
| skipped | where | why |
|---|---|---|
node_modules, .git, .cascade |
any depth | never first-party source |
__tests__, __mocks__ |
any depth | a test is a different program, wherever it sits |
dist, build, coverage, public |
only as a direct child of a source root, or of that root’s package directory | there they are output; deeper down they are ordinary names |
*.d.ts |
any | a type declaration has no call in it |
*.test.*, *.spec.* |
any | same reason as __tests__ |
*.min.js |
any | a bundle, not a source |
| files over 2 MB | any | recorded as skipped: "too-large", never dropped silently |
The position rule on the third row is load-bearing, not a nicety. A blanket
“skip anything called build” silently drops src/views/tool/build/ — a form
BUILDER, six real screens in one of the frontends this lane was measured on. A
build directory is a build directory where output goes: at the top of the
package, or at the top of a source root. Anywhere else the word is just a word.
One consequence worth knowing: cascade init’s discovery uses a blanket
skip list (src/core/discover.mjs), because the same list protects the Java
lane from Gradle’s build/ output, and widening it there would let a Gradle
build directory into the Java lane. So the webFiles and vueFiles counts
discovery reports, and the counts cascade estimate prints from them, can be a
few files below what the lane reads: on one of the measured frontends
discovery counts 91 .vue files where the lane reads 97. The lane line printed
after a run is the measured number; the estimate’s is a lower bound.
A file the parser cannot read at all is recorded as a parse_error with its
line, its column and the message, and it is printed on the lane line. A file
that fails to parse contributes no call and no route, and nothing else in the
run would say so.
Beside the source roots it also reads, once per package directory (the directory
holding the nearest package.json): the dotenv files (.env, .env.local,
.env.<mode>, .env.<mode>.local), vue.config.js and vite.config.* for the
dev-server proxy table, and tsconfig.json / jsconfig.json / the bundler
config for path aliases. Those three between them decide what '/api' in a call
actually reaches, and the bridge uses all of them.
One JSONL record per fact, all of them file-local (the worker resolves nothing across files; that is the bridge’s job):
| record | what it says |
|---|---|
file |
the language, the Vue script blocks, how many errors the parser recovered from |
import / export |
what this file takes and gives, dynamic import() included |
function |
the named functions, with the rule that a callback gets no name of its own and is attributed to the nearest named function |
constant |
an enum or object literal of string members, and export const X = '/x' |
binding |
a top-level const whose initializer is a call, a new, or another name, plus the baseURL when one is built there |
class |
a class, with the methods and fields it declares (a client written as a class is as common as one written as a function) |
assign |
this.<field> = … anywhere in a class body, with the same init shape a binding carries — this is where a class puts the client it sends through |
call |
a call site that goes through an import or a local binding, carries a URL-looking argument, or is fetch / XMLHttpRequest.open |
route |
a route declaration, with its path AS WRITTEN, its component, its parent and its child count |
config |
the env values, the proxy rules and the aliases described above |
A function also carries what it RETURNS, when the last return at the top
level of its body is a call or a new — that is how a factory (return new
Client(opts)) and a forwarding method (return this.request(…)) are followed.
A this-rooted callee inside a class body says which class it belongs to
(binding: {kind: "this", class: "…"}), so this.inner.request(cfg) can be
traced to the field the constructor assigned.
A call carries the URL argument resolved as far as one file allows: a
literal, a template ('/things/' + id and `/things/${id}` both become
/things/{*}), a member of a constant declared in the same file, or a local
const followed once. Anything it cannot resolve says why:
parameter, expression or imported-constant, and an imported constant keeps
the binding so the bridge can go and look.
cascade analyze [--web-src <dir>... | --no-web] [--openapi <file>... | --no-openapi]
[--har <file>...]
--web-src <dir> — a frontend source root. Repeatable.--no-web — do not read the frontend even when the project declares it.--openapi <file> — an OpenAPI 3 or Swagger 2 document. Repeatable.--no-openapi — read no document even when the profile or discovery names one.--har <file> — a browser recording. Repeatable. See Recordings below.With no flag, the lane runs over the roots discovery found, but only when the
profile declares the web framework pack. cascade init declares it whenever
it finds a package.json with a vue, react, @angular/core or svelte
dependency, and adds vue-router / react-router when the same package depends
on one. The documents follow the same three-way rule with no pack to declare:
--openapi first, then the profile’s openapi.documents, then whatever
discovery found.
A route object has no syntax of its own: it is a plain object whose KEY NAMES a
framework decided on. Those names live in adapters/web/packs/*.json, one file
per convention, and the worker loads every file in that directory at start. Two
ship today:
{
"pack": "vue-router",
"routeObject": {
"pathKey": "path",
"componentKeys": ["component", "components"],
"childrenKey": "children",
"nameKey": "name",
"metaKey": "meta",
"titleKey": "title",
"redirectKey": "redirect",
"hiddenKey": "hidden"
},
"registrars": ["createRouter", "VueRouter", "Router"],
"routesKey": "routes"
}
An object literal is a route when it has a string pathKey and at least one
of componentKeys, childrenKey, redirectKey, or indexKey set to true, and
it sits inside an array literal, inside a registrar call’s routesKey, or is a
top-level exported object. A file with none of the registrars still yields
routes, because plenty of projects export a plain array and register it
somewhere else.
A jsx block (as react-router.json has) says which JSX element is a route and
which attributes name its path and its component, so a <Routes><Route …> tree
is read as the same tree.
When two packs could both claim one object, the one whose distinctive keys
the object carries wins (meta/hidden/redirect/name against
element/lazy/index), and a registrar the file actually calls breaks a tie.
To add a convention, drop a third JSON file in that directory. There is no code
to change.
src/adapters/web_bridge.mjs runs after the Java bridge (it needs the routes)
and turns each call site into one edge per route it can be shown to reach.
| Evidence | Grade | Why |
|---|---|---|
| an OpenAPI document that declares the route | EXACT, as a declaration | the document is the project’s own statement that the route exists, so the endpoint node is exact about THAT and about nothing else. A route the code also serves is corroborated and keeps the grade the code lane gave it; a route only the document names gets no handler edge, so a frontend call reaches the endpoint and stops there, and the code axis says degraded with that reason. See OpenAPI documents below |
a platform sink (fetch, XMLHttpRequest.open) with a URL that matches a route this pack serves |
SOUND_SET | the browser sends the request itself and the URL argument is the URL by contract; nothing had to decide that this is an HTTP call |
| an HTTP client library instance (a declaration pack names the library) called through one of its verbs | SOUND_SET | the library sends the request, and the URL is the argument the library reads |
| a wrapper traced back to one of those, by following what each name is bound to | SOUND_SET | every hop is a binding the lane read, and the hops are on the edge (evidence.sink.chain) |
| a URL-shaped argument handed to a call the lane could not trace to any sink | HEURISTIC | the call may send this URL or may only build it: a rule guessed |
| any of the above where the prefix was chosen by match count, a path alias was assumed, or the call has no method at all | HEURISTIC | one part of the answer is a guess, so the whole edge is |
| a browser recording (HAR) | RUNTIME_ONLY | a recording proves a request happened once and proves nothing about what the code can do, so the edge sits below every query mode’s floor: it is shown (observed: true) and never walked, and it never raises the grade of the static edge beside it. An APM trace or an access log is still not read. See Recordings (HAR) below |
| a URL that resolved but no route here answers, or one naming another host | UNRESOLVED | the edge is below every mode’s floor, so no walk follows it. The route is a node marked outbound, source: "web", exactly as a Feign call that leaves the pack is |
A call whose URL never resolved at all gets no edge and is counted by the
reason the worker gave (parameter, expression, importedConstant). Two more
reasons come from the bridge: noMatch (it resolved, nothing here serves it),
outsidePack (another host) and allHoles (see below). Nothing is dropped in
silence.
A frontend almost never calls axios directly. It calls its own function, which
calls another, which eventually calls the library. This lane follows that chain
by SHAPE, never by name:
axios.get(…)), a
top-level const initialised from the library’s own factory
(axios.create(…)), or a class field assigned one (this.inner =
axios.create(…));So a class whose get(config) returns this.request({ …config, method: 'GET' })
and whose request(config) returns this.inner.request(config) is followed all
the way to axios.create, and the edge records the chain and the depth. The
method comes from the wrapper’s own verb where it has one, then from the call’s
config, then from the library’s documented default (method.from says which).
The URL in the source is almost never the URL the backend serves. Between them
sit the client’s baseURL and the dev server’s proxy. The bridge settles it per
client instance, in this order, and the answer is on every edge as
evidence.prefix.from:
declared — the profile’s gatewayRoutes names it. Nothing is guessed;
the edge keeps its grade.derived — the base URL was read from the source (a literal, an env
value in a .env file, an absolute address’s path part) and a dev-proxy rule
explains what of it reaches the server: a rule with a rewrite strips it, a
rule without one keeps it. A client built with no base URL is derived
with an empty prefix, because that is what the library does, not a guess.auto — nothing in the source states it: the base URL names an env value
no .env file declares, the modes disagree with no proxy rule to settle
them, or a relative prefix has no proxy rule at all. Every candidate is then
matched against the routes this pack serves and the one with the most exact
hits wins. That is a guess, so every edge through it is HEURISTIC and the
candidate counts are on the edge.none — even that matched nothing, so the URL is used as written.To stop the guessing, declare the mapping in .cascade/profile.json:
{ "gatewayRoutes": { "/dev-api": "" } }
The key is the prefix the FRONTEND writes, the value the prefix the BACKEND
serves. "/dev-api": "" says “the dev server strips it”. A key of "*" applies
to every call in the project. Declaring it moves the axis from degraded to
shipped and the edges from HEURISTIC to SOUND_SET.
gatewayRoutes is read in exactly one place (src/adapters/web_bridge.mjs).
Declared with no web lane to read it, cascade analyze says so as a
RECORDED_NOT_ACTED diagnostic rather than silently ignoring it.
Which libraries send requests, and which of their methods are verbs, is a
DECLARATION, not code: adapters/web/packs/http-clients.json.
{ "module": "axios",
"instanceFactories": ["create"],
"verbs": { "get": "GET", "post": "POST", "put": "PUT", "delete": "DELETE" },
"generic": ["request", "(call)"],
"configUrlKey": "url", "configMethodKey": "method", "configBaseUrlKey": "baseURL",
"defaultMethod": "GET" }
"(call)" means the instance itself is callable (service({ url })). To teach
the lane a library it does not know, add a row to that file. There is no code to
change, and nothing in the bridge names a library.
Two rules keep the graph from filling up with things that are not routes, and both are counted rather than silently applied:
Cookies.get('size') is a verb-named call on a library that is not an HTTP
client. Counted as calls.notUrlShaped;`/${a}/${b}` →
/{*}/{*}) names no route: it matches every route of that length. Counted as
unresolved.byReason.allHoles, and listed in unmatchedUrls like any other
miss.The route path is a template ({id}, {key:.+} and * each take one segment,
** takes the rest); the call’s {*} takes one whole segment when the segment
is nothing else, and part of a segment otherwise. Exact string equality is tried
first (match: "exact"), then the template match (match: "template"). The
method has to agree, unless the route is ANY; a call with no method at all
matches by path and is HEURISTIC for it.
A call that matches several routes gets an edge to each, and every one of them
carries evidence.candidates saying how many.
Web lane: 120 file(s) (65 .vue, 0 .ts/.tsx, 55 .js/.jsx), 0 parse error(s); 132 call site(s) carry a URL
(128 literal, 2 template, 1 constant, 1 unresolved), 57 route declaration(s), 2 alias(es), 1 proxy rule(s)
Web lane: 127 call site(s), 121 resolved (121 sound, 0 heuristic), 6 unresolved (noMatch 5, expression 1),
4 outside-pack; prefix front: /admin (derived)
Web lane: 1 client instance(s), 0 wrapper(s) (deepest 0), 121 exact and 0 template match(es),
0 call(s) through an assumed alias; bridge 6 ms
The first line is the worker’s, the other two the bridge’s. The same counts go
into pack.meta.laneStats.web, so what was printed and what was recorded cannot
disagree.
The lane’s facts are content-addressed per file, exactly like the Java
lane’s. A shard is one frontend source file’s records; its name is
sha256(file bytes) + the webfacts worker version + the file's root-relative
path, so “is this still valid?” is a name lookup and never a judgement. Edit one
.vue and the next run re-reads that one file and reassembles the rest from the
cache.
That is safe for the same reason it is safe on the Java side: the worker resolves nothing across files. It records what one file imports, exports, binds and calls. Every cross-file step — an import resolved through an alias, a wrapper traced to the client library it forwards to, a URL matched against a route — happens afterwards in the bridge, over the whole assembled set, which every run rebuilds in full.
What is never cached is the package configuration: the .env* values, the
dev-server proxy rules and the path aliases. Those describe a PACKAGE, not the
file they happen to be written in, so no file’s shard could hold them honestly —
and they reshape every URL the frontend sends, so answering from a stale copy
would be the one mistake this lane cannot afford. Every run re-reads them with
node adapters/web/webfacts.mjs --configs-only …, which walks no source file and
costs one process start.
What forces a cold run:
| what moved | why every shard is dropped |
|---|---|
| the webfacts worker version | shards from two generations of a worker mean different things |
the engine’s assembly version (cascade-incremental/N) |
the shard layout or the assembly order changed |
| the lane selection (a web root added, removed or moved) | cold and incremental must analyze the same inputs |
--cold |
you asked |
The invariant this buys is I-9: an incremental run’s pack digest equals a cold
run’s on the same tree. test/incremental.test.mjs proves it by mutating a
random subset of a synthetic project — frontend files included — and comparing
the two packs after every round.
changed_impact (and cascade impact) answers over the bytes on disk, not the
last analysis, and that includes the frontend.
You edited a frontend file. The overlay re-reads that file only, re-reads the package configuration, splices the result over the cached shards and rebuilds the graph. The answer runs DOWN:
touched.webSymbols — the frontend functions in the files you edited, kept
apart from the Java symbols because what is downstream of one is a route.calledEndpoints — the routes those functions call.downstreamColumns — the columns those routes reach, through their handlers.A function no certified run has seen is marked provisional: true beside its
grade. That is a MARKER, not a grade: the lattice is untouched.
You edited a backend file. Each affected route in upstreamEndpoints carries
frontendCalls: how many frontend functions call it. That is the half of the
blast radius that is not below the edit.
Measured on the integration fixture, one edited .vue: 67 ms total for the
lanes (web 63, sql 1, graph 3), against a one-second gate.
A document is an evidence layer, not a lane over source: the project wrote it to say what it serves, and this engine reads it as a declaration.
What is read. A .json, .yaml or .yml file, at most 2 MB, whose first
4 KB carry a top-level openapi: / "openapi" or swagger: / "swagger" key.
Swagger 2 takes its prefix from basePath; OpenAPI 3 from the path part of
servers[0].url (a {variable} in the path is left exactly as written, because
substituting its default would invent a base path the document did not state).
Each (method, path) becomes an endpoint; a path item with no verb at all still
declares the path, with the method ANY.
The YAML subset. This engine has no runtime dependencies, so the YAML reader
is written here (src/adapters/openapi_bridge.mjs) and it is deliberately small:
| accepted | refused, by name |
|---|---|
| block mappings and sequences, by indentation | anchors (&x) and aliases (*x) |
| plain, single-quoted and double-quoted scalars | explicit tags (!!str, !Ref) |
# comments, whole-line and trailing |
a second document in one file (---) |
flow sequences [a, b] and flow mappings {k: v} of scalars |
a tab in the indentation |
\| and > block scalars, kept as text |
an unterminated quote or flow collection |
A refusal names the line and the construct and the whole document is then unread — no half of it enters the graph. A YAML feature silently mis-parsed would put routes in the pack that the file does not declare, which is worse than reading no routes at all. Convert the document to JSON, or write the routes without the construct.
With a Java lane. The document and the code are two independent statements
about the same routes. A route both name is CORROBORATED: the endpoint node gains
declaredBy (the documents that declare it, sorted) plus operationId and
summary when the document carries them, and keeps whatever grade the code lane
gave it. A route only the document names is added with no handler edge. The
drift census is on meta.laneStats.openapi and in the overview’s
openapi-drift gap, in both directions:
This engine reports both and judges neither.
Without a Java lane. This is what the layer is really for. A backend written
in something this engine has no lane for — Node, Go, Python, .NET — still
publishes a document, and the frontend’s calls then land on real endpoint nodes
instead of on nothing. The pack says how far that gets you: the code axis is
degraded, with the reason “endpoints come from an OpenAPI document, not from
source: the routes exist, but nothing below them is walked, so a frontend call
reaches an endpoint and stops there”, and a column question answers
not-shipped rather than an empty list that would read as “we looked”.
A route declaration is not a screen. A screen is the path a user is really
on, and that path is composed rather than read: a router nests, so
{path: '/panel', children: [{path: 'rows'}]} is one screen at /panel/rows.
The composition rules, in full:
/ is absolute and replaces everything
above it;'' contributes nothing;redirect is not a
screen at all: it mounts nothing and shows nothing;declaredAt.The node id is screen:<the composed path>, which is also what flow screen=
and browse kind=screen name it by.
| field | where it comes from |
|---|---|
path |
the composed path |
name |
the route’s own name |
title |
the route’s meta.title, only when screenAxis.nameSource is route-meta |
label |
the last path segment when screenAxis.pathRule is last-segment, otherwise the whole path |
code |
the first match of screenAxis.codeRegex against the name, then the title, then the path |
group |
the first moduleAttribution.codeLength characters of the code when both exist, otherwise the first path segment |
component |
the root-relative file the route’s component resolved to |
file / line / pack |
the declaration itself, and which router pack recognized it |
params |
true when the composed path has a :x or a * in it |
hidden |
present only when the declaration says so |
source |
router, or har for a page only a recording found |
screen --RENDERS--> symbol joins a screen to the frontend functions it can run.
Nothing about it is guessed from a name:
evidence.via, the import chain that reached it. It is a candidate
because which of an imported component’s functions a screen really runs is a
run-time question.laneStats.web.screens.componentUnresolved) with the specifiers that
failed, because an alias or a missing source root is usually what is wrong.Between a component’s function and the route it hits there is normally one more
hop: the api module. symbol --CALLS--> symbol is that hop, and its grade says
how the name was followed:
| grade | when |
|---|---|
EXACT |
a static import with a named or default specifier, followed through a relative path or a DECLARED alias to a function this lane read; or a call by name inside one file (getList(), this.getList()) |
SOUND_SET |
the same, but the name came through an export * barrel or a re-export chain, so WHICH file it came from was a choice |
SOUND_SET |
the function was never called here at all: it was PASSED AS A VALUE to some other call, and the receiver may call it |
HEURISTIC |
an ASSUMED alias was on the path |
A call onto an imported name that is not a function (a constant, a
component) makes no edge and is counted as calls.notAFunction.
A function handed over as a value. Plenty of frontends never call their api function from the view at all. They hand it to a hook:
const { rows, reload } = usePagedList({ api: listRows })
useSubmit(saveRow, { immediate: false })
No call site names listRows, so a rule that follows calls stops at the view.
The worker records what was handed over (fnRefs on the call record): an
identifier in any argument position, or the value of an object
argument’s property one level deep under any key, and only when the file binds
that identifier to an import or to a function it declares itself. A member of a
namespace import (api.list) keeps its root and its path. A string is not a
reference, a call is already a call, and an inline arrow’s body is already
attributed to the function around it.
The bridge resolves that name exactly as it resolves a callee and places
symbol --CALLS--> symbol with evidence.rule: "passed-as-value", the via
(argument or property), the key when there was one, the specifier and the
origin. The grade is SOUND_SET and never EXACT, because nothing here looked
at whether usePagedList calls its api: that would mean following a value into
another module’s body. An assumed alias on the path lowers it to HEURISTIC, and
a pair that is both called and passed keeps the call’s EXACT answer. The target
joins the same fixpoint as a called function, so a screen can reach it.
laneStats.web.calls.passedAsValue counts the references that resolved and
laneStats.web.callsByRule splits the edges by the rule that found them, because
a grade alone cannot tell a followed call from a handed-over function.
Only functions that reach HTTP get a node. A function that sends a request,
or that reaches one through these edges, is in the graph; a formatter or a date
helper is counted (laneStats.web.functions) and left out, because otherwise the
pack doubles in size for code no question is ever about. A function in a
component file (.vue, .tsx, .jsx) carries component: true, so a tool can
tell a function in a screen from an api function. A .ts or .js module that
exports a function returning JSX is a component too, and this version does NOT
catch that: the fact stream carries no JSX marker, so the rule is the file
extension and nothing else.
Plenty of admin products fetch their menu from the backend when the app starts. The screens this lane can see are then not the app, and the axis says so rather than letting the ones in the source read as the whole product.
The rule is the call: one of the frontend’s own calls resolved to a route
this pack serves whose path ends in /getRouters, /menu, /menus, /routes
or /nav. Nothing else is required. The census records
serverDriven: {detected, detectedBy: "menu-call", routes, ceiling,
menuEndpoints}, so a reader can check the rule rather than take it.
The route count does not decide, it only chooses the sentence. Under the 30-route ceiling the reason says most screens arrive when the app runs; over it, screens beyond the N declared arrive when the app runs. That split replaced a rule which needed both halves — a big frontend that fetches its menu is server driven exactly as a small one is.
What this still does not catch, measured and left alone: a product whose menu
rides on an endpoint that is not spelled like one. The largest frontend measured
declares 173 routes — 87 of them the framework’s own demo pages — and fetches
every business screen at run time from a route named after permissions rather
than after menus. No suffix above matches it, its screen axis reads shipped,
and “19 of 166 screens reach a table” reads as a shortfall rather than as a
description. Adding that project’s spelling here would be a rule that works on
that project and nowhere else, so the spelling table stays generic and this is written
down instead.
When it fires the screen axis is degraded with that sentence and the numbers
behind it, and overview carries a screens-from-server gap. It is about what
is MISSING, not about whether to build: a profile with screenAxis.enabled: true
still builds every declared screen. Set screenAxis.enabled: false if you would
rather have no screen axis than a partial one.
{
"screenAxis": {
"enabled": true,
"nameSource": "route-meta",
"pathRule": "last-segment",
"codeRegex": "([A-Z]{2}\\d{4})"
},
"moduleAttribution": { "codeLength": 2 }
}
screenAxis.enabled is the gate, and it has three states:
| value | what it means |
|---|---|
true |
build screens, whatever this run happens to read. cascade init writes this when it finds a router package in the analyzed tree |
false |
build none, whatever this run happens to read. Your word, and the engine does not argue with it |
null, or the key absent |
decide it from what the run READS: on when frameworkPacks names a router pack, or when a frontend package this run really reads depends on vue-router or react-router; off otherwise. This is the default |
The third state exists for the layout --web-src ../front/src describes. cascade init
discovers the analyzed tree, so a backend whose frontend is checked out beside
it has no router package for init to find — and before the third state, a run
that read that whole frontend and resolved its calls onto real routes still
built no screen at all, for a reason that had nothing to do with the code. The
run prints which of the three rules decided it:
screen axis ON (read-router): screenAxis.enabled is undeclared and a frontend
package this run reads depends on vue-router
screenAxis.nameSource is route-meta, none, or jsdoc-comment. The last
is refused with a diagnostic: no lane here reads the comment above a
component, so every title would be null, and the axis is degraded for asking.screenAxis.pathRule changes only the short label, never the path.screenAxis.codeRegex + moduleAttribution.codeLength are for a project whose
screens carry a code of their own (AB1234), where the first characters of the
code name the module. Without a codeRegex no screen has a code, and every
screen is grouped by the first segment of its path instead.shipped needs all three: the gate is on, at least one screen has a RENDERS
edge, and nothing about the reading was a guess. It is degraded when the app
fetches its menu from the server, when more than a fifth of the declared routes name a component
this lane could not resolve, when nameSource asks for something not shipped, or
when screens were built and not one of them reaches a function. It is
not-shipped only when the gate is off or no route was read at all. When it is
off, the axis reason names which of the three rules above turned it off.
A HAR file is what the browser saw: which page was open, and every request it
sent. cascade analyze --har <file> (repeatable) reads one, and the profile’s
runtimeEvidence.har names them for an unflagged run. Nothing is discovered:
a recording is something you made on purpose, and picking one up because it
happens to be in the tree would let an unrelated capture decide what this pack
claims was observed.
In Chrome, open the app, press F12, go to Network, tick Preserve log, walk through the screens you care about, then right-click the request list and choose Save all as HAR with content (the content is not read here, only the URLs and the page each one belongs to). Firefox and Edge write the same format.
/dev-api/system/user/list), and the routes in the graph
carry the backend path, so the front prefix comes off and the back prefix goes
on, using the very prefix decisions the web bridge already made for that
package: declared, then derived, then auto, in that order.https://app.example.com/#/things/list), and that
fragment IS the screen path there. In history mode the path is..js .css .png .woff2 .map .ico .svg) are skipped and counted
apart. Everything else that matches no route is counted by path, never
dropped: a recording that lands nowhere is usually a prefix nobody declared.source: "har", observed: true and no RENDERS edge, because no
source line says which component it mounts.Each matched (screen, route) pair becomes ONE screen --CALLS_HTTP--> endpoint
edge graded RUNTIME_ONLY, carrying {rule: 'har', file, count, firstSeen,
lastSeen, methods}. That grade sits below the floor of every query mode, so:
observed: true beside it.observed: true lands on the screen node and on the endpoint node, and on the
rows of flow, browse kind=screen and screen_impact that name them. The
census is on meta.laneStats.har: {files, entries, matched, unmatched, assets,
pagesWithoutScreen, pairs, screensObserved, endpointsObserved, unmatchedPaths}.
scripts/generality-gate.mjs runs this
engine, unchanged and with nothing configured, over a pinned corpus of real
repositories, and the numbers it reaches are a floor the test suite defends. Four
of those entries are a backend AND a frontend, because a backend alone says
nothing about whether a screen reaches a column. Two of the four carry their
frontend inside the backend repository, so an unconfigured cascade init
declares the web pack and the run reads it with no flag; the other two have a
frontend in a repository of its own, which the corpus pins the same way it pins
the backend (front: {url, sha, dir}) and the runner clones beside it, passing
one --web-src. Nothing else is given: no profile edit, no gatewayRoutes, no
document and no recording.
Two of the guarded counts are this lane’s: webCallsResolved (the call sites
that reached a route this pack serves, over the ones that carry a URL at all) and
screensReachingATable. Both are real numbers for all four pairs, including the
two whose frontend is a repository of its own: the screen axis is decided by what
the run reads (the three states above), so an unconfigured run over a backend
plus --web-src builds screens without anybody editing a profile.
.ts or .js module that
exports a function returning JSX is a component, and this version treats only
.vue, .tsx and .jsx as one.$ref resolution in a document. The routes and their operationId /
summary are read; a $ref to a shared parameter or schema is left alone,
because nothing here needs what it points at.