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.
This commit is contained in:
Claude Opus 5
2026-08-02 01:12:07 +06:00
committed by bdeshi
parent 2609f69a87
commit 1510a5ff7a
6 changed files with 78 additions and 75 deletions
+5 -7
View File
@@ -72,13 +72,11 @@ into an author's prose. It worked and it had tests; `text-wrap: pretty` does it
content, so the feature was deleted (ADR-0045). If a feature only rearranges how something looks, expect the
agent to push back toward CSS or a template.
**The ceilings have moved three times, and the third is a different kind.** The first two were
measurements — a figure costed before any code existed, then a contract that would not fit. The third
(ADR-0074) is a budget, signed off ahead of a list of features rather than in response to one. The values
themselves live in `scripts/budgets.env` and nowhere else, including here: each ADR records the old and new
figures, and prose that repeats them is a copy waiting to go stale. Invariant 9 — "core stops growing after
Arc 2" — is measured by a number that has now moved three times, and still says something only because `ext`
has risen far faster than core.
**Two ceilings, and raising one is a signature.** `CORE_LOC_MAX` and `EXT_LOC_MAX` exist so that "core
stops growing, ext rises" (invariant 9) is observable rather than asserted. The values live in
`scripts/budgets.env`; raising either needs an ADR stating what moved and why, and the ADR log is where that
history belongs, not here. Read a core raise as evidence something belongs in `internal/ext/` before reading
it as evidence the number was small.
**Context is a budget, and parts of it are mechanical.** The limit on this project is how much
work fits in a session, so `docs/context-economy.md` holds the reading, searching and reporting
+1 -1
View File
@@ -8,7 +8,7 @@ Read the one you need. Do not read them all.
| `state.md` | What exists now: inventory, earn-it counters, latent items | Every feature |
| `decisions.md` | ADR log — one entry per load-bearing choice | Append-only |
| `roadmap.md` | Arcs, earn-triggers, the core freeze point | When an arc completes |
| `content-model.md` | On-disk layout, frontmatter, post types, permalinks | When the disk format changes |
| `content-model.md` | On-disk layout, frontmatter, post types, permalinks **what the parser accepts today**, never a plan | When the disk format changes |
| `conventions.md` | Go style floor, package layout, perf and test rules | Rarely; via ADR if contested |
| `extensions.md` | Extension/plugin contract: target shape + earn gates | When earned or frozen |
| `toolchain.md` | Versions and agent-tooling contracts this was built against | When you deliberately move a version |
+11 -60
View File
@@ -7,8 +7,9 @@ statement here describes engine behaviour, not an obligation on content.
The most expensive thing in the engine to change, because published URLs are permanent and the files it
reads are somebody's database.
Markers are build order, not status: `[arc1]` build now · `[spec]` recorded intent, shape decided by
whatever builds it. What exists is `state.md`'s job. Never build a `[spec]` section because it is written here.
This describes what the parser accepts **today**. It is not a plan: a shape nobody has asked for lives in
`ideas/exploration.md`, which is storage and out of context by default. So there is nothing here to build
speculatively from, and no marker saying so.
## The site root
@@ -25,22 +26,22 @@ overrides the defaults the binary embeds, so a bare root still renders. Shortcod
templates/ # html/template overrides, optional
<site>/content/
posts/ # general blog [arc1]
posts/ # general blog
2026-03-hello-world/ # directory bundle
index.en.md
index.bn.md
cover.jpg # local asset, referenced relatively
comics/ # webcomics [spec]
comics/ # webcomics
the-long-monsoon/
_index.en.md # the series bundle; also the cascade point for it
first-rain/ # a chapter because it is nested here (ADR-0033); slug is never the position
index.en.md
page.png
art/ # single images or sets [spec]
writing/ # short stories, poems [spec]
status/ # short IndieWeb-style notes [spec]
art/ # single images or sets
writing/ # short stories, poems
status/ # short IndieWeb-style notes
2026-07-28-1030.md # single-file bundle, no folder
pages/ # about, contact, now [spec]
pages/ # about, contact, now
about/index.en.md
```
@@ -102,8 +103,7 @@ A type is a frontmatter value, never a code path: it may change the default View
membership and nothing else. Adding one must not add a branch to the core. Absent `type`, it defaults
from the top-level section directory.
`[spec]` A declaration file (`types:` in `site.yaml`) carrying per-type defaults — view, ordering, feed
membership, required fields, titleless legality, taxonomies — is recorded intent (`ideas/deferred-decisions.md`), not built.
A declaration file carrying per-type defaults is not built; the trigger is in `ideas/deferred-decisions.md`.
The set below is what the engine knows without one.
| Type | Section | Order | Feeds | Distinguishing need |
@@ -200,26 +200,6 @@ than breaking prev/next. Reading order is *ascending*, the opposite of a dated l
`draft` is not honoured yet, because no bundle carries it and nothing else excludes drafts either; the
feature that adds the field adds it here in the same change.
## Galleries, collections `[spec]`
Both are a Query over bundle metadata with a stable sort; neither justifies a new primitive. A gallery is
a Query plus an image View. Related posts are a Query with a scoring function, not a stored graph.
## Taxonomies `[spec]`
Two kinds, deliberately. Recorded intent (`ideas/deferred-decisions.md`), not built.
**`tags` are one global namespace** across every type. `/tags/{tag}/` lists everything carrying the tag;
`/{section}/tags/{tag}/` narrows it; listing views group results by type so a busy tag stays readable.
Cross-type discovery is the point — one tag spanning a comic, a poem and a photo essay is a feature here,
not noise. Term slugs are derived by locale and overridable by hand (ADR-0015).
**Declared taxonomies** are structural: a fixed term set that changes engine behaviour, declared on the
type — `series` for ordering, `medium` for art, `genre` for writing. These never enter the tag pool. The
test: a tag is free-form and cross-cutting, a declared taxonomy has known terms and drives behaviour.
Feeds follow the same shape: `/tags/{tag}/feed.xml` comes free from the same Query as the listing.
## Scaffolding
`khosra demo <empty dir>` writes a whole site that exercises every feature — both languages with a fallback, a
@@ -253,26 +233,6 @@ every page.
A canonical link, an `hreflang` and an OpenGraph URL become absolute as soon as `base` is declared, because
each is read by something that resolves neither against the page. Links between pages stay root-relative.
### Below the site level `[spec]`
Section-level and bundle-level settings are **not** built: a bundle's own frontmatter covers it, and nothing
reads a section override yet. The parked shape is site → section → bundle, nearest explicit value winning
(`ideas/deferred-decisions.md`).
| Level | Where it lives |
|---|---|
| site | `site.yaml` at the site root |
| section, and any enclosing section | that directory's `_index.<lang>.md` frontmatter |
| bundle | the bundle's own frontmatter |
The cascade carries stage toggles, `view` selection, taxonomy defaults, cache flags, and metadata
defaults — declared keys only, never arbitrary engine internals. Stages run on everything by default and
are switched off by a cascade key, not by a predicate inside the stage: a stage that finds nothing to do
returns early on content *shape* ("no images here"), which has nothing to do with type.
This is also how a template is chosen: type default, then section override, then the bundle's `view`
(ADR-0019).
## Extras: enumerated local assets
A bundle may hold `extras/` — drafts, notes, logs, scans, media — published as artefacts of the process
@@ -493,15 +453,6 @@ updated some of them would be worse than one that updates none (ADR-0055). Editi
The engine notices changes. It never fetches them — pulling a git repository is the operator's business, not
the engine's.
## Time-dependent presentation `[spec]`
Anything derivable from a page plus the current clock is computed by a Stage, never stored in content:
an "this article is old" banner compares `date` to now, relative dates likewise. Because renders are
cached (ADR-0005), such a Stage also declares how long its output stays true — a banner until
`date`+threshold, a relative date for a minute — and the cached entry expires then. Future-dated
publication is the same shape seen from the other side: the page becomes reachable at a moment nobody is
requesting it.
## Metadata and discovery
Canonical links, `hreflang` and OpenGraph are emitted from fields that already exist on the page — the theme
@@ -515,4 +466,4 @@ lists every bundle in every language it exists in — each variant is separately
entry — with `lastmod` only where a bundle carries a date. Both need `base`; without it the sitemap answers
404 rather than listing paths no crawler can resolve.
`[spec]` Twitter cards, JSON-LD and microformats2 have no consumer yet.
Twitter cards, JSON-LD and microformats2 have no consumer yet.
+1 -1
View File
@@ -1163,7 +1163,7 @@ measuring anything. Checked by shape rather than wording, since a gate that dema
and not the thinking.
Consequence: cheap — one column, one gate, and the four re-scopings are now written down where the next
person meets the counter rather than in an ADR they would have to find. Expensive — a new counter costs a
sentence that may be genuinely hard to write, which is the point and will still feel like friction.
sentence that may be genuinely hard to write, which will still feel like friction.
Revisit if: a counter is added whose exclusion is honestly "nothing" — then the column is wrong, or the
counter is.
+5 -6
View File
@@ -206,7 +206,7 @@ not insert characters into an author's prose to influence line breaking.
`<div class="footnotes">` carrying an `<ol>` of notes. Headings arrive with an `id`.
None of it is optional and none of it is configurable: a theme that styles none of these still renders a
correct page, which is the point of the contract. The reference stylesheet does the minimum — a rule under
correct page. The reference stylesheet does the minimum — a rule under
each row, an indent for definitions, smaller footnotes — and nothing more (ADR-0026).
## The stability rule
@@ -276,11 +276,10 @@ two drift together.
## Overriding it
A site root's `templates/` is parsed **after** the embedded set, and the last definition of a name wins, so
a theme redefines one named block and inherits the document (ADR-0019). Each set is built from named files
and only those are overlaid — `base.html` plus that kind's block file (`page.html` or `list.html`), and the
fragments on their own (`shortcodes.html` and `shortcodes/*.html`, ADR-0071). Overlaying every site template into every set would let a listing's `main`
leak into bundle pages, which is the collision per-kind sets exist to prevent.
Each set is built from named files and only those are overlaid — `base.html` plus that kind's block file
(`page.html` or `list.html`), and the fragments on their own (ADR-0019, ADR-0071). Overlaying every site
template into every set would let a listing's `main` leak into bundle pages, which is the collision per-kind
sets exist to prevent.
`templates/theme.css` in the site root replaces the reference stylesheet entirely; there is no merging.
+55
View File
@@ -72,3 +72,58 @@ sneakernet and QR offline distribution.
one endpoint, and it composes with git instead of fighting it.
- **Gemini output** is nearly free once the View layer exists, and it validates the claim that a
View can target something other than HTML. Good early proof, low cost.
## Shapes recorded before anything asked for them
Moved out of `docs/content-model.md`, which now describes only what the parser accepts. Each is
intent, not a plan: the shape is decided by whatever eventually builds it, and being written here is not
permission.
## Galleries, collections `[spec]`
Both are a Query over bundle metadata with a stable sort; neither justifies a new primitive. A gallery is
a Query plus an image View. Related posts are a Query with a scoring function, not a stored graph.
## Taxonomies `[spec]`
Two kinds, deliberately. Recorded intent (`ideas/deferred-decisions.md`), not built.
**`tags` are one global namespace** across every type. `/tags/{tag}/` lists everything carrying the tag;
`/{section}/tags/{tag}/` narrows it; listing views group results by type so a busy tag stays readable.
Cross-type discovery is the point — one tag spanning a comic, a poem and a photo essay is a feature here,
not noise. Term slugs are derived by locale and overridable by hand (ADR-0015).
**Declared taxonomies** are structural: a fixed term set that changes engine behaviour, declared on the
type — `series` for ordering, `medium` for art, `genre` for writing. These never enter the tag pool. The
test: a tag is free-form and cross-cutting, a declared taxonomy has known terms and drives behaviour.
Feeds follow the same shape: `/tags/{tag}/feed.xml` comes free from the same Query as the listing.
## Time-dependent presentation `[spec]`
Anything derivable from a page plus the current clock is computed by a Stage, never stored in content:
an "this article is old" banner compares `date` to now, relative dates likewise. Because renders are
cached (ADR-0005), such a Stage also declares how long its output stays true — a banner until
`date`+threshold, a relative date for a minute — and the cached entry expires then. Future-dated
publication is the same shape seen from the other side: the page becomes reachable at a moment nobody is
requesting it.
### Below the site level `[spec]`
Section-level and bundle-level settings are **not** built: a bundle's own frontmatter covers it, and nothing
reads a section override yet. The parked shape is site → section → bundle, nearest explicit value winning
(`ideas/deferred-decisions.md`).
| Level | Where it lives |
|---|---|
| site | `site.yaml` at the site root |
| section, and any enclosing section | that directory's `_index.<lang>.md` frontmatter |
| bundle | the bundle's own frontmatter |
The cascade carries stage toggles, `view` selection, taxonomy defaults, cache flags, and metadata
defaults — declared keys only, never arbitrary engine internals. Stages run on everything by default and
are switched off by a cascade key, not by a predicate inside the stage: a stage that finds nothing to do
returns early on content *shape* ("no images here"), which has nothing to do with type.
This is also how a template is chosen: type default, then section override, then the bundle's `view`
(ADR-0019).