Skip to content

DOC-6939 Improve the AI-facing JSON and Markdown feed output - #3754

Merged
andy-stark-redis merged 9 commits into
mainfrom
DOC-6939
Aug 5, 2026
Merged

DOC-6939 Improve the AI-facing JSON and Markdown feed output#3754
andy-stark-redis merged 9 commits into
mainfrom
DOC-6939

Conversation

@andy-stark-redis

@andy-stark-redis andy-stark-redis commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Fixes content loss and id defects in the AI-facing JSON and Markdown output, from DOC-6939.

Why

The applied AI team assessed our three machine-readable docs sources as ingestion candidates for the redis.io MCP server (redis_docs_source_assessment.pdf, measured 31 July 2026 against redis/docs@4d51073). This PR addresses the defects that landed on us as publisher, plus several bigger ones found while diagnosing them.

What changed

Content that was missing from the feed entirely

Defect Before After
Pages losing their table-children table from AI output 101 0
Pages with a fused metadata-block closing fence 1,223 0
Sections dropped from an RDI page by an unclosed fence 3 0
  • The table-children shortcode had no handler in the AI output pipeline, so it was stripped. On the release-notes index pages the table is the body, so those pages reached the feed empty or with a dangling colon, and the table's version columns (min-version-db, min-version-rs) appear nowhere else in the feed. New markdown-table-children.html partial renders it as a Markdown table.
  • Six of those 101 leaked the raw shortcode instead. Cause: the strip pattern used a [^>]* class, which can't cross the literal > in columnNames="…<br/>…". Now non-greedy.
  • In single.md/section.md, a trim marker was eating the newline after the metadata block's closing fence, so content started on the fence line. A closing fence may be followed only by whitespace, so it stopped closing anything.
  • integrate/redis-data-integration/installation/upgrade had a fenced block inside a warning shortcode that was never closed. The HTML renders fine (Hugo closes it at the shortcode boundary), but the transform's naive backtick pairing treated two large spans as code and silently dropped three real sections.

Section and example ids

