Leadtype 0.4
Leadtype 0.4 turns leadtype lint into a real quality gate — config-aware,
covering relative links and anchors, and typechecking TypeScript snippets
against your installed package so docs can't rot. It also adds native OpenAPI
reference generation, --watch with incremental builds, redirect tracking for
renamed and deleted pages, and moves agent-facing markdown conversion to the
native Satteri pipeline. Generation is now safe to run concurrently against a
shared output directory.
Upgrade prompts
Hand one of these to a coding agent to move an existing Leadtype project to 0.4. The first migrates a docs site or app; the second adopts the new lint gates on a docs source.
Upgrade this project to Leadtype 0.4. Keep Leadtype as the content and agent-readability layer; do not replace the docs framework or UI.
Inspect first:
- The installed leadtype version, package manager, framework, and every script that runs
leadtype generateorleadtype lint. - Any use of the removed legacy markdown surface: a
markdownEngineoption, imports fromleadtype/remark, or thedefaultRemarkPlugins,legacyDefaultMarkdownTransforms,builtinFlattenerPluginsaliases. - Deprecated CLI flags in scripts:
leadtype generate --mcp,leadtype generate --enrich-git,leadtype init --webmcp. - Any hand-rolled serialization (mutexes, lockfiles,
&&chains added to avoid races) aroundleadtype generatein CI or task graphs.
Implement:
- Upgrade
leadtypeto the latest 0.4 release. - Remove
markdownEngineand allleadtype/remarkimports — agent-facing conversion now always runs the native markdown pipeline. Port any custom flatteners to the markdown transforms API documented in the markdown reference. Bundler-side APIs underleadtype/mdxandleadtype/mdx/sourceare unchanged. - Replace deprecated flags with config:
agents.mcp.enabledindocs.config.tsinstead of--mcp; drop--enrich-git(Git enrichment is the default and best-effort). - Switch the dev loop to
leadtype generate --watch. Repeat builds are incremental by default (cache undernode_modules/.cache/leadtype/); use--forceonly to bypass the cache. For byte-reproducible builds, pass a fixedgeneratedAtto the artifact generators. - If pages have ever been renamed or removed, enable a
redirectsblock indocs.config.ts, commit thepaths.lock.jsonit maintains, and serve redirects withresolveRedirectfromleadtype/redirectsin the app's catch-all — or pass the entries tocreateAgentMarkdownResponsewhere it is already used. See Redirects. - Delete any hand-rolled locking around
leadtype generate— runs are now single-flight per output directory and every artifact is written atomically.
Validate:
- Run generate twice: the first run completes, the second mostly skips via the incremental cache.
- Run bare
leadtype lint(it now discovers the config) and fix what it reports. llms.txtand the markdown mirrors resolve; if redirects are enabled, an old renamed URL answers with a 308 to the new path.- Summarize changed files, removed legacy imports, and any lint findings left for a human.
Adopt the Leadtype 0.4 lint gates for this docs source. Change docs content, the docs config, and CI workflows only — do not edit app routes or UI code.
Inspect first:
- The docs config (
docs.config.ts/leadtype.config.ts), any existinglintblock, and how lint runs in CI today. - The snippet languages used in fenced code blocks, and whether the
typescriptpeer dependency is installed.
Implement:
- Run bare
leadtype lint— it now discovers the config and lints the same tree generate builds, with mounts applied. Fix what it finds: broken relative links, invalid anchors (#fragmenttargets must match a real heading on the target page), stalenavigationandllms.sectionsentries, and links reported as "moved to<new path>". - Enable snippet typechecking with
lint: { snippets: { typecheck: true } }in the docs config. Module-shapedts/tsxsnippets are assembled into virtual modules and typechecked against the project'stsconfig.jsonand realnode_modules. Use twoslash directives sparingly where needed:// @filename:for multi-file examples,// @checkto opt a fragment in,// @noErrorsto opt out,// ---cut---to hide setup lines from readers. - Tune severities with
lint.rules("off"/"warn"/"error") and replace ad-hoc ignore lists withlint.ignore. - Keep
leadtype lintin PR CI, and add the weekly scheduled workflow runningleadtype lint --external-linksfrom the validate-in-CI recipe. Mute known-flaky URL prefixes withlint.externalLinks.ignore.
Validate:
leadtype lintexits clean, with snippet parse and typecheck counts in the output.- A deliberately broken import in one snippet fails lint, then passes again once reverted.
- The scheduled external-link workflow is present and runnable via workflow_dispatch.
Breaking changes
- Agent-facing markdown conversion always runs the native pipeline. The
markdownEngineoption, theleadtype/remarkcompatibility export, and thedefaultRemarkPlugins,legacyDefaultMarkdownTransforms, andbuiltinFlattenerPluginsaliases are removed. Custom conversion logic moves to the markdown transforms API. Source-MDX bundler APIs underleadtype/mdxandleadtype/mdx/sourceare unchanged. leadtype generateis incremental by default and single-flight per output directory. Use--forceto bypass the cache andLEADTYPE_NO_LOCK=1to opt out of the lock.manifest.pages,sitemap.xml, andllms-full.txtare now ordered by navigation reading order instead of alphabeticalurlPathorder. Consumers that depended on alphabetical order should sort on their side.
Docs linting
Lint grew from a frontmatter checker into the merge gate for docs quality: every internal link, anchor, config entry, and code snippet is validated deterministically in PR CI, with external URLs checked on a schedule.
leadtype lintis config-aware: with no--srcit discoversleadtype.config.*ordocs.config.*exactly like generate and lints that tree with the config'smountsapplied, so links under mounted prefixes such as/changelogvalidate like/docslinks. A config that fails to load is a lint failure, not a crash.- The config's own links are linted (new
config-linkrule): curatednavigationentries matching no page andllms.sectionslinks to missing routes are errors; stale feedsource.urlPrefixvalues andredirects.removedpaths that are live again are warnings. - Relative links (
./sibling,../guides/x) resolve against their source file and validate like absolute links; links that climb out of the docs tree are errors. - Anchors are validated (new
invalid-anchorrule): same-page#fragmentlinks and fragments on cross-page links must match a heading anchor on the target page, extracted with the same slugger that builds the site TOC — includes expanded — so lint and the rendered site cannot disagree. Running this on Leadtype's own docs immediately found three silently broken anchors. - With redirect tracking enabled, an
invalid-linkwhose target matches a lockfile redirect reports "moved to<new path>— update the link" instead of a bare missing-route error. - Every fenced code block with a known language must parse (
snippet:parse): TS/TSX/JS via the TypeScript parser, JSON and YAML via real parsers. The checker is fragment-tolerant — bare signatures, config excerpts, sibling JSX, and...ellipsis lines parse without annotation — and anything deliberate can be marked with// @noErrors. Tuned on Leadtype's 51-page docs corpus: zero annotations needed, and the only findings were real bugs. - Opt-in TypeScript snippet typechecking (
snippet:types): withlint: { snippets: { typecheck: true } }, module-shapedts/tsxsnippets are assembled into virtual modules and typechecked against yourtsconfig.jsonand realnode_modules. When a package API changes, every doc example still calling the old API fails lint — docs that can't rot. Twoslash conventions (// @filename,// @check,// @noErrors,// ---cut---) build multi-file examples and hide setup lines, and a default markdown transform strips all directives from generated output. All snippets check in one shared compiler program, so cost stays flat. - Opt-in
external-linkrule for scheduled CI: HEAD with GET fallback, one retry, 429 treated as skip, per-URL dedupe, bounded concurrency, and a 7-day cache of confirmed-live URLs undernode_modules/.cache/leadtype/(failures are never cached). Enable withleadtype lint --external-linksorlint.rules["external-link"]; a copy-pasteable weekly GitHub Actions recipe ships in the validate-in-CI guide. - A new
lintconfig block controls all of it:lint.ignorereplaces the default ignore globs,lint.unknownFieldSeveritysets the unknown-field default, andlint.rulesremaps any rule's severity across the CLI and thelintDocs()API. See the lint reference.
OpenAPI API reference generation
- Added native OpenAPI 3.x page generation: an
openapiblock indocs.config.tsturns a JSON/YAML spec into MDX operation pages that render through your docs UI and flatten into agent-readable markdown — the same pages flow intollms.txt, search, markdown mirrors,AGENTS.md, and Agent Readability artifacts. See the OpenAPI reference. - Each operation page carries the full contract: machine-scannable frontmatter
(
method,path,operationId,apiVersion,canonicalUrl,lastModified, and asourcepointing at the spec), parameter and property tables with nested dotted rows (results[].title), the dereferenced JSON Schema per media type, named or synthesized JSON examples, response headers, and generated cURL/fetchsamples with real bodies, path parameters substituted from spec examples, and auth headers derived from the security scheme. Redocly-stylex-codeSamplesoverride the generated snippets. - A generated overview page per spec lists every operation grouped by tag and joins the navigation as the section landing page; operation pages link back to it from a Related section.
- Three integration shapes:
createDocsSource({ openapi })/fumadocsSource({ openapi })write generated pages into a temp overlay while authored docs stay live and untouched (sources exposecleanup(), with a process-exit sweep as fallback),leadtype generatepicks the config up automatically, and static-glob bundlers (TanStack Start / Vite) usewriteOpenApiPages()into an app-local directory.stageOpenApiDocs()exposes the full-copy staging primitive for custom pipelines. - Rendering follows the existing component naming contract: seven new
Api*tags with prop types fromleadtype/mdx, a dependency-freeflattenApiSchemaRows()helper fromleadtype/mdx/openapiso custom renderers derive the same nested rows as the markdown flatteners, and copyable reference implementations in the Fumadocs and TanStack examples. Operation summaries and descriptions are treated as CommonMark and escaped, so arbitrary specs can't break the MDX build. - OpenAPI-only docs configs are valid, remote specs and remote
$reftargets time out after 30 seconds, and generated pages fail loudly instead of overwriting pre-existing docs files.
Watch mode and incremental builds
leadtype generateis now incremental by default: each converted file's inputs — the MDX source, its<include>targets, the TypeScript files its type tables extract from, and its git enrichment — are content-hashed into a manifest undernode_modules/.cache/leadtype/, and unchanged files are skipped on repeat runs. Outputs whose source was deleted are pruned.--forcebypasses the cache, which also invalidates automatically on leadtype version, docs-config, or flag changes.leadtype generate --watch(or-w) runs the pipeline, then watches the docs source directories and config file and re-runs on change (debounced). With the cache, a one-file edit rebuilds one file.- Library API:
convertAllMdxaccepts a new optionalcacheoption, and conversion reports every extra file it reads — include targets via the existing_compiler.addDependencyprotocol, and now also type-table TypeScript sources viaTypeTableOptions.onDependency.
Redirect tracking
- Added opt-in redirect tracking for renamed and deleted docs pages, so old
URLs stop 404ing in search engines and agent indexes. Enable it with a
redirectsblock indocs.config.ts; generate then maintains a committed lockfile (paths.lock.jsonnext to the docs sources) recording every published path with a content hash, and emits<out>/docs/redirects.json. See Redirects. - Pure moves are detected automatically: a path that disappears while its content hash reappears at a new path gets a permanent 308 redirect with zero authoring. Hashes exclude frontmatter so git-enrichment churn doesn't defeat the match, and ambiguous matches are never guessed.
- Unexplained disappearances fail the build loudly, listing each path with the
fix: add
redirectFrom: [<old path>]frontmatter to the successor page, or acknowledge intentional deletions underredirects.removedto serve 410 Gone.redirectFromis part of the default frontmatter lint schema. - Redirects accumulate and self-maintain: chains from successive renames collapse to the final target, entries whose target is later removed degrade to 410, and entries whose path comes back alive are dropped.
- A new edge-safe
leadtype/redirectsentry point exportsresolveRedirectand the pure computation primitives for serving redirects in any framework's catch-all (no Node built-ins, so it links in Cloudflare Workers and Vercel Edge); lockfile IO lives underleadtype/redirects/node.createAgentMarkdownResponseaccepts the entries directly and answers agent-shaped requests for renamed pages — including.mdmirrors — with the 308/410, while browser requests fall through to the host app's routing. - Enabling
redirectsalso enables conversion pruning, since rename detection requires stale mirrors of renamed sources to be garbage-collected. Filtered generates (--include/--exclude) skip redirect tracking and pruning with a warning.
Native markdown pipeline
convertAllMdx,convertMdxFile,convertMdxToMarkdown, andleadtype generatenow parse MDX through Satteri and run native markdown transforms and stringification for agent-facing output. The legacy agent-side Remark conversion path is removed (see Breaking changes above).- Batched Git frontmatter enrichment during
convertAllMdx: whenenrichFrontmatterFromGitis enabled, conversion reads Git history once for the docs tree and maps results back to each file instead of spawninggit logper file. In a 120-file synthetic benchmark the Git metadata read dropped from ~2.36s of per-file process spawning to ~12ms; enrichment stays best-effort for shallow clones, missing Git, and untracked files, andlastAuthorstill falls back to the latest non-bot author. - Cached repeated
<include>and<import>resolution within a conversion run: pages that reuse the same partial share one raw file read and one parsed markdown AST, while section anchors such asfile.mdx#setupstill extract from cloned ASTs. A synthetic 200-page repeated-include benchmark cut include expansion from ~400ms to ~68ms; the newcreateIncludeResolutionCache()helper exposes cache stats.
Concurrent generation safety
- Made generation safe to invoke concurrently against a shared
outDir. Parallel task graphs where lint, typecheck, and build each depend on docs generation used to race on the output directory, causing intermittent partial reads, ENOENT on files another run had just replaced, and half-written artifacts. - Every generated artifact — converted
docs/*.md,llms.txt,llms-full.txt, the search index, sitemaps, robots, feeds, the MCP server card, NLWeb artifacts, skills, and sync manifests — is now written to a temp sibling and atomically renamed into place, so concurrent readers (including a siblingtscor framework build readingpublic/) see the old content or the new content, never a truncated file. - Removed delete-then-recreate windows: the agent-skills surface and mounted
markdown mirrors write the new files first and prune stale entries after,
instead of
rm -rf-ing a live directory before rebuilding it. leadtype generateruns are single-flight per output directory via a cross-process lock stored under the system temp dir, keyed by the resolved--outpath. Concurrent invocations wait for the in-flight run. Abandoned locks recover fast: interrupted runs release on the way out, hard-killed runs are reclaimed as soon as their recorded process is gone, and unidentifiable locks are reclaimed after 10 minutes. Waiting runs fail loudly after 15 minutes instead of hanging CI (LEADTYPE_LOCK_TIMEOUT_MSoverrides). SetLEADTYPE_NO_LOCK=1to opt out.- Overhead is negligible: the atomic write adds one rename per file (~0.1–0.2ms) and the lock a fixed ~8ms per run — about 1–2% end to end on a 300-page site.
Output hygiene and determinism
- Added an opt-in
pruneoption toconvertAllMdxthat removes orphaned.mdoutputs when a source page is deleted or renamed, ending the hand-rolled garbage-collection step every consumer needed. Only.mdfiles are candidates, symlinks are never followed, pruning is skipped with a warning when any page fails to convert or the source resolves to zero pages, andpruneKeepglobs exempt.mdfiles written by other tools. Pruning holds the same per-outDirlock as generate. - Added a
generatedAtoption to the agent artifact generators so manifests and timestamp fallbacks can be reproduced byte-for-byte across deterministic builds.
Agent discovery and MCP
generateandgenerateAgentArtifacts()now emit/.well-known/api-catalogalongside robots and sitemap artifacts, route handlers can serve it dynamically, andleadtype/llm/readabilityexports helpers for RFC 8288Linkheaders that advertise the catalog, service docs, service description, and sitemap. Robots output includes scanner-friendly AI crawler aliases and renders Content-Signals inai-train, search, ai-inputorder.- The MCP server card carries
serverInfo.instructions(defaulting to a summary-derived line, overridable viaagents.mcp.serverInfo.instructions), and the live server advertises the same instructions in itsinitializeresponse. Tool summaries carryreadOnlyHint/idempotentHintannotations,agents.mcp.icon(or itslogoalias) sets a card icon, and the card is additionally written to a root/mcp.jsonalongside the/.well-known/mcp.jsondiscovery copy. - Invalid MCP tool calls surface structured JSON-RPC errors with proper error codes instead of generic internal errors.
Agent-readability artifacts
- Sorted
manifest.pagesfromgenerateAgentReadabilityArtifactsin navigation order — groups depth-first, then pages within each group — instead of alphabeticalurlPathorder. Navigation order is the authored reading order, which is what agent and LLM consumers of the manifest want. Pages not present in the navigation are appended sorted byurlPath, so the manifest stays fully deterministic without consumer-side re-sorting.sitemap.xmlis rendered from the same page list and shares the order. (#115, #120) - Applied the same ordering to
llms-full.txtin legacygroupsmode.generateLLMFullContextFilespreviously only reordered under curatednav, so the full-context file could disagree with the manifest when onlygroupswere configured; both now share reading order in both modes. - Left the bring-your-own-pages
generateAgentArtifactsentry point unchanged: there the inputpagesorder is the authored order supplied by the caller, now pinned by a regression test.
MDX children typing
-
Added a
ChildrenTypeRegistryaugmentation hook toleadtype/mdx, so framework consumers typechildrenonce per project instead of casting in every component. The registry is empty by default andchildrenstaysunknown— no behavior change without opting in, and Leadtype still ships zero renderer dependencies: -
After that single declaration, every tag prop type (
CalloutProps,TabsProps,StepProps, …) exposes correctly typedchildren, verified through the published type rollup and adopted by the React examples in the docs. The resolved type is also exported asTagChildren.