feat: machine-owned, versioned sidecars and harness migrate #306

Merged
dominik.polakovics merged 4 commits from afk/302 into main 2026-10-15 13:28:30 +02:00

Sidecars are now machine-owned and versioned per file, and the harness migrates them forward. The schema can change after sites are in production without pinning the harness.

What changed

  • Version chain and canonical form (internal/sidecardir):
    • Each family exports a Versions chain (today {Oldest: 1, Current: 1}, no steps) and a key order.
    • Canonical is the one serializer. It emits the two-line machine-owned header, block style and one spelling per value, and checks itself by decoding its output again.
    • Plan computes every file's canonical bytes at the current version. Load applies the same chain in memory, so the loader and harness migrate share one transform.
    • Element sidecars now go through RequireVersion1, so version: 2 is a load error naming the file.
  • harness migrate [--site] [--json] (internal/migratecmd):
    • Rewrites every sidecar and its language files.
    • Updates a receipt's hash only for a rewritten file whose bytes matched the receipt before the rewrite.
    • Exits 0 whether or not anything changed, and 1 when a file can't be migrated; every other file is still migrated. Needs no Hugo.
    • Emits the documented --json shape (formatVersion: 1).
  • harness check warns, per file, about a sidecar or language file that is outdated or not canonical, and names harness migrate as the fix.
  • Canonical writers:
    • The shipped scaffold, the embedded bundles and the clean-site fixtures are canonical; a drift test (scaffold/canonical_test.go) keeps them so.
    • harness add writes bundle sidecars canonical, --dir bundles included, and hashes the bytes it writes.
    • The page-type children edit (add/remove) now decodes the file, edits it and writes it back canonical, replacing about 500 lines of textual editing. It refuses an outdated sidecar.
    • harness sidecar drafts canonical YAML and prints its review hints instead of # TODO comments.
  • internal/sidecarset is the one owner of which site directory holds which family.
  • Docs:
    • ADR 0026.
    • AGENTS.md gets a "Machine-owned sidecars" section.
    • docs/harness.md gets ## Migrate, ### harness migrate --json, the two check warnings and the receipt rule.
    • docs/features.md gets the hash rule, directory bundles and the children edit.
    • architecture.md and the docs map are updated, and README.md gets one command-table line.
    • The facts that only lived in the dropped comments of the shipped sidecars are now in AGENTS.md or the theme README.
    • Every affected skill's check step names the warnings and the fix.

Behaviour to note

  • A hand-written version: "1" or version: 1.0 still loads, as it did before; migrate writes it as 1.
  • Running harness add or harness remove on a site's page type rewrites that file in canonical form. Its comments are dropped, as the issue specifies.
  • deckle-hosted reads no sidecar and parses harness.yaml leniently, so it needs no change. Running migrate on upgrade is its own issue.

Verification

  • go vet ./..., gofmt and go test ./... are all green; npm test in web/ passes 3317 tests.
  • I ran the issue's "How to verify" script on a built binary:
    • init and migrate report unchanged.
    • After add news and a comment added to cards.yaml, check gives one warning; migrate rewrites that one file and no receipt; check is then clean and remove news succeeds.
    • A version: 99 sidecar gives status: error and exit 1.
    • On a second site, a hand-edited news-list.yaml is still reported customised by remove news after migrate.

Closes #302

🤖 Generated with Claude Code

