Skip to content

feat: make the elision verdict inspectable and provable per app - #1312

Draft
vivek7405 wants to merge 10 commits into
mainfrom
feat/elision-inspectable
Draft

feat: make the elision verdict inspectable and provable per app#1312
vivek7405 wants to merge 10 commits into
mainfrom
feat/elision-inspectable

Conversation

@vivek7405

@vivek7405 vivek7405 commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Closes #1308

Summary

Elision is automatic and stays automatic, but until now only the benign half of its verdict was visible. webjs doctor could tell an app which pages and layouts ship when they could have been elided, whose worst outcome is a few wasted kilobytes. Nothing reported the other half: which component modules were DROPPED, and on what evidence. That is the half where a wrong verdict silently costs an app its interactivity in production, and an author had no way to ask what the framework removed from their page. The framework protects itself with a differential on/off test; an app had no equivalent, so every app inherited a guarantee it could not check locally.

This makes the verdict inspectable and provable per app.

What changed

webjs elision prints the per-module verdict: every component as elided or shipped, every shipped one naming the evidence that forced it (own / observed / closure / render / import / unreadable) and the module that did the forcing, every page and layout as inert / import-only / ships-whole, and any orphan class that gets no verdict at all. --json emits the same object, and the new MCP list_elision tool returns it verbatim.

webjs elision --verify boots two request handlers with WEBJS_ELIDE flipped, renders the app's own static route corpus through both, and diffs the masked SSR bytes. It is the framework's own guard pointed at an arbitrary app. It exits non-zero on a divergence AND on a corpus where nothing could be compared, because a verification command that can pass while comparing nothing is worse than no command. Its success output states its own boundary: it proves the bytes you serve did not change, not post-hydration behaviour.

maskJsSet has exactly one definition now, in packages/server/src/elision-differential.js, imported by both the framework's differential test and the CLI, so the two guards cannot drift on what "the JS-loaded set" even means.

webjs doctor grows ELISION_COMPONENTS, which passes with the elided inventory and warns only on an orphan, the one shape that is dropped with no verdict and no escape hatch. Both elision checks share ONE analyzeAppElision call, so the module graph is still built once per doctor run.

webjs dev prints one summary line per warm analysis, re-emitted after each rebuild. Deliberately not a browser push, for the reason in the comment on the code: an inert route ships zero application JS, and a dev-only boot script would corrupt the network-tab check on exactly the pages this exists to prove.

Two corrections the measurement forced. static interactive = true does not rescue a component whose own registration tag is computed (four surfaces claimed it did), and the orphan scan no longer reports documentation code samples as orphans. Both have their own comments on this PR.

Test plan

Every layer, all run and green.

  • Unit npm test: 3978 pass, 0 fail. New: residual-contract.test.js (both residuals + the not-a-residual case, measured), elision-report.test.js (the whole contract, which had no test at all before), elision-differential.test.js, the sigil-coverage evidence guard, test/cli/elision.test.mjs, the doctor block, the MCP drift test, the orphan code-sample case.
  • Browser npm run test:browser: Chromium 755, Firefox 741, WebKit 755, 0 failed on each.
  • E2E WEBJS_E2E=1: 92 pass, 0 fail, including the new static interactive = true network probe.
  • Bun node scripts/run-bun-tests.js: 297 pass, 0 genuine fail, no new DENYLIST entry. test/bun/elision-report.mjs asserts an identical verdict under node and bun (both run).
  • Counterfactuals, each toggled and confirmed red: dropping any noteShip call reds the evidence guard and the matching evidence case; dropping the DOCTOR_CODES entry reds the gate case; deleting static interactive = true from the fixture reds the e2e probe.
  • No verdict regression: ran the current analyser and origin/main's side by side over examples/blog and website and diffed the full elidable / inert / import-only / ships-whole sets. IDENTICAL on both. This change records why a component ships; it changes nothing about whether one does.
  • Dogfood: blog e2e 92/92; website boots 200 on /, /docs/elision, /docs/configuration, /docs/components, /docs/data-fetching, /docs/progressive-enhancement, /ui, /ui/button in dist mode with no broken preloads; webjs check clean on both apps; webjs doctor exits 0 on both.

