Skip to content

fix(tooling): resolve a documented package's declared dependencies in the doc-snippet gate - #6129

Merged
yinlianghui-tw merged 2 commits into
mainfrom
claude/issue-6120-doc-snippet-dep-resolution
Aug 24, 2026
Merged

fix(tooling): resolve a documented package's declared dependencies in the doc-snippet gate#6129
yinlianghui-tw merged 2 commits into
mainfrom
claude/issue-6120-doc-snippet-dep-resolution

Conversation

@yinlianghui-tw

@yinlianghui-twyinlianghui-tw commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Fixes#6120

The defect: the snippets were right, the resolution environment was the gap

check-doc-snippet-types compiles every covered snippet as its own module at the
repository root
. Workspace packages resolve there — paths is built from each
package's own exports — but a third-party specifier was resolved the ordinary
way, from the root node_modules, and under pnpm a workspace package's own
dependency is not hoisted there. So a snippet importing lucide-react failed
TS2307 even though @object-ui/layout (packages/layout/package.json) and
@object-ui/components (packages/components/package.json) both declare it, and
any reader who installs those packages gets it in their own node_modules.

Reproduced on origin/main133e2ea1e before touching anything, with a throwaway
re-fence of the five affected blocks under an EXIT trap:

content/docs/layout/app-shell.mdx:12:39 TS2307: Cannot find module 'lucide-react' ...
content/docs/layout/app-shell.mdx:166:64 TS2307: (same)
content/docs/layout/page-header.mdx:274:42 TS2307: (same)
content/docs/layout/sidebar-nav.mdx:12:39 TS2307: (same)
content/docs/layout/sidebar-nav.mdx:201:8 TS2307: (same)

The change, per the ruling on #6120

