@flashyos/delivery
v0.1.0
Published
delivery/1 — the rungs between a merged commit and a thing somebody actually has. Published, distributed and linked are measured by fetching, never declared.
Maintainers
Readme
@flashyos/delivery
The rungs between a merged commit and a thing somebody actually has.
npm i @flashyos/delivery
npx delivery file the-thing --title "The thing" --url https://example.com/the-thing
npx delivery observe # fetches, and moves the ladder
npx delivery board # what is coming, what is stuck, what fell overOr, for a repository with no package.json: copy vendor-delivery.mjs in and
run it with node. It is the same rules in one dependency-free file, and CI
asserts the two agree.
The problem
Most organisations have a backlog and a changelog. The backlog says what somebody wants. The changelog says what somebody merged. Both are declarations, and between them is a gap wide enough to lose a quarter of work in.
A merged commit is not a page a reader can open. A page a reader can open is not a page a machine can find. A page a machine can find is not a page anything else points at.
Each of those fails separately, none of them appears in either record, and work that failed at one of them looks identical to work that succeeded. This estate found nine finished articles sitting on nine branches, every one of them "shipped" by any reasonable reading of its git history, and none of them readable by anybody.
The idea
Seven rungs, and a line drawn across the middle of them.
filed → committed → built │ shipped → published → distributed → linked
─────── declared ─────── │ ────────── measured ──────────Everything left of the line, somebody says. Everything right of it, something checks:
- shipped — git says the ref is reachable from the default branch.
- published — a fetch of the URL returns a body.
- distributed — the URL appears in a machine surface the property publishes.
- linked — the URL appears in a document served by a different host.
There is no verb that sets those four. Not "you shouldn't"; there is no
function, no flag, and no field. advance(item, { to: 'published' }) throws and
tells you to run observe.
Why the line is where it is
A board is only worth reading if a green tick cannot be typed in. Every project tracker in the world has a status field, and every one of them drifts, because the cost of moving a card is zero and the cost of checking is not. Moving the last four rungs onto evidence makes the cheap action impossible and the expensive one automatic.
It also inverts a rule this estate already runs, and the pair is the whole disposition:
| | who may act | why |
|---|---|---|
| backlog/1 promotion | only a person | disclosure is consent, and an agent cannot consent for you |
| delivery/1 observation | only a machine | measurement is not consent, and a person asserting one is a claim in a lab coat |
What the board tells you that nothing else does
STUCK (3) — built and not shipped for longer than the window
The escape test [built, since 2026-08-14]
Machine-readable organisation [built, since 2026-08-16]
A2A: the provider resolves [built, since 2026-08-17]
REGRESSED (1) — was further up the ladder than it is now
The nth-node test [shipped, since 2026-08-29]
LATE (1) — committed to a date that has passed
Organisations resolve on flashyid [committed, since 2026-08-20]Stuck is the column the format exists for: finished work that never reached anybody. Its window is fourteen days, the shortest in the format, because a branch is where work goes to die quietly.
Regressed is the one nobody else has. Something was live and now is not, and without an append-only history that is indistinguishable from something that was never live at all.
Two dashboards, one file
emit writes the full record to the repository and a public projection to
whatever paths serve names. The projection is not a copy with a filter bolted
on; it is computed from the ladder itself:
- Items at
publishedand above are public, because a stranger can already fetch them. Withholding the row while serving the page would not protect anything, it would only make the board wrong. - Items below are private unless a person promoted them — a public roadmap, in other words, which is a disclosure and needs consent.
partnernever appears on the unauthenticated tier at all.
So the public site can render "what is live across the estate" and the private board can render "everything, including what is stuck", from one source, with no possibility of the private half leaking into the public one by an editing mistake.
Configuration
.delivery/config.json:
{
"source": "repo/gold-holdings",
"org": "org/gord-holdings",
"agent": "agent/gold-holdings-ci",
"branch": "origin/main",
"hosts": ["gord.holdings"],
"surfaces": [
"https://gord.holdings/sitemap.xml",
"https://gord.holdings/llms.txt"
],
"neighbours": [
"https://gda.group/",
"https://flashyos.com/standard"
],
"serve": "public/.well-known/delivery.json"
}hosts is what stops a property settling linked against itself — list every
domain the property serves, or a DNS record will earn the rung.
neighbours is what a round checks for inbound links. It is a list rather than
a crawl on purpose: a format that goes looking for backlinks is a search engine,
and would be wrong about private properties.
agent must be an agent/ node. The config is rejected otherwise, because it
is the identity that settles measurements and a person cannot hold it.
Where a round has to run
The last four rungs cannot be settled from a developer machine, and the format
would rather say so than guess. shipped needs the repository's full history —
a shallow clone answers "not an ancestor" for everything older than its depth
and would silently un-ship the whole board. The three network rungs need
unrestricted egress to the properties being checked.
The first real round of this estate's own board came back 403 Host not in
allowlist, from a sandbox egress proxy answering on the origin's behalf. The
board refused to read that as a live page and refused to read it as a dead one,
which is correct, and it means the honest state off CI is "shipped, and not yet
checked".
Two consequences worth knowing before you run it:
- The fetcher retries silence, never an answer. A dropped connection is retried with backoff; a 404, a 500 or a 403 is the server telling you something, and asking again only makes the round slower and ruder.
- A failed observation quotes the first line of the error body. A bare "returns 403" cost ten minutes; the body said exactly what had happened.
In CI
- run: npx delivery observeExits non-zero on a regression or on a page that is live from an unmerged branch. Both are production incidents; neither shows up in a test suite.
Reading the code
| file | what is in it |
|---|---|
| src/ladder.ts | the rungs, the declared/measured split, the decay windows |
| src/url.ts | why "does this document mention that URL" is not includes() |
| src/observe.ts | the three network rungs, as pure functions over fetched documents |
| src/probe.ts | the one impure step, and why it retries silence but never an answer |
| src/deliverable.ts | the record, its constructors, the checker, the board |
| src/reconcile.ts | applying observations, including falling back |
| vendor-delivery.mjs | all of the above in one dependency-free file |
SPEC.md is the format, written so somebody can implement it without reading
any of this.
Apache-2.0.
