# The Published-URL Ledger — format and rules

A ledger is a plain text file. The value is not in the format; it is in the four rules
attached to it. Both are below.

Version 1.0 · 2026-09-09

---

## Format

```
# Published-URL ledger.
# Every URL here was public at some point and therefore owes a destination forever:
# it must either still be produced by the build, or have a 301 in the redirect config.
#
# Append-only. Adding a line is routine. Removing one discharges an obligation and
# must be an explicit, reviewable change.

# added 2026-09-09
/en/
/en/about/
/en/journal/
/en/journal/rss.xml
/zh/
...
```

- One site-relative path per line.
- `#` starts a comment; blank lines ignored.
- Dated `# added YYYY-MM-DD` blocks make the growth history readable in a diff.
- Directory-style URLs keep their trailing slash; file URLs (feeds, assets) do not.

## Rule 1 — Derive it from the build product

Not from the content directory. Not from the sitemap.

Content-derived is **tautological** for any check about *old* URLs: a page created after a
migration never had an old URL, so deriving the old-URL list from current content asserts
that today's pages redirect to themselves.

Sitemap-derived is **wrong in the other direction**: a page can leave the sitemap and still
answer 200. Language-fallback pages typically do exactly that — they carry a canonical
pointing elsewhere and are deliberately excluded from the sitemap, while continuing to
serve. They owe no redirect. Judged by sitemap membership they look like disappearances.

## Rule 2 — One implementation of "published", shared

The gate that reads the ledger and the tool that appends to it must import the *same*
function. Two implementations of the same definition drift, and a drifted definition does
not fail loudly — it reports disappearances that never happened.

## Rule 3 — Append-only, and appending is not part of the build

Adding lines is a command a person runs after a build. It must never be wired into the
build itself:

> A verifier that rewrites its own expectations can only ever get greener.

The append tool should also **refuse to run** when the build product enumerates zero URLs.
A broken walker and a site that published nothing look identical, and only one of them
should be allowed to empty your ledger.

## Rule 4 — Deleting a line is a decision, not a cleanup

It discharges a permanent obligation. It belongs in a diff a human approves, with a reason.

---

## The four checks to run against it

For each recorded URL, on every build:

| # | Check | Failure means |
|---|---|---|
| 1 | Still produced by the build? | Nothing owed — stop here |
| 2 | Otherwise, a redirect rule matches it | A rename or removal with no redirect |
| 3 | That rule's status is `301` | A permanent move announced as temporary |
| 4 | That rule's target exists in the build | A redirect into a 404 |

Plus two **anti-vacuous** assertions on the ledger itself, without which the four above can
pass while guarding nothing:

- The ledger is **not empty**.
- The ledger **shares at least one URL** with the current build product. Zero overlap means
  it is the wrong ledger, or the URL shape changed underneath it.

And one deliberate non-failure: **a newly published URL that is not yet recorded is not an
error.** The ledger is allowed to lag. Print the outstanding count on every build so the lag
stays visible instead of becoming invisible debt.

## Proving the ledger still bites

Green means nothing went red. It does not mean the gate is still watching. Periodically
inject each of these and confirm the expected outcome:

| Injected state | Expected |
|---|---|
| Delete a published page, add no rule | Fail |
| Add the rule with a temporary status | Fail |
| Point the rule at a nonexistent target | Fail |
| Point the rule at a real target | Pass |
| Remove one ledger line (simulating an unrecorded new page) | Pass, with a notice |
| Empty the ledger | Fail (vacuous) |
| Replace ledger paths with another site's | Fail (wrong ledger) |

If any of these passes when it should fail, the gate has stopped guarding and nobody would
have known.