Defect Before After
Pages with duplicate section ids 440 0
Pages with duplicate example ids 131 0
Sections with {#anchor} leaking into the title 4,363 0
Section ids matching the rendered page anchor 79.6% 91.9%
Placeholder/example mismatches (per section object) 739 1
  • slugify discards punctuation, so headings differing only in punctuation collided — commands/xread has The special `$` ID and The special `+` ID, both becoming the-special-id, merging their examples into one bucket. Ids are now disambiguated with Hugo's -1/-2 convention.
  • Headings using Hugo's explicit anchor syntax weren't parsed, so ## Title {#during-install} published the literal {#…} in the user-visible title and an id of title-during-install, breaking the deep link. The anchor is now used verbatim as the id.
  • An empty fenced block emitted a [code example] placeholder with no corresponding example.
  • The remaining single mismatch is not a defect: ai-agent-resources documents the feed format and writes the literal token in prose.

Metadata table of contents

The TOC in the Markdown metadata block had the same anchor and duplicate-id defects as sections[], and additionally built entries from ## lines inside fenced code blocks — so a YAML comment reading ## the defaults are commented was published as a navigation entry. Both are fixed, and the dedup bound is now the heading count rather than an arbitrary 50.

Build robustness

transform_json_sections.ts consumes the content field and doesn't write it back, so a second run rewrote every content page as an empty index page, silently. It now skips already-transformed files and reports the count.

What a reviewer should focus on

The line count is modest but the output blast radius isn't:

  • sections[].id values change on 440 pages (dedup) and 4,363 sections (explicit anchors). Ids are a consumer-visible contract and are used for url#section-id deep links.
  • Every page's .md output gains a separator after the metadata block (5,732 files).
  • 101 pages gain a table in their JSON/Markdown output that wasn't there before.
  • One deliberate id decision, reversed once during review: where a page has intro prose and an ## Overview heading, the real heading keeps overview and the synthetic intro takes overview-1. Hugo's anchor and the TOC both give overview to the real heading, so doing it the other way mis-resolved #overview citations on 166 pages.

Verification

Full hugo build plus transform, before and after, diffed per page:

  • 5,631 of 5,732 pages byte-identical for the table-children change; the 101 that changed are exactly the ones using the shortcode, 0 collateral
  • 5,732 of 5,732 identical outside the edited region for the fence change
  • table-children Markdown row counts match the HTML table row counts on 101/101 pages, including both limitTags filters and the one from= indirection
  • 0 residual shortcodes across 5,732 JSON pages and all .md output
  • content_hash still reproduces from the published algorithm for all 5,687 content pages
  • transform run twice: second run reports 5,732 skipped, 0 transformed, output intact

One assessment finding is refuted

Their observation 04 says content_hash coverage is undocumented and may exclude code, so a code-only edit wouldn't change the hash. Both halves are wrong: the algorithm is published on ai-agent-resources.md, and it hashes the summary plus every section text plus every example code block, reproducing the published hash for all 5,687 content pages including the 1,211 carrying examples. Consumers can gate refresh on content_hash alone. That item has been withdrawn on the ticket rather than worked.

Not in this PR

  • Aligning slugify with Hugo's anchor algorithm, which would close the remaining 8.1% anchor gap but moves ids well beyond the pages touched here (ticket item C7)
  • The TOC's emphasis-stripping regexes corrupting code-like headings — 841 entries across 263 pages publish incorrect API signatures, e.g. **kwargs becoming *kwargs and redis_url becoming redisurl (ticket item D3, filed from this work)
  • Documentation items B2/B3 (taxonomy-exclusion rule, role vocabulary), which change no output
  • The feed-schema additions in section C, which need a decision first

Four rounds of Cursor Bugbot review are addressed. Two of its findings were valid and one changed a decision I'd made deliberately; the reasoning is in the individual commit messages.

🤖 Generated with Claude Code

The page claimed both feed files contain ~4,100 documents; the live feed has
2,643 (2,611 content + 32 index). The applied AI team's source assessment
flagged it because a consumer taking the figure at face value concludes a
third of the corpus is missing. Replaced it with a growth-safe phrasing
instead of the true figure on purpose — the count climbs by roughly one a day
(2,637 on their 31 July run, 2,643 five days later), so a hardcoded exact
number is precisely what rotted last time.

While verifying the feed I also refuted their observation 04, which claimed
content_hash coverage is undocumented and might exclude code. Both halves are
wrong, and the evidence is on this same page. The algorithm is published a few
sections further down, and it hashes the summary plus every section text plus
every example code block, reproducing the published hash for all 2,611 content
pages including the 1,211 that carry examples. So a code-only edit does change
the hash, and consumers can safely gate refresh on it alone rather than
re-fetching to confirm. Their other four measurable claims reproduced exactly
against today's feed (2,119 distinct ids for 2,643 URLs, 15 role values, 103
placeholder mismatches, 6 sections with residual shortcodes), so the rest of
the assessment is trustworthy where it is measurable.

Learned: content_hash provably covers examples[].code, so the feed hash is safe to gate a refresh on
Constraint: the hash algorithm published on this page must track computeContentHash in build/transform_json_sections.ts
Directive: do not replace "more than 2,600" with an exact count — an exact figure is what went stale here
Rejected: emit the live count from build/generate_ndjson.py into a Hugo data file | needs a build change, disproportionate to a one-line docs fix
Ticket: DOC-6939
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

DOC-6939

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🧠 Redis Memory

Found 5 related items from repository history:

Memory updated at 8323ac4

Adds a markdown-table-children partial so the AI-facing outputs render the
same table the HTML shortcode does, and fixes the shortcode-strip regex that
let six of them leak raw into the feed.

The reported symptom was six release-notes pages carrying unexpanded
shortcodes, which looked like a shortcode missing from the pipeline's
allowlist. It isn't. 101 pages use table-children, and the six that leak are
exactly the six whose attributes contain a literal ">" -- their columnNames
use <br/> to wrap header text. The strip pattern was a [^>]* class, which
cannot cross that ">", so the match fails and the whole shortcode survives
verbatim. Made it non-greedy to the first ">}}" instead.

Fixing the strip alone would have been the wrong fix, and finding out why is
the useful part. The other 95 pages were being stripped "successfully" and
silently losing the entire table -- and on the release-notes index pages the
table IS the body, so those pages reached the feed with either nothing or a
dangling colon ("Here are the most recent changes for Redis Insight:" followed
by no changes). The table's version columns are worse: children[] carries only
id, summary, title and url, so the minimum Redis and cluster versions existed
nowhere else in the feed. The leak was the visible 6% of a defect affecting all
101 pages, and only the leak was reported because a shortcode-shaped regex is
what anyone checks for.