Docs

  • New website/app/docs/elision/page.ts plus its nav entry. Elision was described across six pages with no page owning it, and the opt-out appeared on none of them.
  • website/app/docs/configuration/page.ts gains the webjs.elide / WEBJS_ELIDE section, which existed on no docs page.
  • The skill (.agents/skills/webjs/): references/components.md gains the evidence table, what the override does and does not rescue, and what to do with each verdict; references/testing.md gains the two-run recipe; SKILL.md gains the routing row.
  • AGENTS.md (CLI reference, the elision bullet, the async-render paragraph), packages/server/AGENTS.md (three module-map rows plus invariant 7), packages/mcp/AGENTS.md, packages/core/src/component.d.ts.
  • Scaffold: the gallery comment in server-clock.ts. The agent skill is copied from the repo-root canonical at scaffold time, so there is no second copy to edit.
  • N/A: README.md (no headline capability change), the editor plugins (no template grammar, snippet, or language-service surface changed), marketing copy (no positioning claim changed), a version bump (no published-package release in this PR).

…eport

The elision report told an app which pages ship when they could have been
elided, which is the benign over-ship direction. Nothing reported the other
direction: which components were DROPPED, and on what evidence. That is the
direction where a wrong verdict silently loses interactivity in production.

analyzeElision now records WHY each shipping component ships, alongside every
mustShip write, and returns it as componentVerdicts. analyzeAppElision projects
the whole verdict (components, route modules, orphans, summary) into one sorted,
app-relative, JSON-serializable object. Nothing is re-analysed: the data was
already in memory and was being discarded.

maskJsSet moves into a shared leaf so the framework's own differential guard and
the app-facing one cannot drift apart on what the JS-loaded set even is.
@vivek7405 vivek7405 self-assigned this Aug 6, 2026
…tial

An app could not ask what elision dropped from its pages, and the SSR differential
that proves elision changes nothing was a framework test an app could not run.

`webjs elision` prints the verdict per module with the evidence behind every ship,
--json emits it for a tool, and --verify boots two handlers with WEBJS_ELIDE
flipped and diffs the masked SSR bytes across the app's own static route corpus.
It exits non-zero on a divergence and on a corpus where nothing was compared,
since a vacuous pass is the failure mode a verification command must not have.

The success output states its own boundary: it proves the served bytes did not
change, not post-hydration behaviour.

Also corrects a claim four surfaces carried. `static interactive = true` does not
rescue a component whose own registration tag is computed: the scanner requires
a literal tag, so that component gets no verdict for the override to attach to.
…tests

`list_elision` returns `analyzeAppElision` verbatim: that function already owns
the app-level contract, so unlike list_routes there is no projector leaf and
nothing to keep in sync. list_components deliberately stays a cheap lexical
inventory; growing an elided flag onto it would make it build a module graph.

The dev server prints one summary line per warm analysis, re-emitted after each
fs.watch rebuild. Deliberately NOT a browser push: an inert route ships zero
application JS, and the most useful manual check an author has is opening the
network tab on that route and seeing nothing, which a dev-only boot script would
corrupt on exactly the pages this feature exists to prove.

The contract tests pin both documented residuals as MEASURED, which was prose
until now: a computed whenDefined observer leaves its badge elided, an external
stylesheet's :defined rule does too, and `static interactive = true` rescues
both. A computed Class.register(tag) is recorded as a different and worse shape
that the override does NOT rescue, since the scanner never sees that component.

examples/blog gains forced-badge, display-only in every respect apart from the
override, so the e2e probe can assert the browser really keeps its module.
…override

The CLI tests drive every --verify exit path, including the two that matter
most and are easy to get wrong: a corpus where nothing could be compared exits
1 rather than reporting a vacuous pass, and a real divergence is produced for
real rather than stubbed (a component whose SSR output reads WEBJS_ELIDE is
exactly the class of bug the differential exists to catch).

The doctor tests keep the three carrier cases unchanged, which is the
regression guard for rewiring that check onto the shared report, and pin the
new check to pass-except-orphans plus its gate contract.

The e2e probe is the first coverage anywhere above the analyser unit that
static interactive = true actually keeps a module on the wire; build-stamp on
the same run is the negative control.
Elision was described across six docs pages with no page owning it, and the
opt-out appeared on none of them, which is exactly how the switch went
undocumented. A capability with its own command, config key, env override,
doctor check, and MCP tool has outgrown being a paragraph inside five topics.

