Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ Do not hand-edit `COMMAND_CATALOG`, `OPERATION_MEMBER_PATH_MAP`, `OPERATION_REFE
- `pnpm dev` - dev server from `examples/`
- `pnpm run generate:all` - regenerate schemas, SDK clients, tool catalogs, reference docs
- `pnpm check:public-contract` - validate the published public type contract: wraps build + strict supported-root audit + consumer typecheck matrix. ~3 min. Scoped to the public type surface, not a replacement for `pnpm test` or `pnpm build`. SD-3256.
- `pnpm report:public-contract` - print the public-contract tier metadata (supported / legacy / legacy-raw / asset / deprecated). Read-only. Source of truth: `packages/superdoc/scripts/type-surface.config.cjs` (`publicContract` export). SD-3256.

## Testing

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"check:all": "pnpm run format && pnpm run lint:fix && pnpm --prefix packages/super-editor run types:build && pnpm run test",
"check:examples-demos": "bun scripts/validate-examples-demos.ts",
"check:public-contract": "node scripts/check-public-contract.mjs",
"report:public-contract": "node scripts/report-public-contract.mjs",
"local:publish": "pnpm --prefix packages/superdoc version prerelease --preid=local && pnpm --prefix packages/superdoc publish --registry http://localhost:4873",
"update-preset-geometry": "ROOT=$(pwd) && cd ../superdoc-devtools/preset-geometry && pnpm run build && cp ./dist/index.js ./dist/index.js.map ./dist/index.d.ts \"$ROOT/packages/preset-geometry/\"",
"manual-tag": "bash scripts/manual-tag.sh",
Expand Down
67 changes: 67 additions & 0 deletions packages/superdoc/scripts/type-surface.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,18 @@
* - `rule1Allowlist`: bare `@superdoc/*` specifiers permitted in
* published d.ts. Currently only the legacy public super-editor
* surface per RFC Decision 1.
* - `publicContract`: SD-3256 Phase 2. Tier metadata for every
* `package.json#exports` subpath. Describes what each subpath is
* (supported / legacy / asset / deprecated), not yet enforced.
* `scripts/report-public-contract.mjs` prints this for review.
*
* Adding a new relocation: append one entry to `relocations` with the
* package specifier, the dist target the rewriter should point at, and
* the source-include patterns vite + tsconfig need. Every consumer picks
* up the new entry without further edits.
*
* Adding a new public subpath: append an entry to `publicContract` with
* the correct tier. Keep it in sync with `package.json#exports`.
*/

