Skip to content

Introduce a canonical config API with backward-compatible normalization - #159

Open
KayleeWilliams wants to merge 3 commits into
dx/150-skill-activationfrom
dx/151-canonical-config
Open

Introduce a canonical config API with backward-compatible normalization#159
KayleeWilliams wants to merge 3 commits into
dx/150-skill-activationfrom
dx/151-canonical-config

Conversation

@KayleeWilliams

@KayleeWilliams KayleeWilliams commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Closes #151. Stacked on #158, part of #157.

DocsConfig grew from a description of one MDX folder into the project model — identity, source acquisition, navigation, feeds, redirects, agent surfaces, multi-repo orchestration. Growth like that left field names from the job they used to do, and left each subsystem re-deriving the project from raw config.

Canonical vocabulary

defineLeadtypeConfig names a project/site leadtype.config.ts alongside defineDocsConfig for a source repo's content-owned docs.config.ts. The two shapes are identical; the names mark ownership, which is what decides what may be inherited across repos.

Canonical Older name Why
routePrefix prefix Three different prefixes appear in one multi-collection config; only one reaches a URL.
inheritConfig sourceConfig It declares inheritance from the source repo, not a config object.
frontmatterSchema schema The top-level field of the same purpose is already frontmatterSchema.

One normalizer, one resolved project

normalizeDocsConfig folds deprecated aliases onto canonical names and derives ResolvedDocsConfig. Generate, sync, lint, score, and the runtime source read that and nothing else. Beyond canonical names it adds:

  • A source graph. Collections sharing a (repository, ref) resolve to one acquisition listing both keys — what sync already did internally, now visible — with refKind separating a pinned commit from a mutable branch or tag.
  • Provenance. Every value records whether it was authored here, inherited from a source repo, inferred, or defaulted. A value written under a deprecated name records the name the author actually typed, so a migration diagnostic can point at their line.

Compatibility

Existing configs stay type-valid and produce byte-identical output — there's a test asserting equivalent legacy and canonical configs resolve identically. Deprecated fields carry IDE-visible @deprecated guidance and warn once per config file, not per command. Setting an old name beside its replacement fails naming both rather than applying an invisible precedence rule. Nothing is removed before 1.0; the policy is written down in the new docs/concepts/config-model.mdx.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 241e8177-bb91-4bbb-a4c5-fb865f3e8c0a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The alias-folding half of this PR is solid and I could not find a path where a legacy or canonical field name is silently dropped. The resolved model half is not wired up: nothing in the codebase reads ResolvedDocsConfig, and the new public docs page tells users to read it themselves — where it returns incomplete data for the flagship inheritConfig: true shape.

Reviewed changes — full diff of the single commit f08dcca, plus the surrounding config-load, sync, lint, and generate paths that consume the renamed fields.

  • Three per-collection field renamesprefixroutePrefix, sourceConfiginheritConfig, schemafrontmatterSchema, with the old names kept as @deprecated optional properties on DocsCollection.
  • New normalizerconfig/normalize.ts folds each alias onto its canonical key, deletes the alias, records a ConfigDeprecation, and throws when both spellings are set.
  • New resolved project modelconfig/types.ts adds ResolvedDocsConfig with a deduped (repository, ref) source graph, refKind, per-field FieldProvenance, and serializeResolvedConfig.
  • Single wiring pointloadDocsConfigFromDir normalizes, so generate, sync, and lint all inherit it; every reader was switched to canonical names only (sync.ts resolveCollection, lint/cli.ts schemas, generate.ts inheritance).
  • Deprecation warning at loadwarnConfigDeprecations emits one logger.warn per config path, deduped through a module-global Set.
  • New public exportsdefineLeadtypeConfig / LeadtypeConfig (identity aliases of defineDocsConfig / DocsConfig), normalizeDocsConfig, serializeResolvedConfig, and the resolved-model types.
  • Docs — new concepts/config-model.mdx page wired into nav, and every example across five existing pages moved to canonical names.
  • Tests — a new normalize.test.ts suite (aliases, ambiguity, source graph, prefixes, single-source, serialization) and two cli.test.ts cases for the legacy and canonical end-to-end runs.

⚠️ The resolved config is documented as the model every subsystem reads, but nothing reads it