The skill gets the agent-facing half: how to read the evidence values, what to
do with each verdict, and the two-run recipe for the behaviour half that the
byte differential cannot see.

Dogfooding the new doctor check turned up a real defect in the orphan scan. It
read raw source, so every WebComponent subclass written inside an html template
as a CODE SAMPLE counted as an unregistered component, and the repo own website
reported 17 false orphans. That was tolerable as dev-console noise and is not
tolerable as a doctor warning, so the scan now redacts strings and templates
exactly as extractComponents already did. Both dogfood apps go from 17 and 0
false orphans to none, and a real orphan in the same tree still fires.
@vivek7405
vivek7405 force-pushed the feat/elision-inspectable branch from b770ca1 to ef13733 Compare August 6, 2026 17:04
The scaffold gallery-coverage gate requires every server export to be either
demoed or exempted; maskJsSet and staticPageRoutes are verify-command plumbing,
so they are exempt with the reason they exist at all.

The elision-report fixtures move to template literals. The scanner-fuzz corpus
sweep reads every file under test/elision and compares its lexical class window
against a real AST, and redaction blanks a template body while keeping a plain
string verbatim, so a fixture class in a plain string skews that differential.
@vivek7405

Copy link
Copy Markdown
Collaborator Author

Design rationale: why the report carries evidence, and why an elided row carries none

The verdict data was already in memory and was being thrown away. analyzeElision adds to mustShip from six different places, and each one knows exactly why it is doing so, so I recorded it there rather than deriving it afterwards. Deriving would have meant a second pass that re-answers questions the first pass already answered, and it would have gone stale the moment a rule moved.

First write wins, matching the analyser's first-match convention everywhere else. A component forced by several rules reports the rule that reached it first, which is a truthful account of why the module is on the wire even when it is not the only one.

An elided row deliberately reports reason: null. Elision is the ABSENCE of every signal, so there is no positive fact to state. The only way to produce one would be to re-run the analysis enumerating what was checked, which is a second analysis pass for a sentence. The report says what it knows and the docs carry the signal list.

The one hole in that design is a future rule that adds to mustShip without recording evidence: it would emit evidence: null on a real ship, which reads as "shipped, no idea why". That is not something the type system can catch, so sigil-coverage.test.js now asserts over the blog corpus that every shipping component carries evidence from the closed set. Dropping any one of the six recorder calls reds it, which I checked.

@vivek7405

Copy link
Copy Markdown
Collaborator Author

Decision: the orphan scan now redacts, because dogfooding turned a tolerable warning into a wrong one

findOrphanComponents has always scanned raw source, so every class X extends WebComponent written inside an html template as a code sample counted as a real unregistered class. That was invisible as dev-console noise. It stops being invisible the moment an orphan becomes a webjs doctor warning, which is what this PR does.

I found it by running doctor over our own site: the website reported 17 orphans, every one of them a documentation code sample. Shipping a check that cries wolf on our own dogfood app is the exact failure the check's pass-except-orphans design was trying to avoid, so I fixed the scan rather than soften the check.

The fix is the one extractComponents in the same file has always used: run the scan over redactToPlaceholders output, which blanks string and template bodies while preserving positions. A real class declaration is top-level code, so it still matches; a sample inside a template does not. Both dogfood apps now report zero orphans, and component-scanner.test.js has a case with a sample, a quoted class, and a REAL orphan in one tree, asserting only the real one is reported, so this cannot pass by the scan going blind.

This is strictly a false-positive fix, not a verdict change. Elision decides nothing differently.

@vivek7405

Copy link
Copy Markdown
Collaborator Author

Measured: what static interactive = true actually rescues, and the doc claim that was wrong

Four surfaces said the override covers "a dynamically-computed tag string". I drove the analyser against real on-disk fixtures to check, and that wording conflated two different shapes with opposite outcomes.

What it DOES rescue is the OBSERVER's tag, not the registration's. A shipping module writing customElements.whenDefined(TAG) with a variable misses the observation regex, so the observed display-only component is elided and the await never settles. The override on the observed component fixes it. Same for a :defined rule in an external stylesheet, which is outside the module graph.

