Skip to content

feat(types): deep public-type audit gate (SD-2977)#3185

Draft
caio-pizzol wants to merge 7 commits intomainfrom
caio-pizzol/SD-2977-public-type-deep-audit-gate
Draft

feat(types): deep public-type audit gate (SD-2977)#3185
caio-pizzol wants to merge 7 commits intomainfrom
caio-pizzol/SD-2977-public-type-deep-audit-gate

Conversation

@caio-pizzol
Copy link
Copy Markdown
Contributor

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. That means consumers can still lose IntelliSense after touching a public API surface that resolves to any.

This adds 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 present in deep-type-audit.allowlist.json. The audit runs with skipLibCheck: false and explicitly scans installed declarations for private @superdoc/* specifier leaks, so monorepo-local resolution cannot hide a broken published package.

The allowlist is seeded with the current 293 owned findings and tagged by tier so follow-up PRs can drain it by surface:

  • tier-1-pinia (160): Pinia/store exposure
  • tier-2-toolbar (46): toolbar / custom button config
  • tier-3-helpers (61): track changes and field annotation helpers
  • tier-4-public-contract (2): curated public contract leaks
  • tier-5-other (24): remaining owned declarations

The gate runs in PR CI and release CI, after the consumer matrix prepares the installed tarball. This also closes the release path gap by running the packed consumer matrix and deep audit before publish.

The allowlist is a starting line, not a waiver. New findings fail CI; stale findings fail CI so fixes must remove entries. Open follow-up: Pinia stores account for most entries and may be better hidden from the public surface than typed (SD-2966).

Verified:

  • node tests/consumer-typecheck/typecheck-matrix.mjs: 53 passed, 0 failed
  • node tests/consumer-typecheck/deep-type-audit.mjs: 293 findings, 0 new, 0 stale
  • Removing an allowlist entry triggers NEW finding output and exit 1 (failure path verified)

Linear: SD-2977 (umbrella SD-2976)

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
@caio-pizzol caio-pizzol requested a review from a team as a code owner May 6, 2026 21:14
@linear
Copy link
Copy Markdown

linear Bot commented May 6, 2026

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: 24a87851bd

ℹ️ 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 tests/consumer-typecheck/deep-type-audit.mjs Outdated
Comment thread tests/consumer-typecheck/deep-type-audit.mjs
…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
@codecov-commenter
Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

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
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
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.
…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)
…6 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.
@caio-pizzol caio-pizzol marked this pull request as draft May 7, 2026 14:21
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