Skip to content

finding(docs): snippets that import a package's own third-party dependency (lucide-react) cannot resolve it — the whole content/docs/layout group is blocked from #5867 #6120

Description

@yinlianghui-tw

Measured by #5867 batch 2 while probing which remaining groups can be re-fenced (session session_019b5UBNMtTzKbVtZZGvFuxe, on origin/main b9c367717).

The measurement

check-doc-snippet-types compiles each snippet as its own module at the repository root. It builds paths for every workspace package from that package's own exports, so @object-ui/layout resolves fine — but a third-party specifier is resolved the ordinary way, from the root node_modules, and a package's own dependency does not live there under pnpm:

node -e "require.resolve('lucide-react', { paths: ['<repo root>'] })"
→ MODULE_NOT_FOUND

lucide-react is a declared dependency of @object-ui/layout (packages/layout/package.json:35) and @object-ui/components (packages/components/package.json:77), at ^1.31.0. It is simply not hoisted to the root.

What it blocks

Re-fencing the three content/docs/layout/** pages (12 blocks, the #5867 remainder for that group) was probed with a throwaway mutation, gate run, and restore under a trap. Five of the twelve blocks red on this alone:

content/docs/layout/app-shell.mdx:12:39    TS2307: Cannot find module 'lucide-react' or its corresponding type declarations.
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)

These snippets are correct documentation — SidebarNav's NavItem.icon genuinely takes a lucide icon, and the examples import it exactly as a reader would in an app that depends on @object-ui/layout. There is no in-page fix that does not make the docs worse, and a FRAGMENT_MARKER on a genuinely-TypeScript block is forbidden by the #5867 lane. So the layout group cannot be taken until this is decided.

The deciding question, ⛔ not decided here

Which of these is the intended shape:

  1. the root declares lucide-react as a devDependency so the snippet program can resolve what the documented packages depend on (smallest change, but the root then carries a dep for the gate's benefit);
  2. the gate derives paths/typeRoots entries for the declared dependencies of the packages a covered document imports, so any package's own deps resolve the way a real consumer's would (matches the gate's existing "resolve against what a reader actually imports" principle, more work);
  3. it is accepted as an edge and the affected pages stay uncovered, declared with a reason.

⚠️ The scope is likely wider than layout: the same probe over content/docs/components/** did not hit it (those pages document schema interfaces rather than React usage), but any future page that shows real component usage will.

⛔ Out of scope of #5867's per-block re-fencing, which is why this is filed rather than worked around.

Refs: #5867 (the parent lane) · #6107 (the sibling blocker batch 1 filed).

Activity

  1. yinlianghui-tw commented on Aug 24, 2026

    @yinlianghui-tw
    CollaboratorAuthor

    PM ruling — so #5867's layout group is not blocked indefinitely

    Ruled by the domain:devx @ objectui execution seat (#5748), PM session session_019b5UBNMtTzKbVtZZGvFuxe. Filed by the #5867 batch-2 dev and correctly excluded from that batch.

    The framing is right, and it decides the answer

    The snippets are correct documentation; the resolution environment is the gap.

    That is the whole ruling. lucide-react is a real, declared dependency of @object-ui/layout and @object-ui/components. A consumer who installs those packages can import it. The snippets are teaching the truth; the gate is compiling them in an environment no real consumer has — from the repo root, where a transitive dep of a workspace package is not resolvable.

    So the defect is in the checker's model, not in the documentation.

    Ruled: the gate derives paths for the declared dependencies of the packages a covered document imports

    ⛔ Not a root devDependency. Adding lucide-react to the root manifest would put an entry in the repo's dependency graph that exists only to make a checker pass — it changes what the repository declares in order to satisfy a tool. That is the same shape this session has now refused twice: #5329 (minting a $schema URL because prose named one) and #6107 (minting exports because docs imported them). A manifest is a claim about what this package needs; a doc gate's convenience is not that.

    ⛔ Not accepting the edge. Five blocks left permanently uncovered, on pages that are correct, is the gate declining to check the thing it exists to check.

    ✅ The gate should model the environment a real consumer has. When a covered document imports @object-ui/layout, the program compiling its snippets should be able to resolve what @object-ui/layout itself declares — because that is exactly what a consumer's node_modules provides. Deriving those paths from the imported packages' own manifests keeps the gate honest and keeps the repo's declarations honest.

    ⚠️ Scope this narrowly. Derive from the declared dependencies of packages the covered document actually imports — do not add a blanket path mapping over all of node_modules, which would let a snippet import something no consumer could get and still pass. The gate's value is that it fails when a reader would fail; widening resolution past what the imported package declares destroys that property. State the rule in the gate's own docblock.

    Verification this owes

    • Non-vacuity in the direction that matters: after the change, a snippet importing something the imported package does not declare must still fail TS2307. Without that control you have widened resolution rather than corrected it.
    • The 5 currently-failing layout blocks compile, and the remaining 7 are separately accounted for.
    • Gate counts: covered set does not shrink, declared fragments unmoved, diagnostics 0. ⛔ No FRAGMENT_MARKER — these blocks are correct TypeScript and marking them would record a lie.
    • ⚠️ Re-measure on current origin/main first; batches 1 and 2 both landed since this was filed.

    Left pm:queue and unassigned for the triage seat to grade; the direction above is the ruling, not a claim on the card.

    Refs: #5867 / PR #6123 (batch 2, where this was measured) · #6107 · #5329 (the precedent against changing declarations to satisfy a tool).


    Generated by Claude Code

  2. added
    domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repo
    and removed on Aug 24, 2026
  3. yinlianghui-tw commented on Aug 24, 2026

    @yinlianghui-tw
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 6120,
      "status": "done",
      "branch": "claude/issue-6120-doc-snippet-dep-resolution",
      "pr": "https://github.com/objectstack-ai/objectui/pull/6129",
      "premise_still_valid": true,
      "summary": "Implemented the ruling: check-doc-snippet-types now derives paths for the specifiers each imported workspace 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 shipping no types is left unresolvable rather than approximated (measured: lodash, @types/glob). The rule and its edges are stated in the script's own docblock as the ruling asked. NO manifest in this repository changed — no root devDependency. A fourth harness control (undeclared) now runs on every invocation and bounds that narrowness: a module importing @floating-ui/react-dom — installed here as a transitive of Radix's popper under @object-ui/components' declared @radix-ui/react-popover, declared by no package a covered document imports — MUST still produce TS2307, and the control asserts its own two preconditions (the specifier is installed; no imported package has come to declare it). The layout pages are NOT re-fenced: that stays #5867's lane. No FRAGMENT_MARKER, no UNGATED_DOCS edit, no ledger edit.",
      "tests": "Verification union re-run at final HEAD 5aac1c46e (clean tree), each gate quoting its own printed verdict line, exit codes captured before any pipe. (A) pnpm vitest run scripts/__tests__/check-doc-snippet-types.test.ts from the repo ROOT, under the shared verify lock: 'Test Files  1 passed (1)' / 'Tests  30 passed (30)', wrapper 'VERDICT command-exit 0'. 12 of those 30 are new, pinning both directions of the rule. (B) node scripts/check-doc-snippet-types.mjs: 'Every covered documentation snippet compiles against the built types.', exit 0. (C) pnpm type-check:scripts: exit 0, no diagnostics. (D) pnpm lint:root: 'x 28 problems (0 errors, 28 warnings)' — all pre-existing; the full root population was linted (8s, no narrowing needed) and the two changed files measured separately at 0 errors / 0 warnings. (E) node scripts/check-control-bytes.mjs: 'check-control-bytes: OK (scanned 5081 tracked text file(s); skipped 85 binary).' (F) node scripts/check-changeset-presence.mjs: 'No source of a released package changed in this range, so no changeset is owed.' — an empty-frontmatter changeset is included anyway, this repo's convention for tooling-only PRs (objectui has no skip-changeset label and none was created). REPRODUCTION FIRST, on origin/main 133e2ea1e: a throwaway re-fence of exactly the five affected blocks under an EXIT trap reproduced all five TS2307s at the line:col the card named, and nothing else in four of the five blocks. AFTER THE FIX, same probe: grep -c 'TS2307.*lucide-react' returns 0, and the inferred type of the icon prop changed from a bare any to lucide's real forward-ref component type over LucideProps and RefAttributes of an SVGSVGElement — the types are genuinely loaded, not stubbed. THE GATE READS THE BUILT DIST, its own resolution control line from the final run: \"resolution   Module name '@object-ui/types' was successfully resolved to '/home/user/objectui-6120/packages/types/dist/index.d.ts'\". GATE COUNTS OTHERWISE UNMOVED, before and after on the unmodified tree, identical: '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.' One 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.' ABLATION of the negative control, run on the COMMITTED fix (no build leg — the gate runs from source, so the mutation is the whole artifact), mutation confirmed on disk by grepping for the injected ABLATION-WIDEN marker (count 1) and for the untouched anchor line (count 1), restore leg under an EXIT trap with the marker count measured back to 0 afterwards. Widening resolution to reach anything installed in the pnpm store: gate exit 1, 'undeclared   importing @floating-ui/react-dom (...) produced 0 diagnostic(s)', control failure 'a specifier NO imported package declares now resolves — third-party resolution has widened past the imported packages' own dependencies...' — and, the load-bearing half, 'Semantic phase: 206 of 206 block(s) judged, 0 failed' in the same run: every document stayed green while the gate lost the ability to fail. Predicted direction before running was 'red via the widening branch, documents still green'; 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. That is a real defect in my own control, fixed in the second commit (the check now reads the declared specifier set directly) and the ablation re-run against the corrected control, which is the run quoted above.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #6131 (sub-issue of #5867, unassigned): 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). It is the one layout block that stays red after this fix, for a reason unrelated to resolution. Annotating it NavGroup[] with the type imported in the same block takes it — and all five — to zero, measured: 'Semantic phase: 211 of 211 block(s) judged, 0 failed', gate exit 0. Searched open issues first (no duplicate; #4793 and #3999 are closed and about different keys). Not fixed here: re-fencing the layout blocks is #5867's lane, explicitly out of scope for this card."
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

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

Metadata

Metadata

Labels

domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repopm:dispatchedtooling

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions