Conversation
…hhold an unpaired one
Asciidoctor substitutes `stem:[…]` before the converter's walker ever runs: the
expression arrives as plain text wrapped in MathJax delimiters — `\$…\$` for
asciimath, `\(…\)` for latexmath. There is no node with no rule, so `--strict`
has nothing to warn about, and the text path escaped the notation like prose.
`\$\vec{a}\$` reached the reader as `\\$\\vec{a}\\$`, with every backslash the
expression carries doubled alongside it. 16 expressions on Spring AI's
"Understanding Vectors" page, and a clean `--strict` run over all of them.
Re-emit a paired expression between `$` delimiters, byte-for-byte as upstream
wrote it, and escape everything around it as before — the leading-`#` rule
included, applied to whichever segment actually begins the line.
Pairing is positional, so a run holding an odd number of delimiters cannot be
resolved: given `\$ a \$ b \$` there is no way to tell which one is the stray.
Pairing the first two would splice ` a ` into a formula and strip its escaping,
turning any `*`, `_` or `[` it holds into live Markdown on the published page —
the silent half-rendering ADR-0004 forbids. So such a run is withheld and named
instead: escaped as ordinary prose, and reported through `onUnpairedStem` so
`--strict` fails the build and points at the page. An empty expression is
withheld on the same grounds, because `$$` is display-math delimiters to most
renderers and emitting it would change the construct.
Re-converting framework 7.0.9 with this reproduces its committed tree exactly —
all 464 files byte-identical — and the pattern matches nothing across the
449 MB of corpora under `markdown/`, so no published output moves.
Spring AI's generated half is the degenerate case of the overlay era. Its whole
template is two lines —
version: ${antora-component.version}
prerelease: ${antora-component.prerelease}
— and neither is an asciidoc attribute: `version` is the descriptor field
`overlayDescriptor` already writes from the catalog version, and `prerelease` is
never true because only GA versions are built. The committed `antora.yml`
declares no `asciidoc.attributes` block to top up either, so
`generatedAttributesFor` returns `{}` and the overlay is pure passthrough.
Nothing is downloaded, nothing is reconstructed, and no symlink is declared —
no mode 120000 blob exists under `spring-ai-docs`, and there is no examples tree
to reach through one, because the corpus has no `include-code::`.
The corpus agrees that the build contributes nothing: all 23 `{…}` references in
it are prompt-template placeholders inside code samples, and the
`spring-ai-version` the Hana page mentions is a Maven property written
`${spring-ai-version}` inside an XML listing, not an attribute reference.
One era, no ceiling. `spring-ai-docs/src/main/antora/antora.yml` is
byte-identical at v0.8.0, v1.0.0, v1.1.0, v2.0.0 and v2.0.1 — same component
path, same `mvnw process-resources` collector, single `ROOT` module throughout —
so 2.x is not a second era the way Boot's 4.x is.
The 1.0.0 floor is therefore a publication fact rather than a layout one, as
Boot's 4.0.8 archive floor is: `org/springframework/ai/spring-ai-bom` on Maven
Central begins at `1.0.0-M5`, and 0.8.0 and 0.8.1 are absent from it entirely.
Those releases went to Spring's milestone repository only, so no consumer can
pin a dependency to what their 50 pages describe.
Images come from the release tag, not the reference site, for the reason
Framework and Security already do — `/reference/2.0.1/_images/` answers a 301 to
`/reference/2.0/_images/`, so a URL built from a catalog version would be pinned
in appearance only. Note the path is Antora's short `modules/ROOT/images`, not
the `assets/images` the other two components ship.
1.0.0 converts to 109 pages and 2.0.1 to 121, both exit 0 under `--strict` with
zero converter warnings, and `package-release.ts` produces a valid archive and
manifest from the result.
Closes #75.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 43 |
| Duplication | 0 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
|
The `ai` block copied Spring Framework's shape, which asserts seven facts in one test — so the two blocks matched line for line and SonarCloud read 20 of the PR's new lines as duplicated, over its 3% gate. Spring Security already states the same facts the other way, one fact per test, so split the identity assertions (repo, tag, component path, era descriptor) from the overlay invariant that nothing is downloaded. Every assertion is kept; only where they sit changes.
|
The stem scan claimed any `\$…\$` run, so AsciiDoc that escapes the delimiter in order to write about MathJax — source `\\$x\\$`, which reaches the converter carrying two backslashes — was read as math. The scan started at the second backslash and published `$x\$`, stripping the prose escaping inside the pair on the way. Asciidoctor emits exactly one backslash for a delimiter it substituted, so a lookbehind separates the two cases. The same lookbehind goes on the unpaired-delimiter check: without it an escaped pair would make a real expression beside it look unpaired, and withhold it. A singly escaped `\$x\$` written as prose stays indistinguishable, because Asciidoctor emits the same text for it as for `stem:[x]` and the substitution leaves the text node no provenance — not even the `stem` attribute, which is unset on the very pages carrying the macro. Recorded in the doc comment. Re-converting Spring AI 2.0.1 reproduces all 121 files byte-identically.



