refactor: one owner per rule — chip grammar, defaults, links, params, names, loaders and client fixtures #256

Merged
dominik.polakovics merged 1 commit from afk/244 into main 2026-10-13 02:49:07 +02:00

Closes #244

What

Every rule drift audit #234 (items 3, 4, 6–14 and the node lookup) found evaluated twice now has exactly one owner. Every rule the web client or a theme partial re-evaluates is pinned by a JSON fixture under the owning Go package's testdata/, loaded by both go test and vitest. The vitest side uses one shared loader, web/src/test/goFixture.ts; the old pagepath fixture test uses it too.

Rule Owner Client / theme pin
chip grammar, typed values content serializer content/testdata/chip_cases.json (62 cases)
value spells its empty value element.Param.SpellsEmpty(v, keepRequired) –
placement (3 axes + .inline) elemops.PlacementMsg, content.InsertChild elemops/testdata/placement_cases.json
link destinations, classification, link-bearing params linkrefs.Destinations / Classify / SplitTarget linkrefs/testdata/link_type_cases.json (linkTypeOf + rendered ce/href.html)
front-matter reads, l10n mode content.MetaLookup / MetaText / MetaBool, l10n constants –
shortcode params content.Shortcode.Param / SetParam / DeleteParam (copy-on-write) –
disabled flag elemops.ShortcodeDisabled elemops/testdata/disabled_cases.json (disabledByParams + rendered ce/disabled.html)
language tag pagepath.ValidLangTag –
sidecar names / param names sidecardir.ValidName / CheckName, content.ValidParamName –
sidecar version, name lists sidecardir.RequireVersion1 / ParseNameList –
slug, child placement, category path, rte tokens, canonical anchor pageops, categories, element, elemops one fixture each
page-tree node lookup pagetree.Node.FindByPath –
default pretty URL pagetree.PagePermalink(p, "", ""); pageops.PageURL is removed –
front-matter key grammar (pagetype defaults) content.ValidKey –

Fixes carried by the move

  • Chip floats: a chip whose float param reads 2.0 re-serializes as 2.0. The client keeps a floats list on the chip node. A newline or CR in a chip value is refused by the client with the server's message.
    • This holds for params the element does not declare. A param declared number is still normalised by the server's save (2.0 → 2), as before and as AGENTS.md documents.
  • Publish vs. link resolve: publish's draft-link check now matches by Node.Permalink (or the content path), the same match resolve makes. TestPublishAllCheckAgreesWithLinkResolve covers a page with url: and a page with slug:.
  • Sidecar names: harness sidecar uses the loader's grammar, so MyBlock_2 is accepted and -foo is refused with the loader's message.
    • A bare harness sidecar -bad is still refused (exit 2), but by Go's flag parser as an unknown flag. With harness sidecar -- -bad it gets the loader's message.
  • Snapshot language tags: the snapshot-subject parser no longer reads [EN] … as a language.

