Dogfood log
June 29, 2026 · View on GitHub
Raw, dated notes from using Surface on Surface (and on other repos). Not polished — this is source material for write-ups later. One entry per notable moment: what happened, what the tool did about it, the lesson. Keep it honest; the failures are the interesting part.
2026-06-29 — refs propagation fired first on the commit that created it
Context: PR2 of refs hub composition (#4) turns on staleness propagation: surf check
now flags a hub when a hub it refs has an open divergence (one-hop). The change lives in
check_workspace — which is itself an anchored claim in cli-check.md, and cli-check.md is
referenced by cli-workspace.md and hub-format.md.
What happened: the first surf check after wiring up propagation went red three ways:
DIVERGED hubs/cli-check.md :: surf-cli/src/check.rs > check_workspace
stored 2:4f5890aca70c → now 2:b7b7fd55206e (magnitude: Large)
REF-STALE hubs/cli-workspace.md :: ./cli-check.md
referenced hub `hubs/cli-check.md` has an open divergence — review it, then re-verify
REF-STALE hubs/hub-format.md :: ./cli-check.md
referenced hub `hubs/cli-check.md` has an open divergence — review it, then re-verify
surf check: 3 divergence(s).
The new feature's first real firing was on the very diff that introduced it: editing
check_workspace diverged its own claim, and propagation — the thing being added — immediately
walked the two hubs that compose it and flagged them too. Fixing the root cleared all three at
once: I updated the check_workspace claim prose to describe propagation, re-sealed it
(surf verify "surf-cli/src/check.rs > check_workspace"), and both inherited REF-STALEs
vanished with it.
Why it's a good story: the composition graph proved itself end-to-end without a contrived
example. It also makes the §8/§11.3 risk concrete: one genuine divergence amplified into three
findings (1 root + 2 inherited). That's the cascade the proposal worried about — but the shape
held up: the inherited flags are clearly labelled, point at the root, and clear the instant the
root is re-sealed. One-hop did its job too — cli-check itself refs cli-git/cli-verify,
which were clean, so nothing spread further, and the two REF-STALE hubs didn't re-propagate
onto their referrers (propagation is built only from base divergences).
Lesson / open question: "fix the root, the inherited flags clear" is the property that makes
propagation usable rather than noisy — but it relies on the author recognising a REF-STALE as
derived, not a second thing to fix. Open question: at scale, is a 1→N amplification per stale
hub still legible, or does check eventually want to group inherited flags under their root
(print the root divergence, then "and N hubs that ref it") rather than as N peer lines?
2026-06-29 — The new claim-log nudges flagged 22 of our own hubs
Context: #142 argues the CLI's in-loop signals (surf suggest, lint_under_coverage) teach
agents to write claim-logs — one claim per function, near-1:1 symbol→claim, no prose — because
nothing rewarded consolidation. We added the symmetric counter-pressure: a claim-log warning
(several claims, never a multi-site at:) and a thin-prose warning (multi-claim hub, stub body).
What happened: the moment they ran, surf lint reported 22 warnings on our own hubs —
0 errors, exit 0. Notably zero of our 17 hubs had ever used a multi-site at: list, and
cli-check.md (the example the issue calls out as too thin) tripped both new warnings. The repo
that ships the tool was itself the thing the issue describes.
Why it's a good story: it's the cleanest possible confirmation of the issue's thesis — the authors of Surface, dogfooding daily, still drifted into per-symbol logging because the loop only ever nudged toward more coverage, never toward fewer, coarser claims. The fix isn't "write better docs"; it's adding the missing signal. The warnings are advisory (exit 0) by design, so they nudge without blocking — but 22 of them is a loud, honest nudge.
Lesson / open question: advisory-but-loud is the right register for a stylistic nudge, but
22 warnings risks being tuned out. Open question: should consolidation be a single per-hub summary
line rather than one warning per offending hub, and is the multi-site at: count the best single
proxy for "this author thinks in systems, not symbols"?
Follow-up (same day): we then ate the dogfood — refactored the 6 flagged hubs in the same PR.
Adding real body prose to the 5 thin ones was free (bodies aren't hashed, so no re-verify), and
cli-git got the repo's first multi-site claim: one invariant ("every git query degrades to
None; the verdict never depends on it") sealed across all five helpers, which let us trim the
per-function boilerplate. Writing it surfaced the same thing the AGENTS.md entry did — consolidating
forced us to name the shared contract explicitly. New-warning count on our hubs: 6 → 0. (The 16
under-coverage warnings are a separate, older itch.)
2026-06-17 — Making AGENTS.md a hub caught AGENTS.md lying about itself
Context: We documented that AGENTS.md/CLAUDE.md can double as a hub (any file the hubs
glob matches that parses as a hub counts), then went to actually wire it up here: added AGENTS.md
to the glob and sealed one claim anchored to lint_agents_pointer — the lint rule that polices
AGENTS.md itself.
What happened: the claim couldn't be written as the existing prose. AGENTS.md said:
surf lintenforces that this block stays — pointing at the hubs directory, never duplicating or enumerating individual hubs.
But lint_agents_pointer only checks that the surf:hubs block links the hubs directory and that
the directory exists — it does not enforce non-enumeration (that's design convention, not code).
The prose had quietly overstated the tool. The discipline of writing a claim that must match a
specific symbol's actual behavior forced the correction: split the sentence into what lint enforces
(link + existence) vs. what's by convention.
surf verify "surf-cli/src/lint.rs > lint_agents_pointer" → updated AGENTS.md
surf check → all anchored spans match
Why it's a good story: the self-referential loop closed — AGENTS.md now carries a sealed
claim about the rule that governs AGENTS.md. And the mere act of making a sentence sealable
surfaced that the un-anchored version had drifted from the code. The claim didn't catch drift over
time; it caught an overstatement that already existed, because anchoring forces you to say exactly
what the symbol does.
Lesson / open question: "write it so it can be anchored" is itself a forcing function for
honest prose, separate from the gate ever going red. Open question: how much of an imperative
instructions file is genuinely anchorable? Here it was exactly one sentence — the rest is process,
deliberately left unanchored. Coverage is still the product (cf. the 2026-06-12 entry); over-anchoring
AGENTS.md would just invite rubber-stamping.
2026-06-12 — Instructions are advisory; the gate isn't (agent edition)
Context: Asked Claude to knock out the 0.6.1 quick wins (#71, #67). It changed surf for's
error path in for_path.rs, ran cargo test (64 green), cargo fmt --check, even sh -n on the
installer — and pushed.
What happened: CI went red on the dogfood job:
DIVERGED hubs/cli-for.md :: surf-cli/src/for_path.rs > run
stored 3ffb208cc1db → now 3143f824dcfb (magnitude: Small)
AGENTS.md step 3 says, in so many words, run surf check before you push. The agent had that
instruction in context and skipped it anyway — thorough about the checks it chose, blind to the
one the repo asked for. The gate didn't care. It doesn't read AGENTS.md; it hashes spans.
Second half: this is the same anchor as the PR 1 entry ("the gate caught its own author
lying") — but the opposite branch. There the prose had gone false and needed rewriting. Here the
claim describes the contract (a directory errors, exit 1 — the #53 rewrite already said so),
and the change only improved the message text, so the right move was a bare re-seal:
surf verify, one anchor stamped, green. Both branches of the discrimination the tool forces
have now been walked on the same claim, two days apart.
Why it's a good story: the agent angle. Prose instructions to an agent are advisory — it followed five and dropped the sixth, which is exactly the failure mode prose always had. The deterministic gate was the only layer that didn't depend on being obeyed. If agents are going to write more of the code, "docs enforcement that doesn't rely on the author's diligence" stops being a nice-to-have.
Lesson / open question: agent-proofing isn't more sentences in AGENTS.md — it's hooks. The
pre-commit wiring exists (CONTRIBUTING.md); should installing it be the first thing an agent
session does, or should surf check sit in a pre-push hook so the local gap can't happen at all?
2026-06-12 — The issue tracker is un-anchored prose: #43 rotted
Context: Triaging 0.6.1 for quick wins. #43 said: pick() in surf-core/src/resolve.rs is
duplicated logic, never called, delete it. Filed with provenance and everything — file, line
range.
What happened: the code disagreed. The Go resolver (landed after the issue was filed) calls
pick() twice. The issue's claim was true at filing and went false silently when resolve_go
merged — nothing gates issue text, so it rotted exactly the way the thesis predicts un-anchored
claims do. "Implementing" it would have broken the build. Closed as stale instead.
Why it's a good story: an issue is a claim about code with provenance but no hash — the
purest specimen yet of what's anchored is enforced; what isn't, rots. But there's an honest
second edge: Surface couldn't have gated this one either. "This function is unused" is a
whole-program property — it lives in the callers, not in the span you'd anchor. A hash on
pick() itself would have sat green while the claim went false around it. Same blind-spot family
as the public_symbols coupling in the 06-11 entry.
Lesson / open question: for dead-code claims the right gate is the compiler
(#[warn(dead_code)], or deleting and letting the build vote), not a span hash. Pattern worth
naming when writing this up: match the claim to a gate that can actually see the property —
span-local truths get anchors, whole-program truths get the toolchain.
2026-06-11 — What an anchor can reach, and what it can't
Context: PR 3 of 0.6.0 (#52) — adding surf suggest --all to propose Python classes and
non-callables. It touched the shared public_symbols enumerator, the clap Command enum, and
suggest.rs.
What happened — the reach: surf check tripped on hubs/cli-reference.md, whose claim is
anchored to the Command enum and whose prose literally ends with an instruction to me:
... Adding, removing, or renaming a command or flag, or changing a default, diverges this
anchor — re-read docs/reference/commands.md before sealing.
docs/reference/commands.md is a hand-written human doc with no anchor of its own — nothing
hashes it, so on its own it could rot freely. But because the source of truth (the clap enum)
is anchored, and the claim encodes the cross-reference, adding --all forced the gate red until
I went and updated that un-anchored sibling doc. An anchor on the thing that changes, used as a
tripwire for the prose that describes it elsewhere. That's a pattern worth naming: you don't have
to anchor the downstream doc, you anchor its cause and write the pointer into the claim.
What happened — the blind spot: PR 2 had just re-pointed lint's coverage nudge at
public_symbols. In PR 3 I broadened public_symbols — and if I'd broadened its default
instead of gating the new kinds behind --all, lint would have started flagging every
unanchored class and constant in every repo. The gate could not have caught that: no hash
changes, no anchored span moves — it's a semantic coupling between two callers of a shared
function. I had to hold it in my head and design around it. Nothing in Surface protects you from
it.
Why it's a good story: the two halves are a clean contrast. The gate's reach is longer than "the span you anchored" — via an instruction in the claim it pulled an un-anchored doc into scope. But its blind spot is equally real: behaviour that emerges from how two functions share a third is invisible to a per-span hash. Anchor the cause, not just the symbol — and don't expect the gate to catch coupling it can't see.
Lesson / open question: the commands.md trick (anchor the source of truth, point at the
prose) generalizes — is it worth documenting as an authoring pattern? And the blind spot is the
honest counterweight to the PR 1 entry's "what's anchored is enforced": what's anchored is
enforced span-locally; cross-symbol invariants still live only in your head.
2026-06-11 — The gate caught its own author lying
Context: Implementing PR 1 of the 0.6.0 milestone (#53 + #38) — making surf for,
surf check --files, and surf stats fail loudly on malformed input instead of returning a
falsely-reassuring success.
What happened: After editing for_path.rs and stats.rs, I ran the repo's own gate
(surf check) as the final verification step. It failed — on Surface's own anchored claims:
DIVERGED hubs/cli-for.md :: surf-cli/src/for_path.rs > run
claim: ... It is a query, not a gate, so it always exits 0 whether or not anything matched.
That claim had been true at 0.5.0 and was now false — the whole point of #53 was to make
for exit 1 on a mistyped path. The change to the behavior and the change to the documented
contract were the same act, and the gate refused to let them diverge silently. I couldn't
re-seal the hash without first deciding: is the prose still true? It wasn't, so I rewrote it.
Three claims tripped (cli-for, cli-stats, cli-check). Two were genuine contract changes
that needed new prose; one (check_workspace) only shifted because an adjacent line moved, so it
just needed re-sealing. The tool made me look at all three and tell them apart by hand — which is
exactly the discrimination it's supposed to force.
Why it's a good story: the usual pitch for docs-as-tests is abstract ("docs drift from
code"). This is the concrete version, and it's self-referential: the gate caught its own
maintainer, mid-feature, shipping a behavior change that contradicted a sentence the tool itself
was responsible for guarding. The stale module doc comment in for_path.rs (// A query, not a gate: it always exits 0) was not anchored — so it drifted with zero resistance, and I only
caught it by eye. A nice illustration of the boundary: what's anchored is enforced; what isn't,
rots.
Lesson / open question: the un-anchored doc comment drifting while the anchored claim held is
the sharpest line in the whole episode. Worth a callout in any write-up: coverage is the product.
Possible follow-on — should lint nudge toward anchoring module-level doc comments that restate
a contract? (Adjacent to #54, the coverage-nudge work.)