Drift audit: rules implemented twice across engine, server, client and theme #234
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#234
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
Tracking index for the drift audit: every rule the engine, server, client or theme evaluates in two or more places gets one owner (CLAUDE.md "Every rule has exactly one owner", #233). The work is sliced into two child issues; this issue closes when both have landed.
Children
Verification status (triage, 2026-10-12, at
dcc1067)Every item was re-checked in the source after #239 landed. Already fixed by #239:
PartitionItems,l10n.isItemand the text-merge copies (elemops.IsItemandcontent.MergeTextare the owners). Still open: everything else, with these corrections to the audit:pending.go,linktargets.go, the client) remain, plus one the audit missed, the move-sibling visibility check inelementcrud.ts.2.0→2, newlines, ASCII names).url/slug.add-elementskill documents the loader's grammar.container: truepairing, image types, 2560px/EXIF).Decisions (maintainer, 2026-10-12)
hidden: true⇔ named as some owner's item (#243).Acceptance criteria
internal/orweb/srcfor any rule listed in the original report.How to verify
main;go test ./...andcd web && npm testgreen;harness checkon the scaffold site passes.Why this stays with a human
The remaining work is bookkeeping: close this issue when the children land and decide whether anything the children's PRs surfaced (a hash change, a site that fails the name grammar) needs a follow-up.
Out of scope
Blocked by
Original report
Follow-up to #230 (one split implemented twice, one copy correct), #232 (its real fix) and #233 (the one-owner rule in CLAUDE.md). A read-only sweep of
internal/andweb/srcfor rules evaluated independently in two or more places, with no shared source of truth and no test tying the copies together. Items marked confirmed were checked in the source by hand; the rest are from the sweep and should be verified before work starts. Line numbers are approximate.Ranked by drift likelihood × user-visible impact. Each is probably its own change; this issue is the index.
1. Child role: item, nested element or body content — confirmed, copies disagree
internal/l10n/l10n.goisItem,holdsElement;internal/server/translation.goitemAt(third copy of isItem);internal/elemops/save.goPartitionItems,WouldClobberBody: decide by the parent'sitems.element(pluscontainer, block fallback without schema).internal/server/pending.goisFoldedElement;internal/server/linktargets.go:108: decide by the child'shidden || group == inline.web/src/api/content.tsisInlineGroupNode,isHiddenNode,flattenChildren;web/src/elementcard.tsisCardNode/hasSubList: child-based.element/load.govalidateItems) does not require an item element to be hidden, so one child can be an item for translation and a card for the editor, or the reverse.elemops.ChildRole(parentSchema, child, schemas)used by every server path and serialized into the tree payload so the client stops re-deriving.2. Slot-wrapper predicate — confirmed, copies disagree
elemops/slots.goSlotWrapperName: block + nameslot+ non-emptynameparam.server/linktargets.go:127: block + name.server/pending.go:414,456: name only, any depth.web/src/elementcrud.tsisSlotWrapper,api/content.tsSLOT_WRAPPER_NAME: block + name.slot. Owner:SlotWrapperName, verdict in the tree payload.3. Inline chip grammar and param spelling, client vs engine — plausible from code
web/src/rte/chip.ts(CHIP_START_RE,PARAM_RE,typedParamValue,chipParamText) vs Gocontent/serialize.go(paramValueString,quotedSafe,validShortcodeName,validParamName) and Hugo's lexer typing in parse.2.0→ JS2→ written back2, re-lexed as int64: breaks the zero-diff law for floats with a zero fraction (markdown.test.tscovers only1.5). Client does not refuse newlines in values. Param names ASCII-only on the client, Unicode in Go.go testand vitest.4. "Value spells its default" — confirmed, copies disagree
elemops/save.gospellsDefault(excludes required params, compares the raw value againstDefault) vsl10n/hash.gocanonicalParam(includes required,Type.Readfirst, compares againstEmpty()). A comment claims they spell defaults the same way.element.Param.SpellsEmpty(v).5. Element summary
elemops/summary.goElementSummaryandweb/src/components/ElementForm.tsxrowSummaryagree (roles come from the server).web/src/elementcard.tselementSummaryuses body text first, a hard-coded type list, joins with " · ", ignoressummaryand basename. Cards and the link picker summarize one element differently.6. Placement rules
elemops/placement.goPlacementMsg; clientelementcrud.tsplacementAllowedandinsertRefusal— two client copies, only the latter checks the.inlineparent (Go:content/mutate.gocheckInsert).insertRefusalas the only client copy, backed by a shared fixture.7. Link destination scanning and classification
linkrefs/linkrefs.gonextAnchor,forEachDestinationvsserver/publish.gomarkdownLinkRE,hrefLinkRE,normalizeLinkTarget. Classification:server/linktargets.goexternalSchemeRE,web/src/components/SetLinkForm.tsxlinkTypeOf, themepartials/ce/href.html.elemops/summary.goLinkParamValues(used by publish) only link/page-typed ones. Edges differ: whitespace after](, non-http schemes, relative paths.linkrefsexportsDestinations(raw)andClassify(href); publish and the picker reuse them.8. Front-matter metadata reads
pagetree/build.golookupduplicatescontent.MetaLookup; draft/l10n/url/slug parses re-done inpageops/settings.go,pageops/translation_settings.go,pageops/translation_addr.gometaStr,langcmd/content.go. Mode constants exist inpagetree.goandl10n.go.langcmd/content.go:92hand-builds whatl10n.FrontMatterChangesprovides. Agree today.content/l10nhelpers.9. Shortcode param get/set/sort
l10n/l10n.go,elemops/uid.go,elemops/save.go,server/forms.goparamLookup,elemops/summary.go; ad hoc name sorts inl10n/hash.go,elemops/create.go,server/forms.go,elemops/save.go.setParamcopies the slice,SetShortcodeEnabledfilters in place (sc.Params[:0]): aliasing hazard.content.ShortcodemethodsParam,SetParam(sorted),DeleteParam.10. Reading the disabled flag
server/forms.goshortcodeDisabled,web/src/api/content.tsdisabledByParams, themepartials/ce/disabled.html: all accepttrueor"true"; agree today; single writerelemops/save.gosetShortcodeDisabled.elemops.ShortcodeDisabledplus a fixture shared with client and theme.11. Language-tag grammar
siteconfig.go,langcmd.go,pagepath.go: documented copies.server/git.go:468accepts an uppercase primary subtag — already disagrees.pagepath.ValidLangTag.12. Name grammars
sidecar/sidecar.go(^[a-z0-9-]+$) vssidecardir.go, repeated inaddcmd/children.go. Param names:element/load.go(ASCII) vscontent/serialize.go(Unicode).sidecardir.ValidName,content.ValidParamName.13. Loader helpers and unfixtured client mirrors
requireVersion1,parseNameListcopied inpagetype/load.goandbackendlayout/load.go(documented as identical);editormodule/load.goinlines the version check;element/load.goparseStringListhas different nil semantics.pagemanage.tsisValidSlug(pageops/slug.go),mayPlaceUnder(checkChildAllowed);categorypath.ts(categories.SlugOf);rte/allow.tstoken list (element/schema.go); the canonical-anchor regexrte/markdown.tsvselemops/body.go— the latter is the security gate.internal/pagepath/testdatapattern (one JSON fixture, both suites).14. Skill restating facts — confirmed
scaffold/site/.claude/skills/edit-content/SKILL.md:105-133restates the rte token list, paired vs self-closing (elemops.BlockForSchema), the link grammar and the allowed image types. CLAUDE.md says skills point at AGENTS.md; the drift test cannot catch prose. Replace with pointers.Lower priority
pageops/tree.go,server/modules.go(skips synthesized nodes) andserver/linktargets.go(does not).Suggested order
1 and 2 together (same split, same payload change), then 4 (translation hashes), then 3 and the fixture pattern of 13, which gives every later client mirror a home. 14 is a docs-only change that can land any time.