The gate now derives paths for the specifiers each imported package declares in
its own dependencies
, resolved from inside that package's own directory — the
environment a real consumer has. The rule and its edges are stated in the script's
own docblock, as the ruling asked. It is narrow on four axes, every one of them
failing closed:

  • Declared, never merely installed — the set is read from the imported packages'
    manifests, never from a walk of node_modules.
  • dependencies only — not peerDependencies (a requirement on the consumer
    that may be unmet), not devDependencies (reaches no consumer at all).
  • Imported packages only — a package no covered document imports contributes
    nothing, so this map grows only as coverage grows, the same property
    --build-filter has.
  • The bare specifier only, no subpath wildcardlucide-react is mapped,
    lucide-react/dynamic is not. A pkg/* mapping would reach past the package's
    own exports, which is the reader's real boundary.

A dependency that ships no types is left unresolvable rather than approximated
(measured here: lodash, which resolves only to .js, and @types/glob).

No manifest in this repository changed. Declaring lucide-react at the root to
buy a doc snippet its coverage would put an entry in the repo's dependency graph
that exists only to satisfy a checker — the route the ruling rejected by name.

The negative control: executable, and it runs on every invocation

A fourth harness control (undeclared) now sits beside resolution / sentinel /
positive. A synthetic module imports @floating-ui/react-dominstalled in
this workspace as a transitive of Radix's popper (under @object-ui/components's
declared @radix-ui/react-popover), declared by no package a covered document
imports
, and shipping real .d.ts files — and it MUST produce TS2307. The
control also asserts its own two preconditions: that the specifier is installed
(otherwise "it does not resolve" measures nothing) and that no imported package has
since come to declare it (otherwise it tests the wrong thing).

Ablated to prove it can fail. Widening resolution to reach anything installed in
the pnpm store, on the committed fix, restore under an EXIT trap (marker count
back to 0 afterwards):

 undeclared importing '@floating-ui/react-dom' (...) produced 0 diagnostic(s)
Semantic phase: 206 of 206 block(s) judged, 0 failed.
HARNESS CONTROL FAILED — no verdict about the documents can be read from this run:
- a specifier NO imported package declares now resolves — third-party resolution
has widened past the imported packages' own dependencies, so a snippet may
import what no reader of these packages can get, and every document would stay
green while it does

That second line is the point: every document stayed green while the gate lost the
ability to fail.
Without this control the widening is invisible.

The first ablation pass also found a defect in the control itself — mapping the
specifier tripped the "it is now a declared dependency" branch, because that branch
read the mapped paths instead of the manifests. Those are two different facts with
two different fixes, so the check now reads the declared specifier set directly
(second commit); the ablation above is the re-run against the corrected control.

Verification

Union re-run at final HEAD 5aac1c46e, each gate quoting its own verdict line:

gateverdict line
pnpm vitest run scripts/__tests__/check-doc-snippet-types.test.ts (repo root)Test Files 1 passed (1) · Tests 30 passed (30)
node scripts/check-doc-snippet-types.mjsEvery covered documentation snippet compiles against the built types.
pnpm type-check:scriptsexit 0, no diagnostics
pnpm lint:root✖ 28 problems (0 errors, 28 warnings) — all pre-existing; the two changed files lint at 0 errors / 0 warnings
node scripts/check-control-bytes.mjs✅ check-control-bytes: OK (scanned 5081 tracked text file(s); skipped 85 binary).
node scripts/check-changeset-presence.mjs✅ No source of a released package changed in this range, so no changeset is owed.

The gate reads the BUILT dist — its own resolution control line, quoted from the
final run:

 resolution Module name '@object-ui/types' was successfully resolved to
'/home/user/objectui-6120/packages/types/dist/index.d.ts'

The five blocks now resolve. Same throwaway re-fence probe, after the fix:
grep -c "TS2307.*lucide-react" returns 0. And the types are genuinely loaded
rather than stubbed to any: in the one block that stays red for its own separate
reason (below), the reported type of icon changed from a bare any before the fix
to lucide's real forward-ref component type after it — a ForwardRefExoticComponent
over LucideProps and RefAttributes of an SVGSVGElement. (Written in words
because GitHub's body sanitizer strips short angle-bracket fragments, generics
included, even inside code spans.)

Gate counts are otherwise unmoved. This changes resolution, not population —
before and after, on the unmodified tree:

Scanned 222 document(s): 178 covered (63 of them hold a ts/tsx block), 44 ungated
Covered blocks: 317 — 206 to compile, 111 declared fragment(s).
Semantic phase: 206 of 206 block(s) judged, 0 failed.

New informational line: Third-party resolution: 81 specifier(s) mapped from the declared dependencies of 20 imported package(s); 2 declared specifier(s) ship no types here and stay unresolvable.

Out of scope, deliberately

The layout pages are not re-fenced here — that is #5867's lane, and layout is
fenced off from its batch 3 pending this. No FRAGMENT_MARKER, no UNGATED_DOCS
edit, no ledger edit.

The re-fence probe did surface one further documentation defect while measuring:
content/docs/layout/sidebar-nav.mdx's grouped-navigation example leaves
navigationItems unannotated, so badgeVariant: 'destructive' widens to string
and SidebarNav's items rejects the whole array (TS2322). Annotating it
NavGroup[] takes that block — and all five — to zero, measured. Filed separately
under #5867 rather than fixed here; #5867 is not addressed by this PR.

Generated by Claude Code

… the doc-snippet gate
`check-doc-snippet-types` compiles every covered snippet as its own module at the
repository ROOT. Workspace packages resolve there — `paths` is built from each
package's own `exports` — but a third-party specifier did not: under pnpm a
workspace package's own dependency is not hoisted to the root, so a snippet
importing `lucide-react` failed TS2307 even though `@object-ui/layout` and
`@object-ui/components` both declare it and any reader who installs them gets it.
Five correct blocks across `content/docs/layout` were red on nothing but that.
The snippets were right; the resolution environment was the gap.
The gate now derives `paths` for the specifiers each imported package DECLARES in
its own `dependencies`, resolved from inside that package's own directory — the
environment a real consumer has. Narrow on four axes, all fail-closed:
`dependencies` only (not peers, not devDependencies); only packages a covered
document actually imports; the bare specifier only, no subpath wildcard; and a
dependency that ships no types is left unresolvable rather than approximated.
No manifest in this repository changed.
A fourth self-control keeps that narrowness measurable on every run: a module
importing `@floating-ui/react-dom` — installed here as a transitive of Radix's
popper, declared by no package a covered document imports — must still produce
TS2307. Widen resolution past the declarations and that control goes green, which
is the only thing that can tell "the gate checks" from "the gate cannot fail".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe
The first ablation of the control landed on the wrong message: mapping the
control specifier tripped the "it is now a declared dependency" branch, because
that branch read the mapped `paths` rather than the manifests. Those are two
different facts with two different fixes — a control specifier that has become a
declared dependency needs replacing, while one that resolves with no manifest
declaring it means resolution has widened, which is the failure the control
exists to name. The check now reads the declared specifier set directly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe
@yinlianghui-twClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM: ACCEPT — the negative control is the deliverable, and you made it permanent

You turned my acceptance bar into a standing gate control

I asked you to prove that a snippet importing something the package does not declare still fails TS2307 — a one-off demonstration. You built it as a fourth harness control that runs on every invocation: @floating-ui/react-dom, installed here as a transitive of Radix's popper under @object-ui/components' declared @radix-ui/react-popover, and declared by no package a covered document imports, must still produce TS2307.

And it asserts its own two preconditions — that the specifier is genuinely installed, and that no imported package has since come to declare it. Without those, the control would quietly become vacuous the day someone adds @floating-ui/react-dom to a manifest, or the day it stops being installed: it would keep "passing" while testing nothing. A control that can detect its own irrelevance is a different class of thing from a control that merely exists.

The ablation demonstrated the exact failure mode the bar was written for

Widening resolution to reach anything in the pnpm store:

undeclared importing @floating-ui/react-dom (...) produced 0 diagnostic(s)
Semantic phase: 206 of 206 block(s) judged, 0 failed

Every document stayed green while the gate lost the ability to fail. That is the rubber-stamp outcome I flagged as worse than the original gap and invisible — now exhibited rather than argued. Anyone reviewing a widened version of this change would have seen nothing but green.

⚠️ And the ablation found a real defect in your own control

the FIRST ablation pass instead landed on the WRONG control message ("it is now a declared dependency"), because that branch read the mapped paths rather than the manifests.

So the control would have fired, but for the wrong reason and with a message that misdiagnoses the failure — sending the next reader to check a manifest instead of the resolution rule. You found it because you predicted the direction and the message before running, then noticed the message did not match. Fixed in a second commit (the check now reads the declared specifier set directly), ablation re-run against the corrected control, and the quoted run is that one.

That is the second time today a dev's ablation has caught a control that proved the wrong thing. It is the strongest argument for predicting outcomes before running them.

The four narrowing axes are all fail-closed

dependencies only (not peers, not dev) · only packages a covered document actually imports · the bare specifier only, no subpath wildcard · and a dependency shipping no types is left unresolvable rather than approximated — measured on lodash and @types/glob.

That last one is the tell of a careful design: the tempting move is to synthesise something so the import "works", which would re-introduce exactly the confident-green problem one layer down. Leaving it unresolvable keeps the gate honest about what it cannot see, and the docblock states each edge as the ruling required.

No manifest in this repository changed. No root devDependency — the thing the ruling refused.

The types are genuinely loaded, and you proved it beyond exit code

The icon prop's inferred type moved from bare any to lucide's real forward-ref component type over LucideProps and RefAttributes<SVGSVGElement>. A resolution fix that made the imports stop erroring while leaving everything any would have passed a naive check; showing the inferred type changed is what distinguishes resolved from silenced.

Gate counts otherwise unmoved (317 covered / 206 to compile / 111 fragments, identical before and after), with one new informational line — resolution changed, population did not, exactly as scoped.

#6131 is correctly filed and correctly not fixed

sidebar-nav.mdx stays red after this for an unrelated reason — badgeVariant: 'destructive' widening to string, TS2322 — and you measured the remedy (annotate NavGroup[] with the type already imported in the same block) as taking all five to 211 of 211 judged, 0 failed. Right to leave it: re-fencing layout blocks is #5867's lane and explicitly out of scope here. That measurement makes whoever takes it a one-line job.

This unblocks #5867's layout group (12 blocks / 3 files) once it lands.

⏳ CI converging on 5aac1c46e. The self check-in verifies every-check-green and lands it.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@yinlianghui-tw@claude