From d39c37fe7130f02e27cb5af2abdf98bb8d341155 Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Fri, 22 May 2026 13:33:25 -0300 Subject: [PATCH] ci(types): collapse matrix + audit CI steps into check:public-contract (SD-673 Phase 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Makes `pnpm check:public-contract` the single official path for public-type validation, locally and in CI. **No coverage change**: exactly the same scenarios run; only the orchestration changes. Wrapper changes: - Reordered stages so `typecheck-matrix` runs BEFORE `deep-type-audit`. Matrix packs and installs the superdoc tarball into the consumer fixture as part of its normal run; the audit then reuses that install instead of doing a redundant `--pack` of its own. Drops one pack + install per invocation. Local full-run time: 199s → 135s. - New `--skip-build` flag. CI's existing Build step already runs `pnpm run build` (= `build:superdoc && type-check`); the wrapper must not re-build. Local users get the full from-scratch behavior by default. - Skip output now reports `N ran, M skipped` so CI logs make the skip explicit instead of silently dropping a stage. CI changes (`.github/workflows/ci-superdoc.yml`): - Removed: `Consumer typecheck (matrix)` step - Removed: `Deep public-type audit (supported-root strict, SD-3213e)` step - Added: `Public-contract check (matrix + supported-root strict audit)` which runs `pnpm check:public-contract --skip-build`. Single step, identical coverage. Net: 10 named CI steps → 9. Why now: SD-3256 Phase 1 shipped the wrapper but only as a DX tool. SD-673 Phase 1 makes it the load-bearing CI path so "is the public contract healthy?" has exactly one command answer, locally and in CI. Future phases (additional stages, stricter gates) extend one wrapper instead of duplicating CI yaml. Verified locally: pnpm check:public-contract → PASS (3 stages, 135.6s) pnpm check:public-contract --skip-build → PASS (2 ran, 1 skipped, 97.9s) --- .github/workflows/ci-superdoc.yml | 36 ++++++--------- AGENTS.md | 2 +- scripts/check-public-contract.mjs | 77 ++++++++++++++++++++----------- 3 files changed, 64 insertions(+), 51 deletions(-) diff --git a/.github/workflows/ci-superdoc.yml b/.github/workflows/ci-superdoc.yml index 8229f34b44..aac05c9fd8 100644 --- a/.github/workflows/ci-superdoc.yml +++ b/.github/workflows/ci-superdoc.yml @@ -120,28 +120,20 @@ jobs: # tree (those are tracked under SD-2863 follow-up tickets). run: pnpm --filter superdoc run check:jsdoc - - name: Consumer typecheck (matrix) - # The matrix script owns the published-package validation path: - # it packs superdoc, installs the tarball into the standalone - # fixture (`pnpm install --ignore-workspace`), then runs every - # scenario under its declared resolution mode and strictness - # settings. Replaces the pre-SD-2831 bare `tsc --noEmit` step - # so the new pack-and-install scenarios are actually exercised - # in CI, not just locally. - run: | - cd tests/consumer-typecheck - node typecheck-matrix.mjs - - - name: Deep public-type audit (supported-root strict, SD-3213e) - # Single invocation does both: (1) prints the broad inventory - # (tier counts, top files, entry/root-bucket attribution) for - # visibility, and (2) gates on the supported-root subset against - # `deep-type-audit.supported-root-allowlist.json`. Fails on any - # new `any` finding reachable from root `.` via an export - # classified as `supported-root`, AND on stale entries (a drain - # landed but the allowlist wasn't shrunk). The broad audit - # remains report-only because it includes legacy/raw reach. - run: node tests/consumer-typecheck/deep-type-audit.mjs --strict-supported-root + - name: Public-contract check (matrix + supported-root strict audit) + # SD-673 Phase 1: collapse the previous two CI steps + # ('Consumer typecheck (matrix)' + 'Deep public-type audit') into + # the single wrapper command. Same coverage: + # - typecheck-matrix.mjs packs superdoc, installs the tarball + # into the consumer fixture, runs every scenario. + # - deep-type-audit.mjs --strict-supported-root reuses that + # install and gates on the supported-root any allowlist + # (SD-3213e). Broad inventory still printed for visibility. + # --skip-build because the Build step above already ran + # `pnpm run build` (which includes build:superdoc). + # Local equivalent: `pnpm check:public-contract` (with the build + # stage included). + run: pnpm check:public-contract --skip-build - name: Package shape gates # External package-shape linters (publint + attw) running against diff --git a/AGENTS.md b/AGENTS.md index 15f8ac699b..32de3f1ba7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -72,7 +72,7 @@ Do not hand-edit `COMMAND_CATALOG`, `OPERATION_MEMBER_PATH_MAP`, `OPERATION_REFE - `pnpm test` - unit tests - `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 check:public-contract` - validate the published public type contract: wraps build + consumer typecheck matrix + strict supported-root audit. ~3 min. Scoped to the public type surface, not a replacement for `pnpm test` or `pnpm build`. Also the single command CI runs (with `--skip-build` after its own Build step). SD-3256 / SD-673. - `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 diff --git a/scripts/check-public-contract.mjs b/scripts/check-public-contract.mjs index 9cd3098b2b..3477f50169 100755 --- a/scripts/check-public-contract.mjs +++ b/scripts/check-public-contract.mjs @@ -1,31 +1,34 @@ #!/usr/bin/env node /** - * SD-3256 Phase 1: single command to validate the public type contract. + * Single command to validate the published superdoc package's public + * TypeScript surface end-to-end. * - * Runs the validators that, together, answer "is the published - * superdoc package's public TypeScript surface healthy?" Today these - * run as three separate invocations across CI: + * Stages: + * 1. build:superdoc - vite build + the postbuild validator chain + * (check-tsconfig-type-surface, ensure-types, + * audit-bundle, audit-declarations, + * check-export-coverage, verify-public-facade-emit, + * report-declaration-reachability). + * Skipped when `--skip-build` is passed (CI calls + * `pnpm run build` separately in its own step). + * 2. typecheck-matrix - packs superdoc + installs the tarball into + * tests/consumer-typecheck/node_modules/, then + * runs every consumer scenario. + * 3. deep-type-audit - strict gate on the supported-root public + * surface (must be 0 findings). Reuses the + * install that stage 2 produced (no `--pack`). * - * pnpm run build:superdoc - * # vite build + the postbuild validator chain - * # (check-tsconfig-type-surface, ensure-types, audit-bundle, - * # audit-declarations, check-export-coverage, - * # verify-public-facade-emit, report-declaration-reachability) + * Matrix runs BEFORE audit on purpose: matrix packs + installs the + * tarball once, and the audit then reuses that install. Without this + * order the audit would `--pack` separately and double the work. * - * node tests/consumer-typecheck/deep-type-audit.mjs --pack --strict-supported-root - * # strict gate on the supported public surface; must be 0 findings - * - * node tests/consumer-typecheck/typecheck-matrix.mjs - * # consumer-perspective scenarios compiled against the packed tarball - * - * This wrapper orchestrates them in order, prints section headers, - * fails fast on the first failure, and gives an at-a-glance verdict. - * Zero behavior change for the validators themselves; this is pure DX. - * - * Usage: + * Local usage: * pnpm check:public-contract * - * Tracking: SD-3256 (umbrella). Phase 1. + * CI usage (Build step already ran): + * pnpm check:public-contract --skip-build + * + * SD-3256 Phase 1 (initial wrapper) / SD-673 Phase 1 (CI wiring). */ import { spawnSync } from 'node:child_process'; @@ -34,6 +37,9 @@ import { fileURLToPath } from 'node:url'; const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const flags = new Set(process.argv.slice(2)); +const skipBuild = flags.has('--skip-build'); + const stages = [ { name: 'build:superdoc', @@ -43,20 +49,26 @@ const stages = [ blurb: 'Build dist + run postbuild validators (audit-bundle, audit-declarations, ' + 'check-export-coverage, verify-public-facade-emit, ensure-types, ...).', + skipIf: skipBuild, + skipReason: '--skip-build passed; CI Build step already ran this', }, { - name: 'deep-type-audit --strict-supported-root', + name: 'typecheck-matrix', cwd: resolve(REPO_ROOT, 'tests/consumer-typecheck'), cmd: 'node', - args: ['deep-type-audit.mjs', '--pack', '--strict-supported-root'], - blurb: 'Strict gate on the supported-root public surface (must be 0 findings).', + args: ['typecheck-matrix.mjs'], + blurb: + 'Packs superdoc + installs the tarball into the consumer fixture, ' + + 'then runs every typecheck scenario.', }, { - name: 'typecheck-matrix', + name: 'deep-type-audit --strict-supported-root', cwd: resolve(REPO_ROOT, 'tests/consumer-typecheck'), cmd: 'node', - args: ['typecheck-matrix.mjs'], - blurb: 'Consumer-perspective scenarios against the packed tarball.', + args: ['deep-type-audit.mjs', '--strict-supported-root'], + blurb: + 'Strict gate on the supported-root public surface (must be 0 findings). ' + + 'Reuses the install produced by typecheck-matrix.', }, ]; @@ -64,13 +76,20 @@ const HR = '='.repeat(72); const start = Date.now(); let failed = null; +let ranCount = 0; for (const [i, s] of stages.entries()) { console.log(''); console.log(HR); console.log(`[${i + 1}/${stages.length}] ${s.name}`); + if (s.skipIf) { + console.log(`SKIP: ${s.skipReason}`); + console.log(HR); + continue; + } console.log(s.blurb); console.log(HR); const result = spawnSync(s.cmd, s.args, { cwd: s.cwd, stdio: 'inherit' }); + ranCount += 1; if (result.status !== 0) { failed = { stage: s.name, status: result.status ?? 1 }; break; @@ -89,5 +108,7 @@ if (failed) { console.log(` ${failedStage.cmd} ${failedStage.args.join(' ')}`); process.exit(failed.status); } else { - console.log(`PASS: ${stages.length} stages, ${elapsed}s`); + const skipped = stages.length - ranCount; + const ranLabel = skipped > 0 ? `${ranCount} ran, ${skipped} skipped` : `${ranCount} stages`; + console.log(`PASS: ${ranLabel}, ${elapsed}s`); }