diff --git a/docs/fern/assets/releases.json b/docs/fern/assets/releases.json index 2cac16a089d7..d589c9555222 100644 --- a/docs/fern/assets/releases.json +++ b/docs/fern/assets/releases.json @@ -1808,6 +1808,26 @@ "firstTimers": 34, "breaking": 41, "knownIssues": 14 + }, + "v0.9.0": { + "firstTimers": 14, + "breaking": 1, + "knownIssues": 13 + }, + "v0.8.0": { + "firstTimers": 20, + "breaking": 0, + "knownIssues": 14 + }, + "v0.7.0": { + "firstTimers": 2, + "breaking": 0, + "knownIssues": 7 + }, + "v0.6.0": { + "firstTimers": 4, + "breaking": 0, + "knownIssues": 3 } }, "nightliesNote": "ai-dynamo and ai-dynamo-runtime nightly builds from main publish wheels tagged *.devYYYYMMDD (since Apr 24, 2026). Install with pip or uv using --pre and the NVIDIA extra-index pattern shown above." diff --git a/docs/fern/components/UpgradeSelector.tsx b/docs/fern/components/UpgradeSelector.tsx index 016e82433296..d07a368f940d 100644 --- a/docs/fern/components/UpgradeSelector.tsx +++ b/docs/fern/components/UpgradeSelector.tsx @@ -6,8 +6,9 @@ * * Answers "I run v1.1.x — what must I read to get to the current release?". * Self-configuring from releases.data: the target is CURRENT_VERSION and the - * from-candidates are every older stable release that has a RELEASE_STATS - * entry, labeled by line ("v1.2.x"). Each line maps to its LATEST release + * from-candidates are every older stable release that has both a RELEASE_STATS + * entry and a docs-native notes page, labeled by line ("v1.2.x"). Each line + * maps to its LATEST release * including patches (v1.2.x -> v1.2.1) so the migration strip reflects the * pins the user actually runs. Each panel reuses UpgradePanel's internals * (buildRows + MigrationStrip + ReadingListFooter + UpgradePanelStyles); the @@ -104,10 +105,18 @@ function buildFromLines(): FromLine[] { const currentIdx = RELEASES.findIndex((r) => r.version === CURRENT_VERSION); if (currentIdx < 0) return []; - /* All stable releases carrying RELEASE_STATS, in RELEASES (newest-first) - order, with their array index for older/newer comparisons. */ + /* Stable releases carrying RELEASE_STATS *and* a docs-native notes page, in + RELEASES (newest-first) order, with their array index for older/newer + comparisons. notesHref is the gate because the reading-list chips deep-link + to per-release sections on the Deprecations and Known Issues pages, which + exist only for releases that have a notes page. RELEASE_STATS on its own + also covers pre-v1.0.0 releases, which are counted on Release History but + have no such sections to point at. */ const statStables = RELEASES.map((release, index) => ({ release, index })).filter( - ({ release }) => release.kind === "stable" && RELEASE_STATS[release.version] !== undefined, + ({ release }) => + release.kind === "stable" && + release.notesHref !== undefined && + RELEASE_STATS[release.version] !== undefined, ); return statStables diff --git a/docs/fern/components/releases.data.ts b/docs/fern/components/releases.data.ts index a412bd4af5ef..a72f07d4d7b8 100644 --- a/docs/fern/components/releases.data.ts +++ b/docs/fern/components/releases.data.ts @@ -1020,11 +1020,42 @@ export interface ReleaseStats { knownIssues: number; } +/* COUNTING RULES — apply these when ingesting a new release so rows stay + comparable across the two release-note eras: + - prs / contributors / firstTimers: use the figure the body states outright + ("merged 930 PRs from 125 contributors", "welcome 14 new contributors"). + Where the body only lists first-timers without a total, count the list. + Omit rather than derive: v1.0.0 states commits, not PRs, so prs is absent, + and v1.2.0 names no first-timers at all. + - breaking: top-level entries under Breaking Changes, including its + Deprecated/Removed subsections, but excluding subsections that only + restate a prior release's announced deprecations ("vX.Y.Z + Future-Deprecation Reminders"). Pre-v1.0.0 bodies have no Breaking Changes + section; v0.9.0's lone Deprecation Notices entry is the same entry class + and counts, and a release with no such section at all is a true 0. + - knownIssues: one per named issue — the per-issue heading where the body + gives each issue its own, otherwise the top-level bullets. + Known exception: v1.0.0 breaking is published as 41, but its body holds 40 + top-level entries and no rule reproduces 41. Left as published. + + The absent prs and contributors cells are absent for cause, not for want of + looking. Neither the release bodies, the TPM release archive, nor the git + history yields a figure comparable to the stated ones: the archive's own + numbers disagree with each other (v0.9.0 is written up as both 217 and 935 + PRs for the identical window, and v1.0.0's 708 is quoted as commits in one + place and as merged PRs in another), and no tag-to-tag count reproduces the + three published anchors — the closest method returns 910/572/899 against a + published 930/603/896, missing in both directions, so it cannot be trusted + to fill the rest. Leave them absent unless a method reproduces all three. */ export const RELEASE_STATS: Record = { "v1.3.0": { prs: 930, contributors: 125, firstTimers: 23, breaking: 24, knownIssues: 10 }, "v1.2.0": { prs: 603, contributors: 82, breaking: 5, knownIssues: 11 }, "v1.1.0": { prs: 896, contributors: 113, firstTimers: 12, breaking: 8, knownIssues: 20 }, "v1.0.0": { contributors: 90, firstTimers: 34, breaking: 41, knownIssues: 14 }, + "v0.9.0": { firstTimers: 14, breaking: 1, knownIssues: 13 }, + "v0.8.0": { firstTimers: 20, breaking: 0, knownIssues: 14 }, + "v0.7.0": { firstTimers: 2, breaking: 0, knownIssues: 7 }, + "v0.6.0": { firstTimers: 4, breaking: 0, knownIssues: 3 }, }; export const NIGHTLIES_NOTE = diff --git a/docs/fern/pages/reference/general/compatibility.mdx b/docs/fern/pages/reference/general/compatibility.mdx index 83b9077ce220..276ea3bc7243 100644 --- a/docs/fern/pages/reference/general/compatibility.mdx +++ b/docs/fern/pages/reference/general/compatibility.mdx @@ -8,7 +8,6 @@ subtitle: Hardware, platform, and feature support for Dynamo backends import { ReferenceStyles } from "@/components/ReferenceStyles"; import { CompatibilityHero } from "@/components/CompatibilityHero"; import { FeatureHeatmap } from "@/components/FeatureHeatmap"; -import { RunsWhereWizard } from "@/components/RunsWhereWizard"; @@ -20,11 +19,118 @@ For extended driver compatibility beyond the listed minimums, including forward See [Release Artifacts](release-artifacts.mdx) for the full artifact inventory — container images, wheels, Helm charts, and crates — and [Model Early Access Builds](model-early-access-builds.mdx) for per-model early access container builds. -## Find a Compatible Release +## Release Support Matrix -Pick your backend and the CUDA generation your host driver supports to see which Dynamo releases you can run — and what to pull for the current one. +The card above covers one release at a time. Expand the matrix below to compare CUDA toolkit and minimum driver requirements across releases, per backend. - +{/* support-matrix:begin — generated by scripts/gen_llms_tables.py, do not edit */} + + + +Every released line — stable releases and their patches, grouped by minor line, newest first. Platform previews and model-specific builds are not listed individually; those with a documented toolkit requirement appear in the notes below, and [Releases (machine-readable)](releases-machine-readable.mdx) has the full release inventory. + +**1.3.x** + +| Dynamo | Backend | CUDA Toolkit | Min Driver | Note | +| --- | --- | --- | --- | --- | +| 1.3.0 | SGLang | 13.0 | 580.xx+ | - | +| 1.3.0 | TensorRT-LLM | 13.1 | 580.xx+ | - | +| 1.3.0 | vLLM | 13.0 | 580.xx+ | - | + +**1.2.x** + +| Dynamo | Backend | CUDA Toolkit | Min Driver | Note | +| --- | --- | --- | --- | --- | +| 1.2.1 | SGLang | 12.9 | 575.xx+ | - | +| 1.2.1 | SGLang | 13.0 | 580.xx+ | - | +| 1.2.1 | TensorRT-LLM | 13.1 | 580.xx+ | - | +| 1.2.1 | vLLM | 12.9 | 575.xx+ | - | +| 1.2.1 | vLLM | 13.0 | 580.xx+ | - | +| 1.2.0 | SGLang | 12.9 | 575.xx+ | - | +| 1.2.0 | SGLang | 13.0 | 580.xx+ | - | +| 1.2.0 | TensorRT-LLM | 13.1 | 580.xx+ | - | +| 1.2.0 | vLLM | 12.9 | 575.xx+ | - | +| 1.2.0 | vLLM | 13.0 | 580.xx+ | - | + +**1.1.x** + +| Dynamo | Backend | CUDA Toolkit | Min Driver | Note | +| --- | --- | --- | --- | --- | +| 1.1.1 | SGLang | 12.9 | 575.xx+ | - | +| 1.1.1 | SGLang | 13.0 | 580.xx+ | - | +| 1.1.1 | TensorRT-LLM | 13.1 | 580.xx+ | - | +| 1.1.1 | vLLM | 12.9 | 575.xx+ | - | +| 1.1.1 | vLLM | 13.0 | 580.xx+ | - | +| 1.1.0 | SGLang | 12.9 | 575.xx+ | - | +| 1.1.0 | SGLang | 13.0 | 580.xx+ | - | +| 1.1.0 | TensorRT-LLM | 13.1 | 580.xx+ | - | +| 1.1.0 | vLLM | 12.9 | 575.xx+ | - | +| 1.1.0 | vLLM | 13.0 | 580.xx+ | - | + +**1.0.x** + +| Dynamo | Backend | CUDA Toolkit | Min Driver | Note | +| --- | --- | --- | --- | --- | +| 1.0.2 | SGLang | 12.9 | 575.xx+ | - | +| 1.0.2 | SGLang | 13.0 | 580.xx+ | - | +| 1.0.2 | TensorRT-LLM | 13.1 | 580.xx+ | - | +| 1.0.2 | vLLM | 12.9 | 575.xx+ | - | +| 1.0.2 | vLLM | 13.0 | 580.xx+ | - | +| 1.0.1 | SGLang | 12.9 | 575.xx+ | - | +| 1.0.1 | SGLang | 13.0 | 580.xx+ | - | +| 1.0.1 | TensorRT-LLM | 13.1 | 580.xx+ | - | +| 1.0.1 | vLLM | 12.9 | 575.xx+ | - | +| 1.0.1 | vLLM | 13.0 | 580.xx+ | - | +| 1.0.0 | SGLang | 12.9 | 575.xx+ | - | +| 1.0.0 | SGLang | 13.0 | 580.xx+ | - | +| 1.0.0 | TensorRT-LLM | 13.1 | 580.xx+ | - | +| 1.0.0 | vLLM | 12.9 | 575.xx+ | - | +| 1.0.0 | vLLM | 13.0 | 580.xx+ | - | + +**0.9.x** + +| Dynamo | Backend | CUDA Toolkit | Min Driver | Note | +| --- | --- | --- | --- | --- | +| 0.9.1 | SGLang | 12.9 | 575.xx+ | - | +| 0.9.1 | TensorRT-LLM | 13.0 | 580.xx+ | - | +| 0.9.1 | vLLM | 12.9 | 575.xx+ | - | +| 0.9.0 | SGLang | 12.9 | 575.xx+ | - | +| 0.9.0 | TensorRT-LLM | 13.0 | 580.xx+ | - | +| 0.9.0 | vLLM | 12.9 | 575.xx+ | - | + +**0.8.x** + +| Dynamo | Backend | CUDA Toolkit | Min Driver | Note | +| --- | --- | --- | --- | --- | +| 0.8.1 | SGLang | 12.9 | 575.xx+ | - | +| 0.8.1 | SGLang | 13.0 | 580.xx+ | Experimental | +| 0.8.1 | TensorRT-LLM | 13.0 | 580.xx+ | - | +| 0.8.1 | vLLM | 12.9 | 575.xx+ | - | +| 0.8.1 | vLLM | 13.0 | 580.xx+ | Experimental | +| 0.8.0 | SGLang | 12.9 | 575.xx+ | - | +| 0.8.0 | SGLang | 13.0 | 580.xx+ | Experimental | +| 0.8.0 | TensorRT-LLM | 13.0 | 580.xx+ | - | +| 0.8.0 | vLLM | 12.9 | 575.xx+ | - | +| 0.8.0 | vLLM | 13.0 | 580.xx+ | Experimental | + +**0.7.x** + +| Dynamo | Backend | CUDA Toolkit | Min Driver | Note | +| --- | --- | --- | --- | --- | +| 0.7.1 | SGLang | 12.8 | 570.xx+ | - | +| 0.7.1 | TensorRT-LLM | 13.0 | 580.xx+ | - | +| 0.7.1 | vLLM | 12.9 | 575.xx+ | - | +| 0.7.0 | SGLang | 12.9 | 575.xx+ | - | +| 0.7.0 | TensorRT-LLM | 13.0 | 580.xx+ | - | +| 0.7.0 | vLLM | 12.8 | 570.xx+ | - | + +- Patch versions (e.g. v0.8.1.post1, v0.7.0.post1) have the same CUDA support as their base version. +- Early access v1.1.0-dev.* images follow the same CUDA matrix as v1.0.2. The v1.2.0-deepseek-v4-dev.3 vLLM container is CUDA 13.0 multi-arch; the SGLang containers split by arch (CUDA 12.9 on amd64, CUDA 13.0 on arm64). +- Experimental CUDA 13 images are not published for all versions. + + + +{/* support-matrix:end */} ## Platform Notes diff --git a/docs/fern/pages/reference/general/releases-machine-readable.mdx b/docs/fern/pages/reference/general/releases-machine-readable.mdx index 38320303452e..983e751d92b7 100644 --- a/docs/fern/pages/reference/general/releases-machine-readable.mdx +++ b/docs/fern/pages/reference/general/releases-machine-readable.mdx @@ -237,6 +237,10 @@ Release highlights (stable releases): | v1.2.0 | 603 | 82 | - | 5 | 11 | | v1.1.0 | 896 | 113 | 12 | 8 | 20 | | v1.0.0 | - | 90 | 34 | 41 | 14 | +| v0.9.0 | - | - | 14 | 1 | 13 | +| v0.8.0 | - | - | 20 | 0 | 14 | +| v0.7.0 | - | - | 2 | 0 | 7 | +| v0.6.0 | - | - | 4 | 0 | 3 | ## Nightlies diff --git a/docs/fern/pages/reference/general/releases/release-history.mdx b/docs/fern/pages/reference/general/releases/release-history.mdx index 898c6d0b4765..0e16ad636b3a 100644 --- a/docs/fern/pages/reference/general/releases/release-history.mdx +++ b/docs/fern/pages/reference/general/releases/release-history.mdx @@ -21,3 +21,22 @@ Agents and automation: [Releases (machine-readable)](../releases-machine-readabl ## Releases + +{/* release-stats:begin — generated by scripts/gen_llms_tables.py, do not edit */} + +## Release statistics + +Counts taken from each release's GitHub body. A dash means the count was not recorded for that release, not that it was zero. + +| Release | PRs | Contributors | First-time contributors | Breaking changes | Known issues | +| --- | --- | --- | --- | --- | --- | +| v1.3.0 | 930 | 125 | 23 | 24 | 10 | +| v1.2.0 | 603 | 82 | - | 5 | 11 | +| v1.1.0 | 896 | 113 | 12 | 8 | 20 | +| v1.0.0 | - | 90 | 34 | 41 | 14 | +| v0.9.0 | - | - | 14 | 1 | 13 | +| v0.8.0 | - | - | 20 | 0 | 14 | +| v0.7.0 | - | - | 2 | 0 | 7 | +| v0.6.0 | - | - | 4 | 0 | 3 | + +{/* release-stats:end */} diff --git a/docs/fern/scripts/gen_llms_tables.py b/docs/fern/scripts/gen_llms_tables.py index f475b1d15568..fa8c406dba9c 100755 --- a/docs/fern/scripts/gen_llms_tables.py +++ b/docs/fern/scripts/gen_llms_tables.py @@ -11,6 +11,25 @@ emits equivalent markdown tables into each page between idempotent markers, wrapped in so only agent exports see them. +A page may carry more than one generated span, each keyed by its own marker +name. reference/compatibility.mdx also carries a human-facing +``support-matrix`` span: a single collapsed holding the CUDA +toolkit and minimum driver matrix for every released line (stable releases +and their patches), split into one captioned table per minor line so a +header stays in view while scrolling. The agent twin on the same page keeps +the matrix as one flat table instead -- the human cut is optimized for +scanning, the agent cut for parsing, so the two spans are deliberately +different renderings of the same rows rather than a redundant pair. + +reference/release-notes/README.mdx (Release History) carries a second +human-facing span, ``release-stats``: the per-release PR, contributor, +breaking-change, and known-issue counts. Those counts otherwise appeared +only in releases-data.mdx, which is hidden from the sidebar, so on the page +they had no reader-visible home at all. Both spans render from +``release_stats_table`` so the visible table and the agent mirror cannot +diverge. Like ``support-matrix`` it fails closed when its markers are +missing, keeping placement under human control. + It also emits three machine-readable outputs from the same parse: * pages/reference/general/releases-machine-readable.mdx — a "Releases (machine-readable)" page whose @@ -57,6 +76,7 @@ import re import sys from pathlib import Path +from typing import Callable, NamedTuple SCRIPT_DIR = Path(__file__).resolve().parent DATA_TS = SCRIPT_DIR.parent / "components" / "releases.data.ts" @@ -69,10 +89,16 @@ PROD_HOST = "https://docs.nvidia.com" PROD_BASE = f"{PROD_HOST}/dynamo" -MARKER_BEGIN = ( - "{/* llms-tables:begin — generated by scripts/gen_llms_tables.py, do not edit */}" -) -MARKER_END = "{/* llms-tables:end */}" +GENERATOR_NOTE = "generated by scripts/gen_llms_tables.py, do not edit" + + +def marker_begin(marker: str) -> str: + return f"{{/* {marker}:begin — {GENERATOR_NOTE} */}}" + + +def marker_end(marker: str) -> str: + return f"{{/* {marker}:end */}}" + # Exports the generator depends on. Parsing aborts if any is missing. REQUIRED_EXPORTS = [ @@ -389,11 +415,16 @@ def feature_cell(fc: dict) -> str: # --------------------------------------------------------------------------- -def cuda_table(data: dict) -> str: - """CUDA toolkit and minimum-driver history table.""" +def cuda_table(data: dict, versions: set[str] | None = None) -> str: + """CUDA toolkit and minimum-driver history table. + + ``versions`` optionally restricts the rows to a set of bare release + versions (e.g. ``{"1.3.0", "1.2.0"}``); ``None`` emits the full history. + """ rows = [ [r["version"], r["backend"], r["toolkit"], r["minDriver"], r.get("note")] for r in data["CUDA_HISTORY"] + if versions is None or r["version"] in versions ] return md_table(["Dynamo", "Backend", "CUDA Toolkit", "Min Driver", "Note"], rows) @@ -545,6 +576,12 @@ def render_compatibility(data: dict) -> str: ) ) + # Deliberately restates the support-matrix accordion's table. The + # accordion covers the same rows, but it is markup and this + # twin exists precisely because component output may be dropped from the + # agent-facing exports. Losing the matrix for agents is a worse failure + # than repeating it, so keep both until the deployed .md export is + # confirmed to carry Accordion children. parts.append("**CUDA toolkit and minimum driver per Dynamo release**") parts.append(cuda_table(data)) cuda_notes = data.get("CUDA_NOTES") or [] @@ -561,6 +598,90 @@ def render_compatibility(data: dict) -> str: return "\n\n".join(parts) +# Release kinds the human-facing support matrix covers: the lines a reader +# can actually pull and run. Platform previews (-dev.N) and model-specific +# builds are deliberately excluded -- CUDA_NOTES documents the toolkit support +# of those that have it, which is not all of them: v1.3.0-dev.1 and +# v1.2.0-deepseek-v4-dev.2 have no note and no CUDA_HISTORY row, and no CUDA +# version is recorded for them anywhere in the repo. The accordion caption is +# therefore worded to promise coverage only for the builds a note names, +# rather than to send readers of the others after information that is not +# there. CUDA_HISTORY happens to carry no preview or model-build rows +# today, so this filter is currently a no-op; it is expressed on kind anyway +# so a future preview entry cannot leak into the matrix unnoticed. +SUPPORT_MATRIX_KINDS = ("stable", "patch") + + +def released_versions(data: dict) -> set[str]: + """Bare versions of the released lines -- stable releases and patches.""" + return { + rel["version"].removeprefix("v") + for rel in data["RELEASES"] + if rel.get("kind") in SUPPORT_MATRIX_KINDS + } + + +def minor_line(version: str) -> str: + """'1.2.1' -> '1.2.x' -- the caption a matrix table is grouped under.""" + parts = version.split(".") + if len(parts) < 2 or not (parts[0].isdigit() and parts[1].isdigit()): + raise TSParseError( + f"CUDA_HISTORY version {version!r} is not MAJOR.MINOR... -- the " + "support matrix cannot group it by minor line" + ) + return f"{parts[0]}.{parts[1]}.x" + + +def group_by_minor_line(data: dict, versions: set[str]) -> dict[str, set[str]]: + """Minor line -> its versions, keyed in CUDA_HISTORY order (newest first). + + Insertion order carries the sort: CUDA_HISTORY is maintained newest-first, + so the first time a minor line is seen fixes its position, and dicts + preserve that. No separate version sort to drift out of step with it. + """ + groups: dict[str, set[str]] = {} + for row in data["CUDA_HISTORY"]: + if row["version"] in versions: + groups.setdefault(minor_line(row["version"]), set()).add(row["version"]) + return groups + + +def render_support_matrix(data: dict) -> str: + """Human-facing collapsed CUDA/driver matrix for the released lines. + + One captioned table per minor line, all inside a single . A + flat 60-row run pushes its header off screen inside a collapsed panel; + per-line tables repeat the header every few rows. Separate tables rather + than a version column blanked after its first row -- blank leading cells + are ambiguous once the page is flattened into the agent markdown export. + """ + versions = released_versions(data) + groups = group_by_minor_line(data, versions) + if not groups: + raise TSParseError( + "no CUDA_HISTORY row matches a released RELEASES version -- the " + "support-matrix accordion would render an empty table" + ) + + parts = [ + '', + "Every released line — stable releases and their patches, grouped by " + "minor line, newest first. Platform previews and model-specific " + "builds are not listed individually; those with a documented toolkit " + "requirement appear in the notes below, and " + "[Releases (machine-readable)](releases-machine-readable.mdx) " + "has the full release inventory.", + ] + for line, line_versions in groups.items(): + parts.append(f"**{line}**") + parts.append(cuda_table(data, line_versions)) + cuda_notes = data.get("CUDA_NOTES") or [] + if cuda_notes: + parts.append("\n".join(f"- {note}" for note in cuda_notes)) + parts.append("") + return "\n\n".join(parts) + + def render_release_artifacts(data: dict) -> str: parts: list[str] = [] parts.append( @@ -620,6 +741,57 @@ def release_link(rel: dict) -> str | None: # --------------------------------------------------------------------------- +RELEASE_STATS_COLUMNS = ( + "Release", + "PRs", + "Contributors", + "First-time contributors", + "Breaking changes", + "Known issues", +) + + +def release_stats_table(data: dict) -> str: + """The release-statistics table, shared by both spans that render it. + + Release History shows it to readers and releases-data.mdx mirrors it for + agents. Building both from here means the two cannot drift apart. + """ + rows = [ + [ + version, + stats.get("prs"), + stats.get("contributors"), + stats.get("firstTimers"), + stats.get("breaking"), + stats.get("knownIssues"), + ] + for version, stats in data["RELEASE_STATS"].items() + ] + return md_table(list(RELEASE_STATS_COLUMNS), rows) + + +def render_release_stats(data: dict) -> str: + """Human-facing release statistics for the Release History page. + + Rows come from whatever RELEASE_STATS holds, so a new release picked up + by the per-release bump checklist appears here with no edit to this file. + """ + if not data.get("RELEASE_STATS"): + raise TSParseError( + "RELEASE_STATS is empty -- the release-statistics table on " + "Release History would render with no rows" + ) + return "\n\n".join( + [ + "## Release statistics", + "Counts taken from each release's GitHub body. A dash means the " + "count was not recorded for that release, not that it was zero.", + release_stats_table(data), + ] + ) + + def render_releases_data(data: dict) -> str: """Full releases.data.ts content as plain markdown (not ).""" parts: list[str] = [] @@ -726,30 +898,7 @@ def render_releases_data(data: dict) -> str: parts.append(platform_lines(data)) parts.append("## Release statistics") - stat_rows = [ - [ - version, - stats.get("prs"), - stats.get("contributors"), - stats.get("firstTimers"), - stats.get("breaking"), - stats.get("knownIssues"), - ] - for version, stats in data["RELEASE_STATS"].items() - ] - parts.append( - md_table( - [ - "Release", - "PRs", - "Contributors", - "First-time contributors", - "Breaking changes", - "Known issues", - ], - stat_rows, - ) - ) + parts.append(release_stats_table(data)) if data.get("NIGHTLIES_NOTE"): parts.append("## Nightlies") @@ -884,14 +1033,40 @@ def build_atom(data: dict) -> str: # Emission # --------------------------------------------------------------------------- -# page -> (renderer, wrap_in_llms_only). The three component-backed pages get -# twins (humans see the React components); releases-machine-readable.mdx IS -# the page body, human-viewable and machine-consumable alike. -PAGES = { - "compatibility.mdx": (render_compatibility, True), - "release-artifacts.mdx": (render_release_artifacts, True), - "model-early-access-builds.mdx": (render_model_ea_builds, True), - "releases-machine-readable.mdx": (render_releases_data, False), + +class Block(NamedTuple): + """One generated span in a page, delimited by ``:begin/:end``.""" + + marker: str + renderer: Callable[[dict], str] + # Hide the body from human readers, leaving it only in agent exports. + llms_only: bool + # Append the span at end of file when the page has no markers yet. + # False fails closed instead, so placement stays under human control. + append_if_missing: bool + + +# The three component-backed pages get twins (humans see the React +# components); releases-machine-readable.mdx IS the page body, human-viewable +# and machine-consumable alike. compatibility.mdx additionally carries the +# human-facing support-matrix accordion at a fixed spot in the page. +PAGES: dict[str, tuple[Block, ...]] = { + "compatibility.mdx": ( + Block("support-matrix", render_support_matrix, False, False), + Block("llms-tables", render_compatibility, True, True), + ), + "release-artifacts.mdx": ( + Block("llms-tables", render_release_artifacts, True, True), + ), + "model-early-access-builds.mdx": ( + Block("llms-tables", render_model_ea_builds, True, True), + ), + "releases-machine-readable.mdx": ( + Block("llms-tables", render_releases_data, False, True), + ), + "releases/release-history.mdx": ( + Block("release-stats", render_release_stats, False, False), + ), } # Standalone machine-readable outputs (path -> builder returning full text). @@ -900,25 +1075,36 @@ def build_atom(data: dict) -> str: ATOM_PATH: build_atom, } -# Matches an existing generated span. Tolerant of edits to the note after -# "llms-tables:begin" so a hand-tweaked marker comment is still replaced. -BLOCK_RE = re.compile( - r"\{/\*\s*llms-tables:begin[^*]*\*/\}.*?\{/\*\s*llms-tables:end\s*\*/\}", - re.DOTALL, -) + +def block_re(marker: str) -> re.Pattern[str]: + """Match an existing generated span. Tolerant of edits to the note after + ":begin" so a hand-tweaked marker comment is still replaced.""" + name = re.escape(marker) + return re.compile( + rf"\{{/\*\s*{name}:begin[^*]*\*/\}}.*?\{{/\*\s*{name}:end\s*\*/\}}", + re.DOTALL, + ) -def build_block(body: str, wrap: bool = True) -> str: - if wrap: - return f"{MARKER_BEGIN}\n\n\n{body}\n\n\n{MARKER_END}" - return f"{MARKER_BEGIN}\n\n{body}\n\n{MARKER_END}" +def build_block(block: Block, body: str) -> str: + begin, end = marker_begin(block.marker), marker_end(block.marker) + if block.llms_only: + return f"{begin}\n\n\n{body}\n\n\n{end}" + return f"{begin}\n\n{body}\n\n{end}" -def apply_block(page_text: str, block: str) -> str: - if BLOCK_RE.search(page_text): - return BLOCK_RE.sub(lambda _m: block, page_text, count=1) +def apply_block(page_text: str, block: Block, rendered: str) -> str: + pattern = block_re(block.marker) + if pattern.search(page_text): + return pattern.sub(lambda _m: rendered, page_text, count=1) + if not block.append_if_missing: + raise TSParseError( + f"{block.marker}: markers not found — the page must contain " + f"{marker_begin(block.marker)} and {marker_end(block.marker)} " + "where the generated span belongs" + ) # No markers yet: append at end of file, before the trailing newline. - return page_text.rstrip("\n") + "\n\n" + block + "\n" + return page_text.rstrip("\n") + "\n\n" + rendered + "\n" def main(argv: list[str]) -> int: @@ -939,13 +1125,16 @@ def main(argv: list[str]) -> int: # Render everything up front so a failure on any output emits nothing. try: rendered: dict[Path, str] = {} - for page_name, (renderer, wrap) in PAGES.items(): + for page_name, blocks in PAGES.items(): page_path = REFERENCE_DIR / page_name if not page_path.is_file(): print(f"error: page not found: {page_path}", file=sys.stderr) return 2 - old = page_path.read_text(encoding="utf-8") - rendered[page_path] = apply_block(old, build_block(renderer(data), wrap)) + text = page_path.read_text(encoding="utf-8") + for block in blocks: + body = build_block(block, block.renderer(data)) + text = apply_block(text, block, body) + rendered[page_path] = text for asset_path, builder in ASSET_OUTPUTS.items(): rendered[asset_path] = builder(data) except (TSParseError, KeyError) as exc: @@ -954,7 +1143,13 @@ def main(argv: list[str]) -> int: stale = [] for path, new_text in rendered.items(): - name = path.name + # Relative to the reference root so pages in subdirectories are + # distinguishable -- several are named README.mdx. + name = ( + str(path.relative_to(REFERENCE_DIR)) + if path.is_relative_to(REFERENCE_DIR) + else path.name + ) old_text = path.read_text(encoding="utf-8") if path.is_file() else None if new_text == old_text: print(f"{name}: unchanged")