docs: complete and verify the split documents — every route, CLI, features, architecture and 22 ADRs #274

Merged
dominik.polakovics merged 6 commits from afk/264 into main 2026-10-13 22:02:28 +02:00

Summary

The content pass #263 left for later: the documents under docs/ are now true against the code and complete.

  • docs/api.md: every one of the 66 registered routes has exactly one section in one shape: purpose, Request, Response, Errors (codes from internal/apimsg), Languages, Writes. The 24 stubs are written. Behaviour that was split across feature paragraphs ("Translations on the wire", "Server-described forms" and others) is merged into each route's own section. Shared shapes are stated once under Conventions and linked: notation, request/response rules, errors and coded messages, page addressing, content trees, page answers, forms, sidecar labels, translations and writes. Every key was read from the handler and the Go types it marshals. An adversarial review then re-checked 40 claims against the code.
  • docs/harness.md: the usage block is checked against the binary's output. Every command's flags, exit codes and --json shape are checked against cmd/harness. A new ## Check section lists the rules internal/checkcmd applies. The "ten commands" wording is gone.
  • docs/features.md: checked against internal/addcmd, internal/removecmd, internal/initcmd and the shipped bundles. requires is now stated once. It went from 585 to 470 lines.
  • docs/architecture.md: rewritten as a narrative in reading order, 459 → 198 lines (≤ half), with no wire shapes. Each decision is one sentence plus a link to its ADR.
  • docs/adr/: 22 records. Each one is dated to its originating commit, which was confirmed with git log -S/--grep. Together they cover every decision #264 names: rich text (0006), media gate (0004), publish job (0018), branch mode (0011), branch bundles (0008), publish anyway (0014), host-navigation safety (0010), requires (0021, 78e181e) and no aliases (0017, e4d8138). The others carry the rationale the docs and commits already stated.
  • docs/host-navigation.md: two editorial link fixes only. The v1 shape was compared with deckle-hosted's producer (server/hostnav.go) and the editor's consumer, and they match.
  • scaffold/site/AGENTS.md: no sidecar fact proved wrong against the engine, so it is unchanged.
  • docs/.docs-sync is updated. No code changed since the baseline c33d375.

Verification

  • go test ./... passes, after building web/dist. That includes docs/docs_test.go, the route test in internal/server, the command test in cmd/harness, scaffold/skills_test.go and internal/element/agentsdoc_test.go. go vet ./... is clean.
  • The output of go run ./cmd/harness matches the usage block in docs/harness.md exactly. Every harness <cmd> -h was run.
  • grep -rn 'ten commands' docs README.md returns nothing. grep -c 'Documented in #264' docs/api.md returns 0.
  • wc -l docs/architecture.md is 198, against 459 after #263.

Sampled routes: marshalled type vs documented keys

Route Go type (json tags) Documented
GET /api/page pageResponse (page, type, tree, layout, translation,omitempty); translationInfo (mode, source, changed, untranslated, synced); missingTranslationResponse (page, missing, type, title, file, source) page answer {page, type, tree, layout: PageLayout | null, translation?}; Translation (all five); missing answer (all six)
POST /api/page/save savePageRequest (page, lang, path, name, values, body *string, items *[]map, children *savePageChildren{order:[{uid, enabled *bool}], removed}) → pageResponse {page, lang?, path, name, values, body?, items?, children?: {order: {uid, enabled?}[], removed}} → page answer
GET /api/tree pagetree.Tree (lang, root); pagetree.Node (path, permalink, address, lang, translation?, l10n?, kind, type, title, titleCode?, weight, date?, childOrder?, hideChildPages?, containerFor?, managedBy?, hidden?, problem?, missing?, dir?, children) + enabled from MarshalJSON {lang, root: TreeNode} with every key, ? exactly on the omitempty ones
POST /api/page/delete request {page, lang}; deleteResponse (deleted, translations,omitempty, changed) {page, lang?} → {deleted, translations?, changed}
POST /api/form request {form, params}; formDescription (fields, type?, items?, children?, synced?, errors?); element.FormField (16 tags); itemsDescription (element, label, i18n?, itemLabel, itemI18n?, summary?, fields, rows); itemRow (uid, enabled, fields) same keys and optionality
GET /api/elements elementsResponse (elements, errors); element.Schema (16 tags); element.Param (11 tags, LabelCode -); sidecardir.LoadError (file, error) same; labelCode correctly absent on params
POST /api/element/insert elementInsertRequest (page, lang, slot, parent, index *int, name) → insertResponse (pageResponse + path, uid) {page, lang?, slot?, parent?, index, name} → page answer + {path, uid}
POST /api/element/transfer elementTransferRequest (lang, fromPage, from, toPage, toSlot, toParent, toIndex *int, mode) → transferResponse (source pageResponse, target insertResponse) {lang?, fromPage, from, toPage, toSlot?, toParent?, toIndex, mode} → {source: PageAnswer, target: PageAnswer & {path, uid}}
GET /api/element/body query page, lang, path, name → elementBodyResponse (body) {body: string}
GET /api/element/move-refs query uid, toPage, page, lang, toParentUid → moveRefsResponse (pages, media, translations,omitempty *transferPlan{moved, dropped}) {pages, media, translations?: {moved, dropped}}
POST /api/page/translate translatePageRequest (page, lang, mode, subpages) → translatePageResponse (pageResponse + created, skipped,omitempty) {page, lang, mode?, subpages?} → page answer + {created, skipped?}
GET /api/git/state gitStateResponse (state, reason, reasonCode?, reasonArgs?, lastSnapshotError, undoAvailable, mode, branch, tip, unsynced, unsyncedReason, undoOp?, undoPage?, undoLang?, undoTime?, undoPages?) all 16, same optionality
POST /api/page/undo request {page, lang} → pageStateResponse (page, removed, type,omitempty, tree,omitempty, also,omitempty) {page, lang?} → {page, removed, type?, tree?, also?}
POST /api/publish-all request {checkDraftLinks}; publishJobState (202: job, phase); publishEmptyResponse (empty, pushed, message, code); disabledLinkWarning/disabledLink/linkingPage (disabledLinks; page, title, linkedFrom; page, lang?, title); publishJobRunningResponse (error, code, args, job) same
GET /api/publish/job publishJobState (job, phase, commit?, pushed?, feedback?, message?, messageCode?, status?, error?, code?, args?, retryable?, hugoOutput?) split per phase, every key
GET /api/publish/pending pendingResponse (changes, unavailable?, unavailableCode?, unavailableArgs?); pendingChange (kind, page?, title?, titleCode?, path?, status, elements?, properties?, resources?); elementCounts (added, changed, removed) same
POST /api/media/upload multipart parts file, target, folder → mediaUploadResponse (ref, name, width, height) file, target: "library", folder? → {ref, name, width, height}
GET /api/media/library query folder → mediaLibraryResponse (folders, items); mediaItem (ref, name, size, type) same
POST /api/media/folder/delete request {folder} → mediaDeletePlanResponse (delete, keep); mediaKeepEntry (ref, pages) same

Corrected claims (was / now / verified in)

docs/api.md — conventions
  • was: linkPicker value {href, newTab} / now: {href, newTab, title} / verified in: element.LinkValue, internal/element/ftype_link.go:57
  • was: 422 field keys are bare names, items[<i>].<name> and body / now: children too / verified in: childrenField, handleSavePage (server.go)
  • was: "every other failure keeps the plain body with 400/404/409/500" / now: a plain body can also be 422 (unparseable page, serializer refusal, placement violation); the two 422 shapes differ by errors / verified in: pageAndTree, serializePage, element.go:96-145
  • was: translation on GET /api/page, page/save and element/enabled / now: on every page answer in a non-default language / verified in: pageAnswer (translation.go:275) and callers
  • was: sidecar load errors without a code in elements, backendlayouts, modules / now: pagetypes too; all four {file, error} / verified in: sidecardir.LoadError (sidecardir.go:179)
  • was: create form not listed as default-language only / now: POST /api/form kind create answers 400 editor.createFormDefaultOnly / verified in: forms.go:308
  • was: type fallback is the "section-name" / now: the top-level section's name / verified in: pageops.EffectiveType (pagetypes.go:105)
  • was: (unstated) / now: wrong method is 405 request.method on every route; unknown /api/ path a plain-text 404 / verified in: requirePost (pageops.go:100), route (server.go)
  • was: (unstated) / now: empty/undecodable POST body is 400 request.malformed; unknown keys ignored / verified in: decodeBody (pageops.go:91)
  • was: (unstated) / now: mutation lock covers every POST except form, form/check, form/inline, publish-all, plus GET page and page/elements / verified in: mutatingRoutes (mutation.go)
  • was: "The endpoints (all POST, JSON, none takes the mutation lock):" followed by nothing / now: removed; #writes names them / verified in: mutatingRoutes
  • was: X-Deckle-Draft-Tip on every branch-mode response / now: only while the branch has a commit / verified in: draftTipWriter (mutation.go)
