Item 0 of the roadmap's order of work, and it blocked everything after it: core
sat at 2965 of 3000 while the review scheduled four core-bound items, the first
of which — logging — wanted the whole remainder.
/robots.txt and /sitemap.xml are exact paths somebody else's software asks for by
name. They own no core concept and pass every test the architecture applies to a
feature; they lived in internal/web only because a feature could not own a route
until ADR-0081. internal/ext/discover/ now holds them. Core 2965 → 2913.
The seam gained one parameter to make it possible: a func() *content.Site, since a
sitemap must list what is served now and the index is swapped whole on every
rebuild (ADR-0077). A captured pointer would have frozen the site at startup —
which is the kind of bug that only shows up after a rebuild, in production.
The ceiling rises to 3400 as well as the move, because the move alone could not buy
the room. feed.go and web/extras.go cannot follow discover out: a feed lives at
/{section}/feed.xml and extras under a bundle's own URL, so both are resolver cases
while the seam mounts exact paths only. Raising by the minimum that unblocks one
item produces a ceiling nobody believes, so 3400 fits the View cluster with
headroom. HARNESS.md asks that a raise be read as evidence something belongs in
ext before evidence the number was small; both readings were true, so both actions
were taken.
web no longer reserves those two paths, so a clash between features is wire.go's:
it merges route maps in declaration order, keeps the earlier claim, logs the loser.
Verified — a site shipping root/robots.txt starts, serves the engine's robots.txt,
and logs the passthrough claim, where an unguarded mux.Handle would have panicked.
Evidence: robots.txt and sitemap.xml are byte-identical before and after the move
against the demo site (67 and 2701 bytes, cmp clean), and the sitemap keeps its
application/xml type.
One real cost, recorded in both places rather than hidden. internal/web's
visibility test asserted that a listing, a feed *and* a sitemap all hide
unpublished bundles — one property, one test, because all three share a Query. The
sitemap half moved to the feature instead of a web test importing ext, which would
invert the one-way layering the architecture gate enforces. That property is now
asserted twice, once per package owning a surface.
Three gates caught real mistakes on the way: the staged-tree check found a partial
stage where git rm had staged a deletion while the caller edits were unstaged, the
coupling gates demanded state.md and HARNESS.md, and the nesting advisory rejected
a closure that put the merge loop one level too deep — fixed by making it a plain
function rather than tolerated.
Extensions 6 → 7. Routing cases unmoved: exact paths are mux entries, never
resolver cases, which is what that counter's exclusion column already said.
13 files. Core 2913/3400, ext 2495/3500.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One idea at a time: definition, necessity, priority, layer, recommendation. Every
row is now shipped, scheduled, parked with a trigger, or dropped with a reason —
so both tracking files are gone rather than left as a parallel backlog.
Roughly a third already shipped. A quarter needed only a theme fragment or CSS,
including several the list assumed were features: arbitrary `theme.*` frontmatter
attributes already reach templates through Extra (verified), native lazy loading
already ships and is deliberately absent on lead figures, and in-page timelines
are a theme-defined container.
roadmap.md gains an "Order of work" — nine bodies of work in sequence, because the
sections this review added had accumulated without one. Item 0 is a decision, not
work: core sits at ~2965 of 3000 and logging alone wants the remainder, so
discover.go moves out *and* the ceiling rises with an ADR. feed.go and
web/extras.go cannot move; they are resolver cases, and three parked items now
wait on that same seam decision.
Two ADRs came out of it.
ADR-0083: the dependency rule described a practice it forbade. "Stdlib first,
always" and "usually 40 lines of stdlib" would, read literally, have argued
against all five modules here — every one is a format or algorithm somebody else
specified, and their allowlist comments say so. Surfaced when this agent proposed
hand-rolling Reed–Solomon for QR encoding. The test is now whether you can verify
it, not whether you can write it, with compute-versus-hold as the counterweight.
conventions.md and allowed-deps.txt contradicted it and were realigned.
ADR-0084: the Effect primitive said every Effect is "idempotent and re-runnable
from scratch" — true of derivatives, false of anything outbound, since a sent
message cannot be recalled. Split into artifact-producing and outbound, the latter
idempotent only against a delivery ledger, at-least-once with a dedupe key, and
ledgers in a -state directory distinct from the disposable cache. Asked for as
forward-looking design so webmentions need no redesign; recorded as a shape rather
than built, the way extensions.md records the Extension struct. Also fixed a
STATUS claiming Effects were "not buildable yet" when the derivative pass has
inhabited them since ADR-0042.
Findings that were defects rather than ideas: the Atom feed emits no <author>,
which RFC 4287 requires. HTML comments in content are published verbatim —
verified — so anything commented out is already public, and a check warning is
scheduled rather than the engine deleting authored bytes. `Page` has no Date, so a
theme cannot mark up dt-published or show an article's own date.
A frontmatter naming rule after this agent proposed `archive` alongside the
accepted `archived`: no near-homograph keys, and name the benefit rather than the
vendor.
reference/microformats-and-indieweb.md is new and is the reference asked for —
microformats2 properties and IndieWeb rel values mapped to where a theme puts
them, with spec URLs, marked scheduled. rel="me" is the highest-value lowest-cost
item in that space and needs no endpoint at all.
Four things this agent got wrong and the human caught: asserting "the engine never
fetches" from a sentence scoped to the content repo, twice; inventing a
data-sovereignty argument for an item he had filed as "a cool geeky thing", when
the raw Markdown in his git already is the sovereignty; the archive naming; and
claiming microformats could be fully implemented today.
15 files, +556/-385. No code changed. Nothing was scheduled that this agent could
not name a consumer for.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two answers to "add .sh to sh files". Exactly one file lacks the suffix:
scripts/hooks/pre-commit. git locates a hook by exact filename, so renaming it
would leave a gate that looks present and does nothing — verified in a throwaway
repository, where hooks/pre-commit printed and hooks/pre-commit.sh was ignored
while the commit succeeded regardless. The name belongs to git, so the file keeps
it and conventions.md now states the exception rather than leaving it as an
inconsistency someone will try to tidy again.
The extension was never the defect anyway. The dangling-path gate had only ever
matched backticked citations, so `git add docs/surface.md` in the hook — an
argument, not a citation — survived the docs/ rename and surfaced as a fatal
inside a commit that otherwise succeeded. That gate now also reads paths in
scripts/ unquoted, which is the check that would have caught it.
Proven both ways: restoring the exact bug fails the gate with "reference to a
path that does not exist: docs/surface.md", and a working tree passes. The first
attempt did not catch it — docs had been dropped from the alternation because the
directory no longer exists, which is precisely the class of stale reference worth
failing on, so docs is in the bare pattern on purpose.
Backslashes are stripped before comparing, so a path written as a regex —
\.claude/settings\.json — is checked as the file it means rather than flagged as
the file it is not. Prose is still checked only inside backticks: a sentence
saying "under harness/" makes a point no filesystem can verify, while a script
naming a path either has it right or is broken.
One gate label changed with it. `pass "harness/HARNESS.md coupling"` read fine
while the directory was docs/ and now parses as a filename, which the new check
duly flagged; it is "harness and HARNESS.md move together". No doc cited the old
label.
4 files. No rule or threshold moved — one gate widened, one convention written down.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ADR-0082 moved docs/ to harness/ and left scripts/hooks/pre-commit staging
docs/surface.md. The hook runs on every commit, so the very commit that did the
rename hit it: `git add docs/surface.md` failed with a fatal, the commit
otherwise succeeded, and surface.md went in only because it had been staged by
hand beforehand.
The sweep missed it because the file has no extension and the grep that found
every other reference was filtered by --include='*.sh'. HARNESS.md now says so
where the hook is described, since the next rename will make the same mistake for
the same reason.
Verified by this commit: the hook ran, regenerated harness/surface.md, and staged
it without error.
2 files.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docs/content-model.md opens with "Engine specification". It is also where the
rule lives that a leading underscore makes a file unaddressable — and the human
who owns this site did not know that rule, because nothing in this repository is
addressed to an author. Twelve documents named docs/ while being exclusively
about building the parser is a signpost pointing at the wrong room.
Naming the directory for its audience makes the gap visible instead of hiding it.
docs/ is now reserved and deliberately absent: an empty docs/ is an honest
statement that end-user documentation does not exist, where docs/ full of parser
specs was a claim that it did.
HARNESS.md stays at the root. Root holds the three entry points — README.md for a
human, CLAUDE.md for an agent, HARNESS.md for whoever maintains the machine — and
harness/README.md is the map of the directory, so moving the guide inside would
have collided with it for nothing.
Mechanical and wide: 100 path references across 24 files. Every verify.sh gate
that names a doc by path, the directory lists the dangling-path and ADR-number
gates scan, surface.sh's output target, the Makefile, CLAUDE.md's read order, the
skill, four commands, and two Go package comments. A first pass with a shell loop
silently edited only four files and the rest still said docs/; the fix was to
write the file list out and check the remaining count was zero rather than trust
the loop's exit status.
No rule, threshold, gate or obligation moved — this is a rename, and the gates
demonstrated it twice: they stayed green on the new paths, and the ADR-number gate
caught ADR-0082 before the entry existed.
Deferred, both on the human's call: the end-user documentation site itself, which
wants its own decision about where it lives and whether its claims are gated; and
moving examples/ under docs/, since demo-site is a live site root that verify.sh,
the coverage test and make demo all point at, and moving it would couple a rename
to a design nobody has made.
31 files, +146/-106.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A session picked up work from .scratch/continue.md and offered the human a doc
trim that was already in HEAD. The handoff was written at 01:06 saying the trim
awaited a yes; the commit containing the trim was amended at 01:12, underneath
it. Five defects made that possible, and four of them are the harness's.
The read order inverted trust: docs/README.md sent the next session to the
handoff "if present, otherwise git log" — consulting the one ungated file
instead of the record that cannot lie, which is backwards from every other rule
here, where the code beats state.md and generated surface.md beats memory. The
handoff is now step 4 of CLAUDE.md's read order, explicitly after the harness
and never instead of it, and anything it calls pending is checked with
git log -- <path> first.
The tick fired too early. "A planned item completed → tick it in continue.md"
sat in the Document step, so it recorded the plan's version of events while the
commit could still move; an amend, a squash or a late fix moves it. Reconciling
now happens after the commit exists, and a completed item is deleted rather than
ticked, because a ticked item still reads as an item.
HARNESS.md authorised the drift outright — it said the file holds "what is
done", which is exactly what continue.md's own header promises it never records.
It is now described as what it is: a temporary handoff, uncommitted, ungated,
discardable, holding the continuation point and the carried findings.
state.md still pointed at .scratch/build-queue.md, replaced two commits earlier.
The dangling-path gate missed it because .scratch was absent from its
alternation, so no pointer into the handoff directory was ever checked. It is
included now, guarded on the directory existing — a fresh clone has no handoff
and must stay green. Both directions were run: present with a stale pointer
fails, absent with docs naming it passes. The gate then flagged its own
explanatory comment, which is why that one path is written without backticks.
No gate can compare an uncommitted file against anything, so the file's job is
narrowed instead of enforced, and HARNESS.md now says which mechanism stands in
for the missing gate rather than implying one exists.
6 files, +47/-9. No rule, threshold or counter moved. CLAUDE.md 138 → 142 of 150.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
chroma at render time, emitting CSS classes rather than inline colour, handed to
a `code` theme fragment. Highlighting works with scripting off, in a feed
reader, in a browser that never runs JavaScript. No lighter pure-Go option
exists — every "alternative to chroma" is JavaScript, which the reference theme
is gated against.
A fence's info string carries the rest: title, numbers, start, hl=3,7-9, and
file=name lines=A-B, which reads the snippet out of a file beside the bundle and
numbers it by that file's own lines. So a post quotes several parts of one
program without the copies drifting from it, and a reader can find what they are
looking at. Verified on the real binary: the same file at lines 5-10 and 12-14,
each numbered as it really is, with different lines tinted.
Not a new package: a new one could not import the key=value parser this repo
already has, because ADR-0069 forbids a feature importing its sibling, and a
second parser for the same syntax is what §6 stops.
Two costs, both stated in the ADR rather than buried. The binary goes from ~15MB
to 19MB, for a project whose story is one small binary. And the reference theme
now carries a token palette — the first thing in it that is a taste rather than
a demonstration — kept to eight classes for that reason.
The demo quotes a shell file, not a Go one: a .go file under examples/ joins the
module and has to compile, which the build gate caught before it shipped.
6 of 9 modules, ext 2188/3500.
core 2850 -> 3000, ext 2000 -> 3500, modules 6 -> 9.
ext was the binding one: 1975 of 2000, with syntax highlighting still to write,
so for that ceiling this is not anticipation but the difference between building
the next feature and not. The other two are bought ahead of a list of features
the human has signed for. DEPS_MAX 9 leaves room for chroma and its regexp2 —
which fill 6 exactly — plus two more.
Worth naming rather than glossing: a ceiling raised on evidence is a
measurement, and one raised on intent is a budget. The first two raises were the
first kind; this is the second, which is weaker. ADR-0041's test still stands —
if core approaches 3000 the question is again what belongs in ext, and the
answer is still feed.go and discover.go, waiting on a feature being able to own
a route.
Invariant 9 is measured by a number that has now moved three times. It says
something only because ext rose 75% where core rose 7%.
FILE_LOC_WARN and FUNC_LOC_WARN are untouched: they are about one file being
readable, and nothing about the plan changes that.
Four of these counters were re-scoped the first time anything tested them, and
every re-scoping was a sentence about what had been wrongly included: transforms
were counting parse-phase work goldmark already orders, views were counting
output formats, effects nearly counted an in-memory swap, extensions counts
packages rather than goldmark's own extensions.
So the fix is not another counter but a required shape. The table gains a fifth
column and verify.sh fails on a row that leaves it empty — watched naming the
offending row. Checked by shape rather than by wording, because a gate that
demands a phrase gets the phrase and not the thinking.
Writing the exclusion up front is the cheapest way to find out whether a counter
measures a mechanism or a symptom, and all eight rows could state one, which is
the first evidence that the counters are now scoped right.
Queue entry G6.
"Every feature is a leaf" was enforced by listing the pairs that happen to exist
today: content, render and web may not import ext, plus a separate check for
siblings. A list only forbids what is already there — a core package added next
month could import a feature and pass, which is how an invariant rots while
staying technically true.
One positive rule now: only cmd/ may import internal/ext/…. It covers packages
that do not exist yet and catches a sibling import in the same breath, since a
feature importing another feature is the other way one stops being deletable.
Four rules become one.
Watched rejecting both kinds before keeping it — a core package importing a
feature, and a feature importing its sibling — and watched passing the tree as
it stands. Test files are excluded, which is right: the demo's own test wires
features on purpose.
Queue entry G5. It was the last architecture invariant held only by hand.
`::toc` renders through a `toc` fragment receiving level, text and the id
goldmark already assigns. The engine collects, the theme decides whether that is
a list, a sidebar or nothing — the reference theme emits a flat <ol> with a
level class per entry, so indentation is CSS rather than markup.
A theme cannot enumerate headings, because templates cannot parse HTML, so the
engine is the only thing that can supply them. Collection lives in ext; only the
Heading type and one Fragment field are core.
That is what needed the ceiling: CORE_LOC_MAX 2800 -> 2850, the second raise.
ADR-0041 said to read a second raise as evidence something belongs in ext, and
the test was applied rather than waived. feed.go and discover.go are the
features that should leave core, and they cannot, because an ext feature cannot
own a route until the extension registry exists — which the counter says buys
nothing yet. The thing that should move is blocked on a different decision, so
the honest fix was the ceiling and an ADR saying exactly that. When routes
become ownable, they leave and this comes back down.
Entry text is the heading's words with markup stripped: a link inside a link is
not markup a browser accepts. An entry whose heading has no id is skipped rather
than linked nowhere, and a page with no headings renders no nav at all.
core 2804/2850, ext 1825/2000, 34 gates green, 0 warnings.
Dropping raw HTML was silently destructive. H<sub>2</sub>O rendered as "H2O",
10<sup>6</sup> as "106", <kbd>Ctrl</kbd> as "Ctrl", and khosra check reported
nothing — an author lost meaning with no signal anywhere. Measured on the real
binary before and after.
Invariant 2 already says content from the site root is trusted, so the old gate
was defending the half of the boundary that was never in question while the
untrusted half has no code to defend yet. Chemistry, units, exponents and
keystrokes are what a hard-science site needs and what no Markdown dialect
expresses, so html.WithUnsafe() goes on in internal/render/render.go.
The gate does not disappear; it narrows. verify.sh used to fail on WithUnsafe
appearing anywhere and now fails unless it appears in exactly that one file —
watched doing both, accepting one call site and naming both files when a second
appears. A second pipeline trusting its input is the failure ADR-0003 exists to
prevent, and when comments arrive they get their own goldmark without it. The
gate is the reminder that the split has to be built rather than assumed.
The security test that asserted "raw HTML must still be dropped" now asserts the
property that actually holds and matters more: a shortcode argument stays data
whatever the page around it is allowed to do. ::figure{alt=<b>bold</b>} still
arrives as <b> while the <span> beside it renders.
core 2793/2800, ext 1077/2000, 34 gates green, 0 warnings.
The verified-against sha could only ever be wrong. A commit cannot name itself,
so the line had to be written after the commit it described, which forced a
trailing `state:` commit every time — against conventions.md, which has always
said code, test, state.md row and ADR belong in one commit. Folding those
trailing commits away then left the sha naming a commit that no longer existed.
backup/pre-fold shows the pattern, and 8905686 is the commit that had to name
the survivor afterwards.
Git already knows when each file last changed. The gate now compares the last
commit touching docs/state.md against the last touching a .go file, and the
line is gone. Same intent, nothing to maintain, and no rewrite can invalidate it.
Also records two commit rules the human stated this session: state.md never gets
a commit of its own, and a minor change asked for just after a commit is amended
into it while it is unpushed rather than accumulating as noise.
Adopts ideas/token-conservation.md, parked 2026-07-28, plus the disciplines
the human added: read the compressed form first, discover by mechanism,
shrink output at the source, never pay twice for the same bytes.
docs/context-economy.md owns all of it and leads with a floor, because
every cheap failure mode is also a token saving — skipping the owning doc,
guessing a signature, reporting from a diff, thinning a test — and each has
already cost this repo a defect. Frugality is for presentation and
discovery, never for the artifact or the evidence.
Mechanical, not remembered:
- scripts/surface.sh generates docs/surface.md — every top-level
declaration with its line, 261 lines standing for 3757 of source. The
pre-commit hook regenerates and stages it, so it cannot be stale, and
verify.sh compares independently for a clone that never set
core.hooksPath. The hook refuses a commit with unstaged .go changes,
since what it generated describes the working tree, not the commit.
- verify.sh --quiet: 48 lines of gate output become 1. The hook uses it.
- CLAUDE_LOC_MAX=150, the only budget billed per turn rather than per read.
Both new gates were watched failing before being kept: a doctored
surface.md, and CLAUDE_LOC_MAX temporarily set to 5.
state.md's inventory loses its LOC column. It had already drifted on six
files (content.go 381→450, render.go 447→454, web.go 206→217, check
216→223, watch 129→137, chrome 105→110) which is what a number written in
two places does; the generated file owns sizes now, the table owns purpose.
The subagent question is recorded there as the one open decision, with the
case for and against written out in the idea file.
The human asked for a demo extensive enough to review by hand, tracked as files,
and kept current the way docs are. Generated filler cannot be reviewed — you cannot
read a Go function and see what a reader sees — so the generator is deleted and
`examples/demo-site/` is an ordinary site: 33 Markdown files, six pictures,
site.yaml, a template override and static files.
My reason for generating it was ADR-0011, and I had that rule wrong: it puts the
*author's site root* outside this repository, not fixtures. conventions.md already
keeps golden files in testdata/, and this is the same category one size up. Keeping
both a generator and files would have been two sources of truth, so one had to go.
Kept true by two gates rather than by good intentions. A table-driven test in
internal/web serves the directory through the real handler with one case per
feature — thirty-five of them, each naming what it proves — and verify.sh runs
`khosra check` over it and fails on anything fatal. Adding a feature now means
adding it to the demo and adding its case, and the build says so if you don't.
Extensive on purpose: eleven dated posts so a section actually paginates, a
four-chapter series so first/last are not the neighbours, a gallery with three
JPEGs and an SVG so both the resampled and the untouched paths show, a Bengali-only
bundle, a titleless status, a draft, a future date, an include, a nested extras
tree, and a typography page that demonstrates what the engine will not do.
Two expectations of mine were wrong and the demo corrected them: the site's own
list template overrides *tag* listings too, so there are no group headings there —
which turns out to be the better demonstration of ADR-0046, since the theme picking
the flat shape is exactly the point. And template literal text is not escaped, so
an apostrophe stays an apostrophe.
ADR-0040 puts the dependency approval on the record, as hard rule 2 requires: the
standard library decodes JPEG, PNG and GIF but cannot resize, and image/draw
scales only by nearest neighbour, which is visibly wrong on the photographic
downscales this site is made of. Four modules against a cap of six, no transitive
dependencies, and WebP decoding comes along. AVIF still has no decoder anywhere,
so it will pass through untouched.
ADR-0041 raises CORE_LOC_MAX 2000 → 2800. The old figure was costed before any
code existed, for the spine and the render path, and never budgeted what cmd/ will
hold: `check` ~250, `new` ~100, `-dev` ~100, change detection ~100, page cache
~200. On a core already at 1870 that is ~2620 before anything optional, so the gate
was going to fail work nobody would call excess. A costing error corrected, not
discipline loosened.
The ADR and HARNESS.md both say what a *second* raise would mean: that something
belongs in internal/ext/. Two ceilings exist so "core stops growing, ext rises" is
observable, and that stops being true the moment leaves are let into core.
Earned by a real mistake this session: `web.Handler` gained a parameter, its
caller in cmd/ was updated in the working tree, and `git add internal docs` left
that caller out. verify.sh was green throughout, because every gate looks at the
files on disk rather than at the commit being made. The result was a commit that
did not compile — the kind of thing git bisect trips over for as long as the repo
exists. I rewrote the two local commits rather than adding a fix-up on top.
The check builds a throwaway checkout of the index via `git write-tree`, so it
cannot touch the real index or working tree, and it only runs when something is
staged. Proved both directions: staging a signature change without its caller
fails, a clean tree passes.
Eighth gate defect found by running the harness against real work rather than
reasoning about it — and the first that was a missing gate rather than a wrong one.
Since Arc 1 the latent list has carried "raw HTML is omitted only because
goldmark's default omits it". Shortcodes were the trigger, and they came and went
without needing unsafe mode — a call renders a theme template (ADR-0036). So the
right close is not a note saying we got away with it, but a gate: verify.sh now
fails on WithUnsafe in any non-comment line under cmd/ or internal/.
Proved both directions before keeping it. The first version also failed on the
comment in render.go that explains the rule — a gate that fires on its own
documentation is one nobody keeps, so it now skips comment lines. That is the
seventh gate defect found by running against real code rather than reasoning
about it.
The latent row is gone rather than reworded, and the untrusted-boundary row says
which half is now mechanism and which half still isn't.
The %w advisory counted every fmt.Errorf without %w, including calls that create
an error rather than wrap one — so it fired permanently on correct code. It now
looks for a call that passes an err and drops the %w, which is the actual rule.
Recorded the general form in the loop's Verify step and in HARNESS.md: a warning
that fires on correct code and keeps firing is a defect in the check or the code,
resolved in that change. Two advisories have now been narrowed after firing on
code the harness itself mandates, and the count creeping from one to two was the
only signal either time.
verify.sh is at zero warnings, which is what makes the next one legible.
The advisory fired on stringList(v any), which reads the open page object that
ADR-0002 mandates — so its false-positive rate was total, and a warning that is
always wrong teaches you to skim warnings. conventions.md bans interface{} as an
API escape hatch, which is what the check now looks for: any or interface{} in an
exported func or method signature. Verified by adding an exported func Do(x any)
and watching it fire, then reverting.
The exemption is Claude Code's permission list, not "settings files". khosra will
have its own configuration and none of it is exempt: the engine's settings live in
the site root and the engine validates them, while scripts/budgets.env and
everything else under scripts/ and .claude/ stays gated. Verified by touching
budgets.env and watching the coupling gate fail.
go get marks a module indirect until something imports it, and the allowlist
check only inspects direct requires — so an unapproved dependency could sit in
go.mod unnoticed. Demonstrated: adding github.com/google/uuid passed the
allowlist check and was caught only by this gate.
The coupling gate treated every path under .claude/ as mechanism, so editing the
pre-approved command list demanded an explanation of a machine that had not
changed. HARNESS.md already calls settings.json a convenience — if its schema
changed the harness would still work, you would just get more prompts — so the
gate now exempts that one file and nothing else.
Carries the settings.json edit that exposed it.
The queue is a working plan, not a record: git log already carries what landed on
each step, so committing the plan duplicates it and invites the two to disagree.
.scratch/ is gitignored and skipped by the gate the way ideas/ and reference/ are,
so exploratory files there need not compile.
state.md keeps a conditional pointer rather than a path it cannot promise: if the
scratch file is absent, the log is the plan.
The allowlist parser stripped only whole-line comments, so an entry carrying the
reason it exists — the format allowed-deps.txt documents — failed the gate and
would have blocked every dependency ever added.
The interface{}/any advisory printed a line number instead of a filename when
only one file matched, because grep -n omits the name for a single file, and it
matched the word "any" in prose comments.
"Nesting past 4" fired at four tabs, which a closure inside a function reaches
unaided.
Naming is free before a module is published, a URL is shared, or a binary is
deployed; every day it waits costs more. Swept every form: module path, binary,
cmd/ directory, KHOSRA_SITE, the feature-loop skill directory, and the prose in
earlier ADRs — which describe this project under its old name, not a different
project. Recorded as ADR-0030.
go mod init lands here rather than with the first feature because the module
path is what the rename is about. x/text and yaml.v3 are required but not yet
imported, so both are indirect and no direct dependency is claimed yet.
The test-coupling gate failed comment-only and gofmt-only .go diffs, which
ship no behaviour and owe no test. Bypassing it would have cost every gate at
once, so it now compares added and removed lines with comments, blanks and
whitespace runs stripped: equal sets mean nothing happened. A new .go file is
never exempt.
The exported-doc-comment check is now a warning. No gate can tell
"// Load loads." from a useful sentence, so as a hard failure its cheapest
satisfaction was exactly the noise conventions.md calls worse than nothing.
Package comments and ADR citations in code stay hard.
Also fills in state.md's verified-against line and drops a stray blank line
left in roadmap.md by an earlier gate test.
Twenty-seven ADRs existed before a line of Go. Five specified the shape of
unbuilt mechanisms — cache validity, declared types, the settings cascade,
taxonomies, extras — which breaks the rule against abstraction before a
second concrete use, in prose where the counters cannot see it. They move to
ideas/deferred-decisions.md as recorded intent and return shaped by whatever
implements them.
Citations retargeted throughout; where one was decoration the rule now stands
on its own reasoning. Type declarations and the cascade drop to [spec] with
the MVP behaviour stated instead, so the first prompts have less to build.
conventions.md names http.ServeMux as the router, closing a hole that invited
hand-rolling a path splitter. The ADR gate now checks a number is registered
in the log rather than headed by an entry, so withdrawals resolve and invented
numbers still fail. Two architecture invariants corrected: identity no longer
implies a required language suffix, and the duplicated permalink clause is gone.