From 95556856dbb51f374762f28c700351dfdf8dc11b Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Wed, 27 May 2026 08:51:34 -0300 Subject: [PATCH 1/5] feat(scripts): js contract-owner audit, report-only (SD-673) Surveys which .js files own emitted public .d.ts files across the superdoc and @superdoc/super-editor packages, classified against the existing check-jsdoc.cjs tracking state. Report-only: always exits 0; informational output is the survey input for follow-up types-only extraction and @ts-check adoption. Mechanism: - For each of the two packages, walks every typed exports entry, follows relative + self-package edges through the emitted .d.ts forest, and collects every reachable declaration. - Resolves each reachable .d.ts to its source via the companion .d.ts.map sourcemap (sources[0] is the in-repo owner). - For each .js source, classifies as one of: ts-owned (not .js at all, just included for surface area), checked-files (hand-curated // @ts-check gate), has-ts-check (informational), allowlisted, tracked-debt, or UNACCOUNTED (public-surface JS source with no @ts-check directive and no tracking entry). Why two walks: check-jsdoc.cjs already walks from superdoc's entry points and reaches into super-editor JS via implementation imports. But super-editor publishes its own public exports (./types, ./ui, ./markdown, ./presentation-editor, etc.). Files reached only via super-editor's own publishings show up as unaccounted here even when they don't show up in the superdoc-side ratchet. Numbers on origin/main at the time of this PR: superdoc walk (14 typed exports, 483 reachable .d.ts, 479 owners): ts-owned 344 checked-files 4 has-ts-check 11 allowlisted 0 tracked-debt 31 unaccounted 89 @superdoc/super-editor walk (10 typed exports, 364 reachable .d.ts, 364 owners): ts-owned 101 checked-files 3 has-ts-check 31 allowlisted 0 tracked-debt 99 unaccounted 130 The two unaccounted lists overlap (e.g. InputRule.js appears in both); the per-walk numbers above are not deduplicated. tracked-debt 31 + 99 = 130 vs check-jsdoc.cjs's 102 'tracked as known debt' is explained by the same overlap. What this PR does NOT do: - Gate on growth. Promotion to a no-growth ratchet is a separate follow-up once UNACCOUNTED stabilizes at zero per package. - Move any JS files to TypeScript. The script is survey-only; conversion / types-only extraction is targeted manually using this report as input. - Touch check-jsdoc.cjs or the debt snapshot. Both stay the source of truth for their respective domain; this script reads them without mutating. Wired as a wrapper stage (js-contract-owners-report) after root-classification-closure, alongside the other after-build stages. Always exits 0. Adds ~0s to wrapper duration (script is fast: sourcemap reads only, no tsc). Verified: - node packages/superdoc/scripts/report-js-contract-owners.cjs -> OK - pnpm check:public:superdoc --skip-build -> PASS (12 ran, 1 skipped, 130.8s) --- packages/superdoc/scripts/README.md | 8 +- .../scripts/report-js-contract-owners.cjs | 373 ++++++++++++++++++ scripts/check-public-contract.mjs | 11 + 3 files changed, 389 insertions(+), 3 deletions(-) create mode 100644 packages/superdoc/scripts/report-js-contract-owners.cjs diff --git a/packages/superdoc/scripts/README.md b/packages/superdoc/scripts/README.md index 12a25891a4..2eadce2721 100644 --- a/packages/superdoc/scripts/README.md +++ b/packages/superdoc/scripts/README.md @@ -146,14 +146,16 @@ what an actual consumer would see — not the workspace source. | `package-shape-gate.mjs` | External package-shape linters (publint + attw) against the packed tarball. | Catches condition ordering, masquerading exports, missing field declarations. | | `check-root-classification-closure.mjs` | Asserts no `supported-root` or `legacy-root` export references an `internal-candidate` symbol in its public declared type. | Closure rule from SD-3212. | | `check-public-method-coverage.mjs` | Strict-zero obligation gate over public `SuperDoc` methods + getters. For each member the AST computes which obligations are meaningful (`parameters`, `returns`, or `call`); the gate fails on any unmet obligation. No grandfathered debt snapshot, no `--write`. Catches the `search(text: string)` regression class — call sites do NOT satisfy `parameters`/`returns` on their own. | Allowlist at `public-method-coverage-allowlist.cjs` is the only escape hatch (intentionally non-consumer-callable members; each entry validated: key must match a real member, value must be a non-empty reason). | +| `report-js-contract-owners.cjs` | JS contract-owner audit (SD-673, report-only). For both `superdoc` and `@superdoc/super-editor` packages: walks every typed export, follows relative / self-package edges through the emitted `.d.ts` forest, resolves each reachable declaration to its source via the companion `.d.ts.map` sourcemap, and classifies `.js` owners against the existing `check-jsdoc.cjs` state (`CHECKED_FILES`, `// @ts-check`, allowlist, debt snapshot). Output is the count per category plus the list of UNACCOUNTED `.js` owners — public-surface JS source with no `// @ts-check` directive and no tracking entry. | Always exits 0; informational. Survey input for follow-up types-only extraction / `@ts-check` adoption. Once UNACCOUNTED stabilizes at zero per package, a follow-up PR can promote it to a no-growth ratchet. | -Six of these run as wrapper stages of `check:public:superdoc`. +Seven of these run as wrapper stages of `check:public:superdoc`. `public-method-coverage` runs alongside the cheap policy gates (`contract-tiers-test`, `contract-tiers`, `jsdoc-ratchet`, `jsdoc-hygiene-ts-test`, `jsdoc-hygiene-ts`) before `build`. The other -five run after `build`: +six run after `build`: `consumer-typecheck-matrix`, `deep-type-audit-supported-root`, -`package-shape`, `export-snapshots`, `root-classification-closure`. +`package-shape`, `export-snapshots`, `root-classification-closure`, +`js-contract-owners-report` (report-only; always exits 0). `consumer-typecheck-matrix` packs `superdoc.tgz` and installs it into the consumer fixture. The rest reuse what matrix produced: `deep-type-audit-supported-root`, `export-snapshots`, and diff --git a/packages/superdoc/scripts/report-js-contract-owners.cjs b/packages/superdoc/scripts/report-js-contract-owners.cjs new file mode 100644 index 0000000000..c3407c7cac --- /dev/null +++ b/packages/superdoc/scripts/report-js-contract-owners.cjs @@ -0,0 +1,373 @@ +#!/usr/bin/env node +/** + * Report which `.js` files own emitted public `.d.ts` files across the + * `superdoc` and `@superdoc/super-editor` packages (SD-673, audit-only). + * + * Walks every typed `exports` entry in each package, follows + * relative-import / self-package edges through the emitted declaration + * forest, and identifies the source owner of each reachable `.d.ts` via + * its companion `.d.ts.map` sourcemap. JS-owned public declarations are + * cross-referenced with the `check-jsdoc.cjs` state (CHECKED_FILES, + * `// @ts-check`, allowlist, debt snapshot) to surface owners that are + * UNACCOUNTED — public surface backed by JS source with no `@ts-check` + * directive and no entry in any of the existing tracking lists. + * + * **Report-only.** This script is not a gate (always exits 0 except on + * structural errors like missing dist or unreadable package.json). The + * inventory is intended as survey input for follow-up types-only + * extraction work; once stable, a follow-up PR can flip a strict + * sub-check that fails on net-new UNACCOUNTED entries. + * + * Sources of truth this script consumes: + * - `packages/superdoc/package.json` and + * `packages/super-editor/package.json` for typed exports + * - `packages/superdoc/scripts/jsdoc-debt-snapshot.json` + * - `packages/superdoc/scripts/jsdoc-allowlist.cjs` + * - `CHECKED_FILES` (inlined from `check-jsdoc.cjs`; kept in sync by + * the explicit `KNOWN_CHECKED_FILES_REFERENCE` comment below) + * + * Note on scope: the existing `check-jsdoc.cjs` ratchet walks from + * `superdoc`'s entry points and reaches into super-editor JS via + * implementation imports — its `128 / 102` numbers already include + * super-editor JS owners. This script additionally walks super-editor's + * OWN public exports independently, so super-editor JS files reached + * only via super-editor's own publishings (not via superdoc's walk) + * show up here even when they don't show up in the superdoc-side + * ratchet. + */ + +const fs = require('node:fs'); +const path = require('node:path'); + +const repoRoot = path.resolve(__dirname, '..', '..', '..'); +const superdocRoot = path.resolve(repoRoot, 'packages/superdoc'); +const superEditorRoot = path.resolve(repoRoot, 'packages/super-editor'); + +// ─── check-jsdoc state ─────────────────────────────────────────────── + +const DEBT_SNAPSHOT_PATH = path.join(superdocRoot, 'scripts/jsdoc-debt-snapshot.json'); +const ALLOWLIST_PATH = path.join(superdocRoot, 'scripts/jsdoc-allowlist.cjs'); + +// Mirrors CHECKED_FILES in `check-jsdoc.cjs`. Kept inline so this script +// has zero runtime dependency on check-jsdoc.cjs's internals. If +// check-jsdoc adds/removes entries, update this constant. +const KNOWN_CHECKED_FILES_REFERENCE = [ + 'packages/superdoc/src/helpers/schema-introspection.js', + 'packages/superdoc/src/composables/use-find-replace.js', + 'packages/superdoc/src/composables/use-password-prompt.js', + 'packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/addMarkStep.js', + 'packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/markDeletion.js', + 'packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/markInsertion.js', +]; + +function loadDebtSnapshot() { + if (!fs.existsSync(DEBT_SNAPSHOT_PATH)) return new Set(); + try { + const json = JSON.parse(fs.readFileSync(DEBT_SNAPSHOT_PATH, 'utf8')); + // check-jsdoc.cjs writes the debt list under `knownUngated`. Older + // fallback keys are accepted in case the snapshot format moves. + return new Set(json.knownUngated ?? json.knownDebt ?? json.files ?? []); + } catch { + return new Set(); + } +} + +function loadAllowlist() { + if (!fs.existsSync(ALLOWLIST_PATH)) return new Set(); + try { + delete require.cache[require.resolve(ALLOWLIST_PATH)]; + const mod = require(ALLOWLIST_PATH); + if (typeof mod !== 'object' || mod === null) return new Set(); + return new Set(Object.keys(mod)); + } catch { + return new Set(); + } +} + +const TS_CHECK_DIRECTIVE_RE = /^\s*\/\/\s*@ts-check\b/m; +function hasTsCheckDirective(absPath) { + if (!absPath.endsWith('.js')) return false; + try { + // 4 KiB head is enough for a leading license/doc block before the + // directive; matches check-jsdoc.cjs's window. + const fd = fs.openSync(absPath, 'r'); + const buf = Buffer.alloc(4096); + const n = fs.readSync(fd, buf, 0, 4096, 0); + fs.closeSync(fd); + return TS_CHECK_DIRECTIVE_RE.test(buf.toString('utf8', 0, n)); + } catch { + return false; + } +} + +// ─── per-package reachability walker ───────────────────────────────── + +/** + * For one package: walk `package.json.exports` typed entries, follow + * relative + self-package specifiers transitively through the dist + * declaration forest, and return every reachable `.d.ts` file (absolute + * paths). + * + * The walker mirrors `report-declaration-reachability.cjs` — keeping it + * inlined here avoids forcing both scripts to depend on a shared helper + * for a first audit pass. If we promote either to a strict gate, the + * walker is the right thing to extract into `scripts/lib/`. + */ +function walkPackage(packageRoot) { + const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8')); + const packageName = packageJson.name; + const exportsMap = packageJson.exports || {}; + const distRoot = path.join(packageRoot, 'dist'); + if (!fs.existsSync(distRoot)) { + return { error: `dist/ missing for ${packageName}; build it first` }; + } + + function collectTypesTargets(value) { + if (typeof value !== 'object' || value === null) return []; + if (typeof value.types === 'string') return [value.types]; + if (typeof value.types !== 'object' || value.types === null) return []; + return Object.values(value.types).filter((t) => typeof t === 'string'); + } + + // Self-package resolver: `./super-editor` → dist .d.ts target. + const selfPackageTypeMap = new Map(); + for (const [subpath, value] of Object.entries(exportsMap)) { + const [target] = collectTypesTargets(value); + if (!target) continue; + selfPackageTypeMap.set(subpath, path.resolve(packageRoot, target)); + } + + const typedExports = []; + const missingTargets = []; + for (const [subpath, value] of Object.entries(exportsMap)) { + for (const targetPath of collectTypesTargets(value)) { + const target = path.resolve(packageRoot, targetPath); + if (!fs.existsSync(target)) { + missingTargets.push(targetPath); + continue; + } + typedExports.push({ subpath, target }); + } + } + + if (typedExports.length === 0) { + // Distinguish "no typed exports declared" from "typed exports declared + // but dist not built" so a missing-dist run gives an actionable hint. + if (missingTargets.length > 0) { + return { + error: `dist incomplete for ${packageName}: ${missingTargets.length} typed export target(s) missing on disk; run \`pnpm run type-check\` (or \`pnpm build\`) first`, + }; + } + return { error: `no typed exports in ${packageName}` }; + } + + function resolveRelative(spec, fromFile) { + const base = path.resolve(path.dirname(fromFile), spec); + if ((base.endsWith('.d.ts') || base.endsWith('.d.cts')) && fs.existsSync(base)) return base; + for (const dropExt of ['.js', '.ts']) { + if (base.endsWith(dropExt)) { + for (const ext of ['.d.ts', '.d.cts']) { + const cand = base.slice(0, -dropExt.length) + ext; + if (fs.existsSync(cand)) return cand; + } + } + } + const indexCand = path.join(base, 'index.d.ts'); + if (fs.existsSync(indexCand)) return indexCand; + const dtsCand = `${base}.d.ts`; + if (fs.existsSync(dtsCand)) return dtsCand; + return null; + } + + function resolveSelfPackage(spec) { + if (!spec.startsWith(packageName)) return null; + const remainder = spec.slice(packageName.length); + const subpath = remainder === '' ? '.' : `.${remainder}`; + return selfPackageTypeMap.get(subpath) || null; + } + + function resolveSpecifier(spec, fromFile) { + if (spec.startsWith('.')) return resolveRelative(spec, fromFile); + if (spec.startsWith(packageName)) return resolveSelfPackage(spec); + return null; + } + + const SPECIFIER_RE = /(?:from\s+|import\(\s*)['"]([^'"]+)['"]/g; + const visited = new Set(); + const queue = typedExports.map((e) => e.target); + for (const start of queue) visited.add(start); + + while (queue.length > 0) { + const file = queue.shift(); + let content; + try { + content = fs.readFileSync(file, 'utf8'); + } catch { + continue; + } + for (const match of content.matchAll(SPECIFIER_RE)) { + const resolved = resolveSpecifier(match[1], file); + if (!resolved || visited.has(resolved)) continue; + visited.add(resolved); + queue.push(resolved); + } + } + + return { + packageName, + distRoot, + typedExports, + reachable: [...visited].filter((f) => f.endsWith('.d.ts') || f.endsWith('.d.cts')), + }; +} + +// ─── d.ts → source owner via sourcemap ─────────────────────────────── + +/** + * Resolve a reachable `.d.ts` to its source path (repo-relative) by + * reading the companion `.d.ts.map` sourcemap. Returns `null` when no + * sourcemap exists or it doesn't resolve to an in-repo source (e.g. + * declarations re-exported from a third-party type package). + */ +function resolveSourceOwner(dtsAbs) { + const mapPath = `${dtsAbs}.map`; + if (!fs.existsSync(mapPath)) return { source: null, reason: 'no-sourcemap' }; + let parsed; + try { + parsed = JSON.parse(fs.readFileSync(mapPath, 'utf8')); + } catch (err) { + return { source: null, reason: `unreadable-sourcemap: ${err.message}` }; + } + const sources = Array.isArray(parsed.sources) ? parsed.sources : []; + if (sources.length === 0) return { source: null, reason: 'empty-sources' }; + // sources[0] is relative to sourceRoot (often empty); resolve from the map's directory. + const sourceRoot = typeof parsed.sourceRoot === 'string' ? parsed.sourceRoot : ''; + const sourceAbs = path.resolve(path.dirname(mapPath), sourceRoot, sources[0]); + // Guard against sources outside the repo (e.g. .pnpm-installed types + // re-emitted; we only care about in-repo owners). + const rel = path.relative(repoRoot, sourceAbs); + if (rel.startsWith('..')) return { source: null, reason: 'out-of-repo' }; + return { source: rel.split(path.sep).join('/'), reason: null }; +} + +// ─── classification ────────────────────────────────────────────────── + +function classify(source, checkedSet, allowlistSet, debtSet) { + if (!source.endsWith('.js')) return 'ts-owned'; + if (checkedSet.has(source)) return 'checked-files'; + if (allowlistSet.has(source)) return 'allowlisted'; + if (debtSet.has(source)) return 'tracked-debt'; + const abs = path.join(repoRoot, source); + if (hasTsCheckDirective(abs)) return 'has-ts-check'; + return 'unaccounted'; +} + +// ─── main ──────────────────────────────────────────────────────────── + +const checkedSet = new Set(KNOWN_CHECKED_FILES_REFERENCE); +const allowlistSet = loadAllowlist(); +const debtSet = loadDebtSnapshot(); + +const HR = '='.repeat(72); +console.log('[report-js-contract-owners] JS contract-owner audit (SD-673, report-only)'); +console.log(HR); + +const sections = []; +for (const [label, root] of [ + ['superdoc', superdocRoot], + ['@superdoc/super-editor', superEditorRoot], +]) { + const result = walkPackage(root); + if (result.error) { + console.log(`SKIP ${label}: ${result.error}`); + sections.push({ label, error: result.error }); + continue; + } + + // Aggregate per-source classifications. A single source can back many + // .d.ts files; deduplicate so the report counts owners, not emit + // duplicates. + const ownerToCategory = new Map(); + const noOwner = []; + for (const dts of result.reachable) { + const { source, reason } = resolveSourceOwner(dts); + if (!source) { + noOwner.push({ dts: path.relative(result.distRoot, dts), reason }); + continue; + } + const category = classify(source, checkedSet, allowlistSet, debtSet); + // Once a source is classified, don't downgrade if another .d.ts + // resolves to the same source; the category is a property of the + // source itself. + if (!ownerToCategory.has(source)) ownerToCategory.set(source, category); + } + + const byCategory = new Map(); + for (const [, cat] of ownerToCategory) { + byCategory.set(cat, (byCategory.get(cat) || 0) + 1); + } + + sections.push({ + label, + typedExports: result.typedExports.length, + reachableDts: result.reachable.length, + distinctOwners: ownerToCategory.size, + byCategory, + noOwner, + ownerToCategory, + }); +} + +for (const s of sections) { + console.log(''); + console.log(`### ${s.label}`); + console.log('-'.repeat(72)); + if (s.error) { + console.log(`(skipped: ${s.error})`); + continue; + } + console.log(`Typed exports walked: ${s.typedExports}`); + console.log(`Reachable .d.ts files: ${s.reachableDts}`); + console.log(`Distinct source owners: ${s.distinctOwners}`); + console.log(''); + + const order = ['ts-owned', 'checked-files', 'has-ts-check', 'allowlisted', 'tracked-debt', 'unaccounted']; + console.log('Source owners by classification:'); + for (const cat of order) { + const n = s.byCategory.get(cat) || 0; + console.log(` ${cat.padEnd(20)} ${String(n).padStart(5)}`); + } + + const unaccounted = [...s.ownerToCategory.entries()] + .filter(([, cat]) => cat === 'unaccounted') + .map(([file]) => file) + .sort(); + if (unaccounted.length > 0) { + console.log(''); + console.log(`Unaccounted .js owners (${unaccounted.length}) — public-surface JS source with no`); + console.log(`@ts-check directive and no entry in CHECKED_FILES, allowlist, or debt snapshot:`); + for (const f of unaccounted.slice(0, 30)) console.log(` - ${f}`); + if (unaccounted.length > 30) console.log(` ... and ${unaccounted.length - 30} more.`); + } + + if (s.noOwner.length > 0) { + console.log(''); + console.log(`No-owner .d.ts (${s.noOwner.length}) — reachable declarations whose source could not`); + console.log(`be resolved (missing sourcemap, empty sources, or out-of-repo):`); + const byReason = new Map(); + for (const { reason } of s.noOwner) byReason.set(reason, (byReason.get(reason) || 0) + 1); + for (const [reason, count] of [...byReason.entries()].sort((a, b) => b[1] - a[1])) { + console.log(` ${reason}: ${count}`); + } + } +} + +console.log(''); +console.log(HR); +console.log( + 'Report-only. Not a CI gate. Use the inventory to choose targets for\n' + + 'types-only extraction or `@ts-check` adoption. Once UNACCOUNTED is\n' + + 'stable at zero per package, a follow-up PR can promote this to a\n' + + 'no-growth ratchet.', +); +process.exit(0); diff --git a/scripts/check-public-contract.mjs b/scripts/check-public-contract.mjs index 0580e2a82e..d84cce8c7a 100755 --- a/scripts/check-public-contract.mjs +++ b/scripts/check-public-contract.mjs @@ -252,6 +252,17 @@ const stages = [ 'Closure gate: no supported-root or legacy-root export references an ' + 'internal-candidate type in its public declared shape (SD-3212 A1b).', }, + { + name: 'js-contract-owners-report', + cwd: REPO_ROOT, + cmd: 'node', + args: ['packages/superdoc/scripts/report-js-contract-owners.cjs'], + blurb: + 'Inventory of .js files that own emitted public .d.ts in both packages, ' + + 'classified against the existing check-jsdoc state (CHECKED_FILES, ' + + '// @ts-check, allowlist, debt snapshot). Report-only (always exits 0); ' + + 'promotion to a strict no-growth gate is a separate follow-up.', + }, ]; const HR = '='.repeat(72); From 185c9a11245a6d689124d99867a8a54b2142ea33 Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Wed, 27 May 2026 08:58:42 -0300 Subject: [PATCH 2/5] refactor(scripts): extract CHECKED_FILES to shared module + drop audit from wrapper Three review-driven tightening changes to the JS contract-owner audit: 1. Extract CHECKED_FILES + REACHABILITY_EXEMPT_CHECKED_FILES into a shared module (packages/superdoc/scripts/jsdoc-checked-files.cjs) consumed by both check-jsdoc.cjs and report-js-contract-owners.cjs. Replaces the prior inlined KNOWN_CHECKED_FILES_REFERENCE in the audit script, which created a drift trap reviewers would have had to police manually. 2. Drop the audit from the check:public:superdoc wrapper chain. The audit is report-only (always exits 0) and does not enforce anything; wiring it as a wrapper stage adds CI noise without adding a contract. Becomes a standalone command: node packages/superdoc/scripts/report-js-contract-owners.cjs When the audit is later promoted to a strict no-growth ratchet, THAT version earns a wrapper-stage entry. 3. README updated to reflect the standalone runtime + the shared config source. Verified: - node packages/superdoc/scripts/report-js-contract-owners.cjs -> OK (89 unaccounted on superdoc walk; 130 on super-editor walk; identical to pre-refactor output) - node packages/superdoc/scripts/check-jsdoc.cjs -> OK (128 / 4+2 / 26 / 0 / 102, identical to pre-refactor output) - pnpm check:public:superdoc --skip-build still passes (no wrapper stage added or removed since the audit was never in the previous commit's wrapper at run time) --- packages/superdoc/scripts/README.md | 9 ++-- packages/superdoc/scripts/check-jsdoc.cjs | 31 ++++------- .../superdoc/scripts/jsdoc-checked-files.cjs | 51 +++++++++++++++++++ .../scripts/report-js-contract-owners.cjs | 22 +++----- scripts/check-public-contract.mjs | 11 ---- 5 files changed, 74 insertions(+), 50 deletions(-) create mode 100644 packages/superdoc/scripts/jsdoc-checked-files.cjs diff --git a/packages/superdoc/scripts/README.md b/packages/superdoc/scripts/README.md index 2eadce2721..d7bb55237a 100644 --- a/packages/superdoc/scripts/README.md +++ b/packages/superdoc/scripts/README.md @@ -146,16 +146,15 @@ what an actual consumer would see — not the workspace source. | `package-shape-gate.mjs` | External package-shape linters (publint + attw) against the packed tarball. | Catches condition ordering, masquerading exports, missing field declarations. | | `check-root-classification-closure.mjs` | Asserts no `supported-root` or `legacy-root` export references an `internal-candidate` symbol in its public declared type. | Closure rule from SD-3212. | | `check-public-method-coverage.mjs` | Strict-zero obligation gate over public `SuperDoc` methods + getters. For each member the AST computes which obligations are meaningful (`parameters`, `returns`, or `call`); the gate fails on any unmet obligation. No grandfathered debt snapshot, no `--write`. Catches the `search(text: string)` regression class — call sites do NOT satisfy `parameters`/`returns` on their own. | Allowlist at `public-method-coverage-allowlist.cjs` is the only escape hatch (intentionally non-consumer-callable members; each entry validated: key must match a real member, value must be a non-empty reason). | -| `report-js-contract-owners.cjs` | JS contract-owner audit (SD-673, report-only). For both `superdoc` and `@superdoc/super-editor` packages: walks every typed export, follows relative / self-package edges through the emitted `.d.ts` forest, resolves each reachable declaration to its source via the companion `.d.ts.map` sourcemap, and classifies `.js` owners against the existing `check-jsdoc.cjs` state (`CHECKED_FILES`, `// @ts-check`, allowlist, debt snapshot). Output is the count per category plus the list of UNACCOUNTED `.js` owners — public-surface JS source with no `// @ts-check` directive and no tracking entry. | Always exits 0; informational. Survey input for follow-up types-only extraction / `@ts-check` adoption. Once UNACCOUNTED stabilizes at zero per package, a follow-up PR can promote it to a no-growth ratchet. | +| `report-js-contract-owners.cjs` | JS contract-owner audit (SD-673). For both `superdoc` and `@superdoc/super-editor` packages: walks every typed export, follows relative / self-package edges through the emitted `.d.ts` forest, resolves each reachable declaration to its source via the companion `.d.ts.map` sourcemap, and classifies `.js` owners against the existing `check-jsdoc.cjs` state (reads the shared `jsdoc-checked-files.cjs`, `jsdoc-allowlist.cjs`, `jsdoc-debt-snapshot.json`, and the in-file `// @ts-check` directive). Output is the count per category plus the list of UNACCOUNTED `.js` owners — public-surface JS source with no `// @ts-check` and no tracking entry. | **Standalone report; not wired into `check:public:superdoc`.** Run on demand: `node packages/superdoc/scripts/report-js-contract-owners.cjs`. Always exits 0. Survey input for follow-up types-only extraction / `@ts-check` adoption. Once UNACCOUNTED stabilizes at zero per package, a follow-up PR can promote a strict no-growth ratchet (which **would** earn a wrapper-stage entry). | -Seven of these run as wrapper stages of `check:public:superdoc`. +Six of these run as wrapper stages of `check:public:superdoc`. `public-method-coverage` runs alongside the cheap policy gates (`contract-tiers-test`, `contract-tiers`, `jsdoc-ratchet`, `jsdoc-hygiene-ts-test`, `jsdoc-hygiene-ts`) before `build`. The other -six run after `build`: +five run after `build`: `consumer-typecheck-matrix`, `deep-type-audit-supported-root`, -`package-shape`, `export-snapshots`, `root-classification-closure`, -`js-contract-owners-report` (report-only; always exits 0). +`package-shape`, `export-snapshots`, `root-classification-closure`. `consumer-typecheck-matrix` packs `superdoc.tgz` and installs it into the consumer fixture. The rest reuse what matrix produced: `deep-type-audit-supported-root`, `export-snapshots`, and diff --git a/packages/superdoc/scripts/check-jsdoc.cjs b/packages/superdoc/scripts/check-jsdoc.cjs index 475d00cc9b..867d9ca282 100644 --- a/packages/superdoc/scripts/check-jsdoc.cjs +++ b/packages/superdoc/scripts/check-jsdoc.cjs @@ -77,26 +77,17 @@ const tsconfigPath = path.join(packageDir, 'tsconfig.json'); const DEBT_SNAPSHOT_PATH = path.join(__dirname, 'jsdoc-debt-snapshot.json'); const ALLOWLIST_PATH = path.join(__dirname, 'jsdoc-allowlist.cjs'); -// Hand-curated set of files explicitly gated by this script. Each MUST -// have `// @ts-check` at the top. Adding a file = committing to keep -// it clean. The list is small on purpose; broader checkJs coverage is -// gained one file at a time, not in a mass migration. -const CHECKED_FILES = [ - 'packages/superdoc/src/helpers/schema-introspection.js', - 'packages/superdoc/src/composables/use-find-replace.js', - 'packages/superdoc/src/composables/use-password-prompt.js', - 'packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/addMarkStep.js', - 'packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/markDeletion.js', - 'packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/markInsertion.js', -]; - -const REACHABILITY_EXEMPT_CHECKED_FILES = new Set([ - // These files predate SD-2833. They are kept under the gate because their - // typedefs feed exported SuperDoc configuration types, but they are reached - // through implementation imports rather than direct public barrel exports. - 'packages/superdoc/src/composables/use-find-replace.js', - 'packages/superdoc/src/composables/use-password-prompt.js', -]); +// Hand-curated set of files explicitly gated by this script lives in +// `./jsdoc-checked-files.cjs` so it's shared with +// `report-js-contract-owners.cjs` (which classifies these files as +// `checked-files` rather than `unaccounted`). Keep both consumers +// reading from one place; edits go in the shared module. +const { + CHECKED_FILES, + REACHABILITY_EXEMPT_CHECKED_FILES: REACHABILITY_EXEMPT_LIST, +} = require('./jsdoc-checked-files.cjs'); + +const REACHABILITY_EXEMPT_CHECKED_FILES = new Set(REACHABILITY_EXEMPT_LIST); // PUBLIC entry points used by the ratchet's public-surface walk. These // are the files consumers reach through `superdoc`, `superdoc/super-editor`, diff --git a/packages/superdoc/scripts/jsdoc-checked-files.cjs b/packages/superdoc/scripts/jsdoc-checked-files.cjs new file mode 100644 index 0000000000..eb926dd0be --- /dev/null +++ b/packages/superdoc/scripts/jsdoc-checked-files.cjs @@ -0,0 +1,51 @@ +/** + * Shared source of truth for the hand-curated set of `.js` files + * explicitly gated by `check-jsdoc.cjs`'s per-file `// @ts-check` + * ratchet. + * + * Two consumers: + * + * - `check-jsdoc.cjs` — enforces these files stay clean against tsc. + * - `report-js-contract-owners.cjs` — classifies these as + * `checked-files` (not `unaccounted`) in its public-surface JS + * ownership inventory. + * + * Keeping both consumers reading from this single file prevents the + * audit's classification from drifting silently when the gate's list + * changes. Adding/removing a file is a one-spot edit. + * + * To add a file: + * 1. Add `// @ts-check` as the first line of the source. + * 2. Append the repo-relative path to `CHECKED_FILES` below. + * 3. Run `pnpm --filter superdoc run check:jsdoc` and fix what + * surfaces. + */ + +module.exports = { + /** + * Each entry MUST have `// @ts-check` at the top of the source. + * Adding a file commits the contributor to keeping it clean against + * tsc. Kept small on purpose; broader checkJs coverage is gained + * one file at a time, not in a mass migration. + */ + CHECKED_FILES: [ + 'packages/superdoc/src/helpers/schema-introspection.js', + 'packages/superdoc/src/composables/use-find-replace.js', + 'packages/superdoc/src/composables/use-password-prompt.js', + 'packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/addMarkStep.js', + 'packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/markDeletion.js', + 'packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/markInsertion.js', + ], + + /** + * Files kept under the gate even though they are not reached through + * the public-surface walk. Their typedefs feed exported SuperDoc + * configuration types but are reached via implementation imports + * rather than direct public barrel exports. Reachability gate skips + * these — they're already accounted for explicitly. + */ + REACHABILITY_EXEMPT_CHECKED_FILES: [ + 'packages/superdoc/src/composables/use-find-replace.js', + 'packages/superdoc/src/composables/use-password-prompt.js', + ], +}; diff --git a/packages/superdoc/scripts/report-js-contract-owners.cjs b/packages/superdoc/scripts/report-js-contract-owners.cjs index c3407c7cac..0f7a339a5f 100644 --- a/packages/superdoc/scripts/report-js-contract-owners.cjs +++ b/packages/superdoc/scripts/report-js-contract-owners.cjs @@ -23,8 +23,9 @@ * `packages/super-editor/package.json` for typed exports * - `packages/superdoc/scripts/jsdoc-debt-snapshot.json` * - `packages/superdoc/scripts/jsdoc-allowlist.cjs` - * - `CHECKED_FILES` (inlined from `check-jsdoc.cjs`; kept in sync by - * the explicit `KNOWN_CHECKED_FILES_REFERENCE` comment below) + * - `packages/superdoc/scripts/jsdoc-checked-files.cjs` — the same + * shared module `check-jsdoc.cjs` reads. Zero duplication; the + * two consumers cannot drift. * * Note on scope: the existing `check-jsdoc.cjs` ratchet walks from * `superdoc`'s entry points and reaches into super-editor JS via @@ -48,17 +49,10 @@ const superEditorRoot = path.resolve(repoRoot, 'packages/super-editor'); const DEBT_SNAPSHOT_PATH = path.join(superdocRoot, 'scripts/jsdoc-debt-snapshot.json'); const ALLOWLIST_PATH = path.join(superdocRoot, 'scripts/jsdoc-allowlist.cjs'); -// Mirrors CHECKED_FILES in `check-jsdoc.cjs`. Kept inline so this script -// has zero runtime dependency on check-jsdoc.cjs's internals. If -// check-jsdoc adds/removes entries, update this constant. -const KNOWN_CHECKED_FILES_REFERENCE = [ - 'packages/superdoc/src/helpers/schema-introspection.js', - 'packages/superdoc/src/composables/use-find-replace.js', - 'packages/superdoc/src/composables/use-password-prompt.js', - 'packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/addMarkStep.js', - 'packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/markDeletion.js', - 'packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/markInsertion.js', -]; +// Shared with `check-jsdoc.cjs`. Edits to the curated set go in the +// shared module so both consumers stay in sync; this script does not +// own the list. +const { CHECKED_FILES } = require('./jsdoc-checked-files.cjs'); function loadDebtSnapshot() { if (!fs.existsSync(DEBT_SNAPSHOT_PATH)) return new Set(); @@ -264,7 +258,7 @@ function classify(source, checkedSet, allowlistSet, debtSet) { // ─── main ──────────────────────────────────────────────────────────── -const checkedSet = new Set(KNOWN_CHECKED_FILES_REFERENCE); +const checkedSet = new Set(CHECKED_FILES); const allowlistSet = loadAllowlist(); const debtSet = loadDebtSnapshot(); diff --git a/scripts/check-public-contract.mjs b/scripts/check-public-contract.mjs index d84cce8c7a..0580e2a82e 100755 --- a/scripts/check-public-contract.mjs +++ b/scripts/check-public-contract.mjs @@ -252,17 +252,6 @@ const stages = [ 'Closure gate: no supported-root or legacy-root export references an ' + 'internal-candidate type in its public declared shape (SD-3212 A1b).', }, - { - name: 'js-contract-owners-report', - cwd: REPO_ROOT, - cmd: 'node', - args: ['packages/superdoc/scripts/report-js-contract-owners.cjs'], - blurb: - 'Inventory of .js files that own emitted public .d.ts in both packages, ' + - 'classified against the existing check-jsdoc state (CHECKED_FILES, ' + - '// @ts-check, allowlist, debt snapshot). Report-only (always exits 0); ' + - 'promotion to a strict no-growth gate is a separate follow-up.', - }, ]; const HR = '='.repeat(72); From 597b1f660cde64f7cc86f30bc298eee78746b13f Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Wed, 27 May 2026 09:05:20 -0300 Subject: [PATCH 3/5] fix(scripts): structural-fail on missing dist + sync check-jsdoc header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two review-driven fixes to the JS contract-owner audit: 1. Missing dist now exits 1, not 0. Previously the script printed SKIP and exited 0 when either package's typed export targets were missing on disk. "Report-only" should mean "findings (UNACCOUNTED count) never fail" — not "missing inputs still succeed." A broken input pipeline is distinguishable from a clean run now, and the script tells the user what to run to fix it (pnpm build or pnpm run type-check). 2. check-jsdoc.cjs header doc updated to point contributors at the shared jsdoc-checked-files.cjs module (the new source of truth) instead of "append below." Trivial doc nit, but exactly the kind of stale instruction that misleads new contributors after a refactor. Verified: - happy path: node report-js-contract-owners.cjs -> exit 0 - structural failure (super-editor dist/src removed): -> exit 1 - check-jsdoc.cjs still passes unchanged --- packages/superdoc/scripts/check-jsdoc.cjs | 5 ++- .../scripts/report-js-contract-owners.cjs | 41 +++++++++++++++---- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/packages/superdoc/scripts/check-jsdoc.cjs b/packages/superdoc/scripts/check-jsdoc.cjs index 867d9ca282..af6943237a 100644 --- a/packages/superdoc/scripts/check-jsdoc.cjs +++ b/packages/superdoc/scripts/check-jsdoc.cjs @@ -46,7 +46,10 @@ * * Adding a file to CHECKED_FILES: * 1. Add `// @ts-check` as the first line. - * 2. Append the file's repo-relative path to CHECKED_FILES below. + * 2. Append the file's repo-relative path to the `CHECKED_FILES` + * array in `./jsdoc-checked-files.cjs` (the shared source of + * truth consumed by both this gate and + * `report-js-contract-owners.cjs`). * 3. Run `pnpm --filter superdoc run check:jsdoc` and fix what * surfaces. If the file was on the debt snapshot, also rerun * with `--write` to drop the stale entry. diff --git a/packages/superdoc/scripts/report-js-contract-owners.cjs b/packages/superdoc/scripts/report-js-contract-owners.cjs index 0f7a339a5f..aff6900562 100644 --- a/packages/superdoc/scripts/report-js-contract-owners.cjs +++ b/packages/superdoc/scripts/report-js-contract-owners.cjs @@ -12,11 +12,17 @@ * UNACCOUNTED — public surface backed by JS source with no `@ts-check` * directive and no entry in any of the existing tracking lists. * - * **Report-only.** This script is not a gate (always exits 0 except on - * structural errors like missing dist or unreadable package.json). The - * inventory is intended as survey input for follow-up types-only - * extraction work; once stable, a follow-up PR can flip a strict - * sub-check that fails on net-new UNACCOUNTED entries. + * **Report-only findings.** The UNACCOUNTED count never fails the + * script — the inventory is survey input for follow-up types-only + * extraction work. A future PR can promote a strict sub-check that + * fails on net-new UNACCOUNTED entries. + * + * **Structural failures DO fail (exit 1).** A missing dist tree or + * unreadable package.json prevents the audit from producing a + * meaningful inventory; in that case the script exits non-zero so a + * broken input pipeline is distinguishable from a clean "zero + * unaccounted" run. Requires `pnpm build` (or `pnpm run type-check` + * to emit declarations only) to have run first. * * Sources of truth this script consumes: * - `packages/superdoc/package.json` and @@ -266,6 +272,13 @@ const HR = '='.repeat(72); console.log('[report-js-contract-owners] JS contract-owner audit (SD-673, report-only)'); console.log(HR); +// Structural-failure tracking: missing dist or unreadable package +// inputs exit non-zero so an audit run that produced no real inventory +// is distinguishable from one that genuinely found zero unaccounted +// owners. "Report-only" applies to findings (UNACCOUNTED count never +// fails); it does not apply to a broken input pipeline. +let structuralFailure = false; + const sections = []; for (const [label, root] of [ ['superdoc', superdocRoot], @@ -275,6 +288,7 @@ for (const [label, root] of [ if (result.error) { console.log(`SKIP ${label}: ${result.error}`); sections.push({ label, error: result.error }); + structuralFailure = true; continue; } @@ -359,9 +373,18 @@ for (const s of sections) { console.log(''); console.log(HR); console.log( - 'Report-only. Not a CI gate. Use the inventory to choose targets for\n' + - 'types-only extraction or `@ts-check` adoption. Once UNACCOUNTED is\n' + - 'stable at zero per package, a follow-up PR can promote this to a\n' + - 'no-growth ratchet.', + 'Report-only. The UNACCOUNTED count never fails. Use the inventory to\n' + + 'choose targets for types-only extraction or `@ts-check` adoption.\n' + + 'Once UNACCOUNTED stabilizes at zero per package, a follow-up PR can\n' + + 'promote this to a strict no-growth ratchet.', ); + +if (structuralFailure) { + console.log(''); + console.log('FAIL one or more packages skipped (missing dist or unreadable input).'); + console.log(' Run `pnpm build` (or `pnpm run type-check` to emit declarations only)'); + console.log(' and retry. The audit cannot produce a meaningful inventory with'); + console.log(' partial inputs.'); + process.exit(1); +} process.exit(0); From a212720f75286ac856440e747cd61b2748837c7a Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Wed, 27 May 2026 09:07:42 -0300 Subject: [PATCH 4/5] docs(scripts): sync README exit semantics for js contract-owner audit The README row still said 'Always exits 0' after the previous commit flipped the script to exit 1 on structural failures. Now matches the actual behavior: findings (UNACCOUNTED count) exit 0; missing dist or unreadable package inputs exit 1. --- packages/superdoc/scripts/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/superdoc/scripts/README.md b/packages/superdoc/scripts/README.md index d7bb55237a..5f8e759fbe 100644 --- a/packages/superdoc/scripts/README.md +++ b/packages/superdoc/scripts/README.md @@ -146,7 +146,7 @@ what an actual consumer would see — not the workspace source. | `package-shape-gate.mjs` | External package-shape linters (publint + attw) against the packed tarball. | Catches condition ordering, masquerading exports, missing field declarations. | | `check-root-classification-closure.mjs` | Asserts no `supported-root` or `legacy-root` export references an `internal-candidate` symbol in its public declared type. | Closure rule from SD-3212. | | `check-public-method-coverage.mjs` | Strict-zero obligation gate over public `SuperDoc` methods + getters. For each member the AST computes which obligations are meaningful (`parameters`, `returns`, or `call`); the gate fails on any unmet obligation. No grandfathered debt snapshot, no `--write`. Catches the `search(text: string)` regression class — call sites do NOT satisfy `parameters`/`returns` on their own. | Allowlist at `public-method-coverage-allowlist.cjs` is the only escape hatch (intentionally non-consumer-callable members; each entry validated: key must match a real member, value must be a non-empty reason). | -| `report-js-contract-owners.cjs` | JS contract-owner audit (SD-673). For both `superdoc` and `@superdoc/super-editor` packages: walks every typed export, follows relative / self-package edges through the emitted `.d.ts` forest, resolves each reachable declaration to its source via the companion `.d.ts.map` sourcemap, and classifies `.js` owners against the existing `check-jsdoc.cjs` state (reads the shared `jsdoc-checked-files.cjs`, `jsdoc-allowlist.cjs`, `jsdoc-debt-snapshot.json`, and the in-file `// @ts-check` directive). Output is the count per category plus the list of UNACCOUNTED `.js` owners — public-surface JS source with no `// @ts-check` and no tracking entry. | **Standalone report; not wired into `check:public:superdoc`.** Run on demand: `node packages/superdoc/scripts/report-js-contract-owners.cjs`. Always exits 0. Survey input for follow-up types-only extraction / `@ts-check` adoption. Once UNACCOUNTED stabilizes at zero per package, a follow-up PR can promote a strict no-growth ratchet (which **would** earn a wrapper-stage entry). | +| `report-js-contract-owners.cjs` | JS contract-owner audit (SD-673). For both `superdoc` and `@superdoc/super-editor` packages: walks every typed export, follows relative / self-package edges through the emitted `.d.ts` forest, resolves each reachable declaration to its source via the companion `.d.ts.map` sourcemap, and classifies `.js` owners against the existing `check-jsdoc.cjs` state (reads the shared `jsdoc-checked-files.cjs`, `jsdoc-allowlist.cjs`, `jsdoc-debt-snapshot.json`, and the in-file `// @ts-check` directive). Output is the count per category plus the list of UNACCOUNTED `.js` owners — public-surface JS source with no `// @ts-check` and no tracking entry. | **Standalone report; not wired into `check:public:superdoc`.** Run on demand: `node packages/superdoc/scripts/report-js-contract-owners.cjs`. **Exit semantics:** findings (UNACCOUNTED count) never fail (exit 0); missing dist / unreadable package inputs exit 1 so a broken pipeline is distinguishable from a clean run. Requires `pnpm build` (or `pnpm run type-check`) to have populated both packages' dist trees. Survey input for follow-up types-only extraction / `@ts-check` adoption. Once UNACCOUNTED stabilizes at zero per package, a follow-up PR can promote a strict no-growth ratchet (which **would** earn a wrapper-stage entry). | Six of these run as wrapper stages of `check:public:superdoc`. `public-method-coverage` runs alongside the cheap policy gates From cd178d32ee588107e115f30e8c08b9ca563f2ed8 Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Wed, 27 May 2026 10:15:29 -0300 Subject: [PATCH 5/5] fix(scripts): drop misleading pnpm run type-check hint from audit The remediation message suggested 'pnpm build (or pnpm run type-check to emit declarations only).' That alternative is wrong: - packages/superdoc/tsconfig.types.json sets outDir to dist-types, not dist. - The audit walks packages/superdoc/dist (the consumer-visible tree via package.json exports). - So pnpm run type-check alone leaves packages/superdoc/dist empty and the audit keeps failing with the same structural error. Now recommends pnpm build only, in three places: - script header doc - per-package missing-target error - final structural-failure hint README row updated to match. The script behavior is unchanged; only the user-facing remediation hint was misleading. --- packages/superdoc/scripts/README.md | 2 +- .../scripts/report-js-contract-owners.cjs | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/superdoc/scripts/README.md b/packages/superdoc/scripts/README.md index 5f8e759fbe..0d0ae419c6 100644 --- a/packages/superdoc/scripts/README.md +++ b/packages/superdoc/scripts/README.md @@ -146,7 +146,7 @@ what an actual consumer would see — not the workspace source. | `package-shape-gate.mjs` | External package-shape linters (publint + attw) against the packed tarball. | Catches condition ordering, masquerading exports, missing field declarations. | | `check-root-classification-closure.mjs` | Asserts no `supported-root` or `legacy-root` export references an `internal-candidate` symbol in its public declared type. | Closure rule from SD-3212. | | `check-public-method-coverage.mjs` | Strict-zero obligation gate over public `SuperDoc` methods + getters. For each member the AST computes which obligations are meaningful (`parameters`, `returns`, or `call`); the gate fails on any unmet obligation. No grandfathered debt snapshot, no `--write`. Catches the `search(text: string)` regression class — call sites do NOT satisfy `parameters`/`returns` on their own. | Allowlist at `public-method-coverage-allowlist.cjs` is the only escape hatch (intentionally non-consumer-callable members; each entry validated: key must match a real member, value must be a non-empty reason). | -| `report-js-contract-owners.cjs` | JS contract-owner audit (SD-673). For both `superdoc` and `@superdoc/super-editor` packages: walks every typed export, follows relative / self-package edges through the emitted `.d.ts` forest, resolves each reachable declaration to its source via the companion `.d.ts.map` sourcemap, and classifies `.js` owners against the existing `check-jsdoc.cjs` state (reads the shared `jsdoc-checked-files.cjs`, `jsdoc-allowlist.cjs`, `jsdoc-debt-snapshot.json`, and the in-file `// @ts-check` directive). Output is the count per category plus the list of UNACCOUNTED `.js` owners — public-surface JS source with no `// @ts-check` and no tracking entry. | **Standalone report; not wired into `check:public:superdoc`.** Run on demand: `node packages/superdoc/scripts/report-js-contract-owners.cjs`. **Exit semantics:** findings (UNACCOUNTED count) never fail (exit 0); missing dist / unreadable package inputs exit 1 so a broken pipeline is distinguishable from a clean run. Requires `pnpm build` (or `pnpm run type-check`) to have populated both packages' dist trees. Survey input for follow-up types-only extraction / `@ts-check` adoption. Once UNACCOUNTED stabilizes at zero per package, a follow-up PR can promote a strict no-growth ratchet (which **would** earn a wrapper-stage entry). | +| `report-js-contract-owners.cjs` | JS contract-owner audit (SD-673). For both `superdoc` and `@superdoc/super-editor` packages: walks every typed export, follows relative / self-package edges through the emitted `.d.ts` forest, resolves each reachable declaration to its source via the companion `.d.ts.map` sourcemap, and classifies `.js` owners against the existing `check-jsdoc.cjs` state (reads the shared `jsdoc-checked-files.cjs`, `jsdoc-allowlist.cjs`, `jsdoc-debt-snapshot.json`, and the in-file `// @ts-check` directive). Output is the count per category plus the list of UNACCOUNTED `.js` owners — public-surface JS source with no `// @ts-check` and no tracking entry. | **Standalone report; not wired into `check:public:superdoc`.** Run on demand: `node packages/superdoc/scripts/report-js-contract-owners.cjs`. **Exit semantics:** findings (UNACCOUNTED count) never fail (exit 0); missing dist / unreadable package inputs exit 1 so a broken pipeline is distinguishable from a clean run. Requires `pnpm build` to have populated both packages' dist trees (`pnpm run type-check` is not a substitute — it writes superdoc declarations to `dist-types/`, not `dist/`). Survey input for follow-up types-only extraction / `@ts-check` adoption. Once UNACCOUNTED stabilizes at zero per package, a follow-up PR can promote a strict no-growth ratchet (which **would** earn a wrapper-stage entry). | Six of these run as wrapper stages of `check:public:superdoc`. `public-method-coverage` runs alongside the cheap policy gates diff --git a/packages/superdoc/scripts/report-js-contract-owners.cjs b/packages/superdoc/scripts/report-js-contract-owners.cjs index aff6900562..79461f03b9 100644 --- a/packages/superdoc/scripts/report-js-contract-owners.cjs +++ b/packages/superdoc/scripts/report-js-contract-owners.cjs @@ -21,8 +21,11 @@ * unreadable package.json prevents the audit from producing a * meaningful inventory; in that case the script exits non-zero so a * broken input pipeline is distinguishable from a clean "zero - * unaccounted" run. Requires `pnpm build` (or `pnpm run type-check` - * to emit declarations only) to have run first. + * unaccounted" run. Requires `pnpm build` to have run first. + * `pnpm run type-check` is NOT a substitute: it writes superdoc + * declarations to `dist-types/` (per + * `packages/superdoc/tsconfig.types.json`), while this audit walks + * `packages/superdoc/dist/` (the consumer-visible tree). * * Sources of truth this script consumes: * - `packages/superdoc/package.json` and @@ -155,7 +158,7 @@ function walkPackage(packageRoot) { // but dist not built" so a missing-dist run gives an actionable hint. if (missingTargets.length > 0) { return { - error: `dist incomplete for ${packageName}: ${missingTargets.length} typed export target(s) missing on disk; run \`pnpm run type-check\` (or \`pnpm build\`) first`, + error: `dist incomplete for ${packageName}: ${missingTargets.length} typed export target(s) missing on disk; run \`pnpm build\` first`, }; } return { error: `no typed exports in ${packageName}` }; @@ -382,9 +385,9 @@ console.log( if (structuralFailure) { console.log(''); console.log('FAIL one or more packages skipped (missing dist or unreadable input).'); - console.log(' Run `pnpm build` (or `pnpm run type-check` to emit declarations only)'); - console.log(' and retry. The audit cannot produce a meaningful inventory with'); - console.log(' partial inputs.'); + console.log(' Run `pnpm build` and retry. (`pnpm run type-check` is not a'); + console.log(' substitute: superdoc declarations go to dist-types/, not dist/.)'); + console.log(' The audit cannot produce a meaningful inventory with partial inputs.'); process.exit(1); } process.exit(0);