docs/api.md — site and forms
  • was: GET /api/status "Documented in #264" / now: {hugo: {state, previewUrl, restarts, lastExit, lastBuildError, output, version, source}} / verified in: statusResponse/hugoStatus (server.go:290-326)
  • was: GET /api/site only a link / now: {title, languages: {code,label}[], acceptLanguage} with parse rules / verified in: siteResponse, acceptLanguageTags (server.go:353-461)
  • was: elements/pagetypes/backendlayouts/modules only a link to Sidecar labels / now: every marshalled key / verified in: element.Schema/Param/Items/BodyPolicy.MarshalJSON, pagetype.Schema, backendlayout.Schema, editormodule.Module/FieldInfo/Filter, sidecardir.LoadError
  • was: module field {name, label, labelCode?, widget, sortable} / now: also i18n? / verified in: editormodule.FieldInfo.I18n
  • was: module filter {field, label, labelCode?, options} / now: also i18n? / verified in: editormodule.Filter.I18n
  • was: /api/form → {fields, type?, items?, children?, errors?} / now: adds synced? / verified in: formDescription (forms.go:80-122)
  • was: Enabled field {name, widget, settings, group, tab, value} / now: also label, labelCode: "label.enabled", required: false / verified in: element.EnabledField (element/form.go:124-136)
  • was: a category's form has no Enabled field (unqualified) / now: in a non-default language every translation, a category's and the root's included, has its own / verified in: pageops/translation_settings.go:222-231
  • was: /api/form/check {form, field, value} / now: also params? / verified in: forms.go:792-797
  • was: form/check "a field the form does not have is a 400" / now: 400 field.unknown {field}, 400 form.noItemRows {field} / verified in: forms.go:405-423
  • was: /api/form/inline link stored as href plus <name>Target / now: <name>, <name>Target, <name>Title / verified in: elemops/save.go:184-208 MergeParams
  • was: /api/form/inline "or a 422 while a value is invalid" / now: 422 field errors incl. field.unknownParam, plus 400 form.notInlineKind, editor.noSchema, form.notInline, form.inlineParamType / verified in: forms.go:834-875
docs/api.md — pages
  • was: "Hidden pages (hideChildPages) stay in the payload, marked hidden" / now: hidden only on a module-managed entry whose parent's type sets hideChildPages / verified in: pagetree/pagetree.go:185-207, annotate.go:30
  • was: tree node keys listed partially / now: {lang, root: TreeNode} with every Node key (path, kind, type, titleCode?, weight, date?, childOrder?, hideChildPages?, problem?, missing?, dir?, children, enabled …) / verified in: pagetree.Tree, pagetree.Node + MarshalJSON (pagetree.go:91-243)
  • was: "every page node carries lang" / now: every node, synthesized ones included / verified in: pagetree/build.go:427
  • was: the tree's address of a page with url is the url itself / now: only a root-relative url is verbatim; a relative one lands under the language prefix / verified in: pagetree/build.go:497-503
  • was: GET /api/page and /api/page/elements "See …" stubs / now: page or missing answer; elements {page, elements: {uid, name, label, i18n?, summary}[]}; opening snapshots repair translation or stamp uids / verified in: server.go:835-866, linktargets.go:48-136
  • was: a slot child moved or toggled answers "422" / now: plain 422 element.slotMove / element.slotToggle {name}, not a field error / verified in: elemops/children.go:117-122
  • was: connected-translation items refusal = row added or item left out / now: a uid sent twice too / verified in: translation.go:445-480
  • was: page/save propagation "holds each translation to the source's structure" / now: synced-param copy always; structural repair only when every source element has a sound uid / verified in: translation.go:500-520
  • was: (unstated) / now: page/save values is the whole form (a declared param left out is removed); item rows keep their echoed uid; unknown params of a stored row dropped / verified in: elemops/save.go:155-161,236-257
  • was: (unstated) / now: page/save codes 409 element.pathEmpty|pathRange|pathNotBlock|pathNotShortcode|pathName, 400 editor.noSchema, 400 item.notDeclared|item.noSchema, 409 editor.bodyNestedBlocks / verified in: forms.go:473-511, server.go:965-1080
  • was: 422 on slug when a sibling's translation has that address / now: translation.slugTaken for any sibling holding the address in that language (incl. untranslated siblings' would-be addresses, grouping dirs) / verified in: pageops/translation_settings.go translationAddressTaken
  • was: settings/save rules partial / now: type key → 400 page.typeImmutable; absent slug keeps, "" deletes; unchanged enabled writes nothing / verified in: pageops/settings.go:249-286
  • was: create body {parent, lang, type, title, …} / now: parent and type optional; blank title on quick create is 400 page.titleEmpty / verified in: pageops/create.go:143-195
  • was: create-root, reorder, enabled stubs / now: documented incl. 409 page.rootExists, reorder index required (0..count−1), enabled required, page.rootDisable only for false / verified in: server/pageops.go:207-415, pageops/reorder.go:45
  • was: POST /api/page/delete answers {deleted, changed} / now: {deleted, translations?, changed} / verified in: deleteResponse (server/pageops.go:450-454)
docs/api.md — elements and links
  • was: element POST routes were stubs ("See Translations on the wire") / now: full sections / verified in: internal/server/element.go:49-448, element_enabled.go, element_transfer.go
  • was: GET /api/element/body, /api/page/refs, /api/element/refs "Documented in #264" / now: written from code / verified in: body.go:42-120 (elementBodyResponse), linktargets.go:343-453
  • was: transfer on a connected translation refused for the receiving page and, in a move, the giving page / now: also a same-page copy / verified in: element_transfer.go:200-205
  • was: "a move or delete is mirrored" / now: a move into a container a connected translation has not translated drops the copy there / verified in: l10n/structure.go:295-298, translation_ops.go:182-195
  • was: a cut is dropped "where the target has none or only a free one" / now: also when the target translation cannot be read or lacks the destination parent / verified in: translation_ops.go:259-279 (receiveTranslated)
  • was: transfer snapshot unstated / now: cross-page move one transfer snapshot for both pages; same-page or copy save / verified in: element_transfer.go:291-327
  • was: element/enabled snapshot unstated / now: save, none when nothing changes / verified in: element_enabled.go:84-90
  • was: delete-plan "400 without uid, 404 unknown page or language, 409 no uid" / now: request.missingParam {name:"uid"}, page-addressing errors, 409 uid.notOnPage, 422 error.unexpected, 500 page.read / verified in: translation_ops.go:337-361
  • was: move-refs "409 when toPage has no such element" / now: 409 uid.notOnTarget {uid} / verified in: element_transfer.go:459-470
  • was: move-refs {pages, media} without semantics / now: pages covers every uid in the subtree incl. source page; media the bundle filenames toPage lacks; translations? omitempty / verified in: moveRefsResponse (element_transfer.go:412)
  • was: link/resolve only its language additions / now: full {kind, label, found, address?, fallback?} / verified in: linkResolveResponse (linktargets.go:155-301)
docs/api.md — translations and git
  • was: translate-plan "refuses what the translate would" / now: refuses default language, unknown page/lang, existing translation only; does not check addressTaken, parse the source, and counts subpages translate would skip / verified in: translation_pages.go:142-159, pageops/translation.go:177-189
  • was: translate 409 only "when the translation exists"; skipped = sources that do not parse / now: also 409 translation.addressTaken and translation.appeared; skipped covers subpages whose address is taken; failure rolls back / verified in: pageops/translation.go:244-330
  • was: translate "400 for default language or unknown mode, 404 …" / now: full list incl. trans.defaultLangNamed, page.pathMissing/notMarkdown/translationPath, 422 translation.sourceParse/serialize/frontMatter, 500 trans.readNew / verified in: translation_pages.go:83-128
  • was: translate snapshot unspecified / now: one translate snapshot naming every created page / verified in: translation_pages.go:115, git.go:533
  • was: element translate(-all) write "nothing when nothing changed" / now: also write when opening repaired the translation / verified in: translation_ops.go:489,533,565
  • was: element translate "409 … translate the parent first" / now: the three l10n.*ParentFirst codes; l10n.noElement {uid} for a uid the source lacks / verified in: translation_ops.go:523-531, l10n/translate.go:275-290
  • was: disconnect "400/404/409" only / now: adds 422 error.unexpected and 422 trans.disconnectFrontMatter / verified in: translation_pages.go:171-204
  • was: undo/discard git preconditions unstated / now: undo needs a repository (undo.noRepo/undo.unavailable); discard needs main (discard.unavailable) / verified in: git.go:743,1010
  • was: undo also = translations only / now: also partner pages of a multi-page step (transfer, relocation, translate with subpages) / verified in: git.go:769-805,842-855
  • was: (undocumented) / now: undo answer {page, removed, type?, tree?, also?} (pageStateResponse), no layout/translation / verified in: git.go:680-713
  • was: git/state, sync, badges given only as links / now: full key lists / verified in: gitStateResponse (git.go:398-439), handleGitSync, handleBadges
docs/api.md — publish
  • was: empty answer 200 {empty: true, pushed, message} / now: {empty: true, pushed: false, message, code: "publish.empty"} / verified in: publishEmptyResponse (publish.go:269-282)
  • was: warning {warning: {disabledLinks}} / now: {warning: {disabledLinks: {page, title, linkedFrom: {page, lang?, title}[]}[]}} with sort and match rules / verified in: disabledLinkWarning (publish.go:305-330)
  • was: 409 {error: "a publish is already running", job} / now: 409 {error, code: "publish.alreadyRunning", args: {}, job}; body decoded first / verified in: publishJobRunningResponse (publish.go:149)
  • was: "the git-not-ready 409" (no code) / now: 409 publish.noRepo or 409 publish.unavailable {detail} / verified in: requireGit (git.go:126-196)
  • was: (undocumented) / now: 500 publish.listChanges {detail} / verified in: publish.go:417-420
  • was: job done {job, phase, commit, pushed, feedback, message?} / now: adds messageCode?; feedback is "polling" | "static" / verified in: publishJobState (publish.go:125-139)
  • was: job empty {job, phase, pushed, message} / now: adds messageCode: "publish.empty" / verified in: jobState (publish.go:237-239)
  • was: job failed {…, status, error, retryable?, hugoOutput?} / now: adds code, args?; full per-status code table (422 mediaMissing/mediaUnverifiable/buildFailed/buildTimeout, 502 pushRejected, 500 …) / verified in: publishJobState, publishset.go:131-182
  • was: unknown job id is 404 (no code) / now: 404 publish.noSuchJob / verified in: publish.go:515-518
  • was: GET /api/publish/status stub / now: ?commit= required; {phase: publishing|live|failed|unknown|static, runUrl, message?, code?} / verified in: publishStatusResponse (publish.go:687-715), forge/poller.go
  • was: pending/discard-all "see Translations on the wire" / now: full sections; pending rows page/media/file; discard-all {discarded} with 409 discard.noRepo|discard.unavailable, 500 discard.allFailed / verified in: pendingResponse, pendingChange (pending.go:38-86), discardAllResponse (pending.go:542)