The back-compat mechanism that actually works here is the canonicalized config object — aliases get folded and deleted, and downstream readers were switched to canonical names. ResolvedDocsConfig is a parallel artifact whose only consumer is warnConfigDeprecations, which touches resolved.deprecations and nothing else. Three places state otherwise, and one of them is a shipped docs page: docs/concepts/config-model.mdx line 127 ("the project model that generate, sync, lint, score, and the runtime source all read"), the types.ts header JSDoc lines 7-9, and the changeset. That claim is what makes the model's gaps user-visible rather than internal.

Technical details
# Resolved-config docs overstate what consumes the model

## Affected sites
- `docs/concepts/config-model.mdx:127` — "Normalization turns it into the project model that generate, sync, lint, score, and the runtime source all read. There is one normalizer, so there is one answer to 'what is this project?'". No subsystem reads it; grep for `resolved.collections` / `resolved.sources` / `resolved.provenance` returns only `normalize.ts`, `types.ts`, and the test file.
- `docs/concepts/config-model.mdx:118` — documents `inherited` as an observable origin. No code path ever produces `origin: "inherited"`.
- `packages/leadtype/src/config/types.ts:7-9` — same claim in the header JSDoc.
- `packages/leadtype/src/config/types.ts:103-127``ResolvedDocsCollection` also omits `flatteners`, which `lint/cli.ts:333` reads off the raw collection, so the resolved shape is not yet a superset of what consumers need.
- `.changeset/canonical-config-api.md:9` — "generate, sync, lint, score, and the runtime source all read".

## Required outcome
- The docs and JSDoc describe what is true at merge time. Either the subsystems are switched over to read `resolved`, or the prose is scoped down to what the model is today: an exported, inspectable derivation plus the source of the deprecation warning.
- `ConfigValueOrigin`'s `"inherited"` member and `FieldProvenance.inheritedFrom` are either produced somewhere or dropped until they are.

## Open questions for the human
- Is switching subsystems onto `resolved` intended for this PR or a follow-up? If it is a follow-up, saying so in the changeset costs nothing and stops the docs from promising a guarantee the next reader will try to rely on.

ℹ️ Nitpicks

  • apps/next-example/leadtype.config.ts:56-57 is still authored with prefix / sourceConfig, so the repo's own flagship example now prints the new deprecation warning on every build. Either update it or keep it deliberately as the in-repo legacy fixture — worth a one-line comment saying which.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

config: DocsConfig;
path: string;
/** The resolved project: collections, source graph, provenance, deprecations. */
resolved: ResolvedDocsConfig;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved is snapshotted at config load, but runGenerateCommand (generate.ts:2983-2993) rebuilds loadedConfig after inheritCollectionSourceConfigs and spreads this field through unchanged. For the inheritConfig: true shape the resolved collections therefore omit every inherited navigation, frontmatterSchema, groups, and mounts — the exact shape docs/concepts/config-model.mdx recommends and then tells users to inspect via the newly exported normalizeDocsConfig.

Technical details
# `LoadedDocsConfig.resolved` goes stale after source-config inheritance

## Affected sites
- `packages/leadtype/src/cli/generate.ts:310``resolved` is a required field on `LoadedDocsConfig`, so every holder is entitled to assume it matches `config`.
- `packages/leadtype/src/cli/generate.ts:2983-2993``loadedConfig = { ...loadedConfig, config: { ...loadedConfig.config, collections } }`. `config.collections` is replaced with the post-inheritance map; `resolved` is carried over from before the merge.
- `packages/leadtype/src/config/types.ts:36` and `:53``ConfigValueOrigin`'s `"inherited"` member and `FieldProvenance.inheritedFrom` have no producer anywhere in the codebase, which is the same gap seen from the type side.

## Required outcome
- A `ResolvedDocsConfig` handed to any consumer describes the same collections as the `config` it travels with, including values that arrived by inheritance.
- Values sourced from a source repo's `docs.config.*` resolve with `origin: "inherited"` and an `inheritedFrom` key, or those two declarations go away until something produces them.

## Suggested approach (optional)
- Re-run `normalizeDocsConfig` on the post-inheritance config at the point where `loadedConfig` is rebuilt, and have `mergeInheritedSourceConfig` report which fields it supplied so the second pass can mark them `inherited` rather than `explicit`.

};
}