Sidecars are now machine-owned and versioned per file, and the harness migrates them forward. The schema can change after sites are in production without pinning the harness. ## What changed - **Version chain and canonical form** (`internal/sidecardir`): - Each family exports a `Versions` chain (today `{Oldest: 1, Current: 1}`, no steps) and a key order. - `Canonical` is the one serializer. It emits the two-line machine-owned header, block style and one spelling per value, and checks itself by decoding its output again. - `Plan` computes every file's canonical bytes at the current version. `Load` applies the same chain in memory, so the loader and `harness migrate` share one transform. - Element sidecars now go through `RequireVersion1`, so `version: 2` is a load error naming the file. - **`harness migrate [--site] [--json]`** (`internal/migratecmd`): - Rewrites every sidecar and its language files. - Updates a receipt's hash only for a rewritten file whose bytes matched the receipt before the rewrite. - Exits 0 whether or not anything changed, and 1 when a file can't be migrated; every other file is still migrated. Needs no Hugo. - Emits the documented `--json` shape (`formatVersion: 1`). - **`harness check`** warns, per file, about a sidecar or language file that is outdated or not canonical, and names `harness migrate` as the fix. - **Canonical writers:** - The shipped scaffold, the embedded bundles and the clean-site fixtures are canonical; a drift test (`scaffold/canonical_test.go`) keeps them so. - `harness add` writes bundle sidecars canonical, `--dir` bundles included, and hashes the bytes it writes. - The page-type `children` edit (add/remove) now decodes the file, edits it and writes it back canonical, replacing about 500 lines of textual editing. It refuses an outdated sidecar. - `harness sidecar` drafts canonical YAML and prints its review hints instead of `# TODO` comments. - `internal/sidecarset` is the one owner of which site directory holds which family. - **Docs:** - ADR 0026. - AGENTS.md gets a "Machine-owned sidecars" section. - docs/harness.md gets `## Migrate`, `### harness migrate --json`, the two check warnings and the receipt rule. - docs/features.md gets the hash rule, directory bundles and the children edit. - architecture.md and the docs map are updated, and README.md gets one command-table line. - The facts that only lived in the dropped comments of the shipped sidecars are now in AGENTS.md or the theme README. - Every affected skill's check step names the warnings and the fix. ## Behaviour to note - A hand-written `version: "1"` or `version: 1.0` still loads, as it did before; migrate writes it as `1`. - Running `harness add` or `harness remove` on a site's page type rewrites that file in canonical form. Its comments are dropped, as the issue specifies. - deckle-hosted reads no sidecar and parses `harness.yaml` leniently, so it needs no change. Running `migrate` on upgrade is its own issue. ## Verification - `go vet ./...`, `gofmt` and `go test ./...` are all green; `npm test` in `web/` passes 3317 tests. - I ran the issue's "How to verify" script on a built binary: - init and `migrate` report `unchanged`. - After `add news` and a comment added to `cards.yaml`, `check` gives one warning; `migrate` rewrites that one file and no receipt; `check` is then clean and `remove news` succeeds. - A `version: 99` sidecar gives `status: error` and exit 1. - On a second site, a hand-edited `news-list.yaml` is still reported customised by `remove news` after `migrate`. Closes #302 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Each sidecar family declares its version chain and key order; Load applies
the chain in memory and Plan computes the canonical bytes at the current
version that harness migrate writes. Element sidecars adopt RequireVersion1.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The shipped scaffold, the embedded bundles and the clean-site fixtures are
rewritten canonical, held there by a drift test. harness add writes bundle
sidecars and language files canonical and hashes the bytes written; the
children-list edit works on the raw document and refuses an outdated
sidecar. harness sidecar drafts canonical YAML and prints its review hints.
internal/sidecarset owns which site directory holds which family.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
harness migrate rewrites every sidecar and its language files to the
current version in canonical form through sidecardir.Plan, and updates a
receipt's hash only for a file whose bytes matched it before. harness check
warns about every file migrate would rewrite.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
docs: machine-owned, versioned sidecars and harness migrate
All checks were successful
ci / build-and-test (pull_request) Successful in 3m25s
ci / release (pull_request) Has been skipped
60b2fe6906
ADR 0026; AGENTS.md "Machine-owned sidecars"; features.md receipt hash
rule, directory bundles and the canonical children edit; architecture and
docs map; the facts the shipped sidecars' comments carried moved into
AGENTS.md and the theme README; every skill's check step names the new
warnings and harness migrate.

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

[autoland] verdict: pass

PASS — validated by the lander.

Signal relied on: labctl pr checks 306 aggregate success (ci / build-and-test green; release skipped as usual on a PR). Head 60b2fe6 sits directly on current main (merge-base = main tip 2ce93a7), so there is nothing to resolve.

Conventions: the Conventional Commits title is fine and Closes #302 is present. The diff matches the issue's brief: the version chain and canonical serializer in sidecardir, Load and Plan sharing one chain, element sidecars on RequireVersion1, harness migrate with the documented --json shape and exit codes, the receipt matched-before rule, the check warnings, and canonical writers in init/add/sidecar plus the children edit. The ADR, AGENTS.md, docs/harness.md, docs/features.md, architecture, the docs map, one README line and every listed skill are updated.

Spot checks: I probed whether canonicalizing unquoted scalars (1.50, 01, 0x1F, 1_000, yes, 1e3, dates) changes a loaded Schema. It doesn't, because goccy's typed decode already normalizes them, so a select option or label loads the same before and after migrate. deckle-hosted reads only feature/version from receipts and no sidecar, so migrate's receipt rewrites are safe for it.

Nit, non-blocking: sidecarset.ForPath has no non-test caller yet.

[autoland] verdict: pass PASS — validated by the lander. Signal relied on: `labctl pr checks 306` aggregate **success** (ci / build-and-test green; release skipped as usual on a PR). Head 60b2fe6 sits directly on current main (merge-base = main tip 2ce93a7), so there is nothing to resolve. Conventions: the Conventional Commits title is fine and `Closes #302` is present. The diff matches the issue's brief: the version chain and canonical serializer in sidecardir, Load and Plan sharing one chain, element sidecars on RequireVersion1, `harness migrate` with the documented --json shape and exit codes, the receipt matched-before rule, the check warnings, and canonical writers in init/add/sidecar plus the children edit. The ADR, AGENTS.md, docs/harness.md, docs/features.md, architecture, the docs map, one README line and every listed skill are updated. Spot checks: I probed whether canonicalizing unquoted scalars (1.50, 01, 0x1F, 1_000, yes, 1e3, dates) changes a loaded Schema. It doesn't, because goccy's typed decode already normalizes them, so a select option or label loads the same before and after migrate. deckle-hosted reads only feature/version from receipts and no sidecar, so migrate's receipt rewrites are safe for it. Nit, non-blocking: `sidecarset.ForPath` has no non-test caller yet.
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!306
No description provided.