docs: split the root README into owned documents under docs/ and guard it with drift tests #266

Merged
dominik.polakovics merged 5 commits from afk/263 into main 2026-10-13 20:03:32 +02:00

Closes #263

The 3,182-line README is now a 243-line orientation page. Its content moved without rewording to the document that owns each kind of fact, and four drift tests keep it that way. The rewriting #263 left out (correcting and condensing prose, the 20 stubbed route sections, extracting ADRs) is filed as #265.

Review the move with git diff --color-moved=dimmed-zebra origin/main..HEAD -- README.md docs/ scaffold/site/AGENTS.md.

Where the content went

File Content
README.md (243 lines) intro, Quick start (code block plus one pointer line), Releases, Documentation (new), Commands (new one-row-per-command table), Development and Editor language catalogs, v1 limitations, Repository layout
docs/api.md ## Conventions (Server-described forms, Coded messages, Sidecar labels on the wire, Translations on the wire), then one ### METHOD /api/path heading for each of the 66 registered routes: 15 hold moved material, 31 a one-line pointer to the convention section that covers them, and 20 the stub Documented in #265.
docs/harness.md commands (the usage text in place of the README's outdated sh block), serving (the Quick start paragraph that named GET /api/status), reverse proxy, harness lang, harness.yaml, the receipt, machine-readable output
docs/host-navigation.md ### Host navigation and all of its #### children, verbatim, including "What the editor accepts" (see Deviations)
docs/features.md Scaffolding and Feature bundles, minus the receipt and the machine-readable output (now in harness.md)
docs/architecture.md the How it works prose, one ### per bold-lead paragraph, plus the Multilingual sites model
docs/adr/README.md the record format; no records yet (#265)
docs/agents/docs-map.md code area → owning document, plus the rules: the README gains no sections, condense rather than inflate, a brief names the owning document
docs/.docs-sync the sync baseline: the main commit's SHA and 2026-10-13
CLAUDE.md new section "Every fact has one document": owner table, the README rule, the planning/grilling sentence, and pointers to the guards

Text changed in the move

Moved text was copied byte for byte, apart from heading levels and link targets that point into other files. These are the exceptions, each fixing a reference the move made false:

  • architecture.md, Generated forms: "see Server-described forms below" now reads "… in api.md".
  • api.md, the /api/form bullet: "Coded messages below", "Sidecar labels on the wire below" and "the page tree below" now say "above".
  • api.md, the verdicts paragraph: "Each shortcode node of that tree" is now "… of the page tree", because the paragraph it followed now sits under POST /api/page/save.
  • README, Editor language catalogs: "see 'The editor's language' above" now links docs/host-navigation.md#the-editors-language.
  • docs/harness.md: the README's sh synopsis block (which listed reconcile in a different place from the binary) is replaced by the exact usage() text, as the issue prefers. The section is called "Commands", not "The ten commands".
  • Fresh text: each doc's preamble, the route pointers and stubs, the README's Documentation and Commands sections, the docs map, the ADR README and the CLAUDE.md section.

A coverage script splits the original README into 437 units (paragraphs, list items, tables, fenced blocks). Each unit is either in exactly one new place, or accounted for above or in the AGENTS.md table below. (The one unit found twice is a sentence the original README also contains twice.)

Configuration reference → scaffold/site/AGENTS.md

Each unit of the nine sidecar sections was compared with AGENTS.md:

  • 55 dropped: AGENTS.md already states them. A few are stated in scaffold/themes/deckle/README.md, which AGENTS.md links to: the slot wrapper, the layout resolution order and inheritance.
  • 39 added to AGENTS.md: a new ### Backend layouts section, and additions to "Element sidecar schema", "Body policy", "Page types" (including Content-root authority) and "Editor modules".
  • 8 wire or implementation fragments: placed under GET /api/modules, /api/categories, /api/module/entries and /api/tree in api.md, and a short paragraph in architecture.md.

No AGENTS.md heading was renamed. go test ./scaffold/ ./internal/element/ is green.

Full disposition table (every unit)
README section unit (first ~10 words, quoted) disposition AGENTS.md section
Element sidecar "One file per shortcode, editor/elements/<name>.yaml, where <name> is the shortcode name. Worked example" dropped "Adding or changing an element" (name pairing); "Element sidecar schema (editor/elements/<name>.yaml)"
Element sidecar yaml version: 1 # required, integer >= 1 added (comment on the schema example) "Element sidecar schema (editor/elements/<name>.yaml)"
Element sidecar yaml label: Hero # required added (comment) "Element sidecar schema (editor/elements/<name>.yaml)"
Element sidecar yaml icon: hero # optional added (comment) "Element sidecar schema (editor/elements/<name>.yaml)"
Element sidecar yaml group: content # structure | content | inline (default content) added (default content) "Element sidecar schema (editor/elements/<name>.yaml)"
Element sidecar yaml policy: rte # none | markdown | rte (default none) added (default none, in the example comment and the Body policy paragraph) "Element sidecar schema (editor/elements/<name>.yaml)" — Body policy
Element sidecar yaml allowedInline: [button] # rte-only: inline elements offered as chips dropped (chips) / added (rte-only, see Body policy row) "Element sidecar schema" — Body policy
Element sidecar yaml # allow: [bold, italic, link, bulletList, orderedList, h2, h3, h4] dropped "Element sidecar schema" — Body policy (AGENTS.md's list is the current one, table included)
Element sidecar yaml allowedIn: [page] # parent element names, plus "page" for top level dropped "Element sidecar schema" (example) and Placement
Element sidecar yaml # allowedAt: [post, page] # page-type names dropped "Element sidecar schema" (example) and Placement
Element sidecar yaml type: text # one of the eleven field types (multiselect: page-type fields only) dropped "Element sidecar schema" — Field types
Element sidecar yaml label: Heading / required: true dropped "Element sidecar schema" (example)
Element sidecar yaml # default: ... # any spelling Hugo reads for the field type dropped "Element sidecar schema" — "Every field type reads a value in any spelling Hugo reads"
Element sidecar yaml # options: [a, b] # required for type select, forbidden otherwise added (example comment now says it) "Element sidecar schema (editor/elements/<name>.yaml)"
Element sidecar yaml # tab: appearance # form tab: general (default) | appearance dropped "Element sidecar schema" (example) and Form tabs
Element sidecar "A param's optional tab picks the tab of the editor form" dropped "Element sidecar schema" — Form tabs; "Page types" — tab (access legal on a page-type field)
Element sidecar "Each field type is defined once and reads a value in any spelling" dropped (datetime spellings, UTC, RFC3339, load error, stored = no value, module list, next save, number not 0) "Element sidecar schema" — "Every field type reads a value…"; multiselect single string: "Page types" — multiselect
Element sidecar same paragraph: "every form — element, item row, inline element, properties —" added (replaces "the editor's form (element or properties)") "Element sidecar schema" — "Every field type reads a value…"
The categories field type "categories holds a selection of the site's categories: the category pages" dropped "Element sidecar schema" — Field types (categories paragraph)
The categories field type list "a field or param of type categories with another name;" (3 items) dropped "Element sidecar schema" — Field types (categories paragraph)
The categories field type "The editor offers the categories' titles, sorted by title, and stores their slugs" dropped "Element sidecar schema" — Field types (categories paragraph)
The categories field type same paragraph: "the list comes from GET /api/categories?lang= ({categories: [{slug, title}]}, read fresh on every request)" architecture (HTTP wire → docs/api.md ### GET /api/categories) —
The categories field type table "Declared in | Stored as" dropped "Element sidecar schema" — Field types (two storage bullets)
The categories field type "Saving stores exactly the selected categories, in title order, and saving" dropped (exact set, title order, empty removes, stale slug hidden and dropped, empty picker without the bundle, news requires categories) "Element sidecar schema" — Field types; intro and "Feature bundles" (news requires categories)
The categories field type same paragraph: "(422, …), for element saves, page-property saves, inline elements and the create dialog alike" added "Element sidecar schema" — Field types (categories paragraph)
The categories field type "A page type opts in with one field:" + yaml dropped "Page types" — categories (field type) bullet
Body policy "body is either a scalar policy (body: rte) or a mapping." added "Element sidecar schema" — Body policy
Body policy "allow and allowedInline are legal only when policy: rte." added "Element sidecar schema" — Body policy
Body policy "allow restricts formatting to a subset of {…} — an absent allow means the full set" dropped (AGENTS.md states the current subset and default, which add table opt-in; the README's set is stale) "Element sidecar schema" — Body policy
Body policy "an empty list means no formatting" added "Element sidecar schema" — Body policy
Body policy "h1 is never allowed." dropped "Element sidecar schema" — Body policy
Body policy "allowedInline names other loaded elements whose group is inline; each must exist" added "Element sidecar schema" — Body policy
Body policy "A body save checks every inline element in a markdown or rte body" dropped "Element sidecar schema" — Body policy (same paragraph)
Page-type sidecar "One file per type, editor/pagetypes/<name>.yaml. A page records its type" added "Page types (editor/pagetypes/<name>.yaml)" (opening paragraph)
Page-type sidecar yaml version: 1 # required, must be exactly 1 added (comment) "Page types (editor/pagetypes/<name>.yaml)"
Page-type sidecar yaml label: Page # optional; defaults to the type name added (comment) "Page types (editor/pagetypes/<name>.yaml)"
Page-type sidecar yaml icon: page # optional added (comment) "Page types (editor/pagetypes/<name>.yaml)"
Page-type sidecar yaml # singleton: true # optional; hides create handles for this type added (example line) "Page types (editor/pagetypes/<name>.yaml)"
Page-type sidecar yaml children: [page, section] # page types creatable beneath this one dropped "Page types" (example)
Page-type sidecar yaml # elements: [hero, notice] # element allowlist for pages of this type dropped "Page types" (example); elements bullet
Page-type sidecar yaml fields: # page-level front-matter fields (same vocabulary) + title field dropped "Page types" (example)
Page-type sidecar yaml hidden field, tab: access # form tab: general (default) | appearance | access, default: false dropped "Page types" (example hidden field; tab bullet)
Page-type sidecar "children and elements distinguish three states, preserved to the wire: absent/null = unrestricted" dropped (children) / added (elements, new bullet) "Page types" — children bullet; new elements bullet
Page-type sidecar same paragraph: "preserved to the wire" architecture (wire fact, not a site-developer fact) —
Page-type sidecar "A field's optional tab picks the properties form tab it sits on" dropped "Page types" — tab bullet
Page-type sidecar "singleton is optional and purely presentational: the editor hides create-handles" added "Page types" — new singleton bullet
Page-type sidecar "The reserved keys type, draft, weight, aliases, backendLayout," dropped "Page types" — defaults bullet
Page-type sidecar "A field of the categories type — name: categories" dropped "Page types" — categories (field type) bullet
Page-type sidecar "shownFields picks which fields the page's properties form shows" dropped "Page types" — shownFields bullet
Page-type sidecar same paragraph: "(the scaffold's news-item gets only "Backend layout")" added (as "the news bundle's news-item" — news-item ships in the news bundle, not the scaffold) "Page types" — shownFields bullet
Page-type sidecar yaml shownFields: [teaser, backendLayout] # title + teaser + own layout picker dropped "Page types" (example shownFields line)
Page-type sidecar "Two more page-type keys shape the editor around [editor modules]" dropped (lead-in) "Page types"
Page-type sidecar "hideChildPages: true hides a page's managed children from the page tree" dropped (managed, other children stay, no module hides nothing, stays in tree data) "Page types" — hideChildPages bullet
Page-type sidecar same bullet: "its row still expands when any remain" added "Page types" — hideChildPages bullet
Page-type sidecar same bullet: "selecting one selects its nearest drawn ancestor" added "Page types" — hideChildPages bullet
Page-type sidecar same bullet: "Hidden pages stay in the /api/tree payload (marked hidden)" dropped (the fact) / architecture (the hidden mark on GET /api/tree → docs/api.md) "Page types" — hideChildPages bullet ("stay in the editor's tree data")
Page-type sidecar "create: true on an optional field asks for it in the create dialog" dropped "Page types" — create bullet
Content-root authority "There is no reserved declaration file for the content root." (whole paragraph) added "Page types" — new Content-root authority. paragraph
Content-root authority yaml version: 1 / singleton: true / children: [page, section] added "Page types" — Content-root authority.
Content-root authority "harness add extends this list when a feature bundle asks for it" dropped "Page types" — children bullet; "Feature bundles"
Content-root authority "(The earlier reserved _root.yaml mechanism has been removed; a leftover" added "Page types" — Content-root authority.
Backend layout sidecar "One file per layout, editor/backendlayouts/<name>.yaml. A backend layout is a" added new "Backend layouts (editor/backendlayouts/<name>.yaml)"
Backend layout sidecar yaml (all keys: version, label, allowedAt, rows/columns, name, label, colspan, inherit) added new "Backend layouts (editor/backendlayouts/<name>.yaml)"
Backend layout sidecar "A slot's elements allowlist and the layout's allowedAt follow the same" added new "Backend layouts (editor/backendlayouts/<name>.yaml)"
Backend layout sidecar "The insert options offered in a slot are the intersection of four" added new "Backend layouts (editor/backendlayouts/<name>.yaml)"
Slots, layout resolution, and inheritance "A page persists a slot's content as a top-level slot wrapper shortcode" + fenced block dropped "Editing content by hand" — Slot wrappers; theme README "Backend layouts and slots" (linked from "Backend layouts")
Slots, layout resolution, and inheritance "slot is reserved and harness-managed: it is never offered in the insert" dropped theme README "Backend layouts and slots" (linked from "Backend layouts"); round-trip: "Editing content by hand" intro
Slots, layout resolution, and inheritance "Which layout applies to a page — its effective layout — resolves" + list 1–4 dropped theme README "Resolution" (linked from "Backend layouts"); "none — a flat element list in the editor": "Backend layouts" opening paragraph
Slots, layout resolution, and inheritance "A slot that is empty or absent on a page inherits the nearest" dropped (inherit, no opt-out) theme README "Inheritance (slide)" (linked from "Backend layouts")
Slots, layout resolution, and inheritance same paragraph: "The page module shows inherited content greyed out and read-only, with a link" added new "Backend layouts (editor/backendlayouts/<name>.yaml)"
Slots, layout resolution, and inheritance "The split worth internalizing: **the backend layout decides what is editable" dropped theme README "Authoring a layout" (linked from "Backend layouts")
Slots, layout resolution, and inheritance "Switching a page's layout is non-destructive: slot wrappers that the new grid" added new "Backend layouts (editor/backendlayouts/<name>.yaml)"
Editor module sidecar "One file per module, editor/modules/<name>.yaml; the file name is the module" dropped (id + URL segment, strict decoding, read fresh, language file beside it, rail order after built-ins, bundle ships/add vendors/remove deletes/hand-written) "Editor modules (editor/modules/<name>.yaml)"; "Layout" (editor/modules/ bullet); "Feature bundles"; "Adding or changing an element" (dotted name = language file)
Editor module sidecar same paragraph: "(internal/sidecardir: one per file, …)" and "validated against the loaded page types (internal/editormodule)" architecture (package names); the validation fact itself is dropped ("Editor modules" — Load errors) —
Editor module sidecar same paragraph: "The editor polls the module list with the page tree, so a" added "Editor modules (editor/modules/<name>.yaml)" (opening paragraph)
Editor module sidecar yaml keys version, label, icon, pageTypes, views, columns, card roles, sort, search, filters, pageSize, edit dropped "Editor modules" (identical example)
Editor module sidecar yaml # list | cards; two or more views give a switcher added (comment) "Editor modules" (example)
Editor module sidecar "There is no key for where entries live: the tree selection and the" dropped "Editor modules" (container/entry bullets)
Editor module sidecar same paragraph: "GET /api/tree marks both (containerFor, managedBy)." architecture (HTTP wire → docs/api.md ### GET /api/tree) —
Editor module sidecar "Fields. Field names refer to the fields the listed page types declare," dropped "Editor modules" — Fields.
Editor module sidecar table "Place | Allowed" dropped "Editor modules" — Fields. (bullet list)
Editor module sidecar "GET /api/modules hands the editor no field types: each field the module" … "a declared field's label has none." architecture (HTTP wire → docs/api.md ### GET /api/modules) —
Editor module sidecar same paragraph: "A select or multiselect filter offers the field's options, a categories" dropped (filter options, publish state not filterable, categories titles in columns/tags, stale slug hidden, tree order without sort, missing sort field last) "Editor modules" — Fields.
Editor module sidecar same paragraph: "(select and multiselect, …) is a load error: they match differently" added (AGENTS.md said only "cannot be a filter") "Editor modules" — Fields. (filters bullet)
Editor module sidecar "Load errors. Each rejects that one file with a message naming it" + 8 bullets dropped "Editor modules" — Load errors.
Editor module sidecar reserved-id bullet: "deckle-hosted's proxy does" added "Editor modules" — Load errors.
Editor module sidecar "The tree in a feature module. The page tree stays as the module's left" dropped (selection-only, containers selectable, greyed out but expands, URL names container or entry, "Select a News page…") "Editor modules" — In the editor.
Editor module sidecar same paragraph: "While an entry is open its container is the selected row. Switching modules keeps the page path." added "Editor modules" — In the editor.
Editor module sidecar "The module screen lists the selected container's entries in the declared views." dropped (views, search/sort/filters/paging in URL query, New button per allowed type with singletons excluded, create dialog fields, opens on first tab) "Editor modules" — In the editor.; container bullets; "Page types" — create
Editor module sidecar same paragraph: "(column headers in a list, a sort menu for cards)", "run on the server", "with the active view, so reload, Back and a shared link keep them" added "Editor modules" — In the editor.
Editor module sidecar same paragraph: "(GET /api/module/entries, which serves every datetime as RFC3339)" architecture (HTTP wire → docs/api.md ### GET /api/module/entries) —
Editor module sidecar "The entry editor has one tab per edit item — Properties (the page" dropped (tabs) "Editor modules" — In the editor.
Editor module sidecar same paragraph: "with the active tab in the URL" added "Editor modules" — In the editor.
Editor module sidecar same paragraph: "Its toolbar goes back to the list, saves (Save, Save & close)," … "as everywhere." added (as "The entry editor's toolbar …") "Editor modules" — new paragraph after In the editor.
Editor module sidecar "In the Page module, a container offers "Manage in " (news: "Manage in" dropped (Manage in / Open in) "Editor modules" — In the editor.
Editor module sidecar same paragraph: "its back-button fallback and the landing after deleting it go to its container" added "Editor modules" — In the editor.
Sidecar language files "A sidecar's labels are single-valued; their translations live beside it, the" dropped (beside, four directories, .yaml/.yml, any tag such as de/pt-BR, not a content language, dotted name = language file, text only keyed by name) "Sidecar language files"; "Adding or changing an element" (names hold no dot)
Sidecar language files same paragraph: "any tag pagepath.ValidLangTag accepts" and "internal/sidecardir alone recognises the name (split at the first dot …) … every other walker (harness add, its receipts, harness sidecar) asks it." architecture (internal owner of the name rule) —
Sidecar language files table "Family | Keys" (element, page type, backend layout, editor module) dropped "Element sidecar schema" — Language file; "Page types" — Language file; "Editor modules" — Language file.; backend layout: theme README "Authoring a layout" (pointed at by "Sidecar language files")
Sidecar language files "Every entry is optional, and an empty value counts as absent: the" dropped (optional/empty, load errors and where reported, no version, failed sidecar skips its files, changes nothing but labels) "Sidecar language files" (Lenient, Strict, Text only bullets)
Sidecar language files same paragraph: "The scaffold ships a .de.yaml beside every sidecar under scaffold/editor/ and under both bundles' editor/ trees" dropped "Sidecar language files" — Shipped.; "Layout" (language file bullet)
Sidecar language files same paragraph: "in the register and terms of web/src/i18n/glossary.md; a scaffold test holds each complete (every text key of its source translated). select option values, feature.yaml manifests and the starter home page are not translated." architecture (deckle repository's translation policy); select values untranslated is also stated in "Element sidecar schema" — Language file —

Guards

Each guard fails with a message naming the owning document. These were captured by breaking each invariant once:

  1. Routes (internal/server/routes_doc_test.go). route() now dispatches through an apiRoutes table: the same 66 paths and handlers, unknown /api/ paths still 404. That table is the one owner of the route set. TestAPIDocNamesEveryRoute holds it equal to the ### route headings in docs/api.md and rejects /api/host (reserved for the host). TestMutatingRoutesAreRegistered checks every mutatingRoutes key against the table.
    docs/api.md has heading "GET /api/frobnicate", which is not a registered route: remove it, or register the route in apiRoutes
    
  2. Commands (cmd/harness/docs_test.go). A commands table drives run() and the commands: part of usage(); the printed usage text is byte-identical to before. lang gets a langSubcommands table. docs/harness.md must carry the usage text verbatim and a bullet for every command, including version. The README's command table must have one row per command, each with its usage summary.
    docs/harness.md does not document command "version": the CLI reference is docs/harness.md (see docs/agents/docs-map.md); add its bullet there (- **`version`** …) in the section that holds the usage text
    
  3. README shape (docs/docs_test.go). At most 300 lines; no harness /api/ path (a forge URL like git.cloonar.com/api/v1/… is allowed); no json/yaml block; the Documentation section links every owning doc.
    README.md names an HTTP API path on 1 lines (line 245): routes and wire shapes belong in docs/api.md under a `### METHOD /api/path` heading — the README links to it and names no route
    
  4. Links (docs/docs_test.go). Every relative link and #anchor in README.md and docs/**/*.md must resolve.
    docs/features.md:175: link (harness.md#the-recipt): heading #the-recipt does not exist in docs/harness.md — headings are matched by their GitHub anchor, so a renamed or moved heading breaks its links
    

The heading and anchor and link rules live once, in the new internal/mddoc. scaffold/skills_test.go's private headings() was removed in favour of it.

Deviations and things to check

  • .claude/skills/ is not tracked in this repo. The lab's info/exclude excludes it, and it is a vendored skill bundle. So the requested edits to update-docs/SKILL.md and triage/AGENT-BRIEF.md cannot ship from this branch. The two rules live in docs/agents/docs-map.md instead, which update-docs step 3 already treats as overriding its generic steps. Proposed text for the vendored skills:
    • update-docs, Standing rules: "Condense, don't inflate. A sync may remove or condense prose that the owning document (per the docs map) now carries, replacing it with a link. A doc that has grown past its scope is drift to report and fix, not surrounding depth to match."
    • triage AGENT-BRIEF.md, Principles: "### Name the owning document — When the change adds or alters a documented fact, the brief names the document that owns it, taken from the repo's docs map (docs/agents/docs-map.md) — never 'document it in the README'."
    • grill-with-docs / ADR-FORMAT already name docs/adr/, and that path now exists. Nothing to change there.
  • "What the editor accepts" stays in host-navigation.md. It describes how the editor validates /api/host data, so it is part of the versioned contract. The issue's table listed it for api.md but also said all of Host navigation's children move verbatim.
  • The route count is 66, not 67. The issue's 67 likely included /api/host, which is reserved and not a route.
  • scaffold/skills_test.go keeps its hand-written harnessCommands map. It is an existing second copy of the command set that the scaffold package cannot import from package main. Worth a follow-up.
  • Stale prose noticed but not fixed (pure move; for #265):
    • The README said an empty slot always inherits; the code only inherits for slots marked inherit. AGENTS.md did not import that sentence.
    • The README's allow token set omitted table.
    • features.md "Installing" says Page-type allowances is in "the next section", but the next section is Bundles from a directory.
  • The scaffolded site skills reference no root-README section, and go test ./scaffold/ confirms they still resolve. They are untouched.
  • web/src/api/*.ts, internal/server and internal/removecmd comments that cited README sections now cite their new document.

Verification

  • go test ./... green; go vet ./... and gofmt clean.
  • wc -l README.md gives 243; grep -cE '(^|[^[:alnum:]_])/api/' README.md gives 0.
  • harness init into a scratch directory, then harness check: ok: no problems found, exit 0.

🤖 Generated with Claude Code

Closes #263 The 3,182-line README is now a 243-line orientation page. Its content moved **without rewording** to the document that owns each kind of fact, and four drift tests keep it that way. The rewriting #263 left out (correcting and condensing prose, the 20 stubbed route sections, extracting ADRs) is filed as **#265**. Review the move with `git diff --color-moved=dimmed-zebra origin/main..HEAD -- README.md docs/ scaffold/site/AGENTS.md`. ## Where the content went | File | Content | |---|---| | `README.md` (243 lines) | intro, Quick start (code block plus one pointer line), Releases, **Documentation** (new), **Commands** (new one-row-per-command table), Development and Editor language catalogs, v1 limitations, Repository layout | | `docs/api.md` | `## Conventions` (Server-described forms, Coded messages, Sidecar labels on the wire, Translations on the wire), then one `### METHOD /api/path` heading for each of the **66** registered routes: 15 hold moved material, 31 a one-line pointer to the convention section that covers them, and 20 the stub `Documented in #265.` | | `docs/harness.md` | commands (the usage text in place of the README's outdated `sh` block), serving (the Quick start paragraph that named `GET /api/status`), reverse proxy, `harness lang`, `harness.yaml`, the receipt, machine-readable output | | `docs/host-navigation.md` | `### Host navigation` and all of its `####` children, verbatim, including "What the editor accepts" (see Deviations) | | `docs/features.md` | Scaffolding and Feature bundles, minus the receipt and the machine-readable output (now in harness.md) | | `docs/architecture.md` | the How it works prose, one `###` per bold-lead paragraph, plus the Multilingual sites model | | `docs/adr/README.md` | the record format; no records yet (#265) | | `docs/agents/docs-map.md` | code area → owning document, plus the rules: the README gains no sections, condense rather than inflate, a brief names the owning document | | `docs/.docs-sync` | the sync baseline: the main commit's SHA and 2026-10-13 | | `CLAUDE.md` | new section "Every fact has one document": owner table, the README rule, the planning/grilling sentence, and pointers to the guards | ### Text changed in the move Moved text was copied byte for byte, apart from heading levels and link targets that point into other files. These are the exceptions, each fixing a reference the move made false: - architecture.md, Generated forms: "see *Server-described forms* below" now reads "… in [api.md](api.md#server-described-forms)". - api.md, the `/api/form` bullet: "**Coded messages** below", "**Sidecar labels on the wire** below" and "the page tree below" now say "above". - api.md, the verdicts paragraph: "Each shortcode node of that tree" is now "… of the page tree", because the paragraph it followed now sits under `POST /api/page/save`. - README, Editor language catalogs: "see 'The editor's language' above" now links `docs/host-navigation.md#the-editors-language`. - docs/harness.md: the README's `sh` synopsis block (which listed `reconcile` in a different place from the binary) is replaced by the exact `usage()` text, as the issue prefers. The section is called "Commands", not "The ten commands". - Fresh text: each doc's preamble, the route pointers and stubs, the README's Documentation and Commands sections, the docs map, the ADR README and the CLAUDE.md section. A coverage script splits the original README into 437 units (paragraphs, list items, tables, fenced blocks). Each unit is either in exactly one new place, or accounted for above or in the AGENTS.md table below. (The one unit found twice is a sentence the original README also contains twice.) ## Configuration reference → `scaffold/site/AGENTS.md` Each unit of the nine sidecar sections was compared with AGENTS.md: - **55 dropped:** AGENTS.md already states them. A few are stated in `scaffold/themes/deckle/README.md`, which AGENTS.md links to: the `slot` wrapper, the layout resolution order and inheritance. - **39 added to AGENTS.md:** a new `### Backend layouts` section, and additions to "Element sidecar schema", "Body policy", "Page types" (including Content-root authority) and "Editor modules". - **8 wire or implementation fragments:** placed under `GET /api/modules`, `/api/categories`, `/api/module/entries` and `/api/tree` in api.md, and a short paragraph in architecture.md. No AGENTS.md heading was renamed. `go test ./scaffold/ ./internal/element/` is green. <details><summary>Full disposition table (every unit)</summary> | README section | unit (first ~10 words, quoted) | disposition | AGENTS.md section | | --- | --- | --- | --- | | Element sidecar | "One file per shortcode, `editor/elements/<name>.yaml`, where `<name>` is the shortcode name. Worked example" | dropped | "Adding or changing an element" (name pairing); "Element sidecar schema (`editor/elements/<name>.yaml`)" | | Element sidecar | yaml `version: 1 # required, integer >= 1` | added (comment on the schema example) | "Element sidecar schema (`editor/elements/<name>.yaml`)" | | Element sidecar | yaml `label: Hero # required` | added (comment) | "Element sidecar schema (`editor/elements/<name>.yaml`)" | | Element sidecar | yaml `icon: hero # optional` | added (comment) | "Element sidecar schema (`editor/elements/<name>.yaml`)" | | Element sidecar | yaml `group: content # structure \| content \| inline (default content)` | added (default content) | "Element sidecar schema (`editor/elements/<name>.yaml`)" | | Element sidecar | yaml `policy: rte # none \| markdown \| rte (default none)` | added (default none, in the example comment and the **Body policy** paragraph) | "Element sidecar schema (`editor/elements/<name>.yaml`)" — **Body policy** | | Element sidecar | yaml `allowedInline: [button] # rte-only: inline elements offered as chips` | dropped (chips) / added (rte-only, see Body policy row) | "Element sidecar schema" — **Body policy** | | Element sidecar | yaml `# allow: [bold, italic, link, bulletList, orderedList, h2, h3, h4]` | dropped | "Element sidecar schema" — **Body policy** (AGENTS.md's list is the current one, `table` included) | | Element sidecar | yaml `allowedIn: [page] # parent element names, plus "page" for top level` | dropped | "Element sidecar schema" (example) and **Placement** | | Element sidecar | yaml `# allowedAt: [post, page] # page-type names` | dropped | "Element sidecar schema" (example) and **Placement** | | Element sidecar | yaml `type: text # one of the eleven field types (multiselect: page-type fields only)` | dropped | "Element sidecar schema" — **Field types** | | Element sidecar | yaml `label: Heading` / `required: true` | dropped | "Element sidecar schema" (example) | | Element sidecar | yaml `# default: ... # any spelling Hugo reads for the field type` | dropped | "Element sidecar schema" — "Every field type reads a value in any spelling Hugo reads" | | Element sidecar | yaml `# options: [a, b] # required for type select, forbidden otherwise` | added (example comment now says it) | "Element sidecar schema (`editor/elements/<name>.yaml`)" | | Element sidecar | yaml `# tab: appearance # form tab: general (default) \| appearance` | dropped | "Element sidecar schema" (example) and **Form tabs** | | Element sidecar | "A param's optional `tab` picks the tab of the editor form" | dropped | "Element sidecar schema" — **Form tabs**; "Page types" — **`tab`** (access legal on a page-type field) | | Element sidecar | "Each field type is defined once and reads a value in any spelling" | dropped (datetime spellings, UTC, RFC3339, load error, stored = no value, module list, next save, `number` not `0`) | "Element sidecar schema" — "Every field type reads a value…"; multiselect single string: "Page types" — **`multiselect`** | | Element sidecar | same paragraph: "every form — element, item row, inline element, properties —" | added (replaces "the editor's form (element or properties)") | "Element sidecar schema" — "Every field type reads a value…" | | The `categories` field type | "`categories` holds a selection of the site's categories: the category pages" | dropped | "Element sidecar schema" — **Field types** (`categories` paragraph) | | The `categories` field type | list "a field or param of type `categories` with another name;" (3 items) | dropped | "Element sidecar schema" — **Field types** (`categories` paragraph) | | The `categories` field type | "The editor offers the categories' titles, sorted by title, and stores their slugs" | dropped | "Element sidecar schema" — **Field types** (`categories` paragraph) | | The `categories` field type | same paragraph: "the list comes from `GET /api/categories?lang=` (`{categories: [{slug, title}]}`, read fresh on every request)" | architecture (HTTP wire → docs/api.md `### GET /api/categories`) | — | | The `categories` field type | table "Declared in \| Stored as" | dropped | "Element sidecar schema" — **Field types** (two storage bullets) | | The `categories` field type | "Saving stores exactly the selected categories, in title order, and saving" | dropped (exact set, title order, empty removes, stale slug hidden and dropped, empty picker without the bundle, news requires categories) | "Element sidecar schema" — **Field types**; intro and "Feature bundles" (news requires categories) | | The `categories` field type | same paragraph: "(422, …), for element saves, page-property saves, inline elements and the create dialog alike" | added | "Element sidecar schema" — **Field types** (`categories` paragraph) | | The `categories` field type | "A page type opts in with one field:" + yaml | dropped | "Page types" — **`categories`** (field type) bullet | | Body policy | "`body` is either a scalar policy (`body: rte`) or a mapping." | added | "Element sidecar schema" — **Body policy** | | Body policy | "`allow` and `allowedInline` are legal only when `policy: rte`." | added | "Element sidecar schema" — **Body policy** | | Body policy | "`allow` restricts formatting to a subset of {…} — an absent `allow` means the full set" | dropped (AGENTS.md states the current subset and default, which add `table` opt-in; the README's set is stale) | "Element sidecar schema" — **Body policy** | | Body policy | "an empty list means no formatting" | added | "Element sidecar schema" — **Body policy** | | Body policy | "`h1` is never allowed." | dropped | "Element sidecar schema" — **Body policy** | | Body policy | "`allowedInline` names other loaded elements whose `group` is `inline`; each must exist" | added | "Element sidecar schema" — **Body policy** | | Body policy | "A body save checks every inline element in a `markdown` or `rte` body" | dropped | "Element sidecar schema" — **Body policy** (same paragraph) | | Page-type sidecar | "One file per type, `editor/pagetypes/<name>.yaml`. A page records its type" | added | "Page types (`editor/pagetypes/<name>.yaml`)" (opening paragraph) | | Page-type sidecar | yaml `version: 1 # required, must be exactly 1` | added (comment) | "Page types (`editor/pagetypes/<name>.yaml`)" | | Page-type sidecar | yaml `label: Page # optional; defaults to the type name` | added (comment) | "Page types (`editor/pagetypes/<name>.yaml`)" | | Page-type sidecar | yaml `icon: page # optional` | added (comment) | "Page types (`editor/pagetypes/<name>.yaml`)" | | Page-type sidecar | yaml `# singleton: true # optional; hides create handles for this type` | added (example line) | "Page types (`editor/pagetypes/<name>.yaml`)" | | Page-type sidecar | yaml `children: [page, section] # page types creatable beneath this one` | dropped | "Page types" (example) | | Page-type sidecar | yaml `# elements: [hero, notice] # element allowlist for pages of this type` | dropped | "Page types" (example); **`elements`** bullet | | Page-type sidecar | yaml `fields: # page-level front-matter fields (same vocabulary)` + `title` field | dropped | "Page types" (example) | | Page-type sidecar | yaml `hidden` field, `tab: access # form tab: general (default) \| appearance \| access`, `default: false` | dropped | "Page types" (example `hidden` field; **`tab`** bullet) | | Page-type sidecar | "`children` and `elements` distinguish three states, preserved to the wire: absent/null = unrestricted" | dropped (`children`) / added (`elements`, new bullet) | "Page types" — **`children`** bullet; new **`elements`** bullet | | Page-type sidecar | same paragraph: "preserved to the wire" | architecture (wire fact, not a site-developer fact) | — | | Page-type sidecar | "A field's optional `tab` picks the properties form tab it sits on" | dropped | "Page types" — **`tab`** bullet | | Page-type sidecar | "`singleton` is optional and purely presentational: the editor hides create-handles" | added | "Page types" — new **`singleton`** bullet | | Page-type sidecar | "The reserved keys `type`, `draft`, `weight`, `aliases`, `backendLayout`," | dropped | "Page types" — **`defaults`** bullet | | Page-type sidecar | "A field of the [`categories` type](#the-categories-field-type) — `name: categories`" | dropped | "Page types" — **`categories`** (field type) bullet | | Page-type sidecar | "**`shownFields`** picks which fields the page's properties form shows" | dropped | "Page types" — **`shownFields`** bullet | | Page-type sidecar | same paragraph: "(the scaffold's `news-item` gets only "Backend layout")" | added (as "the news bundle's `news-item`" — news-item ships in the news bundle, not the scaffold) | "Page types" — **`shownFields`** bullet | | Page-type sidecar | yaml `shownFields: [teaser, backendLayout] # title + teaser + own layout picker` | dropped | "Page types" (example `shownFields` line) | | Page-type sidecar | "Two more page-type keys shape the editor around [editor modules]" | dropped (lead-in) | "Page types" | | Page-type sidecar | "**`hideChildPages: true`** hides a page's **managed** children from the page tree" | dropped (managed, other children stay, no module hides nothing, stays in tree data) | "Page types" — **`hideChildPages`** bullet | | Page-type sidecar | same bullet: "its row still expands when any remain" | added | "Page types" — **`hideChildPages`** bullet | | Page-type sidecar | same bullet: "selecting one selects its nearest drawn ancestor" | added | "Page types" — **`hideChildPages`** bullet | | Page-type sidecar | same bullet: "Hidden pages stay in the `/api/tree` payload (marked `hidden`)" | dropped (the fact) / architecture (the `hidden` mark on `GET /api/tree` → docs/api.md) | "Page types" — **`hideChildPages`** bullet ("stay in the editor's tree data") | | Page-type sidecar | "**`create: true`** on an optional field asks for it in the create dialog" | dropped | "Page types" — **`create`** bullet | | Content-root authority | "There is no reserved declaration file for the content root." (whole paragraph) | added | "Page types" — new **Content-root authority.** paragraph | | Content-root authority | yaml `version: 1` / `singleton: true` / `children: [page, section]` | added | "Page types" — **Content-root authority.** | | Content-root authority | "`harness add` extends this list when a feature bundle asks for it" | dropped | "Page types" — **`children`** bullet; "Feature bundles" | | Content-root authority | "(The earlier reserved `_root.yaml` mechanism has been removed; a leftover" | added | "Page types" — **Content-root authority.** | | Backend layout sidecar | "One file per layout, `editor/backendlayouts/<name>.yaml`. A backend layout is a" | added | new "Backend layouts (`editor/backendlayouts/<name>.yaml`)" | | Backend layout sidecar | yaml (all keys: `version`, `label`, `allowedAt`, `rows`/`columns`, `name`, `label`, `colspan`, `inherit`) | added | new "Backend layouts (`editor/backendlayouts/<name>.yaml`)" | | Backend layout sidecar | "A slot's `elements` allowlist and the layout's `allowedAt` follow the same" | added | new "Backend layouts (`editor/backendlayouts/<name>.yaml`)" | | Backend layout sidecar | "The insert options offered in a slot are the intersection of four" | added | new "Backend layouts (`editor/backendlayouts/<name>.yaml`)" | | Slots, layout resolution, and inheritance | "A page persists a slot's content as a top-level **`slot` wrapper shortcode**" + fenced block | dropped | "Editing content by hand" — **Slot wrappers**; theme README "Backend layouts and slots" (linked from "Backend layouts") | | Slots, layout resolution, and inheritance | "`slot` is reserved and harness-managed: it is never offered in the insert" | dropped | theme README "Backend layouts and slots" (linked from "Backend layouts"); round-trip: "Editing content by hand" intro | | Slots, layout resolution, and inheritance | "Which layout applies to a page — its **effective layout** — resolves" + list 1–4 | dropped | theme README "Resolution" (linked from "Backend layouts"); "none — a flat element list in the editor": "Backend layouts" opening paragraph | | Slots, layout resolution, and inheritance | "A slot that is **empty or absent** on a page inherits the nearest" | dropped (inherit, no opt-out) | theme README "Inheritance (slide)" (linked from "Backend layouts") | | Slots, layout resolution, and inheritance | same paragraph: "The page module shows inherited content greyed out and read-only, with a link" | added | new "Backend layouts (`editor/backendlayouts/<name>.yaml`)" | | Slots, layout resolution, and inheritance | "The split worth internalizing: **the backend layout decides what is editable" | dropped | theme README "Authoring a layout" (linked from "Backend layouts") | | Slots, layout resolution, and inheritance | "Switching a page's layout is **non-destructive**: slot wrappers that the new grid" | added | new "Backend layouts (`editor/backendlayouts/<name>.yaml`)" | | Editor module sidecar | "One file per module, `editor/modules/<name>.yaml`; the file name is the module" | dropped (id + URL segment, strict decoding, read fresh, language file beside it, rail order after built-ins, bundle ships/`add` vendors/`remove` deletes/hand-written) | "Editor modules (`editor/modules/<name>.yaml`)"; "Layout" (`editor/modules/` bullet); "Feature bundles"; "Adding or changing an element" (dotted name = language file) | | Editor module sidecar | same paragraph: "(`internal/sidecardir`: one per file, …)" and "validated against the loaded page types (`internal/editormodule`)" | architecture (package names); the validation fact itself is dropped ("Editor modules" — **Load errors**) | — | | Editor module sidecar | same paragraph: "The editor polls the module list with the page tree, so a" | added | "Editor modules (`editor/modules/<name>.yaml`)" (opening paragraph) | | Editor module sidecar | yaml keys `version`, `label`, `icon`, `pageTypes`, `views`, `columns`, `card` roles, `sort`, `search`, `filters`, `pageSize`, `edit` | dropped | "Editor modules" (identical example) | | Editor module sidecar | yaml `# list \| cards; two or more views give a switcher` | added (comment) | "Editor modules" (example) | | Editor module sidecar | "There is no key for where entries live: the tree selection and the" | dropped | "Editor modules" (container/entry bullets) | | Editor module sidecar | same paragraph: "`GET /api/tree` marks both (`containerFor`, `managedBy`)." | architecture (HTTP wire → docs/api.md `### GET /api/tree`) | — | | Editor module sidecar | "**Fields.** Field names refer to the fields the listed page types declare," | dropped | "Editor modules" — **Fields.** | | Editor module sidecar | table "Place \| Allowed" | dropped | "Editor modules" — **Fields.** (bullet list) | | Editor module sidecar | "`GET /api/modules` hands the editor no field types: each field the module" … "a declared field's label has none." | architecture (HTTP wire → docs/api.md `### GET /api/modules`) | — | | Editor module sidecar | same paragraph: "A select or multiselect filter offers the field's options, a categories" | dropped (filter options, publish state not filterable, categories titles in columns/tags, stale slug hidden, tree order without `sort`, missing sort field last) | "Editor modules" — **Fields.** | | Editor module sidecar | same paragraph: "(`select` and `multiselect`, …) is a load error: they match differently" | added (AGENTS.md said only "cannot be a filter") | "Editor modules" — **Fields.** (`filters` bullet) | | Editor module sidecar | "**Load errors.** Each rejects that one file with a message naming it" + 8 bullets | dropped | "Editor modules" — **Load errors.** | | Editor module sidecar | reserved-id bullet: "deckle-hosted's proxy does" | added | "Editor modules" — **Load errors.** | | Editor module sidecar | "**The tree in a feature module.** The page tree stays as the module's left" | dropped (selection-only, containers selectable, greyed out but expands, URL names container or entry, "Select a News page…") | "Editor modules" — **In the editor.** | | Editor module sidecar | same paragraph: "While an entry is open its container is the selected row. Switching modules keeps the page path." | added | "Editor modules" — **In the editor.** | | Editor module sidecar | "**The module screen** lists the selected container's entries in the declared views." | dropped (views, search/sort/filters/paging in URL query, New button per allowed type with singletons excluded, create dialog fields, opens on first tab) | "Editor modules" — **In the editor.**; container bullets; "Page types" — **`create`** | | Editor module sidecar | same paragraph: "(column headers in a list, a sort menu for cards)", "run on the server", "with the active view, so reload, Back and a shared link keep them" | added | "Editor modules" — **In the editor.** | | Editor module sidecar | same paragraph: "(`GET /api/module/entries`, which serves every datetime as RFC3339)" | architecture (HTTP wire → docs/api.md `### GET /api/module/entries`) | — | | Editor module sidecar | "**The entry editor** has one tab per `edit` item — **Properties** (the page" | dropped (tabs) | "Editor modules" — **In the editor.** | | Editor module sidecar | same paragraph: "with the active tab in the URL" | added | "Editor modules" — **In the editor.** | | Editor module sidecar | same paragraph: "Its toolbar goes back to the list, saves (Save, Save & close)," … "as everywhere." | added (as "The entry editor's toolbar …") | "Editor modules" — new paragraph after **In the editor.** | | Editor module sidecar | "In the Page module, a container offers "Manage in <label>" (news: "Manage in" | dropped (Manage in / Open in) | "Editor modules" — **In the editor.** | | Editor module sidecar | same paragraph: "its back-button fallback and the landing after deleting it go to its container" | added | "Editor modules" — **In the editor.** | | Sidecar language files | "A sidecar's labels are single-valued; their translations live beside it, the" | dropped (beside, four directories, `.yaml`/`.yml`, any tag such as `de`/`pt-BR`, not a content language, dotted name = language file, text only keyed by name) | "Sidecar language files"; "Adding or changing an element" (names hold no dot) | | Sidecar language files | same paragraph: "any tag `pagepath.ValidLangTag` accepts" and "`internal/sidecardir` alone recognises the name (split at the first dot …) … every other walker (`harness add`, its receipts, `harness sidecar`) asks it." | architecture (internal owner of the name rule) | — | | Sidecar language files | table "Family \| Keys" (element, page type, backend layout, editor module) | dropped | "Element sidecar schema" — **Language file**; "Page types" — **Language file**; "Editor modules" — **Language file.**; backend layout: theme README "Authoring a layout" (pointed at by "Sidecar language files") | | Sidecar language files | "Every entry is optional, and an empty value counts as absent: the" | dropped (optional/empty, load errors and where reported, no `version`, failed sidecar skips its files, changes nothing but labels) | "Sidecar language files" (**Lenient**, **Strict**, **Text only** bullets) | | Sidecar language files | same paragraph: "The scaffold ships a `.de.yaml` beside every sidecar under `scaffold/editor/` and under both bundles' `editor/` trees" | dropped | "Sidecar language files" — **Shipped.**; "Layout" (language file bullet) | | Sidecar language files | same paragraph: "in the register and terms of `web/src/i18n/glossary.md`; a scaffold test holds each complete (every text key of its source translated). `select` option values, `feature.yaml` manifests and the starter home page are not translated." | architecture (deckle repository's translation policy); `select` values untranslated is also stated in "Element sidecar schema" — **Language file** | — | </details> ## Guards Each guard fails with a message naming the owning document. These were captured by breaking each invariant once: 1. **Routes** (`internal/server/routes_doc_test.go`). `route()` now dispatches through an `apiRoutes` table: the same 66 paths and handlers, unknown `/api/` paths still 404. That table is the one owner of the route set. `TestAPIDocNamesEveryRoute` holds it equal to the `###` route headings in docs/api.md and rejects `/api/host` (reserved for the host). `TestMutatingRoutesAreRegistered` checks every `mutatingRoutes` key against the table. ``` docs/api.md has heading "GET /api/frobnicate", which is not a registered route: remove it, or register the route in apiRoutes ``` 2. **Commands** (`cmd/harness/docs_test.go`). A `commands` table drives `run()` and the `commands:` part of `usage()`; the printed usage text is byte-identical to before. `lang` gets a `langSubcommands` table. docs/harness.md must carry the usage text verbatim and a bullet for every command, including `version`. The README's command table must have one row per command, each with its usage summary. ``` docs/harness.md does not document command "version": the CLI reference is docs/harness.md (see docs/agents/docs-map.md); add its bullet there (- **`version`** …) in the section that holds the usage text ``` 3. **README shape** (`docs/docs_test.go`). At most 300 lines; no harness `/api/` path (a forge URL like `git.cloonar.com/api/v1/…` is allowed); no json/yaml block; the Documentation section links every owning doc. ``` README.md names an HTTP API path on 1 lines (line 245): routes and wire shapes belong in docs/api.md under a `### METHOD /api/path` heading — the README links to it and names no route ``` 4. **Links** (`docs/docs_test.go`). Every relative link and `#anchor` in README.md and `docs/**/*.md` must resolve. ``` docs/features.md:175: link (harness.md#the-recipt): heading #the-recipt does not exist in docs/harness.md — headings are matched by their GitHub anchor, so a renamed or moved heading breaks its links ``` The heading and anchor and link rules live once, in the new `internal/mddoc`. `scaffold/skills_test.go`'s private `headings()` was removed in favour of it. ## Deviations and things to check - **`.claude/skills/` is not tracked in this repo.** The lab's `info/exclude` excludes it, and it is a vendored skill bundle. So the requested edits to `update-docs/SKILL.md` and `triage/AGENT-BRIEF.md` cannot ship from this branch. The two rules live in `docs/agents/docs-map.md` instead, which `update-docs` step 3 already treats as overriding its generic steps. Proposed text for the vendored skills: - update-docs, Standing rules: *"**Condense, don't inflate.** A sync may remove or condense prose that the owning document (per the docs map) now carries, replacing it with a link. A doc that has grown past its scope is drift to report and fix, not surrounding depth to match."* - triage AGENT-BRIEF.md, Principles: *"### Name the owning document — When the change adds or alters a documented fact, the brief names the document that owns it, taken from the repo's docs map (`docs/agents/docs-map.md`) — never 'document it in the README'."* - grill-with-docs / ADR-FORMAT already name `docs/adr/`, and that path now exists. Nothing to change there. - **"What the editor accepts" stays in host-navigation.md.** It describes how the editor validates `/api/host` data, so it is part of the versioned contract. The issue's table listed it for api.md but also said all of Host navigation's children move verbatim. - **The route count is 66, not 67.** The issue's 67 likely included `/api/host`, which is reserved and not a route. - **`scaffold/skills_test.go` keeps its hand-written `harnessCommands` map.** It is an existing second copy of the command set that the `scaffold` package cannot import from `package main`. Worth a follow-up. - **Stale prose noticed but not fixed (pure move; for #265):** - The README said an empty slot always inherits; the code only inherits for slots marked `inherit`. AGENTS.md did not import that sentence. - The README's `allow` token set omitted `table`. - features.md "Installing" says Page-type allowances is in "the next section", but the next section is Bundles from a directory. - The scaffolded site skills reference no root-README section, and `go test ./scaffold/` confirms they still resolve. They are untouched. - `web/src/api/*.ts`, `internal/server` and `internal/removecmd` comments that cited README sections now cite their new document. ## Verification - `go test ./...` green; `go vet ./...` and gofmt clean. - `wc -l README.md` gives 243; `grep -cE '(^|[^[:alnum:]_])/api/' README.md` gives 0. - `harness init` into a scratch directory, then `harness check`: `ok: no problems found`, exit 0. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The 3,182-line README becomes a 243-line orientation page; its content
moves, unreworded, to the document that owns each kind of fact:
docs/api.md (one ### heading per registered route, 20 stubs pointing at
#265), docs/harness.md, docs/host-navigation.md (verbatim), docs/features.md
and docs/architecture.md. The Configuration reference's sidecar sections
fold into scaffold/site/AGENTS.md: facts it lacked are added there, the
rest dropped in favour of its existing sections.

Guards, each failing with a message that names the owning document:
- internal/server: route() dispatches through an apiRoutes table, and
  TestAPIDocNamesEveryRoute holds it equal to docs/api.md's route headings
  (plus every mutatingRoutes path must be registered);
- cmd/harness: a commands table drives run() and usage(); docs/harness.md
  must carry the usage text verbatim and a bullet per command, and the
  README's command table one row per command;
- docs: the README's 300-line budget, no /api/ path or json/yaml block,
  the Documentation links, and every link and anchor in README.md and
  docs/ resolves;
- internal/mddoc owns headings, GitHub anchors and links for all of them
  (scaffold/skills_test.go now uses it).

CLAUDE.md gains "Every fact has one document"; docs/agents/docs-map.md
and docs/adr/ are new.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
docs: record the docs sync baseline
All checks were successful
ci / build-and-test (pull_request) Successful in 3m5s
ci / release (pull_request) Has been skipped
d71da57377
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Author
Owner

[autoland] verdict: pass

Lander verdict: CONCERNS (non-blocking). Approved, not merged. Autoland only merges a clean PASS.

Signal relied on: labctl pr checks 266 gave success (ci / build-and-test: npm build and vitest, go vet ./..., go test ./..., build). The new drift tests in docs/, internal/server and cmd/harness ran there. I didn't re-run them.

Verified locally:

  • apiRoutes (internal/server/server.go:244) has the same 66 path→handler pairs as main's route() switch, and unknown /api/ paths still return 404.
  • harness with no args, an unknown command, lang, and lang zz give byte-identical stderr and exit 2 on main and on this head.
  • Content coverage: I split main's README into 485 units and searched the new README, docs/, AGENTS.md and the theme README for each one. Outside "Configuration reference", every unit is found verbatim (ignoring link targets), except the edits listed under "Text changed in the move". I spot-checked a sample of the "dropped" rows in the sidecar disposition table against scaffold/site/AGENTS.md, and AGENTS.md states those facts.
  • .claude/ is excluded by info/exclude and is not tracked, so the skill edits really can't ship from this branch.
  • No merge conflict: the base is origin/main (d7bc3dd).

Concerns:

  1. The stubs point at a duplicate follow-up issue. Issue #263's acceptance criterion says the stubs point at #264. #264 is the maintainer's ready-for-agent follow-up. This PR filed #265 (needs-triage, created 1.5 minutes after #264, "referred to there as #ISSUE2") and points everything at it:

    • 20 Documented in #265. stubs in docs/api.md, e.g. lines 291, 542 and 614.
    • The failure message and fixture in internal/server/routes_doc_test.go:63 and :108.

    Two open issues now cover the same work. Either retarget the stubs and the test to #264 and close #265 as a duplicate, or decide which issue survives.

  2. A second heading and fence parser in the test code. cmd/harness/docs_test.go:46-80 (markdownHeading plus sectionAround) re-implements "what is a heading / what is a fenced block" outside internal/mddoc. It only handles ``` fences: no ~~~, and no check on fence length. The PR says "the heading and anchor and link rules live once, in the new internal/mddoc", and CLAUDE.md's "Every rule has exactly one owner" forbids adding a sibling. The fix is to give mddoc.Heading an offset or line and use it here.

  3. Unmet acceptance criterion, needs a human. .claude/skills/update-docs/SKILL.md (the condense/remove rule) and the triage AGENT-BRIEF (the docs-map line) are vendored and untracked, so the PR couldn't change them. The rules live in docs/agents/docs-map.md instead. The proposed skill text in the PR body still has to go to the vendored skill bundle.

Non-blocking note, out of scope per the issue: the scaffolded add-backend-layout/SKILL.md still points at AGENTS.md "Layout" for the schema. That still resolves through the bullet's new pointer, but it could name the new AGENTS.md "Backend layouts" section directly.

[autoland] verdict: pass **Lander verdict: CONCERNS (non-blocking). Approved, not merged.** Autoland only merges a clean PASS. **Signal relied on:** `labctl pr checks 266` gave `success` (`ci / build-and-test`: npm build and vitest, `go vet ./...`, `go test ./...`, build). The new drift tests in `docs/`, `internal/server` and `cmd/harness` ran there. I didn't re-run them. **Verified locally:** - `apiRoutes` (`internal/server/server.go:244`) has the same 66 path→handler pairs as main's `route()` switch, and unknown `/api/` paths still return 404. - `harness` with no args, an unknown command, `lang`, and `lang zz` give byte-identical stderr and exit 2 on main and on this head. - Content coverage: I split main's README into 485 units and searched the new README, `docs/`, AGENTS.md and the theme README for each one. Outside "Configuration reference", every unit is found verbatim (ignoring link targets), except the edits listed under "Text changed in the move". I spot-checked a sample of the "dropped" rows in the sidecar disposition table against `scaffold/site/AGENTS.md`, and AGENTS.md states those facts. - `.claude/` is excluded by `info/exclude` and is not tracked, so the skill edits really can't ship from this branch. - No merge conflict: the base is `origin/main` (d7bc3dd). **Concerns:** 1. **The stubs point at a duplicate follow-up issue.** Issue #263's acceptance criterion says the stubs point at **#264**. #264 is the maintainer's ready-for-agent follow-up. This PR filed **#265** (needs-triage, created 1.5 minutes after #264, "referred to there as #ISSUE2") and points everything at it: - 20 `Documented in #265.` stubs in `docs/api.md`, e.g. lines 291, 542 and 614. - The failure message and fixture in `internal/server/routes_doc_test.go:63` and `:108`. Two open issues now cover the same work. Either retarget the stubs and the test to #264 and close #265 as a duplicate, or decide which issue survives. 2. **A second heading and fence parser in the test code.** `cmd/harness/docs_test.go:46-80` (`markdownHeading` plus `sectionAround`) re-implements "what is a heading / what is a fenced block" outside `internal/mddoc`. It only handles ```` ``` ```` fences: no `~~~`, and no check on fence length. The PR says "the heading and anchor and link rules live once, in the new `internal/mddoc`", and CLAUDE.md's "Every rule has exactly one owner" forbids adding a sibling. The fix is to give `mddoc.Heading` an offset or line and use it here. 3. **Unmet acceptance criterion, needs a human.** `.claude/skills/update-docs/SKILL.md` (the condense/remove rule) and the triage AGENT-BRIEF (the docs-map line) are vendored and untracked, so the PR couldn't change them. The rules live in `docs/agents/docs-map.md` instead. The proposed skill text in the PR body still has to go to the vendored skill bundle. **Non-blocking note, out of scope per the issue:** the scaffolded `add-backend-layout/SKILL.md` still points at AGENTS.md "Layout" for the schema. That still resolves through the bullet's new pointer, but it could name the new AGENTS.md "Backend layouts" section directly.
The README paragraphs main added since the branch point (switch hint
codes, errors-at-rest shape, the Enabled switch's coded hints, the
upload gate's refusal codes, the baseline snapshot and undoOp, branch
mode's first undo) move verbatim into the document that owns each:
docs/api.md for the wire shapes, docs/architecture.md for the git
backbone and media gate narrative, the README for its v1 limitation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
docs: record the docs sync baseline
Some checks failed
ci / release (pull_request) Has been cancelled
ci / build-and-test (pull_request) Has been cancelled
f91bec45fe
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
dominik.polakovics force-pushed afk/263 from f91bec45fe
Some checks failed
ci / release (pull_request) Has been cancelled
ci / build-and-test (pull_request) Has been cancelled
to 3efc24eeea
All checks were successful
ci / build-and-test (pull_request) Successful in 3m15s
ci / release (pull_request) Has been skipped
2026-10-13 19:40:49 +02:00
Compare
Author
Owner

x

x
Author
Owner

Addressed the lander's concerns (head is now 3efc24e):

  1. Stubs retargeted to #264. All 20 Documented in #264. stubs in docs/api.md, plus the hint and fixture in internal/server/routes_doc_test.go, now name #264. #265 is closed as a duplicate; nothing references it any more.
  2. One heading/fence parser. cmd/harness/docs_test.go no longer has markdownHeading/sectionAround. mddoc.Heading gained Offset (byte offset of the heading line) and internal/mddoc gained Section(doc, at), the one owner of "which section holds this offset", with its own test (tilde fences, nested fences, a fenced # that is not a heading). The harness test calls mddoc.Section.
  3. Vendored skill edits (update-docs/SKILL.md, triage AGENT-BRIEF.md) still need a human: .claude/skills/ is untracked here. The proposed text is in the PR body under "Deviations".

Also: the non-blocking note — add-backend-layout/SKILL.md now points at AGENTS.md "Backend layouts" directly.

Merged origin/main (the branch was behind by #268). Main's four README edits since the branch point moved verbatim into the owning documents: the switch onCode/offCode settings, the errors-at-rest list shape and the Enabled switch's coded hints into docs/api.md (Server-described forms, Coded messages); the upload gate's refusal codes under ### POST /api/media/upload in docs/api.md with a link from the Media gate paragraph; the baseline snapshot, snapshotOps and undoOp into docs/architecture.md "Git backbone" (with an undoOp pointer under ### GET /api/git/state); branch mode's first-undo paragraph into "Branch mode"; the Single-step undo limitation stays in the README. docs/.docs-sync is bumped to the fix commit.

Verified: go vet ./..., go test ./... green (including the four drift tests and the new mddoc tests), vitest 3210 passed, gofmt clean.

Addressed the lander's concerns (head is now `3efc24e`): 1. **Stubs retargeted to #264.** All 20 `Documented in #264.` stubs in `docs/api.md`, plus the hint and fixture in `internal/server/routes_doc_test.go`, now name #264. #265 is closed as a duplicate; nothing references it any more. 2. **One heading/fence parser.** `cmd/harness/docs_test.go` no longer has `markdownHeading`/`sectionAround`. `mddoc.Heading` gained `Offset` (byte offset of the heading line) and `internal/mddoc` gained `Section(doc, at)`, the one owner of "which section holds this offset", with its own test (tilde fences, nested fences, a fenced `#` that is not a heading). The harness test calls `mddoc.Section`. 3. **Vendored skill edits** (`update-docs/SKILL.md`, triage `AGENT-BRIEF.md`) still need a human: `.claude/skills/` is untracked here. The proposed text is in the PR body under "Deviations". Also: the non-blocking note — `add-backend-layout/SKILL.md` now points at AGENTS.md "Backend layouts" directly. **Merged `origin/main`** (the branch was behind by #268). Main's four README edits since the branch point moved verbatim into the owning documents: the `switch` `onCode`/`offCode` settings, the `errors`-at-rest list shape and the Enabled switch's coded hints into `docs/api.md` (Server-described forms, Coded messages); the upload gate's refusal codes under `### POST /api/media/upload` in `docs/api.md` with a link from the Media gate paragraph; the baseline snapshot, `snapshotOps` and `undoOp` into `docs/architecture.md` "Git backbone" (with an `undoOp` pointer under `### GET /api/git/state`); branch mode's first-undo paragraph into "Branch mode"; the Single-step undo limitation stays in the README. `docs/.docs-sync` is bumped to the fix commit. Verified: `go vet ./...`, `go test ./...` green (including the four drift tests and the new `mddoc` tests), vitest 3210 passed, gofmt clean.
Author
Owner

[autoland] verdict: fix-done

[autoland] verdict: fix-done
Author
Owner

[autoland] verdict: pass

Lander verdict: CONCERNS (non-blocking). Approved, not merged. Autoland only merges a clean PASS.

Signal relied on: labctl pr checks 266 --wait gave success on head 3efc24e: ci / build-and-test (npm build and vitest, go vet ./..., go test ./..., build) passed, and ci / release was skipped. That covers the four drift tests, the new mddoc Section tests, scaffold/skills_test.go and internal/element/agentsdoc_test.go. I didn't re-run them.

Verified locally on this head:

  • The previous concerns are fixed. No file references #265 any more. docs/api.md has 19 Documented in #264. stubs, plus the POST /api/media/upload section, whose remaining request/response gap points at #264. routes_doc_test.go:63 and :108 name #264. #265 is closed as a duplicate.
  • One heading parser. markdownHeading and sectionAround are gone from cmd/harness/docs_test.go, which now calls mddoc.Section (internal/mddoc/mddoc.go:112). That function is built on Headings, so it uses the same fence rules (tilde fences, fence length). TestSection covers a fenced #.
  • add-backend-layout/SKILL.md now names AGENTS.md "Backend layouts" directly.
  • The route table is unchanged. apiRoutes (internal/server/server.go:246) holds the same 66 paths as origin/main's route() switch, and unknown /api/ paths still 404.
  • The merge of origin/main (#268) kept main's text. Each of main's README edits since d7bc3dd is in its owning document, word for word except for added links:
    • switch onCode/offCode, the at-rest errors list and the Enabled hint codes: docs/api.md:26, :473, :488, :129.
    • The upload refusal codes: docs/api.md:754-762, linked from Media gate at docs/architecture.md:129.
    • The baseline snapshot, snapshotOps and undoOp: docs/architecture.md:146-166, with a pointer at docs/api.md:651.
    • Branch mode's first undo: docs/architecture.md:254.
    • The Single-step undo limitation: still in the README (README.md:208).
  • README is 245 lines with no /api/ path. docs/.docs-sync = c33d375 (the last content commit) and 2026-10-13.

Concerns:

  1. An acceptance criterion of #263 is still unmet and needs a human. The criterion: ".claude/skills/update-docs/SKILL.md carries the condense/remove rule; the triage brief guide names the docs map". .claude/ is excluded by info/exclude and is not tracked on main (git ls-tree origin/main .claude is empty), so no PR in this repo can satisfy it. Merging auto-closes #263, and the follow-up would then exist only in this PR's "Deviations" section. Before or right after merging, either apply the proposed text to the vendored skill bundle, or waive the criterion on #263.

Non-blocking note: the PR description still says the follow-up is "filed as #265" and that the stubs read Documented in #265.. Both changed in c33d375. Worth correcting before the merge, so the merge commit's message doesn't point readers at the closed duplicate.

[autoland] verdict: pass **Lander verdict: CONCERNS (non-blocking). Approved, not merged.** Autoland only merges a clean PASS. **Signal relied on:** `labctl pr checks 266 --wait` gave `success` on head `3efc24e`: `ci / build-and-test` (npm build and vitest, `go vet ./...`, `go test ./...`, build) passed, and `ci / release` was skipped. That covers the four drift tests, the new `mddoc` `Section` tests, `scaffold/skills_test.go` and `internal/element/agentsdoc_test.go`. I didn't re-run them. **Verified locally on this head:** - **The previous concerns are fixed.** No file references `#265` any more. `docs/api.md` has 19 `Documented in #264.` stubs, plus the `POST /api/media/upload` section, whose remaining request/response gap points at #264. `routes_doc_test.go:63` and `:108` name #264. #265 is closed as a duplicate. - **One heading parser.** `markdownHeading` and `sectionAround` are gone from `cmd/harness/docs_test.go`, which now calls `mddoc.Section` (`internal/mddoc/mddoc.go:112`). That function is built on `Headings`, so it uses the same fence rules (tilde fences, fence length). `TestSection` covers a fenced `#`. - `add-backend-layout/SKILL.md` now names AGENTS.md "Backend layouts" directly. - **The route table is unchanged.** `apiRoutes` (`internal/server/server.go:246`) holds the same 66 paths as `origin/main`'s `route()` switch, and unknown `/api/` paths still 404. - **The merge of `origin/main` (#268) kept main's text.** Each of main's README edits since `d7bc3dd` is in its owning document, word for word except for added links: - `switch` `onCode`/`offCode`, the at-rest `errors` list and the Enabled hint codes: `docs/api.md:26`, `:473`, `:488`, `:129`. - The upload refusal codes: `docs/api.md:754-762`, linked from Media gate at `docs/architecture.md:129`. - The baseline snapshot, `snapshotOps` and `undoOp`: `docs/architecture.md:146-166`, with a pointer at `docs/api.md:651`. - Branch mode's first undo: `docs/architecture.md:254`. - The Single-step undo limitation: still in the README (`README.md:208`). - README is 245 lines with no `/api/` path. `docs/.docs-sync` = `c33d375` (the last content commit) and 2026-10-13. **Concerns:** 1. **An acceptance criterion of #263 is still unmet and needs a human.** The criterion: "`.claude/skills/update-docs/SKILL.md` carries the condense/remove rule; the triage brief guide names the docs map". `.claude/` is excluded by `info/exclude` and is not tracked on `main` (`git ls-tree origin/main .claude` is empty), so no PR in this repo can satisfy it. Merging auto-closes #263, and the follow-up would then exist only in this PR's "Deviations" section. Before or right after merging, either apply the proposed text to the vendored skill bundle, or waive the criterion on #263. **Non-blocking note:** the PR description still says the follow-up is "filed as **#265**" and that the stubs read `Documented in #265.`. Both changed in `c33d375`. Worth correcting before the merge, so the merge commit's message doesn't point readers at the closed duplicate.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/deckle!266
No description provided.