39 Commits
Author SHA1 Message Date
bdeshiandClaude Opus 5 69a7eb4733 move robots and sitemap out of core, and raise the ceiling on purpose
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>
2026-08-03 16:26:40 +06:00
bdeshiandClaude Opus 5 b313b729d9 review 130 feature ideas, and delete the lists that held them
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>
2026-08-03 16:14:52 +06:00
bdeshiandClaude Opus 5 16fde5ee68 lift the skill's question cap, which CLAUDE.md lifted and it did not
SKILL.md said "maximum three" clarifying questions while CLAUDE.md §4 said
"there is no cap" — lifted in 25d7045 and never propagated. The constitution is
always loaded and wins, so the skill was simply wrong, and an agent reading only
the skill would have split a six-fork request across turns to stay under a cap
that no longer exists.

The redundancy between the two files is deliberate: the constitution is always in
context and the skill is not, so both state the loop. HARNESS.md now records that
this deliberate copy has drifted twice — this, and the skill's conflict table
still deferring to `[spec]` markers after they were deleted. No gate can catch
it, because both files are prose and each is internally consistent, so the only
mechanism is grepping the other whenever a rule changes in either.

3 files. No rule changed — one restated correctly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 23:16:11 +06:00
bdeshiandClaude Opus 5 078ec8eedf check bare paths in scripts/, and record why one file has no .sh
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>
2026-08-02 20:49:08 +06:00
bdeshiandClaude Opus 5 07cf655d09 fix the pre-commit hook's path, missed by the rename
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>
2026-08-02 20:12:12 +06:00
bdeshiandClaude Opus 5 ec6e9838f0 rename docs/ to harness/, and reserve docs/ for the reader
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>
2026-08-02 20:11:06 +06:00
bdeshiandClaude Opus 5 4fff6fcd43 stop the harness deferring to a marker system it deleted
/refresh-docs step 4 told the agent to check content-model.md's `[spec]` versus
`[live]` markers. The `[spec]`/`[arc1]` system was deleted in 1510a5f, and
`[live]` never existed at all — so the step was unperformable in both halves
while reading as a check. It now says what the doc actually is: no markers, every
sentence a claim to test, with the failure that has really shipped named
explicitly — a subcommand or gate described in the present tense that no code
dispatches. That is exactly what the previous commit found five of.

The feature-loop conflict table was worse than stale. Its Soft row treated "an
unbuilt `[spec]` section's suggested shape" as a convention to deviate from, and
its None row said a request matching a `[spec]` section should proceed with
nothing said about it. Those sections now live in ideas/exploration.md, whose own
header reads "Presence in this list is not permission to build" — so the skill
authorised building from parked ideas that the storage forbids. Both rows now
point at what a doc describes as built.

HARNESS.md gains the fact that explains the asymmetry, since it is now the only
place both marker systems are visible at once: architecture.md's STATUS markers
stayed and content-model.md's went, because a primitive's endgame is load-bearing
for the next decision while an unbuilt disk format is not. A marker inside a doc
the agent already has open still gets read, which is why the second system was
deleted rather than given a stricter rule.

3 files, +12/-6. No gate, threshold or counter moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 17:00:01 +06:00
bdeshiandClaude Opus 5 8f5d479f06 stop the handoff outranking the log, and gate the pointers into it
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>
2026-08-02 16:53:58 +06:00
Claude Opus 5andbdeshi 1510a5ff7a compact the harness: no change history, no fact stated twice, no plans in the contract
HARNESS.md described how the ceilings got where they are — a paragraph of
changelog that grew with each raise. The ADR log is where history belongs, so
the section now states what the two ceilings are for, where the values live, and
how to read a raise. Any doc that narrates its own edits will do this again.

theme-contract.md explained parse order three times: once in the fragments
section, once under the stability rule, once under overriding. Once now, with
the other two pointing at it.

