Skip to content

feat(types): explicit public facade root scaffold (SD-3178)#3358

Merged
caio-pizzol merged 3 commits into
mainfrom
caio-pizzol/SD-3178-public-facade-scaffold-root
May 17, 2026
Merged

feat(types): explicit public facade root scaffold (SD-3178)#3358
caio-pizzol merged 3 commits into
mainfrom
caio-pizzol/SD-3178-public-facade-scaffold-root

Conversation

@caio-pizzol
Copy link
Copy Markdown
Contributor

Phase 3 of SD-3175 (path-as-contract public facade umbrella). Lands the first real source-side public facade file for SuperDoc, plus the pipeline support and a postbuild verifier. No runtime, contract, or package-shape change.

What this PR does

Adds packages/superdoc/src/public/index.ts as the first explicit facade entry. It re-exports the same four symbols SD-3177 validated as a feasibility spike: SuperDoc, Config, Editor, EditorCommands. The narrow surface is intentional - it covers a runtime value, a config type, and the augmentation-bearing pair that exercises the SD-2965 regression vector.

Path-as-contract: anything under src/public/** is public by intent. Anything outside is implementation detail. The rule is enforced by the verification script described below, not by the JSON policy in PR #3294 (closed unmerged).

Pipeline wiring

  • vite.config.js: adds 'public': 'src/public/index.ts' to rollupOptions.input.
  • scripts/ensure-types.cjs: adds an entry to cjsDeclarationShims so the facade gets a .d.cts alongside its .d.ts.
  • scripts/verify-public-facade-emit.cjs (new): postbuild verifier with four invariants.
  • package.json#scripts.postbuild: runs the verifier between check-export-coverage and report-declaration-reachability.

What is NOT in this PR

  • No package.json#exports change. The facade emits to dist/superdoc/src/public/index.{d.ts,d.cts,es.js,cjs} but is not advertised as a published subpath. Phase 4 owns the contract switch.
  • No moves of existing exports. Existing entries continue unchanged.
  • No expansion to other facade files (types.ts, ui.ts, headless-toolbar.ts, legacy/*). Each lands in its own follow-up.

Verifier invariants

verify-public-facade-emit.cjs runs after ensure-types.cjs and asserts:

  1. Expected symbol set. EXPECTED_NAMES lists the four facade exports. New entries must be added here in the same PR, and the PR must link to SD-3175 for reviewer sign-off.
  2. ESM/CJS parity. Both index.d.ts and index.d.cts enumerate the same names.
  3. Augmentation probe. A TypeScript compile asserts that EditorCommands['setBold'] resolves through the facade. This is the SD-2965 regression vector: command-map declare module augmentations getting silently dropped on the way to consumers. If this assertion fails, the facade emit broke augmentations.
  4. Leak grep. No private @superdoc/* specifiers, no .pnpm/, no absolute local paths into the repo or node_modules.

Relative declaration references into the per-package dist tree (e.g. from '../../../super-editor/src/index.js') are expected at this phase. The dts pipeline relocates @superdoc/super-editor specifiers via ensure-types.cjs. Later SD-3178 follow-ups reduce how much the facade depends on the broader declaration graph.

Verified

  • pnpm --filter superdoc run build:es passes. Verifier output: Facade emits cleanly: 4 exports, ESM/CJS in parity, augmentations survive.
  • Drift test: renamed EditorCommandsEditorCommands2 in the emitted .d.ts → verifier exit 1 with expected: ..., actual: ....
  • Leak test: injected import { Foo } from "@superdoc/internal-thing"; into the emitted .d.ts → verifier exit 1 with private workspace specifier: from "@superdoc/.

Related

Phase 3 of SD-3175. Lands the first real source-side public facade for
SuperDoc: `packages/superdoc/src/public/index.ts`. Narrow scope on
purpose - same four symbols SD-3177 validated (`SuperDoc`, `Config`,
`Editor`, `EditorCommands`) so the augmentation regression path stays
covered, and nothing else.

The facade file uses named exports only, with explicit `.js` source
specifiers. Path-as-contract: anything under `src/public/**` is public
by intent, anything outside is implementation detail.

No `package.json#exports` change. Phase 4 owns the contract flip.
Today the facade emits to `dist/superdoc/src/public/index.{d.ts,d.cts,es.js,cjs}`
but is not advertised as a published subpath.

Wiring:
- vite.config.js: add `'public': 'src/public/index.ts'` to rollupOptions.input
- ensure-types.cjs: add the facade entry to cjsDeclarationShims so a
  `.d.cts` is generated next to the ESM declaration
- new postbuild step: scripts/verify-public-facade-emit.cjs runs after
  ensure-types to assert four invariants on the emitted declarations:
    1. expected symbol set
    2. ESM/CJS export parity
    3. command-map augmentation visible
       (`EditorCommands['setBold']` resolves through the facade)
    4. no private workspace specifiers, package-manager internals, or
       absolute local paths in the emit

Empirically verified the verifier rejects:
- a renamed symbol (drift in EXPECTED_NAMES) - exit 1
- an injected `@superdoc/internal` import in the emitted .d.ts - exit 1
- clean tree - exit 0
@caio-pizzol caio-pizzol requested a review from a team as a code owner May 17, 2026 15:45
@linear-code
Copy link
Copy Markdown

linear-code Bot commented May 17, 2026

SD-3178

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: 87db4186b3

ℹ️ 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 packages/superdoc/scripts/verify-public-facade-emit.cjs Outdated
Three small follow-ups on PR #3358:

- Promoted the two highest-risk-of-violation constraints in the facade
  scaffold to `AIDEV-NOTE:` anchors per `comment-policy.md`:
    - "Named exports only" in src/public/index.ts (an agent that
      "simplifies" via `export *` re-introduces the leak this exists to
      close).
    - "package.json#exports is intentionally not yet updated; Phase 4
      owns the contract switch" in vite.config.js (an agent that
      "finishes" the wiring ships a new public subpath under the radar).
    - The "update EXPECTED_NAMES in verify-public-facade-emit.cjs in the
      same PR" rule, so the postbuild gate stays meaningful.

- Coverage smoke test for src/public/index.ts. The two runtime
  re-exports showed 0% in the unit-test coverage report. The postbuild
  verifier covers the declaration surface but not the JS runtime. Two
  trivial assertions.
Codex review on PR #3358 caught that the augmentation probe was a
no-op. Verified at ChainedCommands.ts:141 - EditorCommands is
intersected with `Record<string, AnyCommand>`, so the indexer always
resolves and `type Probe = EditorCommands['setBold']` passes even
when the specific signatures are dropped.

Replaces the indexer-only probe with a return-type check on two
commands from two signature sources:

  setBold     - from FormattingCommandAugmentations
  insertComment - from CommentCommands

Both must resolve to `(...args: any[]) => boolean`. If the facade
falls back to the AnyCommand indexer, the return type is `unknown`
and the conditional resolves to `false`, failing the assignment with
TS2322.

A previous attempt at the fix used `true as Result` to assign the
conditional value; that laundered the failure through `never` and
the probe still passed. The literal stays un-cast on purpose, with
an inline note.

Reworded the surrounding prose to say "command signature surface"
instead of "augmentation surface" - the codebase composes
EditorCommands via explicit imports (CoreCommandSignatures,
FormattingCommandAugmentations, etc.), not exclusively via
`declare module` augmentation.

Empirically verified: simulated regression (EditorCommands replaced
with `Record<string, AnyCommand>`) now fails with TS2322 from each
of the two probe lines, verifier exits 1. Clean tree exits 0.
@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 caio-pizzol merged commit f84c326 into main May 17, 2026
70 checks passed
@caio-pizzol caio-pizzol deleted the caio-pizzol/SD-3178-public-facade-scaffold-root branch May 17, 2026 20:04
caio-pizzol added a commit that referenced this pull request May 17, 2026
Stacked on PR #3358 (SD-3178). Second facade file in the path-as-contract
scaffold under SD-3175 / Phase 3.

`superdoc/headless-toolbar`, `superdoc/headless-toolbar/react`, and
`superdoc/headless-toolbar/vue` are reclassified as legacy public
compatibility surface. Existing consumers keep compiling with full
types; new custom UI integrations should use `superdoc/ui` and
`superdoc/ui/react` instead. The next-generation typed UI controller
replaces the default headless-toolbar path.

Source facade:

- packages/superdoc/src/public/legacy/headless-toolbar.ts: 16 named
  exports (3 runtime + 13 types) mirroring src/headless-toolbar.js +
  src/headless-toolbar.d.ts. Lives under legacy/ to mark it compat-only.
- vite.config.js + ensure-types.cjs: build wiring for the new entry.
- verify-public-facade-emit.cjs: refactored to be config-driven via a
  FACADE_ENTRIES list. Adding a new facade file is now a single append.
- src/public/legacy/headless-toolbar.test.ts: 3 smoke assertions.

Policy:

- docs/architecture/package-boundaries.md: inventory table rows for
  `./headless-toolbar` and its react/vue subpaths reclassified from
  Public subpath to Legacy public compatibility surface. Decision 4's
  migration table extended to cover all three.

No-growth enforcement (extends SD-3176):

- tests/consumer-typecheck/snapshot-superdoc-legacy-exports.mjs: adds
  `./headless-toolbar`, `./headless-toolbar/react`, and
  `./headless-toolbar/vue` to the SUBPATHS list.
- snapshots/: three new baselines (16/1/1 resolved exports).
- README updated with the new entries.

No package.json#exports change. Phase 4 owns the contract flip.

Empirically verified the multi-entry facade verifier:
- clean: OK, 2 entries (root: 4 exports, legacy/headless-toolbar: 16)
- drift on legacy/headless-toolbar facade .d.ts: exit 1
- leak in legacy/headless-toolbar facade .d.ts: exit 1
- command-signature probe still fires on root entry only

Empirically verified the no-growth gate:
- clean across all 7 subpaths: OK
- drift on superdoc/headless-toolbar published .d.ts: exit 1
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.

2 participants