The leading newline in the replacement is deliberate and load-bearing. Without
it, on pages whose whole body is the shortcode, the table opens on the same
line as the closing fence of the Markdown metadata block, which stops that
being a valid closing fence and swallows the table into the code block. That
pre-existing fusing affects 1,383 pages site-wide and is not fixed here; the
newline just keeps this change from adding to it, and happens to clear 7.

Verified by building the site twice, before and after, and diffing every
page's JSON content: 5,631 byte-identical, 101 changed, 0 changed that don't
use the shortcode. Markdown row counts match the HTML table row counts on all
101, including both limitTags filters and the one from= indirection.

Learned: the six leaking pages were the visible edge of a defect dropping the table from all 101, so the reported symptom understated it by 16x
Constraint: never match shortcodes with a [^>] class -- attribute values legitimately contain > (columnNames uses <br/>)
Directive: keep the leading newline in the table replacement or the table gets swallowed by the metadata block's closing fence
Rejected: fix only the strip regex | makes the 6 consistent with the other 95 but leaves all 101 tables missing from AI output
Gaps: the 1,383-page fused closing fence in .md output is pre-existing, unfixed, and needs its own change
Ticket: DOC-6939
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🧠 Redis Memory

Found 5 related items from repository history:

Memory updated at 9d1b68d

Comment thread layouts/partials/markdown-table-children.html
Comment thread layouts/partials/markdown-table-children.html
…tput

The closing fence of the metadata block was landing on the same line as the
first line of page content, on 1,223 of 5,732 pages. A closing fence may be
followed only by whitespace, so it stopped closing anything: 860 pages went out
with an unterminated code block, and on the rest a later bare fence closed it
but inverted the open/close role of every fence after it, so real code examples
and prose swapped places.

The cause is whitespace trimming, not the content. Both single.md and section.md
had the closing fence, a literal blank line, and then a template comment opening
with a left-trim marker. That trim eats all preceding whitespace, so it removed
the blank line and the fence's own newline, leaving the fence flush against the
content. The two now emit the separator explicitly instead of relying on literal
blank lines that a trim marker can reach.

Two dead ends worth recording. Simply dropping the dash to stop the trim does
not compile: a Go template comment must open as "{{/*" or "{{- /*", and "{{ /*"
is a parse error. And even with the valid no-trim form, the following line's own
left-trim reaches back through the blank line and re-fuses the fence, so no
arrangement of literal whitespace is safe here.

I also had the cause wrong at first, in a way worth flagging: the injected code
examples legend looked like the trigger, since it is what appears fused on the
client pages. It accounts for only 120 of the 1,223. The trailing text is
whatever each page's content happens to begin with, so this was never specific
to pages carrying code examples.

Verified by diffing the Markdown output of a full build before and after, with
the edited region normalised on both sides: 5,732 files identical outside it,
zero collateral change. Fused fences 1,223 to 0, unterminated blocks 860 to 10.
Note that Python-Markdown is a misleading oracle here because it is not
CommonMark-compliant and reported the fix as making things worse; the number
above comes from applying the fence rules directly.

Learned: a Go template comment must open as {{/* or {{- /*, and "{{ /*" is a parse error, so a trim marker cannot be removed from a comment without restructuring
Constraint: never separate the metadata block from page content with literal blank lines -- a following trim marker will reach back and eat them, so emit the newlines explicitly
Directive: verify Hugo output against a clean build, as hugo does not empty public/ and stale alias files survive to produce phantom findings
Rejected: dropping the left-trim marker from the comment | "{{ /*" does not parse, and the valid form still leaves the next line's trim to re-fuse the fence
Gaps: 10 pages still emit an unterminated fence from source authoring errors (7 kubernetes logs pages and rdi installation/ha-test fuse a command onto its opening fence; 2 search-and-query pages have an odd fence count)
Ticket: DOC-6939
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🧠 Redis Memory

Found 5 related items from repository history:

Memory updated at 60847d4

@andy-stark-redis

Copy link
Copy Markdown
Contributor Author

Correction to the Gaps: trailer on 60847d4

That commit says 10 pages still emit an unterminated fence "from source authoring errors", and names the Kubernetes logs pages, rdi installation/ha-test, and two search-and-query pages. All 10 were false positives. There are no authoring errors on those pages and nothing to fix. The trailer should be disregarded; I can't amend it without rewriting pushed history.

The detector I used was line-based and got three separate things wrong, each producing its own class of phantom finding:

  1. A backtick fence's info string may not contain a backtick. So ```kubectl exec …``` alone on a line is not a fence opener — it's an inline code span, which is exactly how Goldmark renders it (<p><code>kubectl exec …</code></p>). That accounted for the 7 Kubernetes logs pages and ha-test.
  2. Fences can be longer than three characters. ```` opens a block that only a ```` or longer closes. My detector read the fourth backtick as an info string, stayed "open", and inverted the role of every fence after it. That accounted for both search-and-query pages.
  3. Fences nest inside list items. In re-clusters/connect-to-admin-console the opener sits at 4 spaces of indentation and the closer at 3, which is legal relative to the list container but looks unbalanced to a detector with no notion of containers. This class only surfaced after I fixed (1) and (2), which is why the "remaining" list changed from 10 pages to a different 7.

Verified all 15 pages implicated across the three classes render with balanced <pre>/</pre> in Hugo's own CommonMark output. Every one is fine.

What still stands

The fix in this commit is unaffected, and its primary measurement was made by a different, unambiguous detector — it locates the json metadata block and inspects only that block's closing line, where there is no indentation, fence-length or info-string ambiguity:

  • fused metadata closing fence: 1,223 of 5,732 pages → 0
  • Markdown output byte-identical outside the edited region on 5,732 of 5,732 files

What I'm retracting

The secondary figure in the commit body, "unterminated blocks 860 to 10". Both numbers came from the buggy detector. A line-based check can't model CommonMark's container and indentation rules, so treat page-level fence termination as unmeasured rather than as 860, 848, 10, or 7. Fixing it properly needs a real CommonMark parser, which isn't worth adding for this.

🤖 Generated with Claude Code

Two separate defects sat behind the reported placeholder/examples mismatch, and
neither is the cause the assessment guessed (a placeholder emitted for non-code
blocks such as command output or tables).

The first is small. extractCodeBlocks returned the [code example] placeholder
unconditionally but only recorded an example when the block had content, so an
empty fenced block left a placeholder with nothing in examples[] to resolve it.
16 sections. It now drops the placeholder along with the empty block.

The second is the interesting one and explains the mismatches that ran the other
way, which the assessment could not account for. Section ids were not unique
within a page: 440 pages carried at least one collision. slugify discards
punctuation, so headings that differ only in punctuation reduce to the same
string -- commands/xread has "The special `$` ID." and "The special `+` ID", both
becoming the-special-id. Because examples key off the section id, the two
sections' examples merged into one bucket of 4 while each section's own text held
only 2 placeholders, which is what surfaced as more examples than placeholders.

The knock-on effects were worse than the count mismatch. examples[].id is built
from the section id, so it was not unique either: 131 pages had colliding example
ids, up to 5 examples sharing examples-ex0. Deep links of the form
url#section-id were ambiguous, and any consumer keying sections by id silently
dropped one of a colliding pair. Ids are now disambiguated with Hugo's own -1,
-2 suffix convention.

One divergence from Hugo is deliberate: where a page has intro prose and also an
"Overview" heading, the synthetic intro section keeps the overview id and the
real heading becomes overview-1, whereas Hugo would give the heading the plain
anchor. Keeping the established id for the intro section matters more to existing
consumers than matching the anchor in that narrow case.

Mismatches counted per section object drop from 739 to 1, duplicate section ids
from 440 to 0, duplicate example ids from 131 to 0. The single remaining
mismatch is not a defect: ai-agent-resources documents the feed format and so
writes the literal token in prose, which any placeholder count will pick up.
Confirmed content_hash still reproduces from the published algorithm for all
5,687 content pages.

Learned: transform_json_sections.ts is not idempotent -- it strips the content field it reads, so a second run silently turns every content page into an empty index page with no error
Constraint: section ids must be unique within a page -- they join examples[].section_id, build examples[].id, and are the target of url#section-id deep links
Rejected: aligning slugify with Hugo's anchor algorithm outright | changes ids on far more than the colliding pages, so it belongs with the anchor-parity work, not here
Gaps: section ids still do not match rendered page anchors in general -- 79.6% on a 400-page sample -- and the {#explicit-anchor} heading defect leaking into 4,363 section titles is untouched
Ticket: DOC-6939
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🧠 Redis Memory

Found 5 related items from repository history:

Memory updated at 3e64858

Comment thread build/transform_json_sections.ts
Headings using Hugo's explicit anchor syntax were not parsed, so the literal
brace expression was treated as part of the heading text. For "## Specify socket
file location during install {#during-install}" the feed published the syntax
inside the user-visible title, an id of
specify-socket-file-location-during-install-during-install, and therefore a dead
deep link, while the anchor Hugo actually rendered was during-install. 4,363
sections across 773 source files. The title is now the heading without the
anchor, and the id is the anchor itself.

The explicit anchor is used verbatim rather than slugified. Hugo takes it as the
rendered id unchanged, so passing it through slugify would produce a different
string from the anchor on the page and defeat the point of the change.

The match is deliberately strict, and this is the part worth knowing before
touching it. Many headings end in a brace that is not an anchor: 725 Python
signatures ending in an empty dict, 280 dict defaults such as extra ignore, and
145 or more relref shortcodes. A loose trailing-brace match would silently eat
all of them from the titles. Measured across all 40,951 level 2 and 3 headings,
the strict form and a loose form happen to agree today at 4,368 matches with no
corrupting cases, and no match sits inside a code span -- but the strict form is
what keeps that true when someone writes a heading ending in a dict literal.

Anchor parity on the same seeded 400-page sample used to measure the problem
rises from 79.6% to 91.9%. The residual is a separate, well-understood
difference between slugify and Goldmark rather than anything left of this defect:
Goldmark keeps dots and underscores where slugify replaces them, so
application.properties becomes application-properties, and it drops apostrophes
where slugify hyphenates them, so "What You'll Learn" becomes what-you-ll-learn
against Goldmark's what-youll-learn. Closing that means aligning slugify with
Goldmark, which moves ids on far more pages than this and is tracked as the
remainder of C7 on the ticket.

Verified the A3 invariants still hold: duplicate section ids and duplicate
example ids both remain at zero, and content_hash still reproduces from the
published algorithm for all 5,687 content pages.

Learned: most braces in a heading are not anchors, so the anchor pattern has to be shape-checked rather than matched as a trailing brace, or 1,000-plus Python signatures and dict defaults lose text from their titles
Constraint: an explicit heading anchor must be used verbatim as the section id, never slugified, or it stops matching the anchor Hugo renders
Gaps: 8.1% of section ids still differ from the rendered anchor because slugify and Goldmark disagree on dots, underscores and apostrophes
Ticket: DOC-6939
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🧠 Redis Memory

Found 5 related items from repository history:

Memory updated at 95bcfb1

Three fixes from the Bugbot review of this branch. The third is the substantive
one and it was worse than reported.

Bugbot flagged that the metadata tableOfContents ids no longer agree with the
JSON sections[] ids now that sections carry explicit anchors and dedup suffixes.
True, but on reading toc-from-markdown.html the TOC turned out to still have both
of the defects just fixed on the JSON side: the literal brace expression sitting
in its user-visible titles, and the anchor concatenated onto the slug instead of
replacing it. So change-location-socket-files was publishing a TOC entry titled
"Specify socket file location during install {#during-install}" with the id
specify-socket-file-location-during-install-during-install. The same strict
anchor pattern and the same suffix convention now apply there, taking brace
leakage in TOC titles to zero and duplicate TOC ids to zero.

That leaves a divergence worth knowing about before touching any of this: there
are now three separate slug implementations in play, and they disagree. The TOC
strips inline formatting then deletes anything outside a-z0-9 and hyphen, so
Pub/Sub becomes pubsub; slugify in the transform replaces punctuation runs with a
hyphen, so the same heading becomes pub-sub; Goldmark does a third thing.
About 9.2% of TOC ids still do not appear in sections[] for that reason alone.
Consolidating the three is the same work as the anchor-parity item on the ticket.

The table-children partial no longer falls back to the current page's children
when a from= target fails to resolve, which would publish a plausible-looking
table of the wrong data. Bugbot's stated reason for this one does not hold up --
it says the HTML shortcode renders an empty table in that case, whereas the HTML
dereferences .Pages on a nil page and should error instead. I did not run it to
confirm, and the fix does not depend on which is right, since silently emitting
another page's rows is wrong either way.

The partial also now emits the header row when every row is filtered out,
matching the HTML shortcode, which always renders thead. Unreachable today, as
all 101 pages using the shortcode have rows, but a table that filtered down to
nothing would otherwise blank the entire body of the release-notes index pages,
which is the defect this branch started from.

One process note, because it nearly went into a report as a regression: the
row-count parity check reads the page's content field, and the transform strips
that field. Run after the transform it silently compares against an empty string
and reports all 101 pages as broken. Re-pointed at sections[] text it is 101 of
101 matching, with residual shortcodes still at zero.

Learned: three slug implementations now exist across toc-from-markdown.html, transform_json_sections.ts and Goldmark, and they disagree on punctuation, which caps TOC-to-sections id agreement at about 91%
Constraint: the explicit-anchor parsing and the id dedup convention are implemented twice, in toc-from-markdown.html and in build/transform_json_sections.ts, and must be changed together or the metadata TOC and the JSON sections drift apart again
Directive: when verifying this pipeline, read the artifact for the stage you are at -- the transform deletes the content field it consumes, so a post-transform check of content compares against empty and reports false failures
Rejected: Bugbot's premise that the HTML shortcode renders an empty table for an unresolvable from= | it dereferences .Pages on nil and should error, so the fix rests on avoiding silent wrong data instead
Gaps: about 9.2% of TOC ids still differ from sections[] ids purely from the competing slug implementations, unfixed here
Ticket: DOC-6939
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🧠 Redis Memory

Found 5 related items from repository history:

Memory updated at e974a94

Comment thread build/transform_json_sections.ts
Comment thread layouts/partials/process-markdown-content.html
The script consumes the content field and does not write it back, so running it
twice over the same output was silently destructive: the second pass found no
content and rewrote every content page as an empty index page, with no error and
no warning. Normal builds are safe because the Makefile always chains it after a
fresh hugo, but nothing stopped a re-run, and the failure mode is total loss of
feed content. It cost two rebuilds during this branch.

A file that has a page_type but no content has already been transformed, so it
is now skipped, and the run reports how many it skipped rather than staying
quiet about it.

The obvious alternative -- writing content back so the script really is
idempotent -- was rejected. Replacing content with sections and examples is the
whole purpose of the transform, and keeping the original alongside them would
roughly double the size of every page record and of the feed.

Verified by building once and transforming twice. The second run reports 5,732
skipped and 0 transformed, and the output still holds 5,687 content pages, 46,348
sections and 29,319 examples, with content_hash verifying on all 5,687. Before
this change the second run left zero content pages.

Learned: the failure was silent because an already-transformed page is structurally valid input, just with no content, so the index-page branch accepted it happily
Rejected: writing the content field back to make the script truly idempotent | replacing content with sections is the point of the transform, and keeping both would roughly double the feed size
Ticket: DOC-6939
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🧠 Redis Memory

Found 5 related items from repository history:

Memory updated at 501e77f

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

There are 3 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 501e77f. Configure here.

Comment thread layouts/partials/toc-from-markdown.html
Comment thread layouts/partials/toc-from-markdown.html
Reverses a decision made deliberately in 3e64858, which reasoned that where a
page has intro prose and also an "## Overview" heading, the synthetic intro
should keep the plain overview id and the real heading should take overview-1,
because the established id mattered more to existing consumers than matching
the anchor in that narrow case. That reasoning was wrong and the commit message
for it should be read with this one.

What it missed is that the mismatch is not only against Hugo. On the 166 affected
pages Hugo renders the anchor overview for the real heading, and the metadata
tableOfContents also gives overview to the real heading, so sections[] was the
only one of the three naming the intro prose. A consumer following a url#overview
citation landed on the intro instead of the section it asked for. That is a wrong
answer rather than an id-stability preference, so it wins.

Real heading ids are now reserved before the synthetic intro is named, so the
heading keeps overview and the intro takes overview-1. Same id vocabulary, no new
values, and the suffix now falls on the section that has no anchor on the page at
all. All 166 pages check out: the intro holds the suffixed id and the plain
overview is a real heading that Hugo has anchored.

Also adds the &#43; unescape that the post-table-children block was missing
relative to the block above it. Unreachable today -- no built page contains that
entity, no table-children column name or row contains a plus -- so this is
closing an inconsistency, not fixing an observed defect. A version column reading
"6.0+" would have exposed it.

Duplicate section ids, duplicate example ids and brace leakage into titles all
remain at zero, and content_hash still verifies for all 5,687 content pages.

Worth knowing for the next verification: the check I first wrote for this used
the section role to tell the synthetic intro from a real heading, which is
meaningless, because assignRole gives a heading titled "Overview" the role
overview too. It reported 165 of 166 still broken when the fix was already
correct. Inspecting one page's section list in order settled it in seconds.

Learned: the intro section holding the plain overview id mis-resolved url#overview citations on 166 pages, because Hugo's anchor and the metadata TOC both give that id to the real heading
Constraint: reserve the ids of real headings before naming synthetic sections like the intro, or the intro takes an id that belongs to a heading and deep links resolve to the wrong section
Rejected: keeping the plain overview id on the synthetic intro for consumer stability, as 3e64858 did | it breaks the real heading's deep link, and the intro has no page anchor to protect
Ticket: DOC-6939
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🧠 Redis Memory

Found 5 related items from repository history:

Memory updated at 317acdc

…hat hid three sections

Two Bugbot findings on the TOC partial, plus a content bug that following the
second one uncovered.

The TOC was built from every ##/### line in the raw markdown, including lines
inside fenced code blocks, while splitContentIntoSections skips those. A YAML
comment reading "## the defaults are commented" was therefore published as a
navigation entry on the context-retriever install page. The TOC now drops fenced
blocks before matching headings, using the same pairing rule as the transform so
the two agree on what counts as code.

The id dedup loop was also bounded at an arbitrary 50, where the TypeScript side
is unbounded. Not reachable today -- the largest same-base-id group on any page is
19 -- but an arbitrary cap fails by silently leaving duplicates. It is now bounded
by the heading count, which a page can never exceed.

The content bug is the more valuable find and it runs the opposite way to what
Bugbot described. On integrate/redis-data-integration/installation/upgrade a
fenced block inside a warning shortcode was never closed. Hugo does not mind,
because the shortcode body ends and the HTML renders correctly with every heading
present, so nothing looked wrong on the site. The transform pairs ``` marks
naively, so it paired that opener with the next block's opener and treated two
large spans as code, silently dropping three real sections from the feed:
Upgrading a Kubernetes installation, Upgrading to RDI 1.8.0 or later from an
earlier version, and Verifying the upgrade. That page went from 5 sections to its
full 8 once the fence was closed.

Order mattered here. Aligning the TOC with the transform's code-block rule while
the source was still broken would have made the TOC drop those three headings
too, so the two artifacts would have agreed by being equally wrong. The source is
fixed first for that reason.

TOC entries with no counterpart in sections[] fall from 60 to 53, duplicate TOC
ids stay at zero, and the section-id and example-id invariants and content_hash
verification are unchanged.

My fence detection was wrong for the fourth time in this branch on the way here:
of four headings it reported as sitting inside code blocks, three were not, all
on this same RDI page, because an indented fence it cannot model flipped its
state. Comparing the built TOC against the built sections[] -- artifact against
artifact -- found the real one and the dropped sections that a source-parsing
heuristic had missed entirely.

Learned: a source fence that Hugo renders correctly can still break the feed, because Hugo closes it at the shortcode boundary while the transform pairs backtick marks naively -- so "the page looks right" is not evidence the feed is right
Constraint: the TOC partial and splitContentIntoSections must use the same rule for what counts as a fenced code block, or their ids diverge again
Directive: never bound the id dedup loop by a round number -- bound it by the heading count, since an arbitrary cap fails by leaving silent duplicates
Gaps: 53 TOC entries still have no counterpart in sections[], now from a different defect -- the TOC's emphasis-stripping regexes mangle code-like headings, turning a Python signature into "class AsyncSearchIndex(schema, , redisurl=None)"
Ticket: DOC-6939
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🧠 Redis Memory

Found 5 related items from repository history:

Memory updated at 33353bf

@andy-stark-redis andy-stark-redis changed the title DOC-6939 Correct stale document count in the NDJSON feed docs DOC-6939 Improve the AI-facing JSON and Markdown feed output Aug 5, 2026
@andy-stark-redis
andy-stark-redis requested a review from a team August 5, 2026 15:31
@andy-stark-redis andy-stark-redis self-assigned this Aug 5, 2026
@andy-stark-redis andy-stark-redis added infrastructure AI-friendliness Features that help AI tools understand and use the doc pages more easily. labels Aug 5, 2026

@dwdougherty dwdougherty left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Skimmed; approved.

@andy-stark-redis

Copy link
Copy Markdown
Contributor Author

Thanks @dwdougherty !

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

Labels

AI-friendliness Features that help AI tools understand and use the doc pages more easily. infrastructure

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants