Warn on unknown config keys and harden diagnostic channels - #181
Warn on unknown config keys and harden diagnostic channels#181KayleeWilliams wants to merge 5 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1f29a59c30
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Important
The --docs-dir parity work reads the flag differently from generate for the documented <dir>=<url-prefix> form, which turns a working invocation into a doctor exit 1. Details inline.
Reviewed changes — full diff of the single commit 1f29a59 across 12 files, plus the generate implementations this PR claims parity with and the DocsConfig / DocsCollection / DocsNavNode types the new allow-lists mirror.
- Unknown config keys now warn —
validateDocsConfigcollectsConfigWarnings for keys outside hand-maintained allow-lists at the top level, collection level,gitSourcelevel and inside navigation entries, with a bounded-Levenshtein did-you-mean; they surface aswarndiagnostics andconfig.unknown-keydoctor findings, never as errors. - Load-time warnings route through a caller-supplied sink —
ConfigWarningSinkis threaded fromresolveProjectdown towarnConfigDeprecations/ the newwarnConfigUnknownKeys, defaulting to the process logger; doctor and nav pass their injectedio.stderr. - Repeated
--docs-diris honored —resolveProjectsynthesizes an extra collection per additional directory at/docs/<basename>in single-source mode, reusinggenerate's collision message. configPathis no longer fabricated — an in-memory config gets no path, and the newconfigOrigin: "file" | "caller"records how the config arrived.contentDirinvariant is named —requireResolvedContentDirreplacescollection.contentDir as stringincreateDocsProject.- Docs corrected —
source.inherit-failedis documented as an error,config.unknown-keyis added to the finding table, andeditDistanceWithinmoves tointernal/edit-distance.tsshared with search.
I checked the new tests against the bugs they claim to pin: the assertions are exact (toEqual on sorted owner paths, literal did you mean "…" substrings) rather than loose, the process.stderr spy works because the logger's module-global stream is that same object, and every fixture uses a fresh mkdtemp so the path-keyed dedupe sets cannot bleed across tests. I also verified the three allow-lists carrying satisfies are complete against their types today.
ℹ️ --docs-dir alongside a collections config is still accepted where generate rejects it
generate refuses the flag outright when the root config declares collections (packages/leadtype/src/cli/generate.ts:1684-1689), and the docs state the rule twice. The new expansion is gated on mode === "single-source", so in multi-source mode doctor and nav drop the extra directories with no error and no diagnostic — a project that generate rejects gets a clean ok: true report. This is pre-existing at the entry points rather than introduced here, but it is the other half of the "same flag, same reading, same error" claim.
Technical details
# `--docs-dir` is not rejected in multi-source mode
## Affected sites
- `packages/leadtype/src/config/project.ts:542-546` — the expansion is skipped in multi-source mode, and nothing else reports that the flag was ignored.
- `packages/leadtype/src/cli/doctor.ts:376-392` and `packages/leadtype/src/cli/nav.ts:250-267` — pass `docsDirs` through unconditionally, with no equivalent of generate's guard.
- `packages/leadtype/src/cli/generate.ts:1684-1689` — the authoritative rule and its message.
- `docs/pipeline/configure-sources.mdx:193` and `docs/reference/cli.mdx:116` — document the rule.
## Required outcome
- Passing `--docs-dir` to `doctor` or `nav` on a project whose config declares `collections` should end the same way it ends under `generate`, rather than being silently discarded.
## Open questions for the human
- Should this be generate's hard error (consistent, but makes `doctor` refuse to diagnose the very project the user is confused about), or a `warn`-level `config.unknown-key`-style finding naming `--docs-dir` as the owner? A read-only diagnostic tool arguably wants the latter.ℹ️ Nitpicks
NAV_NODE_KEYSandNAV_INCLUDE_KEYS(packages/leadtype/src/config/load.ts:139-155) are the only two allow-lists without theas const satisfies readonly (keyof T)[]guard their three siblings carry; adding it againstDocsNavNode/DocsNavIncludeEntrywould make a future rename of a nav field a compile error instead of a false "unknown field" warning. Note that nosatisfiescatches the other drift direction — a new field added to any of these types without touching the list makes every config using it warn — so aExclude<keyof T, (typeof LIST)[number]> extends neverassertion would be the guard that actually covers the false-positive case.writeLoadWarninginpackages/leadtype/src/cli/doctor.ts:246-251and the inline callback inpackages/leadtype/src/cli/nav.ts:261-266are the same six lines; one shared helper next toConfigWarningSinkwould keep the two commands' warning format from drifting.- Both injected sinks always emit human-readable text, so under
LEADTYPE_LOG_FORMAT=jsona doctor/nav run mixes a human warning line into otherwise-JSON stderr, where the defaultlogger.warnpath would have honored the format.
Claude Opus | 𝕏
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6502c7883e
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
ℹ️ No critical issues — minor suggestions inline.
6502c788 addresses all three inline findings from the previous review, and I verified each fix independently rather than taking the tests at their word:
- Nearest-key suggestion —
closestKnownKeynow widens the distance bound in stages (load.ts:169-182) instead of taking the first candidate within the maximum bound. Tracedprefx:refis eliminated by the length filter,prefixwins at bound 1. Correct. - Per-sink dedupe — the process-global
Set<string>is now aWeakMap<ConfigWarningSink, Set<string>>(load.ts:1293-1317). The default sink keys offlogger.warn, which is a stable arrow property on aconstobject literal (internal/logger.ts), so the long-lived watch-loop path still dedupes as before. The new test discriminates — under the old global set the third resolve would leavesecondempty, not at length 1. <dir>=<url-prefix>parsing —normalizeDocsSourceInput/parseDocsSourceInputare extracted tointernal/docs-source.tsbyte-identically to the block removed fromgenerate.ts, and bothdoctorandnavnow route through them. A documented invocation like--docs-dir guides=/guidesno longer exits 1.
All three prior threads were already marked resolved by the author; nothing left to retire there.
ℹ️ resolveProject's public contract now has a throwing input grammar
resolveProject is publicly exported (src/index.ts:24-28) and its module JSDoc states it is "non-throwing for environmental problems," reserving throws for genuinely malformed config. The eager parseDocsSourceInput call at project.ts:338-344 extends that throwing surface to a new place: the shape of a caller-supplied option value. That is a defensible reading of "malformed," and both CLI callers catch it — but ResolveProjectOptions.docsDirs' own JSDoc (project.ts:139-143, outside this diff) still describes the field as plain directory paths:
Docs directories relative to
cwd, for a single-source project whose content root the config does not state.
Worth a sentence there noting the <dir>[=<url-prefix>] grammar and that a malformed value throws, so external callers of the exported function know to guard it.
Nitpick
docs/reference/doctor.mdx:156 — this PR rewrites the --docs-dir row to explain the repeatable multi-mount behavior, which is the right call, but it still omits the =<url-prefix> form that doctor now accepts. docs/reference/cli.mdx:50 has the same gap (outside this diff).
Claude Opus | 𝕏
5bc94d2 to
1703cf0
Compare
6502c78 to
1955309
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1955309481
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bfd266cab4
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
ℹ️ No critical issues — one suggestion inline.
Reviewed changes since 6502c788:
- First
--docs-dirvalue's=<url-prefix>is now honored. ThedocsDirs.length > 1gate is gone, and the primary collection'sroutePrefixis overridden fromdocsDirInputs[0].urlPrefix(project.ts:808-826). Checked that this clobbers nothing authored:normalize.ts:417hardcodes/docsfor single-source, so the value being replaced is always the default.leadtype doctor --docs-dir docs=/customnow agrees with whatgeneratepublishes. This retires the first thread from the previous review. resolveConfiglessProject(project.ts:391-472) — new behavior, not a refactor. When discovery finds no config but a content root exists,resolveProjectbuilds the collectionsgeneratewould stage instead of returningemptyProject. I traced the blast radius rather than the flag:docsDirsdefaults to["docs"], so this fires for any configless project with adocs/dir. EveryResolvedProjectconsumer already optional-chainsconfig/resolved—createDocsProjectstill throws atproject/index.ts:284,doctorreportsconfig.mode: "none"next to populated collections,resolveCollectionNavigationis null-safe. And I diffed the derivation againstgenerate'sresolveDocsSources(generate.ts:841-870) case by case: index-0 vs later mount paths, explicit prefixes at every index, the collision message, the malformed-value message, and missing-dir handling all agree.synthesizeExtraDirCollections(project.ts:294-381) extracted from the inline block, now shared by the config and configless paths. Same collision message andsource.dir-missingerror as before.- Three new regression tests, all with exact
toEquals that fail with the bug present — thedoctorprimary-prefix test pins[["docs", "/manual"]], the configless test pins[["docs","/docs",1],["changelog","/docs/changelog",1]]. Fixtures usemkdtemp, so the path-keyed dedupe Sets can't collide across tests.
npx vitest run src/config/project.test.ts src/cli/nav.test.ts → 53 passed; tsgo --noEmit clean.
One carry-over: the origin: "explicit" provenance thread from the last review is still open, and the rework has spread it from one site to three — project.ts:821 (primary, config path), :369 (extras), :438 (configless primary). A CLI-supplied prefix reported as explicit still sends readers hunting through a config file for a value that isn't there, while the sibling dir provenance on the same collections correctly attributes the flag via inferredFrom.
ℹ️ Nitpick
nav.ts:274's collections.length === 0 → reportInferredTree fallback is now reachable only when the docs dir doesn't exist — the case where it prints the leadtype init hint (pinned by nav.test.ts:167). Not a break, but the branch no longer means what its surroundings suggest, and a reader will assume it still covers "configless project."
Claude Opus | 𝕏
Six small diagnostics fixes from review of the resolver stack, each
re-verified before fixing:
Unknown config keys no longer vanish silently. Validation warns — never
errors, for forward compatibility — on unrecognized keys at the top
level, collection level, gitSource level, and inside navigation entry
objects, with a did-you-mean when the key is close to a known one
(shared bounded-Levenshtein helper extracted from search into
internal/edit-distance). Warnings ride the loaded config into
resolveProject diagnostics, so doctor reports them as config.unknown-key
findings with the owning field path. Deliberately open surfaces —
frontmatterSchema contents, mounts entries, llms sections, and the
separately validated organization/agents/openapi/lint objects — are
exempt.
The load-time deprecation warning (and the new unknown-key warning) now
route through a caller-provided sink. doctor and nav pass their injected
io, so warnings are capturable in tests and can never interleave with
--json stdout; entry points without injected io keep the process logger.
doctor's repeatable --docs-dir now honors every value the way generate's
legacy multi-dir path does: the first directory mounts at the docs root,
each further one under its folder name as a synthesized collection, and
colliding mount basenames fail with generate's exact message. Previously
only docsDirs[0] was read, so the report missed every page the build
actually stages.
resolveProject no longer fabricates `<root>/leadtype.config.ts` as the
path of a caller-supplied in-memory config. configPath is absent when
there is no file, and a new configOrigin ("file" | "caller") records how
the config arrived; doctor's report omits config.path rather than naming
a file that does not exist.
createDocsProject replaces the `collection.contentDir as string` cast
with an explicit invariant check (requireResolvedContentDir), so a
future warn-level directory diagnostic fails as a named
internal-invariant error instead of handing createDocsSource an
undefined path.
docs/reference/doctor.mdx now lists source.inherit-failed as error,
matching the code — createDocsProject and generate refuse to run on it,
so doctor exiting 0 would let CI pass a project the build then fails on
— and documents config.unknown-key. Lockfile entry regenerated for the
edited page only.
…n once per sink - Lift parseDocsSourceInput/normalizeDocsSourceInput out of cli/generate into internal/docs-source and read the flag through it in resolveProject, so the documented <dir>=<url-prefix> form resolves the real directory and mounts at the explicit prefix instead of tripping source.dir-missing. Malformed values fail with generate's error; nav stops pre-resolving the raw string. - closestKnownKey widens the edit-distance bound one step at a time so the nearest candidate wins, not the first within the fixed bound. - Deprecation/unknown-key warning dedupe is keyed per sink (WeakMap) as well as per config path: a fresh caller-injected sink always hears the warning once, and the process logger keeps its once-per-file behavior.
generate applies an explicit =<url-prefix> to every parsed --docs-dir, including the first, and stages every directory even when no config exists. resolveProject skipped index 0 (reporting the primary at the default /docs) and returned an empty project from the missing-config branch, so doctor and nav described a project the build never ships — and still exited 0. Apply the first value's prefix to the primary collection and synthesize the same fallback collections generate stages when there is no config but content exists.
- Run inherited source configs through the unknown-key collector. inheritConfig only extracts the fields it knows, so a `navigatoin` in the source repo's docs.config was silently inert and the collection quietly fell back to an inferred tree. Warnings name the source config file and collection, land in the project diagnostics, and reach the warn sink. The allowed set is the full authored key set — the default source config is the source repo's own docs.config, so product and friends are legitimate there. - Key the once-per-sink warning dedupe on path plus rendered content, not path alone. generate --watch reloads the config through the same process-wide sink, so replacing one typo with a different one printed nothing until the process restarted. Identical reloads stay quiet; a changed warning set re-emits. Deprecations get the same key. - Resolve an authored single-source tree over the union of every --docs-dir, the way generate's staged mirror does (extras mounted under their folder names): navigation: ["guides/setup"] with --docs-dir docs --docs-dir guides resolved only against docs and reported nav.unresolvable — doctor exit 1 for a build that exits 0 (verified against the real command). The extra dirs fold into the primary's manifest instead of getting per-directory inferred trees generate never derives, and they now follow generate's derivation opt-outs: no inferred trees once a tree is authored or when i18n is set.
c44f0cc to
123c43b
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 123c43ba8b
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| try { | ||
| const imported = await importConfigModule(configPath); | ||
| onSourceConfig?.(imported, configPath, entry.key); | ||
| return validateSourceOwnedConfigFields(imported, configPath, entry.key); |
There was a problem hiding this comment.
Emit inherited-config warnings during generation
When leadtype generate processes a collection with inheritConfig, this optional visitor is never supplied by executeGenerate (cli/generate.ts calls inheritCollectionSourceConfigs with only two arguments). A source-owned config containing navigatoin therefore still generates silently with inferred navigation, even though doctor/nav now warn. The prior fix only wires the visitor through resolveProject; this unchanged direct generate call is fresh evidence that the production generation path remains uncovered.
Useful? React with 👍 / 👎.
| const contentRoots = options.contentDir | ||
| ? [path.resolve(options.contentDir)] | ||
| : docsDirs; |
There was a problem hiding this comment.
Resolve a relative contentDir from the project root
When a configless caller passes both cwd and a relative contentDir, such as resolveProject({ cwd: "/repo", contentDir: "manual" }), path.resolve(options.contentDir) resolves from the process working directory rather than /repo, contrary to the option's documented relative-to-cwd semantics. If /repo/manual is the only content directory, this existence check returns an empty project; if an unrelated manual directory exists under the process cwd, the resolver can inspect the wrong files. Resolve the override against rootDir here and in resolveConfiglessProject.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
ℹ️ No critical issues — one suggestion inline.
Reviewed changes since f7a88a80 (the branch was force-pushed again, so SHAs churned while commit messages stayed stable — bfd266ca→fca3d728, c44f0ccb→123c43ba. This covers both commits since the last review.)
- All three prior Pullfrog threads are genuinely fixed, verified against the tree rather than the replies. The flag-supplied prefix sites now carry
{ origin: "default", inferredFrom: "url prefix (--docs-dir)" }, matching the siblingdirprovenance, so a--docs-dir docs=/customno longer reports as config-authored. The extras'infernow carriesnormalized.config.i18n === undefined && !primaryTreeAuthored(project.ts:907-910) — going further than the thread asked, and correctly so:generateskips derivation for an authorednavigation/groupstoo, not just fori18n. - Source-owned config unknown keys (
inherit.tsSourceConfigVisitor→collectSourceConfigUnknownKeys). I checked the obvious false-positive risk and it doesn't fire:inheritlives on the host config'scollection.inheritConfig.inherit(read bysourceConfigInheritFields,inherit.ts:327-331), never as a source-config top-level key, and all fiveSourceOwnedConfigFieldsare inTOP_LEVEL_CONFIG_KEYS. Validating against the full key set rather than the inheritable subset is the right call — a source repo's owndocs.config.*legitimately carriesproduct. Diagnostics owning as<sourcePath>#<field>plus the newcontextparameter means the message names the source file, not the host. - Content-keyed dedupe (
warnedKey(configPath, ...content)). The watch-mode bug it fixes is real and the rationale is in the comment atload.ts:1357-1362. Empty warning sets are never remembered, so a fixed-then-rebroken config still re-warns for a different typo. - Union navigation view for repeated
--docs-dir(navigationExtraDirs→mountedDocsDirs, plus thesubsumedDirsfilter that stops double-counting and a spurious"mixed"origin). I checked the mount-ordering hazard here — the array puts the catch-all{ pathPrefix: "" }base mount before the extras — and it's safe:resolveDocsPathMountsorts bypathPrefix.lengthdescending (internal/docs-url.ts:95) before matching, and the empty prefix is only reachable via theif (!mount.pathPrefix)fallback at:98. I also confirmedwithPrefixlands exactly once, never twice: bothllm.ts:1755and:1774recompute an unprefixed path fromdocsDirrather than reusing the prefixed one. - Tests assert URLs, not just presence —
project.test.ts:735-741pins/docs/guides/setupinroutedUrlPathsanddoctor.test.ts:444-450pinsunrepresentedPages: ["/docs"], so a shadowing regression in mount resolution would fail them.
npx vitest run src/config/project.test.ts src/cli/doctor.test.ts src/cli/nav.test.ts → 94 passed; tsgo --noEmit clean.
The one inline finding is the remaining seam in the same mountedDocsDirs work: the prefix is applied to a path list whose only consumer expects unprefixed input.
Claude Opus | 𝕏
| const files = filterFile ? collected.filter(filterFile) : collected; | ||
| const relativePaths = files.map((filePath) => | ||
| normalizeDocsPath(path.relative(docsDir, filePath)) | ||
| withPrefix(normalizeDocsPath(path.relative(docsDir, filePath))) |
There was a problem hiding this comment.
Prefixing relativePaths disables the i18n ambiguous-layout guard for every mounted --docs-dir.
relativePaths has exactly one consumer — assertUnambiguousDefaultLocaleLayout at line 1739-1743. (It appears only at 1730 and 1740 in this function; the URL and logical paths are built independently at 1764-1770 and 1774, each recomputing from docsDir.) So the withPrefix here doesn't feed any output path, it only reshapes the assertion's input.
That assertion keys off the first path segment (llm.ts:1611-1617):
const first = normalizeDocsPath(relativePath).split("/")[0] ?? "";
return !localeCodes.has(first); // hasRootDefault
…
normalizeDocsPath(relativePath).startsWith(`${defaultLocale}/`) // hasDefaultFolderWith pathPrefix: "guides", every path now starts with guides/, so first is never a locale code (hasRootDefault unconditionally true) and nothing starts with en/ (hasDefaultFolder unconditionally false). hasRootDefault && hasDefaultFolder can never hold, so the guard cannot fire for a mounted directory regardless of its actual layout.
Dropping the prefix here can't under-prefix anything, since no output path reads this list.
| withPrefix(normalizeDocsPath(path.relative(docsDir, filePath))) | |
| normalizeDocsPath(path.relative(docsDir, filePath)) |
Reachability and symptom
Reachable on the config-bearing path: project.ts:919 attaches navigationExtraDirs under primaryTreeAuthored && collections[0] with no i18n exclusion — the normalized.config.i18n === undefined guard at :909 gates only the inferred synthesis, not the union attachment. navigation.ts:217-225 then puts mountedDocsDirs and i18n on the same resolveConfig, and the localized branch runs per extra locale.
So: a localized project with an authored top-level navigation (or groups) plus a second --docs-dir.
Symptom: a genuinely ambiguous layout inside the mounted dir — guides/setup.mdx alongside guides/en/other.mdx — no longer throws Ambiguous i18n default-locale layout. Use either root docs files or docs/en/ files…. It proceeds, and only surfaces later (if the paths literally collide) as the generic duplicate-route error from selectLocalizedFiles at llm.ts:1657-1661.
Not a wrong-output bug — the direction is a false negative, a guard that stops guarding — which is why this is a suggestion rather than a blocker.

Stacked on #179 → #167. Those PRs made resolution and navigation shared and diagnosable; review of the result found six places where the diagnostics story itself leaks — a warning that bypasses the injected io, a config path that names no file, a typo that does nothing at all. Each was reproduced before fixing.
The evidence
Unknown config keys vanished silently. Only
organization.*rejected unknown keys (they become invalid Schema.org properties); everywhere else a typo'd field was dropped without a word. In an untyped.js/.mjsconfig — or one written by an agent —navigatoin:quietly reverts the tree to inferred androutePrefx:quietly keeps the default, which is the exact opposite of the provenance/"explain why" story. Validation now warns (never errors — an older CLI reading a newer config must still load it) on unrecognized keys at the top level, collection level, gitSource level, and inside navigation entry objects, with a did-you-mean when the key is within edit distance of a known one:unknown field "collections.docs.routePrefx" — did you mean "routePrefix"?. The suggestion reuses search's bounded Levenshtein, extracted tointernal/edit-distance.ts. Deliberately open surfaces are exempt —frontmatterSchemacontents,mountsentries,llmssections, and the separately validatedorganization/agents/openapi/lintobjects. Warnings flow through the normal diagnostic channel, so doctor reports them asconfig.unknown-keyfindings with the owning field path, exit 0.The deprecation warning bypassed the injected io.
warnConfigDeprecationswrote to the process logger, whichrunDoctorCommand's injectedDoctorIonever sees — observed leaking into test-runner output while--jsonstdout stayed clean, and unverifiable from any test. Load-time warnings now route through a caller-provided sink; doctor and nav pass their io, entry points without injected io keep the process logger. A regression test spies on realprocess.stderrand asserts it stays untouched while the injected stderr carries the warning and stdout stays parseable JSON.--docs-dirwas documented repeatable but only[0]was read.generate's legacy path honors every value — first directory at the docs root, each further one mounted under its folder name — so doctor reported a project missing every page the build actually stages. Doctor and nav now expand additional directories into synthesized collections with the same mount shape (/docs/<basename>), and colliding basenames fail withgenerate's exact message. Consistency over cleverness: same flag, same reading, same error.resolveProjectfabricatedleadtype.config.tsas the path of an in-memory config. A caller-supplied config withoutconfigPathwas stamped<root>/leadtype.config.ts— a path that names nothing on disk, which a doctor-style consumer would then report as the discovered config file.configPathis now absent when there is no file, andconfigOrigin: "file" | "caller"records how the config arrived; doctor's report omitsconfig.pathrather than inventing one.collection.contentDir as stringrested on an implicit invariant — that every unresolved contentDir produced an error-level diagnostic the blocking check already threw on. True today, and one future warn-level directory diagnostic away from handingcreateDocsSourcean undefined path to crash on somewhere deeper. The cast is nowrequireResolvedContentDir, which throws a named internal-invariant error naming the collection.The docs said
source.inherit-failedis a warn; the code emits an error. Error is correct:createDocsProjectandgenerateboth refuse to run on it, so a doctor that exits 0 would let CI pass a project the build then fails on — the divergence Resolve the project once instead of in every command #167 fixed, reintroduced in prose. The finding-id table now says error, and documentsconfig.unknown-key.Verification
Each fix carries a regression test: unknown-key warnings present for typos at every covered level (including
sources.<id>.…andnavigation[0].pages[0].pinspaths) and absent for known keys and open surfaces; the deprecation warning captured via injected io with real stderr spied clean; repeated--docs-dirhonored (two collections,/docs+/docs/guides, both counted) and collisions rejected; the in-memory config resolving with noconfigPathandconfigOrigin: "caller"; the contentDir invariant throwing by name.841 tests pass (829 on the base), plus this repo's own
doctorandlint docsCI gates re-run locally — doctor on this repo reports no unknown-key findings, so the new check adds no noise to a clean config.docs/reference/doctor.mdx's lockfile entry regenerated for the edited page only.tsgo --noEmitand lint pass at the package level; the repo-level parallel-build race is #166, offmain.Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.