Merge main into stable#3378
Conversation
* 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.
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.
* fix: missing headers in collab mode; wrong odd/even headers * fix: rebuild cache and clean up * fix: improve odd/even header selection
…el (SD-3109) (#3243) The Document API write side accepts text offsets in a flattened model: text contributes its length, leaf atoms contribute 1, inline wrappers (run, etc.) contribute 0. Position readback was using raw PM arithmetic (`pos - resolved.start(depth)`), which counts the wrapper boundary tokens the flattened model skips. Result: `editor.doc.bookmarks.list()` returned offsets that drifted by the number of inline wrappers in the targeted block. - Route both readback helpers through `pmPositionToTextOffset` so read and write share one offset model. - Same fix applied in `permission-ranges-adapter.ts`, which carried a parallel copy of the buggy helper. - Regression test for the round-trip invariant (write at offset N, list returns offset N). - Includes regression coverage for SD-3108 (content-control wrap by text offset). That bug did not reproduce on main; the tests stay as guardrails.
…(SD-2781) (#3203) * feat(pm-adapter): preserve run-level bidi/script metadata on TextRun (SD-2781) Adds two preservation-only fields to the layout text-run contract, kept on separate axes per ECMA's own categorization: - TextRun.bidi (RunBidiContext): direction signals only - run rtl flag now, embedding (w:dir) and override (w:bdo) wired in Wave 1c. - TextRun.script (RunScriptContext): complex-script flag + per-script language metadata (default / complexScript / eastAsian) per §17.3.2.20. Both populated by pm-adapter from raw run properties when present. Wave 1a does not render either; Wave 1b will gate the formatting-stack selection on script.complexScript, Wave 1c will read bidi.embedding/override. Why now: ECMA puts direction (rtl, dir, bdo) and script formatting (cs, lang/@bidi) in different categories. Lumping them under one bidi field would collapse the axes and lie about the schema. Adding both fields now means Wave 1b/1c don't have to introduce both the data path and the rendering at once. The RunScriptContext.language field expanded from a single string to a structured object with three optional tags. No production consumer reads the field yet (preservation-only since #3184), so the shape change is safe. Tests: - bidi populated only on rtl, script populated only on cs/lang - explicit rtl=false preserved (a meaningful override) - the three lang tags land on separate fields - axis non-collapse: rtl never leaks into script, cs never leaks into bidi * fix(contracts): import RunBidiContext + RunScriptContext locally for TextRun The previous commit added `bidi?: RunBidiContext` and `script?: RunScriptContext` to TextRun in contracts/src/index.ts but only re-exported the type names; they were not in local type scope, so `tsc --project` failed: src/index.ts(324,10): error TS2304: Cannot find name 'RunBidiContext'. src/index.ts(330,12): error TS2304: Cannot find name 'RunScriptContext'. Vitest's transform doesn't enforce type-only imports the way tsc does, so the unit tests passed even though the build was broken. CI build would have caught it; the missing piece was running `pnpm build` locally before pushing. Adds the two names to the existing local `import type` statement alongside ParagraphDirectionContext (same pattern, same line 19). Also adds two integration tests in pm-adapter/src/integration.test.ts that exercise the full PM -> FlowBlock conversion through the unmocked applyInlineRunProperties pipeline. The previous unit tests in common.test.ts mock computeRunAttrs, which is why they couldn't catch shape-level regressions in the contracts package. The integration tests prove a real PM doc with raw runProperties (rtl/cs/lang on a run-wrapper node) produces a TextRun with populated bidi/script, and that runs without signals don't gain empty objects. * docs: drop duplicate ticket prefix on SD-2781 test block comment Repo convention puts the ticket reference in the `describe` label only (e.g., `describe('SD-1333: ...')`). Trims the redundant `SD-2781:` prefix from the block comment, keeps the non-obvious "Wave 1a preserves these signals; nothing renders them yet" note. * fix(pm-adapter): read bidi/script from raw inline runProperties + reassign on token runs Two codex findings on PR #3203: 1. Cascade-leak: applyInlineRunProperties was receiving cascade-resolved runProperties from runNodeChildrenToRuns and populating bidi/script from them. Style-inherited runs ended up with bidi/script metadata they did not have inline, making preservation indistinguishable from direct formatting and bloating the layout tree on every styled run. Fix: thread the raw inline runProperties through InlineConverterParams (alongside the existing cascade-resolved runProperties). applyInlineRunProperties gains a fourth parameter that bidi/script populate from. When the caller doesn't opt in (no inline parameter), no metadata is attached - safer default than reading the cascaded view. 2. Token-run drop: generic-token.ts called applyInlineRunProperties without reassigning the return value. Since the helper builds a new object via spread, all merged fields (including the SD-2781 bidi/script) were lost for page-number / total-page-count token runs inside an rtl run wrapper. Fix: change the local to `let` and reassign. Also forwards inlineRunProperties through. no-break-hyphen.ts (another caller) updated for consistency. Tests added: - 3 unit tests in common.test.ts proving bidi/script populate from inlineRunProperties only, never from cascade-resolved runProperties - Existing 7 SD-2781 unit tests updated to pass inlineRunProperties (the new opt-in source) - 1 integration test proving bidi/script propagate to page-number tokens inside an rtl run wrapper All 1800 pm-adapter tests, 12644 super-editor tests, and 1201 layout-bridge tests pass. Contracts + pm-adapter both build clean. * fix(pm-adapter): cs absence != false + forward inlineRunProperties through nested converters Two round-3 findings on PR #3203: 1. RunScriptContext.complexScript was required (boolean), so a run with only w:lang and no w:cs ended up with { complexScript: false, language: {...} }. Per ECMA §17.3.2.7, absent w:cs inherits from the style hierarchy and ultimately falls back to Unicode-based script detection - it is NOT the same as explicit false. Misrepresenting absence as false would mislead Wave 1b CS-formatting selection. Fix: make complexScript optional. Only set it when raw cs is explicitly present (true OR false - both are meaningful toggle states per §17.17.4). Leave undefined otherwise so consumers can fall back correctly. 2. Three nested inline converters (page-reference, bookmark-start, structured-content) called visitNode without forwarding the new activeInlineRunProperties arg, so text inside a PAGEREF result, a bookmark span, or an SDT wrapper lost run-level bidi/script metadata even when the enclosing run wrapper had it. Fix: thread inlineRunProperties through all three converters. In page-reference's bookmark path, also pass the locally-scanned run's raw runProperties (not the cascade-resolved version) as inlineRunProperties for the synthesized token run. Tests: - cs absent + only lang -> script exists, complexScript is undefined - cs=false explicit -> script.complexScript === false (meaningful off) - existing 'three lang tags' test unchanged - new integration test: text inside structuredContent wrapper preserves bidi/script from the enclosing run wrapper All 1803 pm-adapter tests pass (+3 new), 12644 super-editor pass, contracts + pm-adapter both build clean. * docs: drop round-counter prefix from SD-2781 integration test header Per project CLAUDE.md, code comments shouldn't reference iteration / review state ("round-3 (codex finding)") - those belong in the PR description and rot once the PR merges. The load-bearing content (which converters, what invariant) stays. --------- Co-authored-by: Caio Pizzol <caiopizzol@gmail.com>
* feat(types): deep public-type audit gate (SD-2977) The existing public-type checks lock in exported type names and top-level `any` assertions, but they do not catch `any` on public members, callback params, return types, or nested type arguments. Consumers can still lose IntelliSense after touching a public API surface that resolves to `any`. Add a deep public-type audit that builds a TypeScript Program from the packed-and-installed `superdoc` tarball, recursively walks every owned type reachable from public export entries, and fails CI if a finding is not in `deep-type-audit.allowlist.json`. Also fails on stale allowlist entries (so fixes must remove their entry), on compiler diagnostics in the published surface, and on private `@superdoc/*` specifiers that survived rewriting in the installed package. Allowlist seeded with the current 293 owned findings, tagged by tier so follow-up PRs can drain by surface (Pinia stores, toolbar config, helpers, curated public contract, other). The gate runs in PR CI and release CI, after the consumer matrix prepares the installed tarball. Closes the gap where release publish did not run packed consumer checks. The allowlist is a starting line, not a waiver. Per the README: do not drain by replacing `any` with `unknown` unless the value is genuinely opaque - prefer precise upstream or local types so IntelliSense actually restores. Verified locally: - node tests/consumer-typecheck/typecheck-matrix.mjs: 53 passed - node tests/consumer-typecheck/deep-type-audit.mjs: 293 findings, 0 new, 0 stale - removing an allowlist entry produces NEW finding output and exit 1 * fix(types): walk construct signatures and index types in deep audit (SD-2977) Codex review on the initial PR pointed out two walker blind spots: 1. The walker only visited `getCallSignatures()`, so a public `constructor(...args: any[])` (SuperConverter, DocxZipper, and others) shipped without producing a finding. 2. The walker only enumerated `getProperties()`, so `[key: string]: any` index signatures on public classes were never traversed. Both gaps confirmed in the installed dist before the fix. Walker now visits construct signatures alongside call signatures and queries `getStringIndexType()` / `getNumberIndexType()` for index members. New findings are tagged `tier-4-public-contract` for SuperConverter and DocxZipper (per SD-2966's done-when criteria) and `tier-5-other` for the remaining constructor-surface findings. Allowlist regenerated: 293 → 337 entries. Verified: - node tests/consumer-typecheck/deep-type-audit.mjs: 337/0/0 PASS - SuperConverter[string] and DocxZipper[string] now in tier-4 * fix(types): close 3 deep-audit walker blind spots (SD-2977) Code review surfaced three coverage gaps in the seeded allowlist: 1. Sibling members sharing an instantiated container type (`a: any[]; b: any[]`) were path-order-dependent. TypeScript caches `Array<any>` and `Promise<any>` per-shape so siblings share a type id; the persistent-visited gate short-circuited the second sibling before its inner `any` could be recorded. Fix: pre-record `any` inside array elements and type arguments BEFORE the visited gate. Cycle protection still applies to structural members. 2. Class exports walked only the instance side (getDeclaredTypeOfSymbol), so `constructor(...args: any[])` and `static foo(): any` on public classes (SuperConverter, DocxZipper) never produced a finding. Fix: walkExport now also walks the value type (getTypeOfSymbolAtLocation) when it differs from the declared type, prefixed with `.<value>` so constructor / static findings are clearly distinguished. 3. `--pack` bootstrap failed on fresh checkouts because typescript was resolved at module-load (line 49) before the doPack block (line 52) that runs the fixture install. Fix: move the typescript require after the optional pack-and-install block so the documented bootstrap command actually works on a clean tree. A stack-scoped visited variant was tried first but caused combinatorial blow-up on highly interconnected types (16+ minutes with no progress on the public surface). The pre-record + persistent-visited combination keeps cycle protection bounded while still catching sibling regressions. Allowlist regenerated: 337 -> 678 entries, with new tier-4 entries covering SuperConverter / DocxZipper class-side `any`s and tier-5 absorbing the long tail of sibling-shared findings. Verified: - node tests/consumer-typecheck/deep-type-audit.mjs: 678/0/0 PASS - SuperConverter.<value>.new(args)<0>, .new(args)[], static methods all in tier-4 - DocxZipper.<value>.new(args)<0>, .new(args)[] all in tier-4 - Run completes well under 60 seconds * fix(types): raise deep-audit depth cap and surface truncation (SD-2977) A third review round flagged three potential walker issues. Two were stale (P1: SuperConverter/DocxZipper class-side findings — already caught in fcf84a2, 16 entries in allowlist; P2: visited-singleton-any skip — already prevented by isAnyType returning before the visited gate in walkType lines 220-223). The third is real: MAX_DEPTH=8 silently truncated 302,642 paths in a single audit run, leaving deep public types invisible to the gate and contradicting the README's claim of walking every reachable type. Persistent visited handles cycles, but TypeScript materializes generic instantiations on demand with fresh type ids that visited cannot dedupe, so the cap is load-bearing memory protection (cap=256 OOM'd at ~4GB). Empirical sweet spot at MAX_DEPTH=16: deep enough to reach 451 more legitimate findings (allowlist 678 -> 1129), shallow enough to bound memory. depthCapHits counter now surfaces in the run report so any remaining truncation is visible rather than silent. The new findings are concentrated in pinia store internals, vue reactive chains, and prosemirror type expansions — surface areas the team already knows are noisy. The cap warning lets future maintainers decide whether further bumps are worth the memory cost. Verified: - node tests/consumer-typecheck/deep-type-audit.mjs: 1129/0/0 PASS - WARN line surfaces depth-cap hits in the report - No OOM at cap=16 * fix(types): cleanup audit per code review (SD-2977) Three small cleanups bundled: 1. Strip em dashes from README and code comments. User CLAUDE.md prohibits em dashes in all output including code comments and docs; replaced with ":", "(", or "." per surrounding grammar. 2. Drop "flagged by Codex on the initial PR, fixed here" trailing clauses on two comments. The surrounding rationale (call sigs vs construct sigs; index sigs vs properties) is load-bearing and stays; only the review-process narration goes. CLAUDE.md says comments should not reference the current task or fix. 3. Fix walkExport's stale "stack-scoped" comment + add visited snapshot/restore around the value-side walk. The previous comment claimed visited pops on exit (the failed try/finally variant), but the actual code uses persistent per-root visited. That meant structural types reachable from both an export's instance and value sides were silently skipped on the value side. Snapshot/restore scopes the value walk's visited freshly without polluting subsequent exports' declared walks. The visited fix surfaced 670 previously-hidden findings (1129 -> 1799), 990 of which live on .<value> paths (class value side). Composition reinforces the umbrella framing: most additional findings are Pinia store internals and other surfaces that SD-2966's facade should hide, not type. Verified: - node tests/consumer-typecheck/deep-type-audit.mjs: 1799/0/0 PASS - 0 em dashes remaining in either file - 0 "flagged by Codex" comments remaining Two related walker gaps (sig.thisParameter and `<T = any>` defaults) were investigated and confirmed real but have zero current matches in the published superdoc dist; deferred as future-hardening rather than landed pre-emptively. * refactor(types): pivot audit to report-only inventory; defer gate to post-SD-2966 (SD-2977) Three independent code-review opinions converged on the same conclusion: landing a 17K-line allowlist of accidental public surface is the wrong move. The current 1799 findings are largely from Pinia stores, EventEmitter generics, Vue SFC component types, and other code that was never deliberately committed as public API. Committing them as an allowlist would risk legitimizing internals as the type contract and forcing the team to type things that should be hidden. Pivot: 1. Delete the 17K-line `deep-type-audit.allowlist.json`. The walker stays; the artifact does not. 2. Make the audit report-only by default. Always exits 0 unless the script itself errors. Prints inventory: total findings, by-tier counts, top files, depth-cap warnings. 3. Add `--strict` flag for the eventual gate behavior. Not used in CI yet because it's only meaningful once SD-2966 defines the public facade and the allowlist is re-seeded against that smaller surface. 4. CI workflows updated: both PR CI and release CI run the audit in report-only mode. Comments explain that `--strict` is added later. 5. README rewritten with prominent "Status: report-only inventory" at the top, explaining what defers the gate and what re-emerges after SD-2966. What this delivers today: - The walker logic ships and runs in CI from day one - The 10K-line public artifact goes away - Inventory data appears in every CI run (visible signal of how much accidental surface is leaking) - No commitment to typing 1799 entries on accidental surface What this defers: - Hard gate against new regressions on the current accidental surface. Net cost: a few weeks where new `any` could land on already-`any`-heavy code. Acceptable because the work to drain that surface shouldn't start before SD-2966 anyway. Verified: - node tests/consumer-typecheck/deep-type-audit.mjs: PASS, exit 0 - node tests/consumer-typecheck/deep-type-audit.mjs --strict: FAIL, exit 1 - node tests/consumer-typecheck/deep-type-audit.mjs --write: still works (creates allowlist; intended for use post-SD-2966) * refactor(types): make audit report-only; add --strict for post-SD-2966 gate (SD-2977) Companion to the allowlist removal: rewires the walker so default mode prints inventory and always exits 0, while a new --strict flag preserves the original gate behavior for use once SD-2966 defines the public facade. - Compiler diagnostics, private specifier leaks, and allowlist comparison all switch from process.exit(1) to print-and-continue unless --strict is set - New report section prints by-tier and top-files breakdowns regardless of mode (visible CI signal) - When no allowlist file exists, the run prints an explanatory note pointing at SD-2966 and the --write workflow - CI step comments updated in both ci-superdoc.yml and release-superdoc.yml to reflect the report-only intent - README rewritten with prominent "Status: report-only inventory" section at the top; commands section documents --strict and clarifies --write is intended for post-SD-2966 baselining Verified: default exits 0, --strict exits 1, --write still seeds an allowlist correctly.
#3199) * feat(pm-adapter): plumb body section direction context through resolver Per ECMA §17.3.1.41, paragraph w:textDirection inherits from the parent section when omitted. The previous call site built sectionContext from `undefined`, so directionContext.writingMode was always 'horizontal-tb' even when the body's w:sectPr declared a vertical writing-mode. This wires SectionDirectionContext through ConverterContext, populated once at top-level conversion from the body sectPr. Paragraphs that omit their own w:textDirection now correctly inherit writing-mode. Scope: - Body-level sectPr only. Per-paragraph-section variation (each section with its own sectPr) and table-cell direction context are not yet plumbed through. Both gaps are documented inline and tracked under SD-2777 (migrate remaining direction-aware consumers). - No consumer currently reads directionContext.writingMode in production, so this fixes the data contract before the first consumer arrives. Tests: - New: paragraph inherits body sectionDirectionContext.writingMode - New: paragraph w:textDirection still wins as explicit override * fix(pm-adapter): recompute sectionDirectionContext per-document Codex finding on PR #3199: when callers reuse one ConverterContext across documents (toFlowBlocksMap does this), the previous `??` cache let the first document's body sectPr resolve once and stick. A vertical doc 1 followed by a horizontal doc 2 would have doc 2's paragraphs inherit doc 1's writing-mode. Fix: drop the `??` and always overwrite. The shared ConverterContext is mutated freshly each call before children read it, so per-document recomputation is enough. (The pre-existing `sectionDirection` field on the line above has the same pattern but is out of scope for this PR.) Test added: toFlowBlocksMap with two docs (vertical w:textDirection then horizontal w:textDirection) sharing one converterContext - asserts each doc's paragraphs get their own writingMode. Failed before the fix because the cached vertical-rl persisted into the horizontal doc; passes after. --------- Co-authored-by: Caio Pizzol <caiopizzol@gmail.com>
* fix(mcp): rename wire fields to contract names before dispatch The MCP wire schema (generated from apps/cli operation params) exposes PARAM_FLAG_OVERRIDES renames such as `commentId` → `id` and `parentCommentId` → `parentId`. Without an inverse translation at dispatch time, the contract validator rejects the wire field names because it expects the canonical contract names — so `superdoc_comment` actions `delete`/`get`/`update` fail with `comments.<action> commentId must be a non-empty string` even when the caller follows the documented schema. The CLI applies the inverse rename in extractInvokeInput() (apps/cli/src/lib/invoke-input.ts PARAM_RENAMES). This commit adds the matching layer for the MCP path: a small `applyParamRenames` helper in a standalone module, applied in `executeOperation` immediately before `api.invoke`. Affected operations: comments.create (parentId → parentCommentId), comments.patch / comments.delete / comments.get (id → commentId), getNodeById (id → nodeId). Includes unit tests for the rename helper. * review: fix comment count/style and document output-rename asymmetry - param-renames.ts: fix "Three" → "Five" (5 operations, not 3) and replace em-dash per style rule (caio-pizzol review suggestion). - intent.ts: add AIDEV-NOTE explaining why input renames are not inverted on output — canonical names in responses are usable directly, so inverting adds complexity for no practical benefit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * review: clarify output-rename asymmetry is incidental, not deliberate Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Caio Pizzol <97641911+caio-pizzol@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* fix: align rtl date token rendering with word parity Fix RTL date rendering parity with Word per SD-3098. The browser's UBA does not reorder numerics inside an RTL run the way Word does, and run-boundary separator drift breaks mixed-direction date strings like `-03-23` + `2026`. The fix is paint-time only - it never touches PM/model/export: 1. DomPainter sets dir="rtl" on the span when run.bidi.rtl === true (per-run bidi isolation eliminates run-boundary separator drift) 2. DomPainter sets dir="ltr" on date-like LTR runs (per regex) inside RTL contexts (prevents the third case where a non-rtl date inside an rtl paragraph reorders unpredictably) 3. normalizeRtlDateTokenForWordParity injects U+200F (RLM) around `./- ` separators inside RTL date-like text so Word and SuperDoc render the same visual order (e.g. XML `23/03/2026` -> visual `2026/03/23`) Three test cases in rtl-dates.docx (the Linear-attached "Date Being Weird" fixture): - Header: single <w:rtl/> run `23/03/2026` -> Word visual `2026/03/23` - Body 1: LTR run `-03-23` + RTL run `2026` -> Word visual `2026-03-23` - Body 2 (control): single LTR run `2026/03/26` -> unchanged Other changes: - bidiCompatible guard in mergeAdjacentRuns: prevents a <w:rtl/> run from merging with a plain run and silently losing the bidi flag - run-visual-marks.ts and versionSignature.ts: include run.bidi in the caching hashes so a rtl-only edit invalidates measure/DOM cache - New painter unit tests (rtl-date-parity.test.ts) verifying dir + RLM - New behavior spec (rtl-dates-word-parity.spec.ts) using the real fixture This PR builds on the run-level bidi metadata SD-2781 (#3203) added to the TextRun contract - reads from TextRun.bidi.rtl (the merged shape), not a parallel RunMarks.bidiContext field. * test(rtl-date-parity): cover bidi hash + block-version + painter edge cases Adds pre-merge coverage for SD-3098 rendering invariants: - hashRunVisualMarks: bidi field changes the dirty-run hash (rtl=true vs absent, rtl=true vs rtl=false, embedding-only changes). Stale hashes would let an edit that flips just <w:rtl/> reuse stale measure/DOM. - deriveBlockVersion: bidi flips invalidate the cached block version. Without this, the painter would reuse a cached block snapshot after an rtl-only edit. - DomPainter painter tests: mixed rtl + ltr runs on the same line stay as separate spans with distinct dir attrs; non-date-like rtl runs keep dir="rtl" without RLM injection; non-rtl plain text leaves the span without a dir attribute. Also adds rtl-mixed-run-line.docx + behavior spec as a negative test asserting Hebrew + date + Hebrew paragraphs don't regress (Hebrew runs stay rtl, date run is not RLM-injected since it isn't rtl-tagged). --------- Co-authored-by: Caio Pizzol <caiopizzol@gmail.com> Co-authored-by: Caio Pizzol <97641911+caio-pizzol@users.noreply.github.com> Co-authored-by: Caio Pizzol <caio@superdoc.dev>
* feat(types): harden package shape (SD-2978)
* fix(types): gate stable release with same package-shape checks (SD-2978)
The original SD-2978 commit only added consumer matrix + package-shape
gates to release-superdoc.yml. Between sessions, release-stable.yml
landed as the new central orchestrator for the npm `latest` publish
lane, and SuperDoc's stable releases now route through that workflow
instead of release-superdoc.yml (which is now `@next` only).
Without this patch the stable lane would publish without the matrix or
publint/attw gates running, defeating the purpose of "package-shape
honest in CI" because the most-consumed dist-tag would still be
unverified at publish time.
Adds the same three steps (matrix, deep-type-audit, package-shape-gate)
between Build packages and the orchestrator step in release-stable.yml.
* fix(types): teach deep audit to read nested types: { import, require } (SD-2978)
The deep audit assumed `entry.types` is always a string. SD-2978's
manifest changes nest it as `{ import: '...d.ts', require: '...d.cts' }`
for the three entries that publish CJS. The audit threw
ERR_INVALID_ARG_TYPE on `path.resolve(root, entry.types)` when entry.types
was an object.
Add a small helper that picks the ESM target from either shape (string
or condition object). Walking the .d.ts side is sufficient because the
.d.cts is a generated shim of the same surface.
Verified: audit now exits 0 with the same 1799 findings as pre-fix.
* fix(types): make sanitizer re-entrant + route pack:local through pack:es (SD-2978)
Code review found that `pnpm run pack` was broken by the new
prepack/postpack lifecycle. pnpm wraps prepack/postpack around scripts
named exactly `pack`, and the user `pack` script itself invokes
`pnpm pack` which triggers a second prepack. The inner prepack hit the
"backup exists" guard and exited 1, the outer postpack was skipped, and
the workspace was left with `package.json` mutated and
`.package.json.prepack-backup` orphaned.
Two changes:
1. Make `prepare` re-entrant. If the backup file exists AND the current
manifest already looks sanitized (no `source` conditions, no `unpkg`
or `jsdelivr` fields), no-op so the inner prepack falls through and
the inner postpack can restore cleanly. If the backup exists but the
manifest is NOT sanitized, fail loudly with a clear message — that
means the workspace is in an inconsistent state from a previous
failed pack and the developer needs to clean up. `restore` was
already idempotent (no-op when backup missing).
2. Route `pack:local` through `pack:es` directly. Both ultimately do
the same thing, but going through `pack:es` (whose name does not
collide with the lifecycle trigger) avoids the double-fire on the
common local-pack path.
Verified with a synthetic harness covering 5 cases: clean run,
double-fire (outer + inner), failed run (state inspection), retry after
failure (self-heal), inconsistent state (loud refusal). All pass.
Verified live in the worktree:
- pnpm run pack:es: tarball created, manifest restored, no orphan backup
- pnpm run pack: same (was broken before this commit, now works)
…nt-api-alignment feat(types): document api alignment for public facade (SD-3185)
* feat: improve format painter ux to match ms word * fix: address review comments * fix: calculate state for headless mode * fix: build issue
* feat: add toc toolbar item * fix: move toc icon to overflow menu * fix: check if toc node can be inserted * fix: address comments
…ta-api feat(document-api): metadata.* for anchored payloads (SD-3104)
Add facade entries for the two remaining Phase-3 legacy framework helpers.
Each entry re-exports useHeadlessToolbar only. Mirrors SD-3179's posture
for ./headless-toolbar root and keeps the path-as-contract facade
complete for everything except the deferred ./super-editor leaf (SD-3181).
- src/public/legacy/headless-toolbar-{react,vue}.ts: single named export.
- verify-public-facade-emit.cjs: 2 new FACADE_ENTRIES, both with cjs path
for Phase 4 readiness (mirrors the existing SD-3179 entry shape).
- ensure-types.cjs: 2 new .d.cts shim entries (now emits 8 shims total).
- vite.config.js: 2 new entries in rollupOptions.input.
- 2 smoke tests covering the runtime re-exports.
No package.json#exports change. Phase 4 owns the contract flip.
Verifier: 10 entries clean. Consumer-typecheck: 57/57 matrix scenarios
pass, snapshots unchanged. Smoke tests 2/2.
Customer-facing citation demo built on the metadata.* contract from SD-3104. Customer-facing counterpart to the developer-shaped SD-3199 demo (#3370, draft); both lean on the same anchored-metadata model. What this shows: - Generate draft with sources — mocked stand-in for a chat-driven RAG pipeline. Inserts a pre-canned paragraph and attaches citations to specific phrases. - Sources sidebar grouped by sourceId, showing source title, type, provider, deep link, and the cited locations within the document. - Hover popover with displayText, locator, excerpt, provider, confidence — the verification surface so a lawyer can trust the output without leaving the doc. - Highlight overlay on cited spans using getRect.rects, so line-wrapped citations get clean per-line underlines visible through the text. - Scroll-to, Edit, Remove per citation. All six metadata.* methods exercised. What this does not show: - The button is a stand-in for a chat-driven AI workflow, not the recommended product UX. A real integration plugs a chat panel + Insert into document action. - Generation is mocked. No LLM is wired. - Verification status is a render-time concern; external verification signals can change over time, so it is not persisted in the payload.
…SD-3208) - Rename 'Generate draft with sources' to 'Insert sample cited draft' so the button doesn't overclaim — the demo is mocked, not a real AI pipeline. - Match the help text and empty-state copy. - Replace the hardcoded rgba on .citation-highlight with color-mix against --accent so the highlight tints follow theme changes.
Repeated clicks were colliding on hardcoded citation ids and surfacing 'Anchored metadata id already exists' errors in the UI. Each draft holds fixed ids (cite-001, cite-002, cite-003); the cycling-by-index logic inevitably re-inserted them on the third click and failed. Single-shot now: one click inserts every MOCK_DRAFTS paragraph and attaches every citation in one go. The button hides as soon as citations exist, so the click-collision path is unreachable. To re-run the demo, remove the citations from the sidebar or reload the page.
…ss-toolbar-react-vue feat(public-facade): legacy headless-toolbar/react and /vue (SD-3207)
… partial attach (SD-3208) - mockDraft.ts: drop vendor names and the stale 'Generate draft with sources' label from the file header. Vendor/product research belongs in the PR/Linear notes, not source comments. - GenerateDraftButton: if a partial-attach failure leaves an error mid-flight, keep the component mounted (and the error message visible) instead of silently disappearing on the next render. The early-return now gates on 'has citations AND no error'.
… (SD-3201)
Closes the round-trip gap from SD-3104: SuperDoc -> DOCX -> Word save /
edit -> SuperDoc. The SuperDoc -> DOCX -> SuperDoc round-trip was already
proven by tests/doc-api-stories/tests/metadata/all-commands.ts; this adds
Word in the middle as deterministic fixtures replayed in CI.
Two fixtures, both generated from one SuperDoc-side source (one paragraph,
one anchored citation 'fixture-cite-001' over 'duty of care'):
- baseline-word-resaved.docx: opened in Word, re-saved without edits.
Proves Word does not strip the customXml part or the hidden SDT
wrapper on save.
- baseline-word-edited.docx: opened in Word with text inserted inside
the cited span. Proves the SDT survives content edits and the anchor
expands to cover the edited content (no orphan, no detach).
Replay test under tests/doc-api-stories/tests/word-roundtrip/:
- list({ namespace }) returns the citation
- get({ id }) returns the payload byte-for-byte (Word does not
renormalize the customXml part)
- resolve({ id }) returns a SelectionTarget
- list({ within: resolvedTarget }) confirms the anchor still covers
the post-edit range
What this does not cover
- New Word version regressions. A separate live script gated on Word
API access is the pre-customer-rollout pre-flight; not in this PR.
- Word's 'Inspect Document' / Document Inspector cleanup, which can
strip Custom XML Data as 'personal information.' Documented as a
known limitation in the namespace adoption guide (SD-3209).
…3201) Soften the inline comment to match what the test actually asserts: the anchor did not detach (resolve returns a selection target with text endpoints), and the citation is still listable via within-scoped list. Text-content equality of the resolved range against the edited string is explicitly not asserted here. Also drop the 'byte-for-byte' wording on the no-edit fixture — the assertion is parsed-payload equality, not raw XML byte equality.
…-3211) TypeScript contract switch for 9 scaffolded subpaths only. Each subpath's `types` and `typesVersions` field now resolves through the curated `dist/superdoc/src/public/**` declarations built by SD-3178 and its leaves. Subpaths flipped: ./types, ./ui, ./ui/react, ./headless-toolbar, ./headless-toolbar/react, ./headless-toolbar/vue, ./converter, ./docx-zipper, ./file-zipper. Explicitly NOT in this PR: Root `.` entry stays on historical types. Dry-run found a 196-name consumer-fixture surface vs the 24-name SD-3185 curated facade. The mismatch requires classification before flipping; tracked as Phase 4b (SD-3212). `./super-editor` entry stays on historical types (SD-3181 deferred). No `import` / `require` / `source` field changes. Runtime module identity unchanged for every consumer. Verified locally: - Facade verifier: clean across 10 entries. - Consumer-typecheck matrix: 57/57 scenarios pass. - check-export-coverage: 12 entries OK. - SD-3176 subpath snapshots: clean. This is the safe half of Phase 4. The root half (SD-3212) is the strategic work, scoped separately to keep regressions isolated.
…D-3201)
Replaces the self-referential metadata.list({ within: resolved.target })
block with a slice of the resolved block's text via editor.doc.extract.
The slice now asserts the SDT range covers 'duty' + 'reasonable' +
'care' — proving the Word-inserted word is inside the anchor using a
read surface (extract) that's independent of the metadata store.
…on layout (SD-3208) P1: CitationEditor had its own useCitations() instance, so save() re-hydrated only the child's local citations state. Since a payload- only metadata.update doesn't change the SDT, the parent's content- controls slice doesn't tick, leaving the parent panel stale after Save. Lift update from the parent useCitations() and pass it down so save() refreshes the list being rendered. Verified in browser: locator field updates immediately on Save. P2: CitationHighlights remeasured on window scroll/resize only. Document edits that move cited spans without changing window geometry (typing above a span, paragraph reflow) left the underline rects pinned at their old positions. Added a ResizeObserver on the editor canvas (catches pagination/zoom) plus a MutationObserver on the canvas DOM (catches text edits). Both funnel through a single rAF tick so burst events collapse into one remeasure per frame. Verified in browser: 3x Enter at the document start shifts cited spans by ~1400px and the rects track them.
…-types-flip feat(public-facade): phase 4a flip subpath types to public facade (SD-3211)
…ation-showcase feat(custom-ui): AI citation showcase over editor.doc.metadata.* (SD-3208)
…oundtrip-validation test(document-api): word-in-the-loop validation for anchored metadata (SD-3201)
chore: fix comments behavior tests
chore: editor-neutral layout identity and hit mapping
|
📖 Docs preview: https://superdoc-merge-main-into-stable-2026-05-19.mintlify.app |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0072e4f003
ℹ️ 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".
| const selectedContent = parentRun.content.cut($from.parentOffset, $to.parentOffset); | ||
| const leftContent = parentRun.content.cut(0, $from.parentOffset); | ||
| const rightContent = parentRun.content.cut($to.parentOffset); | ||
| const sdtNode = nodeType.create(attrs, selectedContent); |
There was a problem hiding this comment.
Preserve run formatting inside metadata anchors
When metadata.attach targets a range inside a single run, this branch splits the surrounding run with parentRun.attrs but moves the selected content into the hidden inline SDT as bare inline content. For formatted runs, that strips run-level properties such as font family/size or bidi attributes from only the anchored span after wrapping/export, even though the visible document text should be unchanged. The SDT content should preserve the selected run wrapper (or otherwise copy the parent run attrs) rather than inserting selectedContent directly.
Useful? React with 👍 / 👎.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Summary
merge/main-into-stable-2026-05-19fromstablemaininto the candidate branchstableAuto-created by promote-stable workflow.