docs/api.md — media
  • was: upload request/success "documented in #264" / now: multipart parts file, target ("library"), folder?; response {ref, name, width, height} / verified in: mediaUploadResponse (internal/server/media.go:76-81, 90-168)
  • was: (missing) / now: upload also answers 400 library.uploadNotMultipart|uploadFormRead|uploadMissingFile|uploadTarget|uploadFolder, 500 library.uploadRead|uploadStore, 500 error.unexpected on re-encode failure / verified in: media.go:100-179
  • was: ceiling only via args.limit / now: 15 MiB (15728640 bytes) file, body 1 MiB more / verified in: internal/media/gate.go:9, media.go:67
  • was: 14 media routes were stubs / now: written from handlers / verified in: media.go:214-888, internal/mediaops
docs/api.md — integration review
  • was: create-root errors 409 page.rootExists · 422 · 500 page.writeRoot / now: adds 400 request.malformed, 404 editor.unknownLang / verified in: internal/server/pageops.go:229-236 (decodeBody, opsRootDefault)
  • was: create errors without 404 editor.unknownLang / now: added / verified in: internal/server/pageops.go:196 → opsRoot
  • was: rename "slug derived from title; categories section keeps its slug" / now: a title that yields no slug also keeps the current slug / verified in: internal/pageops/relocate.go:72-75
  • was: media/thumb and media/delete lang "validated only" / now: validated only for a bundle reference, not read for a library one / verified in: internal/server/media.go:504-530, 824-847
  • was: publish-all answers 202 / now: 202 only when it starts a job; empty and warning answers are 200 / verified in: internal/server/publish.go:423-443
docs/harness.md
  • was: harness check --json paragraph links "The ten commands" / now: wording removed; grep -rn 'ten commands' docs README.md is empty (README never had it) / verified in: cmd/harness/main.go commands table (10 commands incl. version)
  • was: features --json / add --json examples show version 5 (news) and 2 (categories), installedVersion 1 / now: news v6, categories v3 (actual harness add news output) / verified in: scaffold/features/*/feature.yaml via harness features --json
  • was: no description of harness add --json failure object / now: {formatVersion, feature, status: conflict|error, conflicts?, error} / verified in: cmd/harness/features.go failureOutput
  • was: features --json failure not documented / now: {"formatVersion":1,"error":"..."} (no status) / verified in: cmd/harness/features.go featuresOutput
  • was: check --json described as one-liner, "(not a Hugo site)" only / now: full shape with severity, area values, counts, failure object / verified in: cmd/harness/check.go checkOutput, checkFailureOutput; internal/checkcmd/check.go Finding, Counts, Area constants
  • was: reconcile --json shape not documented (only prose) / now: success and failure objects, resolved[{path,winner}] / verified in: cmd/harness/reconcile.go reconcileOutput, reconcileFailureOutput
  • was: check prose lists only part of the rules / now: adds errors for slot shortcode that is not a wrapper, two pages at one address, translation without source, unreadable hugo.toml language config, hugo.toml/harness.yaml language disagreement; warnings for connected-translation structure and default-language-suffixed page / verified in: internal/checkcmd/check.go:516, 556, 587, 605-641, 733, 547
  • was: remove "blocked" lists only content in use / now: a vendored file the owner customised also counts as in use (blocks without --purge) / verified in: internal/removecmd/run.go "Refuse or purge", Result.CustomizedFiles
  • was: remove --json version unmentioned / now: version omitted when no receipt / failed early (omitempty) / verified in: cmd/harness/features.go removeOutput
  • was: remove --json error: "carries every array empty" only for early failures / now: error carries partial report if writing began, else empty arrays; text mode prints a "re-run to finish" hint / verified in: cmd/harness/features.go removeCmd
  • was: lang add JSON: hugoVersion always present / now: only with status added (omitempty); already-present has home: "", no hugoVersion / verified in: cmd/harness/lang.go langAddOutput; ran lang add de twice
  • was: lang remove not-present/error language label unspecified / now: language.label is "" / verified in: internal/langcmd/langcmd.go Remove; ran lang remove fr
  • was: lang add refusals omit missing harness.yaml/hugo.toml / now: both required ("not a deckle site") / verified in: internal/langcmd/langcmd.go loadSite
  • was: effective Hugo version "hugo.binary reports, else hugo.version, else PATH" / now: hugo.version is taken as-is when set and hugo.binary is not; the binary (or PATH hugo) is probed otherwise / verified in: internal/langcmd/langcmd.go checkHugo
  • was: serve lifecycle only listed output lines / now: start order, git degradation, branch-mode startup sync, DECKLE_FORGE_TOKEN publish feedback, media dir creation, exit 1 causes, SIGINT/SIGTERM exit 0, usage errors (exit 2) for non-loopback --addr and bad preview flags / verified in: cmd/harness/main.go serve, parseServeArgs, buildForgeClient, startupSync
  • was: receipt example "11 files" categories example with all 3 steps, long prose / now: condensed to news example + key table + one disableKinds edit; fields edit/removed/added/after/gap / verified in: internal/addcmd/receipt.go Receipt, internal/addcmd/config.go ConfigEdit; actual harness add news receipt (18 files, version 6)
  • was: flags prose scattered, -h behaviour undocumented / now: Conventions bullet: single dash accepted, positional either side of flags, <cmd> -h prints flags and exits 2, harness -h/--help = unknown command exit 2 / verified in: ran every harness <cmd> -h
  • was: harness.yaml section: hugo.version "exact" only / now: invalid format is a load error; empty languages list, bad/duplicate code load errors; hugo.binary relative to site dir; what init writes / verified in: internal/siteconfig/siteconfig.go Load, validateLanguages; internal/initcmd/init.go; generated harness.yaml
  • was: per-command JSON shapes split between "harness lang" and "Machine-readable output" / now: all --json shapes live under ## Machine-readable output (one ### per command); harness lang links to them / verified in: n/a (restructure; no test pins it)
docs/features.md
  • was: init substitutes @@TITLE@@, @@LANG@@, @@HUGO_VERSION@@ in five files / now: four markers, @@LANG_LABEL@@ too (the language label in harness.yaml) / verified in: internal/initcmd/init.go:44-49,266-272; scaffold/site/harness.yaml:7
  • was: scaffold file list omitted assets/media/user_upload/ / now: listed (scaffold/site/assets/media/user_upload/.gitkeep) / verified in: internal/initcmd/init.go buildPlan; scaffold/site tree
  • was: allow.childOf is "one of the site's own" (unenforced prose) / now: childOf must NOT be a page type the bundle ships; pageType must be one it ships; pairs may not repeat / verified in: internal/addcmd/manifest.go parseManifest (allow loop)
  • was: no mention that a hint may not contain a line break / now: stated in the manifest table / verified in: internal/addcmd/manifest.go parseManifest (hints loop)
  • was: manifest requires rules described as "acyclic, which a test proves" / now: dropped from the key table (not a manifest-validation rule; only the three checks valid name / not self / not repeated are) / verified in: internal/addcmd/manifest.go parseManifest (requires loop)
  • was: remove section silent on the home page / now: home page is never deleted even if its type belongs to the feature; it is warned about / verified in: internal/removecmd/usage.go package doc ("The site's home page is the one page never deleted")
  • was: remove section silent on files behind a symlinked directory / now: left in place and named in a warning (step 2) / verified in: internal/removecmd/run.go (splitSymlinked, "Write order" step 2)
  • was: remove section silent on failure behaviour / now: nothing rolled back, receipt deleted last so a re-run finishes the job / verified in: internal/removecmd/run.go "Write order" and Run doc comment
  • was: page-type allowance skip cases = missing / unparsable / unmodellable sidecar / now: also a symlinked sidecar / verified in: internal/addcmd/children.go:288 (doc comment on symlinked sidecar skipped)
  • was: allowance next-step wording only / now: unchanged in substance; "manual form of any skipped allowance precedes the hints" stated / verified in: internal/addcmd/add.go Result.NextSteps comment
  • was: categories delete "answers {deleted, changed}" in features.md / now: link to api.md#get-apipagedelete-plan (wire shape belongs to api.md) / verified in: docs/api.md:433 heading
  • was: news "18 files", categories "eleven files", versions 6 and 3 / now: unchanged, re-counted / verified in: find scaffold/features -type f (news 18 excl. feature.yaml, categories 11), scaffold/features/*/feature.yaml
  • was: collision status conflict, requirement failure status error never conflict / now: unchanged, confirmed / verified in: cmd/harness/features.go:363-372, isRequirementFailure
  • was: exit codes / error messages for unshipped requirement, dependents, check warning / now: unchanged, confirmed verbatim / verified in: internal/addcmd/add.go (UnshippedRequirementError.Error, RequirementError.Error), internal/removecmd/run.go (DependentsError.Error), internal/checkcmd/check.go:761, internal/addcmd/source.go:112
