Version Packages - #190
Open
github-actions[bot] wants to merge 1 commit into
Open
Conversation
github-actions
Bot
force-pushed
the
changeset-release/main
branch
8 times, most recently
from
August 23, 2026 17:51
b5fdcf2 to
ef7bfe5
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
August 24, 2026 11:24
ef7bfe5 to
34064b7
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
Releases
leadtype@0.5.0
Minor Changes
3981fd2: Make
baseUrla config field, so the common path stops repeating it.A docs audit found
baseUrlwas the one value every snippet had to state twice —generate --base-url https://…for the CLI andcreateDocsProject({ baseUrl })for the runtime — because it was not part of the config. It now is: a site-owned, top-level field next toproduct.With it set, the scaffolded common path carries no knobs at all:
leadtype generateneeds no--base-url, and the runtime iscreateDocsProject()with zero arguments.leadtype initwrites the value once, intodocs/docs.config.ts, and nowhere else.Precedence is explicit-wins: the
--base-urlflag and thecreateDocsProject({ baseUrl })argument override the config field, and a config without the field keeps the deployment-URL env fallbacks exactly as before. One shared validator now covers every authored spelling — the config field, the flag, and the argument: a base URL must be an absolute http(s) URL, optionally with a path prefix, carrying no query, fragment, or embedded credentials, and is normalized (trailing slashes stripped) so URL joins cannot produce//. For the flag and the argument this is stricter than before — they used to forward whatever they were given, so a value likeftp://acme.dev,https://acme.dev?, or an unparseable string flowed straight into every joined URL and corrupted the generated artifacts. Such a value now fails up front instead:generateexits 2 andcreateDocsProjectthrows.Because a base URL says where this site publishes, the field is never inherited via
inheritConfig— a source-owneddocs.config.*consumed by several sites should leave it unset and let each site supply its own.The resolved value is inspectable like every other derived-or-authored value: provenance carries a
baseUrlentry (explicitwith the config path, ordefaultnaming the env fallback chain),leadtype doctorreports the resolved URL and its origin, andgenerate --explainreports the fallback when nothing was authored anywhere.4c1b962: Add a canonical config vocabulary and one resolved-config normalizer.
defineLeadtypeConfignames the project/site config inleadtype.config.ts, alongsidedefineDocsConfigfor a source repo's content-owneddocs.config.ts. Three collection fields are renamed:prefix→routePrefix,sourceConfig→inheritConfig,schema→frontmatterSchema.Every existing config keeps working. Both spellings normalize to one internal
ResolvedDocsConfigthat generate, sync, lint, score, and the runtime source all read, so no subsystem re-derives the project from raw config. The resolved shape adds a deduped source graph — collections sharing a(repository, ref)resolve to one acquisition — and per-field provenance recording whether each value was authored, inherited from a source repo, inferred, or defaulted.Deprecated fields carry
@deprecatedguidance in the IDE and warn once per config file at load. Setting an old name and its replacement together is an error naming both rather than a silent precedence rule. Nothing is removed before 1.0.5aa8d2b: Derive navigation and the
llms.txtbody when they aren't authored, so a new project reaches useful output from identity alone.Navigation is derived from the content tree — root pages at the root, one section per top-level directory titled from its
indexpage, orderedindexfirst then frontmatterorderthen path, so it never depends on filesystem enumeration order. Thellms.txtbody becomes a "Best Starting Points" block built from that same resolved navigation, which keeps an agent's entry points from drifting from a reader's. Product name and tagline fall back topackage.json.Both
leadtype generateandcreateDocsSource()derive the same tree, so the rendered sidebar and the generated artifacts agree. Explicit configuration always wins: authoringnavigation,groups, orllms.sectionsturns inference off for that field, and inference never merges with or rewrites an authored value.leadtype generate --explainreports every derived value, what it was derived from, and the field that takes control of it. Ambiguous derivations — pages with no frontmattertitle, or more pages than the starting-points block lists — warn with the field to set.leadtype initnow scaffolds identity only.2fda088: Add
createDocsProject()— the resolved project config as a runtime source.createDocsSource()describes one content directory, so an app restates what its config already says: the content root, navigation, mounts, the frontmatter schema, and for a multi-collection project all of that per collection plus route prefixes and source-owned inheritance. Two descriptions of one project drift, and when they do the rendered site and the generated agent artifacts disagree about what exists.A project reads the same resolved config the artifact pipeline reads, and returns a superset of
DocsSource, so every first-party adapter accepts it unchanged:Multi-collection projects get one merged, route-aware page API —
listPages()tags each page with its collection,loadPage()accepts a collection-local slug or the full route — plusproject.collections,project.sources, andproject.getSource(key)for custom integrations. Source-owned config inheritance now runs through one shared implementation, so human rendering and generated artifacts cannot resolve it differently.Remote collections are cache-only: a missing, unverifiable, or wrong-revision cache fails with a diagnostic naming
leadtype syncrather than cloning inside a request. Route collisions name both collections.createDocsSource()stays fully supported and is what the project is built on.leadtype initnow scaffolds the project primitive.43883be: Add
gitSource()for declaring one git acquisition with its content collections beneath it.The flat
collectionsmap makes every collection carryrepository,ref, andcacheDireven when several come from the same repository — so a config repeats acquisition three times for one clone, and a reader has to know that matching(repository, ref)pairs are deduped. A source group declares the clone once:The source owns acquisition and the default inheritance policy; each collection owns its directory, route prefix, navigation, and any inheritance exception. Both forms normalize to the same source graph, and
sourcesmay be used alongsidecollections.Collection ids stay global rather than being scoped to their source, because they name staging mounts, error messages, and JSON output — two sources declaring the same id is an error naming both, not an auto-rename. So is declaring one
(repository, ref)under two source names.sparselimits a checkout to the repository paths you actually need, via a blobless partial clone — pinning a docs directory out of a monorepo no longer downloads the whole repository. Collections sharing an acquisition must agree on the path set, and the set is recorded in the sync manifest so adding a path re-clones rather than reusing an incomplete cache.leadtype syncnow reports each source id with its dependent collections, and warns when a source tracks a mutable ref instead of a pinned commit.leadtype generate --jsonreports the same acquisition graph, using the same ids — a named source keeps its authored name, an anonymous one is identified byrepository#ref.inheritConfig: falseopts a collection out of a source-level inheritance default.c21b6b9: Add
leadtype doctor— a read-only explanation of the resolved project.The other commands each answer a question about a project by doing something to it. None answered "what is this project, and why?" — which config was discovered, whether it resolved single-source or multi-repo, which values were authored versus inherited versus inferred, which collections share one clone, what routes will exist, and which command fixes what is currently wrong.
Doctor never clones, refreshes, writes, or generates, so an unsynced remote is a finding naming
leadtype syncrather than a fetch. Everything it reports comes from the same config loader and resolversgenerateandsyncuse, so a clean doctor run and a clean generate run cannot disagree.Checks cover config discovery and provenance, deprecated aliases, the deduped acquisition graph with pinned-versus-mutable refs and cache freshness, collection directories and include globs, the resolved navigation tree and pages that fall back to the root instead of being placed, expected output artifacts and their staleness, the detected framework adapter, and enabled agent surfaces.
Human output is concise and every finding names its owning config field and a concrete next command.
--jsoncarries stable finding ids and provenance so agents can act without parsing prose. Exit0when nothing is an error,1when a required input is missing or invalid.ba6ecdb: Reduce navigation config churn for large and repeated docs trees.
A curated tree earns its cost while every entry is a decision. It stops earning it when a section is mostly inventory — twenty pages where three must lead and the rest could be alphabetical — because then every new page is added twice, on disk and in the config, and the two drift.
navigation.fromDirectory()(fromleadtype/navigation) includes a directory without listing it, and the newpinoption on include entries keeps the pages whose position is a decision at the front:Pinned pages lead in the order given; the rest follow in
sortorder, so adding a page appends it to the tail and never displaces a deliberate choice. Explicit refs and an expansion can share onepagesarray. A pin matching nothing is an error rather than a silent no-op — that is almost always a rename the config missed. Expansion still happens once during navigation resolution, so one resolved tree keeps driving the sidebar,llms.txt,AGENTS.md, the sitemap, and Agent Readability metadata.leadtype navprints the tree your config actually resolves to and reports drift: pages no curated entry places, pages two entries both claim, and pages whosegroup:names a group the config never declares. Human and--jsonoutput, per collection, read-only — it never writes config, moves content, or changes a public route.f20c593: Describe the NLWeb
/askendpoint with a generated OpenAPI 3.1 document.With
agents.nlweb.enabled,leadtype generatenow writes an OpenAPI 3.1 JSON description of the endpointcreateAskHandler()serves to/openapi.json, so a machine client reads the contract instead of inferring it from prose. It modelsGETandPOST(both the flat and the NLWeb request shapes, with the body optional because aPOSTmay carry the query in URL parameters), thequery/q/query_id/streamingparameters, theOPTIONSpreflight and its CORS headers, the JSON answer and thetext/event-streamresponse on the same200, and one failure envelope witherror.codetyped as an enum.GETandPOSTdeclare400and500, the body-limit413belongs only toPOST, and405is published on the path item because it belongs to methods with no operation. SSE event payloads are published as anx-sse-eventsmap — OpenAPI 3.1 has no vocabulary for them. Every operation carries a stableoperationId(nlwebAskGet,nlwebAskPost,nlwebAskOptions), a description, and typed parameters and responses, so it converts to an LLM function definition without guesswork. Content negotiation is represented by the two200media types; the reservedAcceptheader is not emitted as a Parameter Object because OpenAPI consumers must ignore it there.agents.nlweb.openapiconfigures it:enabledturns it off,urlsets the public URL (default/openapi.json), andoutputsets the emitted path. Each defaults from the other, so moving one moves both; explicitly empty values are rejected instead of invoking those defaults. The derived output uses decoded URL pathname segments, never its query or fragment.urlrejects malformed percent escapes, backslashes, ASCII controls, and userinfo;outputrejects percent escapes, ASCII controls, and Windows-reserved device-name segments and is validated at config load rather than at build time — it must be a relative.jsonpath inside the output root, so an absolute path, a.or..segment, or a path that would overwrite another generated artifact or sit beneath one is rejected before a build can clobber it. The public generator repeats this validation before writing its schema artifacts. An absoluteagents.nlweb.endpointis split into the document'sserversentry and itspathskey, so a cross-origin/askis described honestly. NLWeb endpoints that are empty, whitespace-only, surrounded by whitespace, parent-relative, or contain a query string or fragment; MCP endpoints that contain only whitespace (the empty-string default remains valid); protocol-relative endpoints; non-HTTP scheme-like endpoints; absolute endpoints with userinfo; and NLWeb or MCP endpoints with backslashes, ASCII controls, or malformed percent escapes now fail at config load.leadtype generaterecords emitted OpenAPI ownership per output root in<source>/.leadtype/nlweb.json, outside the public output directory, and removes only that owned document after a successful move or after NLWeb is removed from config. The ownership map is best-effort: an immutable source tree still generates every public artifact, and a checksummedx-leadtype-generatedextension lets later runs update an unchanged-path contract without writable state. Move or disable cleanup still cannot be tracked until the source state becomes writable. Changingurloroutputdoes not leave a stale contract at the old URL, while an untracked, hand-authoredopenapi.jsonis never deleted. Ownership remains on an old output until its deletion succeeds, so a failed cleanup is retried instead of forgotten. Moves between ancestor and descendant paths stage the replacement and keep a rollback copy while resolving file/directory conflicts; an unowned sibling is preserved and aborts the move with the old document restored. Cleanup compares filesystem entries when classifying moves, so case-only and case-equivalent ancestor or descendant moves on a case-insensitive filesystem preserve the new document.The endpoint now lists itself in the RFC 9727 API catalog, so an NLWeb site publishes a catalog without declaring anything in
agents.apis. With--base-urlor a non-local deployment environment URL, the NLWeb endpoint,service-desc, OpenAPI server, schema-feed URLs, MCP endpoint and server card, and agent-interface links use one effective base, including its pathname. Without a publishable base, all of those URLs stay relative for request-origin resolution. Turning OpenAPI off leaves the endpoint listed without a dead description link. A site that declares/askitself keeps its own entry; only a missingservice-descis filled in. Catalog rendering deduplicates entries after URL resolution and merges unique links within matching relations.leadtype generatereports the emitted file asfiles.nlwebOpenapi.leadtype/nlwebexportsbuildAskOpenApiDocument,resolveNlwebOpenApiConfig,resolveAskEndpointLocation,nlwebApiCatalogEntry,withNlwebApiCatalogEntry, andwriteAskOpenApiDocumentfor hosts that build or serve the document themselves.4120312: Give the NLWeb
/askhandler a machine-actionable failure contract.Every non-2xx response now returns an NLWeb-shaped envelope with a leadtype-defined
errorextension: a stableerror.code, a message, and a shorterror.resolution. The bodyless204answer to anOPTIONSpreflight is the only exception, and failures stay JSON even when the request asked for streaming.405keeps itsAllowheader and gains the envelope; both carry the JSON content type and CORS headers.Codes ship as
NLWEB_ERROR_CODES:invalid_json,invalid_request,request_too_large,missing_query,method_not_allowed,artifacts_unavailable, andinternal_error. A non-empty body that fails to parse is nowinvalid_jsoninstead of being silently reported as a missing query, JSON that parses but isn't an/askdocument — a non-object payload, or aquery/query_idof the wrong type — isinvalid_request, and a body over 16 KiB returns413 request_too_largewith a size-specific recovery hint. An emptyPOSTbody still answers from the URL query.artifacts_unavailableandinternal_errorresponses are generic: the artifacts error no longer puts local directory paths in the HTTP body. A URLquery_idis echoed on every failure.POSTfailures also accept the body field, so rejected requests can be correlated without making unsupported methods read their bodies.Artifact validation now requires a content store, recomputes every stored chunk length from its selected content, and rejects empty or malformed posting lists before search. Corrupt indexes fail as
artifacts_unavailableinstead of returning plausible but misranked or empty answers.Search artifact format version 3 persists each transformed chunk's code text alongside its visible content. Runtime validation can therefore recompute every title, heading, body, and code posting count exactly, rejecting missing, forged, or inflated weights. Regenerate version 2 search artifacts with this Leadtype release before deploying
/ask.Core search, browser clients, and MCP artifact loading validate that separately stored index and content artifacts have matching versions, timestamps, and chunk counts. During an in-place v2-to-v3 deployment, a mixed pair falls back to index-only search instead of crashing while the content artifact catches up. Browser clients do not retain that transient mismatch: a later search retries the pair and caches it once both artifacts match.
Request-body parsing is now strict. A malformed or non-object JSON body, an invalid
query, or a non-stringquery_idreturns400even when the URL contains a valid query; previously those bodies could be ignored and answered from URL parameters. Use the optionalonErrorcallback to send the original server-side error to your logger while keeping HTTP responses sanitized. A promise returned by the callback is awaited before the response is sent.POSTJSON bodies are capped at 16 KiB. Unsupported methods return405without consuming their bodies.NlwebAskResponseis now a discriminated union ofNlwebAskAnswerandNlwebAskFailure(narrow with"error" in body), withNlwebAskErrorandNlwebErrorCodeexported alongside it.8e3c431: Add
resolveProject()— one function for the whole config pipeline.Answering "what is this project?" takes five ordered steps: discover the config, apply source-owned inheritance, normalize to canonical names, derive what wasn't authored, and resolve each collection's content directory through the sync cache.
generate,doctor,nav, andcreateDocsProjecteach assembled those by hand, anddoctorandnavshipped with the same two bugs as a result — both skipped inheritance, and both resolved a remote collection'sdiragainst the config directory rather than its checkout.They now read one resolved project. It throws only for a malformed config; everything environmental — an unsynced source, a missing directory, unreadable source config — is a diagnostic carrying a stable id, the owning config field, and the command that fixes it. That split is what lets
doctorreport a problem and keep going whilecreateDocsProjectrefuses to hand a renderer a source it cannot read.createDocsProject()no longer needs a config passed to it: it discoversleadtype.config.*ordocs.config.*the same way the CLI does, so an app with a config file doesn't import it just to hand it back.Config loading also moves out of the CLI into
leadtype's config module, so the runtime no longer reaches through the generate pipeline to answer which config describes a project.5f29292: List real API endpoints in the API catalog, per RFC 9727.
The generated
/.well-known/api-catalogused to describe documentation artifacts: a publisher-root anchor pointing back at itself, plusservice-doc,service-desc, anddescribedby. It contained no link relation that identifies an API, which is the one thing RFC 9727 requires of a catalog. Declare your APIs in site-ownedagents.apisand the catalog now anchors itself, lists every API withitem, and anchors each API to carry its ownservice-desc,service-doc,service-meta, andstatuslinks, plus media type, title, and optional version. Anhrefmay be root-relative, document-relative, or absolute, so a catalog can publish cross-origin APIs.Catalog membership is configured separately from the discovery
Linkheader:agents.apisnames APIs,AgentDiscoveryLinksConfignames artifact paths. Pass the manifest tocreateAgentDiscoveryHeaders()/createAgentDiscoveryLinkHeader()and theapi-cataloglink is advertised only when a catalog was generated.service-descno longer defaults to/docs/agent-readability.jsonon either surface — that file describes documentation readability, not an API contract, and clients that fetched it as an API description got nothing usable. PointserviceDescPath(with the newserviceDescType) at a real OpenAPI document instead.Catalog and discovery URLs reject special-scheme forms that omit their required slashes instead of accepting WHATWG repairs, while valid opaque URIs remain supported and serialize spaces as
%20. API item and relationtypeattributes must be valid printable-ASCII media types at config load and direct rendering boundaries.The zero-argument discovery helpers also stop advertising an API catalog by default, because they cannot know whether one was generated. Pass a manifest when available, or set
apiCatalogPathexplicitly to opt in.createApiCatalogResponse()now servesapplication/linkset+json; profile="https://www.rfc-editor.org/info/rfc9727", adds the self-referentialLink: …; rel="api-catalog"header RFC 9727 expects onHEAD, and returns a bodyless response whenmethodisHEAD.Sites with no configured APIs no longer publish a catalog at all.
leadtype generateandgenerateAgentArtifacts()skip the file and omitfiles.apiCatalogfrom the manifest, and the runtime adapters 404 the well-known route. Accordingly,createApiCatalogResponse()returnsResponse | nullandrenderApiCatalog()throws when no APIs are configured — an empty catalog sends agents looking for APIs that were never declared.fa2b38c: Derive framework adapter params from routes, not collection-local slugs.
All five static-params helpers — Next
createGenerateStaticParams, AstrocreateGetStaticPaths, NuxtcreatePrerenderRoutes, SvelteKitcreateEntries, TanStack StartcreateStaticParams— used to map each page's collection-localslug, which is mount-unaware and drops a collection'sroutePrefix. A changelog page at/changelog/1-0yielded['1-0'], collection indexes yielded duplicate params, and amountsentry rendered pages at URLs the generated sitemap never advertises.Params are now each page's mount-aware
urlPathrelative to a route base, and the load helpers resolve those params back through the same route space:routePrefix, a new property onDocsSource(the catch-all mount'surlPrefix,"/docs"otherwise). For an unmounted single collection the emitted params are byte-identical to before — existingapp/docs/[[...slug]]catch-alls keep working untouched.project.getSource(key)sources carry their collection'sroutePrefix, so one catch-all per collection needs no extra wiring in any adapter — including Nuxt, whosecreatePrerenderRoutespreviously joined slugs ontoconfig.basePath ?? "/docs"and now emits each page's real route.basePathoption on every params/load helper names the prefix the catch-all is actually mounted at;basePath: "/"serves a whole multi-collection project from one site-root catch-all with route-prefixed params.mountsthat move pages within the prefix now round-trip correctly (params match the advertised URL and still load the page). Raw collection-local slugs passed to the load helpers keep resolving unless another page's mounted route claims the same params; in that collision, the route owner wins.2c5a3c6: Tell a broken markdown mirror apart from a page that never existed.
createAgentMarkdownResponse()answered both with the same 200 "Page not found" body. That is right for one of them and wrong for the other. When the manifest lists a page, generation recorded that the page and its mirror exist — so areadMarkdownFilethat comes back empty means a stale build output, a half-deployed asset host, or an adapter whose fetch failed. Reporting that as a missing page tells an agent a live URL is dead, and invites a CDN to cache the claim.That case now answers
500withCache-Control: no-storeand a body that says what actually happened, keeping the markdownContent-Type, canonicalLink, andllms-txtdiscovery headers so the response stays readable, and sending no body forHEAD. The newrenderUnreadableMarkdown()renders it. Adapters inherit this: a Next proxy whose markdown fetch fails for a manifest page now reports a server error instead of a soft miss. The core and framework handler configs also acceptonReadError(target, cause?)for reporting rejected, empty, or invalid manifest-target reads without changing the stable response. Invalid manifest targets omittarget.filePathand include the validation error ascause.Routes with no page behind them — an explicit unknown
.mdpath, or an agent-shaped request for a URL the manifest never listed — return 200 with the recovery body by default. Explicit.mdpaths enter recovery even without a MarkdownAcceptheader or agent user-agent, so mount site-wide middleware after static routes or scope it when the host owns other.mdroutes. Leadtype's generated Agent Skills and agent-card paths are excluded and keep falling through to the static host. The newmissingStatusoption takes200(default) or404for sites where dead-link detection, crawl budgets, or monitoring matter more; the recovery body, canonical URL, and discovery links are identical at either status. Every framework adapter accepts it in its handler config.missingStatusnever touches the other outcomes: an existing page still returns 200, a renamed page 308, a removed page 410, and an unreadable mirror 500.Manifest pages now also carry
markdownFilePath, recording where the generator wrote each markdown mirror relative to the output directory.resolveManifestMarkdownMirrorTarget()returns it asMarkdownMirrorTarget.filePath, so both the docs-tree and bring-your-own-pages generators resolve their actual layouts. Older manifests keep using the previousdocs/<relativePath>.mdfallback.For older bring-your-own-pages manifests, runtime readers retry the legacy root-relative mirror when the guessed
docs/path is missing or throws. The retry is accepted only when its generator-ownedcanonical_urlandlast_updatedfrontmatter match the manifest page, preventing a stale root file from being served for an old mounted docs-tree manifest. A successful retry suppresses the guessed-path error; if both attempts fail, reporting preserves the original failure context.The default-locale manifest may point at separately generated locale manifests without listing their pages itself. Pass those preloaded manifests through
localizedManifests, keyed by locale code, to serve their pages. Cross-locale reads now require an exact page entry from the matching version-1 manifest and use its recordedmarkdownFilePath, includingindex.mdstorage. Missing or mismatched locale manifests and unlisted pages fail closed without probing a guessed path, so stale files left on disk are never served.Generated filesystem paths may contain literal percent signs. Next proxy requests encode each path segment at the URL boundary, while encoded traversal and separator attempts remain rejected during manifest resolution.
Patch Changes
d725444: Route
leadtype generateandleadtype lintthrough the same resolved project as doctor and the runtime.Generate used to assemble a project of its own after sync, and lint read collections without running source-owned inheritance — so a remote collection with
inheritConfig: truewas checked against the defaults, and a stale checkout'sdocs.configcould still be imported before the cache was verified. Both commands now callresolveProjectFromLoaded. Inheritance is applied; a missing or wrong-revision cache is a diagnostic namingleadtype syncrather than a silently empty lint or a module that should never have run.bdc62b3: Keep repeated search-result heading anchors aligned with the table of contents and rendered page. Search indexing now uses the same page-scoped heading allocator and recognizes the same ATX, Setext, and fenced-code boundaries, including headings that do not produce a search chunk.
b392b12: Anchor search result excerpts on the match in the original text.
buildExcerptlocated the query inside the NFKD-normalized text and then sliced the raw text with that offset, but NFKD is not length-preserving (…→...,½→1⁄2,fi→fi, CJK compatibility forms), so the excerpt window drifted past the match — far enough on some chunks that the excerpt came back as just....a9ba44c: Keep table-of-contents and search anchors aligned with rendered Setext and ATX headings containing inline HTML or MDX markup, including indentation and line-ending edge cases.
d517dc5: Make the generated docs-skill and the
leadtype initAGENTS.md pointer describe documentation authoring, not just retrieval. A skill's description is the whole activation signal a client sees before loading it, so "read and search the docs" lost the most common docs task there is — writing, editing, reviewing, and restructuring the pages themselves.f9d2185: Hash
paths.lock.jsonentries from authored source, not generated markdown.The lockfile exists to remember published paths so a later generate can detect renames. Fingerprinting the generated
.mdmirror folded ExtractedTypeTable rows, expanded includes, and converter formatting into every hash — so working on unrelated types, running generate in--watch, or even regenerating with a slightly different pipeline rewrote a committed lockfile. Hashes now come from the source.mdx/.mdbody when one exists (frontmatter still excluded). Pages with no authored source, such as generated OpenAPI reference pages, still hash the mirror.redirectFromis still read from the generated mirror, soafterFrontmattertransformers that add it keep working.The next generate after upgrading rewrites hashes once. Old and new hashes cannot match, so a rename in that same generate will fail as an unmatched disappearance instead of auto-redirecting. Upgrade and rename in two separate generates (two commits). After the first rewrite, authored pages only change the lockfile when their source body or the path set changes.
99fb9a4: Make
leadtype syncconsume the resolved source graph instead of rebuilding one.resolveSources(what doctor andgenerate --jsonreport) andresolveRemoteSources(what sync cloned from) used the same(repository, ref)key with different rules, so a config could look coherent in doctor and then fail at clone time — or worse, look coherent and sync the wrong checkout. Three of those gaps were real: collections that disagreed onsparsemerged silently, a mix of explicit and defaultcacheDirresolved one way in normalize and the other in sync, and a git source namedlocalcollided with the implicit local source.Those checks now live in
resolveSources. A shared acquisition must agree on its sparse set and its cache directory (compared as resolved paths, so spelling out the default location is still valid). Duplicate source ids are rejected at config load.syncSourcesprojects that graph; it no longer decides identity. Configs that already synced keep the same clone layout, manifests, and output. Configs that could not sync now fail when the config loads, with the same specifics, instead of only whenleadtype syncruns.f3891e8: Number table-of-contents anchors across every heading, not just the ones inside
minLevel/maxLevel.createDocsHeadingSluggeris the shared page-wide counter: first-party heading renderers (and a page-scopedcreateMdxHeadingComponents()snippet) now use it sosource.loadPage().tochashes match renderedids. Setext headings are counted too, and emitted ids are reserved soFoo/Foo/Foo-1becomesfoo/foo-1/foo-1-1. A heading filtered out of the TOC still claims its anchor, so with the default 2..3 range a page opening with# Installfollowed by## Installemits#install-1for the h2 instead of the h1's#install.leadtype lintalready collected anchors over the full 1..6 range; the TOC now agrees with it.Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.