dist.town

docs/How dist.town thinks

How dist.town thinks


A handful of ideas carry the whole system. None of them is complicated, and together they explain every URL and command.

Projects

A project is a name in your repo: lowercase letters, digits, and interior hyphens, at most 64 characters, no leading or trailing hyphen. Names can nest — dist-town/cli is a component of dist-town — and nesting is never required.

Nesting has two spellings for one name. URLs join segments with /; records and at:// URIs join them with :, so the page at /graham.systems/dist-town/cli is the project record named dist-town:cli. The CLI and the read plane accept either form. Hierarchy lives in the name itself; there is nothing else to manage.

Publishing a version creates its project automatically. An umbrella is a project like any other, and it needs its own record before it has a page: /{handle}/{umbrella} lists the children of a project that exists, and answers 404 for a name that only appears as a prefix. disttown project create writes the record for an umbrella you never publish releases under.

disttown project rename renames without breaking links. The old name stays behind as an alias: its page still renders, with its own releases and pointers, now carrying a note and a link to the successor. Nothing redirects — old URLs answer where they always did — and the alias drops out of the publisher’s project list.

Releases

A release is an immutable record: a version string (semver, dates, whatever your project uses — dist.town doesn’t interpret it), artifacts with sizes, platforms, and sha256 digests, and lifecycle status. Once published, a release never changes — corrections are new releases, and problems are handled by status:

  • yank — do-not-newly-adopt. Anyone pinned to the version keeps working; channel resolution stops offering it.
  • deprecate — still resolves, always surfaced with a warning.
  • archive — the word for something finished. It reads most naturally on a project, and applies to a release the same way.

Two records can’t both be canonical for one version. If a later release record claims a version an earlier one already claimed, the earlier record wins by creation time and the later is marked disputed: it never resolves, never becomes a pointer target, and never appears in feeds or summaries. It isn’t hidden either — the project page lists it as a disputed claimant, so an accidental double-publish is visible rather than silent.

Retraction

Yank is the norm, because it keeps every pin working. Retraction is the exception, for a release that should never have existed: disttown release retract deletes the release record and, in the same commit, writes testimony — when, an optional kind (broken, mistake, and so on), and a reason.

The version does not become a 404. The read plane answers 410 ReleaseRetracted carrying the testimony; the front door answers 410 with X-Dist-Status: retracted and a body reading retracted by the publisher <RFC3339 timestamp> (<kind>): <reason>; the release page renders that testimony as a page. A pin the publisher deliberately removed gets an answer, not silence.

Testimony matches on project and version together, so a sibling project that happens to share a version string is unaffected. What retraction cannot do is un-download the bytes, and once the record is gone there is nothing left to repair — which is why yank is the right tool for anything that ever had a consumer.

Locations and verification

An artifact’s record says where its bytes live. dist.town fetches each location and hashes it against the digest in your signed record, and each location carries the state that check left behind:

  • pending — not verified yet. Downloads answer 425 until it is.
  • verified — bytes hashed, digest matched.
  • failed — bytes at the origin do not match the record.
  • suspended — the origin’s ETag or length changed since verification; a full re-check is pending.

A location is also either declared — derived from the release record’s own storage — or discovered, found at index time because its bytes matched a digest your record already states. Verified locations are re-checked on a schedule (24 hours by default), and pinned again at redirect time: if the origin changed since the last full verification, the download is refused rather than followed.

The front door redirects to the location your record declares. Discovered locations appear on the release page and in the read plane’s locations array — evidence about where else the bytes are, not a second download URL.

Pointers

Pointers are the mutable names — latest, next, whatever channels you define. mytool@latest resolves through the pointer; mytool@1.0.0 names a version directly. Publishing moves latest by default — unless you name a channel, in which case the default pointer follows the channel instead: publish --channel beta 2.0.0-rc1 moves beta and leaves latest alone, so a release candidate can never take latest silently. Naming any --pointer replaces the default entirely, and --no-pointers moves nothing.

Yank and pointers interact deliberately, and not identically on every surface. The front door refuses a pointer whose target is yanked: 409 with an explanation, so no script downloads a known-bad version by following a channel. The read plane and this site answer differently on purpose — they return the yanked release itself, flagged, with a banner saying what happened. Nothing anywhere substitutes a different release for the one a selector names, and a pinned version always resolves.

Changelogs and publications

Release notes are markdown, published as standard.site documents — plain records any standard.site reader renders with no dist.town involvement. Attach them at publish time (publish --notes) or later (disttown changelog set); one document can describe several releases, and re-running the command edits in place.

A document belongs to a release when it carries a release backlink and lives in the same repo as the release. Same-repo authority is structural: documents are stored per repo, so a stranger’s document can never attach itself to your release. If several of your documents backlink one release, the one with the highest record key wins.

What this site renders from a document depends on what it carries. at.markpub.markdown content renders to HTML (GitHub-flavored markdown, sanitized). A document in any other content format falls back to its plain text. A document with neither, but with an http(s) site and a path, renders as a link out to its canonical URL. A document with none of the three renders empty.

A publication groups a family of components’ changelogs under one standard.site identity — created once with disttown publication set, anchored at your umbrella project, linked from as many components as you like. dist.town reads the link and passes it through the read plane; reading a publication as a feed is a standard.site reader’s job, not this site’s. Documents may carry tags; here they render as plain labels and filter nothing.

Source

A release can say which source snapshot it corresponds to: publish --tag v1.2.3 pins the git commit — and, for annotated tags, the tag object — into the release record. Because git is content-addressed, that one hash pins the entire source tree. The tag name is just a label (tags can be force-moved; hashes can’t), so tooling trusts the hashes and displays the name.

Prefer annotated tags (git tag -a): only they have a tag object, and the tag object hash is what forge integration keys on. Set your project’s source once (it accepts an at:// record URI or an https:// forge URL) and every release inherits it; a release only states its own repo when it differs.

Be clear about what this claims: association, not build provenance. A source link says “this release corresponds to that snapshot,” not “these bytes were built from it” — that would take reproducible builds, which no registry gives you today. One thing is checked structurally: when your project’s source is a record in your own repo (a Tangled repository, say), the link is signed by the same key as the release, and pages badge it same-repo. An https:// source renders as the plain claim it is.

A separate, weaker claim records where a project came from: derivedFrom names an upstream project a fork descends from. It is written into your manifest when you adopt someone else’s manifest under your own identity, and it lands on your project record as a claim nobody verifies. It renders on the fork’s own page only — it is never aggregated onto the upstream’s page, because an outbound claim is not the upstream’s statement.

Tangled artifacts

If your CI already uploads artifacts to Tangled, dist.town treats them as first-class locations rather than a rival copy:

  • At publish time, publish --tag finds your sh.tangled.repo.artifact records for that tag and offers to attach them (--attach-tangled skips the prompt in CI). The CLI downloads each blob and hashes it locally — every digest in your release is your own measurement, never the forge’s claim — and the release stores only a reference to the Tangled record.
  • Afterward, if a Tangled upload’s bytes match a digest your release already declares, it joins that release as a discovered location — including for releases published before the upload existed. It shows on the release page and in the read plane; the front door keeps redirecting to the location the record declares.
  • Order matters for what lands in the record: upload to Tangled first, then cut the release. The record is immutable, so a later upload can never be added to it. It can only be recognized as another location for a digest already there — and an upload matching no digest in the record appears on the release page under an explicit “not in the record” caveat, unverifiable by construction.

dist.town reads Tangled’s records; it never trusts them. If their schema drifts, discovery degrades — verification doesn’t.

Moderation

dist.town moderates its own surfaces and nothing else. Suppressing a subject hides it from this site’s pages and from the read plane; the records stay in the publisher’s repo, where anyone can still fetch them. Suppressing an umbrella suppresses its subtree, matching the way hierarchy works everywhere else.

Actions are legible. A suppression publishes a signed label under the service identity did:web:dist.town, with the record’s at:// URI as the label subject — for a project, its project record. Lifting a suppression publishes a negation. Reasons stay out of the labels. Resolving a report without acting on it publishes nothing, because nothing was done to any subject.

Reports arrive at com.atproto.moderation.createReport on this origin; intake is rate-limited per source address. A report sent through your own PDS with service auth is attributed to you; one sent from the report form is anonymous.

The lens

Here’s the part worth understanding once: dist.town holds none of this. Every project, release, pointer, and changelog is a record in your repo, signed by your identity. Artifacts live on your PDS or storage you choose. Downloads redirect to your storage; checksums derive from your signed records; every page the site renders is built from data anyone can fetch without asking dist.town. (One decoration is fetched elsewhere: a publisher’s avatar and display name come from the public Bluesky appview.)

That has practical consequences. Your releases resolve even if dist.town is down — the chain from your handle to verified bytes needs no dist.town service. If your storage dies, you can repair every historical release without editing a record. And anything this site shows, your own tools can fetch the same way — there is no private API.

The CLI’s own reads follow the same split. disttown release list and release show ask the index, because release records are keyed by time and the index has already organized them by project; disttown project list and pointer list always read your repo directly, because those records are keyed by name and your repo answers instantly. Every read says which source answered it, --pds forces the repo for the reads that would otherwise ask the index, and --compare reports exactly where the index and your repo disagree.

dist.town is a lens over data you already own. The convenience is the product; the custody was never ours.

The claim isn’t rhetorical: disttown update is a running proof. When the CLI updates itself, it resolves its publisher’s identity, follows the latest pointer in their repo, fetches the release record, and verifies the download against the record’s digest — never touching a dist.town service at any step. If this site went dark, the updater would not notice.