docs(site): make the llms.txt artifacts discoverable, and fix the landing-card parser - #23336
docs(site): make the llms.txt artifacts discoverable, and fix the landing-card parser#23336bloxster wants to merge 21 commits into
Conversation
llms.txt and llms-full.txt are published but nothing points at them: they are static files, so Docusaurus never routes them, the default sitemap omits them, and no page links to them. A crawler or agent can only reach them by guessing the path, which in practice means they are never found — a browsing model asked one flag question read ~20 GitHub issue threads instead, none of which are authoritative. Advertise them three ways: - two <link rel="alternate" type="text/plain"> head tags, so every page declares where the machine-readable copies live - createSitemapItems, appending both URLs to the generated sitemap. The sibling ignorePatterns/lastmod options are closure-bound inside defaultCreateSitemapItems, so appended items are neither filtered nor double-processed and /search stays excluded - a reader-facing section on the MCP page, with a pointer from "Why using Erigon?" The section goes on the MCP page rather than "Why using Erigon?" because the latter is a card-grid landing page, whose body generate-llms.py replaces with synthesized bullets — prose added there would render on the site but never reach llms-full.txt. robots.txt is left alone: there is no standard directive for advertising llms.txt, and the Sitemap: line already there now leads to both files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dx34ND1m4ySTJXqMR8kRDX
_LANDING_CARD_RE matched a whole card with one pattern: `[^<]+` for the title and description text, `(?:.*?)` for the gaps, under re.DOTALL. `[^<]+` cannot cross a `<`, so a description containing inline markup (<strong>, <code>) fails to match where it stands — and the engine then scans forward through the permissive gap and matches the *next* card's description and </Link>, swallowing the card in between and pairing a title with the wrong description. This is live in the published corpus, on the page whose job is explaining what makes Erigon different. why-using-erigon has 11 cards; llms-full.txt carried 8: Immutable, Decentralised Data <- Staged Sync's description Flexible Pruning <- RPC Providers' description Staged Sync, RPC Providers & Large Stakers, Developers <- absent Parse in two stages instead: match each <Link> block first, then find the title and description within that block only. A card boundary is then unrepresentable, so no match can cross one. Add a count guard. This failed silently for as long as it existed because `--check` only compares generated output against committed output, which makes a systematic generator bug invariant under it: CI stays green while the corpus is wrong. The guard compares parsed cards against lp-card-title occurrences and fails loudly on a mismatch.
There was a problem hiding this comment.
Pull request overview
Improves discovery of Erigon’s LLM documentation artifacts and fixes landing-card extraction.
Changes:
- Advertises artifacts through page metadata, sitemap entries, and documentation links.
- Parses landing cards within individual
<Link>boundaries. - Adds regression tests and regenerates the full corpus.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
llms-full.txt |
Updates the repository corpus. |
docs/site/static/llms-full.txt |
Updates the deployed corpus. |
docs/site/scripts/test_generate_llms.py |
Adds parser regression tests. |
docs/site/scripts/generate-llms.py |
Fixes card parsing and adds validation. |
docs/site/docusaurus.config.ts |
Adds head and sitemap discovery. |
docs/site/docs/get-started/why-using-erigon.mdx |
Links to LLM artifacts. |
docs/site/docs/fundamentals/mcp.mdx |
Documents artifact usage. |
Suppressed comments (1)
docs/site/docusaurus.config.ts:177
- The MCP page now links both artifacts, so “Nothing else on the web links to them” is inaccurate. The relevant rationale is that static files are omitted from Docusaurus's default sitemap.
// The llms.txt artifacts live in static/, so Docusaurus never routes
// them and the default sitemap omits them. Nothing else on the web
// links to them either, which leaves them unindexable and unreachable
// by search — append them explicitly.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
yperbasis
left a comment
There was a problem hiding this comment.
Requesting changes for three correctness issues:
-
docs/site/docusaurus.config.ts: advertise/llms.txtwithrel="describedby". The llms.txt v2 proposal reservesrel="alternate" type="text/markdown"for a page-specific Markdown representation and definesrel="describedby"for the llms.txt file covering a page: https://llmstxt.org/#proposal. These site-wide aggregate files are not alternate representations of every page, and v2-aware agents may specifically look fordescribedby. Keepllms-full.txtdiscoverable through llms.txt, the visible docs, and the sitemap instead of declaring it a page-wide alternate. -
docs/site/scripts/generate-llms.py: the mismatch guard is bypassed when every card fails extraction.if not cards: return Noneruns beforeexpectedis computed, socollect_pagessilently falls back tostrip_mdx. Computeexpectedbefore the early return and add an all-malformed-grid regression test. This matches the existing unresolved thread: #23336 (comment). -
docs/site/docs/fundamentals/mcp.mdx:llms-full.txtis advertised as containing every documentation page in full, butcollect_pagesreplaces the complete body of every card-grid page with the synthesized card list. For example, the generated Why using Erigon entry omits its introduction, benefits prose, and MCP section. Preserve the non-card prose or describe this as a cleaned and synthesized corpus instead of claiming complete page contents.
Reviewed at eee030a9f1c37e951048dc8420c5af6d61fcce63. The 81 documentation-script tests, artifact check, diff check, and GitHub docs build are green.
… claims Review feedback from @yperbasis and Copilot on #23336. Five fixes. 1. Advertise llms.txt with rel="describedby", not rel="alternate". The llmstxt.org proposal defines describedby for the llms.txt file that covers a page, and reserves alternate + text/markdown for a *per-page* Markdown representation. A site-wide index is not an alternate representation of every page, and v2-aware agents look for describedby. llms-full.txt is no longer advertised in head at all: it describes no single page. It stays discoverable through llms.txt, the sitemap, and the MCP docs page. 2. Close a hole in the card-count guard. `if not cards: return None` ran before the count was taken, so a grid where *every* card failed to parse was indistinguishable from an ordinary prose page: the caller fell back to strip_mdx and the guard never ran — silently degrading the exact case it exists to catch. Count first, parse second, and return None only when the page has no cards at all. 3. Stop claiming llms-full.txt holds "every documentation page, in full". It does not: synthesize_landing replaces the whole body of a card-grid page with its card list, so why-using-erigon loses its introduction and prose. Describe the corpus as cleaned rather than verbatim, and say what is dropped. 4. Stop grouping llms.txt with llms-full.txt as "the whole documentation as one plain-text file" on why-using-erigon. llms.txt is only an index. 5. Drop "nothing else links to them" from the config comment — this PR adds the MCP page links, which makes it false. Also refresh the stated file size, 420 KB -> 430 KB.
|
@yperbasis all three addressed in 1dd741e, rebased onto your merge of 1. 2. Guard hole — confirmed before fixing. With every card malformed, 3. "Every documentation page, in full" — confirmed and materially false: Verified after the rebase: One thing worth flagging: #23335 already merged into |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Suppressed comments (1)
docs/site/docusaurus.config.ts:100
- The PR description still promises two
rel="alternate" type="text/plain"head tags on every page, while this now emits onedescribedbylink and deliberately omitsllms-full.txt. Please either restore the advertised tags or update the description and verification so they match the shipped discovery contract.
rel: 'describedby',
href: 'https://docs.erigon.tech/llms.txt',
yperbasis
left a comment
There was a problem hiding this comment.
Requesting changes for three remaining correctness issues:
- The landing-card mismatch guard still misses cards whose title marker disappears.
- The advertised llms.txt index does not directly expose llms-full.txt.
- The new completeness wording ignores the published archived documentation versions.
Reviewed at 1dd741e8c1. Targeted validation is green: 82 documentation tests and the llms artifact drift check pass.
…, scope Follow-up review from @yperbasis on #23336. Three findings, all confirmed by reproduction before fixing. 1. The guard counted the marker it was validating. `expected` came from `lp-card-title`, so if a card lost or renamed that marker the count shrank in step with the loss it was meant to detect: verified that a two-card grid with one renamed marker emitted one bullet and raised nothing. If every marker changed, `expected` hit zero and the page fell back to strip_mdx. Count `lp-card` containers instead — the wrapper is not consumed by the parse, so the two signals stay independent. Attributes are now matched order-independently and `to=` is read separately, which the container match no longer pins down. 2. llms.txt had no route to llms-full.txt. Neither committed index contained the string at all, so once the llms-full head tag was removed, an agent following rel="describedby" reached an index with no way to find the full corpus. The generator now emits that link, and both copies are regenerated. 3. "Every documentation page" was still wrong. SECTIONS scans only docs/ and help-center/, while the site also publishes v3.3 and v3.4 from versioned_docs/ — neither artifact contains those URLs. Say current documentation, and state the exclusion outright. Two new tests: a renamed title marker must raise rather than be absorbed (verified to fail against the previous count), and card attributes must parse in either order.
…, scope Follow-up review from @yperbasis on #23336. Three findings, all confirmed by reproduction before fixing. 1. The guard counted the marker it was validating. `expected` came from `lp-card-title`, so if a card lost or renamed that marker the count shrank in step with the loss it was meant to detect: verified that a two-card grid with one renamed marker emitted one bullet and raised nothing. If every marker changed, `expected` hit zero and the page fell back to strip_mdx. Count `lp-card` containers instead — the wrapper is not consumed by the parse, so the two signals stay independent. Attributes are now matched order-independently and `to=` is read separately, which the container match no longer pins down. 2. llms.txt had no route to llms-full.txt. Neither committed index contained the string at all, so once the llms-full head tag was removed, an agent following rel="describedby" reached an index with no way to find the full corpus. The generator now emits that link, and both copies are regenerated. 3. "Every documentation page" was still wrong. SECTIONS scans only docs/ and help-center/, while the site also publishes v3.3 and v3.4 from versioned_docs/ — neither artifact contains those URLs. Say current documentation, and state the exclusion outright. Two new tests: a renamed title marker must raise rather than be absorbed (verified to fail against the previous count), and card attributes must parse in either order.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
Suppressed comments (2)
docs/site/scripts/generate-llms.py:299
- This block records the old regex's failure sequence and test-level details rather than the lasting invariant, making it easy for the explanation to become stale. Keep only the card-boundary and independent-count rationale; the regression tests preserve the specific history.
# Parsed in two stages, deliberately. A single pattern spanning the whole card
# cannot express "and never cross into the next card": with `[^<]+` for the text
# and `.*?` for the gaps, a description containing inline markup (`<strong>`,
# `<code>`) fails to match locally, and the engine then scans forward and pairs
# the title with the *next* card's description — silently swallowing the card in
docs/site/docusaurus.config.ts:100
- The PR description and verification still promise two
rel="alternate" type="text/plain"tags for both artifacts, but this implementation intentionally emits onerel="describedby"link forllms.txt. Please update the PR description and verification claims (including the parser guard's stated count source) to match the final implementation, or restore the advertised behavior.
rel: 'describedby',
href: 'https://docs.erigon.tech/llms.txt',
yperbasis
left a comment
There was a problem hiding this comment.
Requesting changes for three remaining correctness issues and one repository-guideline issue.
Reviewed at a97ed68440. Targeted validation is green: 84 documentation tests and the llms artifact drift check pass; the GitHub docs-site build is also green.
… marker Third review round on #23336. The `describedby` link sat in config-level `headTags`, which Docusaurus emits on every route — /v3.3/** and /v3.4/** included. generate-llms.py never walks versioned_docs, so llms.txt covers only the current docs and the help center: an agent reading an archived page was pointed at current, version-specific guidance. It moves to src/theme/Root.tsx, which drops it on archived routes. Verified against a full build: present on /, /fundamentals/**, /help-center, absent on /v3.3/** and /v3.4/**. The expected card count still came from the wrapper alone. That fixed the renamed-title case and left its mirror open: rename `lp-card` and the card vanishes from `containers` and from `expected` together, so one renamed wrapper silently omits a card and a wholesale rename returns None and falls back to strip_mdx. The count is now the largest of three independent markers — wrapper, title, desc — so no single rename can shrink both sides of the guard. Card fields were also accepted empty: `(.*?)` matches "" and `_card_text` reduces markup-only content to "", while the match object stays truthy, so a card emitted as `- [](url): ` passed the count guard. Empty flattened title or description now fails the card, which the guard reports. Adds four regression tests (renamed wrapper, wholesale wrapper rename, empty title, markup-only description) and trims the parser comment to the two invariants it exists to state, per AGENTS.md.
|
Head moved to The only conflict was positional: Artifacts regenerated against the merged docs — 72 pages now rather than 73, which is
|
yperbasis
left a comment
There was a problem hiding this comment.
Requesting changes for four parser-correctness issues and one repository-guideline issue.
-
[P2] Scope the structural card counter to landing pages.
_card_shaped_link_countmakesexpected=1for an ordinary<Link to="/details"><div className="callout">...</div></Link>on any documentation page.synthesize_landingthen raisesparsed 0 of 1instead of returningNone, so valid non-landing MDX fails the documentation build. -
[P2] Mask JSX expressions before structural scans.
_code_maskedblanks code and JSX comments but leaves expression strings visible. A valid card description such asUse {"</div>"} literally, then keep this text.renders completely in MDX, but_div_span_endtreats the string as a real closing tag and emits onlyUse {". All five counts still agree, so the guard does not detect the truncation. The extraction path must handle the same expression regions. -
[P2] Recognize fenced blocks nested under Markdown containers.
_FENCE_OPEN_REaccepts only zero to three spaces at the raw line start, but the installed MDX compiler accepts a four-space-indented fence under a list. With a blank line after that opener, a fencedlp-cardexample remains visible to every structural scan and is emitted as a real## Sectionsbullet inside a dangling fence. The repository already uses four-space-indented fences. -
[P2] Preserve the rest of a hero block.
_landing_heroemits only the first paragraph whileproseremoves the complete hero span. A second visible paragraph, link, or call-to-action therefore disappears, contrary to the new document-order and retained-prose contract. Exclude only content that was actually emitted, or flatten the remainder separately. -
[P3] Remove forensic narratives from test docstrings. For example,
test_hero_with_inline_markup_does_not_swallow_the_pagerecords the old regex and its exact failure sequence. The repository guidelines require docstrings to state the invariant and keep this history in the commit or PR description.
The existing tilde-fence thread at discussion_r3851145007 is also valid and unresolved.
Reviewed at 0fc5956e3dad273ad5febbf6ef8120f0a6b40fcf. The 120 documentation-script tests, four-artifact drift check, and git diff --check pass. Focused reproductions confirm the four behavior issues above, and the relevant snippets compile as valid MDX with the repository dependency.
`_code_masked` masked fences in a pass of their own, before comments existed,
so a lone fence marker inside `{/* ... */}` — comment text to MDX — opened a
phantom block and blanked the live markup below it. Every count signal reads
that same mask, so both sides of the guard shrank together and it stayed
silent: a grid following such a comment was dropped from the link list and
re-emitted as flattened prose, or the page lost its cards entirely.
Fences, code spans and comments are now one left-to-right alternation, earliest
opener wins, which is how MDX resolves them. Two bounds keep that faithful. A
span's closer is bounded by the next fence as well as by the block, because a
fenced block interrupts the paragraph. And a comment ends at the balanced `}`
that MDX actually closes on — `*/ }` and `*/ /* b */ }` close one as surely as
`*/}` does — because matching only the literal three-character spelling walks
past the real close and pairs with a `*/}` inside a later fence, losing every
card between the two silently.
Six tests. Two fail against 0fc5956 and pin the phantom-fence defect; one
covers the new helper; three guard behaviour this rewrite could regress and
pass against the parent by design. The fence bound is one of those three, and
it is load-bearing: relaxing it to the end of the document fails that test and
nothing else.
126 script tests, the four-artifact drift check and ruff are green. The mask is
byte-identical to the parent's on all 199 md/mdx files in the tree, and all four
artifacts are unchanged, so no shipped page is affected either way.
b7dfdf4 to
e687419
Compare
Five review points, all in the parser.
**One fence scanner.** `_code_masked` knew `~~~` and CRLF fences; the four
fence-aware passes behind `strip_mdx` each re-implemented the test as
`lstrip().startswith("```")`, so a tilde-fenced example was code to the
structural scan and live markup to the stripper, and ```` ```jsx ```` still
closed an open ```` ```md ````. They now share `_fence_flags`.
**Fences nest under list items.** The three-space indent cap is gone: a fence
inside a list item is indented to its container's content column, and this tree
has 36 such fences. Capping it made them ordinary text, which is why unifying
the scanners had to wait for this — with the cap in place, unification stripped
`<your-datadir>` out of a jwt-secret example and emptied a `--data '{...}'`
payload.
**A shape is not a landing page.** `_card_shaped_link_count` counted any `<Link>`
wrapping any `<div>` and could set `expected` on its own, so the ordinary
call-to-action `<Link><div>Label</div></Link>` made `synthesize_landing` raise
"parsed 0 of 1" on prose pages holding no cards, against its documented
contract. The class-keyed signals now decide whether a page is a landing page
and the shape-only one cross-checks the count. A rename of `lp-card` under an
intact grid still trips the guard; renaming the whole namespace now reads as
prose, which `--check` catches because the corpus changes.
**Expressions are not structure.** The mask now blanks `{...}` too, so a quoted
`</div>` inside one no longer bounds a card field early and truncates it. MDX
renders a string expression as its characters, so `_card_text` holds that text
aside before the tag passes rather than losing it.
**The hero keeps its whole block.** `_landing_hero` emitted only the first
paragraph while `prose` dropped the entire span, so a second paragraph or a call
to action vanished. Every part is emitted now, the `<h1>` aside, which the page
title already carries.
Forensic narrative is out of the test docstrings it had crept into.
130 script tests, the four-artifact drift check and ruff are green. All four
artifacts are byte-identical and the mask preserves offsets on all 199 md/mdx
files, so no shipped page moves.
|
All five addressed in 1 — shape is not a landing page. The class-keyed signals now decide whether a page is a landing page; the shape-only signal only cross-checks the count once one of them has fired. 2 — expressions masked. The structural mask blanks 3 — indent cap removed. This one has an ordering constraint worth recording. Removing the cap had to come before unifying the fence scanners: with the cap in place, unification reclassified this tree's 36 list-nested four-space fences as ordinary text, which stripped 4 — hero. 5 — docstrings. Narrative removed from The tilde thread is fixed by the same scanner unification as point 3. The older closing-fence thread was already fixed in 130 script tests, four-artifact drift check, ruff and Two things I did not fix, so they are not silently pending. |
|
@yperbasis re-requested — current head is Since my last comment: I ran a full All five points and both Copilot threads are done and resolved. Two things I deliberately left, noted at the end of the description rather than left silent: |
Two review points, both in what the previous commit changed.
The prose stripper got the shared fence scanner but not the mask's precedence
rule, so a fence marker inside `{/* ... */}` still opened a block there and
`strip_mdx` dropped every visible line after it. `_fence_flags` now reads the
fenced regions off the same alternation the mask uses, which is where comments
already win over fences, so the two paths cannot disagree again — one scanner
was not enough, it had to be one decision.
That table keeps the line terminators. Measuring lines with `splitlines()` while
consuming offsets built on `split("\n")` drifts a character per line under CRLF,
which flagged the line after a fence as fenced and leaked an `import` into the
corpus.
The hero was emitted before the page walk began, which hoisted it above any
prose or card that preceded it in the source. It is now substituted into the
prose segment that contains it, so the document order the walk exists to
preserve also covers the hero.
133 script tests, the four-artifact drift check and ruff are green. All three
new tests fail against the previous head. The artifacts stay byte-identical and
the mask preserves offsets, so no shipped page moves.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
docs/site/scripts/generate-llms.py:462
_jsx_expr_endskips block comments but not valid JavaScript//comments. A}inside a line comment is therefore treated as the expression close, leaving card-shaped JSX later in the same MDX expression unmasked; it can then be parsed as a real card or inflate the count guard. Skip line comments before counting braces and add a regression case.
if text.startswith('/*', i):
close = text.find('*/', i + 2)
if close == -1:
return -1
i = close + 2
yperbasis
left a comment
There was a problem hiding this comment.
I found two issues that should be fixed before merge:
-
[P2] Scope the shape-only count to card grids (
docs/site/scripts/generate-llms.py:700)_card_shaped_link_count(mask)scans the entire page. Once a reallp-gridmakesnamednonzero, an unrelated<Link><div>…</div></Link>call-to-action in the hero or surrounding prose increasesexpected. A one-card grid followed by that normal CTA raisesparsed 1 of 2 expected, so the artifacts cannot be regenerated. The preceding logic already identifies this as the CTA idiom, but the gate only protects pages that contain no cards. Please scope this count to the matched grid spans, or otherwise exclude non-grid links, and add a mixed grid-plus-CTA regression test. -
[P2] Preserve CommonMark indentation semantics when detecting fences (
docs/site/scripts/generate-llms.py:110)^[ \t]*treats a backtick run at any indentation as a fence. At document root, however, four spaces make it an indented code block, not a fenced block (CommonMark section 4.5). With a literal four-space-indented backtick fence marker followed by a live card grid, this scanner treats the line as an unterminated fence through EOF. The live grid is masked, every count signal disappears, and regeneration quietly emits raw card markup instead of the synthesized list. Please make fence recognition container-aware: allow deeper raw indentation only after accounting for a list or blockquote container, while retaining the three-space limit at document root, and add this case as a regression test.
…ontainer The shape-only card count scanned the whole page, so a <Link> wrapping a <div> in the prose — the call-to-action idiom — raised the expected count and aborted regeneration of a well-formed page. Count the boxes instead: a card carries a title and a description, a button carries a label. That reads no class name, so a renamed card is still cross-checked against its siblings, and a call to action stays out of the count whether it stands alone or in a row. Fence detection accepted a backtick run at any indentation. At document root four spaces make an indented code block (CommonMark 4.5), so such a line was read as an unterminated fence that masked the rest of the document and left a grid below it to regenerate as raw card markup. Track the container instead and allow three columns past its content column, which is where this tree's fences already sit inside list items. Containers close only at a block boundary: a paragraph wrapping to a lower column inside a list item is a lazy continuation and leaves the item open, so the fence indented under it stays a fence. A thematic break borrows the list markers but opens nothing, a blank line ends a blockquote, and the gap after a marker is measured from the column after it so a tab lands on the next stop from there.
A thematic break and a container marker are matched per line, so both need the `\r?$` every other line-anchored pattern here already carries: in a CRLF document `- - -\r` fell through to the container match and pushed a content column, which is what legitimized an indented backtick run as a fence. The gap after a marker is whitespace only, so a matched terminator no longer counts toward it. Also drop a re.DOTALL that no longer has a `.` to apply to.
|
Both fixed in Scope the shape-only count. Not scoped to the grid spans: that keys it to The docstring's claim was also wrong and is corrected: Container-aware fences. Three details, each with a regression test:
Left alone: Validation at |
yperbasis
left a comment
There was a problem hiding this comment.
Requesting changes for four corpus-correctness issues. The committed artifact check, all 144 documentation-script tests, Ruff, and git diff --check pass, but four focused valid-MDX regression cases fail at this head.
| while stack and indent < stack[-1][0]: | ||
| stack.pop() | ||
| boundary = False | ||
| if _fence_marker(line): |
There was a problem hiding this comment.
[P2] Recognize fences after Markdown container markers. _fence_marker receives the raw line, so > ```json and - ```json are not marked as fenced. strip_mdx then treats the payload as live MDX; in a direct reproduction the blockquoted {"jsonrpc":"2.0"} line becomes empty, and card-shaped JSX may be synthesized. Parse the container prefix before testing the fence and cover both blockquote and same-line list forms.
There was a problem hiding this comment.
Fixed in bf75b48. Reproduced both forms first — the blockquoted {"jsonrpc":"2.0"} line came back empty exactly as you described.
_peel_containers now blanks each container marker out to the column its content starts at, so the fence test sees the line MDX sees while absolute columns stay valid for the indent comparison. Both the blockquote and same-line list forms are covered.
Two things fell out of it that are worth flagging:
- A closer must be matched at the opener’s blockquote depth, not its marker count. Blockquotes continue by repeating
>; a list item continues by indentation and its closer carries no marker. Counting markers left a- ```jsonfence open to end of file, masking every live line below. - A blockquote with no space after
>is still not recognized (_CONTAINER_RErequires whitespace or EOL). That is pre-existing and I left it alone: a bare>at line start is also how a multi-line JSX tag closes, so recognizing it risks false positives on these pages. Happy to take it separately if you want it.
| text = _BREAK_TAG_RE.sub(' ', text) | ||
| # A tag name follows the `<` immediately, so a bare `<` in prose keeps the | ||
| # text after it instead of being read as a tag that swallows a code span. | ||
| text = re.sub(r'\s+', ' ', re.sub(r'</?[A-Za-z][^>]*>', '', text)).strip() |
There was a problem hiding this comment.
[P2] Make tag stripping quote-aware. [^>]* stops at a > inside a quoted JSX attribute. A valid description Before <span title="a > b">inside</span> after. becomes Before b">inside after. while every card count still passes. Scan tags with quoted values correctly rather than terminating at the first greater-than character.
There was a problem hiding this comment.
Fixed in bf75b48. Your example produced Before b">inside after. as written.
Tag matching now scans quoted attribute values, including backslash escapes, so it ends at the first > outside a value. The bare-<-in-prose behaviour is unchanged and still tested.
One measurement worth recording: the new pattern is the same complexity class as the old [^>]* on unclosed-tag input but with a roughly 30× constant. I checked reachability rather than optimising blind — the largest lp-card-desc field in the corpus is 201 bytes and the full generator runs in 0.28s, so it needs tens of KB inside a single card field to matter. I did not rewrite the matcher into a scanner here; say the word if you would rather have that than the regex.
Seven defects, each reproduced before it was fixed:
- A fence opening on a container marker's own line (`> ```json`,
`- ```json`) is now recognized; its body was read as live MDX.
- A marker followed by five or more spaces holds an indented code block,
not a fence, so the peeled tail keeps its real column.
- Container columns come from live structure only. A marker inside a fence
or a JSX comment left a stale column that made a later root-indented
backtick line look like an unterminated fence, masking the grid below.
- Comment detection resolves backtick-versus-comment precedence through
_inline_regions, so a literal `{/*` in a code span opens nothing. A raw
scan for the marker deleted real fenced code and prose from the corpus
on pages that document MDX comments.
- A code span is bounded by any block boundary, not just a blank line:
ATX heading, setext underline, fence or thematic break.
- A fence four or more columns in is an indented code block and does not
bound a span.
- Tag stripping scans quoted attribute values, so a `>` inside one no
longer ends the tag and spill the rest into the prose.
Sixteen regression tests, each failing before its fix.
Proposal: read page text from the built site instead of parsing MDXI've moved this to draft because I think your four open P2 threads point at something the current approach can't fix, and I'd rather put the alternative in front of you than send another round of patches. WhyThe generator reconstructs what MDX renders by hand — CommonMark block parsing (fences inside containers, code spans vs block boundaries, HTML blocks, setext underlines, indented code blocks, closer depth) plus JSX — to recover the landing-page card grids. Every review round on this PR has found another rule it gets wrong. That isn't bad luck: the surface is a spec being reimplemented, so there is no round at which it is finished. Docusaurus has already done that work by build time. Components are expanded, links are resolved to absolute paths, build-time variables are substituted, and the page body sits in one So the four threads become moot rather than fixed: the code they refer to is gone. What it measuresAgainst the previous committed output, 72 pages both sides, none dropped:
Chrome subtrees (breadcrumbs, mobile TOC, footer, heading anchors) are dropped by their stable Costs, stated plainly
On the review historyI put this through several independent reviews before proposing it, and they found real defects in my own rewrite, including two that were briefly in the committed artifact: every code fence double-spaced (Docusaurus wraps each code line in a Both slipped through for the same reason, and it is the part I'd flag hardest: the tests were synthetic, and 66 tests, ruff clean, What I'd like from youMainly whether you see a reason to keep parsing source that I'm missing — a case where the built HTML is the wrong input. If you'd rather not take the build dependency, that's a fair reason to say no, and I'll go back to fixing the four threads on the current parser instead. If the direction is fine, the sequencing question is whether this should land on The implementation is ready but I have not pushed it — this branch still holds the parser fixes you last reviewed, and I didn't want to overwrite those before you've weighed in. Say the word and I'll push it here for you to read as a diff. |
Makes the generated
llms.txtartifacts discoverable, and fixes the landing-page card parser that feeds them.Discoverability — advertises
/llms.txtwithrel="describedby"(the relation the llms.txt v2 proposal defines for a file describing a page;alternate/text/markdownis reserved for a page-specific Markdown representation). The descriptor is scoped away from archived doc versions.llms-full.txtstays reachable throughllms.txt, the visible docs, and the sitemap. Adds an MCP page describing both artifacts.Card parser (
docs/site/scripts/generate-llms.py) — landing pages are JSX grids, and the generator has to tell code apart from page structure before flattening them. Fixes, each with a regression test:> ```json,- ```json) is recognized, and its closer is matched at the opener's blockquote depth — a blockquoted line does not close a root fence, and a list-item fence closes on its marker-less closer instead of running to end of file._inline_regions, so a literal{/*inside a code span opens nothing.>inside one no longer ends the tag.strip_mdxsilently. The guard raises rather than logs, since--checkcompares generated output against committed output and would not catch a systematic regression.The committed artifacts are unchanged by the parser fixes; they close latent cases, not current output.