What it does NOT rescue is a component whose OWN registration tag is computed. scanComponents requires a literal, so that component is never in the component set at all: nothing consults the analyser for it, and the override has nothing to attach to. The page sees only a register(...) call, which the module-scope side-effect check explicitly exempts, so the page is classified inert and BOTH modules are dropped. The element silently never registers. I confirmed adding the override changes nothing there.

So the fix for that case is documentary plus visibility, not code. Invariant 3 already requires a literal tag, the docs now say so where they used to promise the override, and the shape surfaces as an orphans row in the report and a doctor warning instead of vanishing. residual-contract.test.js pins all three outcomes, so a change in any direction is visible rather than silent.

…ile column

The help renderer hardcoded the heading Config:, which is what the doctor
severity gate is and what its help test pins. The elision block is a caveat
about what --verify proves, not configuration, so an entry now names its own
heading and doctor keeps the default.

The verdict table capped every column at the same width, which is right for the
tag column (a file registering five tags is the rare case) and wrong for the
file column: on a freshly scaffolded app almost every module path is longer
than the cap, so almost every row overflowed instead of one outlier.

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went back over this one properly and it is not ready. The shape of the change is right, and the analyser side holds up: recording the evidence at each mustShip write rather than deriving it later is the correct call, and the verdict is provably unchanged against main on both dogfood apps.

The problem is that two of the three things this PR sells can currently lie. --verify never forces elision ON, so on an app with elide: false it renders the same thing twice and reports the routes identical with elision on vs off, which is a false statement about a run where elision was never on. That is the exact vacuous-pass class the zero-route exit code was added to prevent, and it is worse than the zero-route case because it prints a confident pass. The e2e probe has the same flavour of problem: its declared negative control asserts a module that is not on that page at all, so it cannot fail and the test is really a single positive assertion wearing a control.

The other two are smaller but both mislead a reader: the new docs page's meta description tells people to run a command that does not exist, and list_elision is missing from six places that enumerate the MCP tools, including the published README and the CLI's own help text.

Worth saying what is good here, because the review reads harshly: the residual contract tests are the most valuable part of the diff. Pinning what static interactive = true does and does not rescue, measured rather than asserted from the docs, is what stops the next person repeating the mistake this PR is correcting.

Comment thread packages/cli/bin/webjs.js Outdated
Comment thread test/e2e/e2e.test.mjs
Comment thread website/app/docs/elision/page.ts Outdated
Three things this PR sells could lie.

--verify deleted WEBJS_ELIDE for its ON side, which only falls back to
webjs.elide, so on an app that opts out BOTH handlers ran with elision off and
the command reported the routes identical with elision on vs off and exited 0.
That is a confident pass on a run where elision was never on, worse than the
zero-route vacuity the exit code already guarded. The ON side is now forced on
through the override, which wins over the config key, and the run reports how
many modules elision actually dropped so a trivially-true pass is visible too.

The e2e probe asserted build-stamp was not downloaded on /observed as its
negative control, but build-stamp is only on /, so that assertion was true
whether or not elision worked and the test was one positive assertion wearing a
control. /observed now renders it, and WEBJS_ELIDE=0 reds the control.

The new docs page meta description named two commands that do not exist, and
list_elision was missing from six surfaces that enumerate the MCP tools,
including the published README and the CLI help text.

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Read the fix commit again with fresh eyes, tracing what each change touches across the rest of the branch. The two substantive fixes hold up: forcing the ON side really does make an opted-out app compare a real pair, and the e2e control now reds under WEBJS_ELIDE=0.

What it missed is its own blast radius on the docs. The commit set out to add list_elision everywhere the MCP tools are enumerated and got six of eight: the CLI-reference block in AGENTS.md (the same file it edited, different section) and the start-work skill still list the old four. Separately, examples/blog/AGENTS.md owns an "Elision fixtures" section that enumerates exactly these files, and it is now wrong twice over: build-stamp is documented as rendered on / only, and forced-badge, a fixture this PR adds specifically to be e2e-pinned, is not listed at all. That section is the thing a future reader consults before touching a fixture.

Two smaller ones on the new docs page: the meta description carries literal backticks, which land verbatim in the <meta> tag and the llms.txt entry rather than rendering as code, and the paragraph explaining that --verify forces the ON side was inserted after the two-run e2e code block, so it reads as describing that recipe instead of --verify.

On the exit code for a corpus where elision dropped nothing: I am leaving that at 0 deliberately, reasoning in the thread.

