1# The workbench23**Status: built.** This document specifies the workbench: the pages it4serves, where their facts come from, the operation-runner library, and the5safety properties operations are held to.67The implementation is `internal/workbench`, the runner is8`internal/workbench/oprunner`, and the host is `atlas workbench`9(`cmd/atlas/workbench.go`). Where this document and the code disagree, take it10as a defect in one of them and say so.1112The [application](app.md) serves the build of every volume a reader should13have and asks no questions. The workbench answers the questions: what each build14is worth, what moved between two of them, what the collection owes the people15whose work it carries, and what the pipeline should do next.1617---1819## 1. Running it2021```22atlas workbench [-addr 127.0.0.1:6180] [-bundles DIR] [-archive DIR] [-tiles DIR]23```2425| Flag | What it is | Without it |26|---|---|---|27| `-addr` | Where the workbench listens. | `127.0.0.1:6180`. |28| `-bundles` | The registry of `.atlas` files every measurement page is about. | The application's own library. |29| `-archive` | The capture archive root operations read and write. | Crawl, tiles, compose and enrich say they cannot run. |30| `-tiles` | The derived tile set directory. | Tiles, compose and enrich say they cannot run. |3132The listening URL is printed on stdout as product output; the event stream goes33to stderr like every other subcommand ([logging](logging.md)).3435Reading the library is free and happens on every page load. **Nothing runs on a36schedule or at startup.** An operation exists only between a submitted form and37its subprocess's exit.3839## 2. The pages4041| Route | Page | What it says |42|---|---|---|43| `GET /` | Library | Every volume, headlined by its serving build's score, with the movement against the build before it. Bundles that will not measure are listed as warnings rather than failing the page. |44| `GET /volume/{slug}` | Measurement | The serving score and how it moved; then every build in full — per-world score breakdown, the five absolute axes as diagnostics, and the whole of every ledger. |45| `GET /volume/{slug}/diff?a=&b=` | Diff | Two builds side by side, headlined by the score delta and its verdict; per-world deltas, axis deltas, the features added and removed, and matched-pair stability. |46| `GET /sources` | Sources | One card per capture source: licence, attribution, id space, and whether it is crawled from here. |47| `GET /operations` | Operations | What may be run, what each operation needs, and what this workbench was pointed at. |48| `POST /operations/run` | — | One operation, streamed back as HTML rows. |49| `GET /assets/{path...}` | — | `workbench.css`, and `htmx.js` when a host handed a runtime over. |5051**Measurement first.** Every page leads with the score, because the score is the52only number anything gates on ([enrich](enrich.md), the monotonicity gate). The53five axes — annotation, cartography, structure,54icons, conventions — are printed under it as diagnostics and are labelled as55such on the page. Percentages survive there and nowhere else.5657### The score and its movement5859A volume's movement is `maturity.Compare(previous, serving)`: the same60comparison the build gate reads, printed rather than enforced. Three things61come with it and are all rendered:6263- **Comparability.** Two builds scored under different point-table versions64 conclude nothing, and the page says so instead of showing a delta.65- **The allowance.** A decline is permitted up to what the later build's ledger66 accounts for in corrections; the reasons are printed.67- **The verdict** on a diff — *richer*, *corrected*, *poorer*, *unmoved*, or68 *not comparable* — is that arithmetic in one word.6970### Where the facts come from7172Two sources, and neither is a copy.7374**Scores** are `internal/enrich/maturity` reading the registry directory75directly. The directory is re-read on every request — a build installed by an76operation is on the page at the next load, with no watcher and no cache to77invalidate by hand — and scoring is memoised per file by size and modification78time, the same test the format's registry uses to tell an untouched file from79one rewritten under its name.8081**Source registry entries** arrive as **data, handed to the handler at82construction** by `atlas workbench`. The workbench may not import the generate83lane (issue #5 §3.2), and `cmd/` is the one place allowed to wire every lane, so84the wiring reads each source's own `Describe()` and the crawl registry's own85`Usage()` and passes the result to `workbench.New`. Two alternatives were86weighed and refused:8788- an `atlas sources -json` subcommand the workbench shells out for — it puts a89 second copy of every licence on a wire, and adds a subprocess between a page90 and a fact that is already in the binary;91- a curation data file restating the entries — a licence that exists twice can92 be wrong in one of them.9394Wiring passes the registry entry itself, once. A source added to the generate95lane appears on the card wall with no second edit anywhere.9697## 3. Operations9899An operation is the `atlas` binary invoked exactly as a person at a terminal100would invoke it. The workbench does not link a pipeline lane: it **shells out to101the lane CLIs** (issue #5 §3.1), which is what keeps a lane's work inside its102lane while the page that starts it imports nothing but the format and the score.103104The binary is this process's own executable (`os.Executable`), so an operation105runs the same build of the pipeline that is serving the page. No repository106checkout is involved.107108| Operation | Needs | argv |109|---|---|---|110| `crawl` | archive | `atlas crawl --log-json -archive A -source NAME TARGET` |111| `tiles` | archive, tile set | `atlas tiles --log-json -archive A -output T` |112| `compose` | archive, tile index, registry | `atlas compose --log-json -archive A -tiles T/index.json -bundles R [volume]` |113| `enrich` | archive, tile index, registry | `atlas enrich --log-json -archive A -tiles T/index.json -bundles R [volume]` |114| `measure` | registry | `atlas measure --log-json -bundles R [volume]` |115116An operation whose targets are not all configured is not offered: the card says117what is missing instead of letting a person find out by pressing a button.118119Three request-borne values exist, and each is validated before an argv does:120the operation name (a table lookup), the source name (a registry lookup, plus a121crawler must exist for it), and the target and volume slug (`ValidTarget`,122below). Nothing composes a shell command; argv is a slice and the operating123system gets it as one.124125## 4. The operation runner126127`internal/workbench/oprunner` is a small library the workbench consumes. It is128not workbench-internal code (issue #5 §5.6): the safety properties are the129interesting part, they are testable without a page around them, and a second130consumer of the pipeline gets them by importing rather than by copying.131132```go133type Operation struct { Name, Dir string; Argv []string }134func (o Operation) Validate() error135func (o Operation) Command() string136137type Row struct {138 Seq int // 1, 2, 3… in arrival order across both streams139 Kind Kind // command | event | output | result140 Stream string // stdout | stderr, for the rows the subprocess spoke141 Level string // INFO, WARN… on a parsed record142 Time string // the record's own timestamp, verbatim143 Message string144 Attrs []Attr // documented vocabulary first, then the rest, alphabetically145 Failed bool // an ERROR record, or a result that did not succeed146}147148type Runner struct{ /* the one operation slot; zero value ready */ }149func (r *Runner) Acquire(name string) (release func(), ok bool)150func (r *Runner) Busy() string151func (r *Runner) Run(ctx context.Context, op Operation, emit func(Row) error) error152func (r *Runner) Serve(w http.ResponseWriter, req *http.Request, op Operation, render RowWriter)153func Stream(ctx context.Context, op Operation, emit func(Row) error) error154155func CheckOrigin(r *http.Request) error156func ValidTarget(target string, pair bool) error157```158159**Rows are data, not markup.** `atlas` writes slog records to stderr and JSON160when asked, so the runner parses each line into a `Row` carrying level, message161and the documented attribute vocabulary of [logging](logging.md); a line that is162not a record is carried verbatim, because a tool's own words are worth more than163a runner's opinion of them. Rendering a row as HTML is the consumer's business,164which is what keeps the framework vocabulary in templates (issue #5 §4.3). The165workbench's `RowWriter` is one template, `op-row.tmpl`.166167Every run reads the same way: one **command** row, then the subprocess's own168**event** and **output** rows in arrival order, then exactly one **result** row.169A program that will not start, a program that exits non-zero, and a program that170succeeds all end in a result row — the difference is `Failed`.171172### The safety properties173174Named because they are the contract:1751761. **Origin-checked POSTs.** A browser sends `Origin` on any cross-site POST and177 omits it on an ordinary same-origin form submission. A present one must agree178 with the host the request arrived at, scheme included; a foreign one is179 **403** and nothing is planned, let alone run.1802. **One operation at a time.** The slot is a mutex; a second submission is181 **409** with the name of what is running, never a queue. Both refusals are182 decided before the first row goes out, because once a body is streaming the183 status line is spent.1843. **Target validation.** `ValidTarget` admits lowercase letters, digits, `-`,185 `_`, `.`, and — for a source addressed as two slugs — exactly one interior186 slash. A leading dash is refused so a target can never be read as a flag. A187 bad target is **400**.1884. **An operation dies with its request.** The subprocess runs under the189 request's context: a page abandoned mid-operation stops its operation, so190 nothing crawls on with nobody watching. An emit that fails — the page went191 away — ends the run the same way.1925. **A strict content security policy** on every page:193 `default-src 'none'; style-src 'self'; script-src 'self'; img-src 'self';194 form-action 'self'; base-uri 'none'; frame-ancestors 'none'`. Nothing loads195 from anywhere but this server, nothing but this server's forms may be196 submitted to, and no page may be framed — a page that operates a pipeline is197 exactly the page that must not be reachable through somebody else's document.1986. **Pages are rendered into a buffer first**, so a template error is a clean199 500 rather than half a page.200201## 5. Hypermedia202203The workbench is pure HTMX and has no seam. Real URLs for every view, ordinary204links and forms, and **one** swap: the operations console, which grows a row at205a time (`hx-post` + `hx-target="#op-log"` + `hx-swap="beforeend"`) — the206streaming swap of issue #5 §4.3, the same shape the application's import stream207takes. No `hx-on`; the `hx-*` vocabulary lives in `templates/` and nowhere else.208209The runtime is **not** vendored here. htmx is vendored once, with the210application's assets, and reaches this handler as bytes through211`Options.Runtime`: one vendored copy, one licence file, and no import edge212between two lanes that must not depend on each other. `atlas workbench` — which213may import both — hands it over.214215A workbench mounted without a runtime is a working workbench. Every page is216plain HTML, every form is an ordinary POST, and an operation still streams: the217browser renders the rows as they arrive instead of a swap appending them. The218stylesheet is the identity's token system (carried verbatim from the219application's assets) plus one file of this surface's own.220221---222223## 6. Scope224225The workbench is the pipeline dashboard's whole surface: measurement-first226pages over a registry, source cards fed by the lanes' own registry entries,227and pipeline operations behind the safety properties of §4. Its archived228predecessor is `cmd/cartograph` on the `golden-reference` tag.229230What carries the surface: the score and its movement on every page, the231build gate's own comparison rendered rather than reimplemented, the whole232ledger instead of its counts, the enrich lane's operations beside the generate233lane's, structured rows instead of a text pane, and a runner whose safety234properties are a tested library rather than four paragraphs of handler.