Other behaviour changes (all toward agreement)

  • Bool params with no default: MergeParams and the hash share SpellsEmpty, which compares against Empty(). So an unheld bool param with no default: is no longer written as x=false on an unchanged save; this is the #219 symptom for that type.
    • No shipped sidecar has such a bool. AGENTS.md now says so.
    • The hash is byte-identical for every scaffold schema: 2070 spellings were compared against the old function.
  • Links inside string values: linkrefs now also sees and rewrites links written inside string params and front-matter values, for example a rich-text param. It also allows whitespace after ](. Publish already saw these links through its raw regex scan, and the rewrite engine must never miss a link publish sees.
  • Query strings in links: link resolve strips ?query before the page lookup, as the theme and publish do.

Docs and skills

  • scaffold/site/AGENTS.md gains:
    • container: true ⇒ paired tags, in a new Containers paragraph;
    • the accepted upload types, and the 2560px resize with EXIF/GPS stripping, in Image values and uploads;
    • the element-name grammar, stated once;
    • the bool-without-default rule;
    • links inside values being rewritten.
  • The edit-content, migrate-site, add-element and remove-element skills point at those sections instead of restating them.
  • The README.md publish link-check paragraph is updated.
  • themes/deckle/README.md is unchanged: no partial's contract changed.

Verification

  • go vet ./... and go test ./... pass with Hugo 0.167.0 on PATH, so the theme render tests ran rather than skipped.
  • cd web && npm run build && npm test passes: 120 files, 3117 tests. tsc --noEmit is clean.
  • harness init into a temp site, then harness check, reports no problems. harness sidecar MyBlock_2 writes the sidecar.
  • Assumptions from the issue, checked:
    • Hugo types 2 as int and 2.0 as float (round trip through content.Parse).
    • No element name in deckle-hosted or in the scaffold/testdata sites changes validity under the loader grammar.
    • The required exemption is kept as #219 intended; the mutation tests fail without it.

Follow-ups noticed, out of scope

  • Rename/move misses links on custom slugs: pageops/relocate.go builds rename pairs from the default pretty URL, so moving a page with slug: misses links that store its permalink. This is the same bug class as the publish fix.
  • Duplicates not on this issue's list:
    • the Hugo config-file list in pagetree/hugopath.go and langcmd;
    • looksLikeHugoSite in cmd/harness and checkcmd;
    • the folder-name check in MediaModule.tsx.
  • Chip number edge cases, all predating this change: ints above 2^53 are typed as strings on the client, and integral numbers of 1e21 or more without the float flag are written with an exponent.
  • Unused schema field: element.Uses.PageRef no longer has a non-test reader.
  • Canonical anchor and javascript:: the canonical-anchor gate is shape-only and admits a javascript: href. Go and the client agree on this, and the fixture records it explicitly. It is worth a security look on its own.

🤖 Generated with Claude Code

Closes #244 ## What Every rule drift audit #234 (items 3, 4, 6–14 and the node lookup) found evaluated twice now has exactly one owner. Every rule the web client or a theme partial re-evaluates is pinned by a JSON fixture under the owning Go package's `testdata/`, loaded by both `go test` and vitest. The vitest side uses one shared loader, `web/src/test/goFixture.ts`; the old pagepath fixture test uses it too. | Rule | Owner | Client / theme pin | |---|---|---| | chip grammar, typed values | `content` serializer | `content/testdata/chip_cases.json` (62 cases) | | value spells its empty value | `element.Param.SpellsEmpty(v, keepRequired)` | – | | placement (3 axes + `.inline`) | `elemops.PlacementMsg`, `content.InsertChild` | `elemops/testdata/placement_cases.json` | | link destinations, classification, link-bearing params | `linkrefs.Destinations` / `Classify` / `SplitTarget` | `linkrefs/testdata/link_type_cases.json` (`linkTypeOf` + rendered `ce/href.html`) | | front-matter reads, l10n mode | `content.MetaLookup` / `MetaText` / `MetaBool`, `l10n` constants | – | | shortcode params | `content.Shortcode.Param` / `SetParam` / `DeleteParam` (copy-on-write) | – | | disabled flag | `elemops.ShortcodeDisabled` | `elemops/testdata/disabled_cases.json` (`disabledByParams` + rendered `ce/disabled.html`) | | language tag | `pagepath.ValidLangTag` | – | | sidecar names / param names | `sidecardir.ValidName` / `CheckName`, `content.ValidParamName` | – | | sidecar version, name lists | `sidecardir.RequireVersion1` / `ParseNameList` | – | | slug, child placement, category path, rte tokens, canonical anchor | `pageops`, `categories`, `element`, `elemops` | one fixture each | | page-tree node lookup | `pagetree.Node.FindByPath` | – | | default pretty URL | `pagetree.PagePermalink(p, "", "")`; `pageops.PageURL` is removed | – | | front-matter key grammar (pagetype defaults) | `content.ValidKey` | – | ## Fixes carried by the move - **Chip floats:** a chip whose float param reads `2.0` re-serializes as `2.0`. The client keeps a `floats` list on the chip node. A newline or CR in a chip value is refused by the client with the server's message. - This holds for params the element does not declare. A param declared `number` is still normalised by the server's save (`2.0` → `2`), as before and as AGENTS.md documents. - **Publish vs. link resolve:** publish's draft-link check now matches by `Node.Permalink` (or the content path), the same match resolve makes. `TestPublishAllCheckAgreesWithLinkResolve` covers a page with `url:` and a page with `slug:`. - **Sidecar names:** `harness sidecar` uses the loader's grammar, so `MyBlock_2` is accepted and `-foo` is refused with the loader's message. - A bare `harness sidecar -bad` is still refused (exit 2), but by Go's flag parser as an unknown flag. With `harness sidecar -- -bad` it gets the loader's message. - **Snapshot language tags:** the snapshot-subject parser no longer reads `[EN] …` as a language. ## Other behaviour changes (all toward agreement) - **Bool params with no default:** `MergeParams` and the hash share `SpellsEmpty`, which compares against `Empty()`. So an unheld `bool` param with no `default:` is no longer written as `x=false` on an unchanged save; this is the #219 symptom for that type. - No shipped sidecar has such a bool. AGENTS.md now says so. - The hash is byte-identical for every scaffold schema: 2070 spellings were compared against the old function. - **Links inside string values:** `linkrefs` now also sees and rewrites links written inside string params and front-matter values, for example a rich-text param. It also allows whitespace after `](`. Publish already saw these links through its raw regex scan, and the rewrite engine must never miss a link publish sees. - **Query strings in links:** link resolve strips `?query` before the page lookup, as the theme and publish do. ## Docs and skills - `scaffold/site/AGENTS.md` gains: - `container: true` ⇒ paired tags, in a new **Containers** paragraph; - the accepted upload types, and the 2560px resize with EXIF/GPS stripping, in **Image values and uploads**; - the element-name grammar, stated once; - the bool-without-default rule; - links inside values being rewritten. - The `edit-content`, `migrate-site`, `add-element` and `remove-element` skills point at those sections instead of restating them. - The `README.md` publish link-check paragraph is updated. - `themes/deckle/README.md` is unchanged: no partial's contract changed. ## Verification - `go vet ./...` and `go test ./...` pass with Hugo 0.167.0 on PATH, so the theme render tests ran rather than skipped. - `cd web && npm run build && npm test` passes: 120 files, 3117 tests. `tsc --noEmit` is clean. - `harness init` into a temp site, then `harness check`, reports no problems. `harness sidecar MyBlock_2` writes the sidecar. - Assumptions from the issue, checked: - Hugo types `2` as int and `2.0` as float (round trip through `content.Parse`). - No element name in `deckle-hosted` or in the scaffold/testdata sites changes validity under the loader grammar. - The `required` exemption is kept as #219 intended; the mutation tests fail without it. ## Follow-ups noticed, out of scope - **Rename/move misses links on custom slugs:** `pageops/relocate.go` builds rename pairs from the default pretty URL, so moving a page with `slug:` misses links that store its permalink. This is the same bug class as the publish fix. - **Duplicates not on this issue's list:** - the Hugo config-file list in `pagetree/hugopath.go` and `langcmd`; - `looksLikeHugoSite` in `cmd/harness` and `checkcmd`; - the folder-name check in `MediaModule.tsx`. - **Chip number edge cases**, all predating this change: ints above 2^53 are typed as strings on the client, and integral numbers of 1e21 or more without the float flag are written with an exponent. - **Unused schema field:** `element.Uses.PageRef` no longer has a non-test reader. - **Canonical anchor and `javascript:`:** the canonical-anchor gate is shape-only and admits a `javascript:` href. Go and the client agree on this, and the fixture records it explicitly. It is worth a security look on its own. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
refactor: one owner per rule for chips, defaults, links, params, names and loaders
All checks were successful
ci / build-and-test (pull_request) Successful in 3m8s
ci / release (pull_request) Has been skipped
68857e13f3
Every rule drift audit #234 (items 3, 4, 6-14) found evaluated twice now
has one owner, and each rule the web client or a theme partial re-evaluates
is pinned by a JSON fixture under the owning package's testdata/, loaded by
go test and by vitest (shared helper web/src/test/goFixture.ts).

Owners:
- chip grammar: content serializer; testdata/chip_cases.json pins chip.ts
- spells-empty: element.Param.SpellsEmpty(v, keepRequired), called by
  MergeParams and the l10n hash (hash output unchanged for every scaffold
  sidecar)
- placement: insertRefusal is the only client predicate, pinned against
  PlacementMsg + content.InsertChild
- links: linkrefs.Destinations / Classify / SplitTarget; publish has no
  regex of its own and keys draft targets by Node.Permalink;
  elemops.LinkParamValues removed; pageops's default URL is
  pagetree.PagePermalink
- front matter: content.MetaLookup/MetaText/MetaBool, one set of l10n mode
  constants, langcmd uses l10n.FrontMatterChanges
- shortcode params: content.Shortcode Param/SetParam/DeleteParam,
  copy-on-write
- disabled flag: elemops.ShortcodeDisabled, pinned for the client and
  ce/disabled.html
- language tag: pagepath.ValidLangTag
- sidecar names: sidecardir.ValidName/CheckName; param names:
  content.ValidParamName (sidecars keep the ASCII subset)
- loader helpers: sidecardir.RequireVersion1/ParseNameList
- slug, child placement, category path, rte tokens, canonical anchor:
  fixtures on both sides
- page-tree node lookup: pagetree.Node.FindByPath

Fixes carried by the move:
- a chip float param `2.0` re-serializes as `2.0`; a newline in a chip
  value is refused by the client with the server's message
- publish's draft-link check and link resolve agree on pages with a custom
  url or slug
- `harness sidecar` accepts exactly the names the loader accepts
- the snapshot-subject parser no longer reads `[EN]` as a language

AGENTS.md gains container paired tags, accepted upload types and the
resize/strip rule, and the element name grammar; edit-content,
migrate-site, add-element and remove-element point at it instead of
restating facts.

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

[autoland] verdict: pass

PASS — validated against #244. Signal: CI aggregate success (ci / build-and-test: npm build+test, go vet, go test with Hugo 0.167.0, harness build). Conventional title, Closes #244 present, branch current with main (merge-base = main HEAD 8fffc2f). Diff matches the issue's owners table and acceptance criteria: one owner per rule, every client/theme mirror pinned by a Go-owned JSON fixture loaded by both suites, AGENTS.md + edit-content/migrate-site/add-element/remove-element skills updated, upload facts in AGENTS.md verified against internal/media. Non-blocking note: publish's draft-link check now matches exact permalinks only (internal/server/publish.go ~L850), so a non-canonical hand-written link (no trailing slash, or relative) to a draft page is no longer warned about; that follows from the issue's 'match as resolve does' decision.

[autoland] verdict: pass PASS — validated against #244. Signal: CI aggregate success (ci / build-and-test: npm build+test, go vet, go test with Hugo 0.167.0, harness build). Conventional title, Closes #244 present, branch current with main (merge-base = main HEAD 8fffc2f). Diff matches the issue's owners table and acceptance criteria: one owner per rule, every client/theme mirror pinned by a Go-owned JSON fixture loaded by both suites, AGENTS.md + edit-content/migrate-site/add-element/remove-element skills updated, upload facts in AGENTS.md verified against internal/media. Non-blocking note: publish's draft-link check now matches exact permalinks only (internal/server/publish.go ~L850), so a non-canonical hand-written link (no trailing slash, or relative) to a draft page is no longer warned about; that follows from the issue's 'match as resolve does' decision.
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!256
No description provided.