Skip to content

feat(typecheck): public-method fixture coverage gate#3481

Open
caio-pizzol wants to merge 3 commits into
mainfrom
caio-pizzol/SD-typecheck-fixture-coverage-gate
Open

feat(typecheck): public-method fixture coverage gate#3481
caio-pizzol wants to merge 3 commits into
mainfrom
caio-pizzol/SD-typecheck-fixture-coverage-gate

Conversation

@caio-pizzol
Copy link
Copy Markdown
Contributor

Mechanizes the rule documented in CONTRIBUTING.md after #3480. Every public SuperDoc method or getter must satisfy its computed type obligations via a typed consumer fixture, or be explicitly allowlisted with a reason.

Why this exists

The SuperDoc.jsSuperDoc.ts migration shipped search(text: string), a narrowed version of the previous string | RegExp contract. Every existing gate passed - check:types, check:public:superdoc, the consumer matrix, the deep-type audit, 1054 tests. The bot caught it because no consumer fixture asserted Parameters<SuperDoc['search']>. The miss class: an unguarded public method can ship with wrong-but-explicit types.

This gate closes that class. It does not guarantee the assertion is correct (a typed-but-wrong assertion would still pass); it guarantees a reviewer was asked to write one - per obligation. The gate is obligation-based, not mention-based: for each public member, the AST computes which assertions are meaningful, and the fixture must satisfy each one.

Obligation model (per member, from the AST)

  • method with ≥1 parameter → requires parameters
  • method with non-void return → requires returns
  • getter → requires returns
  • zero-param method that returns void/Promise → requires call (otherwise renaming would slip past silently)

Satisfaction patterns