function assertUniqueRoutePrefixes(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two rules already exist downstream: resolveDocsSourcesFromCollections (generate.ts:2210-2226) rejects the site root and duplicate URL prefixes with its own wording, and resolveSources' cacheDir check duplicates resolveRemoteSources (sync.ts:161-183) — with different semantics, since this copy compares authored strings and treats undefined as no-opinion while sync compares resolved absolute paths, so an explicit-vs-default pair conflicts there and not here. Given the PR's "one normalizer, one answer" framing, collapsing to one owner per rule is the point.

Technical details
# Route-prefix and cacheDir validation now lives in two places with divergent semantics

## Affected sites
- `packages/leadtype/src/config/normalize.ts:404-423` (`assertUniqueRoutePrefixes`) vs `packages/leadtype/src/cli/generate.ts:2210-2226` — same two rules, two error messages for the same misconfiguration depending on which layer trips first.
- `packages/leadtype/src/config/normalize.ts:221-233` vs `packages/leadtype/src/sync/sync.ts:161-183` — the `cacheDir` conflict rule. normalize compares the authored `cacheDir` strings and does `existing.cacheDir ??= collection.cacheDir`; sync compares the *resolved* cacheDir (with its `.leadtype/sources/<slug>@<ref>` default applied). A config where one collection sets `cacheDir` and a sibling on the same `(repository, ref)` does not passes normalize and is then rejected by sync — and in the meantime `resolved.sources[*].cacheDir` advertises a value sync will not accept.

## Required outcome
- One owner per rule, so a given misconfiguration produces one message and `resolved.sources` cannot describe an acquisition that the sync layer rejects.

## Open questions for the human
- Is the downstream copy meant to stay as a defence for callers that build collections without going through `loadDocsConfigFromDir`? If there are no such callers, the generate-side check is now unreachable and can go.

Comment thread packages/leadtype/src/config/normalize.test.ts
Comment thread docs/reference/cli.mdx Outdated
Comment thread docs/pipeline/collections.mdx
Comment thread packages/leadtype/src/cli/generate.ts Outdated

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The config migration in this commit is clean, but it also carries six generated artifact files into apps/sveltekit-example/static/ with http://localhost:5173 baked into them, because the root .gitignore's sveltekit list is missing six of the entries its apps/*/public/* twin has.

Reviewed changesf08dcca..b93ddd0:

  • apps/next-example/leadtype.config.ts moved onto defineLeadtypeConfig / routePrefix / inheritConfig, which retires the only nitpick from the previous review.
  • docs/docs.config.ts switched from import type { DocsConfig } + object literal to defineDocsConfig(...).
  • docs/concepts/config-model wired into the Concepts nav and added to docs/paths.lock.json; the five refreshed hashes match exactly the mdx pages f08dcca edited.
  • Six generated artifacts committed under apps/sveltekit-example/static/ — see the inline comment.

All six inline threads from the previous review were re-checked against the working tree, not just by anchor position; b93ddd0 touches none of those files, so they all stay open.

ℹ️ Nitpicks

  • docs/docs.config.ts:3-4 — the new comment is missing its verb: "This repo's docs are source-owned content, so defineDocsConfig — the same helper the docs tell everyone else to use." Adding "so it uses defineDocsConfig" closes the sentence.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Allow: /sitemap.xml
Allow: /sitemap.md

Sitemap: http://localhost:5173/sitemap.xml

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These six files are build output, not source. apps/sveltekit-example/package.json runs leadtype generate --src ../.. --docs-dir docs --out static --base-url http://localhost:5173, and dev, build, and check-types all invoke it — so every one of these files is rewritten (with fresh dateModified timestamps) the moment anyone runs the example, and every URL inside them now has http://localhost:5173 committed into git.

Root cause is the root .gitignore: it maintains two parallel generated-output lists, but the apps/sveltekit-example/static/* block has only 5 entries where the apps/*/public/* block has 11. git check-ignore --no-index confirms the asymmetry — apps/next-example/public/robots.txt is ignored via .gitignore:46, and none of the six sveltekit files are ignored at all. Nothing else has ever been tracked in that directory, and CI never builds the example apps, so this drift cannot be caught automatically.

Technical details
apps/sveltekit-example/static/robots.txt         (Sitemap: http://localhost:5173/sitemap.xml)
apps/sveltekit-example/static/sitemap.xml        (223 lines of http://localhost:5173/... locs)
apps/sveltekit-example/static/sitemap.md
apps/sveltekit-example/static/schema-map.xml
apps/sveltekit-example/static/mcp.json
apps/sveltekit-example/static/feeds/schema.jsonl

Affected sites: .gitignore:39-54, apps/sveltekit-example/package.json

Required outcome: nothing generated is tracked under apps/sveltekit-example/static/, and git status stays clean after running the example.

Suggested approach: git rm --cached the six files and add the six missing entries (feeds/, mcp.json, robots.txt, schema-map.xml, sitemap.md, sitemap.xml) to the sveltekit block — or drop both hand-maintained lists in favour of an app-local .gitignore, which is what apps/c15t-example and apps/tanstack already do.

Open question: were these swept in accidentally while regenerating docs for the new config-model page?

@KayleeWilliams
KayleeWilliams force-pushed the dx/151-canonical-config branch from b93ddd0 to cd5cb75 Compare August 4, 2026 10:05
DocsConfig grew from a description of one MDX folder into the project model —
identity, source acquisition, navigation, feeds, redirects, agent surfaces,
multi-repo orchestration. Growth like that left field names from the job they
used to do, and left each subsystem re-deriving the project from raw config.

Canonical vocabulary:

- `defineLeadtypeConfig` for a project/site `leadtype.config.ts`, alongside
  `defineDocsConfig` for a source repo's content-owned `docs.config.ts`. The
  two shapes are identical; the names mark ownership, which is what decides
  what may be inherited across repos.
- `prefix` → `routePrefix` (three different prefixes appear in one
  multi-collection config; only one reaches a URL), `sourceConfig` →
  `inheritConfig` (it declares inheritance, not a config object), `schema` →
  `frontmatterSchema` (matching the top-level field of the same purpose).

One normalizer, one resolved project. `normalizeDocsConfig` folds deprecated
aliases onto canonical names and derives `ResolvedDocsConfig`; generate, sync,
lint, score, and the runtime source read that and nothing else. Beyond
canonical names it adds:

- A source graph. Collections sharing a `(repository, ref)` resolve to one
  acquisition listing both keys — what sync already did internally, now visible
  — with `refKind` separating a pinned commit from a mutable branch or tag.
- Provenance. Every value records whether it was authored here, inherited from
  a source repo, inferred, or defaulted, and a value written under a deprecated
  name records the name the author actually typed.

Compatibility is the point, not an afterthought: existing configs stay
type-valid and byte-for-byte identical in output (asserted by test), deprecated
fields carry IDE-visible `@deprecated` guidance and warn once per config file,
and setting an old name beside its replacement fails naming both rather than
applying an invisible precedence rule. Nothing is removed before 1.0.
The PR claimed docs and examples use only the canonical syntax. They did not:
`apps/next-example/leadtype.config.ts` — the flagship pinned-source dogfood —
still used `prefix` and `sourceConfig`, so running `doctor` or `generate`
against our own example printed the deprecation warning this branch adds. A
deprecation whose reference implementation triggers it is not a migration
path, it is a warning users learn to ignore.

It is a site config pinning a source repo, so it is `defineLeadtypeConfig` too.

`docs/docs.config.ts` also declared `const config: DocsConfig = {…}` rather
than calling `defineDocsConfig()` — the helper every page of our own
documentation tells readers to use.
Committed build output. Six generated files under
`apps/sveltekit-example/static/` went in with the config migration — rewritten
with fresh timestamps whenever anyone runs the example, and carrying
`http://localhost:5173` into the repo. Removed. The ignore rules that stop it
recurring are in the example-app hygiene PR, which is off `main`.

The resolved config went stale after inheritance. `runGenerateCommand` swapped
the inherited collections into `loadedConfig.config` and spread `resolved`
through untouched, so for `inheritConfig: true` — the shape the config-model
page recommends — the resolved model omitted every inherited `navigation`,
`frontmatterSchema`, `groups`, and `mounts`. It re-normalizes now, keeping
deprecations and the acquisition graph from the first pass, which is the only
one that saw the authored aliases and source names.

A test that could not fail: both fixtures in the alias-equivalence case used
prefixes identical to their collection keys, which is exactly what the
`/${key}` fallback produces — so deleting the `prefix` fold entirely left it
green. The legacy fixture now uses a prefix that differs from its key.

Find-and-replace damage in `cli.mdx` ("its own public URL routePrefix" —
`routePrefix` is the field name, not a noun), a `schema:` left unrenamed in the
per-collection frontmatter example on the page most likely to be copied, and a
`resetConfigDeprecationWarnings` export described as a test seam that no test
uses.

Verified with the hook's own command run standalone: 743 pass, 0 fail. The
in-hook run flaked on 5s timeouts under contention, failing a different test
each attempt.
@KayleeWilliams
KayleeWilliams force-pushed the dx/151-canonical-config branch from cd5cb75 to 0f7ede9 Compare August 4, 2026 10:48

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

0f7ede9 closes four of the seven open threads, and the re-normalization fix is the right call — the old object spread really did leave inherited navigation/groups/frontmatterSchema out of the resolved model. But re-running normalizeDocsConfig on an already-normalized config rebuilds provenance from a config whose alias keys pass 1 already deleted, so the resolved model now contradicts two of the contracts docs/concepts/config-model.mdx introduces in this same PR. Separately, switching docs/docs.config.ts from import type to a value import makes a leadtype build a hard prerequisite for bun test.

Reviewed changes — new commit 0f7ede9 (5 files in PR scope):

  • packages/leadtype/src/cli/generate.tsresetConfigDeprecationWarnings deleted (grep confirms zero remaining references), and executeGenerate now re-normalizes after inheritance instead of spreading collections in. See the inline comment on the provenance side effect.
  • packages/leadtype/src/config/normalize.test.ts — the changelog fixture moved to /releases, so the headline "produces the same collections and sources" test can now actually fail if the alias fold regresses. This was the strongest of the previous review's findings and it is properly fixed.
  • docs/pipeline/collections.mdx:256schema:frontmatterSchema:.
  • docs/reference/cli.mdx:127 — "public URL routePrefix" → "public URL prefix".
  • docs/docs.config.ts — moved onto defineDocsConfig(...). See the inline comment.

docs/paths.lock.json's two refreshed hashes match exactly the two mdx files this commit edited.

The branch was rebased again (base tip 7967d825227964), so four files in the 26-file total — .agents/skills/leadtype/SKILL.md, .github/workflows/ci.yml, evals/EVAL-AREAS.md, packages/leadtype/src/llm/skills.test.ts — are #158's scope, not this PR's. git diff-tree 7967d82 5227964 confirms.

I re-checked all seven prior threads against the working tree rather than by anchor position. Four are now addressed and have been replied to and resolved. Three stay open:

  • packages/leadtype/src/cli/generate.ts:310 — the inherited provenance origin and inheritedFrom still have no producer anywhere in the codebase.
  • packages/leadtype/src/config/normalize.ts:404 — the duplicated route-prefix validation is untouched.
  • apps/sveltekit-example/static/robots.txt:210 — the six generated artifacts are still tracked and .gitignore:50-54 still has 5 entries where its apps/*/public/* twin has 11. Verified on disk, not just from the diff.

Also still open from the previous review body, with no line in this delta to anchor to: docs/concepts/config-model.mdx:86, packages/leadtype/src/config/types.ts:7-9, and .changeset/canonical-config-api.md:9 all say the resolved model is what "generate, sync, lint, score, and the runtime source all read". Grep says only two call sites read it at all — warnConfigDeprecations reads resolved.deprecations, and generate.ts:2998 reads resolved.sources. That is a description of where this is heading, not where it is.

ℹ️ Nitpicks

  • docs/docs.config.ts:3-4 — the comment is still missing its verb: "This repo's docs are source-owned content, so defineDocsConfig — the same helper the docs tell everyone else to use." Carried from the previous review.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment on lines +2996 to +2999
resolved: {
...renormalized.resolved,
sources: loadedConfig.resolved.sources,
deprecations: loadedConfig.resolved.deprecations,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment reasons carefully about carrying sources and deprecations forward from pass 1 because "the first pass ... is the only one that saw the authored aliases and source names" — but provenance has exactly the same property and is not carried forward, so pass 2's version wins.

Pass 1's applyCollectionAliases does delete mutable[alias.deprecated] after folding, so by the time pass 2 runs, prefix/sourceConfig/schema are gone from the config. Pass 2 therefore takes the authored === undefined path and records routePrefix via recordExplicit, i.e. a bare { origin: "explicit", configPath } with no authoredAs. Two documented contracts break, both introduced by this same PR:

  • docs/concepts/config-model.mdx:126 documents the output as { origin: "explicit", authoredAs: "prefix", configPath: "/repo/leadtype.config.ts" }. After inheritance runs, authoredAs is absent.
  • docs/concepts/config-model.mdx:118 documents an inherited origin. Every field that inheritance just supplied — navigation, groups, frontmatterSchema, flatteners, mounts — is now recorded as explicit with the project's configPath, attributing source-repo values to a file that never authored them. That is the one place inherited could plausibly have been produced, so this also makes the generate.ts:310 thread strictly harder to close.

To be clear about blast radius: nothing reads resolved.provenance yet, and I confirmed independently that this call changes no output file, warning, thrown error, or exit code versus the spread it replaced — inheritance can only ever write the five SOURCE_CONFIG_INHERIT_FIELDS, none of which can trip pass 2's routePrefix, cacheDir, or both-spellings-set checks, and normalizeDocsConfig doesn't mutate its input. So this is a latent correctness problem in the model, not a live bug.

Required outcome: after inheritance, resolved.provenance still reflects what the user actually authored, and inherited fields are distinguishable from project-authored ones.

Suggested approach: merge pass 1's provenance over pass 2's the same way sources and deprecations are already merged, keeping pass 2's entries only for keys pass 1 had none for. Or, if the inherited origin is meant to become real here, have mergeInheritedSourceConfig record { origin: "inherited", inheritedFrom: <source config path> } per field it writes, which would close generate.ts:310 at the same time. Either way a test asserting authoredAs survives inheritance would pin it.

Comment thread docs/docs.config.ts
Comment on lines +1 to +5
import { defineDocsConfig } from "leadtype";

const config: DocsConfig = {
// This repo's docs are source-owned content, so `defineDocsConfig` — the same
// helper the docs tell everyone else to use.
const config = defineDocsConfig({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This turns leadtype from a types-only dependency of this file into a runtime one. packages/leadtype/package.json's exports all point at ./dist/*, so on a fresh clone with no build, loading this config fails with No "exports" main defined in .../node_modules/leadtype/package.json — I hit exactly that: 10 failures in packages/leadtype/src/cli.test.ts (lines 224, 249, 622, 2049, 2090, 2125, 2157, 2264, 2333, 2363), all reported as failed to load docs config at ".../docs/docs.config.ts". bun run --filter leadtype build fixes it and the suite goes green (50 files / 697 tests). CI stays green because its build step precedes test, so this won't be caught there — it lands on whoever clones and runs bun test first, and the error message points at the config file rather than at the missing build.

The import type version had no such ordering requirement, because the type import erased.

Required outcome: bun test passes on a clean clone without a prior package build, while this file still gets full type checking against DocsConfig.

Suggested approach: satisfies gives identical checking with no runtime import. Applying the suggestion below also requires changing the closing }); on line 340 to } satisfies DocsConfig;. (If the value import is deliberate — so this repo dogfoods the exact helper the docs recommend — then the ordering needs to be enforced somewhere a contributor will see it, e.g. a pretest that builds the package, rather than left implicit in CI's step order.)

Suggested change
import { defineDocsConfig } from "leadtype";
const config: DocsConfig = {
// This repo's docs are source-owned content, so `defineDocsConfig` — the same
// helper the docs tell everyone else to use.
const config = defineDocsConfig({
import type { DocsConfig } from "leadtype";
// This repo's docs are source-owned content, so this config is checked against
// the same `DocsConfig` shape the docs tell everyone else to use.
const config = {

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Introduce a canonical config API with backward-compatible normalization

1 participant