docs/architecture.md
  • was: alt text required by default on the shipped image elements; a missing alt blocks the save / now: the only shipped element with an alt param, img, has required: false; the theme always emits alt / verified in: scaffold/editor/elements/img.yaml:29-35, scaffold/themes/deckle/layouts/partials/ce/img.html:41-50
  • was: "Not in the engine" — the editor side (language switch, Translate dialogs, Languages view) is #192, translations made through the API or by hand / now: removed, it landed / verified in: merges dafdf11 (#201), 354a2ac (#202), 2b929f7 (#203)
  • was: translating the editor's own UI is out of scope / now: landed; architecture.md links "The editor's language" / verified in: ce46b73 (PR #229), 9da8dc8 (PR #258), web/src/i18n/{en,de}.json
  • was: out of scope: the hosted platform's Hugo pin (a two-language site may fail to verify there) / now: dropped; deckle-hosted pins 0.167.0+ and defaults to 0.168.0 / verified in: imports/deckle-hosted/features/languages_test.go:55-66
  • was: "a move whose post-write step fails rolls back" / now: rename and move share one relocation mechanism that rolls back / verified in: internal/pageops/doc.go:77-84
  • was: POST /api/page/delete answers {deleted, changed} / now: {deleted, translations?, changed} (moved to api.md) / verified in: internal/server/pageops.go:450-454 (deleteResponse)
  • was: sync answers 409 in WIP mode / now: also 409 when the site is not a repository, 500 on a local failure (moved to api.md) / verified in: internal/server/git.go:629-650
  • was: every /api/ response in branch mode carries X-Deckle-Draft-Tip / now: only when the tip is non-empty (moved to api.md) / verified in: internal/server/mutation.go (draftTipWriter.inject)
  • was: insert, move, delete, duplicate on a connected translation are refused 409 / now: transfer and page/save with children too (code trans.connectedStructure) / verified in: internal/server/element_transfer.go:200-203, server.go:1003, translation_ops.go:53-64
  • was: the lock is held by every mutating handler, publish and sync / now: GET /api/page and /api/page/elements (uid repair) also hold it; publish hands it to the job / verified in: internal/server/mutation.go:15-21,43-60
  • was: "A single-language site renders exactly as before, apart from tel: link params" / now: dropped — a change note from #191, not architecture
  • was: (architecture.md Media gate) "the extension always matches the sniffed type" / now: the extension follows the stored, re-encoded format (WebP/AVIF stored as .jpg/.png) / verified in: internal/media/process.go:86-97
