refactor: deepen the element mutation engine into internal/elemops #118
No reviewers
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/deckle!118
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "afk/111"
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?
Closes #111
Extracts element-tree semantic mutation out of the HTTP server package into a new module,
internal/elemops— a sibling ofinternal/contentandinternal/element, named after thepageops/mediaopsconvention. Behavior-preserving throughout: on-disk bytes, HTTP statuses, error messages, and orderings are unchanged; the full existing test suite (Go + frontend) passes without modification and served as the byte-for-byte compatibility net.What moved, in five reviewable commits
900311d— uid stamping (StampUIDs/RestampUIDs), the slot-wrapper model, and the placement-rule engine move toelemops; the server keeps IO and layout-view assembly.073236d— the rte/markdown body validator (goldmark policy checker), element creation (NewShortcode/BlockForSchema), and the pure lookup/summary walkers move; the server keeps the HTTP endpoints.9765062— the deepening step:elemopsgains the validated-edit save contract.ApplyEditabsorbs coerce → param merge → items/body child composition (the four-way switch moves verbatim with its compatibility comments);WouldClobberBodyis the body-save guard predicate;MergeParams(shortcode params) andFrontMatterChanges(front-matter keys) are the contract's two sinks sharing one delete-on-absent +<name>Targetlink-companion semantics.handleSavePageshrinks to parse → validate → apply → write;pageops.SaveSettingsdelegates its hand-rolled coerce/apply + companion loop to the same module.1c5a12c— the duplicated front-matter readers (server.metaString/pageops.metaLookup) fold intocontent.MetaLookup+content.MetaStringon the read half of the metadata seam, with unit tests.0968270— 39 direct in-package unit tests through the new module's interface: four-way composition (incl. byte-preserving text coalescing and the legacy self-closing upgrade), uid echo/dedup/stamping, slot wrapping, param merging and companion round-trips,FrontMatterChangeschange-set shape — no HTTP server, no tempdir.Acceptance criteria
<name>Targetcompanion logic inhandleSavePage/SaveSettings— both delegate toelemopsinternal/server(remaining free functions are HTTP/structural or wire-struct view assembly)go test ./...fully green, unchanged (content roundtrip/fidelity/fuzz + all HTTP-level tests); frontend suite 871/871scaffold/site/AGENTS.mdchecked: the drift test passes and no documented canonicalization rule changed (pure move)Out of scope, per the issue: on-disk format, HTTP API surface, atomic-write engine (#116), sidecar-loader consolidation (#114), effective-page-type unification (#115).
🤖 Generated with Claude Code
Landing audit — PASS
Verification signal relied on: the repo's own Forgejo Actions CI (
ci / build-and-test) — success, 8m21s, run 103. Not re-run locally; it already coversgo test ./...(content roundtrip/fidelity/fuzz, the HTTP-level tests) and theinternal/element/agentsdoc_test.goAGENTS.md drift test, which are exactly the byte-for-byte compatibility net this refactor leans on.Checked:
afk/111. Title is Conventional Commits (refactor:).Closes #111; branch matches the issue, so the merge releases the run's claim.main, no conflict resolution needed.internal/{elemops,server,pageops,content}. Nothing in the issue's out-of-scope list is touched: no on-disk shortcode format change, no HTTP API surface change, no atomic-write engine (#116), no sidecar-loader consolidation (#114), no effective-page-type unification (#115), no web frontend.internal/server" — confirmed by enumerating every remaining free function in the package; what is left is HTTP/structural (decodeBody,requirePost,writeJSONError, media/publish helpers) or wire-struct view assembly (layoutFields,decodeFrontMatter,unusedContent). No unqualifiedmetaString/placementPage/slotShortcode/newShortcode/partitionItems/mergeParamsreferences survive in non-test files.Behaviour-preservation review — two independent source-level audits, line-by-line against
origin/main, targeted at the parts that are not pure moves and are therefore where a regression would actually hide:pageops.SaveSettingsdelegating to the shared contract. Previously it had its own hand-rolled coerce/apply +<name>Targetcompanion loop; it now callselemops.FrontMatterChanges. Verified identical: sameCoerceValues, same iteration order (schema.Paramsis the oldfieldsslice, so emitted[]content.Changeordering is unchanged), same set-when-present / delete-when-absent rule (neither version ever sets-to-empty), same link-companion change emitted immediately after its base field.metaString/metaLookuptwins folded intocontent.MetaLookup+content.MetaString. Verified the twins were already semantically identical, and — importantly — that no caller crossed over: everypageopscall site still gets the raw-anylookup and does its own cast, everyservercall site still gets the string-coercing one. Nil, missing-key, and non-string values all behave as before; no trimming or coercion was introduced on either side. The case-variant-key map-iteration quirk is pre-existing and now documented.ApplyEdit, including the legacy self-closing→paired upgrade, the newline text-node coalescing on items extraction, and "a body-capable element is canonically a block even with an empty body".ApplyEditrunsMergeParamsbeforePartitionItems, where the old handler partitioned first.MergeParamsonly writessc.Paramsand neversc.Children, andcoalesceTextcopies rather than mutates text nodes, so both partitions see identical input. LikewiseWouldClobberBodyre-derivesrestinstead of reusing it — same predicate, same short-circuit order.MergeParams,WouldClobberBody, uid stamping, the slot-wrapper family, the placement engine, the goldmark rte/body policy checker, element creation, and the summary/lookup walkers are byte-identical modulo package clause, export renames, and doc comments. Every error string and HTTP status inhandleSavePageis unchanged in wording, condition, and check ordering. The only altered string is an unreachablecrypto/rand unavailablepanic prefix (server:→elemops:).Verdict: PASS — no behaviour differences found. Held at the merge gate for explicit human confirmation.