content-model.md carried a marker system — `[arc1]` build now, `[spec]` recorded
intent, plus a standing instruction never to build from a `[spec]` section. The
better answer than a stricter marker is no marker: the doc now describes only
what the parser accepts, and the shapes nobody has asked for moved to
ideas/exploration.md, which is storage and out of context by default. There is
nothing left to build speculatively from, so nothing needs to say so. 518 lines
to 469, and rule 6 plus the leaf/trunk test already cover the rest.

A sweep for filler phrasing found almost none — the prose was already tight — so
that part is two rhetorical tics rather than the cull expected. Reporting it
honestly matters more than manufacturing a diff.

No rule, gate, threshold or obligation moved.
2026-08-02 01:12:07 +06:00
Claude Opus 5andbdeshi 77c658bf98 move the feature catalogue out of docs, and replace the queue with a plan
Two structural changes, both about what an agent may pull into context.

exploration.md catalogues engine features nobody has asked for. That is storage,
not working material, so it moves to ideas/ where nothing sweeps it and it is
opened only when named — the same rule the other parked material already
follows. Six references repointed; the ideas gates then demanded an index line
and a status, and both were supplied rather than exempted.

build-queue.md was 516 lines, nearly all of it entries 0-23 finished months of
work ago, with the plan buried at the top. It becomes .scratch/continue.md at
49: where the code is, what is planned, and the findings worth carrying that no
doc owns — chiefly that silent damage to prose is this engine's recurring
failure mode, and that three defects this arc were invisible to curl.

Docs and HARNESS point at the new names. No rule, gate or threshold changed.
2026-08-02 00:57:55 +06:00
Claude Opus 5andbdeshi 25d7045133 uncap the clarifying questions, and stop the harness asserting numbers
The cap was arbitrary and the wrong lever: what matters is that a question's
answer changes the code, not how many such questions a request happens to carry.
A request with six real forks now gets six, batched into one turn — splitting
them to look brisk costs the human more than asking once. The instruction to ask
is unchanged and still gated by "each with a default so silence answers".

The rest is numbers the harness had no business holding. Three kinds, swept
across every harness doc, and none of them wanted machinery.

Restated values were the real fault: HARNESS.md carried the ceiling figures,
which docs/README.md's single-source table says live in scripts/budgets.env and
nowhere else. Prose repeating a value is a copy waiting to go stale, which is
exactly the rule I was quoting at everything else. The ADRs keep old and new, as
an append-only log should; the harness names the concept and points.

Illustrative figures — how long verify.sh prints, surface.md against the source,
how much the binary grew — drift every commit and carry no decision, so they now
say "a line per gate", "an order of magnitude smaller", "roughly a third".

Counts that restate their own list are the same fault in miniature: "the three
gates people skip", "six commands", "three pieces of it are mechanical", "two
sibling folders", "three cases, no fourth". Each is a number that goes wrong the
day the list beneath it changes, and none of them was doing any work.

Left alone deliberately: values with no other home — the ~50% overrun, the
72-character subject, 40 lines of stdlib over a dependency, toolchain versions.
Those are the source, not a copy of one.

No gate for this. It would have to guess which four-digit number is a budget
rather than a year, an ADR, an HTTP status or a Go version, and a gate that
fires on correct prose is a defect.
2026-08-02 00:56:41 +06:00
Claude Opus 5andbdeshi 67defae912 highlight code server-side, and let a block quote a file
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.
2026-08-02 00:16:32 +06:00
Claude Opus 5andbdeshi 4c5bda98ab raise the ceilings for a list of features, and say which kind of raise it is
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.
2026-08-02 00:09:01 +06:00
Claude Opus 5andbdeshi 751ab9c06f move the demo's coverage test beside the wiring it proves
The test rebuilt the feature list by hand, because a package cannot import a
main, and it drifted three times in one session — the dialect, notation, Compose
— each caught by a failing case rather than by the copy.

