$ digline view --suite suite.py
digline view on http://127.0.0.1:7373/ — read-only; --allow-promote to promote — ctrl-c to stop
Screens over .digline/, stdlib only, no JavaScript, no state of its own:
no session, no preference, no cache. Restarting it loses nothing, because there
was nothing to lose. --host and --port are available; --port 0 lets the
operating system choose and the printed line carries the bound port.
It promotes nothing unless you say so. Without --allow-promote there is
no button on any row and POST /promote is not a route — a POST to it gets the
404 any unknown path gets. With it, it promotes only for the first browser
that opens the address it prints, and that address works once. See below.
The screens¶
Run list (/) — key, date, environment, commit and the aggregates. It
is the table you choose a run to promote from, reading precision and accuracy
down a column and taking the median. Without the aggregates the choice cannot be
made and the first green run gets frozen as the baseline.
Every action belongs to a row; there is nothing to select first.
| On the row | What it does |
|---|---|
Compare |
this run against the baseline — GET /compare?run=KEY |
Open |
before the first promotion, in place of Compare: the run on its own, at the same address |
Make baseline |
promotes it — only under --allow-promote |
On a server started without the flag there is no Make baseline anywhere, and
nothing stands in its place: that is a fact about the server, not about a
run, so it is said once in the header beside the suite name and never down a
column of rows. The markers below are the other kind — this run cannot be
promoted — and they stay either way, because they go on being true.
Where the flag is given, the button appears only where promote_baseline would
accept. Where it does not, a marker takes its place and its tooltip carries the
sentence:
| Marker | Why there is no button |
|---|---|
BASELINE |
it already is the baseline; promoting it to itself is not an action |
OLDER SUITE |
produced under an earlier version of the suite — the row stays, attenuated: comparable still, since those numbers were measured, only under other rules |
N NOT JUDGED |
N cases errored, and an error is not a reference |
The order is promote_baseline's own — the suite before errors — so the
page can never announce a different reason from the one the call would give. The
store's refusal stays underneath as the second line of defence.
Below the table, two pickers compare any two runs. Comparing with the baseline is not among them: that question is the button on the row.
Comparison, or diff (/compare) — one route, two questions, and the
reference is what tells them apart.
Against the baseline — which is what the row's button asks, and what an
omitted against means — this is a run held against an approved reference.
That is compare()'s question, so the page is the verdict document: it calls
render_html, the same function digline report writes to a file, and the
unified diff of any file under test appears here too, above the score deltas.
With no baseline yet there is nothing to hold the run against, so the
page is the run on its own: render_run_html, which is what digline report
writes in the same state, with its cases and the sentence saying there is no
reference. It is what the first baseline is chosen from — the table above
carries aggregates only, and a first promotion made from a row of numbers is
made by somebody who has not seen one case.
Against any other run — the two pickers — neither side was approved by anybody, so the page is the diff report instead. Until ADR 0008 this route rendered the verdict for every pair, which put two candidates under a heading asking "Did it get worse?" beside a column called "Reference": the diff's need served with the verdict's semantics. A refusal from a diff — two runs measured under different rules — arrives here as the same sentence the CLI prints, because it is the same refusal and a refusal worded twice would be two refusals.
History of a case (/case/<id>) — one row per run, one column per
assertion, and for sampled assertions the raw votes under the combined
score. That is where judge noise is visible, and it is the calibration table
that used to be built by hand with a script.
Suspension (/suspend/<id>) — produces the line to add to the suite and
writes nothing. A suspension lives in the code, so the reason travels with the
case in the same review as everything else.
Promotion, and the flag that enables it¶
digline view --allow-promote is the server that promotes. digline view is
not, and that is the default.
The flag is spelled in full, and so is every view option. Other commands
accept a shortened option, --loc for --locale, but view does not. The
Claude Code plugin asks a person before any command carrying
--allow-promote, and it recognises the flag by its name. On 0.20.0, --a
started the same server and nobody was asked, so now the full word is
the only spelling view takes.
The reason is not that the two surfaces should match. It is the reason the MCP
server has no promote tool, applied to the same facts: a baseline is an
approved reference, the approval is a person's, and a rule about who
decides is not satisfied by a surface that lets a non-person decide, whatever
that surface is called. The default falls on the cheap side of an asymmetry —
forgetting --allow-promote costs a restart and the header tells you which
eight characters to add; forgetting a hypothetical --read-only costs an
unreviewed baseline, silently. (ADR 0032)
Without the flag the refusal is an absence, in both places a caller looks:
no button on the page, and /promote is not a route on the wire. A POST to it
answers 404 no such action, the same as any unknown path — not 403, which
would say you may not and so imply a someone who may. Both come from one
value, so the page and the dispatcher cannot drift into offering a button the
route rejects.
The Claude Code plugin's hook stays silent on digline view and asks a person
about digline view --allow-promote. The flag is not a smaller thing than
digline promote but a larger one: the same decision, made ambient for every
run in the store for as long as the server is up.
It promotes for the browser that opens the address it prints¶
$ digline view --suite suite.py --allow-promote
digline view on http://127.0.0.1:7373/?launch=… — promotion enabled, for the first browser that opens this address — ctrl-c to stop
Open that address. The server trades the launch value for a cookie and
sends you to / without it, and from then on Make baseline works
in that browser until the server stops. A POST without that cookie — a
curl, a script, an agent's shell, another browser — is refused with 403.
The address works once. Opening it spends the key: the browser gets a separate secret as its cookie, and the same address opened again is refused. This matters because the address does not stay in your terminal. A browser writes the pages it opened into its history file, and the key used to stay valid there for as long as the server ran. In 0.21.0, a process that read that file could promote with it. (ADR 0033 §11)
The flag decides whether this server may write. It cannot decide who is asking, and before this, once a person had started the flagged server, any process of that user could move the baseline with one POST, and the plugin's hook never saw it. The key is what closes that. (ADR 0033)
- There is nothing to configure. The key is minted when the server starts and held in its memory — never written to disk, never read from a file or the environment, and no option sets or disables it. A key that could be supplied would live somewhere every process of the same user can read, and a key that could be switched off would become the control nobody reads.
- It is never in a page. Every screen answers anybody on the machine, so the key only ever appears on the startup line.
- What changes for you: restarting the server gives a new key, so an open
tab or a bookmark from the last start is refused, with a sentence saying why.
localhostand127.0.0.1are different hosts to a browser, so stay on the one the line printed. A second browser, or the right one after opening the address in the wrong one, needs a restart and the new address: the old address has been spent. - Why a
403here and a404on the default server. On the default server nobody may promote, so/promotedoes not exist. Here somebody may, so a caller without the key is refused rather than told there is nothing here. Same fact, two servers, two truthful answers.
With the flag, POST /promote goes through the same promote_baseline as digline promote,
with the same refusals, and it checks the Origin header. The form carries
the key of the baseline the page was drawn against, as --replacing does on the
command line: if somebody promoted in between — a second tab, a second person on
the same server — the promotion is refused and names both keys rather than
replacing a reference nobody on this page compared against. Binding to loopback is not a boundary: any page
open in the developer's browser can POST to localhost. A missing Origin is
allowed — that is curl, which the launch key refuses instead — and an
Origin that is not ours is refused with 403. The origin check stops a page;
the key stops a process.
A path outside the routes is a 404 and never a file: the view serves pages it
renders, never bytes off the disk.
What one connection can cost the server is bounded. A promotion form is at
most 4096 bytes. A larger declared Content-Length is refused with 413
before any of the body is read. A length that is not a count is refused with
400, and so is a body that ends before its declared length. Every connection
has 30 seconds to send its request and to take its answer. A body that stops
arriving is answered 408, and headers that stop arriving are dropped.
Without these limits, one caller that declared a large body or went quiet held
a server thread for as long as it liked.
What keeps it honest¶
Every screen is a pure function in digline.report.pages, so the tests are
about the page and not about a socket. Two of them are structural rather than
cosmetic:
- the comparison page, with the navigation bar and its stylesheet stripped out,
must equal what
render_htmlproduces byte for byte. If the page and the exported document ever disagreed about a run, that would be a defect and not a difference of medium. - the server is exercised end to end in a subprocess, for the things only a
real server can show: that the routes are wired, that a POST from another
origin is refused, that the default server answers
POST /promotewith a 404 and leaves the baseline where it was — a refusal that only returned the right status would prove nothing about the store — and that the flagged server answers a POST without its key with a 403, the store read on both sides again.
See also¶
api.md— the public APIadr/0002-three-worlds-and-where-the-data-lives.md— why the baseline is a reviewed artifact and what may cross a perimeter