Sidecar versioning: machine-owned canonical sidecars, harness migrate, in-memory migration chain #302
Labels
No labels
bug
enhancement
needs-triage
ready-for-agent
ready-for-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/deckle#302
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Sidecars become machine-owned, versioned files that the harness migrates forward: a
harness migratecommand rewrites every sidecar and its language files to the current version in one canonical form,harness checkwarns about files that need it, and the loaders apply the same migration in memory so an unmigrated site always opens. This is what lets the sidecar schema change after sites are in production without pinning the harness.Current behavior
version. Page types, backend layouts and editor modules pin it to exactly 1 throughsidecardir.RequireVersion1; element sidecars (internal/element/load.go) accept any integer>= 1and ignore it. Every shipped sidecar is version 1.scaffold/editor/elements/*.yaml, the bundles). The sidecar drafter (internal/sidecar) writes a# NOTEcomment in one case.<name>.<code>.yaml, "Sidecar language files" in AGENTS.md) carry no version; they mirror the sidecar's keys and are read only after their sidecar loaded.editor/features/<name>.yaml, docs/harness.md "The receipt") records the SHA-256 of every vendored file as written byharness add. Nothing ever re-derives it; a file whose bytes differ counts as customised, which makes the feature "in use" and blocksharness removebehind--purge.harness checkalready warns about a content file that is not canonical; there is no equivalent for sidecars.harness servedirectly on a site's worktree and never runsharness checkbefore opening the editor. It parsesharness.yamlandfeature.yaml, never sidecars.Desired behavior
Versions. Each sidecar family has a current version the harness knows. A file at the current version loads as today. A file at an older supported version loads through the migration chain in memory, in every loader (
harness serve,harness check,harness add, …), so no release ever locks a site out of its editor. A file above the current version, or below the oldest supported one, is a load error naming the file and the version.Canonical form. The harness owns the sidecar's serialization the way the content engine owns the content format (ADR 0001): fixed key order, one spelling per value, no comments, and a header comment the serializer itself emits saying the file is machine-owned by the harness and that comments do not survive. Every sidecar the harness writes (
harness init,harness add,harness sidecar,harness migrate) is canonical. The shipped scaffold and the embedded bundles are canonical and at the current version, enforced by a drift test.harness migrate [--site <dir>] [--json]. Rewrites every sidecar of every family (elements, page types, backend layouts, editor modules) and each sidecar's language files to the current version in canonical form; a file already canonical and current is not touched. For every rewritten file listed in a feature receipt whose bytes matched the receipt's hash before the rewrite, the receipt's hash is updated; a file that did not match is left as customised. Exit 0 whether or not anything changed; exit 1 when a file cannot be migrated (it does not load even through the chain, or cannot be written), with every other file still migrated. Runs without Hugo, likeharness add, so a host can run it in a clean clone.--jsonfollows the add command's conventions:{"formatVersion": 1, "status": "migrated" | "unchanged", "files": [paths rewritten], "receipts": [receipt paths rewritten]}, and{"formatVersion": 1, "status": "error", "error": "…", "files": […]}on exit 1.harness check. Warns, per file, about a sidecar or language file below the current version or not in canonical form, namingharness migrateas the fix. Both are warnings: the file still loads.Loader and chain. One chain of per-version transforms per family, each a pure function from the raw document of version N and its language files to version N+1. The loader applies it in memory and
harness migrateapplies it to disk; nothing else reads an old version. Today the chain holds no transform (there is no version 2); the structure, its tests and the two call sites are the deliverable, with the canonicalization of a version 1 file as the first real rewrite.Directory bundles.
harness add --dirmigrates a bundle's sidecars and language files while copying them, and the receipt records the bytes written.Decisions
sidecardir.RequireVersion1, the helper the other three families use — one owner of the rule, and a site must never hold a meaningless version 2 before version 2 has a meaning.migrateon disk) — "Every rule has exactly one owner" (CLAUDE.md); a loader that read old versions its own way would be a second copy.--purgeto remove after one migration, and the gap fill never overwrites a file, so the hash would never catch up.harness migrateper site on each harness upgrade, commits to main and reconciles the editor branch, through the path a feature add already uses; that is a deckle-hosted issue. The--jsonshape here is the cross-repo contract it consumes, henceformatVersion.Key interfaces
sidecardir.Loadandsidecardir.Config[T](internal/sidecardir) — the single entry for the four families; the version check, the in-memory chain and the canonical serializer belong beside it.sidecardir.RequireVersion1— element loading (internal/element/load.go,loadOne) adopts it.harness init,harness add,harness sidecarandharness migrate; the drafter's# NOTEcomment moves to stderr or theharness sidecaroutput text.Receipt(internal/addcmd/receipt.go) — the migrate command updatesfiles[].sha256under the matched-before rule.cmd/harness—migrateinusage(); the command test tiesusage()to docs/harness.md.internal/checkcmd— the two new warnings.internal/sidecardir,internal/element/load.go,internal/addcmd,internal/checkcmd,internal/sidecar/generate.go,cmd/harness.Documentation and skills (same change)
docs/harness.md:migratein the command table, a## Migratesection, the two warnings under "Check", a### harness migrate --jsonsection, the receipt interaction under "The receipt".docs/features.md: the receipt hash rule under "Removing" and "Filling gaps", migration of directory bundles under "Bundles from a directory".scaffold/site/AGENTS.md: the machine-owned header and what it means for hand edits, the meaning ofversion(bump only on shape change), language files bound to the sidecar's version,harness migrateunder the command references.README.md: one line in the command table, nothing else.docs/agents/docs-map.md: the new area.harness checkgains the warning and its fix; at leastadd-element,add-page-type,add-backend-layout,create-feature(bundle sidecars must be canonical),migrate-site,verify-site,setup-site.scaffold/skills_test.gochecks thatmigrateis a real command.Acceptance criteria
version: 2fails to load with an error naming the file, the same way a page type does today.harness migrate --site .on a freshharness initsite rewrites nothing and reportsunchanged; on a site whose sidecars carry comments or non-canonical key order it rewrites exactly those files, each resulting file loads identically to the original (sameSchemavalue), and a second run reportsunchanged.harness add newsandharness migrate,harness remove newson an otherwise untouched site is not blocked as customised; a vendored file edited by hand before the migrate is still reported customised afterwards.harness checkwarns per outdated or non-canonical sidecar and language file, exit 0, and the warning namesharness migrate; afterharness migratethe warnings are gone.harness serveopens a site whose sidecars are non-canonical and reports no schema error for them.harness migrate --jsonemits exactly the documented object onmigrated,unchangedanderror, and exits 0, 0 and 1.harness migrateshare the chain: a test-only transform registered in the chain changes what the loader reads and what the command writes identically.harness init,harness addandharness sidecarwrite is canonical; a drift test fails when a scaffold or embedded bundle sidecar or language file is not canonical or not at the current version.harness add --dirwith a bundle holding a non-canonical sidecar writes the canonical bytes and records their hash.docs/docs_test.go, the command test incmd/harness,internal/element/agentsdoc_test.goandscaffold/skills_test.gopass.go test ./...andnpm testinweb/pass.How to verify
go test ./...;cd web && npm test.harness init,harness add news, add a comment toeditor/elements/cards.yaml, runharness check(warning),harness migrate(one file, one receipt untouched since cards is not a bundle file),harness check(clean),harness remove news(not blocked).editor/elements/news-list.yamlby hand,harness migrate,harness remove newsreports it customised.Assumptions to check
versionabove 1; the shipped ones are all 1, production sites cannot be checked from the repository.sidecardir.Loadis the only entry through which the four families are read, including byharness check,harness addandharness remove.harness.yamlleniently, so dropping comments and reordering keys there needs no hosted change; confirm againstdeckle-hosted/harness/pin.go.Out of scope
Blocked by
None - can start immediately