The obvious fix was a composition package under internal/ext, and it was the
wrong one: ADR-0069 forbids a feature importing its sibling, so that package
would have failed the gate on its first build and the invariant would have been
weakened one commit after becoming mechanical. Moving the test is smaller and
points the other way — cmd stays the only place a feature is named, which is
what extensions.md asks for, and wire_test.go was already here for the same
reason.

runServe and the test now call one function for the renderer, so there is no
list to keep in step. Four files named a feature before; three do now, all of
them package main, plus a benchmark that deliberately wires one extension to
measure the render path and never claimed to be the shipped list.

The core ceiling paid for it rather than being raised a third time: `given` was
a helper with one caller and is now inlined into the only function that used it.
core 2842/2850.

Latent item cleared — the one that was marked due.
2026-08-01 23:47:55 +06:00
Claude Opus 5andbdeshi 482a862ff5 require a counter row to say what does not count
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.
2026-08-01 23:25:22 +06:00
Claude Opus 5andbdeshi 8a4718d775 state invariant 7 as a rule the gate can hold
"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.
2026-08-01 23:21:46 +06:00
Claude Opus 5andbdeshi 8bfacc7e98 build a table of contents from the document's headings
`::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.
2026-08-01 22:09:14 +06:00
Claude Opus 5andbdeshi 1f168d973b render the HTML an author writes, and narrow the gate to one call site
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 &lt;b&gt; while the <span> beside it renders.

core 2793/2800, ext 1077/2000, 34 gates green, 0 warnings.
2026-08-01 21:08:06 +06:00
Claude Opus 5andbdeshi 7f9ac3c412 compare state.md's currency instead of declaring it
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.
2026-08-01 10:54:30 +06:00
Claude Opus 5andbdeshi 36194a16d8 point the editor preview at the demo, and shorten the skill name
Four small things from one sitting, none of which would be reverted without the
others:

- .claude/launch.json describes the only dev server this repo has: make demo on
  localhost:8080. Deliberately not autoPort — examples/demo-site declares
  base: http://localhost:8080, so canonical, hreflang, OpenGraph and sitemap URLs
  are built from that port, and a reassigned one would make the absolute URLs on
  the page wrong while still rendering fine.
- The skill is .claude/skills/feature-loop/, without the khosra- prefix. ADR-0054
  records it, because ADR-0030 had named the prefixed form while settling the
  project name and decisions.md is append-only.
- Makefile .PHONY was missing quiet and surface, added two commits ago. A file of
  either name in the repo root would have silently shadowed the target.
- A latent row: the root listing's title reads "A Khosra Demo · A Khosra Demo",
  because base.html joins page title and site title unconditionally and at the
  root they are the same string. Found by looking at the served page — no test
  asserts a title. Theme layer, one if, and it waits for Phase G4.
2026-08-01 02:46:30 +06:00
Claude Opus 5andbdeshi aad7d64270 record who authored a commit, and who merely asked for it
Authorship named the human on commits the agent wrote start to finish, with a
Co-Authored-By trailer as the only trace of who did the work. That is backwards:
directing a change is not writing it, and a log that cannot tell the two apart
cannot answer "how much of this did the agent write" — a question worth being
able to ask honestly about a repository built this way.

Three cases, no fourth: agent alone is agent-authored with no trailer, since the
author field already says it; both is the human's with the agent as co-author;
the human alone names only the human. The committer and the GPG signature stay
the human's throughout — the key attests to taking responsibility for a commit,
not to having typed it.

Applied to the whole history in the same sitting: 62 of 63 commits are now
agent-authored, init is the human's with a co-author trailer, and every tree,
message, parent and author date is byte-identical to before. Committer dates were
restored from backup/pre-fold, since the earlier fold had reset all 62 of them to
the moment of the replay.

The state.md sha rides along rather than taking its own commit: rewriting the
history is what invalidated it, and the verify.sh advisory that caught it is the
same one that caught the previous rewrite.
2026-08-01 02:37:37 +06:00
Claude Opus 5andbdeshi 7796b0d919 make context a budget the harness enforces
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.
2026-08-01 02:16:11 +06:00
Claude Opus 5andbdeshi 8256c72180 commit as part of the loop, and announce it when unasked
The loop gains a seventh step. Work was being committed all session on a
verbal instruction, which means it was a property of one conversation
rather than of the harness: a fresh session would have left everything in
the working tree and reported success.

Why automatic: a commit is one `git revert` from undone, and work that
only ever existed in the working tree is not recoverable — this session
already lost uncommitted work to a `git checkout` while signing was
broken. Why announced: the safety argument covers the loss, not the
surprise, so a session that did not ask for this gets one prominent line
before the first commit and the off switch, once.

Ownership split so nothing is stated twice: conventions.md "Git" owns
what one commit contains — now one *revertible* unit rather than one
feature, which is the human's correction to the "never bundle two passes"
reading — and CLAUDE.md §4 owns when a commit happens. ADR-0052 records
both, including that no gate can check this: no script can see whether a
sentence was said.

Docs 6 files, +72/-6 lines. No code, no counters moved.
2026-08-01 01:41:11 +06:00
Claude Opus 5andbdeshi de1ce73430 track the demo as a real site in examples/, gated like the docs
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.
2026-07-31 20:08:28 +06:00
Claude Opus 5andbdeshi cf65c82df6 name the layer, not only the primitive
The constitution asks which primitive a feature is. It never asked *where* the
feature belongs, and that let a whole feature get built at the wrong layer.

Four layers, outermost wins: content on disk, engine (facts only the engine can
produce), theme (markup), browser (CSS, then JS). Two rules fall out — the engine
never edits authored text to change how it looks, and a feature needing no engine
fact is not an engine feature. architecture.md carries the table and the test;
CLAUDE.md carries the one-line version, since it is the file always loaded.

The example is named in the docs on purpose. A rule with a scar attached is one an
agent can apply; a rule stated in the abstract gets reasoned around.
2026-07-31 12:46:41 +06:00
Claude Opus 5andbdeshi 98d0e52936 approve x/image and re-cost the core ceiling
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.
2026-07-31 02:48:52 +06:00
Claude Opus 5andbdeshi e6c0673374 gate that the staged tree builds, not just the working tree
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.
2026-07-31 02:25:01 +06:00
Claude Opus 5andbdeshi f721cbf645 gate the raw-HTML default instead of remembering it
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.
2026-07-30 10:31:01 +06:00
Claude Opus 5andbdeshi 159e5feb94 harness: a standing advisory is a defect
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.
2026-07-30 02:00:09 +06:00
Claude Opus 5andbdeshi b6806fa544 harness: flag any only in exported signatures
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.
2026-07-30 01:46:48 +06:00
Claude Opus 5andbdeshi f37e7b1308 harness: say which settings file the exemption means
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.
2026-07-30 01:39:28 +06:00
Claude Opus 5andbdeshi e14171fa02 harness: fail an untidy go.mod
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.
2026-07-30 01:36:34 +06:00
Claude Opus 5andbdeshi 2466ce79c7 harness: permissions changes owe no HARNESS.md note
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.
2026-07-30 01:35:14 +06:00
Claude Opus 5andbdeshi 242bd60d9c keep the build queue in an uncommitted .scratch/
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.
2026-07-30 01:30:48 +06:00
Claude Opus 5andbdeshi 781c756a0f harness: three gate fixes found by running against real code
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.
2026-07-30 01:30:11 +06:00
Claude Opus 5andbdeshi 9d817dcadf rename the project to khosra, initialise the module
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.
2026-07-30 01:14:01 +06:00
Claude Opus 5andbdeshi 126fdbc861 harness: soften two gates that would have taught --no-verify
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.
2026-07-30 01:09:22 +06:00
Claude Opus 5andbdeshi 99f8c730b0 harness: withdraw five pre-code ADRs to deferred ideas
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.
2026-07-30 01:01:38 +06:00
bdeshiandClaude Opus 5 02268f9121 init
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 00:34:18 +06:00