pipelines/ROADMAP_FRESHNESS.md
T99-operational. Sets no canon. Subordinate to the CVD, the T1 tier docs and the T0 registries
in the authority order at docs/START_HERE.md:10-22. This file defines when a per-pipeline roadmap
row counts as current, who owns moving it, and what measures it.
The question it answers, Josh, 2026-08-09: *"how's the roadmap for each pipeline staying up to
date"*. Before this file the honest answer was: nothing. The roadmap document carried a
supersession-in-place law (docs/ROADMAP_VERTICAL_SLICE_2026-08.md:9-12) and no instrument, so
compliance was a habit rather than a reading.
---
A per-pipeline roadmap is a claim about the future written against a snapshot of the present. Work
lands underneath it; the snapshot does not move; and the row goes on reading as current, because a
document has no way of noticing that the world changed. **Staleness with no surface is
indistinguishable from freshness** — which is the same shape as the night's F4 (a real debt
recorded only in a commit message, where no surface tracks it) and F3 (a record asserting a deploy
while the live page still showed the superseded text).
The first run of the instrument found the class fully instantiated: **six of the seven §4 pipeline
sections had not been edited since the day the document landed**, while five of those six had work
landing underneath them. One section — Characters — had moved twice. Nobody had done anything
wrong; there was simply nothing that made the gap visible.
C1 — THE CLOSE THAT LANDS THE WORK OWNS THE ROW. A wave close that lands work in a pipeline
updates that pipeline's roadmap section in the same close, edited in place per the document's
own supersession law. Not the next close, not a follow-up commit, not a note in the commit message.
A row moved in the same close is a row nobody has to remember.
C2 — THE CLOSE RITUAL RUNS THE INSTRUMENT. Every seat's close ritual runs:
python harness/roadmap_freshness.py --emit
which prints the full table to the console and appends one conforming block to
build/ops/SEAT_REPORTS.md — the channel the director already reads at touchpoints. Staleness
therefore becomes visible the moment it exists, in the surface that is already read, at zero
director token cost. A seat that lands pipeline work and sees its own pipeline in that block has its
next action named for it.
The block is not the table, and the difference was a real defect for the emitter's first hours.
The channel is parsed and rendered by harness/site/seats.py, which skips markdown headings and
renders each block as ONE clipped line of escaped text (_seat_line, 190 characters) — so the
original heading-plus-table append arrived as several undated, unattributed blocks, and on a
phone as a truncated run of pipe characters. What lands now is a head line in the section-1 format
(UTC first, hyphen-free seat, blocked-on: last) plus one short bullet per stale pipeline, with **no
blank line inside the block** because a blank line splits it in two. Controlled by self-test C7,
which parses the emitted head line with seats.py's own regexes rather than checking it by eye.
C3 — WHAT "UPDATED" MEANS. Editing the section's honest-state paragraph, or the affected step's
DONE WHEN / status, or striking a step the work closed. Touching the section to touch it is a
defect: the instrument reads git, and a whitespace commit satisfies it while lying to Josh. **The row
must say something that was not true before the work landed.**
C4 — THE ESCALATION. A row that cannot be moved honestly — the work contradicts the plan, or the
pipeline needs a ruling before its next step can be written — is one line in
build/ops/NEEDS_DIRECTOR.md. It is not left stale in silence.
C5 — ADVISORY, NOT A GATE. roadmap_freshness.py exits 0 whatever it finds. This is deliberate
under the CREATION-FIRST LAW and the gate moratorium: it ships as a surface, not as a new red
gate in run_gates.py. --strict exists for a caller that wants teeth, and turning it on anywhere
is a decision with a named owner, not a default.
C6 — THE DEPLOY AXIS BELONGS TO THE SITE LANE. A fresh document behind a stale deploy still
shows Josh the old plan, because the site's roadmap pages are generated from the document at
build time (harness/site/roadmap.py:54). This instrument reports how far the published commit
trails HEAD, read from build/site_deploy_record.json; it never builds or deploys. Closing that
axis is harness/site/build_progress_site.py's (see §5).
harness/roadmap_freshness.py subtracts two dates per pipeline, both read off git:
git log -L <a>,<b>:<file>, thesection's own last-change commit. A file-level date is a lie on a document where one section
is edited daily and six have not moved since they were written: the file would have read
"2026-08-10" and every pipeline would have looked current. The line range is resolved from the
live heading, and a section ends at the next heading of the same or shallower depth — the same
swallow-the-following-chapters bug harness/site/roadmap.py:94-100 records having shipped once.
game build) whose touched paths classify to that pipeline's family.
The classifier is not redefined. It is imported from harness/site/ledger.py — the same
FAMILIES path rules, the same majority vote, the same NOISE co-traveller list already used by
the live ledger and the site's roadmap pages. A second copy of those rules would be a second answer
to one question and it would drift in exactly the silence this instrument exists to break. The
mapping in harness/roadmap_freshness.py:SURFACES therefore carries no path globs of its own —
only the binding from roadmap section to family id, which is the one thing ledger.py does not
already know.
Two tolerance axes, and both are load-bearing. A row is STALE when either is exceeded:
more than --grace days (default 1) or more than --grace-commits landings (default 3) behind. A
wave lands several commits and updates its row at the close, so an in-flight wave must not read red;
but the first run proved a day-only rule blind — music-audio read FRESH at a one-day gap while
carrying thirteen landings its section did not mention. A pipeline can move a long way inside a
day.
Pure-bookkeeping landings are not work. A commit whose every path is a ledger.NOISE
co-traveller (a scorecard rewrite, a DOC_MAP row, the roadmap document itself) does not count.
Without that rule the roadmap would be made stale by its own update
(docs/ROADMAP_VERTICAL_SLICE_*.md is itself a NOISE path), and a measurement that can never read
green measures nothing.
Verdicts. FRESH · STALE (with the gap in days AND the count of landings since) · IDLE (no
landings in the window — reported as its own state, never as freshness, because an uncontrolled zero
is the failure this repo names by name) · MISSING (the surface or its heading is gone — reported
loudly, because a row that silently vanishes from a freshness table is the freshness table lying).
Recorded because both were hit while cross-checking the first run, and both are silent:
git log --since=<bare-date> is approxidate. A date with no time is resolved using thecurrent time of day, so a run at 06:12 silently excludes everything that landed before 06:12
on the boundary day. Two real music landings (e4e27141, 4aa90b87, both 00:xx on 2026-08-09)
vanished from the cross-check for exactly this reason.
git log -- <pathspec> applies history simplification. It prunes commits TREESAME to aparent, so a pathspec-filtered log is not a complete list of commits touching that path.
The instrument avoids both: it reads the full log with --name-only over an N days ago window
(ledger.git_log) and classifies paths itself. Anyone re-deriving these numbers with a
pathspec-filtered --since=<date> query will get a smaller answer, and the instrument is the
one that is right.
python harness/roadmap_freshness.py --self-test — eight positive controls, each named for the
failure it catches: every surface heading still resolves (C1); section ranges are non-degenerate and
disjoint (C2); every family id bound here exists in the ledger, so no row is bound to a bucket
nothing can fall into and reads FRESH forever (C3); the work sweep actually matched in at least
three families, so a dead sweep cannot masquerade as a quiet factory (C4); the day arithmetic and
the grace threshold mutate in both directions (C5); the commit axis fires on its own, controlled
with the exact case only it can catch — inside the day grace, past the landing grace (C5b); the
roadmap document is confirmed to be a ledger co-traveller (C6); and the emitted channel block is
parsed by the SITE'S OWN reader, so a format regression fails the self-test instead of silently
renaming the writer and undating its report on Josh's phone (C7).
harness/site/build_progress_site.py belongs to the site lane; this file does not edit it. The hook
it should call, when that lane wants it:
import roadmap_freshness # harness/ is already on the path at build time fresh = roadmap_freshness.measure() # dict; never raises on a missing surface
fresh["rows"] as a small freshness strip on /roadmap/ — one chip per pipeline carryingits verdict, the date its row moved, and the landings-since count. A roadmap page that shows its
own staleness cannot mislead the reader who is looking straight at it.
fresh["deploy"]["behind_commits"] is the deploy axis; publishing it on the page closes the F3shape from the page's own side.
emit() is called explicitly, so it is safeinside a build that a critic may run on a shared tree.
build/ops/SEAT_REPORTS.md — where --emit appends; the director's read surface.build/ops/NEEDS_DIRECTOR.md — where an unmovable row escalates (C4).docs/ROADMAP_VERTICAL_SLICE_2026-08.md §4.1-4.7 and docs/PIPELINE_LEDGER.md — the measuredsurfaces.
harness/site/roadmap.py / harness/site/ledger.py — the generated pages and the sharedclassifier.
Window 42 days, grace 1 day / 3 landings. Verdicts as measured, not as hoped:
| pipeline | verdict | row moved | newest landing | gap | landings since | breach |
|---|---|---|---|---|---|---|
| qa-ops | STALE | 2026-08-08 | 2026-08-10 | 2d | 43 | days+commits |
| music-audio | STALE | 2026-08-08 | 2026-08-09 | 1d | 13 | commits |
| world-engine | STALE | 2026-08-08 | 2026-08-10 | 2d | 1 | days |
| characters | FRESH | 2026-08-10 | 2026-08-10 | 0d | 0 | |
| quests-runtime | FRESH | 2026-08-08 | 2026-08-08 | 0d | 0 | |
| pipeline-ledger | FRESH | 2026-08-10 | 2026-08-10 | 0d | 0 | |
| combat-systems | FRESH | 2026-08-08 | 2026-08-07 | -1d | 0 | |
| ui-vo | FRESH | 2026-08-08 | 2026-08-06 | -2d | 0 |
Reading it honestly. Only characters (§4.1) and PIPELINE_LEDGER.md have been edited since the
roadmap document landed on 2026-08-08 (2d7d6e0f); the other six sections still carry their landing
text verbatim. Three pipelines are genuinely behind their work. The four FRESH §4 sections other
than Characters are fresh because their pipelines have been quiet, not because anyone maintained
them — combat-systems and ui-vo show negative gaps, meaning the document is newer than the last
work in those families. That is a real pass, and it is a weak one: the contract has not yet been
tested by a busy pipeline other than Characters.
Not claimed. This measures whether a section was EDITED after work landed. It cannot measure
whether the edit was TRUE — C3 is a rule, not a gate, and no instrument reads the prose. A seat that
touches a section without saying anything new satisfies the instrument and violates the contract.