Skip to content
digline
digline view

digline view — the local browser UI

$ 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. localhost and 127.0.0.1 are 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 403 here and a 404 on the default server. On the default server nobody may promote, so /promote does 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_html produces 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 /promote with 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