feat(tree): server-owned child role, slot wrapper and summary verdicts #246

Merged
dominik.polakovics merged 2 commits from afk/243 into main 2026-10-13 01:03:48 +02:00

Closes #243

What

One rule, one owner. internal/elemops/roles.go (RootRole, ChildRole, WalkRoles, Verdicts) decides each shortcode node's role: slot, element, item or text.

  • Under the root, a named block slot is a wrapper, an inline-group chip is text, and anything else is an element.
  • Under a wrapper, children are judged like root children, except that a slot is an element there.
  • Under an element:
    • IsItem(parent, child) → item. The parent's items.element decides; the child's hidden flag is never read.
    • An items owner's other children → text (its body region).
    • An inline-group chip → text.
    • A child with a sidecar → element.
    • A container's child → element.
    • Otherwise a block → element and an inline → text.

Slot treatments, pinned by tests in every reader. A nameless top-level slot, a self-closing slot and a nested slot are all ordinary elements without a sidecar. They get a uid, show as a card, are listed by the link picker, are counted by pending and are translated elements. harness check reports each as an error.

Payload. Every page tree the server sends carries role and summary (elemops.ElementSummary) on every shortcode node. That covers GET /api/page, every mutation, translate and undo/discard answer, and the layout's inherited-slot children. It is built by content.AnnotatedDocument / AnnotatedNodes, which share one encoder with the plain MarshalJSON, through a single server helper pageTree.

Server readers.

  • pending.go: isFoldedElement is gone.
  • linktargets.go: rows come from WalkRoles.
  • itemAt: uses the role function.
  • Slot refusals go through elemops.IsSlotShortcode.
  • l10n: holdsElement, walkElements, MissingUIDs, Hash, markTranslated and StampSource all use the role.
  • StampUIDs skips exactly role-slot nodes.

Client. isHiddenNode, isInlineGroupNode, isCardNode, isSlotWrapper, SLOT_WRAPPER_NAME, elementSummary and plainText are gone. Walkers read node.role.

  • Cards, translation placeholders and the link picker all show the same served summary.
  • A card without a summary shows only its label, which is already its title line, so no second line repeats it.
  • The move-sibling check (stepTarget) now skips item rows, with a regression test.

Loader. An element named by an owner's items.element must declare hidden: true, and hidden: true requires such an owner. A violation is a sidecar load error, shown by harness check and in the editor's schema-error list.

Docs. scaffold/site/AGENTS.md updates the sidecar hidden line, "Item collections", "Insertability", a new "Slot wrappers" bullet in "Editing content by hand", and the Translations element-position list. The add-element and edit-content skills point at those sections.

Behaviour changes worth noting

These follow from the single rule:

  • A hidden element placed outside its owner is now a visible card, and can be deleted.
  • A top-level inline-group chip is not a translated element.
  • A non-inline element with a sidecar nested inside a non-container element is an element, where before it was l10n body.
  • A container's inline chip is text, where before it was an l10n element.
  • Pending folds an item's whole subtree into its owner's fingerprint.
  • The card summary is now the link picker's line (summary key, then text params, then image basename). The old "body text first" card line is dropped.

Verification

  • make test: vitest passes 116 files and 2860 tests, and go test ./... passes.
  • go vet ./... and gofmt are clean.
  • On a freshly initialized site, harness check passes.
    • Adding a hidden: true sidecar with no owner fails with a load error.
    • Appending {{< slot />}} to a page fails with a content error.

🤖 Generated with Claude Code

Closes #243 ## What **One rule, one owner.** `internal/elemops/roles.go` (`RootRole`, `ChildRole`, `WalkRoles`, `Verdicts`) decides each shortcode node's role: `slot`, `element`, `item` or `text`. - Under the root, a named block `slot` is a wrapper, an inline-group chip is text, and anything else is an element. - Under a wrapper, children are judged like root children, except that a slot is an element there. - Under an element: - `IsItem(parent, child)` → item. The parent's `items.element` decides; the child's `hidden` flag is never read. - An items owner's other children → text (its body region). - An inline-group chip → text. - A child with a sidecar → element. - A container's child → element. - Otherwise a block → element and an inline → text. **Slot treatments, pinned by tests in every reader.** A nameless top-level slot, a self-closing slot and a nested slot are all ordinary elements without a sidecar. They get a uid, show as a card, are listed by the link picker, are counted by pending and are translated elements. `harness check` reports each as an error. **Payload.** Every page tree the server sends carries `role` and `summary` (`elemops.ElementSummary`) on every shortcode node. That covers GET `/api/page`, every mutation, translate and undo/discard answer, and the layout's inherited-slot children. It is built by `content.AnnotatedDocument` / `AnnotatedNodes`, which share one encoder with the plain `MarshalJSON`, through a single server helper `pageTree`. **Server readers.** - `pending.go`: `isFoldedElement` is gone. - `linktargets.go`: rows come from `WalkRoles`. - `itemAt`: uses the role function. - Slot refusals go through `elemops.IsSlotShortcode`. - l10n: `holdsElement`, `walkElements`, `MissingUIDs`, `Hash`, `markTranslated` and `StampSource` all use the role. - `StampUIDs` skips exactly role-`slot` nodes. **Client.** `isHiddenNode`, `isInlineGroupNode`, `isCardNode`, `isSlotWrapper`, `SLOT_WRAPPER_NAME`, `elementSummary` and `plainText` are gone. Walkers read `node.role`. - Cards, translation placeholders and the link picker all show the same served summary. - A card without a summary shows only its label, which is already its title line, so no second line repeats it. - The move-sibling check (`stepTarget`) now skips item rows, with a regression test. **Loader.** An element named by an owner's `items.element` must declare `hidden: true`, and `hidden: true` requires such an owner. A violation is a sidecar load error, shown by `harness check` and in the editor's schema-error list. **Docs.** `scaffold/site/AGENTS.md` updates the sidecar `hidden` line, "Item collections", "Insertability", a new "Slot wrappers" bullet in "Editing content by hand", and the Translations element-position list. The `add-element` and `edit-content` skills point at those sections. ## Behaviour changes worth noting These follow from the single rule: - A `hidden` element placed outside its owner is now a visible card, and can be deleted. - A top-level inline-group chip is not a translated element. - A non-inline element with a sidecar nested inside a non-container element is an element, where before it was l10n body. - A container's inline chip is text, where before it was an l10n element. - Pending folds an item's whole subtree into its owner's fingerprint. - The card summary is now the link picker's line (`summary` key, then text params, then image basename). The old "body text first" card line is dropped. ## Verification - `make test`: vitest passes 116 files and 2860 tests, and `go test ./...` passes. - `go vet ./...` and `gofmt` are clean. - On a freshly initialized site, `harness check` passes. - Adding a `hidden: true` sidecar with no owner fails with a load error. - Appending `{{< slot />}}` to a page fails with a content error. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(tree): server-owned child role, slot wrapper and summary verdicts
Some checks failed
ci / build-and-test (pull_request) Has been cancelled
ci / release (pull_request) Has been cancelled
ba8bdb7f8b
One function in elemops (roles.go: RootRole, ChildRole, WalkRoles) now
decides what a shortcode node is in the element tree — slot wrapper,
element, items row or body text — from its parent: the owner's
items.element makes a child an item, never the child's hidden flag. A slot
wrapper is a named block "slot" directly under the root; a nameless,
self-closing or nested slot is an ordinary element everywhere, and
harness check reports it.

Every page tree the server sends carries a role and an
elemops.ElementSummary string per shortcode node
(content.AnnotatedDocument / AnnotatedNodes, one encoder), inherited
slots included. Pending, the link picker, translation itemAt, uid
stamping and l10n all read the rule; the web client reads the served
verdicts and no longer derives item, card, slot wrapper or summary from
schemas. Cards show the link picker's summary.

The loader enforces hidden <=> items element. AGENTS.md and the
add-element / edit-content skills document the enforced rule and the
slot wrapper definition.

Closes #243

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merge branch 'main' into afk/243
All checks were successful
ci / build-and-test (pull_request) Successful in 20m40s
ci / release (pull_request) Has been skipped
0b9efbd803
Bring in #242 (the preview is just the site). Its removal of the editor
bridge deleted web/src/test/Bridge.test.tsx and web/src/test/bridge.test.ts,
which this branch had only touched to add the role/summary fixture fields;
the deletion is kept.

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

[autoland] verdict: pass

Validated — CONCERNS (non-blocking). Approved, NOT merged: CI never ran.

Concern — CI has not vouched. ci / build-and-test sat at "Waiting to run" through two full labctl pr checks --wait cycles. That happened on the original head ba8bdb7 (run 242) and on the merge head 0b9efbd (run 243). It looks like no runner is picking jobs up. The validation core never lands on pending. Merge once run 243 (or a re-run) goes green.

