Skip to content

Merge main into stable#3228

Closed
caio-pizzol wants to merge 23 commits into
stablefrom
merge/main-into-stable-2026-05-11
Closed

Merge main into stable#3228
caio-pizzol wants to merge 23 commits into
stablefrom
merge/main-into-stable-2026-05-11

Conversation

@caio-pizzol
Copy link
Copy Markdown
Contributor

Summary

  • creates merge/main-into-stable-2026-05-11 from stable
  • merges current main into the candidate branch
  • resolves the only merge conflict in .github/workflows/release-stable.yml by keeping the updated orchestrator docs-promotion comment from main

Note

The promote-stable workflow dispatch failed before opening a PR because the GitHub App token cannot push workflow-file changes without the Workflows permission. This branch was pushed manually with a token that has workflow scope.

caio-pizzol and others added 20 commits May 7, 2026 17:35
* ci: auto-resolve version conflicts in promote-stable workflow

* ci: also match root-level release artifact files in auto-resolve
…#3200)

* fix(mcp): fall back to z.unknown() for oneOf with non-object variants

z.looseObject({}) emits type:"object" which is right for object-only
unions but rejects arrays/booleans/etc. at runtime when the union
includes non-object variants. Gate the looseObject path on "every
variant is type:object" and fall back to z.unknown() otherwise. The
only catalog field this affects today is superdoc_edit.content
(oneOf object|array), where the array form was getting rejected
before reaching DocumentApi.

Adds a unit test that walks the catalog and checks the emitted
type for both branches.

* docs(mcp): tighten oneOf branch comment with AIDEV-NOTE anchor
Strengthens the existing reminder so agents look for comment-policy.md
explicitly and run /comment-audit to validate changes, instead of
relying on the bare 'follow comment-policy.md' wording.
* feat(contracts): add typed direction context types (SD-2775)

Introduces orthogonal direction context types so future RTL work cannot
accidentally collapse axes that ECMA-376 keeps separate:

- BaseDirection, WritingMode (enums)
- SectionDirectionContext (page direction, gutter — chrome only)
- TableDirectionContext (visual cell ordering only)
- CellDirectionContext (cell writing mode)
- ParagraphDirectionContext (paragraph inline base direction + writing mode)
- RunBidiContext, RunScriptContext (run-level signals; consumed in 1b/1c)

Adds `directionContext` field to ParagraphAttrs alongside the existing
`direction` scalar. Both are populated by pm-adapter from the same source;
consumers can migrate gradually.

Per ECMA-376 §17.6.1 / §17.3.1.6 / §17.4.1 / §17.3.1.41, each axis stays
separate: section bidi is chrome only, paragraph bidi is paragraph-local,
table visual direction is cell ordering, writing mode is the one
inheriting axis.

No behavior change. Resolver chain and migration follow in subsequent
commits.

* feat(pm-adapter): add direction resolver module (SD-2776)

New module pm-adapter/src/direction/ with:

- resolveSectionDirection / resolveTableDirection / resolveCellDirection /
  resolveParagraphDirection — context propagation chain mirroring OOXML
  containment hierarchy
- logicalSides helpers (resolveLogicalAlignment, resolveLogicalIndent,
  physicalSide, isRtl, toBaseDirection) — direction-aware logical→physical
  mapping
- 12 non-collapse tests enforcing the four ECMA spec rules:
  1. Section w:bidi MUST NOT make paragraphs RTL (§17.6.1)
  2. Table w:bidiVisual MUST NOT make cell paragraphs RTL (§17.4.1)
  3. Run-level w:rtl MUST NOT bubble up to paragraph
  4. Paragraph w:bidi DOES produce paragraph RTL (§17.3.1.6, including
     style cascade through docDefaults per §17.7.2)
- Writing mode IS the one inheriting axis (§17.3.1.41) — paragraph→cell→
  section→default

Co-located README documenting the spec rules, a worked-example for
downstream consumers, and explicit non-goals (script classifier and bidi
controls deferred to Wave 1b / 1c).

