Skip to content

fix(typecheck): enforce public-contract tier discipline in check:public:superdoc#3473

Merged
caio-pizzol merged 4 commits into
mainfrom
caio-pizzol/SD-typecheck-tier-enforcement
May 24, 2026
Merged

fix(typecheck): enforce public-contract tier discipline in check:public:superdoc#3473
caio-pizzol merged 4 commits into
mainfrom
caio-pizzol/SD-typecheck-tier-enforcement

Conversation

@caio-pizzol
Copy link
Copy Markdown
Contributor

@caio-pizzol caio-pizzol commented May 24, 2026

PR 4 of the type-checking organization plan. Adds a real enforcement gate for the SD-3256 publicContract tier metadata, which today is read-only (report:public:superdoc prints drift but never fails CI). Per the SD-3256 Phase 2 docstring: "Phase 4 will gate" - this is that gate. Builds on #3472 (merged).

What changed

scripts/report-public-contract.mjs - one script, two modes

The existing read-only report and the new enforcement gate share one file. The pure validatePublicContract(publicContract, exportsMap) function is exported for unit testing; the CLI dispatches on --check:

  • pnpm report:public:superdoc (default, no flag) - human-readable tier listing + a validator status block. Report mode does not fail on contract drift; load/runtime errors can still exit non-zero.
  • node scripts/report-public-contract.mjs --check - enforcing gate. Fails on any invariant violation.