Merge resolution (pushed as 0b9efbd). main had moved on with #242 (the preview is just the site). That PR deleted web/src/test/Bridge.test.tsx and web/src/test/bridge.test.ts. This branch had only added the role/summary fixture fields to those two files. They test bridge.ts, which #242 removed, so the deletion was kept. Every other file merged cleanly.

Signal relied on: my own checks on the merged tree (0b9efbd).

  • tsc --noEmit is clean and vite build succeeds.
  • vitest: 115 files, 2826 tests pass.
  • go vet ./... and gofmt -l are clean, and go test ./... passes (all 32 packages ok).
  • On a freshly initialized site, harness check reports "ok: no problems found".

Conventions.

  • The title is a Conventional Commit.
  • Closes #243 is present.
  • AGENTS.md ("Item collections", "Insertability", the sidecar hidden line, "Slot wrappers", the Translations element positions) and the add-element and edit-content skills are updated, and the drift tests are green.
  • The diff matches the brief in #243:
    • one owner for the child role (elemops.ChildRole/RootRole/WalkRoles), the slot wrapper and the summary;
    • verdicts served on every tree payload through pageTree;
    • the client re-derivations (isHiddenNode, isInlineGroupNode, isCardNode, isSlotWrapper, SLOT_WRAPPER_NAME, elementSummary) are removed;
    • the loader enforces hidden ⇔ items;
    • each slot case has a test in every reader (pending, linktargets, l10n, client), with the stepTarget regression test.
  • No server package outside elemops matches the name slot. The remaining schema.Hidden/GroupInline reads (element.go, element_transfer.go, forms.go) are insert, duplicate and inline-form refusals, not role decisions.

Note for the record, not blocking: the loader still accepts container: true together with items. Under the new ChildRole, such an owner's non-item children become text (body region), where before they were elements. No shipped element combines the two keys, which is consistent with #239's split.

[autoland] verdict: pass Validated — CONCERNS (non-blocking). Approved, NOT merged: CI never ran. **Concern — CI has not vouched.** `ci / build-and-test` sat at "Waiting to run" through two full `labctl pr checks --wait` cycles. That happened on the original head ba8bdb7 (run 242) and on the merge head 0b9efbd (run 243). It looks like no runner is picking jobs up. The validation core never lands on pending. Merge once run 243 (or a re-run) goes green. **Merge resolution (pushed as 0b9efbd).** `main` had moved on with #242 (the preview is just the site). That PR deleted `web/src/test/Bridge.test.tsx` and `web/src/test/bridge.test.ts`. This branch had only added the `role`/`summary` fixture fields to those two files. They test `bridge.ts`, which #242 removed, so the deletion was kept. Every other file merged cleanly. **Signal relied on: my own checks on the merged tree (0b9efbd).** - `tsc --noEmit` is clean and `vite build` succeeds. - vitest: 115 files, 2826 tests pass. - `go vet ./...` and `gofmt -l` are clean, and `go test ./...` passes (all 32 packages ok). - On a freshly initialized site, `harness check` reports "ok: no problems found". **Conventions.** - The title is a Conventional Commit. - `Closes #243` is present. - AGENTS.md ("Item collections", "Insertability", the sidecar `hidden` line, "Slot wrappers", the Translations element positions) and the `add-element` and `edit-content` skills are updated, and the drift tests are green. - The diff matches the brief in #243: - one owner for the child role (`elemops.ChildRole`/`RootRole`/`WalkRoles`), the slot wrapper and the summary; - verdicts served on every tree payload through `pageTree`; - the client re-derivations (`isHiddenNode`, `isInlineGroupNode`, `isCardNode`, `isSlotWrapper`, `SLOT_WRAPPER_NAME`, `elementSummary`) are removed; - the loader enforces hidden ⇔ items; - each slot case has a test in every reader (pending, linktargets, l10n, client), with the `stepTarget` regression test. - No server package outside `elemops` matches the name `slot`. The remaining `schema.Hidden`/`GroupInline` reads (`element.go`, `element_transfer.go`, `forms.go`) are insert, duplicate and inline-form refusals, not role decisions. Note for the record, not blocking: the loader still accepts `container: true` together with `items`. Under the new `ChildRole`, such an owner's non-item children become `text` (body region), where before they were elements. No shipped element combines the two keys, which is consistent with #239's split.
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!246
No description provided.