From 614d4f6e9cfd9932c7ab1bcfc1b512d3e43b413a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 19:25:15 +0000 Subject: [PATCH 1/2] fix(tooling): resolve a documented package's declared dependencies in the doc-snippet gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe --- .../6120-doc-snippet-dependency-resolution.md | 31 +++ .../__tests__/check-doc-snippet-types.test.ts | 151 ++++++++++++ scripts/check-doc-snippet-types.mjs | 223 +++++++++++++++++- 3 files changed, 403 insertions(+), 2 deletions(-) create mode 100644 .changeset/6120-doc-snippet-dependency-resolution.md diff --git a/.changeset/6120-doc-snippet-dependency-resolution.md b/.changeset/6120-doc-snippet-dependency-resolution.md new file mode 100644 index 0000000000..01f3621a10 --- /dev/null +++ b/.changeset/6120-doc-snippet-dependency-resolution.md @@ -0,0 +1,31 @@ +--- +--- + +Doc-snippet gate tooling only, no published package source changed. + +`check-doc-snippet-types` compiles every covered snippet as its own module at the +repository ROOT. Workspace packages resolved there — it builds `paths` 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 that +imports `lucide-react` failed `TS2307` even though `@object-ui/layout` and +`@object-ui/components` both declare it and any reader who installs those +packages gets it. Five correct blocks across `content/docs/layout` were red on +nothing but that, which blocked the whole group from being brought under the +gate. 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 directory — the +environment a real consumer has. Deliberately narrow, and it fails closed: +`dependencies` only (not peers, not devDependencies), only packages a covered +document actually imports, only the bare specifier (no subpath wildcard), and a +dependency shipping no types is left unresolvable rather than approximated. The +repository's own manifests are untouched — declaring `lucide-react` at the root +to buy a snippet its coverage would change what this repo claims to need in order +to satisfy a checker. + +A fourth self-control (`undeclared`) now runs on every invocation and keeps that +narrowness honest: 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 way to notice that the gate has become +a rubber stamp no snippet can fail. diff --git a/scripts/__tests__/check-doc-snippet-types.test.ts b/scripts/__tests__/check-doc-snippet-types.test.ts index 4389443d98..571f20a73a 100644 --- a/scripts/__tests__/check-doc-snippet-types.test.ts +++ b/scripts/__tests__/check-doc-snippet-types.test.ts @@ -8,9 +8,12 @@ import { fileURLToPath } from 'node:url'; // `tsconfig.scripts.json` (`allowJs`), so no `@ts-expect-error` here. import { FRAGMENT_MARKER_EXAMPLES, + UNDECLARED_CONTROL_PACKAGE, UNGATED_DOCS, analyze, + deriveDeclaredDependencyPaths, derivePackageTypePaths, + findInstalledCopy, listDocuments, scanFences, } from '../check-doc-snippet-types.mjs'; @@ -35,6 +38,13 @@ import { * `tsconfig.json` maps the workspace to source; a harness that inherited it * would check the docs against code no consumer sees. * 5. **The gate is wired**, in a workflow a docs-only pull request can start. + * 6. **Third-party resolution reaches exactly as far as the imported packages + * DECLARE** (objectui#6120). This one's failure mode is the worst in the list + * because it is invisible: widen resolution past the declarations and every + * document stays green while the gate stops being able to fail. The suite + * therefore pins both directions — a declared dependency IS mapped, and an + * installed-but-undeclared one is NOT — plus the two preconditions the + * UNDECLARED control needs in order to mean anything. * * Fixtures are throwaway trees, never `content/docs`: a committed fixture page * would have to contain a deliberately broken snippet, and this very gate scans @@ -212,6 +222,147 @@ describe('this repository', () => { }); }); +describe('third-party resolution reaches exactly as far as the imported packages declare', () => { + /** A workspace package with its own `node_modules`, the way pnpm links one. */ + function treeWithDependency(files: Record = {}): string { + return tempTree({ + 'content/docs/a.mdx': [FENCE + 'ts', "import 'declared-dep';", FENCE].join('\n'), + 'packages/pkg-a/package.json': JSON.stringify({ + name: 'pkg-a', + dependencies: { 'declared-dep': '^1.0.0' }, + peerDependencies: { 'peer-dep': '^1.0.0' }, + devDependencies: { 'dev-dep': '^1.0.0' }, + }), + 'packages/pkg-a/node_modules/declared-dep/package.json': JSON.stringify({ + name: 'declared-dep', + types: 'index.d.ts', + }), + 'packages/pkg-a/node_modules/declared-dep/index.d.ts': 'export declare const declared: number;\n', + // Installed right beside it and NOT declared: the shape a blanket mapping + // over node_modules would pick up, and the one no consumer can import. + 'packages/pkg-a/node_modules/undeclared-dep/package.json': JSON.stringify({ + name: 'undeclared-dep', + types: 'index.d.ts', + }), + 'packages/pkg-a/node_modules/undeclared-dep/index.d.ts': 'export declare const undeclared: number;\n', + 'packages/pkg-a/node_modules/peer-dep/package.json': JSON.stringify({ name: 'peer-dep', types: 'index.d.ts' }), + 'packages/pkg-a/node_modules/peer-dep/index.d.ts': 'export declare const peer: number;\n', + 'packages/pkg-a/node_modules/dev-dep/package.json': JSON.stringify({ name: 'dev-dep', types: 'index.d.ts' }), + 'packages/pkg-a/node_modules/dev-dep/index.d.ts': 'export declare const dev: number;\n', + ...files, + }); + } + + const derive = (root: string, imported: string[] = ['pkg-a']) => + deriveDeclaredDependencyPaths(root, imported, { 'pkg-a': 'packages/pkg-a' }) as unknown as { + paths: Record; + declaredBy: Record; + untyped: { specifier: string }[]; + }; + + it('maps a specifier the imported package DECLARES — that is what a consumer resolves', () => { + const { paths, declaredBy } = derive(treeWithDependency()); + expect(Object.keys(paths)).toContain('declared-dep'); + expect(paths['declared-dep'][0]).toMatch(/declared-dep[\\/]index\.d\.ts$/); + expect(declaredBy['declared-dep']).toBe('pkg-a'); + }); + + it('does NOT map a package that is merely INSTALLED — the control that keeps this a check', () => { + // If this ever passes, resolution has been widened to a blanket mapping and + // a snippet may import what no reader of these packages can get. + const { paths } = derive(treeWithDependency()); + expect(Object.keys(paths)).not.toContain('undeclared-dep'); + }); + + it('does not map peerDependencies or devDependencies — it fails CLOSED', () => { + const { paths } = derive(treeWithDependency()); + expect(Object.keys(paths)).not.toContain('peer-dep'); + expect(Object.keys(paths)).not.toContain('dev-dep'); + }); + + it('maps nothing for a package no covered document imports', () => { + const { paths } = derive(treeWithDependency(), []); + expect(paths).toEqual({}); + }); + + it('leaves a specifier that ships no types unresolvable rather than approximating it', () => { + // A JS-only dependency: declared, installed, and carrying nothing a strict + // program can judge. Mapping it to something approximate would report green + // over a snippet nobody type-checked; leaving it unresolvable fails honestly. + const root = tempTree({ + 'packages/pkg-a/package.json': JSON.stringify({ + name: 'pkg-a', + dependencies: { 'untyped-dep': '^1.0.0' }, + }), + 'packages/pkg-a/node_modules/untyped-dep/package.json': JSON.stringify({ + name: 'untyped-dep', + main: 'index.js', + }), + 'packages/pkg-a/node_modules/untyped-dep/index.js': 'module.exports = {};\n', + }); + const { paths, untyped } = derive(root); + expect(Object.keys(paths)).not.toContain('untyped-dep'); + expect(untyped.map((u) => u.specifier)).toContain('untyped-dep'); + }); + + it('never maps a workspace package — those come from their own exports, or deliberately not at all', () => { + const root = tempTree({ + 'packages/pkg-a/package.json': JSON.stringify({ name: 'pkg-a', dependencies: { 'pkg-b': 'workspace:*' } }), + 'packages/pkg-a/node_modules/pkg-b/package.json': JSON.stringify({ name: 'pkg-b', types: 'src/index.ts' }), + 'packages/pkg-a/node_modules/pkg-b/src/index.ts': 'export const b = 1;\n', + }); + const { paths } = deriveDeclaredDependencyPaths(root, ['pkg-a'], { + 'pkg-a': 'packages/pkg-a', + 'pkg-b': 'packages/pkg-b', + }) as unknown as { paths: Record }; + expect(Object.keys(paths)).not.toContain('pkg-b'); + }); + + describe('in this repository', () => { + it("maps lucide-react, which the documented packages declare (objectui#6120)", () => { + const state = analyze({}) as unknown as { + dependencyPaths: Record; + dependencyDeclaredBy: Record; + }; + expect(Object.keys(state.dependencyPaths)).toContain('lucide-react'); + expect(state.dependencyPaths['lucide-react'][0]).toMatch(/\.d\.ts$/); + }); + + it('maps only declaration files, and never a package src/', () => { + const state = analyze({}) as unknown as { dependencyPaths: Record }; + const targets = Object.values(state.dependencyPaths).map((v) => v[0]); + expect(targets.length).toBeGreaterThan(10); + for (const target of targets) { + expect(target).toMatch(/\.d\.(ts|mts|cts)$/); + expect(target, 'a snippet must never be judged against a package src/').not.toMatch( + /[\\/]packages[\\/][^\\/]+[\\/]src[\\/]/, + ); + } + }); + + it('the UNDECLARED control specifier is installed here — otherwise it proves nothing', () => { + expect( + findInstalledCopy(repoRoot, UNDECLARED_CONTROL_PACKAGE), + `${UNDECLARED_CONTROL_PACKAGE} is not installed, so "it does not resolve" measures nothing`, + ).toBeTruthy(); + }); + + it('the UNDECLARED control specifier is declared by no workspace package at all', () => { + const packagesDir = path.join(repoRoot, 'packages'); + const declaring = fs + .readdirSync(packagesDir) + .filter((d) => fs.existsSync(path.join(packagesDir, d, 'package.json'))) + .filter((d) => { + const manifest = JSON.parse( + fs.readFileSync(path.join(packagesDir, d, 'package.json'), 'utf8'), + ) as { dependencies?: Record }; + return Boolean(manifest.dependencies?.[UNDECLARED_CONTROL_PACKAGE]); + }); + expect(declaring, 'pick a control specifier no package declares').toEqual([]); + }); + }); +}); + describe('wiring — a script nothing runs is not a gate', () => { const workflowDir = path.join(repoRoot, '.github/workflows'); const workflowPath = path.join(workflowDir, 'doc-snippet-types.yml'); diff --git a/scripts/check-doc-snippet-types.mjs b/scripts/check-doc-snippet-types.mjs index a5312c69a0..27a955bc99 100644 --- a/scripts/check-doc-snippet-types.mjs +++ b/scripts/check-doc-snippet-types.mjs @@ -155,6 +155,68 @@ * types, unbuilt tree) turns every document red at once and reads * as "the docs are full of defects". * + * UNDECLARED a synthetic module importing `@floating-ui/react-dom` MUST produce + * TS2307. That package IS installed in this workspace — Radix's + * popper pulls it in, under `@object-ui/components`'s declared + * `@radix-ui/react-popover` — and NO package a covered document + * imports declares it, so no reader of the documented packages can + * import it either. It is the control on the third-party rule + * below: the moment resolution widens past what the imported + * packages declare, this control goes green and the gate has become + * a rubber stamp no snippet can fail — invisibly, because every + * document stays green while it happens. It also fails loudly if + * the specifier ever BECOMES a declared dependency (pick another), + * or is not installed at all (a specifier that resolves nowhere + * proves nothing about how far resolution reaches). + * + * ## Third-party specifiers resolve exactly as far as the imported packages declare + * + * A snippet that imports `@object-ui/layout` may also import `lucide-react`, + * because `@object-ui/layout` DECLARES `lucide-react`: a reader who installs that + * package gets it in their `node_modules`, and `SidebarNav`'s `NavItem.icon` + * genuinely takes a lucide icon. This program compiles every block at the + * repository ROOT, where under pnpm a workspace package's own dependency is not + * hoisted and so does not resolve — five correct blocks across + * `content/docs/layout` failed TS2307 on nothing but that (objectui#6120). The + * snippets were right; the resolution environment was the gap. + * + * The rule, stated here because its EDGES are the whole of its value: + * + * For every workspace package a COVERED document imports, each specifier that + * package declares in its own `dependencies` is mapped to the types a + * consumer of that package would resolve — resolved from inside that + * package's own directory, exactly the way that package's own code resolves + * it. + * + * Four edges, each deliberate: + * + * - **Declared, never merely installed.** The set comes from `dependencies` in + * the imported packages' manifests, never from a walk of `node_modules`. A + * blanket mapping would let a snippet import a transitive package no consumer + * can reach and still pass green, which is strictly worse than the gap it + * would close: the gate's whole value is that it fails where a reader fails. + * - **`dependencies` only** — not `peerDependencies`, not `devDependencies`. A + * dependency is what the package installs FOR its consumer; a peer is a + * requirement ON the consumer that may be unmet; a devDependency reaches no + * consumer at all. A snippet importing a peer therefore still fails here. + * That is the conservative direction on purpose: this rule fails CLOSED, and + * widening it later is a visible edit with a reason, not a silent drift. + * - **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, for the same reason. + * - **The bare specifier only, no subpath wildcard.** `lucide-react` is mapped; + * `lucide-react/dynamic` is not, and fails closed. Mapping `/*` would + * reach past the package's own `exports`, and `exports` is precisely the + * boundary a reader hits. + * + * ⛔ What this rule exists INSTEAD of: declaring `lucide-react` at the repository + * root. That would put an entry in this repository's dependency graph that exists + * only to make a checker pass — changing what the repo claims to need in order to + * satisfy a tool. The 2026-08-24 ruling on objectui#6120 rejected that route by + * name, alongside objectui#5329 (minting a `$schema` URL because prose named one) + * and objectui#6107 (minting exports because docs imported them). A manifest is a + * claim about what a package needs; a doc gate's convenience is not that claim. + * * ## Coverage is declared, never assumed — and the scan surface is stated here * * A document is covered unless it is named in `UNGATED_DOCS` with a reason. The @@ -597,6 +659,94 @@ export function derivePackageTypePaths(root = repoRoot) { return { paths, packageDirOf, sourceTyped }; } +/** A declaration file, in any of the three spellings a package may ship. */ +const DECLARATION_FILE = /\.d\.(ts|mts|cts)$/; +/** Any path inside a workspace package's `src/` — never a surface a reader gets. */ +const WORKSPACE_SRC = /[\\/]packages[\\/][^\\/]+[\\/]src[\\/]/; +/** Never written to disk: only a location to resolve FROM, inside a package. */ +const DEPENDENCY_PROBE_FILE = '__doc-snippet-dependency-probe__.ts'; + +/** + * `paths` for the THIRD-PARTY specifiers a covered snippet may legitimately + * import: for each workspace package a covered document imports, every specifier + * that package DECLARES in its own `dependencies`, resolved from inside that + * package's directory — which is exactly what the package's own code resolves, + * and exactly what a consumer who installs it gets. + * + * The rule and its four edges are stated in this file's header; the two things + * enforced right here are that the set is read from MANIFESTS (never from a walk + * of `node_modules`) and that a mapping may only ever land on a declaration file + * outside any package's `src/`. A specifier that ships no types is left + * unresolvable and reported as such, never mapped to something approximate: the + * snippet importing it then fails, which is the honest answer. + */ +export function deriveDeclaredDependencyPaths(root = repoRoot, importedPackages = [], packageDirOf = {}) { + const paths = {}; + const declaredBy = {}; + const untyped = []; + const seen = new Set(); + const options = { + module: COMPILER_OPTIONS.module, + moduleResolution: COMPILER_OPTIONS.moduleResolution, + }; + const host = ts.createCompilerHost(options, false); + // Sorted, so which package wins a specifier two of them declare is decided by + // name rather than by walk order — a run must not depend on readdir. + for (const owner of [...importedPackages].sort()) { + const dir = packageDirOf[owner]; + if (!dir) continue; + const manifestPath = join(root, dir, 'package.json'); + if (!existsSync(manifestPath)) continue; + const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')); + for (const specifier of Object.keys(manifest.dependencies || {}).sort()) { + // A workspace package is mapped from its OWN `exports` by + // `derivePackageTypePaths`, and one deliberately left unmapped there + // (source-typed) must STAY unmapped — routing it through a node_modules + // symlink would judge a snippet against a package's `src/`, the exact + // substitution this gate exists to make impossible. + if (specifier in packageDirOf) continue; + if (seen.has(specifier)) continue; + seen.add(specifier); + const resolved = ts.resolveModuleName( + specifier, + join(root, dir, DEPENDENCY_PROBE_FILE), + options, + host, + ); + const file = resolved.resolvedModule ? resolved.resolvedModule.resolvedFileName : null; + if (!file || !DECLARATION_FILE.test(file) || WORKSPACE_SRC.test(file)) { + untyped.push({ specifier, owner, resolved: file }); + continue; + } + paths[specifier] = [file]; + declaredBy[specifier] = owner; + } + } + return { paths, declaredBy, untyped }; +} + +/** + * Where a package is physically installed in this workspace, found WITHOUT + * assuming it resolves from anywhere in particular: pnpm's virtual store holds + * one directory per (package, version, peer-set), named with `/` replaced by `+`. + * Used only by the UNDECLARED control, which must never confuse "this specifier + * does not resolve" with "this package is not installed" — the second proves + * nothing about how far resolution reaches. + */ +export function findInstalledCopy(root = repoRoot, specifier = '') { + const storeDir = join(root, 'node_modules', '.pnpm'); + if (!existsSync(storeDir)) return null; + const prefix = `${specifier.replace(/\//g, '+')}@`; + for (const entry of readdirSync(storeDir).sort()) { + if (!entry.startsWith(prefix)) continue; + const candidate = join(storeDir, entry, 'node_modules', ...specifier.split('/')); + if (existsSync(join(candidate, 'package.json'))) { + return relative(root, candidate).split(sep).join('/'); + } + } + return null; +} + /** Workspace package specifiers a document imports (bare specifier root only). */ function importedSpecifiers(body) { const out = new Set(); @@ -618,6 +768,18 @@ const SENTINEL_EXPORT = 'ThisNameIsDefinitelyNotExported'; const CONTROL_PACKAGE = '@object-ui/types'; const CONTROL_REAL_EXPORT = 'ComponentSchema'; +/** + * The UNDECLARED control's specifier (see the header). Three properties make it + * the right one, and all three are ASSERTED at run time rather than trusted: + * it is installed in this workspace (a transitive of `@radix-ui/react-popover`, + * which `@object-ui/components` declares), it is declared by no package in this + * repository at all, and it ships real `.d.ts` files — so if resolution ever did + * widen to reach it, the control module would compile CLEANLY rather than fail + * for some unrelated reason. It is the difference between "the rule is narrow" + * and "we hope the rule is narrow". + */ +const UNDECLARED_CONTROL_PACKAGE = '@floating-ui/react-dom'; + const COMPILER_OPTIONS = { target: ts.ScriptTarget.ES2020, module: ts.ModuleKind.ESNext, @@ -723,7 +885,30 @@ export function analyze({ root = repoRoot, ungated = UNGATED_DOCS } = {}) { } } - return { documents, covered, compiled, declaredFragments, findings, paths, neededPackages, scans }; + // ── and what THOSE packages declare resolves too, exactly that far ──────── + const { + paths: dependencyPaths, + declaredBy: dependencyDeclaredBy, + untyped: untypedDependencies, + } = deriveDeclaredDependencyPaths(root, neededPackages, packageDirOf); + // Workspace entries win every collision: a workspace package is mapped from + // its own `exports`, and one deliberately left unmapped stays unmapped. + const mergedPaths = { ...dependencyPaths, ...paths }; + + return { + documents, + covered, + compiled, + declaredFragments, + findings, + paths: mergedPaths, + workspacePaths: paths, + dependencyPaths, + dependencyDeclaredBy, + untypedDependencies, + neededPackages, + scans, + }; } /** Phase 1 (syntax) and phase 2 (semantics), kept apart on purpose. */ @@ -763,6 +948,14 @@ export function compileSnippets({ root = repoRoot, compiled, paths }) { positiveFile, `import type { ${CONTROL_REAL_EXPORT} } from '${CONTROL_PACKAGE}';\nexport type Control = ${CONTROL_REAL_EXPORT};\n`, ); + // A namespace import, so that ANY successful resolution reports zero + // diagnostics: the control must distinguish "did not resolve" from "resolved", + // never "resolved but the name I picked happened to be missing". + const undeclaredFile = join(root, VIRTUAL_DIR, '__control_undeclared.ts'); + virtual.set( + undeclaredFile, + `import * as undeclared from '${UNDECLARED_CONTROL_PACKAGE}';\nexport type Undeclared = typeof undeclared;\n`, + ); const options = { ...COMPILER_OPTIONS, baseUrl: root, paths, types: [] }; const host = ts.createCompilerHost(options, true); @@ -795,6 +988,7 @@ export function compileSnippets({ root = repoRoot, compiled, paths }) { const sentinelDiagnostics = [...program.getSemanticDiagnostics(program.getSourceFile(sentinelFile))]; const positiveDiagnostics = [...program.getSemanticDiagnostics(program.getSourceFile(positiveFile))]; + const undeclaredDiagnostics = [...program.getSemanticDiagnostics(program.getSourceFile(undeclaredFile))]; return { parseFailures, @@ -804,6 +998,9 @@ export function compileSnippets({ root = repoRoot, compiled, paths }) { srcLeaks, sentinelDiagnostics, positiveDiagnostics, + undeclaredDiagnostics, + undeclaredMapped: UNDECLARED_CONTROL_PACKAGE in paths, + undeclaredInstalledAt: findInstalledCopy(root, UNDECLARED_CONTROL_PACKAGE), }; } @@ -849,6 +1046,11 @@ function main() { // ── controls, before any verdict about the documents ────────────────────── const controlFailures = []; + // Printed BEFORE the controls: it says how far this run's resolution reaches, + // which is the thing the UNDECLARED control then bounds. + console.log( + `Third-party resolution: ${Object.keys(state.dependencyPaths).length} specifier(s) mapped from the declared dependencies of ${state.neededPackages.size} imported package(s); ${state.untypedDependencies.length} declared specifier(s) ship no types here and stay unresolvable.`, + ); console.log('Controls:'); console.log( ` resolution Module name '${CONTROL_PACKAGE}' was successfully resolved to '${run.resolvedFileName ?? '(unresolved)'}'`, @@ -876,6 +1078,23 @@ function main() { `the positive control failed (${ts.flattenDiagnosticMessageText(run.positiveDiagnostics[0].messageText, ' ')}) — the harness is broken, not the documents`, ); } + const undeclaredCodes = run.undeclaredDiagnostics.map((d) => d.code); + console.log( + ` undeclared importing '${UNDECLARED_CONTROL_PACKAGE}' (installed at ${run.undeclaredInstalledAt ?? '(NOT INSTALLED)'}, declared by no imported package) produced ${run.undeclaredDiagnostics.length} diagnostic(s)${undeclaredCodes.length ? ` (TS${undeclaredCodes.join(', TS')})` : ''}`, + ); + if (run.undeclaredMapped) { + controlFailures.push( + `'${UNDECLARED_CONTROL_PACKAGE}' is now a DECLARED dependency of a package a covered document imports, so it can no longer show that resolution stayed narrow — pick a control specifier no imported package declares`, + ); + } else if (!run.undeclaredInstalledAt) { + controlFailures.push( + `'${UNDECLARED_CONTROL_PACKAGE}' is not installed in this workspace, so its failure to resolve proves nothing about how far resolution reaches — pick an installed specifier no imported package declares`, + ); + } else if (!undeclaredCodes.includes(2307)) { + controlFailures.push( + `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`, + ); + } console.log(''); const total = state.compiled.length + state.declaredFragments.length; @@ -935,4 +1154,4 @@ if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import process.exit(main()); } -export { UNGATED_DOCS, TS_FENCE_LANGUAGES, FRAGMENT_MARKER, main }; +export { UNGATED_DOCS, TS_FENCE_LANGUAGES, FRAGMENT_MARKER, UNDECLARED_CONTROL_PACKAGE, main }; From 5aac1c46ead162d407909eddaa9787171ea95e5f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 19:27:13 +0000 Subject: [PATCH 2/2] fix(tooling): tell the undeclared control's two failure modes apart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe --- scripts/check-doc-snippet-types.mjs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/scripts/check-doc-snippet-types.mjs b/scripts/check-doc-snippet-types.mjs index 27a955bc99..e574f02d1f 100644 --- a/scripts/check-doc-snippet-types.mjs +++ b/scripts/check-doc-snippet-types.mjs @@ -722,7 +722,12 @@ export function deriveDeclaredDependencyPaths(root = repoRoot, importedPackages declaredBy[specifier] = owner; } } - return { paths, declaredBy, untyped }; + // `seen` is exactly the set of non-workspace specifiers the imported packages + // DECLARE, whether or not each one could be mapped. The UNDECLARED control + // reads it to tell its two failure modes apart: a control specifier that has + // become a declared dependency (pick another) is a different fact from one + // that resolves without any manifest declaring it (resolution has widened). + return { paths, declaredBy, untyped, declared: [...seen].sort() }; } /** @@ -890,6 +895,7 @@ export function analyze({ root = repoRoot, ungated = UNGATED_DOCS } = {}) { paths: dependencyPaths, declaredBy: dependencyDeclaredBy, untyped: untypedDependencies, + declared: declaredSpecifiers, } = deriveDeclaredDependencyPaths(root, neededPackages, packageDirOf); // Workspace entries win every collision: a workspace package is mapped from // its own `exports`, and one deliberately left unmapped stays unmapped. @@ -906,13 +912,14 @@ export function analyze({ root = repoRoot, ungated = UNGATED_DOCS } = {}) { dependencyPaths, dependencyDeclaredBy, untypedDependencies, + declaredSpecifiers, neededPackages, scans, }; } /** Phase 1 (syntax) and phase 2 (semantics), kept apart on purpose. */ -export function compileSnippets({ root = repoRoot, compiled, paths }) { +export function compileSnippets({ root = repoRoot, compiled, paths, declaredSpecifiers = [] }) { const parseFailures = []; const virtual = new Map(); const owners = new Map(); @@ -999,7 +1006,7 @@ export function compileSnippets({ root = repoRoot, compiled, paths }) { sentinelDiagnostics, positiveDiagnostics, undeclaredDiagnostics, - undeclaredMapped: UNDECLARED_CONTROL_PACKAGE in paths, + undeclaredDeclared: declaredSpecifiers.includes(UNDECLARED_CONTROL_PACKAGE), undeclaredInstalledAt: findInstalledCopy(root, UNDECLARED_CONTROL_PACKAGE), }; } @@ -1042,7 +1049,12 @@ function main() { return 1; } - const run = compileSnippets({ root: repoRoot, compiled: state.compiled, paths: state.paths }); + const run = compileSnippets({ + root: repoRoot, + compiled: state.compiled, + paths: state.paths, + declaredSpecifiers: state.declaredSpecifiers, + }); // ── controls, before any verdict about the documents ────────────────────── const controlFailures = []; @@ -1082,7 +1094,7 @@ function main() { console.log( ` undeclared importing '${UNDECLARED_CONTROL_PACKAGE}' (installed at ${run.undeclaredInstalledAt ?? '(NOT INSTALLED)'}, declared by no imported package) produced ${run.undeclaredDiagnostics.length} diagnostic(s)${undeclaredCodes.length ? ` (TS${undeclaredCodes.join(', TS')})` : ''}`, ); - if (run.undeclaredMapped) { + if (run.undeclaredDeclared) { controlFailures.push( `'${UNDECLARED_CONTROL_PACKAGE}' is now a DECLARED dependency of a package a covered document imports, so it can no longer show that resolution stayed narrow — pick a control specifier no imported package declares`, );