fix(typecheck): enforce public-contract tier discipline in check:public:superdoc#3473
Merged
Merged
Conversation
…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
There was a problem hiding this comment.
💡 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".
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:superdocprints 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 modesThe 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).--checkfails when:package.json#exportshas a subpath missing frompublicContractpublicContracthas a stale entry not inpackage.json#exportstierfield disagrees with its bucket (e.g.{ subpath: '.', tier: 'legacy' }placed insidepublicContract.supported)supportedroutes throughdist/superdoc/src/public/**(and NOTlegacy/)legacyroutes throughdist/superdoc/src/public/legacy/**legacyRawmust NOT route throughdist/superdoc/src/public/**legacyRawis restricted to the explicitly accepted set (currently only./super-editorper the SD-3256 Phase 3 plan)scripts/report-public-contract.test.mjs- 19 unit testsnode:testcases 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 divergenttypes.requirethat routes outside the supported / legacy directory must fail). Catches regressions in the validator. Runs in ~36ms.scripts/check-public-contract.mjs- wrapperAdded 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.
tier-discipline:test(new) - validator unit teststier-discipline(new) -node scripts/report-public-contract.mjs --checkbuild:superdoctypecheck-matrixdeep-type-audit --strict-supported-rootpackage-shape-gatesnapshot --all --checkcheck-root-classification-closureWhy 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 throughpnpm report:public:superdocorpnpm check:public:superdocanyway. Keeps the scripts directory from growing.Verified end-to-end
node --test scripts/report-public-contract.test.mjs→ 19/19 PASS, ~36msnode 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"AGENTS.md→ 0 broken refsDoc updates
AGENTS.md:check:public:superdocstage count 6 → 8; addedlegacy-rawto the tier list onreport:public:superdoc; clarifiedreport:public:superdocis the read-only sibling.packages/superdoc/scripts/README.md: samelegacy-rawfix; documented the one-script-two-modes shape and the six invariants.