one owner per rule: chip grammar, defaults, links, params, names, loaders and client fixtures (drift audit 3, 4, 6-14) #244

Closed
opened 2026-10-12 22:48:10 +02:00 by dominik.polakovics · 0 comments

This was generated by AI during triage.

Parent

#234 (drift audit), items 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14 and the lower-priority node lookup.

Summary

Every remaining rule the audit found evaluated twice gets exactly one owner, and every rule the web client or a theme partial still needs is pinned by a fixture both suites load. Four of the copies already disagree in user-visible ways and are fixed as part of the move: a float chip param 2.0 is rewritten as 2, publish's draft-link check keys pages by a different URL than link resolve, harness sidecar and the loader accept different element names, and the git snapshot subject parser accepts uppercase language tags.

Current behavior

Confirmed at dcc1067. Each item: the copies, whether they disagree, and the owner this change gives them.

3. Inline chip grammar (client vs engine). web/src/rte/chip.ts (CHIP_START_RE, PARAM_RE, typedParamValue, chipParamText) re-implements internal/content/serialize.go (paramValueString, quotedSafe, validShortcodeName, validParamName) by hand. Disagreements: typedParamValue turns 2.0 into the JS number 2, chipParamText writes 2, Go re-lexes it as int64 and the file changes on a no-op edit (Go's serializer deliberately keeps .0; web/src/test/rte/markdown.test.ts covers only 1.5). The client never refuses \n/\r in a value, Go does. Names are ASCII-only on the client and Unicode in Go. chipParamText's backtick fallback is a hand copy of quotedSafe. No shared fixture.

4. "Value spells its default". elemops/save.go spellsDefault (skips required params, compares ParamValueString of the value against Default) and l10n/hash.go canonicalParam (includes required params, Type.Read first, compares against Empty()). spellsDefault's doc comment claims they agree; they differ on required params and on a type whose Empty() is not its Default (a bool with no default). Harmless for the hash today; the comment is a "same as X" marker.

6. Placement. Go: elemops/placement.go PlacementMsg (three axes) plus the .inline-parent rule in content/mutate.go checkInsert. Client: elementcrud.ts has the three axes twice (placementAllowed, insertRefusal) and the .inline rule twice (insertRefusal, classifyDrop). Logic agrees today; no fixture.

7. Link destinations. Scanning: linkrefs/linkrefs.go (nextAnchor, forEachDestination) vs server/publish.go (markdownLinkRE, hrefLinkRE, normalizeLinkTarget). They disagree on whitespace after ](, on the angle-bracket form, and on a href without a closing quote on the line. Which params are links: linkrefs scans every string param and all front matter but aliases; publish uses elemops.LinkParamValues (link/page-typed params only) and never page-typed front matter. Classification: server/linktargets.go externalSchemeRE, web/src/components/SetLinkForm.tsx linkTypeOf and the theme partial ce/href.html agree on the scheme rule; normalizeLinkTarget has its own short scheme list. Extra, user-visible: publish keys draft-link targets by pageops.PageURL (the default pretty URL) while link resolve matches Node.Permalink (honours url/slug), so a page with a custom url or slug gets a different answer from the two. No test ties any pair.

8. Front-matter reads. pagetree/build.go lookup is content.MetaLookup again. Draft is re-parsed in pageops/settings.go and pageops/translation_settings.go; url/slug in pageops/translation_addr.go metaStr. L10n mode constants are defined in both pagetree and l10n, read two different ways (MetaString vs cast.ToStringE). langcmd/content.go hand-builds the three Set calls l10n.FrontMatterChanges provides, and its metaString is unused. All agree today.

9. Shortcode params. content.Shortcode has no accessor. Getters in l10n/l10n.go, elemops/uid.go, elemops/summary.go, elemops/slots.go, elemops/body.go, server/forms.go, server/pending.go, server/element_transfer.go, mediaops/scan.go; setters in l10n/l10n.go, elemops/uid.go, elemops/save.go; the same .Name < sort in eight places. setParam copies on insert but overwrites in place on update; SetShortcodeEnabled filters in place (sc.Params[:0]); SetShortcodeUID/setShortcodeDisabled append and re-sort in place. No caller shares a backing array today, so the hazard is latent.

10. Disabled flag. server/forms.go shortcodeDisabled, web/src/api/content.ts disabledByParams, scaffold/themes/deckle/layouts/partials/ce/disabled.html all accept true or "true". Agree; no exported Go reader, no fixture.

11. Language tag. ^[a-z]{2,3}(-[a-zA-Z0-9]+)*$ in siteconfig, langcmd and pagepath (each with a "copied, keep in step" comment). server/git.go langTagRE accepts [A-Za-z]{2,3}, so a hand-written [EN] … commit subject is read as a language.

12. Name grammars. Element names: sidecar/sidecar.go (the harness sidecar generator) ^[a-z0-9-]+$ vs sidecardir (the loader) ^[A-Za-z0-9_][A-Za-z0-9_-]*$, copied again in addcmd/children.go. They disagree both ways: the generator refuses uppercase and _ the loader accepts, and accepts a leading - the loader refuses. The add-element skill documents the loader's grammar. Param names: element/load.go ASCII regex vs content/serialize.go Unicode predicate; the element rule is the ASCII subset.

13. Loader helpers and client mirrors. requireVersion1 and parseNameList are byte-identical in pagetype/load.go and backendlayout/load.go; editormodule/load.go inlines the version check; element/load.go parseStringList is the same body on any. Client mirrors with no fixture, all agreeing today: pagemanage.ts isValidSlug (pageops.ValidSlug), mayPlaceUnder (pageops checkChildAllowed), categorypath.ts (categories.SlugOf), rte/allow.ts token list (element/schema.go rteAllowTokens), rte/markdown.ts TARGET_LINK_OPEN_RE (elemops/body.go canonicalAnchorOpenRE, the security gate).

14. Skill prose. scaffold/site/.claude/skills/edit-content/SKILL.md ("Rules, each one enforced by the engine or the theme") restates the rte token list, the paired/self-closing rule, the link grammar and the image rules. Three of its facts are not in AGENTS.md at all: container: true implies paired tags (elemops.BlockForSchema), the accepted image types (JPEG, PNG, WebP, AVIF; PDF for links), and the 2560px long-edge resize with EXIF/GPS stripping. migrate-site/SKILL.md repeats the image facts.

Lower priority. Page-tree node lookup in pageops/tree.go findNode, server/modules.go findNode (skips synthesized nodes) and server/linktargets.go findNodeByPath (does not). Only differs for an empty path, which no caller passes. Node.FindByPermalink in pagetree already applies the skip rule.

Desired behavior

  • Each rule above has one owner, named in the table below, and every former copy calls it. No comment anywhere says a function mirrors, matches, agrees with or must stay in step with another.
  • A rule the client or a theme partial needs is pinned by one JSON fixture under the owning Go package's testdata/, loaded by go test and by vitest (and, for the theme, by a Go test that renders the partial, as internal/theme/disabled_test.go does). The internal/pagepath/testdata/effective_type_cases.json pattern is the model; its loading recipe on the vitest side (the import.meta.url path trick and why) becomes one shared helper in web/src/test/ so later fixtures do not copy it.
  • Behaviour fixes carried by the move:
    • A chip whose float param reads 2.0 re-serializes as 2.0; a no-op chip edit leaves the file byte-identical for every typed value in the fixture. A value containing a newline is refused by the client with the same error the server gives.
    • Publish's draft-link check and link resolve agree on a page's URL, including a page with a custom url or slug.
    • harness sidecar <name> accepts exactly the names the loader accepts.
    • The snapshot-subject parser accepts exactly the language tags pagepath accepts.
  • The hash and the save path spell a param's emptiness with the same function. Required params are always written by save and are included in the hash as they are today; the shared function takes the "required" rule as an explicit input rather than each caller deciding.
  • scaffold/site/AGENTS.md gains the three missing facts (paired tags for container: true, accepted image types, the resize/strip rule), each in the section where the related facts already live. The edit-content and migrate-site skills point at those sections and restate nothing.

Decisions

  • Fixture over verdict for client mirrors here: these rules run on the client before any request (slug validity while typing, chip parsing in the editor, drop classification), so a served verdict is not available; a fixture is the CLAUDE.md-sanctioned form.
  • Save path's rule wins for required params holding their default: required params are always written (the fix for #219 established that save never adds a param only to spell a default, and never drops a required one). The hash keeps including required params; the shared SpellsEmpty-style function compares against Empty() and the callers state whether required params are exempt. Reason: the hash difference is harmless and changing it would re-flag translations.
  • Loader's grammar is the element-name grammar: it is what the add-element skill documents and what every existing site has loaded under. The generator and addcmd call sidecardir's exported validator.
  • pagepath owns the language-tag grammar (it is already the stdlib-only leaf every other package can import). siteconfig, langcmd and server/git.go call it; the git parser's uppercase acceptance is dropped.
  • linkrefs owns destination scanning and classification: it exports a destination iterator over a raw document and a classifier (external, page, anchor, media, as linkTypeOf distinguishes today). Publish scans through it (and so also sees page-typed front matter and all the params linkrefs sees; make the set of link-bearing params one function, elemops.LinkParamValues or linkrefs's, not both). Publish keys targets by the same permalink resolve uses. Reason: the picker's classifier and the publish scanner are the two user-facing readers; linkrefs is the rewrite engine and must never miss a link the others see.
  • content.Shortcode gets Param, SetParam (keeps params sorted), DeleteParam, documented as never aliasing a caller's slice (copy-on-write). Every getter/setter/sort listed above goes through them; content/serialize.go's sorted-order check stays as the invariant.
  • Disabled reader: one exported elemops function; the client and the theme partial are pinned by a fixture of accepted and rejected spellings.
  • Placement: insertRefusal is the only client copy (placementAllowed and classifyDrop's .inline check call it or go away), pinned by a fixture against PlacementMsg plus checkInsert.
  • Loader helpers move to one package both loaders import (sidecardir is the natural home); parseStringList's "absent vs explicit null" distinction is kept by the callers that need it and documented at the helper.
  • Node lookup: one function in pagetree next to FindByPermalink, with the synthesized-node skip; the three callers use it.
  • Skill prose: facts move to AGENTS.md, skills point. Reason: CLAUDE.md's skills rule; the drift test cannot catch prose.

Rules touched and their owners (CLAUDE.md)

Rule Owner Client / theme
chip grammar and typed values content serializer fixture
value spells its empty element.Param –
placement (three axes, .inline) elemops.PlacementMsg, content.checkInsert fixture
link destinations, link classification, link-bearing params linkrefs, elemops.LinkParamValues fixture for linkTypeOf and ce/href.html
front-matter reads, l10n mode content.MetaLookup, l10n –
shortcode params content.Shortcode methods –
disabled flag elemops fixture for disabledByParams and ce/disabled.html
language tag pagepath –
element / sidecar names sidecardir –
param names content –
sidecar version and name lists sidecardir –
slug, child placement, category path, rte tokens, canonical anchor pageops, categories, element, elemops fixture each
page-tree node lookup pagetree –

Affected docs and skills

  • scaffold/site/AGENTS.md: sidecar schema section (container and paired tags), "Editing content by hand" (link grammar already there; add image types and resize/strip near "Library media publishes with the pages that use it"), element naming grammar stated once.
  • scaffold/site/.claude/skills/edit-content/SKILL.md and migrate-site/SKILL.md: replace restated facts with pointers.
  • scaffold/site/.claude/skills/add-element/SKILL.md: the name rule points at AGENTS.md.
  • themes/deckle/README.md only if the ce/href.html or ce/disabled.html contract wording changes (it should not).
  • internal/element/agentsdoc_test.go and scaffold/skills_test.go stay green.

Acceptance criteria

  • grep -rniE "same as|mirrors|keep (the two|them|in step)|identical to|agrees with" internal web/src finds no comment naming a second copy of a rule in this issue's list.
  • A chip fixture of at least: int, float with zero fraction, float with fraction, negative, bool, quoted string with " and backtick, Unicode param name, value with newline (refused) — passes on both sides; 2.0 round-trips byte-identically through a client chip edit (vitest) and through the Go serializer.
  • One element.Param method decides "spells its empty value" and both MergeParams and the l10n hash call it; TestHashSpelledOutDefaultIsNoChange and TestMergeParams* still pass; a new test asserts the two callers agree on every param of every scaffold sidecar.
  • web/src/elementcrud.ts has one placement predicate, fixtured against Go.
  • linkrefs exports destination scanning and classification; server/publish.go has no link regex of its own; a test shows publish and link resolve agree on a page with a custom url and one with a custom slug; linkTypeOf and ce/href.html are fixtured against the classifier.
  • content.Shortcode has Param/SetParam/DeleteParam; no other package indexes sc.Params by name or sorts them; a test asserts the setters never write into a caller's backing array.
  • One exported disabled reader; disabledByParams and ce/disabled.html fixtured against it.
  • One language-tag regex in pagepath; siteconfig, langcmd and server/git.go call it; a test shows [EN] subject is not parsed as a language.
  • harness sidecar and harness add validate names with sidecardir's exported rule; the sidecar generator accepts MyBlock_2 and refuses -foo, matching the loader.
  • requireVersion1/parseNameList/the inline version check exist once; pagetype, backendlayout, editormodule and element loaders call it.
  • Fixtures exist and load on both sides for slug validity, child placement, category path, rte tokens and the canonical anchor regex.
  • pagetree exports the node lookup; pageops/tree.go, server/modules.go, server/linktargets.go use it.
  • langcmd/content.go uses l10n.FrontMatterChanges; its unused metaString is gone; pagetree/build.go uses content.MetaLookup; one set of l10n mode constants.
  • AGENTS.md holds the three missing facts; edit-content, migrate-site and add-element skills restate none of them; drift tests green.

How to verify

  • go vet ./... && go test ./...; cd web && npm test (fixture tests included); harness check --site scaffold/site.
  • Editor: open an element whose chip has ratio=2.0 (add one to a scaffold test page), save the body unchanged, git diff is empty.
  • Publish check: a page with url: /custom/ linked from a draft and a published page; the publish warning and the link picker's resolve agree.
  • harness sidecar my_block --site <tmp site> succeeds; harness sidecar -bad is refused with the loader's message.

Assumptions to check

  • Hugo's shortcode lexer types a bare 2 as int and 2.0 as float (read from the serializer's comment and serialize_test.go; the Hugo module source is not vendored here). Verify with a round trip through content.Parse.
  • content may import nothing that knows schemas; if LinkParamValues must stay in elemops, linkrefs takes the param set as an argument rather than importing elemops in a cycle.
  • No site in deckle-hosted (read-only import in this instance) or the scaffold bundles has an element name the loader grammar accepts but the generator previously refused, or vice versa; the switch must not make an existing site fail harness check.
  • The required exemption in spellsDefault is still what #219's fix intended; its tests say so.

Out of scope

  • Items 1, 2 and 5 of #234 (child role, slot wrapper, summary): #243.
  • Changing what a chip, a link or a sidecar may contain; this is consolidation plus the four named fixes only.
  • Reformatting content or re-hashing translations beyond what the SpellsEmpty decision preserves (none expected; if the hash changes for any scaffold fixture, stop and say so in the PR).
  • internal/removecmd/strip.go (intentionally separate, see #232).

Blocked by

  • #243 (both touch server/linktargets.go, elemops/summary.go, server/pending.go and web/src/api/content.ts; landing the payload change first keeps this rebase small)
> *This was generated by AI during triage.* ## Parent #234 (drift audit), items 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14 and the lower-priority node lookup. ## Summary Every remaining rule the audit found evaluated twice gets exactly one owner, and every rule the web client or a theme partial still needs is pinned by a fixture both suites load. Four of the copies already disagree in user-visible ways and are fixed as part of the move: a float chip param `2.0` is rewritten as `2`, publish's draft-link check keys pages by a different URL than link resolve, `harness sidecar` and the loader accept different element names, and the git snapshot subject parser accepts uppercase language tags. ## Current behavior Confirmed at `dcc1067`. Each item: the copies, whether they disagree, and the owner this change gives them. **3. Inline chip grammar (client vs engine).** `web/src/rte/chip.ts` (`CHIP_START_RE`, `PARAM_RE`, `typedParamValue`, `chipParamText`) re-implements `internal/content/serialize.go` (`paramValueString`, `quotedSafe`, `validShortcodeName`, `validParamName`) by hand. Disagreements: `typedParamValue` turns `2.0` into the JS number `2`, `chipParamText` writes `2`, Go re-lexes it as int64 and the file changes on a no-op edit (Go's serializer deliberately keeps `.0`; `web/src/test/rte/markdown.test.ts` covers only `1.5`). The client never refuses `\n`/`\r` in a value, Go does. Names are ASCII-only on the client and Unicode in Go. `chipParamText`'s backtick fallback is a hand copy of `quotedSafe`. No shared fixture. **4. "Value spells its default".** `elemops/save.go` `spellsDefault` (skips required params, compares `ParamValueString` of the value against `Default`) and `l10n/hash.go` `canonicalParam` (includes required params, `Type.Read` first, compares against `Empty()`). `spellsDefault`'s doc comment claims they agree; they differ on required params and on a type whose `Empty()` is not its `Default` (a bool with no default). Harmless for the hash today; the comment is a "same as X" marker. **6. Placement.** Go: `elemops/placement.go` `PlacementMsg` (three axes) plus the `.inline`-parent rule in `content/mutate.go` `checkInsert`. Client: `elementcrud.ts` has the three axes twice (`placementAllowed`, `insertRefusal`) and the `.inline` rule twice (`insertRefusal`, `classifyDrop`). Logic agrees today; no fixture. **7. Link destinations.** Scanning: `linkrefs/linkrefs.go` (`nextAnchor`, `forEachDestination`) vs `server/publish.go` (`markdownLinkRE`, `hrefLinkRE`, `normalizeLinkTarget`). They disagree on whitespace after `](`, on the angle-bracket form, and on a `href` without a closing quote on the line. Which params are links: linkrefs scans every string param and all front matter but `aliases`; publish uses `elemops.LinkParamValues` (link/page-typed params only) and never page-typed front matter. Classification: `server/linktargets.go` `externalSchemeRE`, `web/src/components/SetLinkForm.tsx` `linkTypeOf` and the theme partial `ce/href.html` agree on the scheme rule; `normalizeLinkTarget` has its own short scheme list. **Extra, user-visible:** publish keys draft-link targets by `pageops.PageURL` (the default pretty URL) while link resolve matches `Node.Permalink` (honours `url`/`slug`), so a page with a custom `url` or `slug` gets a different answer from the two. No test ties any pair. **8. Front-matter reads.** `pagetree/build.go` `lookup` is `content.MetaLookup` again. Draft is re-parsed in `pageops/settings.go` and `pageops/translation_settings.go`; url/slug in `pageops/translation_addr.go` `metaStr`. L10n mode constants are defined in both `pagetree` and `l10n`, read two different ways (`MetaString` vs `cast.ToStringE`). `langcmd/content.go` hand-builds the three `Set` calls `l10n.FrontMatterChanges` provides, and its `metaString` is unused. All agree today. **9. Shortcode params.** `content.Shortcode` has no accessor. Getters in `l10n/l10n.go`, `elemops/uid.go`, `elemops/summary.go`, `elemops/slots.go`, `elemops/body.go`, `server/forms.go`, `server/pending.go`, `server/element_transfer.go`, `mediaops/scan.go`; setters in `l10n/l10n.go`, `elemops/uid.go`, `elemops/save.go`; the same `.Name <` sort in eight places. `setParam` copies on insert but overwrites in place on update; `SetShortcodeEnabled` filters in place (`sc.Params[:0]`); `SetShortcodeUID`/`setShortcodeDisabled` append and re-sort in place. No caller shares a backing array today, so the hazard is latent. **10. Disabled flag.** `server/forms.go` `shortcodeDisabled`, `web/src/api/content.ts` `disabledByParams`, `scaffold/themes/deckle/layouts/partials/ce/disabled.html` all accept `true` or `"true"`. Agree; no exported Go reader, no fixture. **11. Language tag.** `^[a-z]{2,3}(-[a-zA-Z0-9]+)*$` in `siteconfig`, `langcmd` and `pagepath` (each with a "copied, keep in step" comment). `server/git.go` `langTagRE` accepts `[A-Za-z]{2,3}`, so a hand-written `[EN] …` commit subject is read as a language. **12. Name grammars.** Element names: `sidecar/sidecar.go` (the `harness sidecar` generator) `^[a-z0-9-]+$` vs `sidecardir` (the loader) `^[A-Za-z0-9_][A-Za-z0-9_-]*$`, copied again in `addcmd/children.go`. They disagree both ways: the generator refuses uppercase and `_` the loader accepts, and accepts a leading `-` the loader refuses. The `add-element` skill documents the loader's grammar. Param names: `element/load.go` ASCII regex vs `content/serialize.go` Unicode predicate; the element rule is the ASCII subset. **13. Loader helpers and client mirrors.** `requireVersion1` and `parseNameList` are byte-identical in `pagetype/load.go` and `backendlayout/load.go`; `editormodule/load.go` inlines the version check; `element/load.go` `parseStringList` is the same body on `any`. Client mirrors with no fixture, all agreeing today: `pagemanage.ts` `isValidSlug` (`pageops.ValidSlug`), `mayPlaceUnder` (`pageops` `checkChildAllowed`), `categorypath.ts` (`categories.SlugOf`), `rte/allow.ts` token list (`element/schema.go` `rteAllowTokens`), `rte/markdown.ts` `TARGET_LINK_OPEN_RE` (`elemops/body.go` `canonicalAnchorOpenRE`, the security gate). **14. Skill prose.** `scaffold/site/.claude/skills/edit-content/SKILL.md` ("Rules, each one enforced by the engine or the theme") restates the rte token list, the paired/self-closing rule, the link grammar and the image rules. Three of its facts are not in AGENTS.md at all: `container: true` implies paired tags (`elemops.BlockForSchema`), the accepted image types (JPEG, PNG, WebP, AVIF; PDF for links), and the 2560px long-edge resize with EXIF/GPS stripping. `migrate-site/SKILL.md` repeats the image facts. **Lower priority.** Page-tree node lookup in `pageops/tree.go` `findNode`, `server/modules.go` `findNode` (skips synthesized nodes) and `server/linktargets.go` `findNodeByPath` (does not). Only differs for an empty path, which no caller passes. `Node.FindByPermalink` in `pagetree` already applies the skip rule. ## Desired behavior - Each rule above has one owner, named in the table below, and every former copy calls it. No comment anywhere says a function mirrors, matches, agrees with or must stay in step with another. - A rule the client or a theme partial needs is pinned by one JSON fixture under the owning Go package's `testdata/`, loaded by `go test` and by vitest (and, for the theme, by a Go test that renders the partial, as `internal/theme/disabled_test.go` does). The `internal/pagepath/testdata/effective_type_cases.json` pattern is the model; its loading recipe on the vitest side (the `import.meta.url` path trick and why) becomes one shared helper in `web/src/test/` so later fixtures do not copy it. - Behaviour fixes carried by the move: - A chip whose float param reads `2.0` re-serializes as `2.0`; a no-op chip edit leaves the file byte-identical for every typed value in the fixture. A value containing a newline is refused by the client with the same error the server gives. - Publish's draft-link check and link resolve agree on a page's URL, including a page with a custom `url` or `slug`. - `harness sidecar <name>` accepts exactly the names the loader accepts. - The snapshot-subject parser accepts exactly the language tags `pagepath` accepts. - The hash and the save path spell a param's emptiness with the same function. Required params are always written by save and are included in the hash as they are today; the shared function takes the "required" rule as an explicit input rather than each caller deciding. - `scaffold/site/AGENTS.md` gains the three missing facts (paired tags for `container: true`, accepted image types, the resize/strip rule), each in the section where the related facts already live. The `edit-content` and `migrate-site` skills point at those sections and restate nothing. ## Decisions - **Fixture over verdict for client mirrors** here: these rules run on the client before any request (slug validity while typing, chip parsing in the editor, drop classification), so a served verdict is not available; a fixture is the CLAUDE.md-sanctioned form. - **Save path's rule wins for required params holding their default**: required params are always written (the fix for #219 established that save never adds a param only to spell a default, and never drops a required one). The hash keeps including required params; the shared `SpellsEmpty`-style function compares against `Empty()` and the callers state whether required params are exempt. Reason: the hash difference is harmless and changing it would re-flag translations. - **Loader's grammar is the element-name grammar**: it is what the `add-element` skill documents and what every existing site has loaded under. The generator and `addcmd` call `sidecardir`'s exported validator. - **`pagepath` owns the language-tag grammar** (it is already the stdlib-only leaf every other package can import). `siteconfig`, `langcmd` and `server/git.go` call it; the git parser's uppercase acceptance is dropped. - **`linkrefs` owns destination scanning and classification**: it exports a destination iterator over a raw document and a classifier (`external`, `page`, `anchor`, `media`, as `linkTypeOf` distinguishes today). Publish scans through it (and so also sees page-typed front matter and all the params linkrefs sees; make the set of link-bearing params one function, `elemops.LinkParamValues` or linkrefs's, not both). Publish keys targets by the same permalink resolve uses. Reason: the picker's classifier and the publish scanner are the two user-facing readers; linkrefs is the rewrite engine and must never miss a link the others see. - **`content.Shortcode` gets `Param`, `SetParam` (keeps params sorted), `DeleteParam`**, documented as never aliasing a caller's slice (copy-on-write). Every getter/setter/sort listed above goes through them; `content/serialize.go`'s sorted-order check stays as the invariant. - **Disabled reader**: one exported `elemops` function; the client and the theme partial are pinned by a fixture of accepted and rejected spellings. - **Placement**: `insertRefusal` is the only client copy (`placementAllowed` and `classifyDrop`'s `.inline` check call it or go away), pinned by a fixture against `PlacementMsg` plus `checkInsert`. - **Loader helpers** move to one package both loaders import (`sidecardir` is the natural home); `parseStringList`'s "absent vs explicit `null`" distinction is kept by the callers that need it and documented at the helper. - **Node lookup**: one function in `pagetree` next to `FindByPermalink`, with the synthesized-node skip; the three callers use it. - **Skill prose**: facts move to AGENTS.md, skills point. Reason: CLAUDE.md's skills rule; the drift test cannot catch prose. ## Rules touched and their owners (CLAUDE.md) | Rule | Owner | Client / theme | |---|---|---| | chip grammar and typed values | `content` serializer | fixture | | value spells its empty | `element.Param` | – | | placement (three axes, `.inline`) | `elemops.PlacementMsg`, `content.checkInsert` | fixture | | link destinations, link classification, link-bearing params | `linkrefs`, `elemops.LinkParamValues` | fixture for `linkTypeOf` and `ce/href.html` | | front-matter reads, l10n mode | `content.MetaLookup`, `l10n` | – | | shortcode params | `content.Shortcode` methods | – | | disabled flag | `elemops` | fixture for `disabledByParams` and `ce/disabled.html` | | language tag | `pagepath` | – | | element / sidecar names | `sidecardir` | – | | param names | `content` | – | | sidecar version and name lists | `sidecardir` | – | | slug, child placement, category path, rte tokens, canonical anchor | `pageops`, `categories`, `element`, `elemops` | fixture each | | page-tree node lookup | `pagetree` | – | ## Affected docs and skills - `scaffold/site/AGENTS.md`: sidecar schema section (`container` and paired tags), "Editing content by hand" (link grammar already there; add image types and resize/strip near "Library media publishes with the pages that use it"), element naming grammar stated once. - `scaffold/site/.claude/skills/edit-content/SKILL.md` and `migrate-site/SKILL.md`: replace restated facts with pointers. - `scaffold/site/.claude/skills/add-element/SKILL.md`: the name rule points at AGENTS.md. - `themes/deckle/README.md` only if the `ce/href.html` or `ce/disabled.html` contract wording changes (it should not). - `internal/element/agentsdoc_test.go` and `scaffold/skills_test.go` stay green. ## Acceptance criteria - [ ] `grep -rniE "same as|mirrors|keep (the two|them|in step)|identical to|agrees with" internal web/src` finds no comment naming a second copy of a rule in this issue's list. - [ ] A chip fixture of at least: int, float with zero fraction, float with fraction, negative, bool, quoted string with `"` and backtick, Unicode param name, value with newline (refused) — passes on both sides; `2.0` round-trips byte-identically through a client chip edit (vitest) and through the Go serializer. - [ ] One `element.Param` method decides "spells its empty value" and both `MergeParams` and the l10n hash call it; `TestHashSpelledOutDefaultIsNoChange` and `TestMergeParams*` still pass; a new test asserts the two callers agree on every param of every scaffold sidecar. - [ ] `web/src/elementcrud.ts` has one placement predicate, fixtured against Go. - [ ] `linkrefs` exports destination scanning and classification; `server/publish.go` has no link regex of its own; a test shows publish and link resolve agree on a page with a custom `url` and one with a custom `slug`; `linkTypeOf` and `ce/href.html` are fixtured against the classifier. - [ ] `content.Shortcode` has `Param`/`SetParam`/`DeleteParam`; no other package indexes `sc.Params` by name or sorts them; a test asserts the setters never write into a caller's backing array. - [ ] One exported disabled reader; `disabledByParams` and `ce/disabled.html` fixtured against it. - [ ] One language-tag regex in `pagepath`; `siteconfig`, `langcmd` and `server/git.go` call it; a test shows `[EN] subject` is not parsed as a language. - [ ] `harness sidecar` and `harness add` validate names with `sidecardir`'s exported rule; the sidecar generator accepts `MyBlock_2` and refuses `-foo`, matching the loader. - [ ] `requireVersion1`/`parseNameList`/the inline version check exist once; `pagetype`, `backendlayout`, `editormodule` and `element` loaders call it. - [ ] Fixtures exist and load on both sides for slug validity, child placement, category path, rte tokens and the canonical anchor regex. - [ ] `pagetree` exports the node lookup; `pageops/tree.go`, `server/modules.go`, `server/linktargets.go` use it. - [ ] `langcmd/content.go` uses `l10n.FrontMatterChanges`; its unused `metaString` is gone; `pagetree/build.go` uses `content.MetaLookup`; one set of l10n mode constants. - [ ] AGENTS.md holds the three missing facts; `edit-content`, `migrate-site` and `add-element` skills restate none of them; drift tests green. ## How to verify - `go vet ./... && go test ./...`; `cd web && npm test` (fixture tests included); `harness check --site scaffold/site`. - Editor: open an element whose chip has `ratio=2.0` (add one to a scaffold test page), save the body unchanged, `git diff` is empty. - Publish check: a page with `url: /custom/` linked from a draft and a published page; the publish warning and the link picker's resolve agree. - `harness sidecar my_block --site <tmp site>` succeeds; `harness sidecar -bad` is refused with the loader's message. ## Assumptions to check - Hugo's shortcode lexer types a bare `2` as int and `2.0` as float (read from the serializer's comment and `serialize_test.go`; the Hugo module source is not vendored here). Verify with a round trip through `content.Parse`. - `content` may import nothing that knows schemas; if `LinkParamValues` must stay in `elemops`, `linkrefs` takes the param set as an argument rather than importing `elemops` in a cycle. - No site in `deckle-hosted` (read-only import in this instance) or the scaffold bundles has an element name the loader grammar accepts but the generator previously refused, or vice versa; the switch must not make an existing site fail `harness check`. - The `required` exemption in `spellsDefault` is still what #219's fix intended; its tests say so. ## Out of scope - Items 1, 2 and 5 of #234 (child role, slot wrapper, summary): #243. - Changing what a chip, a link or a sidecar may contain; this is consolidation plus the four named fixes only. - Reformatting content or re-hashing translations beyond what the `SpellsEmpty` decision preserves (none expected; if the hash changes for any scaffold fixture, stop and say so in the PR). - `internal/removecmd/strip.go` (intentionally separate, see #232). ## Blocked by - #243 (both touch `server/linktargets.go`, `elemops/summary.go`, `server/pending.go` and `web/src/api/content.ts`; landing the payload change first keeps this rebase small)
Sign in to join this conversation.
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#244
No description provided.