Both modes read the same two inputs (package.json#exports + type-surface.config.cjs) and use the same validator. Cheap (~10ms; no build-output traversal).

--check fails when:

  1. package.json#exports has a subpath missing from publicContract
  2. publicContract has a stale entry not in package.json#exports
  3. A subpath appears in more than one tier
  4. An entry's tier field disagrees with its bucket (e.g. { subpath: '.', tier: 'legacy' } placed inside publicContract.supported)
  5. Routing rules:
    • supported routes through dist/superdoc/src/public/** (and NOT legacy/)
    • legacy routes through dist/superdoc/src/public/legacy/**
    • legacyRaw must NOT route through dist/superdoc/src/public/**
  6. legacyRaw is restricted to the explicitly accepted set (currently only ./super-editor per the SD-3256 Phase 3 plan)

scripts/report-public-contract.test.mjs - 19 unit tests

node:test cases covering every failure class above, plus the three supported exports-entry shapes (string / conditional types object / asset with no types), and the conditional-types divergence cases added in 52ff9c0 (a divergent types.require that routes outside the supported / legacy directory must fail). Catches regressions in the validator. Runs in ~36ms.

scripts/check-public-contract.mjs - wrapper

Added two stages at the top so a tier drift fails fast before the slow build/matrix work, and the validator is verified to work before it's trusted. Stage count: 6 → 8.

  1. tier-discipline:test (new) - validator unit tests
  2. tier-discipline (new) - node scripts/report-public-contract.mjs --check
  3. build:superdoc
  4. typecheck-matrix
  5. deep-type-audit --strict-supported-root
  6. package-shape-gate
  7. snapshot --all --check
  8. check-root-classification-closure

Why one script instead of two

Originally split into check-public-contract-tiers.mjs + report-public-contract.mjs. Folded into one because they're the same concept (public-contract tier metadata) and contributors only ever invoke them through pnpm report:public:superdoc or pnpm check:public:superdoc anyway. Keeps the scripts directory from growing.

Verified end-to-end

  • node --test scripts/report-public-contract.test.mjs → 19/19 PASS, ~36ms
  • node scripts/report-public-contract.mjs --check → PASS (12 exports / 12 contract entries)
  • node scripts/report-public-contract.mjs (default) → tier report + "OK: all tier invariants satisfied"
  • Simulated bad contract → validator returns failures + exits non-zero
  • L1 agent-docs scan of root AGENTS.md → 0 broken refs

Doc updates

  • AGENTS.md: check:public:superdoc stage count 6 → 8; added legacy-raw to the tier list on report:public:superdoc; clarified report:public:superdoc is the read-only sibling.
  • packages/superdoc/scripts/README.md: same legacy-raw fix; documented the one-script-two-modes shape and the six invariants.

…ic:superdoc

Today the SD-3256 publicContract metadata in
packages/superdoc/scripts/type-surface.config.cjs is enforced only
by humans: `report:public:superdoc` prints drift but never fails CI.
Per the SD-3256 Phase 2 docstring, "Phase 4 will gate" — this PR
is that gate.

Adds `scripts/check-public-contract-tiers.mjs`. Cheap (~10ms; reads
package.json + the JS config, no I/O). Fails when:

  1. package.json#exports has a subpath missing from publicContract
  2. publicContract has a stale entry not in package.json#exports
  3. a subpath appears in more than one tier
  4. routing rules: supported routes through src/public/** (not
     legacy/); legacy routes through src/public/legacy/**;
     legacyRaw must NOT route through src/public/**
  5. legacyRaw is restricted to the explicitly accepted set
     (currently only `./super-editor` per SD-3256 Phase 3 plan)

Wired into scripts/check-public-contract.mjs as stage 1 (before
build). Fast-fails so a tier drift surfaces in seconds, before the
slow build/matrix/audit stages even start. Stage count: 6 -> 7.

report:public:superdoc stays read-only by design. Same naming
convention as PR 1: report:* prints, check:* enforces. The two
read the same publicContract source of truth.

Verified by injecting each violation class and confirming the
check fails with a clear, actionable message:

  TEST 1 MISSING contract entry (added export, no tier):
    fails: "MISSING contract entry: ... add to a tier with a routing note."
  TEST 2 STALE contract entry (in config, not in exports):
    fails: "STALE contract entry: ... remove from publicContract or restore the export."
  TEST 3 subpath in two tiers:
    fails: "subpath \"./converter\" appears in multiple tiers: legacy and asset"
  TEST 4 supported routes to non-public path:
    fails: "supported \"./types\": types resolve to ... — expected to route through ./dist/superdoc/src/public/**"
  TEST 5 legacy routes to non-legacy path:
    fails: "legacy \"./converter\": types resolve to ... — expected to route through ./dist/superdoc/src/public/legacy/**"
  TEST 6 legacyRaw not in allowlist:
    fails: "legacyRaw \"./fake-legacy-raw\": not on the accepted list ([./super-editor]). New legacy entries must route through src/public/legacy/** instead."

Verified end-to-end:
  - node scripts/check-public-contract-tiers.mjs -> PASS exit 0
    (12 exports / 12 contract entries / 12 tiered)
  - pnpm check:public:superdoc --skip-build -> PASS 6 ran / 1 skipped, 120.9s
  - pnpm check:public -> PASS umbrella green end-to-end
  - L1 agent-docs scan of root AGENTS.md -> no broken refs

Doc updates: AGENTS.md and packages/superdoc/scripts/README.md
reflect the new 7-stage count, the tier-discipline gate, and the
clarification that report:public:superdoc is still the read-only
sibling.
Review-feedback follow-up to the prior tier-discipline commit. Closes
the one real logic hole and addresses two doc/wording corrections.

scripts/check-public-contract-tiers.mjs:

- Enforce per-entry `tier` field matches its bucket. Previously the
  script trusted bucket position (an entry in `publicContract.supported`
  was assumed supported) and ignored the duplicated `tier` field on
  each entry. A typo like `{ subpath: '.', tier: 'legacy' }` placed
  inside `publicContract.supported` would have silently passed.
  Added a BUCKET_TO_TIER map (handles the kebab-case quirk:
  `legacyRaw` bucket → `'legacy-raw'` tier value) and a per-entry
  check that fails with a clear message.
- Reworded the "no I/O" claim in the docstring to "only reads
  package.json and the JS config, no build-output traversal" —
  technically accurate; the prior wording was sloppy.
- Refactored the validation into a pure exported function
  `validatePublicContract(publicContract, exportsMap)` that returns
  a failure list. The CLI entry still does the filesystem reads,
  prints the report, and sets the exit code. The pure function is
  testable.

scripts/check-public-contract-tiers.test.mjs (new, 17 cases):

- node:test suite covering: minimal pass, missing/stale contract
  entries, multi-tier partition, per-entry tier field (including
  the legacyRaw kebab-case quirk), routing rules per tier (supported
  / legacy / legacyRaw), legacyRaw allowlist (accepted + rejected +
  accidental public/ routing), and the three exports-entry shapes
  (string, conditional types object, no-types asset).
- 17/17 pass under `node --test scripts/check-public-contract-tiers.test.mjs`.

Wrapper (scripts/check-public-contract.mjs):

- Added stage 1 `tier-discipline:test`. Cheap (~50ms). Runs before
  the tier-discipline gate itself, so the validator is verified to
  catch every failure class before the next stage trusts its
  verdict. Stage count: 7 -> 8.
- Updated the docstring stage list and the matrix-ordering note
  (stages 5-8 reuse the install/tarball; was 4-7).

AGENTS.md and packages/superdoc/scripts/README.md:

- Added `legacy-raw` to the tier list on the report:public:superdoc
  line. The previous wording omitted it, which was misleading given
  legacyRaw is the whole point of the new enforcement gate.
- Updated `check:public:superdoc` description to 8 stages.

Verified end-to-end:
- node --test scripts/check-public-contract-tiers.test.mjs → 17/17 PASS
- node scripts/check-public-contract-tiers.mjs → PASS (12/12 tiered)
- pnpm check:public:superdoc --skip-build → PASS 7 ran / 1 skipped, 117.4s
- L1 agent-docs scan of root AGENTS.md → 0 broken refs
@caio-pizzol caio-pizzol requested a review from a team as a code owner May 24, 2026 01:00
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: c63017e331

ℹ️ 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 scripts/check-public-contract-tiers.mjs Outdated
@codecov-commenter
Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…mjs --check

One script for the public-contract tier metadata, two modes:
- default: human-readable report (read-only)
- --check: enforcing gate, fails on any invariant violation

Keeps the same pure validator (now exported from report-public-contract.mjs)
and the same 17 unit tests (renamed to scripts/report-public-contract.test.mjs).
Wrapper updated to call the consolidated script.
resolveTypesPath previously returned only types.import when both
import and require were present. A supported/legacy subpath could
route types.require to the wrong directory and the gate would PASS.

Renamed to resolveTypesPaths returning a deduped array of every
candidate path; the supported/legacy/legacyRaw routing loops now
iterate so each conditional branch is validated independently. Two
new unit tests cover the divergent-require case for supported and
legacy tiers.
@caio-pizzol caio-pizzol merged commit 031d2cf into main May 24, 2026
70 checks passed
@caio-pizzol caio-pizzol deleted the caio-pizzol/SD-typecheck-tier-enforcement branch May 24, 2026 10:17
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