const requiredEntryPoints = [
Expand Down Expand Up @@ -226,6 +233,65 @@ const rule1Allowlist = {
'@superdoc/super-editor': 'legacy public surface (RFC Decision 1)',
};

/**
* SD-3256 Phase 2: tier metadata for every `package.json#exports`
* subpath. Describes what each entry is, not what CI enforces. No
* enforcement is wired up in this phase; the metadata exists so the
* team can review the classification before Phase 3 (./super-editor
* facade curation) and Phase 4 (ratchet against the tiers).
*
* Tier policies (target end state, not all enforced today):
*
* - `supported`: fully typed, no `any`, no accidental internals;
* supported-root strict gate hard-fails regressions. Routes
* through `src/public/**`.
* - `legacy`: must not grow accidentally; typed where supported;
* can be deprecated or migrated over time; new APIs should not
* be added here. Routes through `src/public/legacy/**`.
* - `legacy-raw`: legacy public surface that does NOT yet route
* through `src/public/legacy/**` (the export resolves directly
* to a non-curated dist path). Only `./super-editor` today.
* SD-3256 Phase 3 will curate this through
* `src/public/legacy/super-editor.ts` after team alignment on
* which exports stay public.
* - `asset`: non-type asset (e.g. CSS). Not covered by the type
* contract.
* - `deprecated`: scheduled for removal. None today.
*
* The `internal` tier is implicit: anything not exported here is
* internal and not part of the consumer promise.
*
* Sync rule: keep this list aligned with `package.json#exports`.
* Adding a new export means adding an entry here too.
*/
const publicContract = {
supported: [
{ subpath: '.', tier: 'supported', note: 'root facade; routes through src/public/index.ts' },
{ subpath: './types', tier: 'supported', note: 'type-only facade; src/public/types.ts' },
{ subpath: './ui', tier: 'supported', note: 'UI primitives; src/public/ui.ts' },
{ subpath: './ui/react', tier: 'supported', note: 'React adapter; src/public/ui-react.ts' },
],
legacy: [
{ subpath: './converter', tier: 'legacy', note: 'src/public/legacy/converter.ts' },
{ subpath: './docx-zipper', tier: 'legacy', note: 'src/public/legacy/docx-zipper.ts' },
{ subpath: './file-zipper', tier: 'legacy', note: 'src/public/legacy/file-zipper.ts' },
{ subpath: './headless-toolbar', tier: 'legacy', note: 'src/public/legacy/headless-toolbar.ts' },
{ subpath: './headless-toolbar/react', tier: 'legacy', note: 'src/public/legacy/headless-toolbar-react.ts' },
{ subpath: './headless-toolbar/vue', tier: 'legacy', note: 'src/public/legacy/headless-toolbar-vue.ts' },
],
legacyRaw: [
{
subpath: './super-editor',
tier: 'legacy-raw',
note: 'resolves to dist/superdoc/src/super-editor.d.ts (not src/public/legacy/). SD-3256 Phase 3 will curate.',
},
],
asset: [
{ subpath: './style.css', tier: 'asset', note: 'CSS bundle; no types' },
],
deprecated: [],
};

module.exports = {
requiredEntryPoints,
handwrittenDtsBlocklist,
Expand All @@ -235,4 +301,5 @@ module.exports = {
relocationGuardPackages,
unshimmedPrivateSpecifiers,
rule1Allowlist,
publicContract,
};
109 changes: 109 additions & 0 deletions scripts/report-public-contract.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#!/usr/bin/env node
/**
* SD-3256 Phase 2: print the public-contract tier metadata as a
* human-readable report. Read-only; no validation, no enforcement.
*
* Source of truth: `packages/superdoc/scripts/type-surface.config.cjs`
* (the `publicContract` export). Adding a new public subpath without
* adding it there means it is "internal" by definition of this report.
*
* Cross-checks done by this script:
* 1. Every `package.json#exports` subpath has a `publicContract`
* entry. Missing entries are listed as MISSING.
* 2. Every `publicContract` subpath actually exists in `exports`.
* Stale entries are listed as STALE.
*
* Both cross-checks are reported but do NOT change exit code in this
* phase (Phase 2 is read-only by design). The script always exits 0
* unless it cannot load the config or package.json.
*
* Usage:
* pnpm report:public-contract
*
* Tracking: SD-3256 Phase 2.
*/

import { readFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const require = createRequire(import.meta.url);

const config = require(resolve(REPO_ROOT, 'packages/superdoc/scripts/type-surface.config.cjs'));
const pkg = JSON.parse(readFileSync(resolve(REPO_ROOT, 'packages/superdoc/package.json'), 'utf8'));

const { publicContract } = config;
if (!publicContract) {
console.error('FAIL: publicContract not exported from type-surface.config.cjs');
process.exit(1);
}

const exportsMap = pkg.exports || {};
const exportSubpaths = new Set(Object.keys(exportsMap));

const allContractEntries = [
...publicContract.supported,
...publicContract.legacy,
...publicContract.legacyRaw,
...publicContract.asset,
...publicContract.deprecated,
];
const contractSubpaths = new Set(allContractEntries.map((e) => e.subpath));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Detect duplicate subpath assignments across tiers

The report collapses publicContract entries into a Set, so a subpath listed in multiple tiers is silently deduplicated and the script can still print OK even though the classification is contradictory. This weakens the Phase 2 review signal because one export can appear in two tiers without being flagged; consider validating allContractEntries.length === contractSubpaths.size (and reporting duplicates) before declaring the cross-check clean.

Useful? React with 👍 / 👎.


const HR = '='.repeat(72);

const printTier = (name, entries) => {
console.log('');
console.log(`## ${name} (${entries.length})`);
if (entries.length === 0) {
console.log(' (none)');
return;
}
for (const e of entries) {
console.log(` ${e.subpath.padEnd(28)} ${e.note || ''}`);
}
};

console.log(HR);
console.log('SuperDoc public type contract (SD-3256 Phase 2)');
console.log(HR);
console.log('');
console.log('Tier definitions live in:');
console.log(' packages/superdoc/scripts/type-surface.config.cjs (publicContract)');
console.log('');
console.log(`Total exports in package.json: ${exportSubpaths.size}`);
console.log(`Total entries in publicContract: ${contractSubpaths.size}`);

printTier('Supported', publicContract.supported);
printTier('Legacy (curated through src/public/legacy/**)', publicContract.legacy);
printTier('Legacy-raw (NOT yet curated; SD-3256 Phase 3 target)', publicContract.legacyRaw);
printTier('Asset (non-type)', publicContract.asset);
printTier('Deprecated', publicContract.deprecated);

// Cross-check: missing (in exports but not in contract)
const missing = [...exportSubpaths].filter((s) => !contractSubpaths.has(s));
// Cross-check: stale (in contract but not in exports)
const stale = [...contractSubpaths].filter((s) => !exportSubpaths.has(s));

console.log('');
console.log(HR);
console.log('Cross-checks vs package.json#exports');
console.log(HR);
if (missing.length === 0 && stale.length === 0) {
console.log('OK: every export has a contract entry and vice versa.');
} else {
if (missing.length > 0) {
console.log(`MISSING (${missing.length}): in package.json#exports but not in publicContract:`);
for (const s of missing) console.log(` ${s}`);
}
if (stale.length > 0) {
console.log(`STALE (${stale.length}): in publicContract but not in package.json#exports:`);
for (const s of stale) console.log(` ${s}`);
}
console.log('');
console.log('(Phase 2 is read-only; this does not fail CI. Phase 4 will gate.)');
}

console.log('');
Loading