Adds Spring AI as the fourth upstream project, and fixes the one converter gap its corpus surfaced.
Closes #75.
The two questions the issue left open
Is it
overlay? Yes, and it is the degenerate case of it. The issue's last line said this depended on "the synthesized-assembly refactor in the Framework issue" — that line predates theoverlaydescriptor. Framework landed asoverlay(#83, #140) and Security asoverlayplusderivedAttributes, and Spring AI needs less than either: its entire generated template isNeither line is an asciidoc attribute.
versionis the descriptor fieldoverlayDescriptoralready writes from the catalog version, andprereleaseis never true because only GA versions are built. The committedantora.ymldeclares noasciidoc.attributesblock to top up either, sogeneratedAttributesForreturns{}. Nothing is downloaded and nothing is reconstructed. (correction comment on the issue)The issue also listed
{spring-ai-version}as a build attribute. It is not — its single occurrence is a Maven property written${spring-ai-version}inside an XML listing on the Hana page. All 23{…}references in the corpus are prompt-template placeholders inside code samples.Does 2.x need a second era? No.
spring-ai-docs/src/main/antora/antora.ymlis byte-identical at v0.8.0, v1.0.0, v1.1.0, v2.0.0 and v2.0.1 — same component path, samemvnw process-resourcescollector, singleROOTmodule throughout, and no mode-120000 blob anywhere underspring-ai-docs. One era, no ceiling.Why the floor is 1.0.0 and not 0.8.0
The layout reaches back to v0.8.0, so this is a publication fact rather than a layout one — the same kind of reason Boot's archive era starts at 4.0.8.
org/springframework/ai/spring-ai-bomon Maven Central begins at1.0.0-M5; 0.8.0 and 0.8.1 are absent from it entirely, having gone to Spring's milestone repository only. No consumer can pin a dependency to what their 50 pages describe.The converter gap
--strictpassed with zero warnings while quietly mangling 16 inlinestem:[…]expressions on "Understanding Vectors" into\\$\\vec{a}\\$. Asciidoctor substitutes the stem macro before the converter's walker runs, so there is no unrecognised node and nothing for the strict gate to see — it is structurally invisible to it. That is exactly the "reaches the reader as literal text, which is worse than an absent page because it looks like content" failure ADR-0004 names.Paired expressions now pass through as
$…$, byte-for-byte. An unpaired run is withheld rather than guessed at: pairing is positional, so given\$ a \$ b \$there is no way to tell which delimiter is the stray one, and pairing the first two would spliceainto a formula and strip its escaping — turning any*,_or[it holds into live Markdown. Such a run is escaped as ordinary prose and reported throughonUnpairedStem, so--strictfails and names the page. An empty expression is withheld on the same grounds, since$$is display-math delimiters to most renderers.Verification
bun test tests/unitbunx tsc --noEmit -p .bun run lintai1.0.0 → convert--strictai2.0.1 → convert--strictai1.0.0 →package-release.tsmarkdown/Also updated
NOTICE(Spring AI was missing, unlike Framework/Security/Data/Cloud), the README support table — whose "Buildable versions" row had named Boot alone since Framework and Security landed, and now covers all four — and.claude/skills/release-pipeline/SKILL.md, including a new note that--strictcannot see a construct Asciidoctor substitutes before the walker runs.Out of scope
Tagging
ai-1.0.0and registering it incatalog.jsonare a separate step, to be done after this merges.Summary by cubic
Adds Spring AI as the fourth upstream project, and fixes the stem handling its corpus surfaced.
New Features
Bug Fixes
stem:[…]expressions now pass through as$…$; previously they reached readers escaped as literal text like\\$\\vec{a}\\$.onUnpairedStem, so--strictfails and names the page instead of shipping a guessed pairing.No published output moves: framework 7.0.9 re-converts byte-identical, and the stem pattern matches nothing across the existing corpora.
Closes #75.
Written for commit 8ce42e6. Summary will update on new commits.