docs/host-navigation.md
  • was: "This section is the whole contract." (line 11; the file is a document now) / now: "This document is the whole contract." / verified in: docs/host-navigation.md (structure after #263 split)
  • was: "(i18n on the wire, see Sidecar labels on the wire below)" (line ~472; that section is in docs/api.md, not below in this file) / now: link [Sidecar labels on the wire](api.md#sidecar-labels-on-the-wire) / verified in: docs/api.md:145 (### Sidecar labels on the wire)

Follow-ups filed (behaviour documented as it is, per #264's scope)

  • #269: POST /api/media/delete with a page deletes any file in the bundle, the page's own _index.md included.
  • #270: git sync's deckle: save before sync commit shows up as an undo of a page named "before sync".
  • #271: harness init writes --lang and --title into harness.yaml without validating them.
  • #272: API error-body inconsistencies: rules with two owners, the path/page naming, uncoded text, absolute paths in args, stale comments.
  • #273: minor API behaviour inconsistencies: lang validation in link/resolve, duplicate's slot allowlist, the root page/refs, media folder rules, title trimming, snapshot ops, and others.

No uncatalogued error code was found: every code a handler writes is defined in internal/apimsg.

Closes #264

🤖 Generated with Claude Code

## Summary The content pass #263 left for later: the documents under `docs/` are now true against the code and complete. - **`docs/api.md`**: every one of the 66 registered routes has exactly one section in one shape: purpose, Request, Response, Errors (codes from `internal/apimsg`), Languages, Writes. The 24 stubs are written. Behaviour that was split across feature paragraphs ("Translations on the wire", "Server-described forms" and others) is merged into each route's own section. Shared shapes are stated once under **Conventions** and linked: notation, request/response rules, errors and coded messages, page addressing, content trees, page answers, forms, sidecar labels, translations and writes. Every key was read from the handler and the Go types it marshals. An adversarial review then re-checked 40 claims against the code. - **`docs/harness.md`**: the usage block is checked against the binary's output. Every command's flags, exit codes and `--json` shape are checked against `cmd/harness`. A new `## Check` section lists the rules `internal/checkcmd` applies. The "ten commands" wording is gone. - **`docs/features.md`**: checked against `internal/addcmd`, `internal/removecmd`, `internal/initcmd` and the shipped bundles. `requires` is now stated once. It went from 585 to 470 lines. - **`docs/architecture.md`**: rewritten as a narrative in reading order, 459 → 198 lines (≤ half), with no wire shapes. Each decision is one sentence plus a link to its ADR. - **`docs/adr/`**: 22 records. Each one is dated to its originating commit, which was confirmed with `git log -S`/`--grep`. Together they cover every decision #264 names: rich text (0006), media gate (0004), publish job (0018), branch mode (0011), branch bundles (0008), publish anyway (0014), host-navigation safety (0010), `requires` (0021, `78e181e`) and no aliases (0017, `e4d8138`). The others carry the rationale the docs and commits already stated. - **`docs/host-navigation.md`**: two editorial link fixes only. The v1 shape was compared with deckle-hosted's producer (`server/hostnav.go`) and the editor's consumer, and they match. - **`scaffold/site/AGENTS.md`**: no sidecar fact proved wrong against the engine, so it is unchanged. - `docs/.docs-sync` is updated. No code changed since the baseline `c33d375`. ## Verification - `go test ./...` passes, after building `web/dist`. That includes `docs/docs_test.go`, the route test in `internal/server`, the command test in `cmd/harness`, `scaffold/skills_test.go` and `internal/element/agentsdoc_test.go`. `go vet ./...` is clean. - The output of `go run ./cmd/harness` matches the usage block in `docs/harness.md` exactly. Every `harness <cmd> -h` was run. - `grep -rn 'ten commands' docs README.md` returns nothing. `grep -c 'Documented in #264' docs/api.md` returns 0. - `wc -l docs/architecture.md` is 198, against 459 after #263. ## Sampled routes: marshalled type vs documented keys | Route | Go type (json tags) | Documented | |---|---|---| | GET /api/page | `pageResponse` (page, type, tree, layout, translation,omitempty); `translationInfo` (mode, source, changed, untranslated, synced); `missingTranslationResponse` (page, missing, type, title, file, source) | page answer `{page, type, tree, layout: PageLayout \| null, translation?}`; Translation (all five); missing answer (all six) | | POST /api/page/save | `savePageRequest` (page, lang, path, name, values, body *string, items *[]map, children *savePageChildren{order:[{uid, enabled *bool}], removed}) → `pageResponse` | `{page, lang?, path, name, values, body?, items?, children?: {order: {uid, enabled?}[], removed}}` → page answer | | GET /api/tree | `pagetree.Tree` (lang, root); `pagetree.Node` (path, permalink, address, lang, translation?, l10n?, kind, type, title, titleCode?, weight, date?, childOrder?, hideChildPages?, containerFor?, managedBy?, hidden?, problem?, missing?, dir?, children) + `enabled` from MarshalJSON | `{lang, root: TreeNode}` with every key, `?` exactly on the omitempty ones | | POST /api/page/delete | request {page, lang}; `deleteResponse` (deleted, translations,omitempty, changed) | `{page, lang?}` → `{deleted, translations?, changed}` | | POST /api/form | request {form, params}; `formDescription` (fields, type?, items?, children?, synced?, errors?); `element.FormField` (16 tags); `itemsDescription` (element, label, i18n?, itemLabel, itemI18n?, summary?, fields, rows); `itemRow` (uid, enabled, fields) | same keys and optionality | | GET /api/elements | `elementsResponse` (elements, errors); `element.Schema` (16 tags); `element.Param` (11 tags, LabelCode `-`); `sidecardir.LoadError` (file, error) | same; `labelCode` correctly absent on params | | POST /api/element/insert | `elementInsertRequest` (page, lang, slot, parent, index *int, name) → `insertResponse` (pageResponse + path, uid) | `{page, lang?, slot?, parent?, index, name}` → page answer + `{path, uid}` | | POST /api/element/transfer | `elementTransferRequest` (lang, fromPage, from, toPage, toSlot, toParent, toIndex *int, mode) → `transferResponse` (source pageResponse, target insertResponse) | `{lang?, fromPage, from, toPage, toSlot?, toParent?, toIndex, mode}` → `{source: PageAnswer, target: PageAnswer & {path, uid}}` | | GET /api/element/body | query page, lang, path, name → `elementBodyResponse` (body) | `{body: string}` | | GET /api/element/move-refs | query uid, toPage, page, lang, toParentUid → `moveRefsResponse` (pages, media, translations,omitempty *transferPlan{moved, dropped}) | `{pages, media, translations?: {moved, dropped}}` | | POST /api/page/translate | `translatePageRequest` (page, lang, mode, subpages) → `translatePageResponse` (pageResponse + created, skipped,omitempty) | `{page, lang, mode?, subpages?}` → page answer + `{created, skipped?}` | | GET /api/git/state | `gitStateResponse` (state, reason, reasonCode?, reasonArgs?, lastSnapshotError, undoAvailable, mode, branch, tip, unsynced, unsyncedReason, undoOp?, undoPage?, undoLang?, undoTime?, undoPages?) | all 16, same optionality | | POST /api/page/undo | request {page, lang} → `pageStateResponse` (page, removed, type,omitempty, tree,omitempty, also,omitempty) | `{page, lang?}` → `{page, removed, type?, tree?, also?}` | | POST /api/publish-all | request {checkDraftLinks}; `publishJobState` (202: job, phase); `publishEmptyResponse` (empty, pushed, message, code); `disabledLinkWarning`/`disabledLink`/`linkingPage` (disabledLinks; page, title, linkedFrom; page, lang?, title); `publishJobRunningResponse` (error, code, args, job) | same | | GET /api/publish/job | `publishJobState` (job, phase, commit?, pushed?, feedback?, message?, messageCode?, status?, error?, code?, args?, retryable?, hugoOutput?) | split per phase, every key | | GET /api/publish/pending | `pendingResponse` (changes, unavailable?, unavailableCode?, unavailableArgs?); `pendingChange` (kind, page?, title?, titleCode?, path?, status, elements?, properties?, resources?); `elementCounts` (added, changed, removed) | same | | POST /api/media/upload | multipart parts file, target, folder → `mediaUploadResponse` (ref, name, width, height) | `file`, `target: "library"`, `folder?` → `{ref, name, width, height}` | | GET /api/media/library | query folder → `mediaLibraryResponse` (folders, items); `mediaItem` (ref, name, size, type) | same | | POST /api/media/folder/delete | request {folder} → `mediaDeletePlanResponse` (delete, keep); `mediaKeepEntry` (ref, pages) | same | ## Corrected claims (was / now / verified in) <details><summary>docs/api.md — conventions</summary> - was: linkPicker value `{href, newTab}` / now: `{href, newTab, title}` / verified in: element.LinkValue, internal/element/ftype_link.go:57 - was: 422 field keys are bare names, `items[<i>].<name>` and `body` / now: `children` too / verified in: childrenField, handleSavePage (server.go) - was: "every other failure keeps the plain body with 400/404/409/500" / now: a plain body can also be 422 (unparseable page, serializer refusal, placement violation); the two 422 shapes differ by `errors` / verified in: pageAndTree, serializePage, element.go:96-145 - was: `translation` on GET /api/page, page/save and element/enabled / now: on every page answer in a non-default language / verified in: pageAnswer (translation.go:275) and callers - was: sidecar load errors without a code in elements, backendlayouts, modules / now: pagetypes too; all four `{file, error}` / verified in: sidecardir.LoadError (sidecardir.go:179) - was: create form not listed as default-language only / now: POST /api/form kind create answers 400 editor.createFormDefaultOnly / verified in: forms.go:308 - was: type fallback is the "section-name" / now: the top-level section's name / verified in: pageops.EffectiveType (pagetypes.go:105) - was: (unstated) / now: wrong method is 405 request.method on every route; unknown /api/ path a plain-text 404 / verified in: requirePost (pageops.go:100), route (server.go) - was: (unstated) / now: empty/undecodable POST body is 400 request.malformed; unknown keys ignored / verified in: decodeBody (pageops.go:91) - was: (unstated) / now: mutation lock covers every POST except form, form/check, form/inline, publish-all, plus GET page and page/elements / verified in: mutatingRoutes (mutation.go) - was: "The endpoints (all POST, JSON, none takes the mutation lock):" followed by nothing / now: removed; #writes names them / verified in: mutatingRoutes - was: X-Deckle-Draft-Tip on every branch-mode response / now: only while the branch has a commit / verified in: draftTipWriter (mutation.go) </details> <details><summary>docs/api.md — site and forms</summary> - was: GET /api/status "Documented in #264" / now: `{hugo: {state, previewUrl, restarts, lastExit, lastBuildError, output, version, source}}` / verified in: statusResponse/hugoStatus (server.go:290-326) - was: GET /api/site only a link / now: `{title, languages: {code,label}[], acceptLanguage}` with parse rules / verified in: siteResponse, acceptLanguageTags (server.go:353-461) - was: elements/pagetypes/backendlayouts/modules only a link to Sidecar labels / now: every marshalled key / verified in: element.Schema/Param/Items/BodyPolicy.MarshalJSON, pagetype.Schema, backendlayout.Schema, editormodule.Module/FieldInfo/Filter, sidecardir.LoadError - was: module field `{name, label, labelCode?, widget, sortable}` / now: also `i18n?` / verified in: editormodule.FieldInfo.I18n - was: module filter `{field, label, labelCode?, options}` / now: also `i18n?` / verified in: editormodule.Filter.I18n - was: /api/form → `{fields, type?, items?, children?, errors?}` / now: adds `synced?` / verified in: formDescription (forms.go:80-122) - was: Enabled field `{name, widget, settings, group, tab, value}` / now: also `label`, `labelCode: "label.enabled"`, `required: false` / verified in: element.EnabledField (element/form.go:124-136) - was: a category's form has no Enabled field (unqualified) / now: in a non-default language every translation, a category's and the root's included, has its own / verified in: pageops/translation_settings.go:222-231 - was: /api/form/check `{form, field, value}` / now: also `params?` / verified in: forms.go:792-797 - was: form/check "a field the form does not have is a 400" / now: `400 field.unknown {field}`, `400 form.noItemRows {field}` / verified in: forms.go:405-423 - was: /api/form/inline link stored as href plus `<name>Target` / now: `<name>`, `<name>Target`, `<name>Title` / verified in: elemops/save.go:184-208 MergeParams - was: /api/form/inline "or a 422 while a value is invalid" / now: 422 field errors incl. field.unknownParam, plus 400 form.notInlineKind, editor.noSchema, form.notInline, form.inlineParamType / verified in: forms.go:834-875 </details> <details><summary>docs/api.md — pages</summary> - was: "Hidden pages (hideChildPages) stay in the payload, marked hidden" / now: `hidden` only on a module-managed entry whose parent's type sets hideChildPages / verified in: pagetree/pagetree.go:185-207, annotate.go:30 - was: tree node keys listed partially / now: `{lang, root: TreeNode}` with every Node key (path, kind, type, titleCode?, weight, date?, childOrder?, hideChildPages?, problem?, missing?, dir?, children, enabled …) / verified in: pagetree.Tree, pagetree.Node + MarshalJSON (pagetree.go:91-243) - was: "every page node carries lang" / now: every node, synthesized ones included / verified in: pagetree/build.go:427 - was: the tree's address of a page with `url` is the url itself / now: only a root-relative url is verbatim; a relative one lands under the language prefix / verified in: pagetree/build.go:497-503 - was: GET /api/page and /api/page/elements "See …" stubs / now: page or missing answer; elements `{page, elements: {uid, name, label, i18n?, summary}[]}`; opening snapshots `repair translation` or `stamp uids` / verified in: server.go:835-866, linktargets.go:48-136 - was: a `slot` child moved or toggled answers "422" / now: plain 422 element.slotMove / element.slotToggle {name}, not a field error / verified in: elemops/children.go:117-122 - was: connected-translation items refusal = row added or item left out / now: a uid sent twice too / verified in: translation.go:445-480 - was: page/save propagation "holds each translation to the source's structure" / now: synced-param copy always; structural repair only when every source element has a sound uid / verified in: translation.go:500-520 - was: (unstated) / now: page/save `values` is the whole form (a declared param left out is removed); item rows keep their echoed uid; unknown params of a stored row dropped / verified in: elemops/save.go:155-161,236-257 - was: (unstated) / now: page/save codes 409 element.pathEmpty|pathRange|pathNotBlock|pathNotShortcode|pathName, 400 editor.noSchema, 400 item.notDeclared|item.noSchema, 409 editor.bodyNestedBlocks / verified in: forms.go:473-511, server.go:965-1080 - was: 422 on `slug` when a sibling's translation has that address / now: translation.slugTaken for any sibling holding the address in that language (incl. untranslated siblings' would-be addresses, grouping dirs) / verified in: pageops/translation_settings.go translationAddressTaken - was: settings/save rules partial / now: `type` key → 400 page.typeImmutable; absent slug keeps, "" deletes; unchanged enabled writes nothing / verified in: pageops/settings.go:249-286 - was: create body `{parent, lang, type, title, …}` / now: parent and type optional; blank title on quick create is 400 page.titleEmpty / verified in: pageops/create.go:143-195 - was: create-root, reorder, enabled stubs / now: documented incl. 409 page.rootExists, reorder `index` required (0..count−1), enabled required, page.rootDisable only for false / verified in: server/pageops.go:207-415, pageops/reorder.go:45 - was: POST /api/page/delete answers `{deleted, changed}` / now: `{deleted, translations?, changed}` / verified in: deleteResponse (server/pageops.go:450-454) </details> <details><summary>docs/api.md — elements and links</summary> - was: element POST routes were stubs ("See Translations on the wire") / now: full sections / verified in: internal/server/element.go:49-448, element_enabled.go, element_transfer.go - was: GET /api/element/body, /api/page/refs, /api/element/refs "Documented in #264" / now: written from code / verified in: body.go:42-120 (elementBodyResponse), linktargets.go:343-453 - was: transfer on a connected translation refused for the receiving page and, in a move, the giving page / now: also a same-page copy / verified in: element_transfer.go:200-205 - was: "a move or delete is mirrored" / now: a move into a container a connected translation has not translated drops the copy there / verified in: l10n/structure.go:295-298, translation_ops.go:182-195 - was: a cut is dropped "where the target has none or only a free one" / now: also when the target translation cannot be read or lacks the destination parent / verified in: translation_ops.go:259-279 (receiveTranslated) - was: transfer snapshot unstated / now: cross-page move one `transfer` snapshot for both pages; same-page or copy `save` / verified in: element_transfer.go:291-327 - was: element/enabled snapshot unstated / now: `save`, none when nothing changes / verified in: element_enabled.go:84-90 - was: delete-plan "400 without uid, 404 unknown page or language, 409 no uid" / now: request.missingParam {name:"uid"}, page-addressing errors, 409 uid.notOnPage, 422 error.unexpected, 500 page.read / verified in: translation_ops.go:337-361 - was: move-refs "409 when toPage has no such element" / now: 409 uid.notOnTarget {uid} / verified in: element_transfer.go:459-470 - was: move-refs `{pages, media}` without semantics / now: pages covers every uid in the subtree incl. source page; media the bundle filenames toPage lacks; translations? omitempty / verified in: moveRefsResponse (element_transfer.go:412) - was: link/resolve only its language additions / now: full `{kind, label, found, address?, fallback?}` / verified in: linkResolveResponse (linktargets.go:155-301) </details> <details><summary>docs/api.md — translations and git</summary> - was: translate-plan "refuses what the translate would" / now: refuses default language, unknown page/lang, existing translation only; does not check addressTaken, parse the source, and counts subpages translate would skip / verified in: translation_pages.go:142-159, pageops/translation.go:177-189 - was: translate 409 only "when the translation exists"; skipped = sources that do not parse / now: also 409 translation.addressTaken and translation.appeared; skipped covers subpages whose address is taken; failure rolls back / verified in: pageops/translation.go:244-330 - was: translate "400 for default language or unknown mode, 404 …" / now: full list incl. trans.defaultLangNamed, page.pathMissing/notMarkdown/translationPath, 422 translation.sourceParse/serialize/frontMatter, 500 trans.readNew / verified in: translation_pages.go:83-128 - was: translate snapshot unspecified / now: one `translate` snapshot naming every created page / verified in: translation_pages.go:115, git.go:533 - was: element translate(-all) write "nothing when nothing changed" / now: also write when opening repaired the translation / verified in: translation_ops.go:489,533,565 - was: element translate "409 … translate the parent first" / now: the three l10n.*ParentFirst codes; l10n.noElement {uid} for a uid the source lacks / verified in: translation_ops.go:523-531, l10n/translate.go:275-290 - was: disconnect "400/404/409" only / now: adds 422 error.unexpected and 422 trans.disconnectFrontMatter / verified in: translation_pages.go:171-204 - was: undo/discard git preconditions unstated / now: undo needs a repository (undo.noRepo/undo.unavailable); discard needs main (discard.unavailable) / verified in: git.go:743,1010 - was: undo `also` = translations only / now: also partner pages of a multi-page step (transfer, relocation, translate with subpages) / verified in: git.go:769-805,842-855 - was: (undocumented) / now: undo answer `{page, removed, type?, tree?, also?}` (pageStateResponse), no layout/translation / verified in: git.go:680-713 - was: git/state, sync, badges given only as links / now: full key lists / verified in: gitStateResponse (git.go:398-439), handleGitSync, handleBadges </details> <details><summary>docs/api.md — publish</summary> - was: empty answer `200 {empty: true, pushed, message}` / now: `{empty: true, pushed: false, message, code: "publish.empty"}` / verified in: publishEmptyResponse (publish.go:269-282) - was: warning `{warning: {disabledLinks}}` / now: `{warning: {disabledLinks: {page, title, linkedFrom: {page, lang?, title}[]}[]}}` with sort and match rules / verified in: disabledLinkWarning (publish.go:305-330) - was: `409 {error: "a publish is already running", job}` / now: `409 {error, code: "publish.alreadyRunning", args: {}, job}`; body decoded first / verified in: publishJobRunningResponse (publish.go:149) - was: "the git-not-ready 409" (no code) / now: `409 publish.noRepo` or `409 publish.unavailable {detail}` / verified in: requireGit (git.go:126-196) - was: (undocumented) / now: `500 publish.listChanges {detail}` / verified in: publish.go:417-420 - was: job done `{job, phase, commit, pushed, feedback, message?}` / now: adds `messageCode?`; `feedback` is `"polling" | "static"` / verified in: publishJobState (publish.go:125-139) - was: job empty `{job, phase, pushed, message}` / now: adds `messageCode: "publish.empty"` / verified in: jobState (publish.go:237-239) - was: job failed `{…, status, error, retryable?, hugoOutput?}` / now: adds `code`, `args?`; full per-status code table (422 mediaMissing/mediaUnverifiable/buildFailed/buildTimeout, 502 pushRejected, 500 …) / verified in: publishJobState, publishset.go:131-182 - was: unknown job id is 404 (no code) / now: `404 publish.noSuchJob` / verified in: publish.go:515-518 - was: GET /api/publish/status stub / now: `?commit=` required; `{phase: publishing|live|failed|unknown|static, runUrl, message?, code?}` / verified in: publishStatusResponse (publish.go:687-715), forge/poller.go - was: pending/discard-all "see Translations on the wire" / now: full sections; pending rows page/media/file; discard-all `{discarded}` with `409 discard.noRepo|discard.unavailable`, `500 discard.allFailed` / verified in: pendingResponse, pendingChange (pending.go:38-86), discardAllResponse (pending.go:542) </details> <details><summary>docs/api.md — media</summary> - was: upload request/success "documented in #264" / now: multipart parts `file`, `target` ("library"), `folder?`; response `{ref, name, width, height}` / verified in: mediaUploadResponse (internal/server/media.go:76-81, 90-168) - was: (missing) / now: upload also answers 400 library.uploadNotMultipart|uploadFormRead|uploadMissingFile|uploadTarget|uploadFolder, 500 library.uploadRead|uploadStore, 500 error.unexpected on re-encode failure / verified in: media.go:100-179 - was: ceiling only via `args.limit` / now: 15 MiB (15728640 bytes) file, body 1 MiB more / verified in: internal/media/gate.go:9, media.go:67 - was: 14 media routes were stubs / now: written from handlers / verified in: media.go:214-888, internal/mediaops </details> <details><summary>docs/api.md — integration review</summary> - was: create-root errors `409 page.rootExists · 422 · 500 page.writeRoot` / now: adds `400 request.malformed`, `404 editor.unknownLang` / verified in: internal/server/pageops.go:229-236 (decodeBody, opsRootDefault) - was: create errors without `404 editor.unknownLang` / now: added / verified in: internal/server/pageops.go:196 → opsRoot - was: rename "slug derived from title; categories section keeps its slug" / now: a title that yields no slug also keeps the current slug / verified in: internal/pageops/relocate.go:72-75 - was: media/thumb and media/delete `lang` "validated only" / now: validated only for a bundle reference, not read for a library one / verified in: internal/server/media.go:504-530, 824-847 - was: publish-all answers 202 / now: 202 only when it starts a job; empty and warning answers are 200 / verified in: internal/server/publish.go:423-443 </details> <details><summary>docs/harness.md</summary> - was: `harness check --json` paragraph links "[The ten commands](#commands)" / now: wording removed; `grep -rn 'ten commands' docs README.md` is empty (README never had it) / verified in: cmd/harness/main.go `commands` table (10 commands incl. `version`) - was: `features --json` / `add --json` examples show `version` 5 (news) and 2 (categories), `installedVersion` 1 / now: news v6, categories v3 (actual `harness add news` output) / verified in: scaffold/features/*/feature.yaml via `harness features --json` - was: no description of `harness add --json` failure object / now: `{formatVersion, feature, status: conflict|error, conflicts?, error}` / verified in: cmd/harness/features.go `failureOutput` - was: `features --json` failure not documented / now: `{"formatVersion":1,"error":"..."}` (no status) / verified in: cmd/harness/features.go `featuresOutput` - was: `check --json` described as one-liner, "(not a Hugo site)" only / now: full shape with severity, area values, counts, failure object / verified in: cmd/harness/check.go `checkOutput`, `checkFailureOutput`; internal/checkcmd/check.go `Finding`, `Counts`, Area constants - was: `reconcile --json` shape not documented (only prose) / now: success and failure objects, `resolved[{path,winner}]` / verified in: cmd/harness/reconcile.go `reconcileOutput`, `reconcileFailureOutput` - was: `check` prose lists only part of the rules / now: adds errors for slot shortcode that is not a wrapper, two pages at one address, translation without source, unreadable hugo.toml language config, hugo.toml/harness.yaml language disagreement; warnings for connected-translation structure and default-language-suffixed page / verified in: internal/checkcmd/check.go:516, 556, 587, 605-641, 733, 547 - was: `remove` "blocked" lists only content in use / now: a vendored file the owner customised also counts as in use (blocks without --purge) / verified in: internal/removecmd/run.go "Refuse or purge", `Result.CustomizedFiles` - was: `remove --json` `version` unmentioned / now: `version` omitted when no receipt / failed early (`omitempty`) / verified in: cmd/harness/features.go `removeOutput` - was: `remove --json` error: "carries every array empty" only for early failures / now: error carries partial report if writing began, else empty arrays; text mode prints a "re-run to finish" hint / verified in: cmd/harness/features.go removeCmd - was: `lang add` JSON: `hugoVersion` always present / now: only with status `added` (`omitempty`); `already-present` has `home: ""`, no `hugoVersion` / verified in: cmd/harness/lang.go `langAddOutput`; ran `lang add de` twice - was: `lang remove` `not-present`/`error` language label unspecified / now: `language.label` is `""` / verified in: internal/langcmd/langcmd.go Remove; ran `lang remove fr` - was: `lang add` refusals omit missing harness.yaml/hugo.toml / now: both required ("not a deckle site") / verified in: internal/langcmd/langcmd.go `loadSite` - was: effective Hugo version "hugo.binary reports, else hugo.version, else PATH" / now: `hugo.version` is taken as-is when set and `hugo.binary` is not; the binary (or PATH hugo) is probed otherwise / verified in: internal/langcmd/langcmd.go `checkHugo` - was: `serve` lifecycle only listed output lines / now: start order, git degradation, branch-mode startup sync, DECKLE_FORGE_TOKEN publish feedback, media dir creation, exit 1 causes, SIGINT/SIGTERM exit 0, usage errors (exit 2) for non-loopback `--addr` and bad preview flags / verified in: cmd/harness/main.go `serve`, `parseServeArgs`, `buildForgeClient`, `startupSync` - was: receipt example "11 files" categories example with all 3 steps, long prose / now: condensed to news example + key table + one `disableKinds` edit; fields `edit/removed/added/after/gap` / verified in: internal/addcmd/receipt.go `Receipt`, internal/addcmd/config.go `ConfigEdit`; actual `harness add news` receipt (18 files, version 6) - was: flags prose scattered, `-h` behaviour undocumented / now: Conventions bullet: single dash accepted, positional either side of flags, `<cmd> -h` prints flags and exits 2, `harness -h/--help` = unknown command exit 2 / verified in: ran every `harness <cmd> -h` - was: `harness.yaml` section: `hugo.version` "exact" only / now: invalid format is a load error; empty `languages` list, bad/duplicate code load errors; `hugo.binary` relative to site dir; what `init` writes / verified in: internal/siteconfig/siteconfig.go `Load`, `validateLanguages`; internal/initcmd/init.go; generated harness.yaml - was: per-command JSON shapes split between "harness lang" and "Machine-readable output" / now: all `--json` shapes live under `## Machine-readable output` (one `###` per command); `harness lang` links to them / verified in: n/a (restructure; no test pins it) </details> <details><summary>docs/features.md</summary> - was: init substitutes `@@TITLE@@`, `@@LANG@@`, `@@HUGO_VERSION@@` in five files / now: four markers, `@@LANG_LABEL@@` too (the language label in harness.yaml) / verified in: internal/initcmd/init.go:44-49,266-272; scaffold/site/harness.yaml:7 - was: scaffold file list omitted `assets/media/user_upload/` / now: listed (scaffold/site/assets/media/user_upload/.gitkeep) / verified in: internal/initcmd/init.go buildPlan; scaffold/site tree - was: `allow.childOf` is "one of the site's own" (unenforced prose) / now: `childOf` must NOT be a page type the bundle ships; `pageType` must be one it ships; pairs may not repeat / verified in: internal/addcmd/manifest.go parseManifest (allow loop) - was: no mention that a hint may not contain a line break / now: stated in the manifest table / verified in: internal/addcmd/manifest.go parseManifest (hints loop) - was: manifest `requires` rules described as "acyclic, which a test proves" / now: dropped from the key table (not a manifest-validation rule; only the three checks valid name / not self / not repeated are) / verified in: internal/addcmd/manifest.go parseManifest (requires loop) - was: remove section silent on the home page / now: home page is never deleted even if its type belongs to the feature; it is warned about / verified in: internal/removecmd/usage.go package doc ("The site's home page is the one page never deleted") - was: remove section silent on files behind a symlinked directory / now: left in place and named in a warning (step 2) / verified in: internal/removecmd/run.go (splitSymlinked, "Write order" step 2) - was: remove section silent on failure behaviour / now: nothing rolled back, receipt deleted last so a re-run finishes the job / verified in: internal/removecmd/run.go "Write order" and Run doc comment - was: page-type allowance skip cases = missing / unparsable / unmodellable sidecar / now: also a symlinked sidecar / verified in: internal/addcmd/children.go:288 (doc comment on symlinked sidecar skipped) - was: allowance next-step wording only / now: unchanged in substance; "manual form of any skipped allowance precedes the hints" stated / verified in: internal/addcmd/add.go Result.NextSteps comment - was: categories delete "answers `{deleted, changed}`" in features.md / now: link to api.md#get-apipagedelete-plan (wire shape belongs to api.md) / verified in: docs/api.md:433 heading - was: news "18 files", categories "eleven files", versions 6 and 3 / now: unchanged, re-counted / verified in: `find scaffold/features -type f` (news 18 excl. feature.yaml, categories 11), scaffold/features/*/feature.yaml - was: collision status `conflict`, requirement failure status `error` never `conflict` / now: unchanged, confirmed / verified in: cmd/harness/features.go:363-372, isRequirementFailure - was: exit codes / error messages for unshipped requirement, dependents, check warning / now: unchanged, confirmed verbatim / verified in: internal/addcmd/add.go (UnshippedRequirementError.Error, RequirementError.Error), internal/removecmd/run.go (DependentsError.Error), internal/checkcmd/check.go:761, internal/addcmd/source.go:112 </details> <details><summary>docs/architecture.md</summary> - was: alt text required by default on the shipped image elements; a missing alt blocks the save / now: the only shipped element with an alt param, `img`, has `required: false`; the theme always emits `alt` / verified in: scaffold/editor/elements/img.yaml:29-35, scaffold/themes/deckle/layouts/partials/ce/img.html:41-50 - was: "Not in the engine" — the editor side (language switch, Translate dialogs, Languages view) is #192, translations made through the API or by hand / now: removed, it landed / verified in: merges dafdf11 (#201), 354a2ac (#202), 2b929f7 (#203) - was: translating the editor's own UI is out of scope / now: landed; architecture.md links "The editor's language" / verified in: ce46b73 (PR #229), 9da8dc8 (PR #258), web/src/i18n/{en,de}.json - was: out of scope: the hosted platform's Hugo pin (a two-language site may fail to verify there) / now: dropped; deckle-hosted pins 0.167.0+ and defaults to 0.168.0 / verified in: imports/deckle-hosted/features/languages_test.go:55-66 - was: "a move whose post-write step fails rolls back" / now: rename and move share one relocation mechanism that rolls back / verified in: internal/pageops/doc.go:77-84 - was: POST /api/page/delete answers `{deleted, changed}` / now: `{deleted, translations?, changed}` (moved to api.md) / verified in: internal/server/pageops.go:450-454 (deleteResponse) - was: sync answers 409 in WIP mode / now: also 409 when the site is not a repository, 500 on a local failure (moved to api.md) / verified in: internal/server/git.go:629-650 - was: every /api/ response in branch mode carries X-Deckle-Draft-Tip / now: only when the tip is non-empty (moved to api.md) / verified in: internal/server/mutation.go (draftTipWriter.inject) - was: insert, move, delete, duplicate on a connected translation are refused 409 / now: transfer and page/save with `children` too (code trans.connectedStructure) / verified in: internal/server/element_transfer.go:200-203, server.go:1003, translation_ops.go:53-64 - was: the lock is held by every mutating handler, publish and sync / now: GET /api/page and /api/page/elements (uid repair) also hold it; publish hands it to the job / verified in: internal/server/mutation.go:15-21,43-60 - was: "A single-language site renders exactly as before, apart from tel: link params" / now: dropped — a change note from #191, not architecture - was: (architecture.md Media gate) "the extension always matches the sniffed type" / now: the extension follows the stored, re-encoded format (WebP/AVIF stored as .jpg/.png) / verified in: internal/media/process.go:86-97 </details> <details><summary>docs/host-navigation.md</summary> - was: "This section is the whole contract." (line 11; the file is a document now) / now: "This document is the whole contract." / verified in: docs/host-navigation.md (structure after #263 split) - was: "(`i18n` on the wire, see **Sidecar labels on the wire** below)" (line ~472; that section is in docs/api.md, not below in this file) / now: link `[Sidecar labels on the wire](api.md#sidecar-labels-on-the-wire)` / verified in: docs/api.md:145 (`### Sidecar labels on the wire`) </details> ## Follow-ups filed (behaviour documented as it is, per #264's scope) - #269: `POST /api/media/delete` with a page deletes any file in the bundle, the page's own `_index.md` included. - #270: git sync's `deckle: save before sync` commit shows up as an undo of a page named "before sync". - #271: `harness init` writes `--lang` and `--title` into `harness.yaml` without validating them. - #272: API error-body inconsistencies: rules with two owners, the `path`/`page` naming, uncoded text, absolute paths in args, stale comments. - #273: minor API behaviour inconsistencies: lang validation in link/resolve, duplicate's slot allowlist, the root page/refs, media folder rules, title trimming, snapshot ops, and others. No uncatalogued error code was found: every code a handler writes is defined in `internal/apimsg`. Closes #264 🤖 Generated with [Claude Code](https://claude.com/claude-code)
architecture.md drops from 459 to 198 lines, reader-ordered, with no wire
shapes; each decision paragraph is one sentence linking its ADR. The ADRs
are written from the rationale the docs and commits already stated, dated
to the commit that made the decision.

Refs #264

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Drops the 'ten commands' wording, adds a Check section listing the rules
internal/checkcmd applies, and gathers every --json shape under
Machine-readable output.

Refs #264

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
features.md states requires once under Dependencies and links harness.md
for flags and JSON; host-navigation.md gets two editorial fixes only (its
contract is unchanged and matches deckle-hosted's producer).

Refs #264

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every registered route now has exactly one section — request, response,
error codes, language behaviour and writes — read from its handler and the
Go types it marshals; the 24 stubs are written. The scattered partial
descriptions are merged into their route, and the cross-cutting rules
(notation, errors and coded messages, page addressing, content trees, page
answers, forms, sidecar labels, translations, writes) are stated once in
the conventions. Also corrects architecture.md's media gate: the extension
follows the stored format.

Refs #264

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
docs: record the docs sync after the #264 content pass
All checks were successful
ci / build-and-test (pull_request) Successful in 3m9s
ci / release (pull_request) Has been skipped
d3ed2f70b8
Closes #264

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Author
Owner

[autoland] verdict: pass

Lander verdict: CONCERNS (non-blocking; approved, not auto-merged)

What vouches: CI aggregate success: ci / build-and-test ran go vet ./..., go test ./... (including docs/docs_test.go, the route and command drift tests, scaffold/skills_test.go and agentsdoc_test.go) and the frontend build and tests. ci / release was skipped as expected.

Conventions:

  • The title follows Conventional Commits and the body has Closes #264.
  • The diff touches only docs/ and matches #264's scope.
  • No changes to the README, the scaffold or any code.
  • The branch is current with main.

Acceptance criteria checked:

  • No stubs remain.
  • ten commands is gone.
  • architecture.md went from 459 to 198 lines and has no wire shapes.
  • All 22 ADR commit hashes exist, and each Date: matches its commit.
  • The follow-up issues #269–#273 exist.

Spot checks against the code:

  • docs/api.md: 24 former stubs plus about 25 other routes checked; no wrong claims.
  • Error codes: all 271 codes named in docs/api.md exist in internal/apimsg.
  • Mutation lock: the lock claims match mutatingRoutes in both directions.

The PR's own goal is "true against the code". These sentences are not:

Wrong claims

  1. docs/architecture.md:49-50 says "Mutating requests run under one handler-level lock applied in ServeHTTP; reads never take it."
    • Actually GET /api/page and GET /api/page/elements take it (uid repair), and /api/publish-all locks itself outside ServeHTTP (internal/server/mutation.go:15-21, 44-50).
    • The PR's corrected-claims list says this was fixed, and ADR 0012 states it correctly; the narrative brings the old error back.
  2. docs/adr/0004-media-gate.md:21-22 says "the stored extension always matches the sniffed type".
    • Actually rasters get the re-encoded format's extension: WebP, AVIF and opaque PNG become .jpg, alpha becomes .png (internal/media/process.go:87-97; the same at 20192b6).
    • This is the claim the PR corrected in architecture.md, copied into the new record.
  3. docs/harness.md:454 (add --json, added row) says a receipt whose "requires, version or steps differ … is rewritten".
    • Only a requires change triggers a rewrite (internal/addcmd/add.go:920-921). A different version or steps with no file missing gives already-installed, and the receipt is left as is.
    • docs/features.md:253-259 states it correctly, so two documents now disagree on one fact.
  4. docs/harness.md:489-490 says remove --json leaves out version only when there is no receipt or the remove failed before reading one.
    • It is also left out after the "required by …" refusal, which comes after the receipt is read (internal/removecmd/run.go:222 comes before :225).
  5. docs/adr/0003-wip-snapshots-are-the-undo-store.md:18-20 says undo is a restore "through the normal save path".
    • Undo restores raw git blobs, then snapshots, and never goes through the save handler (9d2732f:internal/server/git.go RestorePaths; now git.go:842 RestorePathsFiltered).
  6. docs/adr/0007-module-shell-with-state-in-the-url.md:
    • :11-12 says "a reload … lost what the user was looking at". The selection was in sessionStorage precisely so it survived a reload (b079daa^:web/src/selection.ts:48-52).
    • :31 says "Element selection stays ephemeral, outside the URL". That has been false since 7696f9a (/edit/<lang>/<page>/<uid>), and the paragraph otherwise reads as current state.
  7. docs/adr/0001-canonical-round-trip-content-engine.md:20-21 says "the server is the only canonicalizer".
    • CLI commands parse and serialize content too (internal/removecmd/purge.go:170-193). "never the browser" would be accurate.

Material omissions

  • docs/harness.md ## Check, errors (:254-261): front matter that does not decode is a content error (internal/checkcmd/check.go:678-681) and is not listed.
  • docs/harness.md ## Check, warnings (:262-271): a connected translation whose elements lack a uid or share one (check.go:578-580) is not listed.
  • ADR 0021 is not linked from docs/architecture.md or any other document, although docs/adr/README.md says the narrative links each record. The other 21 are linked.
  • Glossary and German-file test: the old architecture.md said shipped sidecar language files follow web/src/i18n/glossary.md and that scaffold/langfiles_test.go holds each one complete. That fact now appears in no document.

Minor (no action needed to land)

  • docs/api.md: a few rare filesystem 500s are not listed:
    • page.readLanguages and page.listTranslations on media/bundle, thumb and delete, and page.listTranslations on page undo and discard.
    • error.unexpected on library media/delete.
  • docs/api.md:1686: the pending page row title is omitempty.
  • docs/architecture.md:147: "never force-pushed" should be "never a plain --force", since the lease push is --force-with-lease.
  • docs/adr/0017:35: "lang add touches no content". It still creates content/_index.<code>.md.

None of this blocks landing: the documents are far more accurate than on main. Items 1–7 are short sentence fixes. Fixing them before merge keeps the ADR records, which are never edited to match newer code, correct from the start.

[autoland] verdict: pass ## Lander verdict: CONCERNS (non-blocking; approved, not auto-merged) **What vouches:** CI aggregate `success`: `ci / build-and-test` ran `go vet ./...`, `go test ./...` (including `docs/docs_test.go`, the route and command drift tests, `scaffold/skills_test.go` and `agentsdoc_test.go`) and the frontend build and tests. `ci / release` was skipped as expected. **Conventions:** - The title follows Conventional Commits and the body has `Closes #264`. - The diff touches only `docs/` and matches #264's scope. - No changes to the README, the scaffold or any code. - The branch is current with `main`. **Acceptance criteria checked:** - No stubs remain. - `ten commands` is gone. - architecture.md went from 459 to 198 lines and has no wire shapes. - All 22 ADR commit hashes exist, and each `Date:` matches its commit. - The follow-up issues #269–#273 exist. **Spot checks against the code:** - **`docs/api.md`:** 24 former stubs plus about 25 other routes checked; no wrong claims. - **Error codes:** all 271 codes named in `docs/api.md` exist in `internal/apimsg`. - **Mutation lock:** the lock claims match `mutatingRoutes` in both directions. The PR's own goal is "true against the code". These sentences are not: ### Wrong claims 1. **`docs/architecture.md:49-50`** says "Mutating requests run under one handler-level lock applied in `ServeHTTP`; reads never take it." - Actually `GET /api/page` and `GET /api/page/elements` take it (uid repair), and `/api/publish-all` locks itself outside `ServeHTTP` (`internal/server/mutation.go:15-21, 44-50`). - The PR's corrected-claims list says this was fixed, and ADR 0012 states it correctly; the narrative brings the old error back. 2. **`docs/adr/0004-media-gate.md:21-22`** says "the stored extension always matches the sniffed type". - Actually rasters get the re-encoded format's extension: WebP, AVIF and opaque PNG become `.jpg`, alpha becomes `.png` (`internal/media/process.go:87-97`; the same at `20192b6`). - This is the claim the PR corrected in architecture.md, copied into the new record. 3. **`docs/harness.md:454`** (`add --json`, `added` row) says a receipt whose "`requires`, version or steps differ … is rewritten". - Only a `requires` change triggers a rewrite (`internal/addcmd/add.go:920-921`). A different version or steps with no file missing gives `already-installed`, and the receipt is left as is. - `docs/features.md:253-259` states it correctly, so two documents now disagree on one fact. 4. **`docs/harness.md:489-490`** says `remove --json` leaves out `version` only when there is no receipt or the remove failed before reading one. - It is also left out after the "required by …" refusal, which comes after the receipt is read (`internal/removecmd/run.go:222` comes before `:225`). 5. **`docs/adr/0003-wip-snapshots-are-the-undo-store.md:18-20`** says undo is a restore "through the normal save path". - Undo restores raw git blobs, then snapshots, and never goes through the save handler (`9d2732f:internal/server/git.go` `RestorePaths`; now `git.go:842` `RestorePathsFiltered`). 6. **`docs/adr/0007-module-shell-with-state-in-the-url.md`:** - **:11-12** says "a reload … lost what the user was looking at". The selection was in `sessionStorage` precisely so it survived a reload (`b079daa^:web/src/selection.ts:48-52`). - **:31** says "Element selection stays ephemeral, outside the URL". That has been false since `7696f9a` (`/edit/<lang>/<page>/<uid>`), and the paragraph otherwise reads as current state. 7. **`docs/adr/0001-canonical-round-trip-content-engine.md:20-21`** says "the server is the only canonicalizer". - CLI commands parse and serialize content too (`internal/removecmd/purge.go:170-193`). "never the browser" would be accurate. ### Material omissions - **`docs/harness.md` `## Check`, errors (:254-261):** front matter that does not decode is a content error (`internal/checkcmd/check.go:678-681`) and is not listed. - **`docs/harness.md` `## Check`, warnings (:262-271):** a connected translation whose elements lack a uid or share one (`check.go:578-580`) is not listed. - **ADR 0021** is not linked from `docs/architecture.md` or any other document, although `docs/adr/README.md` says the narrative links each record. The other 21 are linked. - **Glossary and German-file test:** the old architecture.md said shipped sidecar language files follow `web/src/i18n/glossary.md` and that `scaffold/langfiles_test.go` holds each one complete. That fact now appears in no document. ### Minor (no action needed to land) - **`docs/api.md`:** a few rare filesystem 500s are not listed: - `page.readLanguages` and `page.listTranslations` on `media/bundle`, `thumb` and `delete`, and `page.listTranslations` on page `undo` and `discard`. - `error.unexpected` on library `media/delete`. - **`docs/api.md:1686`:** the pending page row `title` is `omitempty`. - **`docs/architecture.md:147`:** "never force-pushed" should be "never a plain `--force`", since the lease push is `--force-with-lease`. - **`docs/adr/0017:35`:** "`lang add` touches no content". It still creates `content/_index.<code>.md`. None of this blocks landing: the documents are far more accurate than on `main`. Items 1–7 are short sentence fixes. Fixing them before merge keeps the ADR records, which are never edited to match newer code, correct from the start.
docs: correct the claims the #274 review found untrue against the code
All checks were successful
ci / build-and-test (pull_request) Successful in 3m3s
ci / release (pull_request) Has been skipped
058f34b023
The lander verdict on #274 named sentences that disagree with the code the
documents describe. Each is corrected against the owning code, and the
behaviour is left as it is:

- architecture.md: the mutation lock also covers the two page reads that
  may persist a uid repair, and a publish takes it itself
  (internal/server/mutation.go); the draft branch is pushed with
  --force-with-lease, never a plain --force (internal/gitrepo/branch.go);
  ADR 0021 is linked from the narrative; the glossary and the complete
  language-file test (scaffold/langfiles_test.go) are stated again.
- ADR 0004: the stored extension follows the re-encoded format, not the
  sniffed type (internal/media/process.go).
- ADR 0003: undo restores the snapshot's blobs, never through the save
  handler (internal/server/git.go RestorePathsFiltered).
- ADR 0007: sessionStorage did survive a reload; element selection moved
  into the URL at 7696f9a.
- ADR 0001: the CLI canonicalizes too (internal/removecmd/purge.go); the
  browser never does.
- ADR 0017: lang add still creates content/_index.<code>.md.
- harness.md: only a changed `requires` rewrites the receipt
  (internal/addcmd/add.go planFill), stated once in features.md and linked;
  remove --json omits `version` on the required-by refusal too
  (internal/removecmd/run.go); check's undecodable front matter error and
  the connected-translation uid warning are listed
  (internal/checkcmd/check.go).
- api.md: the pending page row's `title` is omitempty; the filesystem 500s
  page.readLanguages and page.listTranslations on media/bundle (thumb and
  delete inherit them), page.listTranslations on page undo and discard,
  and error.unexpected on a library media delete.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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!274
No description provided.