Obligation Pattern in any tests/consumer-typecheck/src/*.ts
parameters Parameters<SuperDoc['name']>
returns (method) ReturnType<SuperDoc['name']>
returns (getter) SuperDoc['name'] (indexed access) or typeof sd.name
call sd.name( or superdoc.name(

Call sites do NOT satisfy parameters or returns on their own. This is the central distinction from a mention-based ratchet - and the reason v1 of this gate didn't catch search. A call site sd.search('hello') was present; Parameters<SuperDoc['search']> was not.

Failure modes (all verified by simulation)

  1. New unmet obligation - temporarily removed Parameters<SuperDoc['search']> from search-match.ts → FAIL with + search:parameters. Proves the gate catches the bug class it was built for.
  2. Snapshot drift - added nonexistent name to snapshot → FAIL with --write refresh hint.
  3. Allowlist with empty reason → FAIL: broadcastReady: missing or empty reason.
  4. Allowlist key not a member → FAIL: noSuchMethod: not a public member of SuperDoc (typo or stale entry).

Files

  • tests/consumer-typecheck/check-public-method-coverage.mjs - the gate (~300 lines).
  • tests/consumer-typecheck/public-method-coverage-allowlist.cjs - escape hatch. 8 entries today (6 broadcast* lifecycle relays + setActiveEditor + onContentError). Each key validated against the AST; each value must be a non-empty string.
  • tests/consumer-typecheck/public-method-coverage-debt-snapshot.json - 59 unmet obligations across 36 non-allowlisted members. Future PRs drain via fixtures + --write.
  • scripts/check-public-contract.mjs - new stage 4 public-method-coverage between jsdoc-ratchet and build. Stage count: 9 → 10. Docstring + stage blurb use obligation language consistently.
  • AGENTS.md, packages/superdoc/scripts/README.md - refreshed stage count + descriptions to include the new stage.

End-to-end

  • Baseline → OK 75 obligations across 36 members; 59 tracked as known debt; ratchet snapshot in sync.
  • pnpm check:public:superdoc --skip-build → PASS, 9 ran / 1 skipped, ~120s; new stage runs as [4/10].

Scope kept tight

  • Not added: runtime payload tests for SuperDocEventMap events SuperDoc emits directly. Separate planned PR (different shape, ordered cheap → expensive: ready, editorCreate, editorBeforeCreate first).
  • Not added: exact-shape AssertEqual<> enforcement. The current rule is "an assertion exists per obligation." Tightening to "the assertion matches the source signature exactly" requires a TS program (not just AST parsing), is significantly more expensive, and can be a future tightening after the debt drains.

Mechanizes the rule documented in CONTRIBUTING.md after #3480: every
public SuperDoc method or getter must have at least one consumer-side
fixture reference, or be explicitly allowlisted with a reason.

Why this exists: the SuperDoc.js -> SuperDoc.ts migration shipped
`search(text: string)`, a narrowed version of the previous
`string | RegExp` contract. Every existing gate passed -
`check:types`, `check:public:superdoc`, the consumer matrix, the
deep-type audit, 1054 tests. The bot caught it because no consumer
fixture asserted Parameters<SuperDoc['search']>. The miss was: an
unguarded public method can ship with wrong-but-explicit types.

This gate prevents the next instance of that class. It does not
guarantee the assertion is correct (a typed but wrong assertion
would still pass); it guarantees a reviewer was asked to write one.

Implementation matches the JSDoc ratchet shape from PR #3474:

- tests/consumer-typecheck/check-public-method-coverage.mjs - walks
  SuperDoc.ts with the TypeScript AST, enumerates public methods +
  getters (skipping private, static, @internal, # prefix, and the
  EventEmitter inherited surface). For each, scans every fixture
  under tests/consumer-typecheck/src/ for one of
  `Parameters<SuperDoc['name']>`, `ReturnType<SuperDoc['name']>`,
  or `(superdoc|sd).name(`. Fails when a NEW member is uncovered
  (i.e. not on the debt snapshot) or when a snapshot entry is
  stale (now covered).
- tests/consumer-typecheck/public-method-coverage-allowlist.cjs -
  the escape hatch for members that are intentionally not consumer-
  callable (8 entries today: broadcast* lifecycle relays + the
  internal setActiveEditor / onContentError handlers).
- tests/consumer-typecheck/public-method-coverage-debt-snapshot.json
  - records the 33 currently-uncovered public methods. Future PRs
  can drain it by adding fixtures and running `--write`.
- scripts/check-public-contract.mjs - new stage 4
  `public-method-coverage` between jsdoc-ratchet and build. Stage
  count: 9 -> 10.

Verified (all simulated against the actual main HEAD):
- baseline -> OK 36 public members; 33 tracked as known debt
- simulated new uncovered method (drop one snapshot entry) -> FAIL
  with "1 NEW public member(s) without any fixture reference"
- simulated stale entry (add nonexistent name to snapshot) -> FAIL
  with "1 stale entry/entries" + `--write` refresh hint
- pnpm check:public:superdoc --skip-build -> PASS 9 ran / 1 skipped,
  123.4s; new stage runs as [4/10] without disturbing the rest

Followup, separate PR: runtime payload tests for SuperDocEventMap
events SuperDoc emits directly (cheap events first: ready,
editorCreate, editorBeforeCreate).
…llowlist

Review caught three blockers in the first version:

1. The gate counted any call site as coverage, so the search regression
   would have slipped through (search-match.ts has sd.search('hello')).
2. Getters had no satisfaction pattern (Parameters/ReturnType are
   method-shaped); a future getter could only pass via the snapshot.
3. The allowlist contract (non-empty reason, key must match a real
   member) was unvalidated.

Rewrote the gate as obligation-based, not mention-based:

For each public member, compute REQUIRED obligations from the AST:
- method with >=1 param        → requires "parameters"
- method with non-void return  → requires "returns"
- getter                       → requires "returns"
- zero-param void method       → requires "call"
  (otherwise renaming would slip past silently)

Satisfaction patterns:
- parameters       → Parameters<SuperDoc['name']>
- returns (method) → ReturnType<SuperDoc['name']>
- returns (getter) → SuperDoc['name']  or  typeof sd.name
- call             → (superdoc|sd).name(

**Call sites no longer satisfy parameters/returns** - they only
satisfy the "call" obligation. This is the central fix for the
search regression class.

The debt snapshot now tracks obligation-level entries like
"state:returns" instead of just member names, so partial coverage
is visible.

Allowlist validation: each key must match an actual public member
of SuperDoc; each value must be a non-empty string. Empty reasons,
typos, and stale keys all fail with specific messages.

Snapshot count: 33 (member-level) → 59 (obligation-level), reflecting
the actual unmet obligation surface.

Verified:
- Baseline: OK 75 obligations across 36 members, 59 tracked as debt.
- Sim search regression (remove Parameters<SuperDoc['search']>) →
  FAIL with "+ search:parameters". Proves the gate now catches the
  bug class it was built for.
- Sim stale snapshot entry → FAIL with --write hint.
- Sim allowlist with empty reason → FAIL with "missing or empty reason".
- Sim allowlist key not a member → FAIL with "not a public member of
  SuperDoc (typo or stale entry)".
- pnpm check:public:superdoc --skip-build → PASS 9 ran / 1 skipped,
  122.5s; new stage runs as [4/10].
…ew gate

After adding the public-method-coverage stage:

- AGENTS.md: nine → ten stages; added `public-method-coverage` to
  the staged list and to the umbrella description.
- packages/superdoc/scripts/README.md: new row for
  `check-public-method-coverage.mjs` in the consumer-typecheck
  infrastructure table (snapshot + allowlist paths, refresh
  command, the call-site distinction); updated the stage-count
  sentence (five → six wrapper stages of check:public:superdoc
  after the policy gates) and added public-method-coverage to the
  cheap-policy-gate list.
- scripts/check-public-contract.mjs: tightened both the stage 4
  docstring and the stage `blurb` to use 'obligation' language
  consistently. The blurb explicitly says call sites do NOT
  satisfy parameters/returns on their own — the central fix that
  separated v2 from v1.
@caio-pizzol caio-pizzol requested a review from a team as a code owner May 25, 2026 01:03
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: 6ac67cf395

ℹ️ 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 on lines +168 to +169
const files = readdirSync(FIXTURE_DIR).filter(
(f) => f.endsWith('.ts') || f.endsWith('.cts') || f.endsWith('.mts'),
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 Scan fixture tree recursively

The gate advertises coverage across every fixture under tests/consumer-typecheck/src/, but this only reads direct children of src/. If a fixture is added in a nested folder (a common organization change), its assertions are ignored and the ratchet will report unmet obligations even though valid coverage exists, creating false CI failures.

Useful? React with 👍 / 👎.

@codecov-commenter
Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

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