Comment thread website/app/docs/elision/page.ts Outdated
Comment thread website/app/docs/elision/page.ts Outdated
Comment thread packages/cli/bin/webjs.js Outdated
The previous commit added list_elision to six surfaces and missed two: the
CLI-reference block in AGENTS.md, which is a different section of the same file
it edited, and the start-work skill.

examples/blog/AGENTS.md owns the section that enumerates the elision fixtures,
which is what a future reader consults before touching one, and it was wrong
twice: build-stamp is now the negative control on /observed as well as /, and
forced-badge was not listed at all. It also now records the doc-comment
discipline those fixtures depend on, since prose naming a tag or a whenDefined
call would register as a real signal and make the tests pass vacuously.

The new docs page carried literal backticks into its meta description, which is
emitted verbatim into the meta tag and the llms.txt entry, and the paragraph
explaining that --verify forces the ON side sat after the two-run e2e block, so
it read as describing that recipe instead.

The zero-drop message said the run proves nothing about elision while exiting
0, which reads as a self-contradiction. Exit 0 is right there (the question was
answered, and a corpus with nothing elidable is a legitimate app), so the
message now says what actually happened instead of disowning the run.
@vivek7405

Copy link
Copy Markdown
Collaborator Author

Resolution: the two path-level findings from this round

Two of this round's findings had no line in the diff to hang off, so recording them here.

The list_elision sweep was incomplete. I had added it to six surfaces and missed two: the CLI-reference block in AGENTS.md, which is a different section of the same file I had already edited, and .claude/skills/webjs-start-work/SKILL.md. Both now list it. I swept the repo again afterwards for list_components occurrences and the only remaining ones are either already adjacent to a list_elision line or are about the ui tool. The one I left alone deliberately is the published blog post, which describes the tool set as it was when it was written.

examples/blog/AGENTS.md owns the section that enumerates the elision fixtures, and it was stale twice over: build-stamp was documented as rendered on / only when it is now also the negative control on /observed, and forced-badge was not listed at all despite being a fixture this PR adds specifically to be e2e-pinned. Both fixed. I also wrote down the doc-comment discipline those fixtures depend on, which was previously only a comment inside the files themselves: the analyser scans raw source including comments, so prose naming a tag in angle brackets or a whenDefined call shape would register as a real signal and the fixture would ship for the wrong reason, making its own test pass vacuously.

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This round caught me codifying something that is not true. The paragraph I added to the blog's fixture inventory says the analyser reads tags and whenDefined calls out of COMMENTS, so the fixtures avoid writing them. That was true once and #179 closed it: maskComments runs before every signal scan. I drove it to be sure, with a display-only badge whose doc comment names its own tag in angle brackets AND calls whenDefined on it, and the badge is still elided. So the discipline I was promoting from an in-file aside into a normative rule guards nothing.

Worse than being wrong, it was wrong in the one place a future reader goes before touching a fixture, and it would have taught them to write around a mechanism that does not exist. Removing it rather than correcting its count, which also settles the second finding.

The third is a real seam I left open: --verify forces the override on, the plain report path reads the config, and the zero-drop message sends the author to a bare webjs elision. On the opted-out app that forcing exists FOR, that command answers elision is disabled and no verdict at all, so the hint dead-ends exactly where it is most needed.

Comment thread examples/blog/AGENTS.md Outdated
Comment thread examples/blog/components/forced-badge.ts Outdated
Comment thread packages/cli/bin/webjs.js Outdated
The fixture inventory I added said the analyser reads tags and whenDefined
calls out of COMMENTS, so the fixtures deliberately avoid that prose. That was
true once; #179 closed it by masking comments before every signal scan. Driving
a display-only badge whose doc comment names its own tag in angle brackets AND
calls whenDefined on it leaves the badge elided, so the discipline guards
nothing. Removing it rather than correcting its count, since a rule that
protects against a closed failure mode is worse in the fixture inventory than
in the aside it came from. forced-badge.ts carried the same claim and is new
here, so it goes too.

The zero-drop hint also dead-ended: --verify forces the override on while the
plain report path reads the config, so on the opted-out app that forcing exists
for, a bare `webjs elision` answers "elision is disabled" and no verdict. The hint
now carries the same override the run used.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: make the elision verdict inspectable and provable per app

1 participant