No production call sites consume the resolver yet; migration follows.

* refactor(pm-adapter): migrate computeParagraphAttrs to direction resolver

Replaces the cascade in resolveEffectiveParagraphDirection +
inferDirectionFromRuns with the typed resolver chain from
pm-adapter/src/direction/. The cascade had three issues identified by
the audit at .tmp/rtl-audit-findings.md:

1. Section→paragraph fallback (§17.6.1 violation) — section bidi
   propagated to paragraph inline direction. Latin paragraphs in RTL
   sections rendered right-aligned; Word renders them left-aligned.
2. Majority-of-runs heuristic (UAX #9 P2/P3 disagreement) — base
   direction came from counting runs whose w:rtl flag was set, not
   the first strong character of the text content.
3. docDefaultsDirection parameter (redundant) — the style-engine
   cascade in style-engine/src/ooxml/index.ts:165 already resolves
   docDefaults.paragraphProperties.rightToLeft into the paragraph's
   resolved properties before this resolver runs.

Now: paragraph direction comes from paragraph w:bidi (or its style
cascade); when absent, inlineDirection is undefined and the browser
applies UBA via the missing dir attribute. Output corrected for
documents that today render incorrectly; unchanged for documents that
were already correct.

Tests updated:
- paragraph.test.ts: removed cascade/heuristic tests that codified
  the spec violations
- paragraph.test.ts: section-fallback test flipped to assert no
  inheritance
- index.test.ts: two integration tests flipped to expect undefined
  paragraph direction when only section bidi is set

Validation:
- 1,765 pm-adapter unit tests pass
- 211 contracts unit tests pass
- 12,374 super-editor unit tests pass (incl. footer w:rtl roundtrip)
- 51 RTL Playwright behavior tests pass across Chromium/Firefox/WebKit

Closes SD-2776, SD-2778. The legacy attrs.direction scalar remains
populated for backwards compatibility; consumers should migrate to
attrs.directionContext over time.

* fix(direction): accept rightToLeft on TablePropertiesLike

The resolved TableProperties type from the style-engine uses
`rightToLeft` for the bidiVisual flag (matching the existing importer
convention). The resolver previously checked only `bidiVisual`, so
passing real resolved table properties would leave visualDirection
undefined for RTL tables.

Now accepts both `rightToLeft` (style-engine name) and `bidiVisual`
(OOXML name) for safety. Test added to cover the alias.

* fix(direction): map all ST_TextDirection values incl. V-suffix variants

Per ECMA-376 §17.18.93, ST_TextDirection has 12 enumeration values across
the strict and Word-transitional vocabularies. The V-suffix variants are
glyph rotation, which CSS expresses through text-orientation, so they share
the writing-mode of their non-V sibling.

Before this commit the three resolvers (paragraph/section/cell) handled 6
of the 12 values; lrTbV, tbRlV, tbV, lrV, rlV all fell through to undefined
and the resolver silently used the inherited/default writing-mode instead.
The repo's ST_TEXT_DIRECTION contract (registry.ts:18) publishes lrTbV and
tbRlV as accepted values, so this was a contract violation - documents that
imported one of these would lose their writing-mode override.

Adds an exhaustive test that exercises all 12 values on paragraph, section,
and cell.

* fix(layout-bridge): separate text direction from RTL hit testing

* fix(layout-bridge): harden isRtlBlock and anchor compat-fallback rule

Three review-driven nits on the SD-2780 hit-test fix:

1. The directionContext gate used `'inlineDirection' in directionContext`
   which fires for keys with `undefined` values. The resolver can produce
   `inlineDirection: undefined` when no paragraph w:bidi is set anywhere
   in the cascade, and the function would then return false instead of
   falling through to the legacy direction/dir field. Check the value, not
   the key.
2. Anchor the legacy direction/dir fallback as compat-fallback per
   comment-policy.md so future agents know what triggers it (no typed
   directionContext) and when it can be retired (SD-2778 collapses the
   duplicate field).
3. Document why `attrs.textDirection` is no longer in the chain. Per ECMA
   §17.18.93, ST_TextDirection values are writing-mode (lrTb/tbRl/btLr/
   lrTbV/tbRlV/tbLrV); none equal 'rtl'. The old check was always dead.

New test covers the precedence edge case.

---------

Co-authored-by: Caio Pizzol <caiopizzol@gmail.com>
…or (#3201)

Extract resumePackagePublish switch into per-descriptor resumePublish
functions and replace pkg.name === 'sdk' branches with capability checks
(pkg.pythonPackages, pkg.preparePythonSnapshot). No behavior change:
the recovery engine becomes generic so adding superdoc/react/vscode-ext
in follow-up PRs only adds adapters, not new switch arms.

The internal field state.sdkPythonPublished is renamed to
state.pythonPublished and recovery's returned snapshot field
sdkPythonSnapshot to pythonSnapshot. recordSdkPythonSnapshot keeps its
name so it continues emitting the sdk_python_snapshot_* GITHUB_OUTPUT
keys consumed by release-stable.yml.

Existing helper tests updated to match the refactored structure (the
intent - each package has its own explicit resume path - is preserved).
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
#3120)

* chore: save

* feat: update TOC entry in context menu

* fix: update TOC creating extra spaces and changing fonts

* test: created tests around TOC programmatic update

* refactor: removed unused code

* refactor: code style tweaks

* refactor: removed duplicate functions

* test(toc): add regression coverage for SD-2664 review findings

- tabLeader: 'none' must round-trip via serialize/parse (currently lost
  because no \\p is emitted when separator is missing, and the parser has
  no way to disambiguate "default = dots" from "explicit none").
- toc.configure({ tabLeader: 'none' }) on a default-leader TOC must not
  silently no-op (areTocConfigsEqual reports identical serialized output).
- toc.update mode: 'pageNumbers' must find tocPageNumber marks when the
  marked text is nested inside a run wrapper (the rebuild output shape).

All three tests fail on the current branch and lock in the regressions
flagged in code review.

* fix: toc context menu update

* refactor: simplified logic

* refactor: removed unnecessary test suite

* refactor: simplified tests

* chore: small comment tweaks

* test: added behavior test for multiple TOCs updates

* fix: inline partial selection to produce inline text

* fix: toc from empty to non-empty

* fix: early return on TOC update

---------

Co-authored-by: Gabriel Chittolina <gabrielchittolina1@gmail.com>
Co-authored-by: Caio Pizzol <caio@superdoc.dev>
… paragraphs (SD-2973) (#3210)

* fix(converter): preserve hyperlink mark on inserted text split across paragraphs (SD-2973)

SD-2858's preserveRaw path keeps tracked-change-wrapped fields structurally
intact when the field crosses paragraph or wrapper boundaries, avoiding the
import crash. For destructive wrappers (w:del / w:moveFrom) this trade-off
is invisible since the content disappears on accept, but for constructive
wrappers (w:ins / w:moveTo) the user keeps the inserted text and Word
shows it both as inserted AND as a clickable hyperlink — the previous
behaviour rendered it with insertion styling alone.

Add an `isConstructiveTrackChangeElement` predicate, propagate a
`preserveRawConstructive` flag through the field collector, and run a
post-pass that finds the visible runs (between separate and end fldChars)
and wraps them in `w:hyperlink` in-place. The surrounding paragraph and
tracked-change wrapper structure is left intact so the SD-2858 round-trip
guarantee still holds.

* refactor(converter): share hyperlink attribute resolution between field paths (SD-2973)

Per Luccas's review on PR #3210: the URL/anchor parsing and rels-element
construction in applyConstructiveFieldInterpretation duplicated the same
logic in preProcessHyperlinkInstruction.

Extract resolveHyperlinkAttributes(instruction, docx) as the single source
of truth for parsing a HYPERLINK field instruction into the attribute set
that belongs on a w:hyperlink element. Both preProcessHyperlinkInstruction
(the standard field path) and applyConstructiveFieldInterpretation (the
SD-2973 raw-preserved constructive-tracked-change path) call it.

Net change: ~-5 lines, single source of truth, no behaviour change.
* fix: footer tcs in replacement generating one per character (#2965)

* fix: footer tcs in replacement generating one per character

* fix: sdt with "contentLocked" not removable

* chore: removed console log

* fix: allow single click to target whole field

---------

Co-authored-by: Nick Bernal <117235294+harbournick@users.noreply.github.com>
Co-authored-by: Gabriel Chittolina <gabrielchittolina1@gmail.com>
…#3212)

Adds two color swatches to the toolbar that re-theme tracked changes and
comment highlights by writing the public --sd-* CSS variables on :root,
and centers the editor in its column with a max-width wrapper.

- Native <input type="color"> hidden behind a tb-btn label keeps the
  no-UI-kit posture of the demo.
- Icons inside each swatch use mix-blend-mode: difference so they stay
  legible across any picked color.
- The editor sits inside an .editor-canvas wrapper (max-width 880px,
  margin auto) so the document is centered between the toolbar and the
  activity sidebar.
* feat(workflows): notify homepage repo on stable superdoc release

Mirrors the promote-stable-docs.yml pattern: workflow_run on Release
superdoc, gated on success + stable, with a tag-diff against the
triggering head_sha to filter out semantic-release no-ops. Sends
repository_dispatch to superdoc-dev/homepage so a receiver workflow
there can open one bump PR per release.

Kept out of release-superdoc.yml on purpose: that job sits in the
release-stable concurrency group, and a homepage/token failure should
not mark a successful npm publish as failed.

* refactor(workflows): simplify notify workflow to a single step
* fix: clear transient hyperlink styleId on unlink

* test: add unlink regression coverage for transient hyperlink style cleanup

* fix(link): derive underline preservation at unlink time and add imported-link regression

* fix(link): tag paste-added underline as autoAdded so unsetLink removes it

When a user pastes a bare URL, handlePlainTextUrlPaste auto-converts it
to a link and adds an underline mark. The PR's autoAdded mechanism in
unsetLink only removes underline marks tagged autoAdded:true, so the
paste-added underline (untagged) was left behind on Remove Link.

Same one-line fix as setLink at link.js:247. Adds regression coverage
to relationships.test.js for: paste-URL unlink, setLink with longer
replacement text, mixed-underline selections, and re-setLink.

* test(link): drop paraphrase comments to match local convention

Existing tests in relationships.test.js use no inline comments;
removing the four added in the previous commit so the new tests
match local style and the comment policy.

---------

Co-authored-by: Caio Pizzol <caio@harbourshare.com>
Co-authored-by: Caio Pizzol <97641911+caio-pizzol@users.noreply.github.com>
…ands (SD-3083) (#3217)

* fix(super-editor): guard cached paragraph props lookup in indent commands (SD-3083)

Increase/Decrease Indent crashed when fired before the rendering pass had
populated the resolved-paragraph-properties cache (fresh load, freshly
inserted paragraphs). Falls back to compute-on-miss so the style cascade
is honored, instead of a bare guard which would stop the crash but apply
the wrong delta on style-derived indents.

Set/Unset commands keep the original code path - they don't read the
current indent and don't need the resolve work.

* test(super-editor): expand textIndent regression coverage (SD-3083)

Unit tests:
- decreaseTextIndent honors style-derived indent on cache miss (symmetric
  to the existing increase regression test)
- Cache hit short-circuits the compute-on-miss fallback - inverse of the
  set/unset opt-out test, guards the production || short-circuit

Behavior test (tests/behavior/tests/toolbar/paragraph-indent.spec.ts):
- Increase Indent on a fresh paragraph adds indent without crashing
- Decrease Indent removes the indent applied by Increase
- Repeated Increase compounds the left indent

* docs(super-editor): fix unsetTextIndentation @example typo

Pre-existing typo - example used `unsetTextIndent()` but the function
is `unsetTextIndentation`. Found during a comment audit on this branch.
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* feat(super-editor,painter): render images inside Word textboxes (SD-2804)

ECMA-376 §20.4.2.38 (CT_TxbxContent) lets a textbox hold rich body-level
content — paragraphs whose runs can carry inline w:drawing images. The
text-only extractor used to silently skip those drawings, so the textbox
rendered empty even though export round-tripped the image untouched.

The fix surfaces the inline drawing as a textContent part with kind='image'
so the existing shape painter can render it alongside text spans:

- TextPart contract gains optional kind/src/width/height/alt fields.
- extractTextFromTextBox.handleRun branches on w:drawing, reuses the v3
  wp drawing handler (handleImageNode) to resolve rId, then upgrades the
  path-style src to a data URI from converter.media so the painter can
  drop it straight into <img>.
- DomPainter's createFallbackTextElement renders image parts as inline
  <img> elements next to existing text spans.

Linked: SD-2745 (header-anchored floating textboxes — positions the box
where this content now renders).

* fix(super-editor,pm-adapter,painter): address PR #3207 review (SD-2804)

Per Luccas's review on PR #3207:

- (C1) Skip hidden textbox images. handleImageNode flags wp:docPr
  hidden="1" via attrs.hidden, but the new image-part branch only checked
  attrs.src and emitted visible <img>s for them. Top-level hidden drawings
  are filtered later in the pipeline; image parts bypass that filtering.
  Gate the textParts.push on imagePm.attrs.hidden !== true so hidden
  textbox drawings stay hidden, matching the body-level behaviour.

- (C2) Drop the duplicated resolveImagePartSrc helper in the importer
  (it rejected Uint8Array, breaking Y.js binary media). Store the raw
  path + extension + rId on the image part. pm-adapter's hydrateImageBlocks
  gains a vectorShape branch that hydrates textContent.parts alongside
  ImageRuns, so all media path candidates and the Uint8Array → TextDecoder
  decoding live in a single place.

- (C3) Anchored drawings inside textboxes are out of scope — wrap /
  position / transform metadata isn't carried into the text-parts model.
  Restrict the textbox-image branch to wp:inline and document the limit
  in the code comment so a future fixture can extend it intentionally.

- (C4) Align inserted images to the text baseline like body inline images
  do (vertical-align: bottom). ECMA-376 §20.4.2.8 specifies that an inline
  drawing behaves "like a character glyph of similar size", and the body
  inline image renderer defaults to vertical-align: bottom (renderer.ts
  ~L5770, L5847) — the textbox image part used vertical-align: middle,
  visibly misaligning text next to the image inside a textbox compared
  to outside it.
…stop) (#3204)

Adds superdoc to the stable orchestrator so the v* tag drives docs-stable
promotion in the same workflow that releases tools, removing the cross-
workflow concurrency-eviction problem for stable superdoc releases.

The orchestrator now groups packages by chain. Within a chain, fail-stop
applies as before (CLI failure skips SDK/MCP). Across chains, failures
are independent: a tools failure does not skip superdoc and vice versa.

Workflow rewiring:
- release-superdoc.yml stops auto-firing on stable; main pushes still
  publish prereleases, and workflow_dispatch is preserved for recovery.
- promote-stable-docs.yml triggers off release-stable.yml. The conclusion
  gate now accepts both success and failure - a tools-chain failure that
  follows a successful superdoc release should still promote docs. The
  inner git-tag detection (compare tags merged at the run's head_sha vs
  origin/stable) remains the source of truth, so tools-only runs still
  leave docs-stable alone.
- release-stable.yml header comments + step name updated to reflect the
  broader scope; the workflow's name field is unchanged so the existing
  workflow_run trigger and concurrency group continue to match. A rename
  is best as a follow-up cleanup PR.

Stacked on #3201 (descriptor refactor).
Adds react to the core chain after superdoc. react consumes `superdoc`
in dependencies, so releasing them in order through the orchestrator
means consumers never see a react release that pins to an older
superdoc than what just shipped.

- Adds `resumeReactPublish` adapter (uses generic `npm-publish-package.cjs`
  helper, idempotent against npm via dist-tag updates).
- Adds react descriptor to the `core` chain. A superdoc failure now skips
  react (fail-closed within chain); a tools failure still does not.
- `release-react.yml` stops auto-firing on stable; main pushes still
  publish prereleases.

Stacked on #3204 (superdoc orchestrator).
Completes the core chain: superdoc -> react -> vscode-ext. vscode-ext
publishes to the VS Code Marketplace (not npm) and ships a .vsix asset
on the GitHub release; the script's existing helpers already cover both,
so this PR adds the descriptor, the resume adapter, and the workflow
plumbing.

- `resumeVscodeExtPublish` runs `pnpm run package` then `vsce publish
  --skip-duplicate`; idempotent against the marketplace. In a tagged
  snapshot it also runs `build:superdoc` first so esbuild can resolve
  the webview's `superdoc` and `superdoc/style.css` imports through
  packages/superdoc/dist (snapshot only ran `pnpm install`).
- vscode-ext descriptor uses `vsCodeExtensionId` (no `npmPackages`), so
  `inspectPackageReleaseState` probes the marketplace, not npm.
- release-stable.yml's orchestrator step gains `VSCE_PAT` env so vsce can
  authenticate.
- release-vscode-ext.yml stops auto-firing on stable; main pushes still
  build .vsix attachments to the GitHub release.
- Three remaining `pkg.name === 'vscode-ext'` branches in the script
  (`getExpectedReleaseAssets`, `isGitHubReleaseComplete`, `ensureGitHubRelease`)
  switched to `pkg.vsCodeExtensionId` capability checks for consistency
  with PR #3201's refactor pattern.
@github-actions
Copy link
Copy Markdown
Contributor

The MCP ecma-spec tools aren't granted in this session, so I'll verify against ECMA-376 from cross-referenced knowledge of the schema.

Status: PASS

The three OOXML-touching files are spec-compliant:

trackChangeElements.js — The new CONSTRUCTIVE_TRACK_CHANGE_ELEMENT_NAMES = {'w:ins', 'w:moveTo'} categorization is correct per ECMA-376 Part 4 §17.13.5: w:ins (inserted run content) and w:moveTo (move destination) hold content the user keeps on accept; w:del and w:moveFrom hold content that disappears. See https://ooxml.dev/spec?q=ins and https://ooxml.dev/spec?q=moveTo.

encode-image-node-helpers.js — The new branch handles w:drawing > wp:inline inside w:txbxContent. This is a valid traversal:

  • w:drawing is a legal child of w:r (CT_R / EG_RunInnerContent).
  • w:txbxContent (CT_TxbxContent) accepts full block-level content, so paragraphs whose runs carry inline drawings are valid inside textboxes.
  • Limiting to wp:inline (skipping wp:anchor) is a reasonable scope choice and doesn't break the spec — anchored drawings are simply not supported yet inside the text-parts model.
  • The hidden-flag check via wp:docPr@hidden="1" matches CT_NonVisualDrawingProps.

The inline comment cites "ECMA-376 §20.4.2.38" — that reads slightly off (WordprocessingML lives in clause 17 of Part 1/4; clause 20 is DrawingML), but it's a comment, not a spec violation, so not flagging.

encode-image-node-helpers.test.js — The fixture is minimal (omits pic:nvPicPr / pic:spPr, both required by CT_Picture), but that's a unit-test mock for the import path, not a roundtrip target. The implementation under test only reads pic:blipFill > a:blip@r:embed, which is the right path per ECMA-376 §20.2.2 / §20.4.2.

No non-existent elements, no missing-required-attribute writes, no wrong defaults. Looks good.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f57130571a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread .github/workflows/notify-homepage-superdoc-release.yml Outdated
@codecov-commenter
Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

caio-pizzol added a commit that referenced this pull request May 11, 2026
buildTocEntryParagraphs wraps the page-number text inside a run, so the
tocPageNumber mark lives one level below the paragraph's direct
children. sanitizeTocContentForSchema only filtered marks on direct
children, so any schema that omits tocPageNumber (the case the
sanitizer exists to support) would still receive the unknown nested
mark and nodeFromJSON would fail when rebuilding the TOC.

Extracts the per-node mark-stripping into a recursive helper that
walks content arrays, so the sanitizer works regardless of how deep
buildTocEntryParagraphs nests the run wrapper.

Flagged by chatgpt-codex-connector in #3228.
caio-pizzol added a commit that referenced this pull request May 11, 2026
buildTocEntryParagraphs wraps the page-number text inside a run, so the
tocPageNumber mark lives one level below the paragraph's direct
children. sanitizeTocContentForSchema only filtered marks on direct
children, so any schema that omits tocPageNumber (the case the
sanitizer exists to support) would still receive the unknown nested
mark and nodeFromJSON would fail when rebuilding the TOC.

Extracts the per-node mark-stripping into a recursive helper that
walks content arrays, so the sanitizer works regardless of how deep
buildTocEntryParagraphs nests the run wrapper.

Verified empirically before fixing:
- Reproduced the bug against a real prosemirror-model Schema lacking
  the tocPageNumber mark: unfixed sanitizer leaves the nested mark and
  nodeFromJSON throws 'There is no mark type tocPageNumber in this
  schema'; fixed sanitizer strips it and nodeFromJSON succeeds.
- Codified as 4 unit tests in toc-wrappers.test.ts (sanitizer exported
  for testing). With the fix reverted, 2 tests fail with the exact
  surviving-mark assertion. With the fix applied, all 13 tests pass.

Flagged by chatgpt-codex-connector in #3228.
caio-pizzol and others added 3 commits May 11, 2026 13:26
Stable superdoc releases now ship via release-stable.yml (named
'📦 Release stable tooling (CLI/SDK/MCP)'), so listening to
'📦 Release superdoc' on workflow_run silently stopped firing for
stable releases. Mirrors the trigger change already in place on
promote-stable-docs.yml.

- Trigger now matches the orchestrator workflow name.
- Accept conclusion: failure too, so a tools-chain failure following a
  successful superdoc release still notifies (chain-independent
  orchestrator semantics).
- Verify both 'superdoc' and '@harbour-enterprises/superdoc' on npm
  before dispatching; semantic-release's prepare phase pushes the v*
  tag before publish, so a publish failure that does not recover would
  otherwise notify on an unpublished version.
buildTocEntryParagraphs wraps the page-number text inside a run, so the
tocPageNumber mark lives one level below the paragraph's direct
children. sanitizeTocContentForSchema only filtered marks on direct
children, so any schema that omits tocPageNumber (the case the
sanitizer exists to support) would still receive the unknown nested
mark and nodeFromJSON would fail when rebuilding the TOC.

Extracts the per-node mark-stripping into a recursive helper that
walks content arrays, so the sanitizer works regardless of how deep
buildTocEntryParagraphs nests the run wrapper.

Verified empirically before fixing:
- Reproduced the bug against a real prosemirror-model Schema lacking
  the tocPageNumber mark: unfixed sanitizer leaves the nested mark and
  nodeFromJSON throws 'There is no mark type tocPageNumber in this
  schema'; fixed sanitizer strips it and nodeFromJSON succeeds.
- Codified as 4 unit tests in toc-wrappers.test.ts (sanitizer exported
  for testing). With the fix reverted, 2 tests fail with the exact
  surviving-mark assertion. With the fix applied, all 13 tests pass.

Flagged by chatgpt-codex-connector in #3228.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants