Skip to content

docs(block-editor): spec for the embedded-contentlet selection guard (#36985) - #37285

Open
rjvelazco wants to merge 1 commit into
mainfrom
issue-36985-block-editor-embedded-contentlet-cannot-be-selected-when-text-or-other-content-precedes-it-spec
Open

docs(block-editor): spec for the embedded-contentlet selection guard (#36985)#37285
rjvelazco wants to merge 1 commit into
mainfrom
issue-36985-block-editor-embedded-contentlet-cannot-be-selected-when-text-or-other-content-precedes-it-spec

Conversation

@rjvelazco

@rjvelazco rjvelazco commented Aug 28, 2026

Copy link
Copy Markdown
Member

Proposed Changes

Spec-Kit PR 1 of 2 for the reopened #36985 — specification only, no implementation.

Clicking an embedded contentlet card in the new Block Editor does create a NodeSelection. A byte-wise change-guard downstream then rebuilds the whole document and destroys it, so the card shows no selection ring and the toolbar's edit / delete / reorder actions stay disabled.

Why it looked random

The guard compares JSON.stringify of the incoming value against JSON.stringify of editor.getJSON(). TipTap normalizes on parse — fills schema defaults, drops undeclared attrs, reorders keys — so any stored document that does not round-trip byte-identically through the current schema fails the check permanently. It is a per-row data condition, not per content type or per site, which is exactly what authors were reporting.

Three confirmed triggers, all measured

Trigger Cause
chartCount typo The legacy editor wrote SetDocAttrStep('chartCount', …) until 3cd5549d35 (#26025, 2023-09-11). stripDocStats strips charCount, not chartCount.
Missing indent IndentExtension declares indent (default 0) on heading / paragraph / blockquote. Older content has no such key; the editor adds it on parse.
Unknown node types The string branch skips preserveUnknownNodesInDocument, so a custom block is compared placeholder-against-original.

Supplying a missing key is not enough — it has to be in the right position. That is the measured Case B in the spec, and it is what makes this a byte-comparison bug rather than a missing-attribute bug.

Why only one screen

The new Angular Edit Content screen binds with [formControlName], so the value arrives once through writeValue and the effect short-circuits forever after. The legacy JSP contentlet editor assigns blockEditor.value, so its effect is the loader and stays live — the only host where a re-push can reach setContent after load. The spec carries the full host-to-branch map.

What's in this PR

File Contents
specs/36985-block-editor-selection-guard/spec.md Problem, reproduction (server-free and local-instance), root cause, fix scope, non-goals, regression risk, AC-001…AC-008
specs/36985-block-editor-selection-guard/data-model.md Value shapes and the normalization Node.fromJSON applies
specs/36985-block-editor-selection-guard/contracts/content-match.contract.md The comparator's input→verdict table — the source for the Red-phase tests

plan.md, research.md, quickstart.md and checklists/ are Spec-Kit working artifacts and are gitignored by design (.gitignore:229-233).

Reproduction

No customer data required. The spec gives two recipes: a schema round-trip that needs no server, and an API-seeded BROKEN/CONTROL pair on a local instance. The legacy JSP contentlet editor is required — the content type needs CONTENT_EDITOR2_ENABLED=false, and the new Angular screen will not reproduce it.

A convenience seeding script is attached to the QA note on the issue rather than committed; the spec's steps are the source of truth.

Still open, recorded in the spec rather than hidden

  • The local CONTROL click has not been performed. The host reasoning rests on customer-environment evidence plus the routing in dot-contentlet-editor.service.ts:204. If CONTROL also fails locally, "Why only one screen is affected" needs rewriting before anyone implements against it.
  • What re-pushes the value in the legacy host is unidentified. commitEditor after remote customBlocks extensions is excluded for the local fixture. This does not block the fix — once the comparison is structural the verdict becomes "unchanged", so a re-push is harmless rather than merely suppressed.

Checklist

  • Spec validated against specs/36985-block-editor-selection-guard/checklists/requirements.md
  • No [NEEDS CLARIFICATION] markers
  • No customer data — environment, hostnames and content identifiers replaced with a local fixture
  • Every claim measured against the real schema via getSchema(createEditorExtensions(...)), not inferred
  • Approval from another dev — required before the implementation PR proceeds

Notes for reviewers

  • The spec deliberately keeps the Spec-Kit template's mandatory H2 headings, with the readable framing inside them, so /speckit-plan and /speckit-tasks keep resolving.
  • Non-goals worth confirming: the click handler is not touched, no chartCountcharCount data migration, no port of click-to-select to the legacy editor (contentlet-block.node.ts:23-24 has none, so the FEATURE_FLAG_NEW_BLOCK_EDITOR=false workaround still reproduces the original symptom — separate ticket), and no apply-once memo in the effect while the re-push is unidentified.
  • AC-005 is a behaviour change, not a regression guard. A bare array is measured false today and must be true — expect it to fail at Red.

Spec for #36985

🤖 Generated with Claude Code

This PR fixes: #36985

…36985)

Issue-resolution spec for the reopened #36985. Clicking an embedded
contentlet card does create a NodeSelection; a byte-wise change-guard then
rebuilds the document and destroys it, leaving the card unselectable and
the toolbar's contentlet actions disabled.

The guard compares JSON.stringify of the incoming value against
JSON.stringify of editor.getJSON(). TipTap normalizes on parse, so any
stored document that does not round-trip byte-identically through the
current schema fails it permanently -- which is why the defect looks
random across a site. It is a per-row data condition, not per content
type or per site.

Records the measured evidence:

- three confirmed triggers: the legacy chartCount typo (written until
  3cd5549, #26025), missing indent on heading/paragraph/blockquote, and
  the string branch skipping preserveUnknownNodesInDocument for unknown
  node types;
- the host-to-branch map explaining why only the legacy JSP contentlet
  editor is affected -- it is the only host where the value effect stays
  live after load;
- AC-001..AC-008, including the deliberate node/mark split on AC-004.

Also carries data-model.md (value shapes and the normalization fromJSON
applies) and contracts/content-match.contract.md (the comparator's
input-to-verdict table, which is the source for the Red-phase tests).

Spec only -- no implementation. Per the Spec-Kit flow this needs another
dev's approval before /speckit-plan output is acted on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Claude finished @rjvelazco's task in 1m 34s —— View job


Spec review — #36985 selection-guard

This is a documentation-only PR (three Spec-Kit artifacts, no implementation). I reviewed it for technical accuracy against the current source and internal consistency, since a wrong claim here propagates into the implementation PR.

  • Read the three spec documents
  • Verify code references cited in the spec against the actual source
  • Check internal consistency (line numbers, AC/contract mapping)
  • Post review

Verification results

Every load-bearing code claim I could check against the repo holds:

Spec claim Source Verdict
Faulty check at editor.component.ts:137-155 editorContentMatchesParsed spans 137–155
Comparator is JSON.stringify(stripDocStats(incoming)) === JSON.stringify(stripDocStats(editor.getJSON())) :143 + editorDocumentJsonText (:63-65)
String branch (:143) skips preserveUnknownNodesInDocument; object branch (:152) applies it confirmed
Value effect :619-632, short-circuits on if (!v) return :619-632, guard at :621
Other callers commitEditor:498, writeValue:747 both confirmed
stripDocStats strips only charCount/wordCount/readingTime, not chartCount DOC_STAT_ATTRS = ['charCount','wordCount','readingTime']
IndentExtension declares indent on heading/paragraph/blockquote INDENT_TYPES (indent.extension.ts:19)
withDocStats emit at :713 confirmed

Notably, the existing docstring on stripDocStats (doc-stats.utils.ts:6-14) already names #36985 as its motivation and describes the exact "guard permanently false → setContent rebuilds doc → clobbers just-clicked contentlet selection" chain — which independently corroborates the spec's root-cause narrative.

Notes (non-blocking)

  • 🟡 Minor: spec.md:228 — the "Where the code lives" table points stripDocStats to utils/doc-stats.utils.ts:4, but line 4 is the DOC_STAT_ATTRS constant; the stripDocStats function itself is at :19. Trivial, but worth correcting given the spec's stated precision bar ("every claim measured against the real schema").
  • The spec is admirably explicit about what has not been verified — the local CONTROL click in the legacy JSP host, and the identity of the re-push. Both are called out in "Still pending" and "One thing we still don't know" rather than papered over. That's the right call; the reviewer approving this should read those two sections as the real open risk, since the "Why only one screen is affected" section rests on them.
  • Contract requirement Test Branch and Commit #2 (schema.nodes.doc.spec.attrs is null, so Fragment.eq and Node.eq are equivalent today but must not be treated as interchangeable) is the sharpest part of the design and gives the implementer a clear, defensible instruction. Good.
  • AC/contract mapping is coherent: contract rows 1, 3, 6, 8 (the four Red-phase behaviour changes) line up with AC-001/002/005/007, and the "row 4 or row 7 failing at Red means the fixtures are wrong" backstop is a nice guard against an over-permissive fix.

Verdict

No blocking issues. The spec is accurate against the code, internally consistent, and honest about its unverified assumptions — appropriate for a PR-1 spec gated on dev approval. The one factual nit (:4 vs :19) is optional to fix.

No issues found. (spec-only; the single note above is a documentation nit, not a defect.)

· issue-36985-...-spec

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Block Editor: embedded contentlet cannot be selected when text or other content precedes it

2 participants