experience-compiler
v0.1.0
Published
Measure what a web application actually looks like, turn the parts a human confirms into promises, and refuse the changes that would undo them.
Maintainers
Readme
experience-compiler
Measure what a web application actually looks like, turn the parts a human confirms into promises, and refuse the changes that would undo them.
It is a command line tool for repositories that draw a user interface — today, Next and React applications. It reads your source, it drives your own application under a sandboxed environment, and it writes what it decided into a directory in your repository that you can read, edit and delete.
npm i -g experience-compilerNode 22.12 or newer. experience --help lists all seventeen commands. Building from
source instead: npm pack, then npm i -g ./experience-compiler-0.1.0.tgz.
Thirty seconds
experience scan --target ./your-appIt reads the source. Nothing has to be installed in that application, it refuses to write inside the repository it is measuring, and on a repository it has never compiled — which is every repository the first time — it opens no browser. What it prints (one real run, on an application of about fifty files):
MEASURED your-app @ main
7 colours · 10 text sizes · 51 buttons · 51 files
report → mirror-your-app.html (435 KB, 283 KB of it embedded typefaces)
1 finding: named-then-typed · 3 of 12 rules could decide, 9 unmeasured (no screens were read)
colour count is a floor: 37 colour classes across 180 places could not be resolved to a value
35 of those 37 carry a variant or an opacity modifier that THIS PARSER does not strip — not a framework utility it cannot see
measured from source only — no screens photographedThree of those lines are the reason to keep reading. colour count is a floor is the
tool saying its own headline number is wrong, in a known direction. THIS PARSER does not
strip — not a framework utility it cannot see is it taking the blame for its own limit
instead of implying your repository is strange. 3 of 12 rules could decide is it
giving you the denominator. Nothing here is a score.
Open the page it wrote. The finding is specific:
A colour you named, and then typed out by hand. You gave this colour a name —
--color-accent— and then wrote the value out 6 times instead of using it, in among 15 other places that do use the name.
…with six file:line locations.
Now check it, because you should not believe it. Change that token, start your application, and ask the page what it actually computed:
getComputedStyle(document.documentElement).getPropertyValue('--color-accent')
// the new value ← the token moved
getComputedStyle(document.documentElement, '::before').backgroundImage
// rgba(34, 197, 94, …) ← still the old colour, one of the sixIt predicted, from source alone, before a single dependency was installed, that moving that token would leave six places behind. It left six places behind. That is the loop.
After you believe it
experience init writes .experience/ — five files, and it never writes over one
that is already there. tokens.draft.json (the colours and sizes this product has names
for), findings.json (what is worth fixing, with where and what instead),
ratchet.json (today's numbers, each recorded with how it was measured),
exceptions.json (empty), and EXPERIENCE.md explaining the directory in five
sentences.
The room. experience console --install writes a designer into your repository —
source, in .experience/console/, plus one route in your application that refuses to
render outside next dev and 404s in a production build. Open /experience in your own
dev server and you get your real application in an iframe, presets you can try on it, and
a column that says, axis by axis, what it can and cannot move: "this application has no
name for its corner radius, so there is nothing to move." --uninstall takes all of it
back out and tells you what it did not undo.
experience propose fits a promise from what a run already measured, and argues
against it. Every proposal carries a section headed THE GAP whose only job is to say
why it is weaker than it looks — how many pages it would reach, how many were actually
opened, and what the next run will say if you confirm it.
A generator does not get to make a promise. propose writes no invariant. Ever.
--confirm is a separate act by a person, and what it writes says so: "it carries no
mark saying a machine drafted it, because a person confirmed it with the evidence in
front of them and that makes it theirs." Everything a machine authored is marked as
scaffolding until a human takes it.
experience ratchet holds the line. Numbers that went past it are named; numbers it
has never measured say UNMEASURED rather than held; and two numbers taken with
different rulers are refused rather than called a regression — "a line drawn with one
ruler cannot be checked with another."
.experience/exceptions.json is the place to say no. One entry answers one accusation
— a reason code, a finding, a ratchet measure, a route — with a sentence you wrote, your
name, and the date it stops holding. An answered accusation is still measured and still
printed; it is printed as answered.
experience pointer puts an address to .experience/ in the file your coding agent
already reads — AGENTS.md or CLAUDE.md — only when asked, exactly once however often
it runs, and it prints the change before making it. What it does not claim:
This does not make an agent read it. It makes it possible to find out whether one did.
It never writes to your database
Every command in this tool that starts your application rewrites the destinations it
finds in your environment first. A DATABASE_URL is replaced by an address that keeps
the shape of the connection string — because an application may pick its driver from
that shape — and loses the address: the host becomes a .localhost label on port 1,
which is loopback by RFC 6761 and refused in milliseconds. Everything else your env files
declare is withheld unless the binding names it, and an observer is armed to catch a
process that dials somewhere it should not. All of it is printed before anything is
spawned.
Two developers who had never seen this product pointed it at their own applications, both of which were configured against live remote Postgres. Here is exactly what each of them did, because the difference matters.
One checked it rather than believing it, from the outside. They read the substituted
value out of the generated file, resolved the host, got ::1, and measured the connection
refused in 1.9 ms — without running the tool at all. That is possible only because the
substitution is written down as a mechanism instead of asserted as a result.
The other walked a whole session under it, and what they reported was the tool declining to certify itself. Its escape observer said nothing had dialled a blocked address — and then the same output refused to conclude that meant safe: "Either this route asks for no data, or its data path is one no hook in this process can see (a native engine, a vendored copy, a bundled driver)." It exits non-zero rather than printing a green. They called that refusal the single most professional thing in the tool, and it is the sentence to judge this section by: silence measured is not safety proved, and this will not tell you otherwise.
The guarantee is over the commands this tool drives. npm run dev, which you start
yourself, is yours — so console prints the exact substitution a run would have used, so
you can start it the same way.
The commands that open a browser
All seventeen, sorted by whether they need one — read off the three places in this engine that actually open a browser, not off which files import which:
- Always.
run,check,improve, andbind --observe. - Only where the repository already carries a compiled contract — which is not the
case the first time you point this at anything:
scan,init,console,adopt,propose, andratchet --render. All six go through one function that returns before starting anything when there is no compiled contract to drive.scan --no-renderandinit --no-renderkeep it that way even when there is. - Never.
status,codes,compile,review,pointer,migrate,seams,bindwithout--observe, andratchetwithout--render.seamsboots your whole application and still opens no browser.
A browser is about 570 MB, so this package does not install one for you:
npm i -g playwright
npx playwright install chromiumIf you ask for a command that needs one and it is not there, the tool says so before it
starts your application, not after. Those two commands are what has been tested, with the
tool itself installed globally; whether a browser installed this way is found by a copy
run through npx has not been measured.
Exit codes
0 clean — nothing blocking
1 blocking findings — a FAIL, an ERROR, or a regression the mode forbids
2 untrusted or incomplete — a dirty tree, a missing artifact, a ledger that no longer
describes the code, a repository whose dependencies are not installed, or a fault here
3 usage, or a refusal from the linker before anything ranexperience codes prints every reason code the engine can emit and what each one means.
Licence
MIT. See LICENSE.
