From 48a8c5a7e0484a2e6430cbe6c8d77abe09b7089e Mon Sep 17 00:00:00 2001 From: Richard Lander Date: Thu, 4 Dec 2025 21:39:02 -0800 Subject: [PATCH 01/15] Add release notes information graph spec --- .../metrics.md | 1055 +++++++++++++++++ .../release-notes-information-graph.md | 879 ++++++++++++++ 2 files changed, 1934 insertions(+) create mode 100644 accepted/2025/release-notes-information-graph/metrics.md create mode 100644 accepted/2025/release-notes-information-graph/release-notes-information-graph.md diff --git a/accepted/2025/release-notes-information-graph/metrics.md b/accepted/2025/release-notes-information-graph/metrics.md new file mode 100644 index 000000000..351b6fab8 --- /dev/null +++ b/accepted/2025/release-notes-information-graph/metrics.md @@ -0,0 +1,1055 @@ +# Schema Metrics Comparison + +This document compares the data transfer costs between the new **hal-index** schema and the legacy **releases-index** schema for common query patterns. + +## Design Context + +The hal-index schema was designed to solve fundamental problems with the releases-index approach: + +1. **Cache Coherency** - The releases-index.json references external files (e.g., `9.0/releases.json`) that may have different CDN cache TTLs, leading to inconsistent data when patch versions are updated. + +2. **Data Efficiency** - The `releases.json` files contain download URLs and hashes for every binary artifact, making them 30-50x larger than necessary for most queries. + +3. **Atomic Consistency** - The hal-index uses HAL `_embedded` to include all referenced data in a single document, ensuring a consistent snapshot per fetch. + +See [dotnet/core#10143](https://github.com/dotnet/core/issues/10143) for full design rationale. + +## File Characteristics + +The tables below show theoretical update frequency based on practice and design, with file sizes measured from actual files. + +### Hal-Graph Files + +| File | Size | Updates/Year | Description | +|------|------|--------------|-------------| +| `index.json` | 8 KB | ~1 | Root index with all major versions | +| `10.0/index.json` | 15 KB | ~12 | All 10.0 patches (fewer releases so far) | +| `9.0/index.json` | 28 KB | ~12 | All 9.0 patches with CVE references | +| `8.0/index.json` | 34 KB | ~12 | All 8.0 patches with CVE references | +| `timeline/index.json` | 8 KB | ~1 | Timeline root (all years) | +| `timeline/2025/index.json` | 15 KB | ~12 | Year index (all months) | +| `timeline/2024/07/index.json` | 10 KB | ~1 | Month index with embedded CVE summaries | +| `timeline/2025/01/cve.json` | 14 KB | ~1 | Full CVE details for a month | + +### Releases-Index Files + +| File | Size | Updates/Year | Description | +|------|------|--------------|-------------| +| `releases-index.json` | 6 KB | ~12 | Root index (version list only) | +| `10.0/releases.json` | **440 KB** | ~12 | All 10.0 releases with full download metadata | +| `9.0/releases.json` | **769 KB** | ~12 | All 9.0 releases with full download metadata | +| `8.0/releases.json` | **1,233 KB** | ~12 | All 8.0 releases with full download metadata | + +### Measurements + +Actual git commits in the last 12 months (Nov 2024 - Nov 2025): + +| File | Commits | Notes | +|------|---------|-------| +| `releases-index.json` | 29 | Root index (all versions) | +| `10.0/releases.json` | 22 | Includes previews/RCs and SDK-only releases | +| `9.0/releases.json` | 24 | Includes SDK-only releases, fixes, URL rewrites | +| `8.0/releases.json` | 18 | Includes SDK-only releases, fixes, URL rewrites | + +The commit counts are significantly higher than the theoretical ~12/year due to: + +- **SDK-only releases**: Additional releases between Patch Tuesdays (e.g., [9.0.308 SDK](https://github.com/dotnet/core/commit/24a83fcc189ecf3c514dc06963ce779dcbf64ad5) released 8 days after November Patch Tuesday) +- **Metadata corrections**: Simple changes like [updating .NET 9's EOL date](https://github.com/dotnet/core/commit/24ff22598de88e3c9681e579aab5fe344cdc21b0) require updating both `releases-index.json` and `9.0/releases.json` +- **Post-release corrections**: `fix hashes`, `sdk array mismatch` +- **Infrastructure changes**: `Update file URLs`, `Rewrite URLs to builds.dotnet` +- **Rollbacks**: `Revert "Switch links..."` + +The EOL date example illustrates a key architectural tradeoff: with releases-index, metadata changes to any version require updating the root file. With hal-index, we give up rich information in the root file, but in exchange we can safely propagate useful information to many locations (authored in `9.0/_manifest.json`, generated into `9.0/manifest.json`, `9.0/index.json`, timeline indexes, etc.)—provided it doesn't violate the core rule: **the root `index.json` is never touched for version-specific changes**. + +Note that Patch Tuesday releases are batched—a single commit like [November 2025](https://github.com/dotnet/core/commit/484b00682d598f8d11a81607c257c1f0a099b84c) updates `releases-index.json`, `8.0/releases.json`, `9.0/releases.json`, and `10.0/releases.json` together (6,802 lines changed across 30 files). Even with batching, the root file still requires ~30 updates/year. + +These massive commits are difficult to review and analyze. Mixing markdown documentation with JSON data files in the same commit makes it hard to distinguish content changes from data updates. The hal-index design separates these concerns—JSON index files are generated automatically, while markdown content is authored separately. + +**Operational Risk:** These files are effectively mission-critical live-site APIs, driving hundreds of GBs of downloads monthly. Yet they require ~20-30 manual updates per year each, carrying risk of human error (as the fix commits demonstrate), cache invalidation complexity, and CDN propagation delays. + +**Key insight:** The hal-index root file (`index.json`) is updated ~1x/year when a new major version is added. The releases-index root file (`releases-index.json`) is updated ~30x/year. This makes the hal-index root file ideal for aggressive CDN caching, while the releases-index files are constantly-moving targets. + +## Query Comparison + +### Capability Summary + +| Query Type | hal-index | releases-index | Winner | +|------------|-----------|----------------|--------| +| List versions | ✅ | ✅ | releases-index (1.3x smaller) | +| Version lifecycle (EOL, LTS) | ✅ | ✅ | releases-index (1.3x smaller) | +| Latest patch per version | ✅ | ✅ | hal-index (23x smaller) | +| CVEs per version | ✅ | ✅ | hal-index (23x smaller) | +| CVEs per month | ✅ | ❌ | hal-index only | +| CVE details (severity, fixes) | ✅ | ❌ | hal-index only | +| Timeline navigation | ✅ | ❌ | hal-index only | +| SDK-first navigation | ✅ | ❌ | hal-index only | +| Version diff (severity, products) | ✅ | ⚠️ Partial | hal-index (15x smaller) | +| Breaking changes by category | ✅ | ❌ | hal-index only | +| EOL exposure analysis | ✅ | ⚠️ Partial | hal-index (with severity) | + +The sections below demonstrate each query pattern with working examples. + +### Version-Based Queries + +The following queries navigate the schema by major version (8.0, 9.0, 10.0), drilling down to specific patches and CVE details. + +#### Query: "What .NET versions are currently supported?" + +| Schema | Files Required | Total Transfer | +|--------|----------------|----------------| +| hal-index | `index.json` | **8 KB** | +| releases-index | `releases-index.json` | **6 KB** | + +**hal-index:** + +```bash +ROOT="https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" + +curl -s "$ROOT" | jq -r '._embedded.releases[] | select(.supported) | .version' +# 10.0 +# 9.0 +# 8.0 +``` + +**releases-index:** + +```bash +ROOT="https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json" + +curl -s "$ROOT" | jq -r '.["releases-index"][] | select(.["support-phase"] == "active") | .["channel-version"]' +# 10.0 +# 9.0 +# 8.0 +``` + +**Analysis:** + +- **Completeness:** ✅ Equal—both return the same list of supported versions. +- **Boolean vs enum:** The hal-index uses `supported: true`, a simple boolean. The releases-index uses `support-phase: "active"`, requiring knowledge of the enum vocabulary (active, maintenance, eol, preview, go-live). +- **Property naming:** The hal-index uses `select(.supported)` with dot notation. The releases-index requires `select(.["support-phase"] == "active")` with bracket notation and string comparison. +- **Query complexity:** The hal-index query is 30% shorter and more intuitive for someone unfamiliar with the schema. + +**Winner:** releases-index (**1.3x smaller** for basic version queries, but hal-index has better query ergonomics) + +### CVE Queries for Latest Security Patch + +#### Query: "What CVEs were fixed in the latest .NET 8.0 security patch?" + +| Schema | Files Required | Total Transfer | +|--------|----------------|----------------| +| hal-index | `index.json` → `8.0/index.json` → `8.0/8.0.21/index.json` | **52 KB** | +| releases-index | `releases-index.json` + `8.0/releases.json` | **1,239 KB** | + +**hal-index:** + +```bash +ROOT="https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" + +# Step 1: Get the 8.0 version href +VERSION_HREF=$(curl -s "$ROOT" | jq -r '._embedded.releases[] | select(.version == "8.0") | ._links.self.href') +# https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/8.0/index.json + +# Step 2: Get the latest security patch href +PATCH_HREF=$(curl -s "$VERSION_HREF" | jq -r '._links["latest-security"].href') +# https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/8.0/8.0.21/index.json + +# Step 3: Get the CVE records +curl -s "$PATCH_HREF" | jq -r '.cve_records[]' +# CVE-2025-55247 +# CVE-2025-55248 +# CVE-2025-55315 +``` + +**releases-index:** + +```bash +ROOT="https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json" + +# Step 1: Get the 8.0 releases.json URL +RELEASES_URL=$(curl -s "$ROOT" | jq -r '.["releases-index"][] | select(.["channel-version"] == "8.0") | .["releases.json"]') +# https://builds.dotnet.microsoft.com/dotnet/release-metadata/8.0/releases.json + +# Step 2: Find latest security release and get CVE IDs +curl -s "$RELEASES_URL" | jq -r '[.releases[] | select(.security == true)] | .[0] | .["cve-list"][] | .["cve-id"]' +# CVE-2025-55247 +# CVE-2025-55315 +# CVE-2025-55248 +``` + +**Analysis:** Both schemas produce the same CVE IDs. However: + +- **Completeness:** ✅ Equal—both return the CVE identifiers +- **Ergonomics:** The releases-index requires downloading a 1.2 MB file to extract 3 CVE IDs. The hal-index uses a dedicated `latest-security` link, avoiding iteration through all releases. +- **Link syntax:** Counterintuitively, the deeper HAL structure `._links.self.href` is more ergonomic than `.["releases.json"]` because snake_case enables dot notation throughout. The releases-index embeds URLs directly in properties, but kebab-case naming forces bracket notation. +- **Data efficiency:** hal-index is 23x smaller + +**Winner:** hal-index (**23x smaller**) + +### High Severity CVEs with Details + +#### Query: "What High+ severity CVEs were fixed in the latest .NET 8.0 security patch, with titles?" + +| Schema | Files Required | Total Transfer | +|--------|----------------|----------------| +| hal-index | `index.json` → `8.0/index.json` → `8.0/8.0.21/index.json` | **52 KB** | +| releases-index | `releases-index.json` + `8.0/releases.json` | **1,239 KB** (cannot answer) | + +**hal-index:** + +```bash +ROOT="https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" + +# Step 1: Get the 8.0 version href +VERSION_HREF=$(curl -s "$ROOT" | jq -r '._embedded.releases[] | select(.version == "8.0") | ._links.self.href') + +# Step 2: Get the latest security patch href +PATCH_HREF=$(curl -s "$VERSION_HREF" | jq -r '._links["latest-security"].href') + +# Step 3: Filter HIGH+ severity with titles +curl -s "$PATCH_HREF" | jq -r '._embedded.disclosures[] | select(.cvss_severity == "HIGH" or .cvss_severity == "CRITICAL") | "\(.id): \(.title) (\(.cvss_severity))"' +# CVE-2025-55247: .NET Denial of Service Vulnerability (HIGH) +# CVE-2025-55315: .NET Security Feature Bypass Vulnerability (CRITICAL) +``` + +**releases-index:** + +```bash +ROOT="https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json" + +# Step 1: Get the 8.0 releases.json URL +RELEASES_URL=$(curl -s "$ROOT" | jq -r '.["releases-index"][] | select(.["channel-version"] == "8.0") | .["releases.json"]') + +# Step 2: Find latest security release and get CVE IDs (best effort—no severity/title available) +curl -s "$RELEASES_URL" | jq -r '[.releases[] | select(.security == true)] | .[0] | .["cve-list"][] | "\(.["cve-id"]): (severity unknown) (title unknown)"' +# CVE-2025-55247: (severity unknown) (title unknown) +# CVE-2025-55315: (severity unknown) (title unknown) +# CVE-2025-55248: (severity unknown) (title unknown) +``` + +**Analysis:** + +- **Completeness:** ❌ The releases-index only provides CVE IDs and URLs to external CVE databases. It does not include severity scores, CVSS ratings, or vulnerability titles. To get this information, you would need to fetch each CVE URL individually from cve.mitre.org. +- **Ergonomics:** The hal-index embeds full CVE details (`cvss_severity`, `cvss_score`, `title`, `fixes`) directly in the patch index, enabling single-query filtering by severity. + +**Winner:** hal-index (releases-index cannot answer this query—CVE severity and titles are not available) + +### Servicing Version Diff + +#### Query: "What changed between .NET 8.0.15 and 8.0.22?" + +| Schema | Files Required | Total Transfer | +|--------|----------------|----------------| +| hal-index | `index.json` → `8.0/index.json` + 3 patch indexes | **~80 KB** | +| releases-index | `releases-index.json` + `8.0/releases.json` | **1,239 KB** (partial data) | + +**hal-index:** + +This query requires two passes: first to get the release summaries, then to fetch CVE details for severity filtering and affected products. + +```bash +ROOT="https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" + +# Configuration +MAJOR_VERSION="8.0" +FROM_PATCH=15 +TO_PATCH=22 +SEVERITY_FILTER="CRITICAL" # Minimum severity: CRITICAL, HIGH, MEDIUM, or LOW (all) + +# Step 1: Get the major version href +VERSION_HREF=$(curl -s "$ROOT" | jq -r --arg ver "$MAJOR_VERSION" '._embedded.releases[] | select(.version == $ver) | ._links.self.href') + +# Step 2: Get release summaries and security release URLs +VERSION_DATA=$(curl -s "$VERSION_HREF") + +# Extract security release URLs for CVE detail fetching +SECURITY_HREFS=$(echo "$VERSION_DATA" | jq -r --argjson from "$FROM_PATCH" --argjson to "$TO_PATCH" ' + [._embedded.releases[] | + select((.version | split(".")[2] | tonumber) > $from and (.version | split(".")[2] | tonumber) <= $to) | + select(.security) | + ._links.self.href] | .[] +') + +# Step 3: Fetch CVE details from each security release and aggregate +CVE_DETAILS=$(for HREF in $SECURITY_HREFS; do + curl -s "$HREF" | jq -c '._embedded.disclosures[]? | {id, cvss_severity, affected_products, title}' +done | jq -s 'unique_by(.id)') + +# Step 4: Generate the diff report with severity-filtered CVE IDs +echo "$VERSION_DATA" | jq -r --arg major "$MAJOR_VERSION" --argjson from "$FROM_PATCH" --argjson to "$TO_PATCH" \ + --arg severity "$SEVERITY_FILTER" --argjson cve_details "$CVE_DETAILS" ' + # Filter releases in range (excluding start, including end) + [._embedded.releases[] | + select((.version | split(".")[2] | tonumber) > $from and (.version | split(".")[2] | tonumber) <= $to) + ] as $releases | + + # Filter CVEs by minimum severity + [$cve_details[] | select( + ($severity == "LOW") or + ($severity == "MEDIUM" and (.cvss_severity == "MEDIUM" or .cvss_severity == "HIGH" or .cvss_severity == "CRITICAL")) or + ($severity == "HIGH" and (.cvss_severity == "HIGH" or .cvss_severity == "CRITICAL")) or + ($severity == "CRITICAL" and .cvss_severity == "CRITICAL") + )] as $filtered_cves | + + # Aggregate affected products across all CVEs + [$cve_details[].affected_products // [] | .[]] | unique | sort as $all_products | + + { + from_version: "\($major).\($from)", + to_version: "\($major).\($to)", + from_date: (._embedded.releases[] | select(.version == "\($major).\($from)") | .date | split("T")[0]), + to_date: (._embedded.releases[] | select(.version == "\($major).\($to)") | .date | split("T")[0]), + total_releases: ($releases | length), + security_releases: ([$releases[] | select(.security)] | length), + non_security_releases: ([$releases[] | select(.security | not)] | length), + total_cves: ([$releases[].cve_records? // [] | .[]] | unique | length), + cve_ids: [$filtered_cves[] | .id], + cve_severity_filter: $severity, + affected_products: $all_products, + releases: [$releases[] | {version, date: (.date | split("T")[0]), security, cve_count}] + } +' +``` + +**Output:** + +```json +{ + "from_version": "8.0.15", + "to_version": "8.0.22", + "from_date": "2025-04-08", + "to_date": "2025-11-11", + "total_releases": 7, + "security_releases": 3, + "non_security_releases": 4, + "total_cves": 5, + "cve_ids": [ + "CVE-2025-55315" + ], + "cve_severity_filter": "CRITICAL", + "affected_products": [ + "aspnetcore-runtime", + "dotnet-runtime", + "windowsdesktop-runtime" + ], + "releases": [ + { "version": "8.0.22", "date": "2025-11-11", "security": false, "cve_count": 0 }, + { "version": "8.0.21", "date": "2025-10-14", "security": true, "cve_count": 3 }, + { "version": "8.0.20", "date": "2025-09-09", "security": false, "cve_count": 0 }, + { "version": "8.0.19", "date": "2025-08-05", "security": false, "cve_count": 0 }, + { "version": "8.0.18", "date": "2025-07-08", "security": false, "cve_count": 0 }, + { "version": "8.0.17", "date": "2025-06-10", "security": true, "cve_count": 1 }, + { "version": "8.0.16", "date": "2025-05-22", "security": true, "cve_count": 1 } + ] +} +``` + +To include all CVEs regardless of severity: + +```bash +SEVERITY_FILTER="LOW" # LOW is the minimum, so this includes all CVEs +``` + +The script above outputs `from_date` and `to_date`. To calculate the time gap, pipe the output through an additional jq filter: + +```bash +# Pipe the output to calculate days between versions +... | jq -r ' + # Parse ISO dates and calculate difference + ((.to_date | strptime("%Y-%m-%d") | mktime) - + (.from_date | strptime("%Y-%m-%d") | mktime)) / 86400 | floor as $days | + . + { + days_behind: $days, + months_behind: (($days / 30) | floor) + } +' +``` + +This adds `days_behind` and `months_behind` to the output: + +```json +{ + "from_version": "8.0.15", + "to_version": "8.0.22", + "from_date": "2025-04-08", + "to_date": "2025-11-11", + "days_behind": 217, + "months_behind": 7, + ... +} +``` + +**releases-index:** + +```bash +ROOT="https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json" + +# Configuration +MAJOR_VERSION="8.0" +FROM_PATCH=15 +TO_PATCH=22 + +# Step 1: Get the releases.json URL for the major version +RELEASES_URL=$(curl -s "$ROOT" | jq -r --arg ver "$MAJOR_VERSION" '.["releases-index"][] | select(.["channel-version"] == $ver) | .["releases.json"]') + +# Step 2: Filter releases in range and aggregate +curl -s "$RELEASES_URL" | jq -r --arg major "$MAJOR_VERSION" --argjson from "$FROM_PATCH" --argjson to "$TO_PATCH" ' + # Filter releases in range (excluding start, including end) + [.releases[] | select( + (.["release-version"] | split(".")[2] | tonumber) > $from and + (.["release-version"] | split(".")[2] | tonumber) <= $to + )] as $releases | + + { + from_version: "\($major).\($from)", + to_version: "\($major).\($to)", + from_date: ([.releases[] | select(.["release-version"] == "\($major).\($from)")] | .[0] | .["release-date"]), + to_date: ([.releases[] | select(.["release-version"] == "\($major).\($to)")] | .[0] | .["release-date"]), + total_releases: ($releases | length), + security_releases: ([$releases[] | select(.security)] | length), + non_security_releases: ([$releases[] | select(.security | not)] | length), + total_cves: ([$releases[].["cve-list"]? // [] | .[]] | unique | length), + cve_ids: ([$releases[].["cve-list"]? // [] | .[] | .["cve-id"]] | unique | sort), + cve_severity_filter: "(not available)", + affected_products: "(not available)", + releases: [$releases[] | { + version: .["release-version"], + date: .["release-date"], + security, + cve_count: ([.["cve-list"]? // [] | .[]] | length) + }] + } +' +``` + +**Output:** + +```json +{ + "from_version": "8.0.15", + "to_version": "8.0.22", + "from_date": "2025-04-08", + "to_date": "2025-11-11", + "total_releases": 7, + "security_releases": 3, + "non_security_releases": 4, + "total_cves": 5, + "cve_ids": [ + "CVE-2025-26646", + "CVE-2025-30399", + "CVE-2025-55247", + "CVE-2025-55248", + "CVE-2025-55315" + ], + "cve_severity_filter": "(not available)", + "affected_products": "(not available)", + "releases": [ + { "version": "8.0.22", "date": "2025-11-11", "security": false, "cve_count": 0 }, + { "version": "8.0.21", "date": "2025-10-14", "security": true, "cve_count": 3 }, + { "version": "8.0.20", "date": "2025-09-09", "security": false, "cve_count": 0 }, + { "version": "8.0.19", "date": "2025-08-05", "security": false, "cve_count": 0 }, + { "version": "8.0.18", "date": "2025-07-08", "security": false, "cve_count": 0 }, + { "version": "8.0.17", "date": "2025-06-10", "security": true, "cve_count": 1 }, + { "version": "8.0.16", "date": "2025-05-22", "security": true, "cve_count": 1 } + ] +} +``` + +**Analysis:** + +- **Completeness:** ⚠️ Partial—the releases-index can count releases and list CVE IDs, but cannot provide CVE severity, affected products, or detailed CVE information without fetching external CVE URLs. +- **Severity filtering:** The hal-index allows filtering `cve_ids` to specific severity levels (CRITICAL, HIGH, etc.) via the `SEVERITY_FILTER` variable, while `total_cves` always shows the complete count. +- **Affected products:** The hal-index aggregates all affected products across the version range (e.g., `dotnet-runtime`, `aspnetcore-runtime`), enabling teams to identify which components need patching. +- **Executive reporting:** For CIO/CTO reporting, the hal-index provides actionable data (severity-filtered CVEs, affected products) while the releases-index only provides CVE IDs that require manual lookup. + +**Winner:** hal-index (**15x smaller**, with CVE severity filtering and affected products) + +### Breaking Changes Summary + +#### Query: "How many breaking changes are in .NET 10, by category?" + +| Schema | Files Required | Total Transfer | +|--------|----------------|----------------| +| hal-index | `index.json` → `10.0/index.json` → `10.0/breaking-changes.json` | **~45 KB** | +| releases-index | N/A | N/A (not available) | + +**hal-index:** + +```bash +ROOT="https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" +VERSION="10.0" + +# Step 1: Get the version index +VERSION_HREF=$(curl -s "$ROOT" | jq -r --arg ver "$VERSION" '._embedded.releases[] | select(.version == $ver) | ._links.self.href') + +# Step 2: Get the breaking-changes.json link +BC_HREF=$(curl -s "$VERSION_HREF" | jq -r '._links["breaking-changes-json"].href') + +# Step 3: Get counts by category +curl -s "$BC_HREF" | jq -r ' + "Total: \(.breaking_change_count)\n", + ([.breaks[].category] | group_by(.) | map({category: .[0], count: length}) | sort_by(-.count) | .[] | " \(.category): \(.count)") +' +``` + +**Output:** + +```text +Total: 83 + + sdk: 23 + core-libraries: 16 + aspnet-core: 9 + cryptography: 8 + extensions: 6 + windows-forms: 6 + interop: 3 + networking: 3 + reflection: 2 + serialization: 2 + wpf: 2 + containers: 1 + globalization: 1 + install-tool: 1 +``` + +**releases-index:** Not available. + +**Winner:** hal-index only + +### Breaking Changes by Category + +#### Query: "What are the core-libraries breaking changes in .NET 10?" + +| Schema | Files Required | Total Transfer | +|--------|----------------|----------------| +| hal-index | `index.json` → `10.0/index.json` → `10.0/breaking-changes.json` | **~45 KB** | +| releases-index | N/A | N/A (not available) | + +**hal-index:** + +```bash +ROOT="https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" +VERSION="10.0" +CATEGORY="core-libraries" + +# Step 1: Get the version index +VERSION_HREF=$(curl -s "$ROOT" | jq -r --arg ver "$VERSION" '._embedded.releases[] | select(.version == $ver) | ._links.self.href') + +# Step 2: Get the breaking-changes.json link +BC_HREF=$(curl -s "$VERSION_HREF" | jq -r '._links["breaking-changes-json"].href') + +# Step 3: Get breaking changes for category +curl -s "$BC_HREF" | jq -r --arg cat "$CATEGORY" '.breaks[] | select(.category == $cat) | "- \(.title)"' +``` + +**Output:** + +```text +- ActivitySource.CreateActivity and ActivitySource.StartActivity behavior changes +- System.Linq.AsyncEnumerable in .NET 10 +- BufferedStream.WriteByte no longer performs implicit flush +- C# 14 overload resolution with span parameters +- Default trace context propagator updated to W3C standard +- 'DynamicallyAccessedMembers' annotation removed from 'DefaultValueAttribute' ctor +- DriveInfo.DriveFormat returns Linux filesystem types +- FilePatternMatch.Stem changed to non-nullable +- Consistent shift behavior in generic math +- Specifying explicit struct Size disallowed with InlineArray +- LDAP DirectoryControl parsing is now more stringent +- MacCatalyst version normalization +- .NET 10 obsoletions with custom IDs +- .NET runtime no longer provides default termination signal handler +- Arm64 SVE nonfaulting loads require mask parameter +- GnuTarEntry and PaxTarEntry exclude atime and ctime by default +``` + +**releases-index:** Not available. + +**Winner:** hal-index only + +### Breaking Changes Documentation URLs + +#### Query: "Get the raw markdown URLs for core-libraries breaking changes (for LLM context)" + +| Schema | Files Required | Total Transfer | +|--------|----------------|----------------| +| hal-index | `index.json` → `10.0/index.json` → `10.0/breaking-changes.json` | **~45 KB** | +| releases-index | N/A | N/A (not available) | + +**hal-index:** + +```bash +ROOT="https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" +VERSION="10.0" +CATEGORY="core-libraries" + +# Step 1: Get the version index +VERSION_HREF=$(curl -s "$ROOT" | jq -r --arg ver "$VERSION" '._embedded.releases[] | select(.version == $ver) | ._links.self.href') + +# Step 2: Get the breaking-changes.json link +BC_HREF=$(curl -s "$VERSION_HREF" | jq -r '._links["breaking-changes-json"].href') + +# Step 3: Get raw documentation URLs for category +curl -s "$BC_HREF" | jq -r --arg cat "$CATEGORY" ' + .breaks[] | select(.category == $cat) | .references[] | select(.type == "documentation-source") | .url +' +``` + +**Output:** + +```text +https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/core-libraries/10.0/activity-sampling.md +https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/core-libraries/10.0/asyncenumerable.md +https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/core-libraries/10.0/bufferedstream-writebyte-flush.md +https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/core-libraries/10.0/csharp-overload-resolution.md +https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/core-libraries/10.0/default-trace-context-propagator.md +https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/core-libraries/10.0/defaultvalueattribute-dynamically-accessed-members.md +https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/core-libraries/10.0/driveinfo-driveformat-linux.md +https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/core-libraries/10.0/filepatternmatch-stem-nonnullable.md +https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/core-libraries/10.0/generic-math.md +https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/core-libraries/10.0/inlinearray-explicit-size-disallowed.md +https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/core-libraries/10.0/ldap-directorycontrol-parsing.md +https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/core-libraries/10.0/maccatalyst-version-normalization.md +https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/core-libraries/10.0/obsolete-apis.md +https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/core-libraries/10.0/sigterm-signal-handler.md +https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/core-libraries/10.0/sve-nonfaulting-loads-mask-parameter.md +https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/core-libraries/10.0/tar-atime-ctime-default.md +``` + +**releases-index:** Not available. + +**Analysis:** + +- **LLM context:** These raw markdown URLs can be fetched and fed directly to an LLM for analysis or migration assistance. +- **Reference types:** Each breaking change includes multiple reference types (`documentation`, `documentation-source`, `announcement`)—the `documentation-source` type provides the raw markdown. + +**Winner:** hal-index only + +### Timeline-Based Queries + +The following queries demonstrate the hal-index timeline navigation model, which organizes releases chronologically rather than by version. + +### Recent CVEs Across All Versions + +#### Query: "What CVEs were fixed in the last 2 security releases?" + +| Schema | Files Required | Total Transfer | +|--------|----------------|----------------| +| hal-index | `timeline/index.json` → `timeline/2025/index.json` | **23 KB** | +| releases-index | `releases-index.json` + 3 releases.json | **2,448 KB** | + +**hal-index:** + +```bash +TIMELINE="https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/index.json" + +# Step 1: Get the latest year href +YEAR_HREF=$(curl -s "$TIMELINE" | jq -r '._embedded.years[0]._links.self.href') + +# Step 2: Get the last 2 security months with CVEs +curl -s "$YEAR_HREF" | jq -r '[._embedded.months[] | select(.security)] | .[0:2] | .[] | "\(.month)/2025: \(.cve_records | join(", "))"' +# 10/2025: CVE-2025-55248, CVE-2025-55315, CVE-2025-55247 +# 06/2025: CVE-2025-30399 +``` + +**releases-index:** + +```bash +ROOT="https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json" + +# Get all supported version releases.json URLs +URLS=$(curl -s "$ROOT" | jq -r '.["releases-index"][] | select(.["support-phase"] == "active") | .["releases.json"]') + +# For each version, find security releases and collect CVEs (requires multiple large fetches) +for URL in $URLS; do + curl -s "$URL" | jq -r '.releases[] | select(.security == true) | "\(.["release-date"]): \([.["cve-list"][]? | .["cve-id"]] | join(", "))"' +done | sort -r | head -6 +# 2025-10-14: CVE-2025-55247, CVE-2025-55315, CVE-2025-55248 +# 2025-10-14: CVE-2025-55247, CVE-2025-55315, CVE-2025-55248 +# 2025-10-14: CVE-2025-55247, CVE-2025-55315, CVE-2025-55248 +# 2025-06-10: CVE-2025-30399 +# 2025-06-10: CVE-2025-30399 +# 2025-06-10: CVE-2025-30399 +``` + +**Analysis:** + +- **Completeness:** ⚠️ Partial—the releases-index can find CVEs by date, but produces duplicate entries (one per version) and cannot group by month without additional post-processing. +- **Ergonomics:** The hal-index timeline is purpose-built for chronological queries. The releases-index requires fetching all version files (2.4 MB) and manually correlating dates to find "last 2 security releases." +- **Data model:** The releases-index organizes by version; the hal-index timeline organizes by date. For "recent CVEs" queries, the timeline model is fundamentally better suited. + +**Winner:** hal-index (**107x smaller**) + +### CVE Details for a Month + +#### Query: "What CVEs were disclosed in January 2025 with full details?" + +| Schema | Files Required | Total Transfer | +|--------|----------------|----------------| +| hal-index | `timeline/index.json` → `timeline/2025/index.json` → `timeline/2025/01/cve.json` | **37 KB** | +| releases-index | All releases.json (13 versions) | **8.2 MB** (cannot answer) | + +**hal-index:** + +```bash +TIMELINE="https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/index.json" + +# Step 1: Get 2025 year href +YEAR_HREF=$(curl -s "$TIMELINE" | jq -r '._embedded.years[] | select(.year == "2025") | ._links.self.href') +# https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/index.json + +# Step 2: Get January cve.json href +CVE_HREF=$(curl -s "$YEAR_HREF" | jq -r '._embedded.months[] | select(.month == "01") | ._links["cve-json"].href') +# https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/01/cve.json + +# Step 3: Get CVE details +curl -s "$CVE_HREF" | jq -r '.disclosures[] | "\(.id): \(.problem)"' +# CVE-2025-21171: .NET Remote Code Execution Vulnerability +# CVE-2025-21172: .NET and Visual Studio Remote Code Execution Vulnerability +# CVE-2025-21176: .NET and Visual Studio Remote Code Execution Vulnerability +# CVE-2025-21173: .NET Elevation of Privilege Vulnerability +``` + +**releases-index:** + +```bash +ROOT="https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json" + +# Must fetch ALL version releases.json files—cannot filter by "currently supported" +# because we need versions that were supported in January 2025, not today +URLS=$(curl -s "$ROOT" | jq -r '.["releases-index"][] | .["releases.json"]') + +# Find January 2025 releases and get CVE IDs (no details available) +for URL in $URLS; do + curl -s "$URL" | jq -r '.releases[] | select(.["release-date"] | startswith("2025-01")) | select(.security == true) | .["cve-list"][]? | "\(.["cve-id"]): (no description available)"' +done | sort -u +# CVE-2025-21171: (no description available) +# CVE-2025-21172: (no description available) +# CVE-2025-21173: (no description available) +# CVE-2025-21176: (no description available) +``` + +**Analysis:** + +- **Completeness:** ❌ The releases-index provides only CVE IDs. The query asks for "full details" including problem descriptions, CVSS scores, affected products, and fix commits—none of which are available. +- **Historical queries:** The releases-index has no way to determine which versions were supported at a given point in time. To reliably find all CVEs for January 2025, you must fetch *every* version's releases.json file (not just currently supported versions), significantly increasing data transfer. +- **Ergonomics:** The hal-index provides a dedicated `cve.json` file per month with complete CVE records. The releases-index requires fetching all version files and provides only minimal data. + +**Winner:** hal-index (**221x smaller**, and releases-index cannot answer this query—CVE details are not available) + +### Security Patches in the Last 12 Months + +#### Query: "List all CVEs fixed in the last 12 months" + +| Schema | Files Required | Total Transfer | +|--------|----------------|----------------| +| hal-index | `timeline/index.json` → up to 12 month indexes (via `prev` links) | **~90 KB** | +| releases-index | All version releases.json files | **2.4+ MB** | + +**hal-index:** + +```bash +TIMELINE="https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/index.json" + +# Step 1: Get the latest month href +MONTH_HREF=$(curl -s "$TIMELINE" | jq -r '._embedded.years[0]._links["latest-month"].href') + +# Step 2: Walk back 12 months using prev links, collecting security CVEs +for i in {1..12}; do + DATA=$(curl -s "$MONTH_HREF") + YEAR_MONTH=$(echo "$DATA" | jq -r '"\(.year)-\(.month)"') + SECURITY=$(echo "$DATA" | jq -r '.security') + if [ "$SECURITY" = "true" ]; then + CVES=$(echo "$DATA" | jq -r '[._embedded.disclosures[].id] | join(", ")') + echo "$YEAR_MONTH: $CVES" + fi + MONTH_HREF=$(echo "$DATA" | jq -r '._links.prev.href // empty') + [ -z "$MONTH_HREF" ] && break +done +# 2025-10: CVE-2025-55248, CVE-2025-55315, CVE-2025-55247 +# 2025-06: CVE-2025-30399 +# 2025-05: CVE-2025-26646 +# 2025-04: CVE-2025-26682 +# 2025-03: CVE-2025-24070 +# 2025-01: CVE-2025-21171, CVE-2025-21172, CVE-2025-21176, CVE-2025-21173 +``` + +**releases-index:** + +```bash +ROOT="https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json" + +# Get all supported version releases.json URLs +URLS=$(curl -s "$ROOT" | jq -r '.["releases-index"][] | select(.["support-phase"] == "active") | .["releases.json"]') + +# For each version, find security releases in the last 12 months +CUTOFF="2024-12-01" +for URL in $URLS; do + curl -s "$URL" | jq -r --arg cutoff "$CUTOFF" ' + .releases[] | + select(.security == true) | + select(.["release-date"] >= $cutoff) | + "\(.["release-date"]): \([.["cve-list"][]? | .["cve-id"]] | join(", "))"' +done | sort -u | sort -r +# 2025-10-14: CVE-2025-55247, CVE-2025-55315, CVE-2025-55248 +# 2025-06-10: CVE-2025-30399 +# 2025-05-22: CVE-2025-26646 +# 2025-04-08: CVE-2025-26682 +# 2025-03-11: CVE-2025-24070 +# 2025-01-14: CVE-2025-21172, CVE-2025-21173, CVE-2025-21176 +``` + +**Analysis:** + +- **Completeness:** ⚠️ Partial—the releases-index can list CVEs by date, but notice CVE-2025-21171 is missing (it only affected .NET 9.0 which was still in its first patch cycle). The output also shows exact dates rather than grouped by month. +- **Ergonomics:** The hal-index uses `prev` links for natural backward navigation. The releases-index requires downloading all version files (2.4+ MB), filtering by date, and deduplicating results. +- **Navigation model:** The hal-index timeline is designed for chronological traversal. The releases-index has no concept of time-based navigation. + +**Winner:** hal-index (**27x smaller**) + +### Critical CVE This Month + +#### Query: "Is there a critical CVE in any supported release this month?" (November 2025) + +| Schema | Files Required | Total Transfer | +|--------|----------------|----------------| +| hal-index | `timeline/index.json` → `timeline/2025/index.json` → `timeline/2025/11/index.json` | **28 KB** | +| releases-index | `releases-index.json` + all supported releases.json | **2.4+ MB** (incomplete—no severity data) | + +**hal-index:** + +```bash +TIMELINE="https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/index.json" + +# Step 1: Get 2025 year href +YEAR_HREF=$(curl -s "$TIMELINE" | jq -r '._embedded.years[] | select(.year == "2025") | ._links.self.href') + +# Step 2: Get November month href +MONTH_HREF=$(curl -s "$YEAR_HREF" | jq -r '._embedded.months[] | select(.month == "11") | ._links.self.href') + +# Step 3: Check for CRITICAL CVEs +curl -s "$MONTH_HREF" | jq -r '._embedded.disclosures // [] | .[] | select(.cvss_severity == "CRITICAL") | "\(.id): \(.title)"' +# (no critical CVEs this month) +``` + +**releases-index:** + +```bash +ROOT="https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json" + +# Get all supported version releases.json URLs +URLS=$(curl -s "$ROOT" | jq -r '.["releases-index"][] | select(.["support-phase"] == "active") | .["releases.json"]') + +# Find November 2025 releases and check for CVEs (cannot determine severity) +for URL in $URLS; do + curl -s "$URL" | jq -r ' + .releases[] | + select(.["release-date"] | startswith("2025-11")) | + select(.security == true) | + .["cve-list"][]? | "\(.["cve-id"]): (severity unknown)"' +done | sort -u +# (no output—November 2025 had no security releases) +``` + +**Analysis:** + +- **Completeness:** ❌ The releases-index cannot answer this query. Even if there were CVEs in November, the schema only provides CVE IDs and URLs—no severity information. You would need to fetch each CVE URL from cve.mitre.org and parse the CVSS score. +- **Ergonomics:** The hal-index embeds `cvss_severity` directly in the disclosure records, enabling single-query filtering for CRITICAL vulnerabilities. +- **Use case:** This is a common security operations query ("Do I need to patch urgently?"). The hal-index answers it in 28 KB; the releases-index cannot answer it at all. + +**Winner:** hal-index (**88x smaller**, and releases-index cannot answer this query—CVE severity is not available) + +### Hybrid Queries + +The following queries combine version and timeline navigation, demonstrating the full power of the hal-index design. + +### CVE Exposure for EOL Version + +#### Query: "If I'm still on .NET 6, which CVEs am I likely exposed to?" + +This query finds all CVEs fixed in supported versions since .NET 6 went end-of-life. If you're running any .NET 6 version, you're potentially exposed to every vulnerability patched after .NET 6 stopped receiving updates. + +| Schema | Files Required | Total Transfer | +|--------|----------------|----------------| +| hal-index | `index.json` → `6.0/index.json` → timeline months (via `release-month` + `next` links) | **~50-100 KB** | +| releases-index | `releases-index.json` + all supported releases.json | **2.4+ MB** (partial—no severity data) | + +**hal-index:** + +```bash +ROOT="https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" + +# Configuration +EOL_VERSION="6.0" + +# Step 1: Get the EOL version's last patch and its timeline month link +VERSION_HREF=$(curl -s "$ROOT" | jq -r --arg ver "$EOL_VERSION" '._embedded.releases[] | select(.version == $ver) | ._links.self.href') +VERSION_DATA=$(curl -s "$VERSION_HREF") +LAST_PATCH=$(echo "$VERSION_DATA" | jq -r '._embedded.releases[0]') +LAST_PATCH_DATE=$(echo "$LAST_PATCH" | jq -r '.date | split("T")[0]') +MONTH_HREF=$(echo "$LAST_PATCH" | jq -r '._links["release-month"].href') +echo "Last .NET $EOL_VERSION patch: $LAST_PATCH_DATE" +# Last .NET 6.0 patch: 2024-11-12 + +# Step 2: Walk forward from EOL month, collecting CVEs +echo "" +echo "CVEs fixed after .NET $EOL_VERSION EOL:" +echo "========================================" + +CURRENT_HREF="$MONTH_HREF" +while [ -n "$CURRENT_HREF" ]; do + DATA=$(curl -s "$CURRENT_HREF") + + # Move to next month first (we want CVEs AFTER the EOL month) + CURRENT_HREF=$(echo "$DATA" | jq -r '._links.next.href // empty') + [ -z "$CURRENT_HREF" ] && break + + DATA=$(curl -s "$CURRENT_HREF") + YEAR_MONTH=$(echo "$DATA" | jq -r '"\(.year)-\(.month)"') + SECURITY=$(echo "$DATA" | jq -r '.security') + + if [ "$SECURITY" = "true" ]; then + echo "" + echo "$YEAR_MONTH:" + echo "$DATA" | jq -r '._embedded.disclosures[] | " \(.id) [\(.cvss_severity)]: \(.title)"' + fi + + CURRENT_HREF=$(echo "$DATA" | jq -r '._links.next.href // empty') +done +``` + +**Output:** + +```text +Last .NET 6.0 patch: 2024-11-12 + +CVEs fixed after .NET 6.0 EOL: +======================================== + +2025-01: + CVE-2025-21171 [HIGH]: .NET Remote Code Execution Vulnerability + CVE-2025-21172 [HIGH]: .NET and Visual Studio Remote Code Execution Vulnerability + CVE-2025-21173 [MEDIUM]: .NET Elevation of Privilege Vulnerability + CVE-2025-21176 [HIGH]: .NET and Visual Studio Remote Code Execution Vulnerability + +2025-03: + CVE-2025-24070 [HIGH]: ASP.NET Core Elevation of Privilege Vulnerability + +2025-04: + CVE-2025-26682 [HIGH]: .NET Denial of Service Vulnerability + +2025-05: + CVE-2025-26646 [MEDIUM]: .NET Spoofing Vulnerability + +2025-06: + CVE-2025-30399 [HIGH]: .NET Remote Code Execution Vulnerability + +2025-10: + CVE-2025-55247 [HIGH]: .NET Denial of Service Vulnerability + CVE-2025-55248 [MEDIUM]: .NET Denial of Service Vulnerability + CVE-2025-55315 [CRITICAL]: .NET Security Feature Bypass Vulnerability +``` + +**releases-index:** + +```bash +ROOT="https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json" + +# Configuration +EOL_VERSION="6.0" + +# Step 1: Get the EOL version's last patch date +EOL_RELEASES_URL=$(curl -s "$ROOT" | jq -r --arg ver "$EOL_VERSION" '.["releases-index"][] | select(.["channel-version"] == $ver) | .["releases.json"]') +LAST_PATCH_DATE=$(curl -s "$EOL_RELEASES_URL" | jq -r '.releases[0]["release-date"]') +echo "Last .NET $EOL_VERSION patch: $LAST_PATCH_DATE" + +# Step 2: Get all supported version releases.json URLs +URLS=$(curl -s "$ROOT" | jq -r '.["releases-index"][] | select(.["support-phase"] == "active") | .["releases.json"]') + +# Step 3: Find all security releases after the EOL date +echo "" +echo "CVEs fixed after .NET $EOL_VERSION EOL:" +echo "========================================" + +for URL in $URLS; do + curl -s "$URL" | jq -r --arg cutoff "$LAST_PATCH_DATE" ' + .releases[] | + select(.security == true) | + select(.["release-date"] > $cutoff) | + . as $rel | .["cve-list"][]? | "\($rel["release-date"]): \(.["cve-id"]) (severity unknown)"' +done | sort -u +``` + +**Output:** + +```text +Last .NET 6.0 patch: 2024-11-12 + +CVEs fixed after .NET 6.0 EOL: +======================================== +2025-01-14: CVE-2025-21172 (severity unknown) +2025-01-14: CVE-2025-21173 (severity unknown) +2025-01-14: CVE-2025-21176 (severity unknown) +2025-03-11: CVE-2025-24070 (severity unknown) +2025-04-08: CVE-2025-26682 (severity unknown) +2025-05-13: CVE-2025-26646 (severity unknown) +2025-06-10: CVE-2025-30399 (severity unknown) +2025-10-14: CVE-2025-55247 (severity unknown) +2025-10-14: CVE-2025-55248 (severity unknown) +2025-10-14: CVE-2025-55315 (severity unknown) +``` + +**Analysis:** + +- **Completeness:** ⚠️ Partial—the releases-index finds CVE IDs but misses CVE-2025-21171 (only affected .NET 9.0, not in "active" support phase query). More critically, it cannot provide severity or titles. +- **Actionability:** The hal-index output is immediately actionable—security teams can see that CVE-2025-55315 is CRITICAL and prioritize accordingly. The releases-index output requires manual lookup of each CVE. +- **Cross-index navigation:** The hal-index `release-month` link connects version and timeline indexes directly—no need to parse dates or search the timeline. Combined with `next` links, this enables natural forward traversal from any release. +- **Executive reporting:** "You're exposed to 1 CRITICAL, 6 HIGH, and 3 MEDIUM vulnerabilities" vs "You're exposed to 10 CVEs of unknown severity." + +**Winner:** hal-index (releases-index provides incomplete data with no severity information) + +## Cache Coherency + +### releases-index Problem + +```text +Client cache state: +├── releases-index.json (fetched now) +│ └── "latest-release": "9.0.12" ← Just updated +└── 9.0/releases.json (fetched 1 hour ago) + └── releases[0]: "9.0.11" ← Stale! + +Result: Client sees 9.0.12 as latest but can't find its data +``` + +### hal-index Solution + +```text +Client cache state: +├── index.json (fetched now) +│ └── _embedded.releases[]: includes 9.0 summary +└── 9.0/index.json (fetched now) + └── _embedded.releases[0]: "9.0.12" with full data + +Result: Each file is a consistent snapshot +``` + +The HAL `_embedded` pattern ensures that any data referenced within a document is included in that document. There are no "dangling pointers" to data that might not exist in a cached copy of another file. + +## Summary + +| Metric | hal-index | releases-index | +|--------|-------------|----------------| +| Basic version queries | 8 KB | 6 KB | +| CVE queries (latest security patch) | 52 KB | 1,239 KB | +| Recent CVEs (last 2 security releases) | 23 KB | 2.4 MB | +| CVEs in last 12 months | ~90 KB | 2.4 MB | +| Version diff with severity + products | ~80 KB | 1,239 KB (partial—no severity/products) | +| Cache coherency | ✅ Atomic | ❌ TTL mismatch risk | +| Query syntax | snake_case (dot notation) | kebab-case (bracket notation) | +| Link traversal | `._links.self.href` | `.["releases.json"]` | +| Boolean filters | `supported`, `security` | `support-phase == "active"` | +| CVE details | ✅ Full | ❌ ID + URL only | +| Timeline navigation | ✅ | ❌ | + +The hal-index schema is optimized for the queries that matter most to security operations, while maintaining cache coherency across CDN deployments. The use of boolean properties (`supported`) instead of enum comparisons (`support-phase == "active"`) reduces query complexity and eliminates the need to know the vocabulary of valid enum values. Counterintuitively, the deeper HAL link structure (`._links.self.href`) is more ergonomic than flat URL properties (`.["releases.json"]`) because consistent snake_case naming enables dot notation throughout the query path. diff --git a/accepted/2025/release-notes-information-graph/release-notes-information-graph.md b/accepted/2025/release-notes-information-graph/release-notes-information-graph.md new file mode 100644 index 000000000..3e695cc8c --- /dev/null +++ b/accepted/2025/release-notes-information-graph/release-notes-information-graph.md @@ -0,0 +1,879 @@ +# Exposing Release Notes as an Information graph + +Spec for LLMs: + +The rest is for you, friendly human. + +The .NET project has published release notes in JSON and markdown for many years. Our production of release notes has been based on the virtuous cloud-era idea that many deployment and compliance workflows require detailed and structured data to operate safely at scale. For the most part, a highly clustered set of consumers (like GitHub Actions and malware scanners) have adapted _their tools_ to _our formats_ to offer higher-level value to their users. That's all good. The LLM era is strikingly different where a much smaller set of information distributors (LLM model companies) _consume and expose diverse data in standard ways_ according to _their (shifting) paradigms_ to a much larger set of users. The task at hand is to adapt release notes for LLM consumption while continuing to serve and improve workflows for cloud users. + +Release notes are mechanism, not scenario. It likely difficult for users to keep up with and act on the constant stream of .NET updates, typically one or two times a month. Users often have more than one .NET major version deployed, further complicating this puzzle. Many users rely on update orchestrators like APT, Yum, and Visual Studio, however, it is unlikely that such tools cover all the end-points that users care about in a uniform way. It is important that users can reliably make good, straightforward, and timely decisions about their entire infrastructure, orchestrated across a variety of deployment tools. This is a key scenario that release notes serve. + +Obvious questions release notes should answer: + +- What has changed, since last month, since the last _.NET_ update, or since the last _user_ update. +- How many patches back is this machine? +- How/where can new builds be acquired +- Is a recent update more critical to deploy than "staying current"? +- How long until a given major release is EOL or has been EOL? +- What are known upgrade challenges? + +CIOs, CTOs, and others are accountable for maintaining efficient and secure continuity for a set of endpoints, including end-user desktops and cloud servers. They are unlikely to read markdown release notes or perform DIY `curl` + `jq` hacking with structured data, however, they will increasingly expect to be able to answer .NET-related compliance and deployment questions using chat assistants like Claude or Copilot. They may ask ChatGPT to compare treatment of an industry-wide CVE like [CVE-2023-44487](https://nvd.nist.gov/vuln/detail/cve-2023-44487) across multiple application stacks in their portfolio. This already works reasonably well, but fails when queries/prompts demand greater levels of detail with the expectation that they come from an authoritative source. It is very common to see assistants glean insight from a semi-arbitrary set of web pages with matching content. + +LLMs are _much more_ fickle relative to purpose-built tools. They are more likely to give up if release notes are not to their liking, instead relying on comfortable web search with its equal share of benefits and challenges. Regular testing (by the spec writer) of release notes with these chat assistants has demonstrated that LLMs are typically only satiated by a "Goldilocks meal". Obscure formats, large documents, and complicated workflows are unlikely to work well. The task at hand is to adapt our release notes publishing so that it works equally well for LLMs and purpose-built tools, exposes more scenario-targeted information, and avoids reliability and performance challenges of our current solution. + +In the early revisions of this project, the design followed much the same playbook as past schemas, modeling parent/child relationships, linking to more detailed sources of information, and describing information domains in custom schemas. That then progressed into wanting to expose summaries of high-value information from leaf nodes into the trunk or as far as root nodes. This approach didn't work well since the design was lacking a broader information architecure. A colleague noted that the design was [HATEOS](https://en.wikipedia.org/wiki/HATEOAS)-esque but not using one of the standard formats. The benefits of using standard formats is that they are free to use, have gone through extensive design review, can be navigated with standard patterns and tools, and (most importantly) LLMs already understand their vocabulary and access patterns. A new format will be definition not have those characteristics. + +This proposal leans heavily on hypermedia, specifically [HAL+JSON](https://datatracker.ietf.org/doc/html/draft-kelly-json-hal). Hypermedia is very common, with [OpenAPI](https://www.openapis.org/what-is-openapi) and [JSON:API](https://jsonapi.org/) likely being the most common. LLMs are quite comfortable with these standards. The proposed use of HAL is a bit novel (not intended as a positive description). It is inspired by [llms.txt](https://llmstxt.org/) as an emerging standard and the idea that hypermedia is be the most natural next step to express complex diverse data and relationships. It's also expected (in fact, pre-ordained) that older standards will perform better than newer ones due to higher density (or presence at all) in the LLM training data. + +Overall goals: + +- Enable queries with multiple key styles, temporal and version-based queries. +- Describe runtime and SDK versions (as much as appropriate) at parity. +- Intergrate high value data, such as CVE disclosures, breaking changes, and download links. +- Ensure high-performance (low kb cost) and high reliability (TTL resilience). +- Enable aestheticly pleasing queries that are terse, ergonomic, and effective. +- Generate most release note files, like [releases.md](https://github.com/dotnet/core/blob/main/releases.md), and CVE announcements like [CVE-2025-55248](https://github.com/dotnet/announcements/issues/372). +- Use this project as a real-world pilot for exposing an information graph equally to LLMs, client libraries, and DIY `curl` + `jq` hacking. + +## Hypermedia graph design + +This project has adopted the idea that a wide and deep information graph can expose significant information within the graph that satisfies user queries without loading other files. The graph doesn't need to be skeletal. It can have some shape on it. In fact our existing graph with [`release-index.json`](https://github.com/dotnet/core/blob/main/release-notes/releases-index.json) already does this but without the benefit of a standard format or architectural principles. + +The design intent is that a graph should be skeletal at its roots for performance and to avoid punishing queries that do not benefit from the curated shape. The deeper the node is in the graph, the more shape (or weight) it should take on since the data curation is much more likely to hit the mark. + +Hypermedia formats have a long history of satisfying this methodology, long pre-dating, and actually inspiring the World Wide Web and its Hypertext Markup Language (HTML). This project uses [HAL+JSON](https://en.wikipedia.org/wiki/Hypertext_Application_Language) as the "graph format". HAL is a sort of a "hypermedia in a nutshell" schema, initally drafted in 2012. You can develop a basic understanding of HAL in about two minutes because it has a very limited syntax. + +For the most part, HAL defines just two properties: + +- `_links` -- links to resources. +- `_embedded` -- embedded resources, which may include more HAL-style links. + +It seems like this is hardly enough to support the ambitious design approach that has been described. It turns out that the design is more clever than first blush would suggest. + +There is an excellent Australian movie that comes to mind, [The Castle](https://www.imdb.com/title/tt0118826). + +> Judge: “What section of the constitution has been breached?” +> Dennis Denuto: "It’s the constitution. It’s Mabo. It’s justice. It’s law. It’s the vibe … no, that’s it, it’s the vibe. I rest my case" + +HAL is much the same. It defines an overall approach that a schema designer can hang off of these two seemingly understated properties. You just have to follow the vibe of it. + +Here is a simple example from the HAL spec: + +```json +{ + "_links": { + "self": { "href": "/orders/523" }, + "warehouse": { "href": "/warehouse/56" }, + "invoice": { "href": "/invoices/873" } + }, + "currency": "USD", + "status": "shipped", + "total": 10.20 +} +``` + +The `_links` property is a dictionary of link objects with specific named relations. Most links dictionaries start with the standard `self` relation. The `self` relation describes the canonical URL of the given resource. The `warehouse` and `invoice` relations are examples of domain-specific relations. Together, they establish a navigation protocol for this resource domain. One can also imagine `next`, `previous`, `buy-again` as being equally applicable relations for e-commerce. Domain-specific HAL readers will understand these relations and know how or when to act on them. + +The `currency`, `status`, and `total` properties provide additional domain-specific resource metadata. The package should arrive at your door soon! + +The following example is similar, with the addition of the `_embedded` property. + +```json +{ + "_links": { + "self": { "href": "/orders" }, + "next": { "href": "/orders?page=2" }, + "find": { "href": "/orders{?id}", "templated": true } + }, + "_embedded": { + "orders": [{ + "_links": { + "self": { "href": "/orders/123" }, + "basket": { "href": "/baskets/98712" }, + "customer": { "href": "/customers/7809" } + }, + "total": 30.00, + "currency": "USD", + "status": "shipped", + },{ + "_links": { + "self": { "href": "/orders/124" }, + "basket": { "href": "/baskets/97213" }, + "customer": { "href": "/customers/12369" } + }, + "total": 20.00, + "currency": "USD", + "status": "processing" + }] + }, + "currentlyProcessing": 14, + "shippedToday": 20 +} +``` + +The `_embedded` property contains order resources. This is the document payload. Each of those order items have `self` and other related link relations referencing other resources. As stated earlier, the `self` relation references the canonical copy of the resource. Embedded resources may be a full or partial copy of the resource. A domain-specific reader will have a deeper understanding of the resource rules and associated schema. + +This design aspect is the true strength of HAL. It's the mechanism that enables the overall approach of a skeletal root with weighted bottom nodes. It's also what enables these two seemingly aenemic properties to provide so much modeling value. + +The `currentlyProcessing` and `shippedToday` properties provide additional information about ongoing operations. + +We've now seen representative examples of both properties in use. You are now a HAL expert! We can now look at how the same vibe can be applied to .NET release notes. + +## Release Notes Graph + +Release notes naturally describe two information dimensions: time and product version. + +- Within time, we have years, months, and (ship) days. +- Within version, we have major and patch version. We also have runtime vs SDK version. + +These dimensional characteristics form the load-bearing structure of the graph to which everything else is attached. We will have both a timeline and version indices. We've previously only had a version index. + +The following table summarizes the overall shape of the graph, starting at `dotnet/core:release-notes/index.json`. + +| File | Type | Updates when... | Frequency | +|------|------|-----------------|-----------| +| `index.json` (root) | Release version index | New major version, version phase changes | 1x/year | +| `timeline/index.json` | Release timeline index | New year starts, new major version | 1x/year | +| `timeline/{year}/index.json` | Year index | New month with activity, phase changes | 12x/year | +| `timeline/{year}/{month}/index.json` | Month index | Never (immutable after creation) | 0 | +| `{version}/index.json` | Major version index | New patch release | 12x/year | +| `{version}/{patch}/index.json` | Patch version index | Never (immutable after creation) | 0 | + +**Summary:** Cold roots, warm branches, immutable leaves. + +Note: SDK-only releases break the immutable claim a little, but not much. + +We can contrast this approach with the existing release graph, using the last 12 months of commit data (Nov 2024-Nov 2025). + +| File | Commits | Notes | +|------|---------|-------| +| `releases-index.json` | 29 | Root index (all versions) | +| `10.0/releases.json` | 22 | Includes previews/RCs and SDK-only releases | +| `9.0/releases.json` | 24 | Includes SDK-only releases, fixes, URL rewrites | +| `8.0/releases.json` | 18 | Includes SDK-only releases, fixes, URL rewrites | + +**Summary:** Hot everything. + +Conservatively, the existing commit counts are not good. The `releases-index.json` file is a mission-critical live-site resource. 29 updates is > 2x/month! + +### Graph Rules + +The graph has one rule: + +> Every resource in the graph needs to be guaranteed consistent with every other part of the graph. + +The unstated problem is CDN caching. Assume that the entire graph is consistent when uploaded to an origin server. A CDN server is guaranteed by construction to serve both old and new copies of the graph leading to potential inconsistencies. The graph construction needs to be resilient to that. + +Related examples: + +- +- + +Today, we publish [releases-index.json](https://github.com/dotnet/core/blob/main/release-notes/releases-index.json) as the root of our release notes information graph. Some users read this JSON file to learn the latest patch version numbers, while others navigate deeper into the graph. Both are legitimate patterns. However, we've found that our approach has fundamental flaws. + +Problems: + +- Exposing patch versions in multiple files that need to agree is incompatible with using a Content Delivery Network (CDN) that employs standard caching (expiration / TTL). +- The `releases-index.json` file is a critical live site resource driving 1000s of GBs of downloads a month, yet we manually update it multiple times a month, including for previews. + +Solution: + +- Fast changing currency (like patch version numbers) are exposed in (at most) a single resource in the graph. +- The root index file is updated once a year (to add the presence of a new major release). + +The point about the root index isn't a "solution" but an implication of the first point. If the root index isn't allowed to contain fast-moving currency, because the canonical location is another resource, then it is stripped of its reason to change. That will be addressed later in the document. + +There are videos on YouTube with these [crazy gear reductions](https://www.youtube.com/watch?v=QwXK4e4uqXY). You can watch them for a long time! Keen observers will realize our graph will be nothing like that. Well, kindof. One can model years and months and major and patch versions as spinning gears with a differing number of teeth and revolution times. It just won't look the same as those lego videos. + +A celestial orbit analogy would have worked just as well. + +Release notes graph indexes operate like the following (ignoring some annoying details): + +- Timeline index (list of years): one update per year +- Year index (list of months): one update per month +- Month index (list of patches across versions): one update (immutable) + +The same progression for versions: + +- Releases index (list of of major versions): one update per year +- Major version index (list of patches): one update per month +- Patch version index (details about a patch): one update (immutable) + +It's the middle section changing constantly, but the roots and the leaves are either immutable or close enough to it. + +### Resource modeling + +The following example demonstrates what HAL JSON looks like generally. Each node in the graph is named `index.json`. This is the root [index.json](https://github.com/dotnet/core/blob/release-index/release-notes/index.json) file that represents all .NET versions. It exposes the same general information as the existing `releases-index.json`. + +```json +{ + "$schema": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/schemas/dotnet-release-version-index.json", + "kind": "releases-index", + "title": ".NET Release Index", + "description": ".NET Release Index (latest: 10.0)", + "latest": "10.0", + "latest_lts": "10.0", + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json", + "path": "/index.json", + "title": ".NET Release Index", + "type": "application/hal\u002Bjson" + }, + "latest": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/index.json", + "path": "/10.0/index.json", + "title": "Latest .NET release (.NET 10.0)", + "type": "application/hal\u002Bjson" + }, + "latest-lts": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/index.json", + "path": "/10.0/index.json", + "title": "Latest LTS release (.NET 10.0)", + "type": "application/hal\u002Bjson" + }, +``` + +Key points: + +- Schema reference is included +- `kind`, `title`, and `description` describe the resource +- Additional kind-specific properties, like `latest`, describe high-level resource metadata, often useful currency that helps contextualize the rest of the resource without the need to parse/split strings. For example, the `latest_lts` scalar describes the target of the `latest-lts` link relation. +- `_links` and `_embedded` as appropriate. +- Core schema syntax like `latest_lts` uses snake-case-lower for query ergonomics (using `jq` as the proxy for that), while relations like `latest-lts` use kebab-case-lower since they can be names or brands. This follows the approach used by [cve-schema](https://github.com/dotnet/designs/blob/main/accepted/2025/cve-schema/cve_schema.md#brand-names-vs-schema-fields-mixed-naming-strategy). + +Here is the first couple objects within the `_embedded` property, in the same root index: + +```json + "_embedded": { + "releases": [ + { + "version": "10.0", + "release_type": "lts", + "supported": true, + "eol_date": "2028-11-14T00:00:00\u002B00:00", + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/index.json", + "title": ".NET 10.0", + "type": "application/hal\u002Bjson" + } + } + }, + { + "version": "9.0", + "release_type": "sts", + "supported": true, + "eol_date": "2026-11-10T00:00:00\u002B00:00", + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/index.json", + "title": ".NET 9.0", + "type": "application/hal\u002Bjson" + } + } + }, +``` + +This is where we see the design diverge significantly from `releases-index.json`. There are no patch versions, no statement about security releases. It's the most minimal data to determine the release type, if/when it is supported until, and how to access the canonical resource that exposes richer information. This approach removes the need to update the root index monthly. + +Let's look at another section of the graph, the [major version index for .NET 9](https://github.com/dotnet/core/blob/release-index/release-notes/9.0/index.json). + +```json +{ + "$schema": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/schemas/dotnet-release-version-index.json", + "kind": "major-version-index", + "title": ".NET 9.0 Patch Release Index", + "description": ".NET 9.0 (latest: 9.0.11)", + "latest": "9.0.11", + "latest_security": "9.0.10", + "release_type": "sts", + "phase": "active", + "supported": true, + "ga_date": "2024-11-12T00:00:00\u002B00:00", + "eol_date": "2026-11-10T00:00:00\u002B00:00", + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/index.json", + "path": "/9.0/index.json", + "title": ".NET 9.0", + "type": "application/hal\u002Bjson" + }, + "latest": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/9.0.11/index.json", + "path": "/9.0/9.0.11/index.json", + "title": "Latest patch release (9.0.11)", + "type": "application/hal\u002Bjson" + }, + "latest-sdk": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/sdk/index.json", + "path": "/9.0/sdk/index.json", + "title": ".NET SDK 9.0 Release Information", + "type": "application/hal\u002Bjson" + }, + "latest-security": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/9.0.10/index.json", + "path": "/9.0/9.0.10/index.json", + "title": "Latest security patch (9.0.10)", + "type": "application/hal\u002Bjson" + }, +``` + +This index includes much more useful and detailed information, both metadata/currency and patch-version links. It starts to answer the question of "what should I care about _now_?". + +Let's also look at one of the objects from the `_embedded` section as well. + +```json + { + "version": "9.0.10", + "date": "2025-10-14T00:00:00\u002B00:00", + "year": "2025", + "month": "10", + "security": true, + "cve_count": 3, + "cve_records": [ + "CVE-2025-55247", + "CVE-2025-55248", + "CVE-2025-55315" + ], + "support_phase": "active", + "sdk_patches": [ + "9.0.306", + "9.0.111" + ], + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/9.0.10/index.json", + "path": "/9.0/9.0.10/index.json", + "title": "9.0.10 Patch Index", + "type": "application/hal\u002Bjson" + }, + "release-month": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/10/index.json", + "path": "/timeline/2025/10/index.json", + "title": "Release timeline index for 2025-10", + "type": "application/hal\u002Bjson" + }, + "cve-json": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/10/cve.json", + "path": "/timeline/2025/10/cve.json", + "title": "CVE Information", + "type": "application/json" + } + } + }, +``` + +This patch-version object contains even more high-level information that can drive deployment and compliance workflows. The first two link relations are HAL links. The last is a plain JSON link. Non-HAL links end in the format, like `json` or `markdown` or `markdown-rendered`. The links are raw text by default, with `-rendered` HTML content being useful for content targeted for human consumption, for example in generated release notes. + +The design has a concept of "wormhole links". That's what we see with `release-month`. It provides direct access to a high-relevance (potentially graph-distant) resource that would otherwise require awkward indirections, multiple network hops, and wasted bytes/tokens to acquire. These wormhole links massively improve query ergonomics for sophisticated queries. There are multiple of these wormhole links, not just `release-month` that are sprinkled throughout the graph for this purpose. They also provide hints on how the graph is intended to be traversed. + +There is a link `cve.json` file. Our [CVE schema](https://github.com/dotnet/designs/blob/main/accepted/2025/cve-schema/cve_schema.md) is a custom schema with no HAL vocabulary. It's an exit node of the graph. The point is that we're free to describe complex domains, like CVE disclosures, using a clean-slate design methodology. One can also see that some of the `cve.json` information has been projected into the graph, adding high-value shape over the skeleton. + +This one-level-lower index is more nuanced to what we saw earlier with the root version index. As stated, there is a lot more useful detailed currency on offer. However, there is a rule that currency needs to be guaranteed consistent. Let's consider if the rule is obeyed. The important characteristic is that listed versions and links _within_ the resource are consistent by virtue of being _captured_ in the same file. The critical trick is with the links. The link origin is a fast moving resource and target resources are immutable. That combination works. It's easy to be consistent with something immutable, that either exists or doesn't. In contrast, there would be a problem if there was a link between two mutable resources that expose the same currency. This is the problem that `releases-index.json` has. + +The following example is a patch version index, for [9.0.10](https://github.com/dotnet/core/blob/release-index/release-notes/9.0/9.0.10/index.json). + +```json +{ + "$schema": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/schemas/dotnet-patch-detail-index.json", + "kind": "patch-version-index", + "title": ".NET 9.0.10 Patch Index", + "description": "Patch information for .NET 9.0.10", + "version": "9.0.10", + "date": "2025-10-14T00:00:00\u002B00:00", + "support_phase": "active", + "security": true, + "cve_count": 3, + "cve_records": [ + "CVE-2025-55247", + "CVE-2025-55248", + "CVE-2025-55315" + ], + "sdk_patches": [ + "9.0.306", + "9.0.111" + ], + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/9.0.10/index.json", + "path": "/9.0/9.0.10/index.json", + "title": "9.0.10 Patch Index", + "type": "application/hal\u002Bjson" + }, + "next": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/9.0.11/index.json", + "path": "/9.0/9.0.11/index.json", + "title": "9.0.11 Patch Index", + "type": "application/hal\u002Bjson" + }, + "prev": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/9.0.9/index.json", + "path": "/9.0/9.0.9/index.json", + "title": "9.0.9 Patch Index", + "type": "application/hal\u002Bjson" + }, + "latest-sdk": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/sdk/index.json", + "path": "/9.0/sdk/index.json", + "title": ".NET SDK 9.0 Release Information", + "type": "application/hal\u002Bjson" + }, +``` + +This content looks much the same as we saw earlier, except that much of the content we saw in the patch object is now exposed at index root. That's not coincidental, but a key aspect of the model. + +The `next` and `prev` link relations provide some more wormholes, this time to less distant targets. The `latest-sdk` target provides access to `aka.ms` evergreen SDK links and other SDK-related information. The `release-month` and `cve-json` links are still there, but a bit further down the dictionary definition as to what's copied above. + +The `_embedded` property contains a description of all the SDKs released at the same time as the runtime. + +```json + "_embedded": { + "sdk": [ + { + "version": "9.0.306", + "_links": { + "feature-band": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/sdk/sdk-9.0.3xx.json", + "path": "/9.0/sdk/sdk-9.0.3xx.json", + "title": ".NET SDK 9.0.3xx", + "type": "application/json" + }, + "release-notes-markdown": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/9.0.10/9.0.10.md", + "path": "/9.0/9.0.10/9.0.10.md", + "title": "9.0.10 Release Notes", + "type": "application/markdown" + }, + "release-notes-markdown-rendered": { + "href": "https://github.com/dotnet/core/blob/main/release-notes/9.0/9.0.10/9.0.10.md", + "path": "/9.0/9.0.10/9.0.10.md", + "title": "9.0.10 Release Notes (Rendered)", + "type": "application/markdown" + } + } + }, + { + "version": "9.0.111", + "_links": { + "feature-band": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/sdk/sdk-9.0.1xx.json", + "path": "/9.0/sdk/sdk-9.0.1xx.json", + "title": ".NET SDK 9.0.1xx", + "type": "application/json" + }, + "release-notes-markdown": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/9.0.10/9.0.111.md", + "path": "/9.0/9.0.10/9.0.111.md", + "title": "SDK 9.0.111 Release Notes", + "type": "application/markdown" + }, + "release-notes-markdown-rendered": { + "href": "https://github.com/dotnet/core/blob/main/release-notes/9.0/9.0.10/9.0.111.md", + "path": "/9.0/9.0.10/9.0.111.md", + "title": "SDK 9.0.111 Release Notes (Rendered)", + "type": "application/markdown" + } + } + } + ], +``` + +There is an `sdk*.json` that matches each feature band. It is largely the same as `sdk/index.json` but more specific. The rest of the links are for markdown release notes. + +Any CVEs for the month are described in `disclosures`. + +```json + "disclosures": [ + { + "id": "CVE-2025-55247", + "title": ".NET Denial of Service Vulnerability", + "_links": { + "self": { + "href": "https://github.com/dotnet/announcements/issues/370", + "title": "CVE-2025-55247" + } + }, + "cvss_score": 7.3, + "cvss_severity": "HIGH", + "disclosure_date": "2025-10-14", + "affected_releases": [ + "8.0", + "9.0" + ], + "affected_products": [ + "dotnet-sdk" + ], + "platforms": [ + "linux" + ] + }, + { + "id": "CVE-2025-55248", + "title": ".NET Information Disclosure Vulnerability", + "_links": { + "self": { + "href": "https://github.com/dotnet/announcements/issues/372", + "title": "CVE-2025-55248" + } + }, + "fixes": [ + { + "href": "https://github.com/dotnet/runtime/commit/18e28d767acf44208afa6c4e2e67a10c65e9647e.diff", + "repo": "dotnet/runtime", + "branch": "release/9.0", + "title": "Fix commit in runtime (release/9.0)", + "release": "9.0" + } + ], + "cvss_score": 4.8, + "cvss_severity": "MEDIUM", + "disclosure_date": "2025-10-14", + "affected_releases": [ + "8.0", + "9.0" + ], + "affected_products": [ + "dotnet-runtime" + ], + "platforms": [ + "all" + ] + }, +``` + +It's possible to make detail-oriented compliance and deployment decisions based on this information. There's even a commit for the CVE fix with an LLM friendly link style. This is the bottom part of the hypermedia graph. It's far more shapely and weightier than the root. If a consumer gets this far, it is likely because they need access to the exposed information. If they only wanted access to the `cve.json` file, they could have accessed it in the major version index, where it is also made available. + +The timeline index is different, but follows much the same approach. + +## Design tradeoffs + +There are lots of design tradeoffs within the graph design. Ergonomics vs update velocity were perhaps the fiercest of foes in the design. + +As mentioned multiple times, graph consistency is a major design requirement. The primary consideration is avoiding exposing currency that can be misused. If that's avoided, then there are no concerns with CDN consistency. + +If we cleverly apply these rules, we can actually expand these major version objects in our root releases index with convenience links, like the following: + +```json + "_embedded": { + "releases": [ + { + "version": "10.0", + "release_type": "lts", + "supported": true, + "eol_date": "2028-11-14T00:00:00\u002B00:00", + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/index.json", + "title": ".NET 10.0", + "type": "application/hal\u002Bjson" + }, + "latest-patch": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/10.0.1/index.json", + "title": ".NET 10.0.1", + "type": "application/hal\u002Bjson" + }, + "release-month": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/11/index.json", + "path": "/timeline/2025/11/index.json", + "title": "Release timeline index for 2025-11", + "type": "application/hal\u002Bjson" + } + } + }, + { + "version": "9.0", + "release_type": "sts", + "supported": true, + "eol_date": "2026-11-10T00:00:00\u002B00:00", + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/index.json", + "title": ".NET 9.0", + "type": "application/hal\u002Bjson" + }, + "latest-patch": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/9.0.9/index.json", + "title": ".NET 9.0.9", + "type": "application/hal\u002Bjson" + }, + "release-month": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/11/index.json", + "path": "/timeline/2025/11/index.json", + "title": "Release timeline index for 2025-11", + "type": "application/hal\u002Bjson" + } + } + }, +``` + +That would be _so nice_. Consumers could wormhole to high value content without needing to go through the major version index or the year to get to the intended target. From a query ergonomics standpoint, this structure would be superior. + +This approach doesn't violate the consistency rules. There is no badly-behaved currency that can be mis-used. The links are opague and notably target immutable resources. So, why not? Why can't we have nice things? + +The issue is that these high-value links would require updating the root index once a month. Regular updates of a high-value resources signficantly increase the likelihood of an outage and reduces the time that the root index can be cached. Cache aggressiveness is part of the performance equation. It's much better to keep the root lean (skeletal) and highly cacheable. + +There has been significant discussion on consistency. We might as well complete the lesson. + +The following example, with `latest_patch`, is what would cause the worst pain. + +```json + "_embedded": { + "releases": [ + { + "version": "10.0", + "release_type": "lts", + "supported": true, + "eol_date": "2028-11-14T00:00:00\u002B00:00", + "latest_patch": "10.0.1", + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/index.json", + "title": ".NET 10.0", + "type": "application/hal\u002Bjson" + }, + "latest-patch": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/10.0.1/index.json", + "title": ".NET 10.0.1", + "type": "application/hal\u002Bjson" + }, + "release-month": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/11/index.json", + "path": "/timeline/2025/11/index.json", + "title": "Release timeline index for 2025-11", + "type": "application/hal\u002Bjson" + } + } + }, + { + "version": "9.0", + "release_type": "sts", + "supported": true, + "eol_date": "2026-11-10T00:00:00\u002B00:00", + "latest_patch": "9.0.9", + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/index.json", + "title": ".NET 9.0", + "type": "application/hal\u002Bjson" + }, + "latest-patch": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/9.0.9/index.json", + "title": ".NET 9.0.9", + "type": "application/hal\u002Bjson" + }, + "release-month": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/11/index.json", + "path": "/timeline/2025/11/index.json", + "title": "Release timeline index for 2025-11", + "type": "application/hal\u002Bjson" + } + } + }, +``` + +The `latest_patch` property is the "fast-moving currency" that the design attempts to avoid. A consumer can now take the `10.0.1` value and try it on for size in `10.0/index.json`. It might not fit. That's the exact problem we have in `release-index.json` today. Let's not recreate it. + +Another classic movie -- A Few Good Men (1992): + +> Judge Randolph: You don't have to answer that question! +> Jessup: I'll answer the question. You want answers? +> Kaffee: I think I'm entitled to it! +> Jessup: You want answers?! +> Kaffee: I WANT THE TRUTH! +> Jessup: You can't handle the truth! + +This provides perfect clarity on why we cannot include the `latest_patch` propery, even if we might feel entitled to it. + +> "I don't give a DAMN what you think you are entitled to!" + +Source: A Few Good Men (1992; 10s later) + +That would seems to close the book on convenience. + +### Modeling as validation + +As the final graph took shape, distinct relationships inherent to the resource modeling started to emerge. + +- Parent <-> child -- Represents a shift in information depth, scalar <-> vector. The object data (within `_embedded`) in the parent becomes the root metadata in the child, and more complex children appear than were in the parent. +- Version <-> timeline -- Represents an inversion of information, using a different key--temporal or version--to access the same information. The version index converges to a point while the timeline index converges to a slice or row of points. + +This reflection on resource modeling is similar to the mathematic concept of [duals](https://en.wikipedia.org/wiki/Duality_(mathematics)). The elements of the graph are not duals, however, the time and version keys likely are. + +We can also reason about shape in terms of a storage analogy. + +Storage for a 3-level hypermedia design: + +- Root/outer nodes: flat scalars or tuples -- filter operation +- Middle nodes: nested documents -- traverse operation +- Exit nodes: indexed documents -- query operation + +This analogy is attempting to demonstrate the kind of data exposed at each level and the most sophisticated operation that the node can satisfy. The term "document" is intended to align with "document" in "document database". + +These formal descriptions may not help everyone, however, it was used as part of the design process. It can helpful to consider the inherent nature of the data to validate the shape once it is concretely modeled. One can consider the release notes data from one point in the graph to another and pre-conceptualize what it should be accroding to these transformation rules. If it matches, it is likely correct. That's an important approach that was used for graph validation. + +## Quality metrics + +Query metrics were assessed with the earlier CVE schema project. This time, we can do that an also reason about network cost. + +See [metrics.md](./metrics.md) for an in-depth analysis. A few representative tests are included in this document. + +### Query: "What .NET versions are currently supported?" + +| Schema | Files Required | Total Transfer | +|--------|----------------|----------------| +| hal-index | `index.json` | **8 KB** | +| releases-index | `releases-index.json` | **6 KB** | + +**hal-index:** + +```bash +ROOT="https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" + +curl -s "$ROOT" | jq -r '._embedded.releases[] | select(.supported) | .version' +# 10.0 +# 9.0 +# 8.0 +``` + +**releases-index:** + +```bash +ROOT="https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json" + +curl -s "$ROOT" | jq -r '.["releases-index"][] | select(.["support-phase"] == "active") | .["channel-version"]' +# 10.0 +# 9.0 +# 8.0 +``` + +**Analysis:** + +- **Completeness:** ✅ Equal—both return the same list of supported versions. +- **Boolean vs enum:** The hal-index uses `supported: true`, a simple boolean. The releases-index uses `support-phase: "active"`, requiring knowledge of the enum vocabulary (active, maintenance, eol, preview, go-live). +- **Property naming:** The hal-index uses `select(.supported)` with dot notation. The releases-index requires `select(.["support-phase"] == "active")` with bracket notation and string comparison. +- **Query complexity:** The hal-index query is 30% shorter and more intuitive for someone unfamiliar with the schema. + +**Winner:** releases-index (**1.3x smaller** for basic version queries, but hal-index has better query ergonomics) + +### CVE Queries for Latest Security Patch + +#### Query: "What CVEs were fixed in the latest .NET 8.0 security patch?" + +| Schema | Files Required | Total Transfer | +|--------|----------------|----------------| +| hal-index | `index.json` → `8.0/index.json` → `8.0/8.0.21/index.json` | **52 KB** | +| releases-index | `releases-index.json` + `8.0/releases.json` | **1,239 KB** | + +**hal-index:** + +```bash +ROOT="https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" + +# Step 1: Get the 8.0 version href +VERSION_HREF=$(curl -s "$ROOT" | jq -r '._embedded.releases[] | select(.version == "8.0") | ._links.self.href') +# https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/8.0/index.json + +# Step 2: Get the latest security patch href +PATCH_HREF=$(curl -s "$VERSION_HREF" | jq -r '._links["latest-security"].href') +# https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/8.0/8.0.21/index.json + +# Step 3: Get the CVE records +curl -s "$PATCH_HREF" | jq -r '.cve_records[]' +# CVE-2025-55247 +# CVE-2025-55248 +# CVE-2025-55315 +``` + +**releases-index:** + +```bash +ROOT="https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json" + +# Step 1: Get the 8.0 releases.json URL +RELEASES_URL=$(curl -s "$ROOT" | jq -r '.["releases-index"][] | select(.["channel-version"] == "8.0") | .["releases.json"]') +# https://builds.dotnet.microsoft.com/dotnet/release-metadata/8.0/releases.json + +# Step 2: Find latest security release and get CVE IDs +curl -s "$RELEASES_URL" | jq -r '[.releases[] | select(.security == true)] | .[0] | .["cve-list"][] | .["cve-id"]' +# CVE-2025-55247 +# CVE-2025-55315 +# CVE-2025-55248 +``` + +**Analysis:** Both schemas produce the same CVE IDs. However: + +- **Completeness:** ✅ Equal—both return the CVE identifiers +- **Ergonomics:** The releases-index requires downloading a 1.2 MB file to extract 3 CVE IDs. The hal-index uses a dedicated `latest-security` link, avoiding iteration through all releases. +- **Link syntax:** Counterintuitively, the deeper HAL structure `._links.self.href` is more ergonomic than `.["releases.json"]` because snake_case enables dot notation throughout. The releases-index embeds URLs directly in properties, but kebab-case naming forces bracket notation. +- **Data efficiency:** hal-index is 23x smaller + +**Winner:** hal-index (**23x smaller**) + +### Query: "List all CVEs fixed in the last 12 months" + +| Schema | Files Required | Total Transfer | +|--------|----------------|----------------| +| hal-index | `timeline/index.json` → up to 12 month indexes (via `prev` links) | **~90 KB** | +| releases-index | All version releases.json files | **2.4+ MB** | + +**hal-index:** + +```bash +TIMELINE="https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/index.json" + +# Step 1: Get the latest month href +MONTH_HREF=$(curl -s "$TIMELINE" | jq -r '._embedded.years[0]._links["latest-month"].href') + +# Step 2: Walk back 12 months using prev links, collecting security CVEs +for i in {1..12}; do + DATA=$(curl -s "$MONTH_HREF") + YEAR_MONTH=$(echo "$DATA" | jq -r '"\(.year)-\(.month)"') + SECURITY=$(echo "$DATA" | jq -r '.security') + if [ "$SECURITY" = "true" ]; then + CVES=$(echo "$DATA" | jq -r '[._embedded.disclosures[].id] | join(", ")') + echo "$YEAR_MONTH: $CVES" + fi + MONTH_HREF=$(echo "$DATA" | jq -r '._links.prev.href // empty') + [ -z "$MONTH_HREF" ] && break +done +# 2025-10: CVE-2025-55248, CVE-2025-55315, CVE-2025-55247 +# 2025-06: CVE-2025-30399 +# 2025-05: CVE-2025-26646 +# 2025-04: CVE-2025-26682 +# 2025-03: CVE-2025-24070 +# 2025-01: CVE-2025-21171, CVE-2025-21172, CVE-2025-21176, CVE-2025-21173 +``` + +**releases-index:** + +```bash +ROOT="https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json" + +# Get all supported version releases.json URLs +URLS=$(curl -s "$ROOT" | jq -r '.["releases-index"][] | select(.["support-phase"] == "active") | .["releases.json"]') + +# For each version, find security releases in the last 12 months +CUTOFF="2024-12-01" +for URL in $URLS; do + curl -s "$URL" | jq -r --arg cutoff "$CUTOFF" ' + .releases[] | + select(.security == true) | + select(.["release-date"] >= $cutoff) | + "\(.["release-date"]): \([.["cve-list"][]? | .["cve-id"]] | join(", "))"' +done | sort -u | sort -r +# 2025-10-14: CVE-2025-55247, CVE-2025-55315, CVE-2025-55248 +# 2025-06-10: CVE-2025-30399 +# 2025-05-22: CVE-2025-26646 +# 2025-04-08: CVE-2025-26682 +# 2025-03-11: CVE-2025-24070 +# 2025-01-14: CVE-2025-21172, CVE-2025-21173, CVE-2025-21176 +``` + +**Analysis:** + +- **Completeness:** ⚠️ Partial—the releases-index can list CVEs by date, but notice CVE-2025-21171 is missing (it only affected .NET 9.0 which was still in its first patch cycle). The output also shows exact dates rather than grouped by month. +- **Ergonomics:** The hal-index uses `prev` links for natural backward navigation. The releases-index requires downloading all version files (2.4+ MB), filtering by date, and deduplicating results. +- **Navigation model:** The hal-index timeline is designed for chronological traversal. The releases-index has no concept of time-based navigation. + +**Winner:** hal-index (**27x smaller**) From 34b455da1a54777fdda7dca2cdc8f340dbe4f611 Mon Sep 17 00:00:00 2001 From: Richard Lander Date: Fri, 5 Dec 2025 08:25:19 -0800 Subject: [PATCH 02/15] Update spec --- .../release-notes-information-graph.md | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/accepted/2025/release-notes-information-graph/release-notes-information-graph.md b/accepted/2025/release-notes-information-graph/release-notes-information-graph.md index 3e695cc8c..1e9a0b25e 100644 --- a/accepted/2025/release-notes-information-graph/release-notes-information-graph.md +++ b/accepted/2025/release-notes-information-graph/release-notes-information-graph.md @@ -21,7 +21,7 @@ CIOs, CTOs, and others are accountable for maintaining efficient and secure cont LLMs are _much more_ fickle relative to purpose-built tools. They are more likely to give up if release notes are not to their liking, instead relying on comfortable web search with its equal share of benefits and challenges. Regular testing (by the spec writer) of release notes with these chat assistants has demonstrated that LLMs are typically only satiated by a "Goldilocks meal". Obscure formats, large documents, and complicated workflows are unlikely to work well. The task at hand is to adapt our release notes publishing so that it works equally well for LLMs and purpose-built tools, exposes more scenario-targeted information, and avoids reliability and performance challenges of our current solution. -In the early revisions of this project, the design followed much the same playbook as past schemas, modeling parent/child relationships, linking to more detailed sources of information, and describing information domains in custom schemas. That then progressed into wanting to expose summaries of high-value information from leaf nodes into the trunk or as far as root nodes. This approach didn't work well since the design was lacking a broader information architecure. A colleague noted that the design was [HATEOS](https://en.wikipedia.org/wiki/HATEOAS)-esque but not using one of the standard formats. The benefits of using standard formats is that they are free to use, have gone through extensive design review, can be navigated with standard patterns and tools, and (most importantly) LLMs already understand their vocabulary and access patterns. A new format will be definition not have those characteristics. +In the early revisions of this project, the design followed much the same playbook as past schemas, modeling parent/child relationships, linking to more detailed sources of information, and describing information domains in custom schemas. That then progressed into wanting to expose summaries of high-value information from leaf nodes into the trunk or as far as root nodes. This approach didn't work well since the design was lacking a broader information architecure. A colleague noted that the design was [Hypermedia as the engine of application state (HATEOAS)](https://en.wikipedia.org/wiki/HATEOAS)-esque but not using one of the standard formats. The benefits of using standard formats is that they are free to use, have gone through extensive design review, can be navigated with standard patterns and tools, and (most importantly) LLMs already understand their vocabulary and access patterns. A new format will be definition not have those characteristics. This proposal leans heavily on hypermedia, specifically [HAL+JSON](https://datatracker.ietf.org/doc/html/draft-kelly-json-hal). Hypermedia is very common, with [OpenAPI](https://www.openapis.org/what-is-openapi) and [JSON:API](https://jsonapi.org/) likely being the most common. LLMs are quite comfortable with these standards. The proposed use of HAL is a bit novel (not intended as a positive description). It is inspired by [llms.txt](https://llmstxt.org/) as an emerging standard and the idea that hypermedia is be the most natural next step to express complex diverse data and relationships. It's also expected (in fact, pre-ordained) that older standards will perform better than newer ones due to higher density (or presence at all) in the LLM training data. @@ -691,6 +691,27 @@ Source: A Few Good Men (1992; 10s later) That would seems to close the book on convenience. +## Attached data + +> These dimensional characteristics form the load-bearing structure of the graph to which everything else is attached. + +This leaves the question of which data we could attach. + +The following are all in scope to include: + +- Breaking changes (already included) +- CVE disclosures (already included) +- Servicing fixes and commits (beyond CVEs) +- Known issues +- Supported OSes +- Linux package dependencies +- Download links + hashes (partially included) + +Non goals: + +- Preview release details at the same fidelity as GA +- Performance benchmark data + ### Modeling as validation As the final graph took shape, distinct relationships inherent to the resource modeling started to emerge. From 3386c4243f2ec07cf18b0e550774bad219864c6e Mon Sep 17 00:00:00 2001 From: Rich Lander Date: Fri, 5 Dec 2025 16:25:07 -0800 Subject: [PATCH 03/15] Update spec --- .markdownlint.json | 4 +- .../release-notes-information-graph.md | 943 ++++++++++++++++-- 2 files changed, 869 insertions(+), 78 deletions(-) diff --git a/.markdownlint.json b/.markdownlint.json index a02418cfd..501575766 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -3,7 +3,7 @@ "MD003": { "style": "atx" }, "MD004": false, "MD007": { "indent": 4 }, - "MD013": { "tables": false, "code_blocks": false }, + "MD013": false, "MD026": false, "no-hard-tabs": false -} \ No newline at end of file +} diff --git a/accepted/2025/release-notes-information-graph/release-notes-information-graph.md b/accepted/2025/release-notes-information-graph/release-notes-information-graph.md index 1e9a0b25e..990513d4f 100644 --- a/accepted/2025/release-notes-information-graph/release-notes-information-graph.md +++ b/accepted/2025/release-notes-information-graph/release-notes-information-graph.md @@ -1,10 +1,6 @@ # Exposing Release Notes as an Information graph -Spec for LLMs: - -The rest is for you, friendly human. - -The .NET project has published release notes in JSON and markdown for many years. Our production of release notes has been based on the virtuous cloud-era idea that many deployment and compliance workflows require detailed and structured data to operate safely at scale. For the most part, a highly clustered set of consumers (like GitHub Actions and malware scanners) have adapted _their tools_ to _our formats_ to offer higher-level value to their users. That's all good. The LLM era is strikingly different where a much smaller set of information distributors (LLM model companies) _consume and expose diverse data in standard ways_ according to _their (shifting) paradigms_ to a much larger set of users. The task at hand is to adapt release notes for LLM consumption while continuing to serve and improve workflows for cloud users. +The .NET project has published release notes in JSON and markdown for many years. Our production of release notes has been based on the virtuous cloud-era idea that many deployment and compliance workflows require detailed and structured data to safely operate at scale. For the most part, a highly clustered set of consumers (like GitHub Actions and malware scanners) have adapted _their tools_ to _our formats_ to offer higher-level value to their users. That's all good. The LLM era is strikingly different where a much smaller set of information systems (LLM model companies) _consume and expose diverse data in standard ways_ according to _their (shifting) paradigms_ to a much larger set of users. The task at hand is to adapt release notes for LLM consumption while continuing to serve and improve workflows for cloud users. Release notes are mechanism, not scenario. It likely difficult for users to keep up with and act on the constant stream of .NET updates, typically one or two times a month. Users often have more than one .NET major version deployed, further complicating this puzzle. Many users rely on update orchestrators like APT, Yum, and Visual Studio, however, it is unlikely that such tools cover all the end-points that users care about in a uniform way. It is important that users can reliably make good, straightforward, and timely decisions about their entire infrastructure, orchestrated across a variety of deployment tools. This is a key scenario that release notes serve. @@ -17,22 +13,22 @@ Obvious questions release notes should answer: - How long until a given major release is EOL or has been EOL? - What are known upgrade challenges? -CIOs, CTOs, and others are accountable for maintaining efficient and secure continuity for a set of endpoints, including end-user desktops and cloud servers. They are unlikely to read markdown release notes or perform DIY `curl` + `jq` hacking with structured data, however, they will increasingly expect to be able to answer .NET-related compliance and deployment questions using chat assistants like Claude or Copilot. They may ask ChatGPT to compare treatment of an industry-wide CVE like [CVE-2023-44487](https://nvd.nist.gov/vuln/detail/cve-2023-44487) across multiple application stacks in their portfolio. This already works reasonably well, but fails when queries/prompts demand greater levels of detail with the expectation that they come from an authoritative source. It is very common to see assistants glean insight from a semi-arbitrary set of web pages with matching content. +CIOs, CTOs, and others are accountable for maintaining efficient and secure continuity for a set of endpoints, including end-user desktops and cloud servers. They are unlikely to read markdown release notes or perform DIY `curl` + `jq` hacking with structured data. They will increasingly expect to be able to answer .NET-related compliance and deployment questions using chat assistants like Claude or Copilot. They may ask ChatGPT to compare treatment of an industry-wide CVE like [CVE-2023-44487](https://nvd.nist.gov/vuln/detail/cve-2023-44487) across multiple application stacks in their portfolio. This already works reasonably well, but fails when queries/prompts demand greater levels of detail with the expectation that they come from an authoritative source. It is very common to see assistants glean insight from a semi-arbitrary set of web pages with matching content. -LLMs are _much more_ fickle relative to purpose-built tools. They are more likely to give up if release notes are not to their liking, instead relying on comfortable web search with its equal share of benefits and challenges. Regular testing (by the spec writer) of release notes with these chat assistants has demonstrated that LLMs are typically only satiated by a "Goldilocks meal". Obscure formats, large documents, and complicated workflows are unlikely to work well. The task at hand is to adapt our release notes publishing so that it works equally well for LLMs and purpose-built tools, exposes more scenario-targeted information, and avoids reliability and performance challenges of our current solution. +LLMs are _much more_ fickle relative to purpose-built tools. They are more likely to give up if release notes are not to their liking, instead relying on comfortable web search with its equal share of benefits and challenges. Regular testing (by the spec writer) of release notes with these chat assistants has demonstrated that LLMs are typically only satiated by a "Goldilocks meal". Obscure formats, large documents, and complicated workflows don't perform well (or outright fail). -In the early revisions of this project, the design followed much the same playbook as past schemas, modeling parent/child relationships, linking to more detailed sources of information, and describing information domains in custom schemas. That then progressed into wanting to expose summaries of high-value information from leaf nodes into the trunk or as far as root nodes. This approach didn't work well since the design was lacking a broader information architecure. A colleague noted that the design was [Hypermedia as the engine of application state (HATEOAS)](https://en.wikipedia.org/wiki/HATEOAS)-esque but not using one of the standard formats. The benefits of using standard formats is that they are free to use, have gone through extensive design review, can be navigated with standard patterns and tools, and (most importantly) LLMs already understand their vocabulary and access patterns. A new format will be definition not have those characteristics. +In the early revisions of this project, the design followed our existing playbook, modeling parent/child relationships, linking to more detailed sources of information, and describing information domains in custom schemas. That then progressed into wanting to expose summaries of high-value information from leaf nodes into the trunk or as far as root nodes. This approach didn't work well since the design was lacking a broader information architecure. A colleague noted that the design was [Hypermedia as the engine of application state (HATEOAS)](https://en.wikipedia.org/wiki/HATEOAS)-esque but not using one of the standard formats. The benefits of using standard formats is that they are free to use, have gone through extensive design review, can be navigated with standard patterns and tools, and (most importantly) LLMs already understand their vocabulary and access patterns. A new format will be definition not have those characteristics. This proposal leans heavily on hypermedia, specifically [HAL+JSON](https://datatracker.ietf.org/doc/html/draft-kelly-json-hal). Hypermedia is very common, with [OpenAPI](https://www.openapis.org/what-is-openapi) and [JSON:API](https://jsonapi.org/) likely being the most common. LLMs are quite comfortable with these standards. The proposed use of HAL is a bit novel (not intended as a positive description). It is inspired by [llms.txt](https://llmstxt.org/) as an emerging standard and the idea that hypermedia is be the most natural next step to express complex diverse data and relationships. It's also expected (in fact, pre-ordained) that older standards will perform better than newer ones due to higher density (or presence at all) in the LLM training data. Overall goals: +- Ensure high-performance (low kb cost) and high reliability (TTL resilience). +- Enable aestheticly pleasing queries that are terse, ergonomic, and effective, both for their own goals and as a proxy for LLM consumption. - Enable queries with multiple key styles, temporal and version-based queries. - Describe runtime and SDK versions (as much as appropriate) at parity. -- Intergrate high value data, such as CVE disclosures, breaking changes, and download links. -- Ensure high-performance (low kb cost) and high reliability (TTL resilience). -- Enable aestheticly pleasing queries that are terse, ergonomic, and effective. -- Generate most release note files, like [releases.md](https://github.com/dotnet/core/blob/main/releases.md), and CVE announcements like [CVE-2025-55248](https://github.com/dotnet/announcements/issues/372). +- Integrate high value data, such as CVE disclosures, breaking changes, and download links. +- Enable generating most release note files, like [releases.md](https://github.com/dotnet/core/blob/main/releases.md), and CVE announcements like [CVE-2025-55248](https://github.com/dotnet/announcements/issues/372). - Use this project as a real-world pilot for exposing an information graph equally to LLMs, client libraries, and DIY `curl` + `jq` hacking. ## Hypermedia graph design @@ -46,14 +42,14 @@ Hypermedia formats have a long history of satisfying this methodology, long pre- For the most part, HAL defines just two properties: - `_links` -- links to resources. -- `_embedded` -- embedded resources, which may include more HAL-style links. +- `_embedded` -- embedded resources, which will often include `_links`. It seems like this is hardly enough to support the ambitious design approach that has been described. It turns out that the design is more clever than first blush would suggest. There is an excellent Australian movie that comes to mind, [The Castle](https://www.imdb.com/title/tt0118826). > Judge: “What section of the constitution has been breached?” -> Dennis Denuto: "It’s the constitution. It’s Mabo. It’s justice. It’s law. It’s the vibe … no, that’s it, it’s the vibe. I rest my case" +> Dennis Denuto: "It’s the constitution. It’s Mabo. It’s justice. It’s law. It’s the vibe ... no, that’s it, it’s the vibe. I rest my case" HAL is much the same. It defines an overall approach that a schema designer can hang off of these two seemingly understated properties. You just have to follow the vibe of it. @@ -72,7 +68,7 @@ Here is a simple example from the HAL spec: } ``` -The `_links` property is a dictionary of link objects with specific named relations. Most links dictionaries start with the standard `self` relation. The `self` relation describes the canonical URL of the given resource. The `warehouse` and `invoice` relations are examples of domain-specific relations. Together, they establish a navigation protocol for this resource domain. One can also imagine `next`, `previous`, `buy-again` as being equally applicable relations for e-commerce. Domain-specific HAL readers will understand these relations and know how or when to act on them. +The `_links` property is a dictionary of link objects with specific named relations. Most link dictionaries start with the standard `self` relation. The `self` relation describes the canonical URL of the given resource. The `warehouse` and `invoice` relations are examples of domain-specific relations. Together, they establish a navigation protocol for this resource domain. One can also imagine `next`, `previous`, `buy-again` as relations for e-commerce. Domain-specific HAL readers will understand these relations and know how or when to act on them. The `currency`, `status`, and `total` properties provide additional domain-specific resource metadata. The package should arrive at your door soon! @@ -111,13 +107,13 @@ The following example is similar, with the addition of the `_embedded` property. } ``` -The `_embedded` property contains order resources. This is the document payload. Each of those order items have `self` and other related link relations referencing other resources. As stated earlier, the `self` relation references the canonical copy of the resource. Embedded resources may be a full or partial copy of the resource. A domain-specific reader will have a deeper understanding of the resource rules and associated schema. +The `_embedded` property contains order resources. This is the resource payload. Each of those order items have `self` and other related link relations referencing other resources. As stated earlier, the `self` relation references the canonical copy of the resource. Embedded resources may be a full or partial copy of the resource. Again, domain-specific reader will understand this schema and know how to process it. -This design aspect is the true strength of HAL. It's the mechanism that enables the overall approach of a skeletal root with weighted bottom nodes. It's also what enables these two seemingly aenemic properties to provide so much modeling value. +This design aspect is the true strength of HAL. It's the mechanism that enables the overall approach of a skeletal root with weighted bottom nodes. It's also what enables these two seemingly anemic properties to provide so much modeling value. The `currentlyProcessing` and `shippedToday` properties provide additional information about ongoing operations. -We've now seen representative examples of both properties in use. You are now a HAL expert! We can now look at how the same vibe can be applied to .NET release notes. +We can now look at how the same vibe can be applied to .NET release notes. ## Release Notes Graph @@ -126,7 +122,7 @@ Release notes naturally describe two information dimensions: time and product ve - Within time, we have years, months, and (ship) days. - Within version, we have major and patch version. We also have runtime vs SDK version. -These dimensional characteristics form the load-bearing structure of the graph to which everything else is attached. We will have both a timeline and version indices. We've previously only had a version index. +These dimensional characteristics form the load-bearing structure of the graph to which everything else is attached. The new graph exposes both timeline and version indices. We've previously only had a version index. The following table summarizes the overall shape of the graph, starting at `dotnet/core:release-notes/index.json`. @@ -156,7 +152,7 @@ We can contrast this approach with the existing release graph, using the last 12 Conservatively, the existing commit counts are not good. The `releases-index.json` file is a mission-critical live-site resource. 29 updates is > 2x/month! -### Graph Rules +### Graph consistency The graph has one rule: @@ -174,20 +170,20 @@ Today, we publish [releases-index.json](https://github.com/dotnet/core/blob/main Problems: - Exposing patch versions in multiple files that need to agree is incompatible with using a Content Delivery Network (CDN) that employs standard caching (expiration / TTL). -- The `releases-index.json` file is a critical live site resource driving 1000s of GBs of downloads a month, yet we manually update it multiple times a month, including for previews. +- The `releases-index.json` file is a critical live site resource driving 1000s of GBs of downloads a month, yet we update it multiple times a month, including for previews. Solution: - Fast changing currency (like patch version numbers) are exposed in (at most) a single resource in the graph. - The root index file is updated once a year (to add the presence of a new major release). -The point about the root index isn't a "solution" but an implication of the first point. If the root index isn't allowed to contain fast-moving currency, because the canonical location is another resource, then it is stripped of its reason to change. That will be addressed later in the document. +The point about the root index isn't a "solution" but an implication of the first point. If the root index isn't allowed to contain fast-moving currency, because it is present in another resource, then it is stripped of its reason to change. There are videos on YouTube with these [crazy gear reductions](https://www.youtube.com/watch?v=QwXK4e4uqXY). You can watch them for a long time! Keen observers will realize our graph will be nothing like that. Well, kindof. One can model years and months and major and patch versions as spinning gears with a differing number of teeth and revolution times. It just won't look the same as those lego videos. A celestial orbit analogy would have worked just as well. -Release notes graph indexes operate like the following (ignoring some annoying details): +Release notes graph indexes update (gear reduce) like the following: - Timeline index (list of years): one update per year - Year index (list of months): one update per month @@ -201,13 +197,21 @@ The same progression for versions: It's the middle section changing constantly, but the roots and the leaves are either immutable or close enough to it. -### Resource modeling +Note: Some annoying details, like SDK-only releaes, have been ignored. The intent is to reason about rough order of magnitude and the fundamental pressure being applied to each layer. + +A key question about this scheme is when we add new releases. The most obvious answer to add new releaes -The following example demonstrates what HAL JSON looks like generally. Each node in the graph is named `index.json`. This is the root [index.json](https://github.com/dotnet/core/blob/release-index/release-notes/index.json) file that represents all .NET versions. It exposes the same general information as the existing `releases-index.json`. +## Version Index Modeling + +The resource modeling with the graph has to satisfy the rate of these turning gears. The key technique is noticing when a design choice forces a faster update schedule than desired or exposes currency that could be misused. + +### Releases index + +Most nodes in the graph are named `index.json`. This is the root [index.json](https://github.com/dotnet/core/blob/release-index/release-notes/index.json) file that represents all .NET versions. It exposes the same general information as the existing `releases-index.json`. It should look similar to the examples shared earlier from the HAL spec. ```json { - "$schema": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/schemas/dotnet-release-version-index.json", + "$schema": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/schemas/dotnet-release-version-index.json", "kind": "releases-index", "title": ".NET Release Index", "description": ".NET Release Index (latest: 10.0)", @@ -215,34 +219,63 @@ The following example demonstrates what HAL JSON looks like generally. Each node "latest_lts": "10.0", "_links": { "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json", + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/index.json", "path": "/index.json", "title": ".NET Release Index", "type": "application/hal\u002Bjson" }, "latest": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/index.json", + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/index.json", "path": "/10.0/index.json", "title": "Latest .NET release (.NET 10.0)", "type": "application/hal\u002Bjson" }, "latest-lts": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/index.json", + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/index.json", "path": "/10.0/index.json", "title": "Latest LTS release (.NET 10.0)", "type": "application/hal\u002Bjson" }, + "latest-sdk": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/sdk/index.json", + "path": "/10.0/sdk/index.json", + "title": "Latest .NET SDK (10.0)", + "type": "application/hal\u002Bjson" + }, + "timeline-index": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/index.json", + "path": "/timeline/index.json", + "title": ".NET Release Timeline Index", + "type": "application/hal\u002Bjson" + }, + "llms-txt": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/llms/README.md", + "path": "/llms/README.md", + "title": "LLM Quick Reference", + "type": "application/markdown" + } + }, + "glossary": { + "lts": "Long-Term Support \u2013 3-year support window", + "sts": "Standard-Term Support \u2013 18-month support window", + "latest-release": "Highest stable (GA) major version; excludes preview and RC releases", + "latest": "Alias for latest-release", + "latest-lts": "Highest stable LTS major version currently in support", + "latest-sdk": "SDK index for the latest stable major version" + }, ``` Key points: - Schema reference is included - `kind`, `title`, and `description` describe the resource -- Additional kind-specific properties, like `latest`, describe high-level resource metadata, often useful currency that helps contextualize the rest of the resource without the need to parse/split strings. For example, the `latest_lts` scalar describes the target of the `latest-lts` link relation. -- `_links` and `_embedded` as appropriate. +- `latest` and `latest_lts` describe high-level resource metadata, often useful currency that helps contextualize the rest of the resource without the need to parse/split strings. For example, the `latest_lts` scalar describes the target of the `latest-lts` link relation. +- `timeline-index` provides a "wormhole link" (more on that later) to another part of the graph +- `llms-txt` provides instructive content for an LLM. - Core schema syntax like `latest_lts` uses snake-case-lower for query ergonomics (using `jq` as the proxy for that), while relations like `latest-lts` use kebab-case-lower since they can be names or brands. This follows the approach used by [cve-schema](https://github.com/dotnet/designs/blob/main/accepted/2025/cve-schema/cve_schema.md#brand-names-vs-schema-fields-mixed-naming-strategy). +- `glossary` provides consumer with display or knowledge strings, depending on the need. -Here is the first couple objects within the `_embedded` property, in the same root index: +The `_embedded` section has one child, `releases`: ```json "_embedded": { @@ -277,53 +310,121 @@ Here is the first couple objects within the `_embedded` property, in the same ro This is where we see the design diverge significantly from `releases-index.json`. There are no patch versions, no statement about security releases. It's the most minimal data to determine the release type, if/when it is supported until, and how to access the canonical resource that exposes richer information. This approach removes the need to update the root index monthly. -Let's look at another section of the graph, the [major version index for .NET 9](https://github.com/dotnet/core/blob/release-index/release-notes/9.0/index.json). +### Major version index + +One layer lower, we have the major version idex. The followin example is the [major version index for .NET 9](https://github.com/dotnet/core/blob/release-index/release-notes/9.0/index.json). ```json { - "$schema": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/schemas/dotnet-release-version-index.json", + "$schema": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/schemas/dotnet-release-version-index.json", "kind": "major-version-index", "title": ".NET 9.0 Patch Release Index", "description": ".NET 9.0 (latest: 9.0.11)", "latest": "9.0.11", "latest_security": "9.0.10", "release_type": "sts", - "phase": "active", + "support_phase": "active", "supported": true, "ga_date": "2024-11-12T00:00:00\u002B00:00", "eol_date": "2026-11-10T00:00:00\u002B00:00", "_links": { "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/index.json", + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/index.json", "path": "/9.0/index.json", "title": ".NET 9.0", "type": "application/hal\u002Bjson" }, "latest": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/9.0.11/index.json", + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.11/index.json", "path": "/9.0/9.0.11/index.json", "title": "Latest patch release (9.0.11)", "type": "application/hal\u002Bjson" }, "latest-sdk": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/sdk/index.json", + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/sdk/index.json", "path": "/9.0/sdk/index.json", "title": ".NET SDK 9.0 Release Information", "type": "application/hal\u002Bjson" }, "latest-security": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/9.0.10/index.json", + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.10/index.json", "path": "/9.0/9.0.10/index.json", "title": "Latest security patch (9.0.10)", "type": "application/hal\u002Bjson" }, + "release-manifest": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/manifest.json", + "path": "/9.0/manifest.json", + "title": "Release manifest", + "type": "application/hal\u002Bjson" + }, + "releases-index": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/index.json", + "path": "/index.json", + "title": ".NET Release Index", + "type": "application/hal\u002Bjson" + }, + "latest-release-json": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.11/release.json", + "path": "/9.0/9.0.11/release.json", + "title": "Latest release information (9.0.11)", + "type": "application/json" + } + }, + "glossary": { + "lts": "Long-Term Support \u2013 3-year support window", + "sts": "Standard-Term Support \u2013 18-month support window", + "preview": "Pre-release phase for testing and feedback; not supported", + "go-live": "Production-supported release candidate before GA", + "active": "Full support with functional and security improvements", + "maintenance": "Final 6 months of support; security fixes only", + "eol": "End of Life \u2013 No longer supported", + "feature-band": "Quarterly SDK minor version releases (e.g., 8.0.1xx, 8.0.2xx)", + "patch": "Cumulative monthly update released on Patch Tuesday", + "latest-release": "Highest stable (GA) major version; excludes preview and RC releases", + "latest": "Alias for latest-release", + "latest-lts": "Highest stable LTS major version currently in support", + "latest-sdk": "SDK index for the latest stable major version", + "latest-security": "Most recent patch release containing security fixes" + }, ``` This index includes much more useful and detailed information, both metadata/currency and patch-version links. It starts to answer the question of "what should I care about _now_?". -Let's also look at one of the objects from the `_embedded` section as well. +Much of the form is similar to the root index. Instead of `latest_lts`, there is `latest_security`. A new addition is `release-manifest`. That relation stores important but lower value content about a given major release. That will be covered shortly. + +The `_embeeded` section has three children: `releases`, `years`, and `cve_records`. ```json + "_embedded": { + "releases": [ + { + "version": "9.0.11", + "date": "2025-11-11T00:00:00\u002B00:00", + "year": "2025", + "month": "11", + "security": false, + "cve_count": 0, + "support_phase": "active", + "sdk_patches": [ + "9.0.307", + "9.0.112" + ], + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.11/index.json", + "path": "/9.0/9.0.11/index.json", + "title": "9.0.11 Patch Index", + "type": "application/hal\u002Bjson" + }, + "release-month": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/11/index.json", + "path": "/timeline/2025/11/index.json", + "title": "Release timeline index for 2025-11", + "type": "application/hal\u002Bjson" + } + } + }, { "version": "9.0.10", "date": "2025-10-14T00:00:00\u002B00:00", @@ -343,40 +444,225 @@ Let's also look at one of the objects from the `_embedded` section as well. ], "_links": { "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/9.0.10/index.json", + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.10/index.json", "path": "/9.0/9.0.10/index.json", "title": "9.0.10 Patch Index", "type": "application/hal\u002Bjson" }, "release-month": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/10/index.json", + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/index.json", "path": "/timeline/2025/10/index.json", "title": "Release timeline index for 2025-10", "type": "application/hal\u002Bjson" }, "cve-json": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/10/cve.json", + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/cve.json", "path": "/timeline/2025/10/cve.json", "title": "CVE Information", "type": "application/json" } } + }, +``` + +and the other children: + +```json + "years": [ + { + "year": "2025", + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/index.json", + "path": "/timeline/2025/index.json", + "title": ".NET Release Timeline 2025 (chronological)", + "type": "application/hal\u002Bjson" + } + } }, + { + "year": "2024", + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2024/index.json", + "path": "/timeline/2024/index.json", + "title": ".NET Release Timeline 2024 (chronological)", + "type": "application/hal\u002Bjson" + } + } + } + ], + "cve_records": [ + "CVE-2024-43483", + "CVE-2024-43498", + "CVE-2024-43499", + "CVE-2025-21171", + "CVE-2025-21172", + "CVE-2025-21173", + "CVE-2025-21176", + "CVE-2025-24070", + "CVE-2025-26646", + "CVE-2025-26682", + "CVE-2025-30399", + "CVE-2025-55247", + "CVE-2025-55248", + "CVE-2025-55315" + ] + }, ``` -This patch-version object contains even more high-level information that can drive deployment and compliance workflows. The first two link relations are HAL links. The last is a plain JSON link. Non-HAL links end in the format, like `json` or `markdown` or `markdown-rendered`. The links are raw text by default, with `-rendered` HTML content being useful for content targeted for human consumption, for example in generated release notes. +This patch-version objects contains even more detailed information that can drive deployment and compliance workflows. The first two link relations are HAL links. The last is a plain JSON link. Most non-HAL links end in the format, like `json` or `markdown` or `markdown-rendered`. The links are raw text by default, with `-rendered` HTML content being useful for content targeted for human consumption, for example in generated release notes. -The design has a concept of "wormhole links". That's what we see with `release-month`. It provides direct access to a high-relevance (potentially graph-distant) resource that would otherwise require awkward indirections, multiple network hops, and wasted bytes/tokens to acquire. These wormhole links massively improve query ergonomics for sophisticated queries. There are multiple of these wormhole links, not just `release-month` that are sprinkled throughout the graph for this purpose. They also provide hints on how the graph is intended to be traversed. +As mentioned earlier, the design has a concept of "wormhole links". That's what we see with `release-month`. It provides direct access to a high-relevance (potentially graph-distant) resource that would otherwise require awkward indirections, multiple network hops, and wasted bytes/tokens to acquire. These wormhole links massively improve query ergonomics for sophisticated queries. There are multiple of these wormhole links, not just `release-month` that are sprinkled throughout the graph for this purpose. They also provide hints on how the graph is intended to be traversed. There is a link `cve.json` file. Our [CVE schema](https://github.com/dotnet/designs/blob/main/accepted/2025/cve-schema/cve_schema.md) is a custom schema with no HAL vocabulary. It's an exit node of the graph. The point is that we're free to describe complex domains, like CVE disclosures, using a clean-slate design methodology. One can also see that some of the `cve.json` information has been projected into the graph, adding high-value shape over the skeleton. -This one-level-lower index is more nuanced to what we saw earlier with the root version index. As stated, there is a lot more useful detailed currency on offer. However, there is a rule that currency needs to be guaranteed consistent. Let's consider if the rule is obeyed. The important characteristic is that listed versions and links _within_ the resource are consistent by virtue of being _captured_ in the same file. The critical trick is with the links. The link origin is a fast moving resource and target resources are immutable. That combination works. It's easy to be consistent with something immutable, that either exists or doesn't. In contrast, there would be a problem if there was a link between two mutable resources that expose the same currency. This is the problem that `releases-index.json` has. +The `year` property is effectively a baked in pre-query that directs further exploration if the timeline is of interest for this major releases. The `cve_records` property lists all the CVEs for the month, another pre-query baked in. + +As stated, there is a lot more useful detailed currency on offer. However, there is a rule that currency needs to be guaranteed consistent. Let's consider if the rule is obeyed. The important characteristic is that listed versions and links _within_ the resource are consistent by virtue of being _captured_ in the same file. The critical trick is with the links. The link origin is a fast moving resource while link target resources are immutable. That combination works. It's easy to be consistent with something immutable. It will either exist or not. In contrast, there would be a problem if there was a link between two mutable resources that expose the same currency. This is the problem that `releases-index.json` has. + +Back to `manifest.json`. It contains extra data that tools in particular might find useful. The following example is the `manifest.json` file for .NET 9. + +```json +{ + "kind": "manifest", + "title": ".NET 9.0 Manifest", + "version": "9.0", + "label": ".NET 9.0", + "release_type": "sts", + "support_phase": "active", + "supported": true, + "ga_date": "2024-11-12T00:00:00+00:00", + "eol_date": "2026-11-10T00:00:00+00:00", + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/manifest.json", + "path": "/9.0/manifest.json", + "title": ".NET 9.0 Manifest", + "type": "application/hal\u002Bjson" + }, + "compatibility-json": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/compatibility.json", + "path": "/9.0/compatibility.json", + "title": ".NET 9.0 Compatibility", + "type": "application/json" + }, + "compatibility-rendered": { + "href": "https://learn.microsoft.com/dotnet/core/compatibility/9.0", + "title": "Breaking changes in .NET 9", + "type": "text/html" + }, + "downloads-rendered": { + "href": "https://dotnet.microsoft.com/download/dotnet/9.0", + "title": ".NET 9 Downloads", + "type": "text/html" + }, + "os-packages-json": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/os-packages.json", + "path": "/9.0/os-packages.json", + "title": "OS Packages", + "type": "application/json" + }, + "release-blog-rendered": { + "href": "https://devblogs.microsoft.com/dotnet/announcing-dotnet-9/", + "title": "Announcing .NET 9", + "type": "text/html" + }, + "releases-json": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/releases.json", + "path": "/9.0/releases.json", + "title": "Complete (large file) release information for all patch releases", + "type": "application/json" + }, + "supported-os-json": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/supported-os.json", + "path": "/9.0/supported-os.json", + "title": "Supported OSes", + "type": "application/json" + }, + "supported-os-markdown": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/supported-os.md", + "path": "/9.0/supported-os.md", + "title": "Supported OSes", + "type": "application/markdown" + }, + "supported-os-markdown-rendered": { + "href": "https://github.com/dotnet/core/blob/main/release-notes/9.0/supported-os.md", + "path": "/9.0/supported-os.md", + "title": "Supported OSes (Rendered)", + "type": "application/markdown" + }, + "usage-markdown-rendered": { + "href": "https://github.com/dotnet/core/blob/main/release-notes/9.0/README.md", + "path": "/9.0/README.md", + "title": "Release Notes (Rendered)", + "type": "application/markdown" + }, + "whats-new-rendered": { + "href": "https://learn.microsoft.com/dotnet/core/whats-new/dotnet-9/overview", + "title": "What\u0027s new in .NET 9", + "type": "text/html" + } + }, + "_metadata": { + "schema_version": "1.0", + "generated_on": "2025-12-05T23:00:01.7004871+00:00", + "generated_by": "VersionIndex" + } +} +``` + +The relations are in alphabetical order, after `self`. + +Some of the information in this file is sourced from a human-curated `_manifest.json`. This file is used by the graph generation tools, not the graph itself. It provides a path to seeding the graph with data not available elsewhere. + +.NET 9 `_manifest.json`: + +```json +{ + "kind": "manifest", + "title": ".NET 9.0 Manifest", + "version": "9.0", + "label": ".NET 9.0", + "release_type": "sts", + "support_phase": "active", + "ga_date": "2024-11-12T00:00:00Z", + "eol_date": "2026-11-10T00:00:00Z", + "_links": { + "downloads-rendered": { + "href": "https://dotnet.microsoft.com/download/dotnet/9.0", + "title": ".NET 9 Downloads", + "type": "text/html" + }, + "whats-new-rendered": { + "href": "https://learn.microsoft.com/dotnet/core/whats-new/dotnet-9/overview", + "title": "What's new in .NET 9", + "type": "text/html" + }, + "compatibility-rendered": { + "href": "https://learn.microsoft.com/dotnet/core/compatibility/9.0", + "title": "Breaking changes in .NET 9", + "type": "text/html" + }, + "release-blog-rendered": { + "href": "https://devblogs.microsoft.com/dotnet/announcing-dotnet-9/", + "title": "Announcing .NET 9", + "type": "text/html" + } + } +} + +``` + +This links are free form and can be anything. They follow the same scheme as the links used elsewhere in the graph. + +### Patch Version Index The following example is a patch version index, for [9.0.10](https://github.com/dotnet/core/blob/release-index/release-notes/9.0/9.0.10/index.json). ```json { - "$schema": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/schemas/dotnet-patch-detail-index.json", + "$schema": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/schemas/dotnet-patch-detail-index.json", "kind": "patch-version-index", "title": ".NET 9.0.10 Patch Index", "description": "Patch information for .NET 9.0.10", @@ -396,51 +682,108 @@ The following example is a patch version index, for [9.0.10](https://github.com/ ], "_links": { "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/9.0.10/index.json", + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.10/index.json", "path": "/9.0/9.0.10/index.json", "title": "9.0.10 Patch Index", "type": "application/hal\u002Bjson" }, - "next": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/9.0.11/index.json", - "path": "/9.0/9.0.11/index.json", - "title": "9.0.11 Patch Index", - "type": "application/hal\u002Bjson" - }, "prev": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/9.0.9/index.json", + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.9/index.json", "path": "/9.0/9.0.9/index.json", "title": "9.0.9 Patch Index", "type": "application/hal\u002Bjson" }, "latest-sdk": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/sdk/index.json", + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/sdk/index.json", "path": "/9.0/sdk/index.json", "title": ".NET SDK 9.0 Release Information", "type": "application/hal\u002Bjson" }, + "release-major": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/index.json", + "path": "/9.0/index.json", + "title": ".NET 9.0 Patch Release Index", + "type": "application/hal\u002Bjson" + }, + "release-month": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/index.json", + "path": "/timeline/2025/10/index.json", + "title": "Release timeline index for 2025-10", + "type": "application/hal\u002Bjson" + }, + "release-year": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/index.json", + "path": "/timeline/2025/index.json", + "title": "Release timeline index for 2025", + "type": "application/hal\u002Bjson" + }, + "releases-index": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/index.json", + "path": "/index.json", + "title": ".NET Release Index", + "type": "application/hal\u002Bjson" + }, + "cve-json": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/cve.json", + "path": "/timeline/2025/10/cve.json", + "title": "CVE Information", + "type": "application/json" + }, + "cve-markdown": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/cve.md", + "path": "/timeline/2025/10/cve.md", + "title": "CVE Information", + "type": "application/markdown" + }, + "cve-markdown-rendered": { + "href": "https://github.com/dotnet/core/blob/main/release-notes/timeline/2025/10/cve.md", + "path": "/timeline/2025/10/cve.md", + "title": "CVE Information (Rendered)", + "type": "application/markdown" + }, + "release-json": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.10/release.json", + "path": "/9.0/9.0.10/release.json", + "title": "9.0.10 Release Information", + "type": "application/json" + }, + "release-notes-markdown": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.10/9.0.10.md", + "path": "/9.0/9.0.10/9.0.10.md", + "title": "9.0.10 Release Notes", + "type": "application/markdown" + }, + "release-notes-markdown-rendered": { + "href": "https://github.com/dotnet/core/blob/main/release-notes/9.0/9.0.10/9.0.10.md", + "path": "/9.0/9.0.10/9.0.10.md", + "title": "9.0.10 Release Notes (Rendered)", + "type": "application/markdown" + } + }, ``` This content looks much the same as we saw earlier, except that much of the content we saw in the patch object is now exposed at index root. That's not coincidental, but a key aspect of the model. -The `next` and `prev` link relations provide some more wormholes, this time to less distant targets. The `latest-sdk` target provides access to `aka.ms` evergreen SDK links and other SDK-related information. The `release-month` and `cve-json` links are still there, but a bit further down the dictionary definition as to what's copied above. +The `prev` link relation provides another wormhole, this time to a less distant target. A `next` relation isn't provided because it would break the immutability goal. In addition, the combination of a `latest*` property and `prev` links satisfies many scenarios. There are lots of algoriths that [work best when counting backwards](https://www.benjoffe.com/fast-date-64). + +The `latest-sdk` target provides access to `aka.ms` evergreen SDK links and other SDK-related information. The `release-month` and `cve-json` links are still there, but a bit further down the dictionary definition as to what's copied above. -The `_embedded` property contains a description of all the SDKs released at the same time as the runtime. +The `_embedded` property contains two children: `sdk` and `disclosures`. ```json - "_embedded": { + "_embedded": { "sdk": [ { "version": "9.0.306", "_links": { "feature-band": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/sdk/sdk-9.0.3xx.json", + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/sdk/sdk-9.0.3xx.json", "path": "/9.0/sdk/sdk-9.0.3xx.json", "title": ".NET SDK 9.0.3xx", "type": "application/json" }, "release-notes-markdown": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/9.0.10/9.0.10.md", + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.10/9.0.10.md", "path": "/9.0/9.0.10/9.0.10.md", "title": "9.0.10 Release Notes", "type": "application/markdown" @@ -457,13 +800,13 @@ The `_embedded` property contains a description of all the SDKs released at the "version": "9.0.111", "_links": { "feature-band": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/sdk/sdk-9.0.1xx.json", + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/sdk/sdk-9.0.1xx.json", "path": "/9.0/sdk/sdk-9.0.1xx.json", "title": ".NET SDK 9.0.1xx", "type": "application/json" }, "release-notes-markdown": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/9.0.10/9.0.111.md", + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.10/9.0.111.md", "path": "/9.0/9.0.10/9.0.111.md", "title": "SDK 9.0.111 Release Notes", "type": "application/markdown" @@ -477,13 +820,6 @@ The `_embedded` property contains a description of all the SDKs released at the } } ], -``` - -There is an `sdk*.json` that matches each feature band. It is largely the same as `sdk/index.json` but more specific. The rest of the links are for markdown release notes. - -Any CVEs for the month are described in `disclosures`. - -```json "disclosures": [ { "id": "CVE-2025-55247", @@ -542,13 +878,463 @@ Any CVEs for the month are described in `disclosures`. }, ``` -It's possible to make detail-oriented compliance and deployment decisions based on this information. There's even a commit for the CVE fix with an LLM friendly link style. This is the bottom part of the hypermedia graph. It's far more shapely and weightier than the root. If a consumer gets this far, it is likely because they need access to the exposed information. If they only wanted access to the `cve.json` file, they could have accessed it in the major version index, where it is also made available. +Note: `sdks` should probably be plural. + +First-class treatment is provided for SDK releaes, both a root and in `_embedded`. That said, it is obvious that we don't quite do the right thing with release notes. It is very odd that the "best" SDK has to share release notes with the runtime. + +There is an `sdk*.json` that matches each feature band. It is largely the same as `sdk/index.json` but more specific. The rest of the links are for markdown release notes. + +Note: There is currently no runtime variant of `sdk/index.json`. This needs to be resolved and may inspire changes in the SDK design. The SDK design is still a bit shaky. + +Any CVEs for the month are described in `disclosures`. This data provides a useful pre-baked view on data that from `cve.json`. + +It's possible to make detail-oriented compliance and deployment decisions based on this information. There's even a commit for the CVE fix with an LLM friendly link style. This is the bottom part of the hypermedia graph. It's far more shapely and weighty than the root. If a consumer gets this far, it is likely because they need access to the exposed information. If they only want access to the `cve.json` file, it is exposed in the major version index. + +## Timeline Modeling + +The timeline is much the same. The key difference is that the version index converges to a point while the timeline index converges to a slice or row of points. + +### Timeline Index + +The root of the [timeline index](https://github.com/dotnet/core/blob/release-index/release-notes/timeline/index.json) is almost identical to the releases index, with `timeline-index` being inverted into `releases-index`. + +```json +{ + "$schema": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/schemas/dotnet-release-timeline-index.json", + "kind": "timeline-index", + "title": ".NET Release Timeline Index", + "description": ".NET Release Timeline (latest: 10.0)", + "latest": "10.0", + "latest_lts": "10.0", + "latest_year": "2025", + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/index.json", + "path": "/timeline/index.json", + "title": ".NET Release Timeline Index", + "type": "application/hal\u002Bjson" + }, + "latest": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/index.json", + "path": "/10.0/index.json", + "title": "Latest .NET release (.NET 10.0)", + "type": "application/hal\u002Bjson" + }, + "latest-lts": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/index.json", + "path": "/10.0/index.json", + "title": "Latest LTS release (.NET 10.0)", + "type": "application/hal\u002Bjson" + }, + "latest-year": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/index.json", + "path": "/timeline/2025/index.json", + "title": "Latest year (2025)", + "type": "application/hal\u002Bjson" + }, + "releases-index": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/index.json", + "path": "/index.json", + "title": ".NET Release Index", + "type": "application/hal\u002Bjson" + } + }, +``` + +The `_embedded` section naturally contains `years`. + +```json + "_embedded": { + "years": [ + { + "year": "2025", + "description": ".NET release timeline for 2025", + "releases": [ + "10.0", + "9.0", + "8.0" + ], + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/index.json", + "path": "/timeline/2025/index.json", + "title": "Release timeline index for 2025", + "type": "application/hal\u002Bjson" + } + } + }, + { + "year": "2024", + "description": ".NET release timeline for 2024", + "releases": [ + "9.0", + "8.0", + "7.0", + "6.0" + ], + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2024/index.json", + "path": "/timeline/2024/index.json", + "title": "Release timeline index for 2024", + "type": "application/hal\u002Bjson" + } + } + }, +``` + +It also provides a helpful join with the active (not neccessarily supported) releases for that year. This baked-in query helps some workflows. + +This index file similarly avoid fast-moving currency as the root releases index. + +### Year Index + +The year index follows much the same pattern as the major version index. The year objects you see above become the root of the index. This is [year index for 2025](https://github.com/dotnet/core/blob/release-index/release-notes/timeline/2025/index.json). + +```json +{ + "$schema": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/schemas/dotnet-release-timeline-index.json", + "kind": "year-index", + "title": ".NET Release Timeline Index - 2025", + "description": "Release timeline for 2025 (latest: 10.0)", + "year": "2025", + "latest_month": "11", + "latest_security_month": "10", + "latest_release": "10.0", + "releases": [ + "10.0", + "9.0", + "8.0" + ], + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/index.json", + "path": "/timeline/2025/index.json", + "title": "Release timeline index for 2025", + "type": "application/hal\u002Bjson" + }, + "prev": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2024/index.json", + "path": "/timeline/2024/index.json", + "title": "Release timeline index for 2024", + "type": "application/hal\u002Bjson" + }, + "latest-month": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/11/index.json", + "path": "/timeline/2025/11/index.json", + "title": "Latest month (Release timeline index for 2025-11)", + "type": "application/hal\u002Bjson" + }, + "latest-release": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/index.json", + "path": "/10.0/index.json", + "title": "Latest release (.NET 10.0)", + "type": "application/hal\u002Bjson" + }, + "latest-security-month": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/index.json", + "path": "/timeline/2025/10/index.json", + "title": "Latest security month (Release timeline index for 2025-10)", + "type": "application/hal\u002Bjson" + }, + "timeline-index": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/index.json", + "path": "/timeline/index.json", + "title": ".NET Release Timeline Index", + "type": "application/hal\u002Bjson" + } + }, +``` + +Very similar approach as other indices. + +The `_emdedded` section contains: `months` and `releases`. + +```json + "_embedded": { + "months": [ + { + "month": "11", + "security": false, + "cve_count": 0, + "latest_release": "10.0", + "releases": [ + "10.0", + "9.0", + "8.0" + ], + "runtime_patches": [ + "10.0.0", + "9.0.11", + "8.0.22" + ], + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/11/index.json", + "path": "/timeline/2025/11/index.json", + "title": "Release timeline index for 2025-11", + "type": "application/hal\u002Bjson" + } + } + }, + { + "month": "10", + "security": true, + "cve_count": 3, + "cve_records": [ + "CVE-2025-55248", + "CVE-2025-55315", + "CVE-2025-55247" + ], + "latest_release": "9.0", + "releases": [ + "10.0", + "9.0", + "8.0" + ], + "runtime_patches": [ + "10.0.0-rc.2.25502.107", + "9.0.10", + "8.0.21" + ], + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/index.json", + "path": "/timeline/2025/10/index.json", + "title": "Release timeline index for 2025-10", + "type": "application/hal\u002Bjson" + }, + "cve-json": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/cve.json", + "path": "/timeline/2025/10/cve.json", + "title": "CVE Information", + "type": "application/json" + } + } + }, +``` + +`releases` looks like the following: + +```json + "releases": [ + { + "version": "10.0", + "release_type": "lts", + "support_phase": "active", + "supported": true, + "ga_date": "2025-11-11T00:00:00\u002B00:00", + "eol_date": "2028-11-14T00:00:00\u002B00:00", + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/index.json", + "path": "/10.0/index.json", + "title": ".NET 10.0", + "type": "application/hal\u002Bjson" + }, + "latest-month": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/11/index.json", + "path": "/timeline/2025/11/index.json", + "title": "Latest month (2025-11)", + "type": "application/hal\u002Bjson" + }, + "latest-patch": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/10.0.0/index.json", + "path": "/10.0/10.0.0/index.json", + "title": "Latest patch (10.0.0)", + "type": "application/hal\u002Bjson" + } + } + }, +``` + +This is just an inversion on the major version index. + +For light-duty compliance tools, the year index likely provides sufficient information. -The timeline index is different, but follows much the same approach. +### Month index + +The last index to consider is the month index. This is the month index for [January 2025](https://github.com/dotnet/core/blob/release-index/release-notes/timeline/2025/01/index.json). + +```json +{ + "$schema": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/schemas/dotnet-release-timeline-index.json", + "kind": "month-index", + "title": ".NET Release Timeline Index - 2025-01", + "description": "Release timeline for 2025-01 (latest: 9.0)", + "year": "2025", + "month": "01", + "security": true, + "cve_count": 4, + "cve_records": [ + "CVE-2025-21171", + "CVE-2025-21172", + "CVE-2025-21176", + "CVE-2025-21173" + ], + "latest_release": "9.0", + "releases": [ + "9.0", + "8.0" + ], + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/01/index.json", + "path": "/timeline/2025/01/index.json", + "title": "Release timeline index for 2025-01", + "type": "application/hal\u002Bjson" + }, + "prev": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2024/12/index.json", + "path": "/timeline/2024/12/index.json", + "title": "Release timeline index for 2024-12", + "type": "application/hal\u002Bjson" + }, + "timeline-index": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/index.json", + "path": "/timeline/index.json", + "title": ".NET Release Timeline Index", + "type": "application/hal\u002Bjson" + }, + "year-index": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/index.json", + "path": "/timeline/2025/index.json", + "title": ".NET Release Timeline Index - 2025", + "type": "application/hal\u002Bjson" + }, + "cve-json": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/01/cve.json", + "path": "/timeline/2025/01/cve.json", + "title": "CVE Information", + "type": "application/json" + }, + "cve-markdown": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/01/cve.md", + "path": "/timeline/2025/01/cve.md", + "title": "CVE Information", + "type": "application/markdown" + }, + "cve-markdown-rendered": { + "href": "https://github.com/dotnet/core/blob/main/release-notes/timeline/2025/01/cve.md", + "path": "/timeline/2025/01/cve.md", + "title": "CVE Information (Rendered)", + "type": "application/markdown" + } + }, +``` + +This schema also follows the same approach we've seen elsewhere. We also see `prev` wormhold links show up. They cross years, as can be seen in this example. This wormhole links makes backwards `foreach` from `latest-month` trivial. + +The `_embedded` property contains: `releases` and `disclosures`. + +```json + "_embedded": { + "releases": [ + { + "version": "9.0.1", + "date": "2025-01-14T00:00:00\u002B00:00", + "year": "2025", + "month": "01", + "security": true, + "cve_count": 4, + "cve_records": [ + "CVE-2025-21171", + "CVE-2025-21172", + "CVE-2025-21176", + "CVE-2025-21173" + ], + "support_phase": "active", + "sdk_patches": [ + "9.0.102" + ], + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.1/index.json", + "path": "/9.0/9.0.1/index.json", + "title": ".NET 9.0.1", + "type": "application/hal\u002Bjson" + }, + "latest-sdk": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/sdk/index.json", + "path": "/9.0/sdk/index.json", + "title": ".NET SDK 9.0 Release Information", + "type": "application/hal\u002Bjson" + } + } + }, + { + "version": "8.0.12", + "date": "2025-01-14T00:00:00\u002B00:00", + "year": "2025", + "month": "01", + "security": true, + "cve_count": 3, + "cve_records": [ + "CVE-2025-21172", + "CVE-2025-21176", + "CVE-2025-21173" + ], + "support_phase": "active", + "sdk_patches": [ + "8.0.405", + "8.0.308", + "8.0.112" + ], + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/8.0/8.0.12/index.json", + "path": "/8.0/8.0.12/index.json", + "title": ".NET 8.0.12", + "type": "application/hal\u002Bjson" + }, + "latest-sdk": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/8.0/sdk/index.json", + "path": "/8.0/sdk/index.json", + "title": ".NET SDK 8.0 Release Information", + "type": "application/hal\u002Bjson" + } + } + } + ], + "disclosures": [ + { + "id": "CVE-2025-21171", + "title": ".NET Remote Code Execution Vulnerability", + "_links": { + "self": { + "href": "https://github.com/dotnet/announcements/issues/340", + "title": "CVE-2025-21171" + } + }, + "fixes": [ + { + "href": "https://github.com/dotnet/runtime/commit/9da8c6a4a6ea03054e776275d3fd5c752897842e.diff", + "repo": "dotnet/runtime", + "branch": "release/9.0", + "title": "Fix commit in runtime (release/9.0)", + "release": "9.0" + } + ], + "cvss_score": 7.5, + "cvss_severity": "HIGH", + "disclosure_date": "2025-01-14", + "affected_releases": [ + "9.0" + ], + "affected_products": [ + "dotnet-runtime" + ], + "platforms": [ + "all" + ] + }, +``` + +It was stated earlier that the version indexes converges to a point while the timeline index coverges to a row of points. We see that on display here. Otherwise, this is a variation of what we saw in the patch version index. ## Design tradeoffs -There are lots of design tradeoffs within the graph design. Ergonomics vs update velocity were perhaps the fiercest of foes in the design. +There are lots of design tradeoffs within the graph design. Ergonomics vs update velocity were perhaps the most challenging constraints to balance. As mentioned multiple times, graph consistency is a major design requirement. The primary consideration is avoiding exposing currency that can be misused. If that's avoided, then there are no concerns with CDN consistency. @@ -607,11 +1393,13 @@ If we cleverly apply these rules, we can actually expand these major version obj }, ``` -That would be _so nice_. Consumers could wormhole to high value content without needing to go through the major version index or the year to get to the intended target. From a query ergonomics standpoint, this structure would be superior. +That would be _so nice_. Consumers could wormhole to high value content without needing to go through the major version index or or year to get to the intended target. From a query ergonomics standpoint, this structure would be superior. From a file-size standpoint, it would be acceptable. This approach doesn't violate the consistency rules. There is no badly-behaved currency that can be mis-used. The links are opague and notably target immutable resources. So, why not? Why can't we have nice things? -The issue is that these high-value links would require updating the root index once a month. Regular updates of a high-value resources signficantly increase the likelihood of an outage and reduces the time that the root index can be cached. Cache aggressiveness is part of the performance equation. It's much better to keep the root lean (skeletal) and highly cacheable. +The issue is that these high-value links would require updating the root index once a month. Regular updates of a high-value resource signficantly increase the likelihood of an outage and reduces the time that the root index can be cached. Cache aggressiveness is part of the performance equation. It's much better to keep the root lean (skeletal) and highly cacheable. + +On aspect that has (somewhat) haunted the design is deciding if it is appropriate to add a preview version to the high-value index for something as NOT mission critical as .NET 11 preview 1 (for example). On one hand, the answer is "definitely not". On another, by adding an `11.0` link in February, it is much more likely that all caches will have a root `index.json` with an 11.0 link, added in February, by November. We have to add the new major version sometime. Might as well add it at first availability, avoid a "has to be right" change on GA day, and ensure that all caches have the data they need when the special day comes. There has been significant discussion on consistency. We might as well complete the lesson. @@ -712,7 +1500,7 @@ Non goals: - Preview release details at the same fidelity as GA - Performance benchmark data -### Modeling as validation +## Modeling as validation As the final graph took shape, distinct relationships inherent to the resource modeling started to emerge. @@ -898,3 +1686,6 @@ done | sort -u | sort -r - **Navigation model:** The hal-index timeline is designed for chronological traversal. The releases-index has no concept of time-based navigation. **Winner:** hal-index (**27x smaller**) + + +Spec for LLMs: From 600a7a7f00f54cf37c4e4df08a20e559cd8e439e Mon Sep 17 00:00:00 2001 From: Rich Lander Date: Mon, 8 Dec 2025 08:44:13 -0800 Subject: [PATCH 04/15] Update spec --- .../release-notes-information-graph.md | 83 +++++++++++++----- .../releases-json-tokens.png | Bin 0 -> 125569 bytes 2 files changed, 62 insertions(+), 21 deletions(-) create mode 100644 accepted/2025/release-notes-information-graph/releases-json-tokens.png diff --git a/accepted/2025/release-notes-information-graph/release-notes-information-graph.md b/accepted/2025/release-notes-information-graph/release-notes-information-graph.md index 990513d4f..4d65dfe0b 100644 --- a/accepted/2025/release-notes-information-graph/release-notes-information-graph.md +++ b/accepted/2025/release-notes-information-graph/release-notes-information-graph.md @@ -1,6 +1,19 @@ # Exposing Release Notes as an Information graph -The .NET project has published release notes in JSON and markdown for many years. Our production of release notes has been based on the virtuous cloud-era idea that many deployment and compliance workflows require detailed and structured data to safely operate at scale. For the most part, a highly clustered set of consumers (like GitHub Actions and malware scanners) have adapted _their tools_ to _our formats_ to offer higher-level value to their users. That's all good. The LLM era is strikingly different where a much smaller set of information systems (LLM model companies) _consume and expose diverse data in standard ways_ according to _their (shifting) paradigms_ to a much larger set of users. The task at hand is to adapt release notes for LLM consumption while continuing to serve and improve workflows for cloud users. +The .NET project has published release notes in JSON and markdown for many years. The investment in quality release notes has been based on the virtuous cloud-era idea that many deployment and compliance workflows require detailed and structured data to safely operate at scale. For the most part, a highly clustered set of consumers (like GitHub Actions and malware scanners) have adapted _their tools_ to _our formats_ to offer higher-level value to their users. That's all good. The LLM era is strikingly different where a much smaller set of information systems (LLM model companies) _consume and expose diverse data in standard ways_ according to _their (shifting) paradigms_ to a much larger set of users. The task at hand is to modernize release notes to make them more efficient to consume generally and to adapt them for LLM consumption. + +Overall goals for release notes consumption: + +- Deliver high-performance (low kb cost) and high consistency (TTL resilience). +- Enable aestheticly pleasing queries that are terse, ergonomic, and effective, both for their own goals and as a proxy for LLM consumption. +- Support queries with multiple key styles, temporal and version-based queries. +- Expose runtime and SDK versions (as much as appropriate) at parity. +- Expose queryable data beyond version numbers, such as CVE disclosures, breaking changes, and download links. +- Use the same data to generate most release note files, like [releases.md](https://github.com/dotnet/core/blob/main/releases.md), and CVE announcements like [CVE-2025-55248](https://github.com/dotnet/announcements/issues/372), guaranteeing ensuring consistency from a single source of truth. +- Encode release notes update frequency and mechanics into the nature of the graph. +- Use this project as a real-world information graph pilot to inform other efforts that expose information to modern information consumers. + +## Scenario Release notes are mechanism, not scenario. It likely difficult for users to keep up with and act on the constant stream of .NET updates, typically one or two times a month. Users often have more than one .NET major version deployed, further complicating this puzzle. Many users rely on update orchestrators like APT, Yum, and Visual Studio, however, it is unlikely that such tools cover all the end-points that users care about in a uniform way. It is important that users can reliably make good, straightforward, and timely decisions about their entire infrastructure, orchestrated across a variety of deployment tools. This is a key scenario that release notes serve. @@ -13,23 +26,21 @@ Obvious questions release notes should answer: - How long until a given major release is EOL or has been EOL? - What are known upgrade challenges? -CIOs, CTOs, and others are accountable for maintaining efficient and secure continuity for a set of endpoints, including end-user desktops and cloud servers. They are unlikely to read markdown release notes or perform DIY `curl` + `jq` hacking with structured data. They will increasingly expect to be able to answer .NET-related compliance and deployment questions using chat assistants like Claude or Copilot. They may ask ChatGPT to compare treatment of an industry-wide CVE like [CVE-2023-44487](https://nvd.nist.gov/vuln/detail/cve-2023-44487) across multiple application stacks in their portfolio. This already works reasonably well, but fails when queries/prompts demand greater levels of detail with the expectation that they come from an authoritative source. It is very common to see assistants glean insight from a semi-arbitrary set of web pages with matching content. +CIOs, CTOs, and others are accountable for maintaining efficient and secure continuity for a set of endpoints, including end-user desktops and cloud servers. They are unlikely to read long markdown release notes or perform DIY `curl` + `jq` hacking with structured data. They will increasingly expect to be able to get answers to arbitrarily detailed compliance and deployment questions using chat assistants like Copilot. They may ask Claude to compare treatment of an industry-wide CVE like [CVE-2023-44487](https://nvd.nist.gov/vuln/detail/cve-2023-44487) across multiple application stacks in their portfolio. This already works reasonably well, but fails when prompts demand greater levels of detail and with the expectation that the source data comes from authoritative sources. It is very common to see assistants glean insight from a semi-arbitrary set of web pages with matching content. This is particularly problematic for day-of prompts (same day as a security release). -LLMs are _much more_ fickle relative to purpose-built tools. They are more likely to give up if release notes are not to their liking, instead relying on comfortable web search with its equal share of benefits and challenges. Regular testing (by the spec writer) of release notes with these chat assistants has demonstrated that LLMs are typically only satiated by a "Goldilocks meal". Obscure formats, large documents, and complicated workflows don't perform well (or outright fail). +Some users have told us that they enable Slack notifications for [dotnet/announcements](https://github.com/dotnet/announcements/issues), which is an existing "release notes beacon". That's great and intended. What if we could take that to a new level, thinking of release notes as queryable data used by notification systems and LLMs? There is a lesson here. Users (virtuously) complain when we [forget to lock issues](https://github.com/dotnet/announcements/issues/107#issuecomment-482166428). They value high signal to noise. Fortunately, we no longer forget for announcements, but we have not achieved this same disciplined model with GitHub release notes commits (as will be covered later). That's a good goal to set for this project. -In the early revisions of this project, the design followed our existing playbook, modeling parent/child relationships, linking to more detailed sources of information, and describing information domains in custom schemas. That then progressed into wanting to expose summaries of high-value information from leaf nodes into the trunk or as far as root nodes. This approach didn't work well since the design was lacking a broader information architecure. A colleague noted that the design was [Hypermedia as the engine of application state (HATEOAS)](https://en.wikipedia.org/wiki/HATEOAS)-esque but not using one of the standard formats. The benefits of using standard formats is that they are free to use, have gone through extensive design review, can be navigated with standard patterns and tools, and (most importantly) LLMs already understand their vocabulary and access patterns. A new format will be definition not have those characteristics. +LLMs are _much more_ fickle relative to purpose-built tools. They are more likely to give up if release notes are not to their liking, instead relying on comfortable web search with its equal share of benefits and challenges. Regular testing (by the spec writer) of release notes with chat assistants has demonstrated that LLMs are typically only satiated by a "Goldilocks meal". Obscure formats, large documents, and complicated workflows don't perform well (or outright fail). For example, LLMs choke on the 1MB [`releases.json`](https://github.com/dotnet/core/blob/main/release-notes/releases-index.json) files we maintain. -This proposal leans heavily on hypermedia, specifically [HAL+JSON](https://datatracker.ietf.org/doc/html/draft-kelly-json-hal). Hypermedia is very common, with [OpenAPI](https://www.openapis.org/what-is-openapi) and [JSON:API](https://jsonapi.org/) likely being the most common. LLMs are quite comfortable with these standards. The proposed use of HAL is a bit novel (not intended as a positive description). It is inspired by [llms.txt](https://llmstxt.org/) as an emerging standard and the idea that hypermedia is be the most natural next step to express complex diverse data and relationships. It's also expected (in fact, pre-ordained) that older standards will perform better than newer ones due to higher density (or presence at all) in the LLM training data. +![.NET 6.0 releases.json file as tokens](./releases-json-tokens.png) -Overall goals: +This image shows that the worst case for the `releases.json` format is 600k tokens using the [OpenAI Tokenzier](https://platform.openai.com/tokenizer). It is an understatement to say that a file of that size doesn't work well with LLMs. Context: memory budgets tend to max out at 200k tokens. Large JSON files _can_ be made to work in carefully orchestrated flows, but do not enable general purpose LLM workflows. -- Ensure high-performance (low kb cost) and high reliability (TTL resilience). -- Enable aestheticly pleasing queries that are terse, ergonomic, and effective, both for their own goals and as a proxy for LLM consumption. -- Enable queries with multiple key styles, temporal and version-based queries. -- Describe runtime and SDK versions (as much as appropriate) at parity. -- Integrate high value data, such as CVE disclosures, breaking changes, and download links. -- Enable generating most release note files, like [releases.md](https://github.com/dotnet/core/blob/main/releases.md), and CVE announcements like [CVE-2025-55248](https://github.com/dotnet/announcements/issues/372). -- Use this project as a real-world pilot for exposing an information graph equally to LLMs, client libraries, and DIY `curl` + `jq` hacking. +A major point is that workflows that are bad for LLMS are typically not _uniquely_ bad for LLMs but are challenging for other consumers. It is easy to guess that most readers of `releases-index.json` can be well-served by content significantly less than 1MB+ of JSON. This means that we need start from scratch with structured release notes. + +In the early revisions of this project, the design followed our existing playbook, modeling parent/child relationships, linking to more detailed sources of information, and describing information domains in custom schemas. That then progressed into wanting to expose summaries of high-value information from leaf nodes into the trunk or as far as root nodes. This approach didn't work well since the design was lacking a broader information architecure. A colleague noted that the design was [Hypermedia as the engine of application state (HATEOAS)](https://en.wikipedia.org/wiki/HATEOAS)-esque but not using one of the standard formats. The benefits of using standard formats is that they are free to use, have gone through extensive design review, can be navigated with standard patterns and tools, and (most importantly) LLMs already understand their vocabulary and access patterns. A new format will be definition not have those characteristics. + +This proposal leans heavily on hypermedia, specifically [HAL+JSON](https://datatracker.ietf.org/doc/html/draft-kelly-json-hal). Hypermedia is very common, with [OpenAPI](https://www.openapis.org/what-is-openapi) and [JSON:API](https://jsonapi.org/) likely being the most common. LLMs are quite comfortable with these standards. The proposed use of HAL is a bit novel (not intended as a positive description). It is inspired by [llms.txt](https://llmstxt.org/) as an emerging standard and the idea that hypermedia is be the most natural next step to express complex diverse data and relationships. It's also expected (in fact, pre-ordained) that older standards will perform better than newer ones due to higher density (or presence at all) in the LLM training data. ## Hypermedia graph design @@ -318,7 +329,7 @@ One layer lower, we have the major version idex. The followin example is the [ma { "$schema": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/schemas/dotnet-release-version-index.json", "kind": "major-version-index", - "title": ".NET 9.0 Patch Release Index", + "title": ".NET 9.0 Release Index", "description": ".NET 9.0 (latest: 9.0.11)", "latest": "9.0.11", "latest_security": "9.0.10", @@ -364,6 +375,12 @@ One layer lower, we have the major version idex. The followin example is the [ma "title": ".NET Release Index", "type": "application/hal\u002Bjson" }, + "compatibility-json": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/compatibility.json", + "path": "/9.0/compatibility.json", + "title": ".NET 9.0 Compatibility", + "type": "application/json" + }, "latest-release-json": { "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.11/release.json", "path": "/9.0/9.0.11/release.json", @@ -541,12 +558,6 @@ Back to `manifest.json`. It contains extra data that tools in particular might f "title": ".NET 9.0 Manifest", "type": "application/hal\u002Bjson" }, - "compatibility-json": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/compatibility.json", - "path": "/9.0/compatibility.json", - "title": ".NET 9.0 Compatibility", - "type": "application/json" - }, "compatibility-rendered": { "href": "https://learn.microsoft.com/dotnet/core/compatibility/9.0", "title": "Breaking changes in .NET 9", @@ -1559,7 +1570,7 @@ curl -s "$ROOT" | jq -r '.["releases-index"][] | select(.["support-phase"] == "a **Analysis:** - **Completeness:** ✅ Equal—both return the same list of supported versions. -- **Boolean vs enum:** The hal-index uses `supported: true`, a simple boolean. The releases-index uses `support-phase: "active"`, requiring knowledge of the enum vocabulary (active, maintenance, eol, preview, go-live). +- **Boolean vs enum:** The hal-index uses `supported: true`, a simple boolean. The releases-index pnly exposes `support-phase: "active"` (with hal-index also has), requiring knowledge of the enum vocabulary (active, maintenance, eol, preview, go-live). - **Property naming:** The hal-index uses `select(.supported)` with dot notation. The releases-index requires `select(.["support-phase"] == "active")` with bracket notation and string comparison. - **Query complexity:** The hal-index query is 30% shorter and more intuitive for someone unfamiliar with the schema. @@ -1687,5 +1698,35 @@ done | sort -u | sort -r **Winner:** hal-index (**27x smaller**) +## Cache-Control TTL Recommendations + +Time to Live (TTL) values are calibrated for "safe but maximally aggressive" caching based on update frequency and consistency requirements. + +### TTL Summary + +| Resource | Example Path | Update Frequency | Recommended TTL | Cache-Control Header | +|----------|--------------|------------------|-----------------|---------------------| +| Root index | `/index.json` | ~1×/year | 7 days | `public, max-age=604800` | +| Timeline root | `/timeline/index.json` | ~1×/year | 7 days | `public, max-age=604800` | +| Year index | `/timeline/2025/index.json` | ~12×/year | 4 hours | `public, max-age=14400` | +| Month index | `/timeline/2025/10/index.json` | Never (immutable) | 1 year | `public, max-age=31536000, immutable` | +| Major version index | `/9.0/index.json` | ~12×/year | 4 hours | `public, max-age=14400` | +| Patch version index | `/9.0/9.0.10/index.json` | Never (immutable) | 1 year | `public, max-age=31536000, immutable` | +| SDK index | `/9.0/sdk/index.json` | ~12×/year | 4 hours | `public, max-age=14400` | +| Exit nodes (CVE, markdown) | `/timeline/2025/10/cve.json` | Never (immutable) | 1 year | `public, max-age=31536000, immutable` | + +This doesn't take SDK-only releases or mistakes into account. + +## Rationale + +**Root and timeline root (7 days):** These change approximately once per year. A 7-day TTL provides strong caching while ensuring that when a new major version is added (even as a preview in February), propagation completes well before it matters. Worst case: a consumer sees stale data for a week after a yearly update. + +**Year and major version indexes (4 hours):** These update monthly, typically on Patch Tuesday. A 4-hour TTL balances freshness against CDN load. On release day, caches will converge within a business day. Between releases, these are effectively immutable and the 4-hour TTL is conservative. + +**Immutable resources (1 year + immutable directive):** Patch indexes, month indexes, and exit nodes are never modified after creation. The `immutable` directive tells browsers and CDNs to never revalidate—the URL is the version. One year is the practical maximum; longer provides no benefit. + +## LLM Consumption + +A major focus of Spec for LLMs: diff --git a/accepted/2025/release-notes-information-graph/releases-json-tokens.png b/accepted/2025/release-notes-information-graph/releases-json-tokens.png new file mode 100644 index 0000000000000000000000000000000000000000..8cf984b246f097489e8fceb19185eb3e5f1ee931 GIT binary patch literal 125569 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�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 JXGNRW{{u*g5<~z1 literal 0 HcmV?d00001 From 540e3dd3287017766ffe2986e6c6fd345918d475 Mon Sep 17 00:00:00 2001 From: Richard Lander Date: Tue, 16 Dec 2025 20:08:06 -0800 Subject: [PATCH 05/15] Update spec --- .../release-notes-information-graph.md | 235 +++++------------- 1 file changed, 65 insertions(+), 170 deletions(-) diff --git a/accepted/2025/release-notes-information-graph/release-notes-information-graph.md b/accepted/2025/release-notes-information-graph/release-notes-information-graph.md index 4d65dfe0b..364192e81 100644 --- a/accepted/2025/release-notes-information-graph/release-notes-information-graph.md +++ b/accepted/2025/release-notes-information-graph/release-notes-information-graph.md @@ -1,16 +1,15 @@ # Exposing Release Notes as an Information graph -The .NET project has published release notes in JSON and markdown for many years. The investment in quality release notes has been based on the virtuous cloud-era idea that many deployment and compliance workflows require detailed and structured data to safely operate at scale. For the most part, a highly clustered set of consumers (like GitHub Actions and malware scanners) have adapted _their tools_ to _our formats_ to offer higher-level value to their users. That's all good. The LLM era is strikingly different where a much smaller set of information systems (LLM model companies) _consume and expose diverse data in standard ways_ according to _their (shifting) paradigms_ to a much larger set of users. The task at hand is to modernize release notes to make them more efficient to consume generally and to adapt them for LLM consumption. +The .NET project has published release notes in JSON and markdown for many years. The investment in quality release notes has been based on the virtuous cloud-era idea that many deployment and compliance workflows require detailed and structured data to safely operate at scale. For the most part, a highly clustered set of consumers (like GitHub Actions and vulnerability scanners) have adapted _their tools_ to _our formats_ to offer higher-level value to their users. That's all good. The LLM era is strikingly different where a much smaller set of information systems (LLM model companies) _consume and expose diverse data in standard ways_ according to _their (shifting) paradigms_ to a much larger set of users. The task at hand is to modernize release notes to make them more efficient to consume generally and to adapt them for LLM consumption. Overall goals for release notes consumption: -- Deliver high-performance (low kb cost) and high consistency (TTL resilience). -- Enable aestheticly pleasing queries that are terse, ergonomic, and effective, both for their own goals and as a proxy for LLM consumption. -- Support queries with multiple key styles, temporal and version-based queries. -- Expose runtime and SDK versions (as much as appropriate) at parity. +- Graph schema encodes graph update frequency +- Satisfy reasonable expectations of performance (no 1MB JSON files), reliability, and consistency +- Enable aestheticly-pleasing queries that are terse, ergonomic, and effective, both for their own goals and as a proxy for LLM consumption. +- Support queries with multiple key styles, temporal and version-based (runtime and SDK versions) queries. - Expose queryable data beyond version numbers, such as CVE disclosures, breaking changes, and download links. -- Use the same data to generate most release note files, like [releases.md](https://github.com/dotnet/core/blob/main/releases.md), and CVE announcements like [CVE-2025-55248](https://github.com/dotnet/announcements/issues/372), guaranteeing ensuring consistency from a single source of truth. -- Encode release notes update frequency and mechanics into the nature of the graph. +- Use the same data to generate most release note markdown files, like [releases.md](https://github.com/dotnet/core/blob/main/releases.md), and CVE announcements like [CVE-2025-55248](https://github.com/dotnet/announcements/issues/372), guaranteeing ensuring consistency from a single source of truth. - Use this project as a real-world information graph pilot to inform other efforts that expose information to modern information consumers. ## Scenario @@ -28,23 +27,23 @@ Obvious questions release notes should answer: CIOs, CTOs, and others are accountable for maintaining efficient and secure continuity for a set of endpoints, including end-user desktops and cloud servers. They are unlikely to read long markdown release notes or perform DIY `curl` + `jq` hacking with structured data. They will increasingly expect to be able to get answers to arbitrarily detailed compliance and deployment questions using chat assistants like Copilot. They may ask Claude to compare treatment of an industry-wide CVE like [CVE-2023-44487](https://nvd.nist.gov/vuln/detail/cve-2023-44487) across multiple application stacks in their portfolio. This already works reasonably well, but fails when prompts demand greater levels of detail and with the expectation that the source data comes from authoritative sources. It is very common to see assistants glean insight from a semi-arbitrary set of web pages with matching content. This is particularly problematic for day-of prompts (same day as a security release). -Some users have told us that they enable Slack notifications for [dotnet/announcements](https://github.com/dotnet/announcements/issues), which is an existing "release notes beacon". That's great and intended. What if we could take that to a new level, thinking of release notes as queryable data used by notification systems and LLMs? There is a lesson here. Users (virtuously) complain when we [forget to lock issues](https://github.com/dotnet/announcements/issues/107#issuecomment-482166428). They value high signal to noise. Fortunately, we no longer forget for announcements, but we have not achieved this same disciplined model with GitHub release notes commits (as will be covered later). That's a good goal to set for this project. +Some users have told us that they enable Slack notifications for [dotnet/announcements](https://github.com/dotnet/announcements/issues), which is an existing "release notes beacon". That's great and intended. What if we could take that to a new level, thinking of release notes as queryable data used by notification systems and LLMs? There is a lesson here. Users (virtuously) complain when we [forget to lock issues](https://github.com/dotnet/announcements/issues/107#issuecomment-482166428). They value high signal to noise. Fortunately, we no longer forget for announcements, but we have not achieved this same disciplined model with GitHub release notes commits (as will be covered later). It should just just as safe and reliable to use release notes updates as a beacon as dotnet/announcements. -LLMs are _much more_ fickle relative to purpose-built tools. They are more likely to give up if release notes are not to their liking, instead relying on comfortable web search with its equal share of benefits and challenges. Regular testing (by the spec writer) of release notes with chat assistants has demonstrated that LLMs are typically only satiated by a "Goldilocks meal". Obscure formats, large documents, and complicated workflows don't perform well (or outright fail). For example, LLMs choke on the 1MB [`releases.json`](https://github.com/dotnet/core/blob/main/release-notes/releases-index.json) files we maintain. +LLMs are a different kind of "user" than we've previously tried to enable. LLMs are _much more_ fickle relative to purpose-built tools. They are more likely to give up if release notes are not to their liking, instead relying on model knowledge or comfortable web search with its equal share of benefits and challenges. Regular testing (by the spec writer) of release notes with chat assistants has demonstrated that LLMs are typically only satiated by a "Goldilocks meal". Obscure formats, large documents, and complicated workflows don't perform well (or outright fail). LLMs will happily jump to `releases-index.json` and choke on the 1MB+ [`releases.json`](https://github.com/dotnet/core/blob/main/release-notes/releases-index.json) files we maintain if prompts are unable to keep their attention. ![.NET 6.0 releases.json file as tokens](./releases-json-tokens.png) -This image shows that the worst case for the `releases.json` format is 600k tokens using the [OpenAI Tokenzier](https://platform.openai.com/tokenizer). It is an understatement to say that a file of that size doesn't work well with LLMs. Context: memory budgets tend to max out at 200k tokens. Large JSON files _can_ be made to work in carefully orchestrated flows, but do not enable general purpose LLM workflows. +This image shows that the worst case for the `releases.json` format is 600k tokens using the [OpenAI Tokenzier](https://platform.openai.com/tokenizer). It is an understatement to say that a file of that size doesn't work well with LLMs. Context: memory budgets tend to max out at 200k tokens. Large JSON files can be made to work in some scenarios, but not in the general case. -A major point is that workflows that are bad for LLMS are typically not _uniquely_ bad for LLMs but are challenging for other consumers. It is easy to guess that most readers of `releases-index.json` can be well-served by content significantly less than 1MB+ of JSON. This means that we need start from scratch with structured release notes. +A major point is that workflows that are bad for LLMS are typically not _uniquely_ bad for LLMs but are challenging for other consumers. It is easy to guess that most readers of `releases-index.json` would be better-served by content significantly less than 1MB+ of JSON. This means that we need start from scratch with structured release notes. -In the early revisions of this project, the design followed our existing playbook, modeling parent/child relationships, linking to more detailed sources of information, and describing information domains in custom schemas. That then progressed into wanting to expose summaries of high-value information from leaf nodes into the trunk or as far as root nodes. This approach didn't work well since the design was lacking a broader information architecure. A colleague noted that the design was [Hypermedia as the engine of application state (HATEOAS)](https://en.wikipedia.org/wiki/HATEOAS)-esque but not using one of the standard formats. The benefits of using standard formats is that they are free to use, have gone through extensive design review, can be navigated with standard patterns and tools, and (most importantly) LLMs already understand their vocabulary and access patterns. A new format will be definition not have those characteristics. +In the early revisions of this project, the design followed our existing schema playbook, modeling parent/child relationships, linking to more detailed sources of information, and describing information domains in custom schemas. That then progressed into wanting to expose summaries of high-value information from leaf nodes into the trunk. This approach didn't work well since the design was lacking a broader information architecure. A colleague noted that the design was [Hypermedia as the engine of application state (HATEOAS)](https://en.wikipedia.org/wiki/HATEOAS)-esque but not using one of the standard formats. The benefits of using standard formats is that they are free to use, have gone through extensive design review, can be navigated with standard patterns and tools, and (most importantly) LLMs already understand their vocabulary and access patterns. A new format will by definition not have those characteristics. -This proposal leans heavily on hypermedia, specifically [HAL+JSON](https://datatracker.ietf.org/doc/html/draft-kelly-json-hal). Hypermedia is very common, with [OpenAPI](https://www.openapis.org/what-is-openapi) and [JSON:API](https://jsonapi.org/) likely being the most common. LLMs are quite comfortable with these standards. The proposed use of HAL is a bit novel (not intended as a positive description). It is inspired by [llms.txt](https://llmstxt.org/) as an emerging standard and the idea that hypermedia is be the most natural next step to express complex diverse data and relationships. It's also expected (in fact, pre-ordained) that older standards will perform better than newer ones due to higher density (or presence at all) in the LLM training data. +This proposal leans heavily on hypermedia, specifically [HAL+JSON](https://datatracker.ietf.org/doc/html/draft-kelly-json-hal). Hypermedia is very common, with [OpenAPI](https://www.openapis.org/what-is-openapi) and [JSON:API](https://jsonapi.org/) likely being the most common. LLMs are quite comfortable with these standards. The proposed use of HAL is a bit novel (not intended as a positive descriptor). It is inspired by [llms.txt](https://llmstxt.org/) as an emerging standard and the idea that hypermedia is the most natural next step to express complex diverse data and relationships. It's also expected (in fact, pre-ordained) that older standards will perform better than newer ones due to higher density (or presence at all) in the LLM training data. ## Hypermedia graph design -This project has adopted the idea that a wide and deep information graph can expose significant information within the graph that satisfies user queries without loading other files. The graph doesn't need to be skeletal. It can have some shape on it. In fact our existing graph with [`release-index.json`](https://github.com/dotnet/core/blob/main/release-notes/releases-index.json) already does this but without the benefit of a standard format or architectural principles. +This project has adopted the idea that a wide and deep information graph can expose significant information within the graph that satisfies user queries without loading other files. The graph doesn't need to be skeletal. It can have some shape on it. In fact our existing graph with [`release-index.json`](https://github.com/dotnet/core/blob/main/release-notes/releases-index.json) already does this, but without the benefit of a standard format or architectural principles. The design intent is that a graph should be skeletal at its roots for performance and to avoid punishing queries that do not benefit from the curated shape. The deeper the node is in the graph, the more shape (or weight) it should take on since the data curation is much more likely to hit the mark. @@ -79,7 +78,7 @@ Here is a simple example from the HAL spec: } ``` -The `_links` property is a dictionary of link objects with specific named relations. Most link dictionaries start with the standard `self` relation. The `self` relation describes the canonical URL of the given resource. The `warehouse` and `invoice` relations are examples of domain-specific relations. Together, they establish a navigation protocol for this resource domain. One can also imagine `next`, `previous`, `buy-again` as relations for e-commerce. Domain-specific HAL readers will understand these relations and know how or when to act on them. +The `_links` property is a dictionary of link objects with specific named relations. Most link dictionaries start with the standard `self` relation. The `self` relation describes the canonical URL of the given resource. The `warehouse` and `invoice` relations are examples of domain-specific relations. Together, they establish a navigation protocol for this resource domain. One can also imagine `next`, `previous`, `buy-again`, or `i-am-feeling-lucky` as relations for e-commerce. Domain-specific HAL readers will understand these relations and know how or when to act on them. The `currency`, `status`, and `total` properties provide additional domain-specific resource metadata. The package should arrive at your door soon! @@ -120,7 +119,7 @@ The following example is similar, with the addition of the `_embedded` property. The `_embedded` property contains order resources. This is the resource payload. Each of those order items have `self` and other related link relations referencing other resources. As stated earlier, the `self` relation references the canonical copy of the resource. Embedded resources may be a full or partial copy of the resource. Again, domain-specific reader will understand this schema and know how to process it. -This design aspect is the true strength of HAL. It's the mechanism that enables the overall approach of a skeletal root with weighted bottom nodes. It's also what enables these two seemingly anemic properties to provide so much modeling value. +This design aspect is the true strength of HAL, of projecting partial views of resources to their reference. It's the mechanism that enables the overall approach of a skeletal root with weighted bottom nodes. It's also what enables these two seemingly anemic properties to provide so much modeling value. The `currentlyProcessing` and `shippedToday` properties provide additional information about ongoing operations. @@ -169,7 +168,7 @@ The graph has one rule: > Every resource in the graph needs to be guaranteed consistent with every other part of the graph. -The unstated problem is CDN caching. Assume that the entire graph is consistent when uploaded to an origin server. A CDN server is guaranteed by construction to serve both old and new copies of the graph leading to potential inconsistencies. The graph construction needs to be resilient to that. +The unstated problem is CDN caching. Assume that the entire graph is consistent when uploaded to an origin server. A CDN server is guaranteed by construction to serve both old and new copies of the graph -- for existing files that have been updated -- leading to potential inconsistencies. The graph construction needs to be resilient to that. Related examples: @@ -192,9 +191,9 @@ The point about the root index isn't a "solution" but an implication of the firs There are videos on YouTube with these [crazy gear reductions](https://www.youtube.com/watch?v=QwXK4e4uqXY). You can watch them for a long time! Keen observers will realize our graph will be nothing like that. Well, kindof. One can model years and months and major and patch versions as spinning gears with a differing number of teeth and revolution times. It just won't look the same as those lego videos. -A celestial orbit analogy would have worked just as well. +A stellar orbit analogy would have worked just as well. -Release notes graph indexes update (gear reduce) like the following: +Release notes graph indexes are updatd (gear reduce) like the following: - Timeline index (list of years): one update per year - Year index (list of months): one update per month @@ -208,9 +207,17 @@ The same progression for versions: It's the middle section changing constantly, but the roots and the leaves are either immutable or close enough to it. -Note: Some annoying details, like SDK-only releaes, have been ignored. The intent is to reason about rough order of magnitude and the fundamental pressure being applied to each layer. +Note: Some annoying details, like SDK-only releases, have been ignored. The intent is to reason about rough order of magnitude and the fundamental pressure being applied to each layer. -A key question about this scheme is when we add new releases. The most obvious answer to add new releaes +A key question about this scheme is when we add new releases. The most obvious answer to add new releases at Preview 1. The other end of the spectrum would be at GA. From a mission-critical standpoint, GA sounds better. Add it when it is needed and can be acted on for mission critical use. Unintuitively, this approach is likely a priority inversion. + +We should add vNext releases at Preview 1 for the following reasons: + +- vNext is available (in preview form), we so we should advertise it. +- Special once-in-a-release tasks are more likely to fail when done on GA day. +- Adding vNext early enables consumers to cache aggressively. + +The intent is that root `index.json` can be cached aggressively. Adding vNext to `index.json` with Preview 1 is perfectly aligned with that. Adding vNext at GA is not. ## Version Index Modeling @@ -222,57 +229,28 @@ Most nodes in the graph are named `index.json`. This is the root [index.json](ht ```json { - "$schema": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/schemas/dotnet-release-version-index.json", + "$schema": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/schemas/v1/dotnet-release-version-index.json", "kind": "releases-index", "title": ".NET Release Index", - "description": ".NET Release Index (latest: 10.0)", "latest": "10.0", "latest_lts": "10.0", "_links": { "self": { "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/index.json", - "path": "/index.json", - "title": ".NET Release Index", - "type": "application/hal\u002Bjson" + "title": ".NET Release Index" }, "latest": { "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/index.json", - "path": "/10.0/index.json", - "title": "Latest .NET release (.NET 10.0)", - "type": "application/hal\u002Bjson" + "title": "Major version index" }, "latest-lts": { "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/index.json", - "path": "/10.0/index.json", - "title": "Latest LTS release (.NET 10.0)", - "type": "application/hal\u002Bjson" - }, - "latest-sdk": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/sdk/index.json", - "path": "/10.0/sdk/index.json", - "title": "Latest .NET SDK (10.0)", - "type": "application/hal\u002Bjson" + "title": "Latest LTS" }, "timeline-index": { "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/index.json", - "path": "/timeline/index.json", - "title": ".NET Release Timeline Index", - "type": "application/hal\u002Bjson" + "title": ".NET Release Timeline Index" }, - "llms-txt": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/llms/README.md", - "path": "/llms/README.md", - "title": "LLM Quick Reference", - "type": "application/markdown" - } - }, - "glossary": { - "lts": "Long-Term Support \u2013 3-year support window", - "sts": "Standard-Term Support \u2013 18-month support window", - "latest-release": "Highest stable (GA) major version; excludes preview and RC releases", - "latest": "Alias for latest-release", - "latest-lts": "Highest stable LTS major version currently in support", - "latest-sdk": "SDK index for the latest stable major version" }, ``` @@ -282,9 +260,7 @@ Key points: - `kind`, `title`, and `description` describe the resource - `latest` and `latest_lts` describe high-level resource metadata, often useful currency that helps contextualize the rest of the resource without the need to parse/split strings. For example, the `latest_lts` scalar describes the target of the `latest-lts` link relation. - `timeline-index` provides a "wormhole link" (more on that later) to another part of the graph -- `llms-txt` provides instructive content for an LLM. - Core schema syntax like `latest_lts` uses snake-case-lower for query ergonomics (using `jq` as the proxy for that), while relations like `latest-lts` use kebab-case-lower since they can be names or brands. This follows the approach used by [cve-schema](https://github.com/dotnet/designs/blob/main/accepted/2025/cve-schema/cve_schema.md#brand-names-vs-schema-fields-mixed-naming-strategy). -- `glossary` provides consumer with display or knowledge strings, depending on the need. The `_embedded` section has one child, `releases`: @@ -295,12 +271,9 @@ The `_embedded` section has one child, `releases`: "version": "10.0", "release_type": "lts", "supported": true, - "eol_date": "2028-11-14T00:00:00\u002B00:00", "_links": { "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/index.json", - "title": ".NET 10.0", - "type": "application/hal\u002Bjson" + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/index.json" } } }, @@ -308,18 +281,15 @@ The `_embedded` section has one child, `releases`: "version": "9.0", "release_type": "sts", "supported": true, - "eol_date": "2026-11-10T00:00:00\u002B00:00", "_links": { "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/index.json", - "title": ".NET 9.0", - "type": "application/hal\u002Bjson" + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/index.json" } } }, ``` -This is where we see the design diverge significantly from `releases-index.json`. There are no patch versions, no statement about security releases. It's the most minimal data to determine the release type, if/when it is supported until, and how to access the canonical resource that exposes richer information. This approach removes the need to update the root index monthly. +This is where we see the design diverge significantly from `releases-index.json`. There are no patch versions, no statement about security releases. It's the most minimal data to determine the release type, i it is supported, and how to access the canonical resource that exposes richer information. This approach removes the need to update the root index monthly. ### Major version index @@ -327,10 +297,10 @@ One layer lower, we have the major version idex. The followin example is the [ma ```json { - "$schema": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/schemas/dotnet-release-version-index.json", + "$schema": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/schemas/v1/dotnet-release-version-index.json", "kind": "major-version-index", "title": ".NET 9.0 Release Index", - "description": ".NET 9.0 (latest: 9.0.11)", + "target_framework": "net9.0", "latest": "9.0.11", "latest_security": "9.0.10", "release_type": "sts", @@ -340,110 +310,67 @@ One layer lower, we have the major version idex. The followin example is the [ma "eol_date": "2026-11-10T00:00:00\u002B00:00", "_links": { "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/index.json", - "path": "/9.0/index.json", - "title": ".NET 9.0", - "type": "application/hal\u002Bjson" + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/index.json" + }, + "downloads": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/downloads/index.json", + "title": ".NET 9.0 Downloads" }, "latest": { "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.11/index.json", - "path": "/9.0/9.0.11/index.json", - "title": "Latest patch release (9.0.11)", - "type": "application/hal\u002Bjson" + "title": "Latest patch" }, "latest-sdk": { "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/sdk/index.json", - "path": "/9.0/sdk/index.json", - "title": ".NET SDK 9.0 Release Information", - "type": "application/hal\u002Bjson" + "title": ".NET SDK 9.0 Release Information" }, "latest-security": { "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.10/index.json", - "path": "/9.0/9.0.10/index.json", - "title": "Latest security patch (9.0.10)", - "type": "application/hal\u002Bjson" + "title": "Latest security patch" }, "release-manifest": { "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/manifest.json", - "path": "/9.0/manifest.json", - "title": "Release manifest", - "type": "application/hal\u002Bjson" + "title": "Release manifest" }, "releases-index": { "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/index.json", - "path": "/index.json", - "title": ".NET Release Index", - "type": "application/hal\u002Bjson" - }, - "compatibility-json": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/compatibility.json", - "path": "/9.0/compatibility.json", - "title": ".NET 9.0 Compatibility", - "type": "application/json" - }, - "latest-release-json": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.11/release.json", - "path": "/9.0/9.0.11/release.json", - "title": "Latest release information (9.0.11)", - "type": "application/json" + "title": "Release index" } }, - "glossary": { - "lts": "Long-Term Support \u2013 3-year support window", - "sts": "Standard-Term Support \u2013 18-month support window", - "preview": "Pre-release phase for testing and feedback; not supported", - "go-live": "Production-supported release candidate before GA", - "active": "Full support with functional and security improvements", - "maintenance": "Final 6 months of support; security fixes only", - "eol": "End of Life \u2013 No longer supported", - "feature-band": "Quarterly SDK minor version releases (e.g., 8.0.1xx, 8.0.2xx)", - "patch": "Cumulative monthly update released on Patch Tuesday", - "latest-release": "Highest stable (GA) major version; excludes preview and RC releases", - "latest": "Alias for latest-release", - "latest-lts": "Highest stable LTS major version currently in support", - "latest-sdk": "SDK index for the latest stable major version", - "latest-security": "Most recent patch release containing security fixes" - }, ``` This index includes much more useful and detailed information, both metadata/currency and patch-version links. It starts to answer the question of "what should I care about _now_?". Much of the form is similar to the root index. Instead of `latest_lts`, there is `latest_security`. A new addition is `release-manifest`. That relation stores important but lower value content about a given major release. That will be covered shortly. -The `_embeeded` section has three children: `releases`, `years`, and `cve_records`. +The `_embeeded` section has two children: `releases` and `years`. ```json "_embedded": { - "releases": [ + "patches": [ { "version": "9.0.11", - "date": "2025-11-11T00:00:00\u002B00:00", + "release": "9.0", + "date": "2025-11-19T00:00:00\u002B00:00", "year": "2025", "month": "11", "security": false, "cve_count": 0, "support_phase": "active", - "sdk_patches": [ - "9.0.307", - "9.0.112" - ], + "sdk_version": "9.0.308", "_links": { "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.11/index.json", - "path": "/9.0/9.0.11/index.json", - "title": "9.0.11 Patch Index", - "type": "application/hal\u002Bjson" + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.11/index.json" }, "release-month": { "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/11/index.json", - "path": "/timeline/2025/11/index.json", - "title": "Release timeline index for 2025-11", - "type": "application/hal\u002Bjson" + "title": "Release month index" } } }, { "version": "9.0.10", + "release": "9.0", "date": "2025-10-14T00:00:00\u002B00:00", "year": "2025", "month": "10", @@ -455,34 +382,25 @@ The `_embeeded` section has three children: `releases`, `years`, and `cve_record "CVE-2025-55315" ], "support_phase": "active", - "sdk_patches": [ - "9.0.306", - "9.0.111" - ], + "sdk_version": "9.0.306", "_links": { "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.10/index.json", - "path": "/9.0/9.0.10/index.json", - "title": "9.0.10 Patch Index", - "type": "application/hal\u002Bjson" + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.10/index.json" }, "release-month": { "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/index.json", - "path": "/timeline/2025/10/index.json", - "title": "Release timeline index for 2025-10", - "type": "application/hal\u002Bjson" + "title": "Release month index" }, "cve-json": { "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/cve.json", - "path": "/timeline/2025/10/cve.json", - "title": "CVE Information", + "title": "CVE records (JSON)", "type": "application/json" } } - }, + }, ``` -and the other children: +and years: ```json "years": [ @@ -490,10 +408,7 @@ and the other children: "year": "2025", "_links": { "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/index.json", - "path": "/timeline/2025/index.json", - "title": ".NET Release Timeline 2025 (chronological)", - "type": "application/hal\u002Bjson" + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/index.json" } } }, @@ -501,34 +416,14 @@ and the other children: "year": "2024", "_links": { "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2024/index.json", - "path": "/timeline/2024/index.json", - "title": ".NET Release Timeline 2024 (chronological)", - "type": "application/hal\u002Bjson" + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2024/index.json" } } } - ], - "cve_records": [ - "CVE-2024-43483", - "CVE-2024-43498", - "CVE-2024-43499", - "CVE-2025-21171", - "CVE-2025-21172", - "CVE-2025-21173", - "CVE-2025-21176", - "CVE-2025-24070", - "CVE-2025-26646", - "CVE-2025-26682", - "CVE-2025-30399", - "CVE-2025-55247", - "CVE-2025-55248", - "CVE-2025-55315" ] - }, ``` -This patch-version objects contains even more detailed information that can drive deployment and compliance workflows. The first two link relations are HAL links. The last is a plain JSON link. Most non-HAL links end in the format, like `json` or `markdown` or `markdown-rendered`. The links are raw text by default, with `-rendered` HTML content being useful for content targeted for human consumption, for example in generated release notes. +The `patches` objects contain more detailed information which can drive deployment and compliance workflows. The first two link relations, `self` and `release-month` are HAL links while `cve-json` a plain JSON link. Most non-HAL links end in the format, like `json` or `markdown` or `markdown-rendered`. The links are raw text by default, with `-rendered` HTML content being useful for content targeted for human consumption, for example in generated release notes. As mentioned earlier, the design has a concept of "wormhole links". That's what we see with `release-month`. It provides direct access to a high-relevance (potentially graph-distant) resource that would otherwise require awkward indirections, multiple network hops, and wasted bytes/tokens to acquire. These wormhole links massively improve query ergonomics for sophisticated queries. There are multiple of these wormhole links, not just `release-month` that are sprinkled throughout the graph for this purpose. They also provide hints on how the graph is intended to be traversed. From 057e6f71fa446a4fd7c213310e744f03a417a6c3 Mon Sep 17 00:00:00 2001 From: Rich Lander Date: Tue, 16 Dec 2025 22:35:22 -0800 Subject: [PATCH 06/15] Add wormhole section --- .../release-notes-information-graph.md | 157 +++++++++--------- 1 file changed, 82 insertions(+), 75 deletions(-) diff --git a/accepted/2025/release-notes-information-graph/release-notes-information-graph.md b/accepted/2025/release-notes-information-graph/release-notes-information-graph.md index 364192e81..3f5c05de0 100644 --- a/accepted/2025/release-notes-information-graph/release-notes-information-graph.md +++ b/accepted/2025/release-notes-information-graph/release-notes-information-graph.md @@ -35,7 +35,7 @@ LLMs are a different kind of "user" than we've previously tried to enable. LLMs This image shows that the worst case for the `releases.json` format is 600k tokens using the [OpenAI Tokenzier](https://platform.openai.com/tokenizer). It is an understatement to say that a file of that size doesn't work well with LLMs. Context: memory budgets tend to max out at 200k tokens. Large JSON files can be made to work in some scenarios, but not in the general case. -A major point is that workflows that are bad for LLMS are typically not _uniquely_ bad for LLMs but are challenging for other consumers. It is easy to guess that most readers of `releases-index.json` would be better-served by content significantly less than 1MB+ of JSON. This means that we need start from scratch with structured release notes. +A strong belief is that workflows that are bad for LLMS are typically not _uniquely_ bad for LLMs but are challenging for other consumers. It is easy to guess that most readers of `releases-index.json` would be better-served by referenced JSON significantly less than 1MB+. This means that we need start from scratch with structured release notes. In the early revisions of this project, the design followed our existing schema playbook, modeling parent/child relationships, linking to more detailed sources of information, and describing information domains in custom schemas. That then progressed into wanting to expose summaries of high-value information from leaf nodes into the trunk. This approach didn't work well since the design was lacking a broader information architecure. A colleague noted that the design was [Hypermedia as the engine of application state (HATEOAS)](https://en.wikipedia.org/wiki/HATEOAS)-esque but not using one of the standard formats. The benefits of using standard formats is that they are free to use, have gone through extensive design review, can be navigated with standard patterns and tools, and (most importantly) LLMs already understand their vocabulary and access patterns. A new format will by definition not have those characteristics. @@ -219,9 +219,42 @@ We should add vNext releases at Preview 1 for the following reasons: The intent is that root `index.json` can be cached aggressively. Adding vNext to `index.json` with Preview 1 is perfectly aligned with that. Adding vNext at GA is not. +## Graph wormholes + +There are many design paradigms and tradeoffs one can consider with a graph like this. A major design point is the skeletal roots and weighted bottom. That was already been covered. This approach forces significant fetches and inefficiency to get anywhere. To mitigate that, the graph includes a significant number of helpful workflow-specific wormhole links. As the name suggests, these links enable jumps from one part of the graph to another, aligned with expected queries. The wormholes are the most elaborate in the warm branches since they are regularly updated. + +The following are the primary wormhole links. + +Cold roots: + +- `latest` and `latest-lts` -- enables jumping to the matching major version index. +- `latest-year` -- enables jumping to the latest year index. + +Warm branches: + +- `latest` and `latest-security` -- enables jumping to the matching patch version index. +- `latest-month` and `latest-security-month` -- enables jumping to the latest matching month. +- `release-month` -- enables jumping to the month index for a patch version. + +Immutable leaves: + +- `prev` and `prev-security` -- enables jumping to an earlier matching patch version or month index. + +The wormhole links are what make the graph a graph and not just two trees provided alternative views of the same data. They also enable efficient navigation. + +In some cases, it may be better to look at `timeline/2025/index.json` and consider all the security months. In other cases, it may be more efficient to jump to `latest-security-month` and then backwards in time via `prev-security`. Both are possible and legitimate. Note that `prev-security` jumps across year boundaries. + +There are lots of algoriths that [work best when counting backwards](https://www.benjoffe.com/fast-date-64). + +There is no `next` or `next-security`. `prev` and `prev-security` link immutable leaves. It is easy to create a linked-list based on past knowledge. It's not possible to provide `next` links an immutability constraint. + +There is no `latest-sts` link because it's not really useful. `latest` covers it. + +Testing has demonstrated that these wormhole links are one of the defining features of the graph. + ## Version Index Modeling -The resource modeling with the graph has to satisfy the rate of these turning gears. The key technique is noticing when a design choice forces a faster update schedule than desired or exposes currency that could be misused. +The resource modeling witin the graph has to satisfy the intended "gear reduction" mentioned earlier. The key technique is noticing when a design choice forces a faster update schedule than desired or exposes currency that could be misused. This includes the wormhole links, just discussed. ### Releases index @@ -257,9 +290,10 @@ Most nodes in the graph are named `index.json`. This is the root [index.json](ht Key points: - Schema reference is included -- `kind`, `title`, and `description` describe the resource +- All links are raw content (eventually will transition to `builds.dotnet.microsoft.com). +- `kind` and `title` describe the resource - `latest` and `latest_lts` describe high-level resource metadata, often useful currency that helps contextualize the rest of the resource without the need to parse/split strings. For example, the `latest_lts` scalar describes the target of the `latest-lts` link relation. -- `timeline-index` provides a "wormhole link" (more on that later) to another part of the graph +- `timeline-index` provides a wormhole link to another part of the graph - Core schema syntax like `latest_lts` uses snake-case-lower for query ergonomics (using `jq` as the proxy for that), while relations like `latest-lts` use kebab-case-lower since they can be names or brands. This follows the approach used by [cve-schema](https://github.com/dotnet/designs/blob/main/accepted/2025/cve-schema/cve_schema.md#brand-names-vs-schema-fields-mixed-naming-strategy). The `_embedded` section has one child, `releases`: @@ -289,11 +323,11 @@ The `_embedded` section has one child, `releases`: }, ``` -This is where we see the design diverge significantly from `releases-index.json`. There are no patch versions, no statement about security releases. It's the most minimal data to determine the release type, i it is supported, and how to access the canonical resource that exposes richer information. This approach removes the need to update the root index monthly. +This is where we see the design diverge significantly from `releases-index.json`. There are no patch versions, no statement about security releases. It's the most minimal data to determine the release type, if it is supported, and how to access the canonical resource that exposes richer information. This approach removes the need to update the root index monthly. It's fine for tools to regenerate this file monthly. `git` should not see any diffs. ### Major version index -One layer lower, we have the major version idex. The followin example is the [major version index for .NET 9](https://github.com/dotnet/core/blob/release-index/release-notes/9.0/index.json). +One layer lower, we have the major version idex. The following example is the [major version index for .NET 9](https://github.com/dotnet/core/blob/release-index/release-notes/9.0/index.json). ```json { @@ -423,15 +457,17 @@ and years: ] ``` -The `patches` objects contain more detailed information which can drive deployment and compliance workflows. The first two link relations, `self` and `release-month` are HAL links while `cve-json` a plain JSON link. Most non-HAL links end in the format, like `json` or `markdown` or `markdown-rendered`. The links are raw text by default, with `-rendered` HTML content being useful for content targeted for human consumption, for example in generated release notes. +The `patches` object contains detailed information that can drive deployment and compliance workflows. The first two link relations, `self` and `release-month` are HAL links while `cve-json` is a plain JSON link. Most non-HAL links end in the given format, like `json` or `markdown` or `markdown-rendered`. The links are raw text by default, with `-rendered` HTML content being useful for content targeted for human consumption, for example in generated release notes. -As mentioned earlier, the design has a concept of "wormhole links". That's what we see with `release-month`. It provides direct access to a high-relevance (potentially graph-distant) resource that would otherwise require awkward indirections, multiple network hops, and wasted bytes/tokens to acquire. These wormhole links massively improve query ergonomics for sophisticated queries. There are multiple of these wormhole links, not just `release-month` that are sprinkled throughout the graph for this purpose. They also provide hints on how the graph is intended to be traversed. +As mentioned earlier, the design has a concept of "wormhole links". That's what we see with `release-month`. It provides direct access to a high-relevance (potentially graph-distant) resource that would otherwise require awkward indirections, multiple network hops, and wasted bytes/tokens to acquire. These wormhole links massively improve query ergonomics for sophisticated queries. There is a link `cve.json` file. Our [CVE schema](https://github.com/dotnet/designs/blob/main/accepted/2025/cve-schema/cve_schema.md) is a custom schema with no HAL vocabulary. It's an exit node of the graph. The point is that we're free to describe complex domains, like CVE disclosures, using a clean-slate design methodology. One can also see that some of the `cve.json` information has been projected into the graph, adding high-value shape over the skeleton. The `year` property is effectively a baked in pre-query that directs further exploration if the timeline is of interest for this major releases. The `cve_records` property lists all the CVEs for the month, another pre-query baked in. -As stated, there is a lot more useful detailed currency on offer. However, there is a rule that currency needs to be guaranteed consistent. Let's consider if the rule is obeyed. The important characteristic is that listed versions and links _within_ the resource are consistent by virtue of being _captured_ in the same file. The critical trick is with the links. The link origin is a fast moving resource while link target resources are immutable. That combination works. It's easy to be consistent with something immutable. It will either exist or not. In contrast, there would be a problem if there was a link between two mutable resources that expose the same currency. This is the problem that `releases-index.json` has. +As stated, there is a lot more useful detailed currency on offer. However, there is a rule that currency needs to be guaranteed consistent. Let's consider if the rule is obeyed. The important characteristic is that listed versions and links _within_ the resource are consistent by virtue of being _captured_ in the same file. + +The critical trick is with the links. In the case of the `release-month` link, the link origin is a fast moving resource (warm branch) while the link target is immutable. That combination works. It's easy to be consistent with something immutable. It will either exist or not. In contrast, there would be a problem if there was a link between two mutable resources that expose the same currency. This is the problem that `releases-index.json` has. Back to `manifest.json`. It contains extra data that tools in particular might find useful. The following example is the `manifest.json` file for .NET 9. @@ -441,6 +477,7 @@ Back to `manifest.json`. It contains extra data that tools in particular might f "title": ".NET 9.0 Manifest", "version": "9.0", "label": ".NET 9.0", + "target_framework": "net9.0", "release_type": "sts", "support_phase": "active", "supported": true, @@ -448,10 +485,12 @@ Back to `manifest.json`. It contains extra data that tools in particular might f "eol_date": "2026-11-10T00:00:00+00:00", "_links": { "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/manifest.json", - "path": "/9.0/manifest.json", - "title": ".NET 9.0 Manifest", - "type": "application/hal\u002Bjson" + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/manifest.json" + }, + "compatibility": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/compatibility.json", + "title": "Compatibility", + "type": "application/json" }, "compatibility-rendered": { "href": "https://learn.microsoft.com/dotnet/core/compatibility/9.0", @@ -465,7 +504,6 @@ Back to `manifest.json`. It contains extra data that tools in particular might f }, "os-packages-json": { "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/os-packages.json", - "path": "/9.0/os-packages.json", "title": "OS Packages", "type": "application/json" }, @@ -474,51 +512,41 @@ Back to `manifest.json`. It contains extra data that tools in particular might f "title": "Announcing .NET 9", "type": "text/html" }, - "releases-json": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/releases.json", - "path": "/9.0/releases.json", - "title": "Complete (large file) release information for all patch releases", - "type": "application/json" - }, "supported-os-json": { "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/supported-os.json", - "path": "/9.0/supported-os.json", "title": "Supported OSes", "type": "application/json" }, "supported-os-markdown": { "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/supported-os.md", - "path": "/9.0/supported-os.md", "title": "Supported OSes", "type": "application/markdown" }, "supported-os-markdown-rendered": { "href": "https://github.com/dotnet/core/blob/main/release-notes/9.0/supported-os.md", - "path": "/9.0/supported-os.md", "title": "Supported OSes (Rendered)", - "type": "application/markdown" + "type": "text/html" + }, + "target-frameworks": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/target-frameworks.json", + "title": "Target Frameworks", + "type": "application/json" }, "usage-markdown-rendered": { "href": "https://github.com/dotnet/core/blob/main/release-notes/9.0/README.md", - "path": "/9.0/README.md", "title": "Release Notes (Rendered)", - "type": "application/markdown" + "type": "text/html" }, "whats-new-rendered": { "href": "https://learn.microsoft.com/dotnet/core/whats-new/dotnet-9/overview", "title": "What\u0027s new in .NET 9", "type": "text/html" } - }, - "_metadata": { - "schema_version": "1.0", - "generated_on": "2025-12-05T23:00:01.7004871+00:00", - "generated_by": "VersionIndex" } } ``` -The relations are in alphabetical order, after `self`. +This is a dictionary of links with some useful metadata. The relations are in alphabetical order, after `self`. Some of the information in this file is sourced from a human-curated `_manifest.json`. This file is used by the graph generation tools, not the graph itself. It provides a path to seeding the graph with data not available elsewhere. @@ -530,8 +558,9 @@ Some of the information in this file is sourced from a human-curated `_manifest. "title": ".NET 9.0 Manifest", "version": "9.0", "label": ".NET 9.0", + "target_framework": "net9.0", "release_type": "sts", - "support_phase": "active", + "phase": "active", "ga_date": "2024-11-12T00:00:00Z", "eol_date": "2026-11-10T00:00:00Z", "_links": { @@ -557,10 +586,9 @@ Some of the information in this file is sourced from a human-curated `_manifest. } } } - ``` -This links are free form and can be anything. They follow the same scheme as the links used elsewhere in the graph. +These links are free form and can be anything. They follow the same scheme as the links used elsewhere in the graph. ### Patch Version Index @@ -568,10 +596,9 @@ The following example is a patch version index, for [9.0.10](https://github.com/ ```json { - "$schema": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/schemas/dotnet-patch-detail-index.json", + "$schema": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/schemas/v1/dotnet-patch-detail-index.json", "kind": "patch-version-index", "title": ".NET 9.0.10 Patch Index", - "description": "Patch information for .NET 9.0.10", "version": "9.0.10", "date": "2025-10-14T00:00:00\u002B00:00", "support_phase": "active", @@ -582,87 +609,67 @@ The following example is a patch version index, for [9.0.10](https://github.com/ "CVE-2025-55248", "CVE-2025-55315" ], - "sdk_patches": [ + "sdk_version": "9.0.306", + "sdk_feature_bands": [ "9.0.306", "9.0.111" ], "_links": { "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.10/index.json", - "path": "/9.0/9.0.10/index.json", - "title": "9.0.10 Patch Index", - "type": "application/hal\u002Bjson" + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.10/index.json" }, "prev": { "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.9/index.json", - "path": "/9.0/9.0.9/index.json", - "title": "9.0.9 Patch Index", - "type": "application/hal\u002Bjson" + "title": "Patch index" + }, + "prev-security": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.6/index.json", + "title": "Latest security patch" }, "latest-sdk": { "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/sdk/index.json", - "path": "/9.0/sdk/index.json", - "title": ".NET SDK 9.0 Release Information", - "type": "application/hal\u002Bjson" + "title": "SDK index" }, "release-major": { "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/index.json", - "path": "/9.0/index.json", - "title": ".NET 9.0 Patch Release Index", - "type": "application/hal\u002Bjson" + "title": "Major version index" }, "release-month": { "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/index.json", - "path": "/timeline/2025/10/index.json", - "title": "Release timeline index for 2025-10", - "type": "application/hal\u002Bjson" - }, - "release-year": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/index.json", - "path": "/timeline/2025/index.json", - "title": "Release timeline index for 2025", - "type": "application/hal\u002Bjson" + "title": "Release month index" }, "releases-index": { "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/index.json", - "path": "/index.json", - "title": ".NET Release Index", - "type": "application/hal\u002Bjson" + "title": "Release index" }, "cve-json": { "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/cve.json", - "path": "/timeline/2025/10/cve.json", - "title": "CVE Information", + "title": "CVE records (JSON)", "type": "application/json" }, "cve-markdown": { "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/cve.md", - "path": "/timeline/2025/10/cve.md", - "title": "CVE Information", + "title": "CVE records (JSON)", "type": "application/markdown" }, "cve-markdown-rendered": { "href": "https://github.com/dotnet/core/blob/main/release-notes/timeline/2025/10/cve.md", - "path": "/timeline/2025/10/cve.md", - "title": "CVE Information (Rendered)", + "title": "CVE records (JSON) (Rendered)", "type": "application/markdown" }, "release-json": { "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.10/release.json", - "path": "/9.0/9.0.10/release.json", "title": "9.0.10 Release Information", "type": "application/json" }, "release-notes-markdown": { "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.10/9.0.10.md", - "path": "/9.0/9.0.10/9.0.10.md", - "title": "9.0.10 Release Notes", + "title": "Release Notes", "type": "application/markdown" }, "release-notes-markdown-rendered": { "href": "https://github.com/dotnet/core/blob/main/release-notes/9.0/9.0.10/9.0.10.md", - "path": "/9.0/9.0.10/9.0.10.md", - "title": "9.0.10 Release Notes (Rendered)", + "title": "Release Notes (Rendered)", "type": "application/markdown" } }, @@ -670,7 +677,7 @@ The following example is a patch version index, for [9.0.10](https://github.com/ This content looks much the same as we saw earlier, except that much of the content we saw in the patch object is now exposed at index root. That's not coincidental, but a key aspect of the model. -The `prev` link relation provides another wormhole, this time to a less distant target. A `next` relation isn't provided because it would break the immutability goal. In addition, the combination of a `latest*` property and `prev` links satisfies many scenarios. There are lots of algoriths that [work best when counting backwards](https://www.benjoffe.com/fast-date-64). +The `prev` link relation provides another wormhole, this time to a less distant target. A `next` relation isn't provided because it would break the immutability goal. In addition, the combination of a `latest*` property and `prev` links satisfies many scenarios. The `latest-sdk` target provides access to `aka.ms` evergreen SDK links and other SDK-related information. The `release-month` and `cve-json` links are still there, but a bit further down the dictionary definition as to what's copied above. From 38865cc5650205f0d2f55e112eb13019f87f09cc Mon Sep 17 00:00:00 2001 From: Rich Lander Date: Fri, 19 Dec 2025 00:13:40 -0800 Subject: [PATCH 07/15] Update spec --- .../metrics.md | 0 .../release-notes-graph-llms.md | 0 .../release-notes-graph.md | 1416 ++++++++++++++ .../releases-json-tokens.png | Bin .../release-notes-graph/testing/prompts.md | 291 +++ .../2025/release-notes-graph/testing/spec.md | 443 +++++ .../release-notes-information-graph.md | 1634 ----------------- 7 files changed, 2150 insertions(+), 1634 deletions(-) rename accepted/2025/{release-notes-information-graph => release-notes-graph}/metrics.md (100%) create mode 100644 accepted/2025/release-notes-graph/release-notes-graph-llms.md create mode 100644 accepted/2025/release-notes-graph/release-notes-graph.md rename accepted/2025/{release-notes-information-graph => release-notes-graph}/releases-json-tokens.png (100%) create mode 100644 accepted/2025/release-notes-graph/testing/prompts.md create mode 100644 accepted/2025/release-notes-graph/testing/spec.md delete mode 100644 accepted/2025/release-notes-information-graph/release-notes-information-graph.md diff --git a/accepted/2025/release-notes-information-graph/metrics.md b/accepted/2025/release-notes-graph/metrics.md similarity index 100% rename from accepted/2025/release-notes-information-graph/metrics.md rename to accepted/2025/release-notes-graph/metrics.md diff --git a/accepted/2025/release-notes-graph/release-notes-graph-llms.md b/accepted/2025/release-notes-graph/release-notes-graph-llms.md new file mode 100644 index 000000000..e69de29bb diff --git a/accepted/2025/release-notes-graph/release-notes-graph.md b/accepted/2025/release-notes-graph/release-notes-graph.md new file mode 100644 index 000000000..07bcd2135 --- /dev/null +++ b/accepted/2025/release-notes-graph/release-notes-graph.md @@ -0,0 +1,1416 @@ +# Exposing Release Notes as an Information graph + +The .NET project has published [release notes in JSON and markdown](https://github.com/dotnet/core/tree/main/release-notes) for many years. The investment in quality release notes has been based on the virtuous cloud-era idea that many deployment and compliance workflows require detailed structured data to safely operate at scale. For the most part, a highly clustered set of consumers (like GitHub Actions and vulnerability scanners) have adapted _their tools_ to _our formats_ to offer higher-level value to their users. That's all good. The LLM era is strikingly different where a much smaller set of information systems (LLM model companies) _consume and expose diverse data in standard ways_ according to _their (shifting) paradigms_ to a much larger set of users. The task at hand is to modernize release notes to make them more efficient to consume generally and to adapt them for LLM consumption. + +Overall goals for release notes consumption: + +- Graph schema encodes graph update frequency +- Satisfy reasonable expectations of performance (no 1MB JSON files), reliability, and consistency +- Enable aestheticly-pleasing queries that are terse, ergonomic, and effective, both for their own goals and as a proxy for LLM consumption. +- Support queries with multiple key styles, temporal and version-based (runtime and SDK versions) queries. +- Expose queryable data beyond version numbers, such as CVE disclosures, breaking changes, and download links. +- Use the same data to generate most release note markdown files, like [releases.md](https://github.com/dotnet/core/blob/main/releases.md), and CVE announcements like [CVE-2025-55248](https://github.com/dotnet/announcements/issues/372), guaranteeing ensuring consistency from a single source of truth. +- Use this project as a real-world information graph pilot to inform other efforts that expose information to modern information consumers. + +## Scenario + +Release notes are mechanism, not scenario. It likely difficult for users to keep up with and act on the constant stream of .NET updates, typically one or two times a month. Users often have more than one .NET major version deployed, further complicating this puzzle. Many users rely on update orchestrators like APT, Yum, and Visual Studio, however, it is unlikely that such tools cover all the end-points that users care about in a uniform way. It is important that users can reliably make good, straightforward, and timely decisions about their entire infrastructure, orchestrated across a variety of deployment tools. This is a key scenario that release notes serve. + +Obvious questions release notes should answer: + +- What has changed, since last month, since the last _.NET_ update, or since the last _user_ update. +- How many patches back is this machine? +- How/where can new builds be acquired +- Is a recent update more critical to deploy than "staying current"? +- How long until a given major release is EOL or has been EOL? +- What are known upgrade challenges? + +CIOs, CTOs, and others are accountable for maintaining efficient and secure continuity for a set of endpoints, including end-user desktops and cloud servers. They are unlikely to read long markdown release notes or perform DIY `curl` + `jq` hacking with structured data. They will increasingly expect to be able to get answers to arbitrarily detailed compliance and deployment questions using chat assistants like Copilot. They may ask Claude to compare treatment of an industry-wide CVE like [CVE-2023-44487](https://nvd.nist.gov/vuln/detail/cve-2023-44487) across multiple application stacks in their portfolio. This already works reasonably well, but fails when prompts demand greater levels of detail and with the expectation that the source data comes from authoritative sources. It is very common to see assistants glean insight from a semi-arbitrary set of web pages with matching content. This is particularly problematic for day-of prompts (same day as a security release). + +Some users have told us that they enable Slack notifications for [dotnet/announcements](https://github.com/dotnet/announcements/issues), which is an existing "release notes beacon". That's great and intended. What if we could take that to a new level, thinking of release notes as queryable data used by notification systems and LLMs? There is a lesson here. Users (virtuously) complain when we [forget to lock issues](https://github.com/dotnet/announcements/issues/107#issuecomment-482166428). They value high signal to noise. Fortunately, we no longer forget for announcements, but we have not achieved this same disciplined model with GitHub release notes commits (as will be covered later). It should just just as safe and reliable to use release notes updates as a beacon as dotnet/announcements. + +LLMs are a different kind of "user" than we've previously tried to enable. LLMs are _much more_ fickle relative to purpose-built tools. They are more likely to give up if release notes are not to their liking, instead relying on model knowledge or comfortable web search with its equal share of benefits and challenges. Regular testing (by the spec writer) of release notes with chat assistants has demonstrated that LLMs are typically only satiated by a "Goldilocks meal". Obscure formats, large documents, and complicated workflows don't perform well (or outright fail). LLMs will happily jump to `releases-index.json` and choke on the 1MB+ [`releases.json`](https://github.com/dotnet/core/blob/main/release-notes/releases-index.json) files we maintain if prompts are unable to keep their attention. + +![.NET 6.0 releases.json file as tokens](./releases-json-tokens.png) + +This image shows that the worst case for the `releases.json` format is 600k tokens using the [OpenAI Tokenzier](https://platform.openai.com/tokenizer). It is an understatement to say that a file of that size doesn't work well with LLMs. Context: memory budgets tend to max out at 200k tokens. Large JSON files can be made to work in some scenarios, but not in the general case. + +A strong belief is that workflows that are bad for LLMS are typically not _uniquely_ bad for LLMs but are challenging for other consumers. It is easy to guess that most readers of `releases-index.json` would be better-served by referenced JSON significantly less than 1MB+. This means that we need start from scratch with structured release notes. + +In the early revisions of this project, the design followed our existing schema playbook, modeling parent/child relationships, linking to more detailed sources of information, and describing information domains in custom schemas. That then progressed into wanting to expose summaries of high-value information from leaf nodes into the trunk. This approach didn't work well since the design was lacking a broader information architecure. A colleague noted that the design was [Hypermedia as the engine of application state (HATEOAS)](https://en.wikipedia.org/wiki/HATEOAS)-esque but not using one of the standard formats. The benefits of using standard formats is that they are free to use, have gone through extensive design review, can be navigated with standard patterns and tools, and (most importantly) LLMs already understand their vocabulary and access patterns. A new format will by definition not have those characteristics. + +This proposal leans heavily on hypermedia, specifically [HAL+JSON](https://datatracker.ietf.org/doc/html/draft-kelly-json-hal). Hypermedia is very common, with [OpenAPI](https://www.openapis.org/what-is-openapi) and [JSON:API](https://jsonapi.org/) likely being the most common. LLMs are quite comfortable with these standards. The proposed use of HAL is a bit novel (not intended as a positive descriptor). It is inspired by [llms.txt](https://llmstxt.org/) as an emerging standard and the idea that hypermedia is the most natural next step to express complex diverse data and relationships. It's also expected (in fact, pre-ordained) that older standards will perform better than newer ones due to higher density (or presence at all) in the LLM training data. + +## Hypermedia graph design + +This project has adopted the idea that a wide and deep information graph can expose significant information within the graph that satisfies user queries without loading other files. The graph doesn't need to be skeletal. It can have some shape on it. In fact our existing graph with [`release-index.json`](https://github.com/dotnet/core/blob/main/release-notes/releases-index.json) already does this, but without the benefit of a standard format or architectural principles. + +The design intent is that a graph should be skeletal at its roots for performance and to avoid punishing queries that do not benefit from the curated shape. The deeper the node is in the graph, the more shape (or weight) it should take on since the data curation is much more likely to hit the mark. + +Hypermedia formats have a long history of satisfying this methodology, long pre-dating, and actually inspiring the World Wide Web and its Hypertext Markup Language (HTML). This project uses [HAL+JSON](https://en.wikipedia.org/wiki/Hypertext_Application_Language) as the "graph format". HAL is a sort of a "hypermedia in a nutshell" schema, initally drafted in 2012. You can develop a basic understanding of HAL in about two minutes because it has a very limited syntax. + +For the most part, HAL defines just two properties: + +- `_links` -- links to resources. +- `_embedded` -- embedded resources, which will often include `_links`. + +It seems like this is hardly enough to support the ambitious design approach that has been described. It turns out that the design is more clever than first blush would suggest. + +There is an excellent Australian movie that comes to mind, [The Castle](https://www.imdb.com/title/tt0118826). + +> Judge: “What section of the constitution has been breached?” +> Dennis Denuto: "It’s the constitution. It’s Mabo. It’s justice. It’s law. It’s the vibe ... no, that’s it, it’s the vibe. I rest my case" + +HAL is much the same. It defines an overall approach that a schema designer can hang off of these two seemingly understated properties. You just have to follow the vibe of it. + +Here is a simple example from the HAL spec: + +```json +{ + "_links": { + "self": { "href": "/orders/523" }, + "warehouse": { "href": "/warehouse/56" }, + "invoice": { "href": "/invoices/873" } + }, + "currency": "USD", + "status": "shipped", + "total": 10.20 +} +``` + +The `_links` property is a dictionary of link objects with specific named relations. Most link dictionaries start with the standard `self` relation. The `self` relation describes the canonical URL of the given resource. The `warehouse` and `invoice` relations are examples of domain-specific relations. Together, they establish a navigation protocol for this resource domain. One can also imagine `next`, `previous`, `buy-again`, or `i-am-feeling-lucky` as relations for e-commerce. Domain-specific HAL readers will understand these relations and know how or when to act on them. + +The `currency`, `status`, and `total` properties provide additional domain-specific resource metadata. The package should arrive at your door soon! + +The following example is similar, with the addition of the `_embedded` property. + +```json +{ + "_links": { + "self": { "href": "/orders" }, + "next": { "href": "/orders?page=2" }, + "find": { "href": "/orders{?id}", "templated": true } + }, + "_embedded": { + "orders": [{ + "_links": { + "self": { "href": "/orders/123" }, + "basket": { "href": "/baskets/98712" }, + "customer": { "href": "/customers/7809" } + }, + "total": 30.00, + "currency": "USD", + "status": "shipped", + },{ + "_links": { + "self": { "href": "/orders/124" }, + "basket": { "href": "/baskets/97213" }, + "customer": { "href": "/customers/12369" } + }, + "total": 20.00, + "currency": "USD", + "status": "processing" + }] + }, + "currentlyProcessing": 14, + "shippedToday": 20 +} +``` + +The `_embedded` property contains order resources. This is the resource payload. Each of those order items have `self` and other related link relations referencing other resources. As stated earlier, the `self` relation references the canonical copy of the resource. Embedded resources may be a full or partial copy of the resource. Again, domain-specific reader will understand this schema and know how to process it. + +This design aspect is the true strength of HAL, of projecting partial views of resources to their reference. It's the mechanism that enables the overall approach of a skeletal root with weighted bottom nodes. It's also what enables these two seemingly anemic properties to provide so much modeling value. + +The `currentlyProcessing` and `shippedToday` properties provide additional information about ongoing operations. + +Hat tip to [Mike Kelly](https://github.com/mikekelly) for sharing such a simple yet highly effective hypermedia design with the world. + +We can now look at how the same vibe can be applied to .NET release notes. + +## Releases-index schema + +It's important to take a quick look at the [`releases-index.json`](https://raw.githubusercontent.com/dotnet/core/refs/heads/main/release-notes/releases-index.json) schema, to understand our current baseline. It is the schema that this new design is replacing. + +```json +{ + "$schema": "https://json.schemastore.org/dotnet-releases-index.json", + "releases-index": [ + { + "channel-version": "10.0", + "latest-release": "10.0.1", + "latest-release-date": "2025-12-09", + "security": false, + "latest-runtime": "10.0.1", + "latest-sdk": "10.0.101", + "product": ".NET", + "support-phase": "active", + "eol-date": "2028-11-14", + "release-type": "lts", + "releases.json": "https://builds.dotnet.microsoft.com/dotnet/release-metadata/10.0/releases.json", + "supported-os.json": "https://builds.dotnet.microsoft.com/dotnet/release-metadata/10.0/supported-os.json" + }, + { + "channel-version": "9.0", + "latest-release": "9.0.11", + "latest-release-date": "2025-11-19", + "security": false, + "latest-runtime": "9.0.11", + "latest-sdk": "9.0.308", + "product": ".NET", + "support-phase": "active", + "eol-date": "2026-11-10", + "release-type": "sts", + "releases.json": "https://builds.dotnet.microsoft.com/dotnet/release-metadata/9.0/releases.json", + "supported-os.json": "https://builds.dotnet.microsoft.com/dotnet/release-metadata/9.0/supported-os.json" + }, +``` + +Note: `releases-index.json` will continue to be updated. However, it will be deprecated as the preferred solution. + +The basis of this design is that `releases-index.json` is no longer sufficient for our needs. Keep that assumption in mind while considering the rest of the spec. + +## Release Notes Graph + +Release notes naturally describe two information dimensions: time and product version. + +- Within time, we have years, months, and (ship) days. +- Within version, we have major and patch version. We also have runtime vs SDK version. + +These dimensional characteristics form the load-bearing structure of the graph to which everything else is attached. The new graph exposes both timeline and version indices. We've previously only had a version index. + +The following table summarizes the overall shape of the graph, starting at `dotnet/core:release-notes/index.json`. + +| File | Type | Size | Frequency | Updates When... | +| --- | --- | --- | --- | --- | +| `index.json` (root) | Release version index | 4.6 KB | 1-2x/year | New major version, support bool changes | +| `timeline/index.json` | Release timeline index | 4.4 KB | 2x/year | New year starts, new major version | +| `timeline/{year}/index.json` | Year index | 5.5 KB | 12x/year | New month with activity, phase changes | +| `timeline/{year}/{month}/index.json` | Month index | 9.7 KB | 0 | Never (immutable after creation) | +| `{version}/index.json` | Major version index | 20 KB | 12x/year | New patch release | +| `{version}/{patch}/index.json` | Patch version index | 7.7 KB | 0 | Never (immutable after creation) | +| `llms.json` | AI-optimized index | 4.7 KB | 12x/year | New patch release, support status changes | + +**Summary:** Cold roots, warm branches, immutable leaves. + +Notes: + +- 2025 was used for `{year}`, 10 for `{month`}, 8.0 for `{version}`, and 8.0.21 for `{patch}` +- The bottom of the graph has the largest variance over time. `8.0.21/index.json` is `11.5` KB while `8.0.22/index.json` is `4.8` KB, significantly smaller because it was a non-security release. Similarly, `2025/10/index.json` is `9.7` KB while `2025/11/index.json` is `3.5` KB, significantly smaller because there was no security release that month. `2025/12/index.json` was even smaller because there was only a .NET 10 patch, also non-security. Security releases were chosen to demonstrate more realistic sizes. +- `llms.json` will be covered more later. + +We can contrast this approach with the existing release graph. + +| File | Type | Size | Frequency | Updates When... | +| --- | --- | --- | --- | --- | +| `releases-index.json` (root) | Release version index | 6.3 KB | 18x/year | Every patch release | +| `{version}/releases.json` | Major Version Index | 1.3 MB | 18x/year | Every patch release | + +Notes: + +- 8.0 was used for `{version}`. 6.0 `releases.json` is 1.6 MB. +- Frequency is 18x a year to allow for SDK-only releases that occur after Patch Tuesday. + +It is straightforward to see that the new graph design enables access to far more information before hitting a data wall. In fact, in experiments, LLMs reliably hit an HTTP `409` error code as soon as they try to read `release.json`. It hits the LLM context limit, or similar. + +The last 12 months of commit data (Nov 2024-Nov 2025) demonstrates a higher than expected update rate. + +| File | Commits | Notes | +| ---- | ------- | ----- | +| `releases-index.json` | 29 | Root index (all versions) | +| `10.0/releases.json` | 22 | Includes previews/RCs and SDK-only releases | +| `9.0/releases.json` | 24 | Includes SDK-only releases, fixes, URL rewrites | +| `8.0/releases.json` | 1Please8 | Includes SDK-only releases, fixes, URL rewrites | + +**Summary:** Hot everything. + +Conservatively, the existing commit counts are not good. The `releases-index.json` file is a mission-critical live-site resource. 29 updates is > 2x/month! + +### Graph consistency + +The graph has one rule: + +> Every resource in the graph needs to be guaranteed consistent with every other part of the graph. + +The unstated problem is CDN caching. Assume that the entire graph is consistent when uploaded to an origin server. A CDN server is guaranteed by construction to serve both old and new copies of the graph -- for existing files that have been updated -- leading to potential inconsistencies. The graph construction needs to be resilient to that. + +Related examples: + +- +- + +Today, we publish `releases-index.json` as the root of our release notes graph. Some users read this JSON file to learn the latest patch version numbers, while others navigate deeper into the graph. Both are legitimate patterns. However, we've found that our approach has fundamental flaws. + +Problems: + +- Exposing patch versions in multiple files that need to agree is incompatible with using a Content Delivery Network (CDN) that employs standard caching (expiration / TTL). +- The `releases-index.json` file is a critical live site resource driving 1000s of GBs of downloads a month, yet we update it multiple times a month, by virue of the data it exposes. + +It's hard to understate the impact of schema design on file update frequency. Files that expose properties with patch versions (scalars or links) inherently require updates on the patch schedule. If so, `git status` will put that file on a stage. Otherwise, `git status` will not give that file a second look. + +Solution: + +- Fast changing currency (like patch version numbers) are exposed in (at most) a single resource in the graph, and never at the root. +- The root index file is updated once or twice a year (to add the presence of a new major release and change support status; releases come in and go out, typically not on the same day). + +The point about the root index isn't a _solution_ but an _implication_ of the first point. If the root index isn't allowed to contain fast-moving currency, in part because it is present in another resource, then it is stripped of its reason to change. + +There are videos on YouTube with these [crazy gear reductions](https://www.youtube.com/watch?v=QwXK4e4uqXY). You can watch them for a long time! Keen observers will realize our graph will look nothing like that. It's a metaphor. We can model years and months and major and patch versions as spinning gears with a differing number of teeth and revolution times. The remaining design question is whether we like the rate that our gears spin. + +A stellar orbit analogy would have worked just as well. The root `index.json` is the star. It, too, is subject to an orbit. It's just a [lot slower](https://en.wikipedia.org/wiki/Galactic_year). + +Release notes graph indexes are updated (gear reduce) per the following: + +- Timeline index (list of years): one to two updates per year +- Year index (list of months): one update per month +- Month index (list of patches across versions): No updates (immutable) + +The same progression for versions: + +- Releases index (list of of major versions): one to two updates per year +- Major version index (list of patches): one update per month +- Patch version index (details about a patch): No updates (immutable) + +It's the middle section changing constantly, but the roots and the leaves are either immutable or close enough to it. + +Note: Some annoying details, like SDK-only releases, have been ignored. The intent is to reason about rough order of magnitude and the fundamental pressure being applied to each layer. + +A key question about this scheme is when we should a add new major releases, like `12.0`. The most obvious answer to add new releases at Preview 1. They exist! The other end of the spectrum would be at GA. From a mission-critical standpoint, GA sounds better. Indeed, the root `index.json` file is intended as a mission-critical resource. So, we should add an entry for `12.0` when it is needed and can be acted on for production use. Case closed! Unintuitively, this approach is likely a priority inversion. + +We should add vNext releases at Preview 1 for the following reasons: + +- vNext is available (in preview form), we so we should advertise it. +- Special once-in-a-release tasks are more likely to fail when done on the very busy and critical GA day. +- Adding vNext early enables consumers to cache aggressively. + +The last point clinches it. The intent is that root `index.json` can be cached aggressively. Adding vNext to `index.json` with Preview 1 is perfectly aligned with that. Adding vNext at GA is not. This practice supports caching at weeks-long scale (or longer). + +### Graph wormholes + +There are many design paradigms and tradeoffs one can consider with a graph like this. A major focus is the "skeletal root" vs "weighted bottom" design point, discussed earlier. This approach forces significant fetches and inefficiency to get to _any_ useful data, unlike `releases-index.json`. To mitigate that, the graph includes helpful workflow-specific wormhole links. As the name suggests, these links enable jumping from one part of the graph to another, intended to create a kind of ergonomics for expected queries. The wormholes are somewhat emergent, somewhat obvious to expose due to otherwise challenging nature of the design. + +The wormholes links take on a different character at each layer of the graph. + +Cold roots: + +- `latest` and `latest-lts` -- enables jumping to the matching major version index. +- `latest-year` -- enables jumping to the latest year index. + +Warm branches: + +- `latest` and `latest-security` -- enables jumping to the matching patch version index. +- `latest-month` and `latest-security-month` -- enables jumping to the latest matching month. + +Immutable leaves: + +- `prev` and `prev-security` -- enables jumping to an earlier matching patch version or month index. + +Notes: + +- The wormhole links cannot force an update schedule beyond what the file would naturally allow. For the cold roots, a workhole like `latest-lts` is the best we can do. +- There are other such wormhole links. These are the the primary ones, which also best demonstrate the idea. + +The wormhole links are what make the graph a graph and not just two related trees providing alternative views of the same data. They also enable efficient navigation. + +For some scenarios, it can be efficient to jump to `latest-security-month` and then backwards in time via `prev-security`. `prev` and `prev-security` jump across year boundaries, reducing the logic required to use the scheme. + +There is no `next` or `next-security`. `prev` and `prev-security` link immutable leaves, establishing a linked-list based on past knowledge. It's not possible to provide `next` links given the immutability constraint. + +There is no `latest-sts` link because it's not really useful. `latest` covers the same need sufficiently. + +Testing has demonstrated that these wormhole links are one of the defining features of the graph. + +## Version Index Modeling + +The version index has three layers: releases, major version, patch version. Most nodes in the graph are named `index.json`. The examples should look similar to the HAL spec documents shared earlier. + +### Releases index + +The root `index.json` file represents all .NET versions. It is a stripped-down version of the existing `releases-index.json`. + +Source: + +```json +{ + "$schema": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/schemas/v1/dotnet-release-version-index.json", + "kind": "releases-index", + "title": ".NET Release Index", + "latest": "10.0", + "latest_lts": "10.0", + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/index.json" + }, + "latest": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/index.json", + "title": "Latest release - .NET 10.0" + }, + "latest-lts": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/index.json", + "title": "Latest LTS release - .NET 10.0" + }, + "timeline-index": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/index.json", + "title": ".NET Release Timeline Index" + } + }, +``` + +Key points: + +- Schema reference is included +- All links are raw content (will eventually transition to `builds.dotnet.microsoft.com). +- `kind` and `title` describe the resource +- `latest` and `latest_lts` describe high-level resource metadata, often useful currency that helps contextualize the rest of the resource without the need to parse/split strings. For example, the `latest_lts` scalar describes the target of the `latest-lts` link relation. Notably, there are no three-part (patch) versions, like `latest_lts_patch`. +- `timeline-index` provides a wormhole link to another part of the graph, which provides a temporal view of .NET releases. +- Core schema syntax, like `latest_lts`, uses snake-case-lower for query ergonomics (using `jq` as the proxy for that). +- Link relations. like `latest-lts`, use kebab-case-lower since they can be names or brands. This follows the approach used by [cve-schema](https://github.com/dotnet/designs/blob/main/accepted/2025/cve-schema/cve_schema.md#brand-names-vs-schema-fields-mixed-naming-strategy). + +The `_embedded` section has one child, `releases`: + +```json + "_embedded": { + "releases": [ + { + "version": "10.0", + "release_type": "lts", + "supported": true, + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/index.json" + } + } + }, + { + "version": "9.0", + "release_type": "sts", + "supported": true, + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/index.json" + } + } + }, +``` + +This is where we see the design diverge significantly from `releases-index.json`. There are no patch versions, no statement about security releases. It's the most minimal data to determine the release type, if it is supported, and how to access the canonical resource that exposes richer information. This approach removes the need to update the root index monthly. It's fine for tools to regenerate this file monthly. `git` should not see any diffs. + +### Major version index + +One layer lower, we have the major version index, using .NET 9 as the example. + +Source: + +```json +{ + "$schema": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/schemas/v1/dotnet-release-version-index.json", + "kind": "major-version-index", + "title": ".NET Major Release Index - 9.0", + "target_framework": "net9.0", + "latest": "9.0.11", + "latest_security": "9.0.10", + "release_type": "sts", + "support_phase": "active", + "supported": true, + "ga_date": "2024-11-12T00:00:00\u002B00:00", + "eol_date": "2026-11-10T00:00:00\u002B00:00", + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/index.json" + }, + "downloads": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/downloads/index.json", + "title": "Downloads - .NET 9.0" + }, + "latest": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.11/index.json", + "title": "Latest patch - 9.0.11" + }, + "latest-sdk": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/sdk/index.json", + "title": "Latest SDK - .NET 9.0" + }, + "latest-security": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.10/index.json", + "title": "Latest security patch - 9.0.10" + }, + "manifest": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/manifest.json", + "title": "Manifest - .NET 9.0" + }, + "releases-index": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/index.json", + "title": ".NET Release Index" + } + }, +``` + +This index includes much more useful and detailed information, both metadata/currency and patch-version links. It starts to answer the question of "what should I care about _now_?". + +Much of the form is similar to the root index. Instead of `latest_lts`, there is `latest_security`. The `manifest` relation stores important but lower-tier content about a given major release. That will be covered shortly. + +The `_embedded` property (solely) includes `releases`. + +```json + "_embedded": { + "patches": [ + { + "version": "9.0.11", + "release": "9.0", + "date": "2025-11-19T00:00:00\u002B00:00", + "year": "2025", + "month": "11", + "security": false, + "cve_count": 0, + "support_phase": "active", + "sdk_version": "9.0.308", + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.11/index.json" + }, + "release-month": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/11/index.json" + } + } + }, + { + "version": "9.0.10", + "release": "9.0", + "date": "2025-10-14T00:00:00\u002B00:00", + "year": "2025", + "month": "10", + "security": true, + "cve_count": 3, + "cve_records": [ + "CVE-2025-55247", + "CVE-2025-55248", + "CVE-2025-55315" + ], + "support_phase": "active", + "sdk_version": "9.0.306", + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.10/index.json" + }, + "cve-json": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/cve.json" + }, + "release-month": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/index.json" + } + } + }, +``` + +The `patches` object contains detailed information that can drive basic deployment and compliance workflows. The first two link relations, `self` and `release-month` are HAL links while `cve-json` is a plain JSON link. Non-HAL links end in the given format, like `json` or `markdown` or `markdown-rendered`. The links are raw text by default, with `-rendered` HTML content being useful for content targeted for human consumption, for example as links in generated release notes. + +The `release-month` relation is another wormhole link. It provides direct access to a high-relevance and graph-distant resource that would otherwise require awkward indirections, multiple network hops, and wasted bytes/tokens to acquire. These wormhole links massively improve query ergonomics for sophisticated queries. + +There is a link to a `cve.json` file. Our [CVE schema](https://github.com/dotnet/designs/blob/main/accepted/2025/cve-schema/cve_schema.md) is a custom schema with no HAL vocabulary. It's an exit node of the graph. The point is that we're free to describe complex domains, like CVE disclosures, using a clean-slate design methodology. One can also see that some of the `cve.json` information has been projected into the graph, adding high-value shape over the skeleton. + +As stated, there is a lot more useful detailed currency on offer. However (as mentioned earlier), there is a rule that currency needs to be guaranteed consistent. Let's consider if the rule is obeyed. The important characteristic is that listed versions and links _within_ the resource are consistent by virtue of being _captured_ in the same file. + +The critical trick is with the links. In the case of the `release-month` link, the link origin is a fast moving resource (warm branch) while the link target is immutable. That combination works. It's easy to be consistent with something immutable. It will either exist or not, only in the expected form. In contrast, there would be a problem if there was a link between two mutable resources that expose the same currency. This is the problem that `releases-index.json` has. This is not unlike data races due to global mutable state in programming language discussions. + +Back to `manifest.json`. It contains extra data that are helpful in second-tier scenarios. + +Source: + +```json +{ + "kind": "manifest", + "title": ".NET 9.0 Manifest", + "version": "9.0", + "label": ".NET 9.0", + "target_framework": "net9.0", + "release_type": "sts", + "support_phase": "active", + "supported": true, + "ga_date": "2024-11-12T00:00:00+00:00", + "eol_date": "2026-11-10T00:00:00+00:00", + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/manifest.json" + }, + "compatibility": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/compatibility.json", + "title": "Compatibility", + "type": "application/json" + }, + "compatibility-rendered": { + "href": "https://learn.microsoft.com/dotnet/core/compatibility/9.0", + "title": "Breaking changes in .NET 9", + "type": "text/html" + }, + "downloads-rendered": { + "href": "https://dotnet.microsoft.com/download/dotnet/9.0", + "title": ".NET 9 Downloads", + "type": "text/html" + }, + "os-packages-json": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/os-packages.json", + "title": "OS Packages", + "type": "application/json" + }, + "release-blog-rendered": { + "href": "https://devblogs.microsoft.com/dotnet/announcing-dotnet-9/", + "title": "Announcing .NET 9", + "type": "text/html" + }, + "supported-os-json": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/supported-os.json", + "title": "Supported OSes", + "type": "application/json" + }, + "supported-os-markdown": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/supported-os.md", + "title": "Supported OSes", + "type": "application/markdown" + }, + "supported-os-markdown-rendered": { + "href": "https://github.com/dotnet/core/blob/main/release-notes/9.0/supported-os.md", + "title": "Supported OSes (Rendered)", + "type": "text/html" + }, + "target-frameworks": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/target-frameworks.json", + "title": "Target Frameworks", + "type": "application/json" + }, + "usage-markdown-rendered": { + "href": "https://github.com/dotnet/core/blob/main/release-notes/9.0/README.md", + "title": "Release Notes (Rendered)", + "type": "text/html" + }, + "whats-new": { + "href": "https://raw.githubusercontent.com/dotnet/docs/main/docs/core/whats-new/dotnet-9/overview.md", + "title": "What\u0027s new in .NET 9", + "type": "application/markdown" + }, + "whats-new-libraries": { + "href": "https://raw.githubusercontent.com/dotnet/docs/main/docs/core/whats-new/dotnet-9/libraries.md", + "title": "What\u0027s new in .NET libraries for .NET 9", + "type": "application/markdown" + }, + "whats-new-rendered": { + "href": "https://learn.microsoft.com/dotnet/core/whats-new/dotnet-9/overview", + "title": "What\u0027s new in .NET 9", + "type": "text/html" + }, + "whats-new-runtime": { + "href": "https://raw.githubusercontent.com/dotnet/docs/main/docs/core/whats-new/dotnet-9/runtime.md", + "title": "What\u0027s new in the .NET 9 runtime", + "type": "application/markdown" + }, + "whats-new-sdk": { + "href": "https://raw.githubusercontent.com/dotnet/docs/main/docs/core/whats-new/dotnet-9/sdk.md", + "title": "What\u0027s new in the SDK and tooling for .NET 9", + "type": "application/markdown" + } + } +} +``` + +This is a dictionary of links with some useful metadata. The relations are in alphabetical order, after `self`. + +Some of the information in this file is sourced from a human-curated `_manifest.json`. This file is used by the graph generation tools, not the graph itself. It provides a path to seeding the graph with data not available elsewhere. + +It's somewhat surprising, but LLMs can efficiently find a lot of this information, even as tucked away as it is. + +.NET 9 `_manifest.json`: + +Source: + +```json +{ + "kind": "manifest", + "title": ".NET 9.0 Manifest", + "version": "9.0", + "label": ".NET 9.0", + "target_framework": "net9.0", + "release_type": "sts", + "phase": "active", + "ga_date": "2024-11-12T00:00:00Z", + "eol_date": "2026-11-10T00:00:00Z", + "_links": { + "downloads-rendered": { + "href": "https://dotnet.microsoft.com/download/dotnet/9.0", + "title": ".NET 9 Downloads", + "type": "text/html" + }, + "whats-new-rendered": { + "href": "https://learn.microsoft.com/dotnet/core/whats-new/dotnet-9/overview", + "title": "What's new in .NET 9", + "type": "text/html" + }, + "whats-new": { + "href": "https://raw.githubusercontent.com/dotnet/docs/main/docs/core/whats-new/dotnet-9/overview.md", + "title": "What's new in .NET 9", + "type": "application/markdown" + }, + "whats-new-runtime": { + "href": "https://raw.githubusercontent.com/dotnet/docs/main/docs/core/whats-new/dotnet-9/runtime.md", + "title": "What's new in the .NET 9 runtime", + "type": "application/markdown" + }, + "whats-new-libraries": { + "href": "https://raw.githubusercontent.com/dotnet/docs/main/docs/core/whats-new/dotnet-9/libraries.md", + "title": "What's new in .NET libraries for .NET 9", + "type": "application/markdown" + }, + "whats-new-sdk": { + "href": "https://raw.githubusercontent.com/dotnet/docs/main/docs/core/whats-new/dotnet-9/sdk.md", + "title": "What's new in the SDK and tooling for .NET 9", + "type": "application/markdown" + }, + "compatibility-rendered": { + "href": "https://learn.microsoft.com/dotnet/core/compatibility/9.0", + "title": "Breaking changes in .NET 9", + "type": "text/html" + }, + "release-blog-rendered": { + "href": "https://devblogs.microsoft.com/dotnet/announcing-dotnet-9/", + "title": "Announcing .NET 9", + "type": "text/html" + } + } +} +``` + +These links are free form and can be anything. They follow the same scheme as the links used elsewhere in the graph. + +### Patch Version Index + +Last, we have the patch version index. `9.0.10` is used, as the most recent security release (at the time of writing). + +Source: + +```json +{ + "$schema": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/schemas/v1/dotnet-patch-detail-index.json", + "kind": "patch-version-index", + "title": ".NET Patch Release Index - 9.0.10", + "version": "9.0.10", + "date": "2025-10-14T00:00:00\u002B00:00", + "support_phase": "active", + "security": true, + "cve_count": 3, + "cve_records": [ + "CVE-2025-55247", + "CVE-2025-55248", + "CVE-2025-55315" + ], + "sdk_version": "9.0.306", + "sdk_feature_bands": [ + "9.0.306", + "9.0.111" + ], + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/9.0.10/index.json" + }, + "prev": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/9.0.9/index.json", + "title": "Previous patch release - 9.0.9" + }, + "prev-security": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/9.0.6/index.json", + "title": "Previous security patch release - 9.0.6" + }, + "latest-sdk": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/sdk/index.json", + "title": "Latest SDK - .NET 9.0" + }, + "manifest": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/9.0.10/manifest.json", + "title": "Manifest - .NET 9.0" + }, + "release-major": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/index.json", + "title": ".NET Major Release Index - 9.0" + }, + "release-month": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/10/index.json", + "title": ".NET Month Timeline Index - October 2025" + }, + "releases-index": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json", + "title": ".NET Release Index" + }, + "cve-json": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/10/cve.json", + "title": "CVE records - October 2025", + "type": "application/json" + } + }, +``` + +This content looks much the same as we saw earlier, except that much of the content we saw in the patch object is now exposed at index root. That's a key aspect of the model. There is a `manifest` for patch releases as well, to expose lower-tier content. + +The `prev` and `prev-security` link relations provide similar wormhole functionality, this time to less distant targets. A `next` relation isn't provided because it would break the immutability goal. In addition, the combination of a `latest*` properties (in the major version index) and `prev*` links satisfies many scenarios. + +The `latest-sdk` target provides access to `aka.ms` evergreen SDK links and other SDK-related information. + +The `_embedded` property contains three children: `sdk`, `sdk_feature_bands`, and `disclosures`. + +```json + "_embedded": { + "sdk": { + "version": "9.0.306", + "band": "9.0.3xx", + "date": "2025-10-14T00:00:00\u002B00:00", + "label": ".NET SDK 9.0.3xx", + "support_phase": "active", + "_links": { + "downloads": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/downloads/sdk-9.0.3xx.json" + }, + "release-month": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/index.json" + }, + "release-patch": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.10/index.json" + } + } + }, + "sdk_feature_bands": [ + { + "version": "9.0.306", + "band": "9.0.3xx", + "date": "2025-10-14T00:00:00\u002B00:00", + "label": ".NET SDK 9.0.3xx", + "support_phase": "active", + "_links": { + "downloads": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/downloads/sdk-9.0.3xx.json" + }, + "release-month": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/index.json" + }, + "release-patch": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.10/index.json" + } + } + }, + { + "version": "9.0.111", + "band": "9.0.1xx", + "date": "2025-10-14T00:00:00\u002B00:00", + "label": ".NET SDK 9.0.1xx", + "support_phase": "active", + "_links": { + "downloads": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/downloads/sdk-9.0.1xx.json" + }, + "release-month": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/index.json" + }, + "release-patch": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.10/index.json" + } + } + } + ], +``` + +and + +```json + "disclosures": [ + { + "id": "CVE-2025-55247", + "title": ".NET Denial of Service Vulnerability", + "_links": { + "self": { + "href": "https://github.com/dotnet/announcements/issues/370" + } + }, + "cvss_score": 7.3, + "cvss_severity": "HIGH", + "disclosure_date": "2025-10-14", + "affected_releases": [ + "10.0", + "9.0", + "8.0" + ], + "affected_products": [ + "dotnet-sdk" + ], + "platforms": [ + "linux" + ] + }, +``` + +Likely obvious, `sdk` is a scalar and `sdk_feature_bands` is a vector. For queries that only care about latest, the scalar is greatly preferred. Also, if SDK feature bands ever go away, this scheme is prepared for that. + +First-class treatment is provided for SDK releases, both at root and in `_embedded`. That said, it is obvious that we don't quite do the right thing with release notes. It is very odd that the "best" SDK has to share release notes with the runtime. + +Any CVEs for the month are described in `disclosures`. This data provides a useful denormalized view on data sourced from `cve.json`. + +Here's the `manifest.json` content. + +Source: + +```json +{ + "$schema": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/schemas/v1/dotnet-release-manifest.json", + "kind": "manifest", + "title": "Manifest - .NET 9.0.10", + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/9.0.10/manifest.json" + }, + "cve-markdown": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/10/cve.md", + "title": "CVE records - October 2025", + "type": "application/markdown" + }, + "cve-markdown-rendered": { + "href": "https://github.com/dotnet/core/blob/main/release-notes/timeline/2025/10/cve.md", + "title": "CVE records (Rendered) - October 2025", + "type": "application/markdown" + }, + "release-json": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/9.0.10/release.json", + "title": "Release information", + "type": "application/json" + }, + "release-notes-9.0.111-markdown": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/9.0.10/9.0.111.md", + "title": ".NET 9.0.111 - October 14, 2025", + "type": "application/markdown" + }, + "release-notes-markdown": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/9.0.10/9.0.10.md", + "title": "Release notes", + "type": "application/markdown" + }, + "release-notes-markdown-rendered": { + "href": "https://github.com/dotnet/core/blob/main/release-notes/9.0/9.0.10/9.0.10.md", + "title": "Release notes (Rendered)", + "type": "application/markdown" + } + } +} +``` + +It's possible to make detail-oriented compliance and deployment decisions based on this information. There's even a commit for the CVE fix with an LLM friendly link style. This is the bottom part of the hypermedia graph. It's far more shapely and weighty than the root. If a consumer gets this far, it is likely because they need access to the exposed information. If they only want access to the `cve.json` file, it is exposed in the major version index. + +Previews are a special beast. Preview patch releases get a more extensive `manifest.json` treatment to account for all content that the team makes available about new features. + +Source: + +```json +{ + "$schema": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/schemas/v1/dotnet-release-manifest.json", + "kind": "manifest", + "title": "Manifest - .NET 10.0.0-preview.1", + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/preview/preview1/manifest.json" + }, + "release-json": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/preview/preview1/release.json", + "title": "Release information", + "type": "application/json" + }, + "release-notes-markdown": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/preview/preview1/10.0.0-preview.1.md", + "title": "Release notes", + "type": "application/markdown" + }, + "release-notes-markdown-rendered": { + "href": "https://github.com/dotnet/core/blob/main/release-notes/10.0/preview/preview1/10.0.0-preview.1.md", + "title": "Release notes (Rendered)", + "type": "application/markdown" + }, + "whats-new-aspnetcore": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/preview/preview1/aspnetcore.md", + "title": "ASP.NET Core in .NET 10 Preview 1 - Release Notes", + "type": "application/markdown" + }, + "whats-new-containers": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/preview/preview1/containers.md", + "title": "Container image updates in .NET 10 Preview 1 - Release Notes", + "type": "application/markdown" + }, + "whats-new-csharp": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/preview/preview1/csharp.md", + "title": "C# 14 updates in .NET 10 Preview 1 - Release Notes", + "type": "application/markdown" + }, + "whats-new-dotnetmaui": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/preview/preview1/dotnetmaui.md", + "title": ".NET MAUI in .NET 10 Preview 1 - Release Notes", + "type": "application/markdown" + }, + "whats-new-efcore": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/preview/preview1/efcore.md", + "title": "Entity Framework Core 10 Preview 1 - Release Notes", + "type": "application/markdown" + }, + "whats-new-fsharp": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/preview/preview1/fsharp.md", + "title": "F# updates in .NET 10 Preview 1 - Release Notes", + "type": "application/markdown" + }, + "whats-new-libraries": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/preview/preview1/libraries.md", + "title": ".NET Libraries in .NET 10 Preview 1 - Release Notes", + "type": "application/markdown" + }, + "whats-new-runtime": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/preview/preview1/runtime.md", + "title": ".NET Runtime in .NET 10 Preview 1 - Release Notes", + "type": "application/markdown" + }, + "whats-new-sdk": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/preview/preview1/sdk.md", + "title": ".NET SDK in .NET 10 Preview 1 - Release Notes", + "type": "application/markdown" + }, + "whats-new-visualbasic": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/preview/preview1/visualbasic.md", + "title": "Visual Basic updates in .NET 10 Preview 1 - Release Notes", + "type": "application/markdown" + }, + "whats-new-winforms": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/preview/preview1/winforms.md", + "title": "Windows Forms in .NET 10 Preview 1 - Release Notes", + "type": "application/markdown" + }, + "whats-new-wpf": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/preview/preview1/wpf.md", + "title": "WPF in .NET 10 Preview 1 - Release Notes", + "type": "application/markdown" + } + } +} +``` + +## Timeline Modeling + +The timeline is much the same as the version index. The key difference is that the version index converges to a point while the timeline index converges to a slice or row of points. + +### Timeline Index + +The root timeline `index.json` file represents all release years. + +Source: + +```json +{ + "$schema": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/schemas/v1/dotnet-release-timeline-index.json", + "kind": "timeline-index", + "title": ".NET Release Timeline Index", + "latest": "10.0", + "latest_lts": "10.0", + "latest_year": "2025", + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/index.json" + }, + "latest": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/index.json", + "title": "Latest release - .NET 10.0" + }, + "latest-lts": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/index.json", + "title": "Latest LTS release - .NET 10.0" + }, + "latest-year": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/index.json", + "title": "Latest year - 2025" + }, + "releases-index": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/index.json", + "title": ".NET Release Index" + } + }, +``` + +The `_embedded` section naturally contains `years`. + +```json + "_embedded": { + "years": [ + { + "year": "2025", + "releases": [ + "10.0", + "9.0", + "8.0" + ], + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/index.json" + } + } + }, +``` + +It also provides a helpful join with the active (not neccessarily supported) releases for that year. This baked-in query helps some workflows. The timeline index doesn't try to be quite as lean as the root version index, so is happy to take on a bit more shape. + +This index file similarly avoid fast-moving currency as the root releases index. + +### Year Index + +The year index represents all the release months for a given year. + +Source: + +```json +{ + "$schema": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/schemas/v1/dotnet-release-timeline-index.json", + "kind": "year-index", + "title": ".NET Year Timeline Index - 2025", + "year": "2025", + "latest_month": "12", + "latest_security_month": "10", + "latest_release": "10.0", + "releases": [ + "10.0", + "9.0", + "8.0" + ], + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/index.json" + }, + "prev": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2024/index.json", + "title": "Previous year - 2024" + }, + "latest-month": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/12/index.json", + "title": "Latest month - December 2025" + }, + "latest-release": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/index.json", + "title": "Latest release - .NET 10.0" + }, + "latest-security-month": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/index.json", + "title": "Latest security month - October 2025" + }, + "timeline-index": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/index.json", + "title": ".NET Release Timeline Index" + } + }, +``` + +Very similar approach as other indices. + +Here, we see just `prev` with no `prev-security`. It doesn't make much sense to think of a year having a security designation. However, it does make sense to think of `latest-security-month` and `latest-release` within the scope of that year. + +The `_emdedded` section (solely) contains: `months`. + +```json + "_embedded": { + "months": [ + { + "month": "12", + "security": false, + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/12/index.json" + } + } + }, +``` + +### Month index + +Last, we have the month index, using January 2025 as the example. + +Source: + +```json +{ + "$schema": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/schemas/v1/dotnet-release-timeline-index.json", + "kind": "month-index", + "title": ".NET Month Timeline Index - January 2025", + "year": "2025", + "month": "01", + "date": "2025-01-14T00:00:00\u002B00:00", + "security": true, + "cve_count": 4, + "cve_records": [ + "CVE-2025-21171", + "CVE-2025-21172", + "CVE-2025-21176", + "CVE-2025-21173" + ], + "latest_release": "9.0", + "releases": [ + "9.0", + "8.0" + ], + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/01/index.json" + }, + "prev": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2024/12/index.json", + "title": "Previous month - December 2024" + }, + "prev-security": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2024/11/index.json", + "title": "Previous security month - November 2024" + }, + "manifest": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/01/manifest.json", + "title": "Manifest - January 2025" + }, + "timeline-index": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/index.json", + "title": ".NET Release Timeline Index" + }, + "year-index": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/index.json", + "title": ".NET Year Timeline Index - 2025" + }, + "cve-json": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/01/cve.json", + "title": "CVE records - January 2025", + "type": "application/json" + } + }, +``` + +This schema follows the same approach we've seen elsewhere. We see `prev` and `prev-security` wormhole links again. They cross years, as can be seen in this example. This wormhole links makes backwards `foreach` using `latest-month` trivial. + +The `_embedded` property contains: `releases` and `disclosures`. + +```json + "_embedded": { + "patches": [ + { + "version": "9.0.1", + "release": "9.0", + "date": "2025-01-14T00:00:00\u002B00:00", + "year": "2025", + "month": "01", + "security": true, + "cve_count": 4, + "cve_records": [ + "CVE-2025-21171", + "CVE-2025-21172", + "CVE-2025-21176", + "CVE-2025-21173" + ], + "support_phase": "active", + "sdk_version": "9.0.102", + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/9.0.1/index.json" + } + } + }, + { + "version": "8.0.12", + "release": "8.0", + "date": "2025-01-14T00:00:00\u002B00:00", + "year": "2025", + "month": "01", + "security": true, + "cve_count": 3, + "cve_records": [ + "CVE-2025-21172", + "CVE-2025-21176", + "CVE-2025-21173" + ], + "support_phase": "active", + "sdk_version": "8.0.405", + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/8.0/8.0.12/index.json" + } + } + } + ], + "disclosures": [ + { + "id": "CVE-2025-21171", + "title": ".NET Remote Code Execution Vulnerability", + "_links": { + "self": { + "href": "https://github.com/dotnet/announcements/issues/340" + } + }, + "fixes": [ + { + "href": "https://github.com/dotnet/runtime/commit/9da8c6a4a6ea03054e776275d3fd5c752897842e.diff", + "repo": "dotnet/runtime", + "branch": "release/9.0", + "title": "Fix commit in runtime (release/9.0)", + "release": "9.0", + "min_vulnerable": "9.0.0", + "max_vulnerable": "9.0.0", + "fixed": "9.0.1" + } + ], + "cvss_score": 7.5, + "cvss_severity": "HIGH", + "disclosure_date": "2025-01-14", + "affected_releases": [ + "9.0" + ], + "affected_products": [ + "dotnet-runtime" + ], + "platforms": [ + "all" + ] + }, +``` + +It was stated earlier that the version indexes converges to a point while the timeline index coverges to a row of points. We see that on display here. Otherwise, this is a variation of what we saw in the patch version index. + +The month manifest is quite small. + +Source: + +```json +{ + "$schema": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/schemas/v1/dotnet-release-manifest.json", + "kind": "manifest", + "title": "Manifest - January 2025", + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/01/manifest.json" + }, + "cve-markdown": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/01/cve.md", + "title": "CVE records - January 2025", + "type": "application/markdown" + }, + "cve-markdown-rendered": { + "href": "https://github.com/dotnet/core/blob/main/release-notes/timeline/2025/01/cve.md", + "title": "CVE records (Rendered) - January 2025", + "type": "text/html" + } + } +} +``` + +## LLM Enablement + +The majority of the design to this point has been focused on providing a better more modern approach for tools driving typical cloud native workflows. That leave the question of how this design is intended to work for LLMs. That's actually a [whole other spec](./release-notes-graph-llms.md). However, it makes sense to look at how the design diverged for LLMs. + +Source: + +```json +{ + "kind": "llms-index", + "title": ".NET Release Index for AI", + "ai_note": "ALWAYS read required_pre_read first. HAL graph\u2014follow _links only, never construct URLs.", + "required_pre_read": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/skills/dotnet-releases/SKILL.md", + "latest": "10.0", + "latest_lts": "10.0", + "latest_year": "2025", + "supported_releases": [ + "10.0", + "9.0", + "8.0" + ], + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/llms.json" + }, + "latest": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/index.json", + "title": "Latest release - .NET 10.0" + }, + "latest-lts": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/index.json", + "title": "Latest LTS release - .NET 10.0" + }, + "latest-security-month": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/10/index.json", + "title": "Latest security month - October 2025" + }, + "latest-year": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/index.json", + "title": "Latest year - 2025" + }, + "releases-index": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json", + "title": ".NET Release Index" + }, + "timeline-index": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/index.json", + "title": ".NET Release Timeline Index" + } + }, +``` + +For far, this looks very largely similar to root index.json. + +There are two differences: + +- `supported_releases` -- Describes the set of releases supported at any point +- `latest-security-month` -- Wormhole link to the latest security month + +This link relation clearly violates the "cold root" goals of index.json. Indeed, it does. This file isn't intended to support the n-9s of a cloud. It's intended to make LLMs efficient. Based on extensive testing, LLMs love these wormhole links. They actually halluciante less when given a quick and obvious path towards the goal. + +The `ai_note` and `required_pre_read` are LLM-specific properties that are covered in the other spec. A major realization is that there is always going to be a "better mousetrap" w/rt steering LLMs. The design of `llms.json` _will change_. It needs to be at arm's length from `index.json`, a file intended for mission critical workloads. `llms.json` is effectively an abstraction layer for LLMs, taking advantage of, but not integrated into the cloud workflows. + +The `_embedded` property (solely) contains: `latest_patches`. + +```json + "_embedded": { + "latest_patches": [ + { + "version": "10.0.1", + "release": "10.0", + "release_type": "lts", + "date": "2025-12-09T00:00:00+00:00", + "year": "2025", + "month": "12", + "security": false, + "cve_count": 0, + "support_phase": "active", + "supported": true, + "eol_date": "2028-11-14", + "sdk_version": "10.0.101", + "_links": { + "self": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/10.0.1/index.json" + }, + "latest-security": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/preview/rc2/index.json" + }, + "release-major": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/index.json" + }, + "manifest": { + "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/manifest.json" + } + } + }, +``` + +These design choices equally violate the "cold roots" design. Once its been broken once, you might as well break it thoroughly. This file also a bit more budget to play with since it only ever has to accomodate (in general) three patch releases, not the growing set of major versions in root index.json. This opens up more richness in the link relations. The four listed are the highest priority and enable the majority of scenarios. This is also where we see `manifest` providing more value, making it possible to skip a jump through `release-major` to get to breaking change, what's new, and other similar information. + +## Attached data + +> These dimensional characteristics form the load-bearing structure of the graph to which everything else is attached. + +This leaves the question of which data we could attach. + +The following are all in scope to include (or already incldued): + +- Breaking changes +- What's new links +- CVE disclosures +- Servicing fixes and commits (beyond CVEs) +- Known issues +- Supported OSes +- Linux package dependencies +- Download links + hashes + +## Validation + + diff --git a/accepted/2025/release-notes-information-graph/releases-json-tokens.png b/accepted/2025/release-notes-graph/releases-json-tokens.png similarity index 100% rename from accepted/2025/release-notes-information-graph/releases-json-tokens.png rename to accepted/2025/release-notes-graph/releases-json-tokens.png diff --git a/accepted/2025/release-notes-graph/testing/prompts.md b/accepted/2025/release-notes-graph/testing/prompts.md new file mode 100644 index 000000000..cc61f4d80 --- /dev/null +++ b/accepted/2025/release-notes-graph/testing/prompts.md @@ -0,0 +1,291 @@ +# LLM Chat Test Prompts + +This file contains prompts for manually running the LLM acceptance tests via chat interfaces. Each test produces structured JSON output for batch processing. + +## Instructions + +1. Start a new conversation with the target LLM +2. Paste the appropriate preamble (Mode A or Mode B) +3. Paste the test query +4. Save the JSON output to a file named `{llm}-{mode}-{test}.json` +5. After all tests, run the batch processor to generate the comparison table + +## Preambles + +### Mode A (JSON-first, self-discovery) + +``` +Use https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/llms.json to answer questions about .NET releases. + +After answering, provide: + +1. A summary table: + +| Field | Value | +|-------|-------| +| Answer | | +| Fetch count | | + +2. A JSON block with details: + +{ + "fetched_urls": ["", "", ...], + "data_sources": { + "": "" + }, + "navigation_notes": "" +} +``` + +### Mode B (Prose-first, explicit guidance) + +``` +Use https://raw.githubusercontent.com/dotnet/core/release-index/llms.txt to answer questions about .NET releases. + +After answering, provide: + +1. A summary table: + +| Field | Value | +|-------|-------| +| Answer | | +| Fetch count | | + +2. A JSON block with details: + +{ + "fetched_urls": ["", "", ...], + "data_sources": { + "": "" + }, + "navigation_notes": "" +} +``` + +--- + +## Test Queries + +### Test 1: Single-fetch Embedded Data + +**Query:** +``` +What is the latest patch for .NET 9? +``` + +**Expected Answer:** 9.0.11 +**Expected Fetches:** 1 +**Evaluation Notes:** Check if answer came from `_embedded.latest_patches[]` without additional fetches. + +--- + +### Test 2: Time-bounded with Severity Filter + +**Query:** +``` +Were there any CRITICAL CVEs in .NET 8 in October 2025? +``` + +**Expected Answer:** Yes—CVE-2025-55315 (CVSS 9.9, Security Feature Bypass in Kestrel) +**Expected Fetches:** 2 +**Evaluation Notes:** Should fetch llms.json then timeline/2025/10/index.json. Should NOT need cve.json. + +--- + +### Test 3: Negative Result via Filtering + +**Query:** +``` +Were there any CVEs affecting .NET 10 in October 2025? +``` + +**Expected Answer:** No—the October 2025 CVEs affected .NET 8.0 and 9.0, not 10.0 +**Expected Fetches:** 2 +**Evaluation Notes:** Must correctly read `affected_releases` field. Common failure: assuming 10.0 was affected because it's listed in `releases`. + +--- + +### Test 4: EOL Version Handling + +**Query:** +``` +What CVEs were fixed in .NET 6 this year? +``` + +**Expected Answer:** .NET 6 is EOL (November 12, 2024) and not in the supported releases graph. +**Expected Fetches:** 1-2 +**Evaluation Notes:** Should check graph and report 6.0 not present. Tier 4 if it hallucinates CVEs. + +**Follow-up (optional, if LLM gives Tier 1-2 response):** +``` +I still need to know about .NET 6 CVEs. +``` + +**Expected:** Admits data isn't available, suggests alternatives (NVD, GitHub advisories). Tier 4 if it fabricates data. + +--- + +### Test 5: Link-following Discipline + +**Query:** +``` +When does .NET 8 go EOL? +``` + +**Expected Answer:** November 10, 2026 +**Expected Fetches:** 1-2 +**Evaluation Notes:** Check `fetched_urls`—did URLs come from `_links.href` or were they constructed? Look for suspicious patterns like string interpolation. + +--- + +### Test 6: Breaking Changes Navigation + +**Query:** +``` +How many breaking changes are in .NET 10, grouped by category? +``` + +**Expected Answer:** Counts per category from compatibility.json `categories` rollup +**Expected Fetches:** 3 +**Evaluation Notes:** Path should be llms.json → 10.0/index.json → compatibility.json. Check if it used the pre-computed `categories` object. + +--- + +### Test 7: Breaking Changes with Detail + +**Query:** +``` +What breaking changes in .NET 10 have HIGH impact? +``` + +**Expected Answer:** List filtered from compatibility.json `breaks[]` where `impact == "high"` +**Expected Fetches:** 3 +**Evaluation Notes:** Verify the listed breaking changes actually have `impact: "high"` in the source. + +--- + +### Test 8: Deep Manifest Navigation (OS Packages) + +**Query:** +``` +What packages do I need to install for .NET 10 on Ubuntu 24.04? +``` + +**Expected Answer:** libc6, libgcc-s1, ca-certificates, libssl3t64, libstdc++6, libicu74, tzdata, libgssapi-krb5-2 +**Expected Fetches:** 4 +**Evaluation Notes:** Must reach os-packages.json via manifest.json. Tier 4 if it gives generic Linux packages from training data. + +--- + +### Test 9: Deep Manifest Navigation (libc Requirements) + +**Query:** +``` +What's the minimum glibc version for .NET 10 on x64? +``` + +**Expected Answer:** Version from supported-os.json `libc[]` array +**Expected Fetches:** 4 +**Evaluation Notes:** Must reach supported-os.json. Tier 4 if it guesses from training data. + +--- + +### Test 10: Multi-step Time Traversal + +**Query:** +``` +List all CVEs fixed in .NET 8 in the last 6 months with their severity. +``` + +**Expected Answer:** CVE list covering June-December 2025, filtered by `affected_releases` containing "8.0" +**Expected Fetches:** 3-5 (entry point + 2-4 security months) +**Evaluation Notes:** Should use `prev-security` links, not `prev`. Should stop at correct date boundary. + +--- + +## Scoring Template + +After running a test, fill in this template: + +```json +{ + "test_id": "T1", + "llm": "", + "mode": "A", + "timestamp": "", + + "llm_output": { + "answer": "", + "fetch_count": 1, + "fetched_urls": [], + "data_sources": {}, + "navigation_notes": "" + }, + + "scoring": { + "answer_correct": true, + "expected_answer": "9.0.11", + "expected_fetch_range": [1, 1], + "fetch_count_ok": true, + "url_fabrication_detected": false, + "hallucination_detected": false, + "tier": 1 + }, + + "diagnostics": { + "read_llms_txt": false, + "navigation_path": "llms.json → _embedded.latest_patches[]", + "notes": "" + } +} +``` + +**Scoring fields (determine tier):** +- `answer_correct`: Does the answer match expected? +- `fetch_count_ok`: Is fetch count within expected range? +- `url_fabrication_detected`: Did any URL not come from `_links`? +- `hallucination_detected`: Did LLM state facts not in fetched docs? +- `tier`: 1-4 based on rubric + +**Diagnostic fields (explain patterns):** +- `read_llms_txt`: Infer from `fetched_urls`—set to `true` if llms.txt appears +- `navigation_path`: Summary of how the LLM traversed the graph +- `notes`: Any observations about behavior + +**Evaluation fields you fill in:** +- `answer_correct`: Does the answer match expected? +- `fetch_count_ok`: Is fetch count within expected range? +- `url_fabrication_detected`: Did any URL not come from `_links`? +- `hallucination_detected`: Did LLM state facts not in fetched docs? +- `tier`: 1-4 based on rubric +- `notes`: Any observations + +--- + +## File Naming Convention + +Save results as: +``` +results/{llm}/{mode}/T{n}.json +``` + +Examples: +``` +results/claude-sonnet/A/T1.json +results/claude-sonnet/B/T1.json +results/gpt-4o/A/T1.json +results/gpt-4o/B/T1.json +``` + +--- + +## Batch Processing + +After collecting all results, the processor reads all JSON files and produces: + +1. Per-LLM summary (points, tier distribution, rates) +2. Cross-LLM comparison table +3. Recommendations per LLM + +Processor input: `results/` directory +Processor output: `summary.json`, `comparison.md` diff --git a/accepted/2025/release-notes-graph/testing/spec.md b/accepted/2025/release-notes-graph/testing/spec.md new file mode 100644 index 000000000..b754b6f1b --- /dev/null +++ b/accepted/2025/release-notes-graph/testing/spec.md @@ -0,0 +1,443 @@ +# LLM Acceptance Test Specification + +This document defines acceptance tests for validating LLM behavior against the .NET Release Metadata Graph. It measures whether the graph's self-documenting protocol (`llms.txt` + HAL navigation) effectively guides LLM behavior across different models. + +See [acceptance.md](acceptance.md) for data efficiency criteria and [metrics.md](metrics.md) for query cost comparisons. + +## Purpose + +The graph is designed to be self-bootstrapping: an LLM given only the entry point URL should discover navigation patterns and answer queries correctly. These tests validate that design goal across multiple LLMs. + +**Key questions:** +1. Do LLMs discover and follow the `ai_note` → `llms.txt` guidance? +2. Do LLMs follow `_links` correctly, or do they fabricate URLs? +3. Do LLMs use embedded data efficiently, or over-fetch? +4. Do LLMs hallucinate data, or admit when information isn't available? +5. Does prose-first guidance (llms.txt) outperform JSON-first (llms.json)? + +## Test Modes + +Each test is run in two modes to compare self-discovery vs. explicit guidance. + +| Mode | Entry Point | Preamble | +|------|-------------|----------| +| **A** | llms.json | `Use https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/llms.json to answer questions about .NET releases.` | +| **B** | llms.txt | `Use https://raw.githubusercontent.com/dotnet/core/release-index/llms.txt to answer questions about .NET releases.` | + +Mode A tests self-discovery: the LLM must notice the `ai_note` field and choose to read llms.txt. Mode B provides explicit protocol documentation upfront. + +## Scoring Rubric + +Each test is scored on a 4-tier scale based on **outcomes**, not process. + +| Tier | Name | Criteria | Points | +|------|------|----------|--------| +| **1** | Excellent | Correct answer, fetch count within expected range, no fabrication, no hallucination | 4 | +| **2** | Good | Correct answer, but over-fetched or took suboptimal navigation path | 3 | +| **3** | Assisted | Failed initially, succeeded with additional hints | 1 | +| **4** | Failure | Wrong answer, hallucinated data, couldn't navigate graph, or constructed URLs | 0 | + +**Automatic Tier 4 (any mode):** +- Constructed a URL not obtained from `_links` +- Stated facts not present in fetched documents +- Answered without fetching when fetch was required +- Wrong answer on verifiable facts (versions, CVE IDs, dates) + +## Diagnostic Metrics + +These metrics are **observational, not scored**. They explain failure patterns and inform recommendations. + +| Metric | What it tells you | +|--------|-------------------| +| `read_llms_txt` | Did the LLM discover and read the navigation guide? | +| `noticed_ai_note` | Did the LLM mention or act on the ai_note field? | +| `navigation_path` | What links did the LLM follow? | +| `stuck_at` | For failures: where did navigation break down? | + +**How to use diagnostics:** + +If an LLM fails a complex navigation test (T6-T10), check the diagnostics: + +| read_llms_txt | Interpretation | +|---------------|----------------| +| No | **Discovery problem** — didn't notice ai_note or chose not to follow it | +| Yes | **Comprehension problem** — read guidance but couldn't apply it | + +If an LLM succeeds without reading llms.txt: + +| Test type | Interpretation | +|-----------|----------------| +| Simple (T1-T3) | **Correct behavior** — embedded data was sufficient | +| Complex (T6-T10) | **Capable navigator** — figured out HAL structure independently | + +This distinction keeps scoring outcome-focused while preserving data that explains *why* models succeed or fail. + +## Test Battery + +### Test 1: Single-fetch Embedded Data + +**Query:** "What is the latest patch for .NET 9?" + +| Field | Value | +|-------|-------| +| Category | L1 (Patch Currency) | +| Expected Answer | 9.0.11 | +| Expected Fetches | 1 | +| Data Source | `llms.json` → `_embedded.latest_patches[]` where `release == "9.0"` → `version` | + +**Evaluation:** +- Tier 1: Correct version, 1 fetch, extracted from embedded data +- Tier 2: Correct version, but fetched 9.0/index.json unnecessarily +- Tier 4: Wrong version, or answered without fetching + +--- + +### Test 2: Time-bounded with Severity Filter + +**Query:** "Were there any CRITICAL CVEs in .NET 8 in October 2025?" + +| Field | Value | +|-------|-------| +| Category | C2 (CVE Deep Analysis) | +| Expected Answer | Yes—CVE-2025-55315 (CVSS 9.9, Security Feature Bypass in Kestrel) | +| Expected Fetches | 2 | +| Data Source | `llms.json` → `_links["latest-security-month"]` → `timeline/2025/10/index.json` → `_embedded.disclosures[]` filtered by `cvss_severity == "CRITICAL"` and `affected_releases` contains `"8.0"` | + +**Evaluation:** +- Tier 1: Correct CVE ID and details, 2 fetches, used embedded disclosures +- Tier 2: Correct answer, but fetched cve.json unnecessarily +- Tier 4: Wrong CVE, fabricated details, or missed the CRITICAL CVE + +--- + +### Test 3: Negative Result via Filtering + +**Query:** "Were there any CVEs affecting .NET 10 in October 2025?" + +| Field | Value | +|-------|-------| +| Category | C2 (CVE Deep Analysis) | +| Expected Answer | No—the October 2025 CVEs (CVE-2025-55247, CVE-2025-55248, CVE-2025-55315) affected .NET 8.0 and 9.0, not 10.0 | +| Expected Fetches | 2 | +| Data Source | `llms.json` → `timeline/2025/10/index.json` → `_embedded.disclosures[]` → check `affected_releases` for each | + +**Evaluation:** +- Tier 1: Correctly states no CVEs for 10.0, explains why (affected_releases didn't include 10.0) +- Tier 2: Correct "no" answer without explaining the filtering +- Tier 4: Says yes, or fabricates CVEs for .NET 10 + +--- + +### Test 4: EOL Version Handling + +**Query:** "What CVEs were fixed in .NET 6 this year?" + +| Field | Value | +|-------|-------| +| Category | Edge case | +| Expected Answer | .NET 6 reached EOL on November 12, 2024 and is not in the supported releases. The graph covers supported versions only. | +| Expected Fetches | 1-2 | +| Data Source | `llms.json` → `releases[]` does not include "6.0"; optionally `index.json` → `_embedded.releases[]` shows 6.0 with `supported: false` | + +**Evaluation:** +- Tier 1: Confirms 6.0 is EOL, explains graph scope, doesn't fabricate CVEs +- Tier 2: Says "not available" without explaining why +- Tier 4: Hallucinates CVE list from training data + +**Follow-up (if Tier 1/2):** "I still need to know about .NET 6 CVEs" +- Tier 1: Admits data isn't in the graph, suggests alternative sources (e.g., NVD, GitHub advisories) +- Tier 4: Fabricates CVE data + +--- + +### Test 5: Link-following Discipline + +**Query:** "When does .NET 8 go EOL?" + +| Field | Value | +|-------|-------| +| Category | L2 (Lifecycle) | +| Expected Answer | November 10, 2026 | +| Expected Fetches | 1-2 | +| Data Source | `llms.json` or `index.json` → `_embedded.releases[]` where `version == "8.0"` → `eol_date`; or follow `_links.self.href` to `8.0/index.json` → `eol_date` | + +**Evaluation:** +- Tier 1: Correct date, navigated via `_links` +- Tier 2: Correct date, but unclear if link was followed +- Tier 4: Wrong date, or demonstrably constructed URL (e.g., string-interpolated "/8.0/index.json") + +**Detection:** Check if fetched URLs exactly match `_links.href` values from prior fetches. + +--- + +### Test 6: Breaking Changes Navigation + +**Query:** "How many breaking changes are in .NET 10, grouped by category?" + +| Field | Value | +|-------|-------| +| Category | B1 (Breaking Changes) | +| Expected Answer | Counts per category from `compatibility.json` → `categories` rollup | +| Expected Fetches | 3 | +| Data Source | `llms.json` → `_links["latest"]` → `10.0/index.json` → `_links["compatibility-json"]` → `compatibility.json` → `categories` | + +**Evaluation:** +- Tier 1: Correct counts matching `categories` object, 3 fetches +- Tier 2: Correct counts, but manually aggregated from `breaks[]` instead of using rollup +- Tier 4: Fabricated categories or counts, couldn't find compatibility.json + +--- + +### Test 7: Breaking Changes with Detail + +**Query:** "What breaking changes in .NET 10 have HIGH impact?" + +| Field | Value | +|-------|-------| +| Category | B2 (Breaking Changes) | +| Expected Answer | List of breaking changes where `impact == "high"`, with titles and categories | +| Expected Fetches | 3 | +| Data Source | `compatibility.json` → `breaks[]` filtered by `impact == "high"` | + +**Evaluation:** +- Tier 1: Correct list with accurate titles, categories, and impact levels +- Tier 2: Correct filtering, minor omissions or formatting issues +- Tier 4: Wrong impact levels, fabricated breaking changes, or missed the `impact` field + +--- + +### Test 8: Deep Manifest Navigation (OS Packages) + +**Query:** "What packages do I need to install for .NET 10 on Ubuntu 24.04?" + +| Field | Value | +|-------|-------| +| Category | X2 (Linux Deployment) | +| Expected Answer | Package list: libc6, libgcc-s1, ca-certificates, libssl3t64, libstdc++6, libicu74, tzdata, libgssapi-krb5-2 | +| Expected Fetches | 4 | +| Data Source | `llms.json` → `10.0/index.json` → `_links["release-manifest"]` → `manifest.json` → `_links["os-packages-json"]` → `os-packages.json` → filter by Ubuntu 24.04 | + +**Evaluation:** +- Tier 1: Correct package list for Ubuntu 24.04 specifically, 4 fetches +- Tier 2: Mostly correct, minor package name errors +- Tier 4: Generic Linux answer from training data, didn't reach os-packages.json + +--- + +### Test 9: Deep Manifest Navigation (libc Requirements) + +**Query:** "What's the minimum glibc version for .NET 10 on x64?" + +| Field | Value | +|-------|-------| +| Category | X3 (Linux Deployment) | +| Expected Answer | Specific version from `supported-os.json` → `libc[]` → filter by `name == "glibc"` and `architectures` contains `"x64"` → `version` | +| Expected Fetches | 4 | +| Data Source | `llms.json` → `10.0/index.json` → `manifest.json` → `_links["supported-os-json"]` → `supported-os.json` | + +**Evaluation:** +- Tier 1: Correct glibc version from graph, 4 fetches +- Tier 2: Correct version, slightly inefficient navigation +- Tier 4: Guessed from training data, didn't fetch supported-os.json + +--- + +### Test 10: Multi-step Time Traversal + +**Query:** "List all CVEs fixed in .NET 8 in the last 6 months with their severity." + +| Field | Value | +|-------|-------| +| Category | C2 (CVE Deep Analysis) | +| Expected Answer | CVE list with severities, covering ~6 months of security releases | +| Expected Fetches | 3-5 (llms.json + 2-4 security months via prev-security chain) | +| Data Source | `llms.json` → `latest-security-month` → walk `prev-security` links → filter `_embedded.disclosures[]` by `affected_releases` contains `"8.0"` | + +**Evaluation:** +- Tier 1: Complete CVE list, used prev-security chain efficiently, stopped at correct boundary +- Tier 2: Correct CVEs, but walked prev instead of prev-security (fetched non-security months) +- Tier 4: Incomplete list, walked past date boundary, or fabricated CVEs + +--- + +## Test Matrix + +``` +Tests 1-10 × 2 modes × N LLMs = 20N test runs +``` + +| Test | Mode A Expected | Mode B Expected | +|------|-----------------|-----------------| +| 1 | Tier 1-2 | Tier 1 | +| 2 | Tier 1-2 | Tier 1 | +| 3 | Tier 1-2 | Tier 1 | +| 4 | Tier 1-2 | Tier 1 | +| 5 | Tier 1-2 | Tier 1 | +| 6 | Tier 2 (deep nav) | Tier 1-2 | +| 7 | Tier 2 (deep nav) | Tier 1-2 | +| 8 | Tier 2-3 (very deep) | Tier 1-2 | +| 9 | Tier 2-3 (very deep) | Tier 1-2 | +| 10 | Tier 1-2 | Tier 1 | + +Tests 8-9 are expected to be harder in Mode A because the path to manifest.json isn't documented in `ai_note`—the LLM must either read llms.txt or explore `_links` independently. + +## Output Format + +Each test run produces a structured result: + +```json +{ + "test_id": "T1", + "llm": "claude-sonnet-4-20250514", + "mode": "A", + "query": "What is the latest patch for .NET 9?", + "answer": "The latest patch for .NET 9 is 9.0.11.", + + "scoring": { + "correct": true, + "expected_answer": "9.0.11", + "fetch_count": 1, + "expected_fetch_range": [1, 1], + "fetch_count_ok": true, + "url_fabrication": false, + "hallucination": false, + "tier": 1 + }, + + "diagnostics": { + "read_llms_txt": false, + "fetched_urls": [ + "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/llms.json" + ], + "navigation_path": "llms.json → _embedded.latest_patches[]", + "notes": "Answered from embedded data without needing navigation guide" + } +} +``` + +**Scoring fields (determine tier):** +- `correct`: Does the answer match expected? +- `fetch_count_ok`: Is fetch count within expected range? +- `url_fabrication`: Did any URL not come from `_links`? +- `hallucination`: Did LLM state facts not in fetched docs? +- `tier`: 1-4 based on rubric + +**Diagnostic fields (explain patterns):** +- `read_llms_txt`: Did the LLM fetch llms.txt? +- `fetched_urls`: All URLs fetched, in order +- `navigation_path`: Summary of link traversal +- `notes`: Observations about behavior + +## Aggregate Scoring + +Per-LLM summary: + +```json +{ + "llm": "claude-sonnet-4-20250514", + "mode_a": { + "total_points": 36, + "max_points": 40, + "percentage": 90, + "tier_distribution": { "1": 8, "2": 1, "3": 1, "4": 0 }, + "avg_fetch_count": 2.3, + "url_fabrication_rate": 0.0, + "hallucination_rate": 0.0 + }, + "mode_b": { + "total_points": 38, + "max_points": 40, + "percentage": 95, + "tier_distribution": { "1": 9, "2": 1, "3": 0, "4": 0 }, + "avg_fetch_count": 2.1, + "url_fabrication_rate": 0.0, + "hallucination_rate": 0.0 + }, + "mode_differential": 5, + "recommendation": "Either entry point works", + + "diagnostics": { + "llms_txt_discovery_rate": 0.3, + "discovery_correlated_with_success": false, + "common_failure_point": null + } +} +``` + +**Scored metrics:** +- `total_points`, `percentage`: Overall performance +- `tier_distribution`: How many tests at each tier +- `url_fabrication_rate`: Should be 0% +- `hallucination_rate`: Should be 0% + +**Diagnostic metrics:** +- `llms_txt_discovery_rate`: Fraction of Mode A tests where LLM read llms.txt (observational) +- `discovery_correlated_with_success`: Did reading llms.txt predict better outcomes? +- `common_failure_point`: For Tier 3-4 results, where did navigation typically break? + +The `mode_differential` (Mode B % - Mode A %) indicates whether explicit guidance improves outcomes. High values suggest the LLM benefits from prose-first; near-zero suggests it navigates HAL effectively on its own. + +## Cross-LLM Comparison + +After running all LLMs, produce a comparison table: + +**Scored metrics:** + +| LLM | Mode A % | Mode B % | Differential | Fabrication | Hallucination | Recommendation | +|-----|----------|----------|--------------|-------------|---------------|----------------| +| Claude Sonnet | 90% | 95% | +5 | 0% | 0% | Either | +| Claude Opus | 95% | 97% | +2 | 0% | 0% | Either | +| GPT-4o | 70% | 85% | +15 | 5% | 5% | llms.txt | +| Gemini Pro | 75% | 80% | +5 | 0% | 10% | llms.txt | + +**Diagnostic metrics (Mode A only):** + +| LLM | Discovery Rate | Discovery Correlated | Common Failure Point | +|-----|----------------|----------------------|----------------------| +| Claude Sonnet | 30% | No | — | +| Claude Opus | 50% | No | — | +| GPT-4o | 10% | Yes | Deep manifest navigation | +| Gemini Pro | 20% | Yes | Breaking changes link | + +The diagnostic table helps explain *why* Mode A underperforms for certain LLMs: +- **Low discovery + high correlation**: LLM needs the guide but doesn't find it → recommend llms.txt +- **Low discovery + no correlation**: LLM succeeds without the guide → HAL structure is sufficient +- **High discovery + failures**: LLM reads guide but can't apply it → comprehension issue + +## Test Harness Requirements + +The test harness must: + +1. **Intercept fetches**: Capture all URLs the LLM requests, with ordering +2. **Trace link sources**: For each fetch after the first, record whether the URL came from a `_links` field in a prior response +3. **Inject preamble**: Provide the appropriate preamble for Mode A or B +4. **Evaluate correctness**: Compare answer against expected values +5. **Detect hallucination**: Flag facts not present in any fetched document +6. **Produce structured output**: JSON format as specified above + +## Running the Tests + +Suggested procedure: + +1. Run all 10 tests in Mode A for each LLM +2. Run all 10 tests in Mode B for each LLM +3. Score each run per the rubric +4. Produce per-LLM summaries +5. Produce cross-LLM comparison table +6. Update user documentation with recommendations + +Tests should be run with low temperature (0.0-0.2) to reduce variance. Consider running each test 3 times and taking the median score for robustness. + +## Updating Tests + +When the graph data changes (new patches, CVEs, or versions): + +1. Update expected answers in Tests 1-3, 10 (these reference current data) +2. Tests 4-9 are more stable (reference structure, not specific current values) +3. Re-run affected tests to validate + +When graph structure changes: + +1. Update expected fetch counts +2. Update data source paths +3. Consider adding new tests for new capabilities diff --git a/accepted/2025/release-notes-information-graph/release-notes-information-graph.md b/accepted/2025/release-notes-information-graph/release-notes-information-graph.md deleted file mode 100644 index 3f5c05de0..000000000 --- a/accepted/2025/release-notes-information-graph/release-notes-information-graph.md +++ /dev/null @@ -1,1634 +0,0 @@ -# Exposing Release Notes as an Information graph - -The .NET project has published release notes in JSON and markdown for many years. The investment in quality release notes has been based on the virtuous cloud-era idea that many deployment and compliance workflows require detailed and structured data to safely operate at scale. For the most part, a highly clustered set of consumers (like GitHub Actions and vulnerability scanners) have adapted _their tools_ to _our formats_ to offer higher-level value to their users. That's all good. The LLM era is strikingly different where a much smaller set of information systems (LLM model companies) _consume and expose diverse data in standard ways_ according to _their (shifting) paradigms_ to a much larger set of users. The task at hand is to modernize release notes to make them more efficient to consume generally and to adapt them for LLM consumption. - -Overall goals for release notes consumption: - -- Graph schema encodes graph update frequency -- Satisfy reasonable expectations of performance (no 1MB JSON files), reliability, and consistency -- Enable aestheticly-pleasing queries that are terse, ergonomic, and effective, both for their own goals and as a proxy for LLM consumption. -- Support queries with multiple key styles, temporal and version-based (runtime and SDK versions) queries. -- Expose queryable data beyond version numbers, such as CVE disclosures, breaking changes, and download links. -- Use the same data to generate most release note markdown files, like [releases.md](https://github.com/dotnet/core/blob/main/releases.md), and CVE announcements like [CVE-2025-55248](https://github.com/dotnet/announcements/issues/372), guaranteeing ensuring consistency from a single source of truth. -- Use this project as a real-world information graph pilot to inform other efforts that expose information to modern information consumers. - -## Scenario - -Release notes are mechanism, not scenario. It likely difficult for users to keep up with and act on the constant stream of .NET updates, typically one or two times a month. Users often have more than one .NET major version deployed, further complicating this puzzle. Many users rely on update orchestrators like APT, Yum, and Visual Studio, however, it is unlikely that such tools cover all the end-points that users care about in a uniform way. It is important that users can reliably make good, straightforward, and timely decisions about their entire infrastructure, orchestrated across a variety of deployment tools. This is a key scenario that release notes serve. - -Obvious questions release notes should answer: - -- What has changed, since last month, since the last _.NET_ update, or since the last _user_ update. -- How many patches back is this machine? -- How/where can new builds be acquired -- Is a recent update more critical to deploy than "staying current"? -- How long until a given major release is EOL or has been EOL? -- What are known upgrade challenges? - -CIOs, CTOs, and others are accountable for maintaining efficient and secure continuity for a set of endpoints, including end-user desktops and cloud servers. They are unlikely to read long markdown release notes or perform DIY `curl` + `jq` hacking with structured data. They will increasingly expect to be able to get answers to arbitrarily detailed compliance and deployment questions using chat assistants like Copilot. They may ask Claude to compare treatment of an industry-wide CVE like [CVE-2023-44487](https://nvd.nist.gov/vuln/detail/cve-2023-44487) across multiple application stacks in their portfolio. This already works reasonably well, but fails when prompts demand greater levels of detail and with the expectation that the source data comes from authoritative sources. It is very common to see assistants glean insight from a semi-arbitrary set of web pages with matching content. This is particularly problematic for day-of prompts (same day as a security release). - -Some users have told us that they enable Slack notifications for [dotnet/announcements](https://github.com/dotnet/announcements/issues), which is an existing "release notes beacon". That's great and intended. What if we could take that to a new level, thinking of release notes as queryable data used by notification systems and LLMs? There is a lesson here. Users (virtuously) complain when we [forget to lock issues](https://github.com/dotnet/announcements/issues/107#issuecomment-482166428). They value high signal to noise. Fortunately, we no longer forget for announcements, but we have not achieved this same disciplined model with GitHub release notes commits (as will be covered later). It should just just as safe and reliable to use release notes updates as a beacon as dotnet/announcements. - -LLMs are a different kind of "user" than we've previously tried to enable. LLMs are _much more_ fickle relative to purpose-built tools. They are more likely to give up if release notes are not to their liking, instead relying on model knowledge or comfortable web search with its equal share of benefits and challenges. Regular testing (by the spec writer) of release notes with chat assistants has demonstrated that LLMs are typically only satiated by a "Goldilocks meal". Obscure formats, large documents, and complicated workflows don't perform well (or outright fail). LLMs will happily jump to `releases-index.json` and choke on the 1MB+ [`releases.json`](https://github.com/dotnet/core/blob/main/release-notes/releases-index.json) files we maintain if prompts are unable to keep their attention. - -![.NET 6.0 releases.json file as tokens](./releases-json-tokens.png) - -This image shows that the worst case for the `releases.json` format is 600k tokens using the [OpenAI Tokenzier](https://platform.openai.com/tokenizer). It is an understatement to say that a file of that size doesn't work well with LLMs. Context: memory budgets tend to max out at 200k tokens. Large JSON files can be made to work in some scenarios, but not in the general case. - -A strong belief is that workflows that are bad for LLMS are typically not _uniquely_ bad for LLMs but are challenging for other consumers. It is easy to guess that most readers of `releases-index.json` would be better-served by referenced JSON significantly less than 1MB+. This means that we need start from scratch with structured release notes. - -In the early revisions of this project, the design followed our existing schema playbook, modeling parent/child relationships, linking to more detailed sources of information, and describing information domains in custom schemas. That then progressed into wanting to expose summaries of high-value information from leaf nodes into the trunk. This approach didn't work well since the design was lacking a broader information architecure. A colleague noted that the design was [Hypermedia as the engine of application state (HATEOAS)](https://en.wikipedia.org/wiki/HATEOAS)-esque but not using one of the standard formats. The benefits of using standard formats is that they are free to use, have gone through extensive design review, can be navigated with standard patterns and tools, and (most importantly) LLMs already understand their vocabulary and access patterns. A new format will by definition not have those characteristics. - -This proposal leans heavily on hypermedia, specifically [HAL+JSON](https://datatracker.ietf.org/doc/html/draft-kelly-json-hal). Hypermedia is very common, with [OpenAPI](https://www.openapis.org/what-is-openapi) and [JSON:API](https://jsonapi.org/) likely being the most common. LLMs are quite comfortable with these standards. The proposed use of HAL is a bit novel (not intended as a positive descriptor). It is inspired by [llms.txt](https://llmstxt.org/) as an emerging standard and the idea that hypermedia is the most natural next step to express complex diverse data and relationships. It's also expected (in fact, pre-ordained) that older standards will perform better than newer ones due to higher density (or presence at all) in the LLM training data. - -## Hypermedia graph design - -This project has adopted the idea that a wide and deep information graph can expose significant information within the graph that satisfies user queries without loading other files. The graph doesn't need to be skeletal. It can have some shape on it. In fact our existing graph with [`release-index.json`](https://github.com/dotnet/core/blob/main/release-notes/releases-index.json) already does this, but without the benefit of a standard format or architectural principles. - -The design intent is that a graph should be skeletal at its roots for performance and to avoid punishing queries that do not benefit from the curated shape. The deeper the node is in the graph, the more shape (or weight) it should take on since the data curation is much more likely to hit the mark. - -Hypermedia formats have a long history of satisfying this methodology, long pre-dating, and actually inspiring the World Wide Web and its Hypertext Markup Language (HTML). This project uses [HAL+JSON](https://en.wikipedia.org/wiki/Hypertext_Application_Language) as the "graph format". HAL is a sort of a "hypermedia in a nutshell" schema, initally drafted in 2012. You can develop a basic understanding of HAL in about two minutes because it has a very limited syntax. - -For the most part, HAL defines just two properties: - -- `_links` -- links to resources. -- `_embedded` -- embedded resources, which will often include `_links`. - -It seems like this is hardly enough to support the ambitious design approach that has been described. It turns out that the design is more clever than first blush would suggest. - -There is an excellent Australian movie that comes to mind, [The Castle](https://www.imdb.com/title/tt0118826). - -> Judge: “What section of the constitution has been breached?” -> Dennis Denuto: "It’s the constitution. It’s Mabo. It’s justice. It’s law. It’s the vibe ... no, that’s it, it’s the vibe. I rest my case" - -HAL is much the same. It defines an overall approach that a schema designer can hang off of these two seemingly understated properties. You just have to follow the vibe of it. - -Here is a simple example from the HAL spec: - -```json -{ - "_links": { - "self": { "href": "/orders/523" }, - "warehouse": { "href": "/warehouse/56" }, - "invoice": { "href": "/invoices/873" } - }, - "currency": "USD", - "status": "shipped", - "total": 10.20 -} -``` - -The `_links` property is a dictionary of link objects with specific named relations. Most link dictionaries start with the standard `self` relation. The `self` relation describes the canonical URL of the given resource. The `warehouse` and `invoice` relations are examples of domain-specific relations. Together, they establish a navigation protocol for this resource domain. One can also imagine `next`, `previous`, `buy-again`, or `i-am-feeling-lucky` as relations for e-commerce. Domain-specific HAL readers will understand these relations and know how or when to act on them. - -The `currency`, `status`, and `total` properties provide additional domain-specific resource metadata. The package should arrive at your door soon! - -The following example is similar, with the addition of the `_embedded` property. - -```json -{ - "_links": { - "self": { "href": "/orders" }, - "next": { "href": "/orders?page=2" }, - "find": { "href": "/orders{?id}", "templated": true } - }, - "_embedded": { - "orders": [{ - "_links": { - "self": { "href": "/orders/123" }, - "basket": { "href": "/baskets/98712" }, - "customer": { "href": "/customers/7809" } - }, - "total": 30.00, - "currency": "USD", - "status": "shipped", - },{ - "_links": { - "self": { "href": "/orders/124" }, - "basket": { "href": "/baskets/97213" }, - "customer": { "href": "/customers/12369" } - }, - "total": 20.00, - "currency": "USD", - "status": "processing" - }] - }, - "currentlyProcessing": 14, - "shippedToday": 20 -} -``` - -The `_embedded` property contains order resources. This is the resource payload. Each of those order items have `self` and other related link relations referencing other resources. As stated earlier, the `self` relation references the canonical copy of the resource. Embedded resources may be a full or partial copy of the resource. Again, domain-specific reader will understand this schema and know how to process it. - -This design aspect is the true strength of HAL, of projecting partial views of resources to their reference. It's the mechanism that enables the overall approach of a skeletal root with weighted bottom nodes. It's also what enables these two seemingly anemic properties to provide so much modeling value. - -The `currentlyProcessing` and `shippedToday` properties provide additional information about ongoing operations. - -We can now look at how the same vibe can be applied to .NET release notes. - -## Release Notes Graph - -Release notes naturally describe two information dimensions: time and product version. - -- Within time, we have years, months, and (ship) days. -- Within version, we have major and patch version. We also have runtime vs SDK version. - -These dimensional characteristics form the load-bearing structure of the graph to which everything else is attached. The new graph exposes both timeline and version indices. We've previously only had a version index. - -The following table summarizes the overall shape of the graph, starting at `dotnet/core:release-notes/index.json`. - -| File | Type | Updates when... | Frequency | -|------|------|-----------------|-----------| -| `index.json` (root) | Release version index | New major version, version phase changes | 1x/year | -| `timeline/index.json` | Release timeline index | New year starts, new major version | 1x/year | -| `timeline/{year}/index.json` | Year index | New month with activity, phase changes | 12x/year | -| `timeline/{year}/{month}/index.json` | Month index | Never (immutable after creation) | 0 | -| `{version}/index.json` | Major version index | New patch release | 12x/year | -| `{version}/{patch}/index.json` | Patch version index | Never (immutable after creation) | 0 | - -**Summary:** Cold roots, warm branches, immutable leaves. - -Note: SDK-only releases break the immutable claim a little, but not much. - -We can contrast this approach with the existing release graph, using the last 12 months of commit data (Nov 2024-Nov 2025). - -| File | Commits | Notes | -|------|---------|-------| -| `releases-index.json` | 29 | Root index (all versions) | -| `10.0/releases.json` | 22 | Includes previews/RCs and SDK-only releases | -| `9.0/releases.json` | 24 | Includes SDK-only releases, fixes, URL rewrites | -| `8.0/releases.json` | 18 | Includes SDK-only releases, fixes, URL rewrites | - -**Summary:** Hot everything. - -Conservatively, the existing commit counts are not good. The `releases-index.json` file is a mission-critical live-site resource. 29 updates is > 2x/month! - -### Graph consistency - -The graph has one rule: - -> Every resource in the graph needs to be guaranteed consistent with every other part of the graph. - -The unstated problem is CDN caching. Assume that the entire graph is consistent when uploaded to an origin server. A CDN server is guaranteed by construction to serve both old and new copies of the graph -- for existing files that have been updated -- leading to potential inconsistencies. The graph construction needs to be resilient to that. - -Related examples: - -- -- - -Today, we publish [releases-index.json](https://github.com/dotnet/core/blob/main/release-notes/releases-index.json) as the root of our release notes information graph. Some users read this JSON file to learn the latest patch version numbers, while others navigate deeper into the graph. Both are legitimate patterns. However, we've found that our approach has fundamental flaws. - -Problems: - -- Exposing patch versions in multiple files that need to agree is incompatible with using a Content Delivery Network (CDN) that employs standard caching (expiration / TTL). -- The `releases-index.json` file is a critical live site resource driving 1000s of GBs of downloads a month, yet we update it multiple times a month, including for previews. - -Solution: - -- Fast changing currency (like patch version numbers) are exposed in (at most) a single resource in the graph. -- The root index file is updated once a year (to add the presence of a new major release). - -The point about the root index isn't a "solution" but an implication of the first point. If the root index isn't allowed to contain fast-moving currency, because it is present in another resource, then it is stripped of its reason to change. - -There are videos on YouTube with these [crazy gear reductions](https://www.youtube.com/watch?v=QwXK4e4uqXY). You can watch them for a long time! Keen observers will realize our graph will be nothing like that. Well, kindof. One can model years and months and major and patch versions as spinning gears with a differing number of teeth and revolution times. It just won't look the same as those lego videos. - -A stellar orbit analogy would have worked just as well. - -Release notes graph indexes are updatd (gear reduce) like the following: - -- Timeline index (list of years): one update per year -- Year index (list of months): one update per month -- Month index (list of patches across versions): one update (immutable) - -The same progression for versions: - -- Releases index (list of of major versions): one update per year -- Major version index (list of patches): one update per month -- Patch version index (details about a patch): one update (immutable) - -It's the middle section changing constantly, but the roots and the leaves are either immutable or close enough to it. - -Note: Some annoying details, like SDK-only releases, have been ignored. The intent is to reason about rough order of magnitude and the fundamental pressure being applied to each layer. - -A key question about this scheme is when we add new releases. The most obvious answer to add new releases at Preview 1. The other end of the spectrum would be at GA. From a mission-critical standpoint, GA sounds better. Add it when it is needed and can be acted on for mission critical use. Unintuitively, this approach is likely a priority inversion. - -We should add vNext releases at Preview 1 for the following reasons: - -- vNext is available (in preview form), we so we should advertise it. -- Special once-in-a-release tasks are more likely to fail when done on GA day. -- Adding vNext early enables consumers to cache aggressively. - -The intent is that root `index.json` can be cached aggressively. Adding vNext to `index.json` with Preview 1 is perfectly aligned with that. Adding vNext at GA is not. - -## Graph wormholes - -There are many design paradigms and tradeoffs one can consider with a graph like this. A major design point is the skeletal roots and weighted bottom. That was already been covered. This approach forces significant fetches and inefficiency to get anywhere. To mitigate that, the graph includes a significant number of helpful workflow-specific wormhole links. As the name suggests, these links enable jumps from one part of the graph to another, aligned with expected queries. The wormholes are the most elaborate in the warm branches since they are regularly updated. - -The following are the primary wormhole links. - -Cold roots: - -- `latest` and `latest-lts` -- enables jumping to the matching major version index. -- `latest-year` -- enables jumping to the latest year index. - -Warm branches: - -- `latest` and `latest-security` -- enables jumping to the matching patch version index. -- `latest-month` and `latest-security-month` -- enables jumping to the latest matching month. -- `release-month` -- enables jumping to the month index for a patch version. - -Immutable leaves: - -- `prev` and `prev-security` -- enables jumping to an earlier matching patch version or month index. - -The wormhole links are what make the graph a graph and not just two trees provided alternative views of the same data. They also enable efficient navigation. - -In some cases, it may be better to look at `timeline/2025/index.json` and consider all the security months. In other cases, it may be more efficient to jump to `latest-security-month` and then backwards in time via `prev-security`. Both are possible and legitimate. Note that `prev-security` jumps across year boundaries. - -There are lots of algoriths that [work best when counting backwards](https://www.benjoffe.com/fast-date-64). - -There is no `next` or `next-security`. `prev` and `prev-security` link immutable leaves. It is easy to create a linked-list based on past knowledge. It's not possible to provide `next` links an immutability constraint. - -There is no `latest-sts` link because it's not really useful. `latest` covers it. - -Testing has demonstrated that these wormhole links are one of the defining features of the graph. - -## Version Index Modeling - -The resource modeling witin the graph has to satisfy the intended "gear reduction" mentioned earlier. The key technique is noticing when a design choice forces a faster update schedule than desired or exposes currency that could be misused. This includes the wormhole links, just discussed. - -### Releases index - -Most nodes in the graph are named `index.json`. This is the root [index.json](https://github.com/dotnet/core/blob/release-index/release-notes/index.json) file that represents all .NET versions. It exposes the same general information as the existing `releases-index.json`. It should look similar to the examples shared earlier from the HAL spec. - -```json -{ - "$schema": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/schemas/v1/dotnet-release-version-index.json", - "kind": "releases-index", - "title": ".NET Release Index", - "latest": "10.0", - "latest_lts": "10.0", - "_links": { - "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/index.json", - "title": ".NET Release Index" - }, - "latest": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/index.json", - "title": "Major version index" - }, - "latest-lts": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/index.json", - "title": "Latest LTS" - }, - "timeline-index": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/index.json", - "title": ".NET Release Timeline Index" - }, - }, -``` - -Key points: - -- Schema reference is included -- All links are raw content (eventually will transition to `builds.dotnet.microsoft.com). -- `kind` and `title` describe the resource -- `latest` and `latest_lts` describe high-level resource metadata, often useful currency that helps contextualize the rest of the resource without the need to parse/split strings. For example, the `latest_lts` scalar describes the target of the `latest-lts` link relation. -- `timeline-index` provides a wormhole link to another part of the graph -- Core schema syntax like `latest_lts` uses snake-case-lower for query ergonomics (using `jq` as the proxy for that), while relations like `latest-lts` use kebab-case-lower since they can be names or brands. This follows the approach used by [cve-schema](https://github.com/dotnet/designs/blob/main/accepted/2025/cve-schema/cve_schema.md#brand-names-vs-schema-fields-mixed-naming-strategy). - -The `_embedded` section has one child, `releases`: - -```json - "_embedded": { - "releases": [ - { - "version": "10.0", - "release_type": "lts", - "supported": true, - "_links": { - "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/index.json" - } - } - }, - { - "version": "9.0", - "release_type": "sts", - "supported": true, - "_links": { - "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/index.json" - } - } - }, -``` - -This is where we see the design diverge significantly from `releases-index.json`. There are no patch versions, no statement about security releases. It's the most minimal data to determine the release type, if it is supported, and how to access the canonical resource that exposes richer information. This approach removes the need to update the root index monthly. It's fine for tools to regenerate this file monthly. `git` should not see any diffs. - -### Major version index - -One layer lower, we have the major version idex. The following example is the [major version index for .NET 9](https://github.com/dotnet/core/blob/release-index/release-notes/9.0/index.json). - -```json -{ - "$schema": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/schemas/v1/dotnet-release-version-index.json", - "kind": "major-version-index", - "title": ".NET 9.0 Release Index", - "target_framework": "net9.0", - "latest": "9.0.11", - "latest_security": "9.0.10", - "release_type": "sts", - "support_phase": "active", - "supported": true, - "ga_date": "2024-11-12T00:00:00\u002B00:00", - "eol_date": "2026-11-10T00:00:00\u002B00:00", - "_links": { - "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/index.json" - }, - "downloads": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/downloads/index.json", - "title": ".NET 9.0 Downloads" - }, - "latest": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.11/index.json", - "title": "Latest patch" - }, - "latest-sdk": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/sdk/index.json", - "title": ".NET SDK 9.0 Release Information" - }, - "latest-security": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.10/index.json", - "title": "Latest security patch" - }, - "release-manifest": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/manifest.json", - "title": "Release manifest" - }, - "releases-index": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/index.json", - "title": "Release index" - } - }, -``` - -This index includes much more useful and detailed information, both metadata/currency and patch-version links. It starts to answer the question of "what should I care about _now_?". - -Much of the form is similar to the root index. Instead of `latest_lts`, there is `latest_security`. A new addition is `release-manifest`. That relation stores important but lower value content about a given major release. That will be covered shortly. - -The `_embeeded` section has two children: `releases` and `years`. - -```json - "_embedded": { - "patches": [ - { - "version": "9.0.11", - "release": "9.0", - "date": "2025-11-19T00:00:00\u002B00:00", - "year": "2025", - "month": "11", - "security": false, - "cve_count": 0, - "support_phase": "active", - "sdk_version": "9.0.308", - "_links": { - "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.11/index.json" - }, - "release-month": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/11/index.json", - "title": "Release month index" - } - } - }, - { - "version": "9.0.10", - "release": "9.0", - "date": "2025-10-14T00:00:00\u002B00:00", - "year": "2025", - "month": "10", - "security": true, - "cve_count": 3, - "cve_records": [ - "CVE-2025-55247", - "CVE-2025-55248", - "CVE-2025-55315" - ], - "support_phase": "active", - "sdk_version": "9.0.306", - "_links": { - "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.10/index.json" - }, - "release-month": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/index.json", - "title": "Release month index" - }, - "cve-json": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/cve.json", - "title": "CVE records (JSON)", - "type": "application/json" - } - } - }, -``` - -and years: - -```json - "years": [ - { - "year": "2025", - "_links": { - "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/index.json" - } - } - }, - { - "year": "2024", - "_links": { - "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2024/index.json" - } - } - } - ] -``` - -The `patches` object contains detailed information that can drive deployment and compliance workflows. The first two link relations, `self` and `release-month` are HAL links while `cve-json` is a plain JSON link. Most non-HAL links end in the given format, like `json` or `markdown` or `markdown-rendered`. The links are raw text by default, with `-rendered` HTML content being useful for content targeted for human consumption, for example in generated release notes. - -As mentioned earlier, the design has a concept of "wormhole links". That's what we see with `release-month`. It provides direct access to a high-relevance (potentially graph-distant) resource that would otherwise require awkward indirections, multiple network hops, and wasted bytes/tokens to acquire. These wormhole links massively improve query ergonomics for sophisticated queries. - -There is a link `cve.json` file. Our [CVE schema](https://github.com/dotnet/designs/blob/main/accepted/2025/cve-schema/cve_schema.md) is a custom schema with no HAL vocabulary. It's an exit node of the graph. The point is that we're free to describe complex domains, like CVE disclosures, using a clean-slate design methodology. One can also see that some of the `cve.json` information has been projected into the graph, adding high-value shape over the skeleton. - -The `year` property is effectively a baked in pre-query that directs further exploration if the timeline is of interest for this major releases. The `cve_records` property lists all the CVEs for the month, another pre-query baked in. - -As stated, there is a lot more useful detailed currency on offer. However, there is a rule that currency needs to be guaranteed consistent. Let's consider if the rule is obeyed. The important characteristic is that listed versions and links _within_ the resource are consistent by virtue of being _captured_ in the same file. - -The critical trick is with the links. In the case of the `release-month` link, the link origin is a fast moving resource (warm branch) while the link target is immutable. That combination works. It's easy to be consistent with something immutable. It will either exist or not. In contrast, there would be a problem if there was a link between two mutable resources that expose the same currency. This is the problem that `releases-index.json` has. - -Back to `manifest.json`. It contains extra data that tools in particular might find useful. The following example is the `manifest.json` file for .NET 9. - -```json -{ - "kind": "manifest", - "title": ".NET 9.0 Manifest", - "version": "9.0", - "label": ".NET 9.0", - "target_framework": "net9.0", - "release_type": "sts", - "support_phase": "active", - "supported": true, - "ga_date": "2024-11-12T00:00:00+00:00", - "eol_date": "2026-11-10T00:00:00+00:00", - "_links": { - "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/manifest.json" - }, - "compatibility": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/compatibility.json", - "title": "Compatibility", - "type": "application/json" - }, - "compatibility-rendered": { - "href": "https://learn.microsoft.com/dotnet/core/compatibility/9.0", - "title": "Breaking changes in .NET 9", - "type": "text/html" - }, - "downloads-rendered": { - "href": "https://dotnet.microsoft.com/download/dotnet/9.0", - "title": ".NET 9 Downloads", - "type": "text/html" - }, - "os-packages-json": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/os-packages.json", - "title": "OS Packages", - "type": "application/json" - }, - "release-blog-rendered": { - "href": "https://devblogs.microsoft.com/dotnet/announcing-dotnet-9/", - "title": "Announcing .NET 9", - "type": "text/html" - }, - "supported-os-json": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/supported-os.json", - "title": "Supported OSes", - "type": "application/json" - }, - "supported-os-markdown": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/supported-os.md", - "title": "Supported OSes", - "type": "application/markdown" - }, - "supported-os-markdown-rendered": { - "href": "https://github.com/dotnet/core/blob/main/release-notes/9.0/supported-os.md", - "title": "Supported OSes (Rendered)", - "type": "text/html" - }, - "target-frameworks": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/target-frameworks.json", - "title": "Target Frameworks", - "type": "application/json" - }, - "usage-markdown-rendered": { - "href": "https://github.com/dotnet/core/blob/main/release-notes/9.0/README.md", - "title": "Release Notes (Rendered)", - "type": "text/html" - }, - "whats-new-rendered": { - "href": "https://learn.microsoft.com/dotnet/core/whats-new/dotnet-9/overview", - "title": "What\u0027s new in .NET 9", - "type": "text/html" - } - } -} -``` - -This is a dictionary of links with some useful metadata. The relations are in alphabetical order, after `self`. - -Some of the information in this file is sourced from a human-curated `_manifest.json`. This file is used by the graph generation tools, not the graph itself. It provides a path to seeding the graph with data not available elsewhere. - -.NET 9 `_manifest.json`: - -```json -{ - "kind": "manifest", - "title": ".NET 9.0 Manifest", - "version": "9.0", - "label": ".NET 9.0", - "target_framework": "net9.0", - "release_type": "sts", - "phase": "active", - "ga_date": "2024-11-12T00:00:00Z", - "eol_date": "2026-11-10T00:00:00Z", - "_links": { - "downloads-rendered": { - "href": "https://dotnet.microsoft.com/download/dotnet/9.0", - "title": ".NET 9 Downloads", - "type": "text/html" - }, - "whats-new-rendered": { - "href": "https://learn.microsoft.com/dotnet/core/whats-new/dotnet-9/overview", - "title": "What's new in .NET 9", - "type": "text/html" - }, - "compatibility-rendered": { - "href": "https://learn.microsoft.com/dotnet/core/compatibility/9.0", - "title": "Breaking changes in .NET 9", - "type": "text/html" - }, - "release-blog-rendered": { - "href": "https://devblogs.microsoft.com/dotnet/announcing-dotnet-9/", - "title": "Announcing .NET 9", - "type": "text/html" - } - } -} -``` - -These links are free form and can be anything. They follow the same scheme as the links used elsewhere in the graph. - -### Patch Version Index - -The following example is a patch version index, for [9.0.10](https://github.com/dotnet/core/blob/release-index/release-notes/9.0/9.0.10/index.json). - -```json -{ - "$schema": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/schemas/v1/dotnet-patch-detail-index.json", - "kind": "patch-version-index", - "title": ".NET 9.0.10 Patch Index", - "version": "9.0.10", - "date": "2025-10-14T00:00:00\u002B00:00", - "support_phase": "active", - "security": true, - "cve_count": 3, - "cve_records": [ - "CVE-2025-55247", - "CVE-2025-55248", - "CVE-2025-55315" - ], - "sdk_version": "9.0.306", - "sdk_feature_bands": [ - "9.0.306", - "9.0.111" - ], - "_links": { - "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.10/index.json" - }, - "prev": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.9/index.json", - "title": "Patch index" - }, - "prev-security": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.6/index.json", - "title": "Latest security patch" - }, - "latest-sdk": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/sdk/index.json", - "title": "SDK index" - }, - "release-major": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/index.json", - "title": "Major version index" - }, - "release-month": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/index.json", - "title": "Release month index" - }, - "releases-index": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/index.json", - "title": "Release index" - }, - "cve-json": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/cve.json", - "title": "CVE records (JSON)", - "type": "application/json" - }, - "cve-markdown": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/cve.md", - "title": "CVE records (JSON)", - "type": "application/markdown" - }, - "cve-markdown-rendered": { - "href": "https://github.com/dotnet/core/blob/main/release-notes/timeline/2025/10/cve.md", - "title": "CVE records (JSON) (Rendered)", - "type": "application/markdown" - }, - "release-json": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.10/release.json", - "title": "9.0.10 Release Information", - "type": "application/json" - }, - "release-notes-markdown": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.10/9.0.10.md", - "title": "Release Notes", - "type": "application/markdown" - }, - "release-notes-markdown-rendered": { - "href": "https://github.com/dotnet/core/blob/main/release-notes/9.0/9.0.10/9.0.10.md", - "title": "Release Notes (Rendered)", - "type": "application/markdown" - } - }, -``` - -This content looks much the same as we saw earlier, except that much of the content we saw in the patch object is now exposed at index root. That's not coincidental, but a key aspect of the model. - -The `prev` link relation provides another wormhole, this time to a less distant target. A `next` relation isn't provided because it would break the immutability goal. In addition, the combination of a `latest*` property and `prev` links satisfies many scenarios. - -The `latest-sdk` target provides access to `aka.ms` evergreen SDK links and other SDK-related information. The `release-month` and `cve-json` links are still there, but a bit further down the dictionary definition as to what's copied above. - -The `_embedded` property contains two children: `sdk` and `disclosures`. - -```json - "_embedded": { - "sdk": [ - { - "version": "9.0.306", - "_links": { - "feature-band": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/sdk/sdk-9.0.3xx.json", - "path": "/9.0/sdk/sdk-9.0.3xx.json", - "title": ".NET SDK 9.0.3xx", - "type": "application/json" - }, - "release-notes-markdown": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.10/9.0.10.md", - "path": "/9.0/9.0.10/9.0.10.md", - "title": "9.0.10 Release Notes", - "type": "application/markdown" - }, - "release-notes-markdown-rendered": { - "href": "https://github.com/dotnet/core/blob/main/release-notes/9.0/9.0.10/9.0.10.md", - "path": "/9.0/9.0.10/9.0.10.md", - "title": "9.0.10 Release Notes (Rendered)", - "type": "application/markdown" - } - } - }, - { - "version": "9.0.111", - "_links": { - "feature-band": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/sdk/sdk-9.0.1xx.json", - "path": "/9.0/sdk/sdk-9.0.1xx.json", - "title": ".NET SDK 9.0.1xx", - "type": "application/json" - }, - "release-notes-markdown": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.10/9.0.111.md", - "path": "/9.0/9.0.10/9.0.111.md", - "title": "SDK 9.0.111 Release Notes", - "type": "application/markdown" - }, - "release-notes-markdown-rendered": { - "href": "https://github.com/dotnet/core/blob/main/release-notes/9.0/9.0.10/9.0.111.md", - "path": "/9.0/9.0.10/9.0.111.md", - "title": "SDK 9.0.111 Release Notes (Rendered)", - "type": "application/markdown" - } - } - } - ], - "disclosures": [ - { - "id": "CVE-2025-55247", - "title": ".NET Denial of Service Vulnerability", - "_links": { - "self": { - "href": "https://github.com/dotnet/announcements/issues/370", - "title": "CVE-2025-55247" - } - }, - "cvss_score": 7.3, - "cvss_severity": "HIGH", - "disclosure_date": "2025-10-14", - "affected_releases": [ - "8.0", - "9.0" - ], - "affected_products": [ - "dotnet-sdk" - ], - "platforms": [ - "linux" - ] - }, - { - "id": "CVE-2025-55248", - "title": ".NET Information Disclosure Vulnerability", - "_links": { - "self": { - "href": "https://github.com/dotnet/announcements/issues/372", - "title": "CVE-2025-55248" - } - }, - "fixes": [ - { - "href": "https://github.com/dotnet/runtime/commit/18e28d767acf44208afa6c4e2e67a10c65e9647e.diff", - "repo": "dotnet/runtime", - "branch": "release/9.0", - "title": "Fix commit in runtime (release/9.0)", - "release": "9.0" - } - ], - "cvss_score": 4.8, - "cvss_severity": "MEDIUM", - "disclosure_date": "2025-10-14", - "affected_releases": [ - "8.0", - "9.0" - ], - "affected_products": [ - "dotnet-runtime" - ], - "platforms": [ - "all" - ] - }, -``` - -Note: `sdks` should probably be plural. - -First-class treatment is provided for SDK releaes, both a root and in `_embedded`. That said, it is obvious that we don't quite do the right thing with release notes. It is very odd that the "best" SDK has to share release notes with the runtime. - -There is an `sdk*.json` that matches each feature band. It is largely the same as `sdk/index.json` but more specific. The rest of the links are for markdown release notes. - -Note: There is currently no runtime variant of `sdk/index.json`. This needs to be resolved and may inspire changes in the SDK design. The SDK design is still a bit shaky. - -Any CVEs for the month are described in `disclosures`. This data provides a useful pre-baked view on data that from `cve.json`. - -It's possible to make detail-oriented compliance and deployment decisions based on this information. There's even a commit for the CVE fix with an LLM friendly link style. This is the bottom part of the hypermedia graph. It's far more shapely and weighty than the root. If a consumer gets this far, it is likely because they need access to the exposed information. If they only want access to the `cve.json` file, it is exposed in the major version index. - -## Timeline Modeling - -The timeline is much the same. The key difference is that the version index converges to a point while the timeline index converges to a slice or row of points. - -### Timeline Index - -The root of the [timeline index](https://github.com/dotnet/core/blob/release-index/release-notes/timeline/index.json) is almost identical to the releases index, with `timeline-index` being inverted into `releases-index`. - -```json -{ - "$schema": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/schemas/dotnet-release-timeline-index.json", - "kind": "timeline-index", - "title": ".NET Release Timeline Index", - "description": ".NET Release Timeline (latest: 10.0)", - "latest": "10.0", - "latest_lts": "10.0", - "latest_year": "2025", - "_links": { - "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/index.json", - "path": "/timeline/index.json", - "title": ".NET Release Timeline Index", - "type": "application/hal\u002Bjson" - }, - "latest": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/index.json", - "path": "/10.0/index.json", - "title": "Latest .NET release (.NET 10.0)", - "type": "application/hal\u002Bjson" - }, - "latest-lts": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/index.json", - "path": "/10.0/index.json", - "title": "Latest LTS release (.NET 10.0)", - "type": "application/hal\u002Bjson" - }, - "latest-year": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/index.json", - "path": "/timeline/2025/index.json", - "title": "Latest year (2025)", - "type": "application/hal\u002Bjson" - }, - "releases-index": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/index.json", - "path": "/index.json", - "title": ".NET Release Index", - "type": "application/hal\u002Bjson" - } - }, -``` - -The `_embedded` section naturally contains `years`. - -```json - "_embedded": { - "years": [ - { - "year": "2025", - "description": ".NET release timeline for 2025", - "releases": [ - "10.0", - "9.0", - "8.0" - ], - "_links": { - "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/index.json", - "path": "/timeline/2025/index.json", - "title": "Release timeline index for 2025", - "type": "application/hal\u002Bjson" - } - } - }, - { - "year": "2024", - "description": ".NET release timeline for 2024", - "releases": [ - "9.0", - "8.0", - "7.0", - "6.0" - ], - "_links": { - "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2024/index.json", - "path": "/timeline/2024/index.json", - "title": "Release timeline index for 2024", - "type": "application/hal\u002Bjson" - } - } - }, -``` - -It also provides a helpful join with the active (not neccessarily supported) releases for that year. This baked-in query helps some workflows. - -This index file similarly avoid fast-moving currency as the root releases index. - -### Year Index - -The year index follows much the same pattern as the major version index. The year objects you see above become the root of the index. This is [year index for 2025](https://github.com/dotnet/core/blob/release-index/release-notes/timeline/2025/index.json). - -```json -{ - "$schema": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/schemas/dotnet-release-timeline-index.json", - "kind": "year-index", - "title": ".NET Release Timeline Index - 2025", - "description": "Release timeline for 2025 (latest: 10.0)", - "year": "2025", - "latest_month": "11", - "latest_security_month": "10", - "latest_release": "10.0", - "releases": [ - "10.0", - "9.0", - "8.0" - ], - "_links": { - "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/index.json", - "path": "/timeline/2025/index.json", - "title": "Release timeline index for 2025", - "type": "application/hal\u002Bjson" - }, - "prev": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2024/index.json", - "path": "/timeline/2024/index.json", - "title": "Release timeline index for 2024", - "type": "application/hal\u002Bjson" - }, - "latest-month": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/11/index.json", - "path": "/timeline/2025/11/index.json", - "title": "Latest month (Release timeline index for 2025-11)", - "type": "application/hal\u002Bjson" - }, - "latest-release": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/index.json", - "path": "/10.0/index.json", - "title": "Latest release (.NET 10.0)", - "type": "application/hal\u002Bjson" - }, - "latest-security-month": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/index.json", - "path": "/timeline/2025/10/index.json", - "title": "Latest security month (Release timeline index for 2025-10)", - "type": "application/hal\u002Bjson" - }, - "timeline-index": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/index.json", - "path": "/timeline/index.json", - "title": ".NET Release Timeline Index", - "type": "application/hal\u002Bjson" - } - }, -``` - -Very similar approach as other indices. - -The `_emdedded` section contains: `months` and `releases`. - -```json - "_embedded": { - "months": [ - { - "month": "11", - "security": false, - "cve_count": 0, - "latest_release": "10.0", - "releases": [ - "10.0", - "9.0", - "8.0" - ], - "runtime_patches": [ - "10.0.0", - "9.0.11", - "8.0.22" - ], - "_links": { - "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/11/index.json", - "path": "/timeline/2025/11/index.json", - "title": "Release timeline index for 2025-11", - "type": "application/hal\u002Bjson" - } - } - }, - { - "month": "10", - "security": true, - "cve_count": 3, - "cve_records": [ - "CVE-2025-55248", - "CVE-2025-55315", - "CVE-2025-55247" - ], - "latest_release": "9.0", - "releases": [ - "10.0", - "9.0", - "8.0" - ], - "runtime_patches": [ - "10.0.0-rc.2.25502.107", - "9.0.10", - "8.0.21" - ], - "_links": { - "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/index.json", - "path": "/timeline/2025/10/index.json", - "title": "Release timeline index for 2025-10", - "type": "application/hal\u002Bjson" - }, - "cve-json": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/cve.json", - "path": "/timeline/2025/10/cve.json", - "title": "CVE Information", - "type": "application/json" - } - } - }, -``` - -`releases` looks like the following: - -```json - "releases": [ - { - "version": "10.0", - "release_type": "lts", - "support_phase": "active", - "supported": true, - "ga_date": "2025-11-11T00:00:00\u002B00:00", - "eol_date": "2028-11-14T00:00:00\u002B00:00", - "_links": { - "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/index.json", - "path": "/10.0/index.json", - "title": ".NET 10.0", - "type": "application/hal\u002Bjson" - }, - "latest-month": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/11/index.json", - "path": "/timeline/2025/11/index.json", - "title": "Latest month (2025-11)", - "type": "application/hal\u002Bjson" - }, - "latest-patch": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/10.0.0/index.json", - "path": "/10.0/10.0.0/index.json", - "title": "Latest patch (10.0.0)", - "type": "application/hal\u002Bjson" - } - } - }, -``` - -This is just an inversion on the major version index. - -For light-duty compliance tools, the year index likely provides sufficient information. - -### Month index - -The last index to consider is the month index. This is the month index for [January 2025](https://github.com/dotnet/core/blob/release-index/release-notes/timeline/2025/01/index.json). - -```json -{ - "$schema": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/schemas/dotnet-release-timeline-index.json", - "kind": "month-index", - "title": ".NET Release Timeline Index - 2025-01", - "description": "Release timeline for 2025-01 (latest: 9.0)", - "year": "2025", - "month": "01", - "security": true, - "cve_count": 4, - "cve_records": [ - "CVE-2025-21171", - "CVE-2025-21172", - "CVE-2025-21176", - "CVE-2025-21173" - ], - "latest_release": "9.0", - "releases": [ - "9.0", - "8.0" - ], - "_links": { - "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/01/index.json", - "path": "/timeline/2025/01/index.json", - "title": "Release timeline index for 2025-01", - "type": "application/hal\u002Bjson" - }, - "prev": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2024/12/index.json", - "path": "/timeline/2024/12/index.json", - "title": "Release timeline index for 2024-12", - "type": "application/hal\u002Bjson" - }, - "timeline-index": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/index.json", - "path": "/timeline/index.json", - "title": ".NET Release Timeline Index", - "type": "application/hal\u002Bjson" - }, - "year-index": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/index.json", - "path": "/timeline/2025/index.json", - "title": ".NET Release Timeline Index - 2025", - "type": "application/hal\u002Bjson" - }, - "cve-json": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/01/cve.json", - "path": "/timeline/2025/01/cve.json", - "title": "CVE Information", - "type": "application/json" - }, - "cve-markdown": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/01/cve.md", - "path": "/timeline/2025/01/cve.md", - "title": "CVE Information", - "type": "application/markdown" - }, - "cve-markdown-rendered": { - "href": "https://github.com/dotnet/core/blob/main/release-notes/timeline/2025/01/cve.md", - "path": "/timeline/2025/01/cve.md", - "title": "CVE Information (Rendered)", - "type": "application/markdown" - } - }, -``` - -This schema also follows the same approach we've seen elsewhere. We also see `prev` wormhold links show up. They cross years, as can be seen in this example. This wormhole links makes backwards `foreach` from `latest-month` trivial. - -The `_embedded` property contains: `releases` and `disclosures`. - -```json - "_embedded": { - "releases": [ - { - "version": "9.0.1", - "date": "2025-01-14T00:00:00\u002B00:00", - "year": "2025", - "month": "01", - "security": true, - "cve_count": 4, - "cve_records": [ - "CVE-2025-21171", - "CVE-2025-21172", - "CVE-2025-21176", - "CVE-2025-21173" - ], - "support_phase": "active", - "sdk_patches": [ - "9.0.102" - ], - "_links": { - "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/9.0.1/index.json", - "path": "/9.0/9.0.1/index.json", - "title": ".NET 9.0.1", - "type": "application/hal\u002Bjson" - }, - "latest-sdk": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/9.0/sdk/index.json", - "path": "/9.0/sdk/index.json", - "title": ".NET SDK 9.0 Release Information", - "type": "application/hal\u002Bjson" - } - } - }, - { - "version": "8.0.12", - "date": "2025-01-14T00:00:00\u002B00:00", - "year": "2025", - "month": "01", - "security": true, - "cve_count": 3, - "cve_records": [ - "CVE-2025-21172", - "CVE-2025-21176", - "CVE-2025-21173" - ], - "support_phase": "active", - "sdk_patches": [ - "8.0.405", - "8.0.308", - "8.0.112" - ], - "_links": { - "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/8.0/8.0.12/index.json", - "path": "/8.0/8.0.12/index.json", - "title": ".NET 8.0.12", - "type": "application/hal\u002Bjson" - }, - "latest-sdk": { - "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/8.0/sdk/index.json", - "path": "/8.0/sdk/index.json", - "title": ".NET SDK 8.0 Release Information", - "type": "application/hal\u002Bjson" - } - } - } - ], - "disclosures": [ - { - "id": "CVE-2025-21171", - "title": ".NET Remote Code Execution Vulnerability", - "_links": { - "self": { - "href": "https://github.com/dotnet/announcements/issues/340", - "title": "CVE-2025-21171" - } - }, - "fixes": [ - { - "href": "https://github.com/dotnet/runtime/commit/9da8c6a4a6ea03054e776275d3fd5c752897842e.diff", - "repo": "dotnet/runtime", - "branch": "release/9.0", - "title": "Fix commit in runtime (release/9.0)", - "release": "9.0" - } - ], - "cvss_score": 7.5, - "cvss_severity": "HIGH", - "disclosure_date": "2025-01-14", - "affected_releases": [ - "9.0" - ], - "affected_products": [ - "dotnet-runtime" - ], - "platforms": [ - "all" - ] - }, -``` - -It was stated earlier that the version indexes converges to a point while the timeline index coverges to a row of points. We see that on display here. Otherwise, this is a variation of what we saw in the patch version index. - -## Design tradeoffs - -There are lots of design tradeoffs within the graph design. Ergonomics vs update velocity were perhaps the most challenging constraints to balance. - -As mentioned multiple times, graph consistency is a major design requirement. The primary consideration is avoiding exposing currency that can be misused. If that's avoided, then there are no concerns with CDN consistency. - -If we cleverly apply these rules, we can actually expand these major version objects in our root releases index with convenience links, like the following: - -```json - "_embedded": { - "releases": [ - { - "version": "10.0", - "release_type": "lts", - "supported": true, - "eol_date": "2028-11-14T00:00:00\u002B00:00", - "_links": { - "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/index.json", - "title": ".NET 10.0", - "type": "application/hal\u002Bjson" - }, - "latest-patch": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/10.0.1/index.json", - "title": ".NET 10.0.1", - "type": "application/hal\u002Bjson" - }, - "release-month": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/11/index.json", - "path": "/timeline/2025/11/index.json", - "title": "Release timeline index for 2025-11", - "type": "application/hal\u002Bjson" - } - } - }, - { - "version": "9.0", - "release_type": "sts", - "supported": true, - "eol_date": "2026-11-10T00:00:00\u002B00:00", - "_links": { - "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/index.json", - "title": ".NET 9.0", - "type": "application/hal\u002Bjson" - }, - "latest-patch": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/9.0.9/index.json", - "title": ".NET 9.0.9", - "type": "application/hal\u002Bjson" - }, - "release-month": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/11/index.json", - "path": "/timeline/2025/11/index.json", - "title": "Release timeline index for 2025-11", - "type": "application/hal\u002Bjson" - } - } - }, -``` - -That would be _so nice_. Consumers could wormhole to high value content without needing to go through the major version index or or year to get to the intended target. From a query ergonomics standpoint, this structure would be superior. From a file-size standpoint, it would be acceptable. - -This approach doesn't violate the consistency rules. There is no badly-behaved currency that can be mis-used. The links are opague and notably target immutable resources. So, why not? Why can't we have nice things? - -The issue is that these high-value links would require updating the root index once a month. Regular updates of a high-value resource signficantly increase the likelihood of an outage and reduces the time that the root index can be cached. Cache aggressiveness is part of the performance equation. It's much better to keep the root lean (skeletal) and highly cacheable. - -On aspect that has (somewhat) haunted the design is deciding if it is appropriate to add a preview version to the high-value index for something as NOT mission critical as .NET 11 preview 1 (for example). On one hand, the answer is "definitely not". On another, by adding an `11.0` link in February, it is much more likely that all caches will have a root `index.json` with an 11.0 link, added in February, by November. We have to add the new major version sometime. Might as well add it at first availability, avoid a "has to be right" change on GA day, and ensure that all caches have the data they need when the special day comes. - -There has been significant discussion on consistency. We might as well complete the lesson. - -The following example, with `latest_patch`, is what would cause the worst pain. - -```json - "_embedded": { - "releases": [ - { - "version": "10.0", - "release_type": "lts", - "supported": true, - "eol_date": "2028-11-14T00:00:00\u002B00:00", - "latest_patch": "10.0.1", - "_links": { - "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/index.json", - "title": ".NET 10.0", - "type": "application/hal\u002Bjson" - }, - "latest-patch": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/10.0.1/index.json", - "title": ".NET 10.0.1", - "type": "application/hal\u002Bjson" - }, - "release-month": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/11/index.json", - "path": "/timeline/2025/11/index.json", - "title": "Release timeline index for 2025-11", - "type": "application/hal\u002Bjson" - } - } - }, - { - "version": "9.0", - "release_type": "sts", - "supported": true, - "eol_date": "2026-11-10T00:00:00\u002B00:00", - "latest_patch": "9.0.9", - "_links": { - "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/index.json", - "title": ".NET 9.0", - "type": "application/hal\u002Bjson" - }, - "latest-patch": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/9.0.9/index.json", - "title": ".NET 9.0.9", - "type": "application/hal\u002Bjson" - }, - "release-month": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/11/index.json", - "path": "/timeline/2025/11/index.json", - "title": "Release timeline index for 2025-11", - "type": "application/hal\u002Bjson" - } - } - }, -``` - -The `latest_patch` property is the "fast-moving currency" that the design attempts to avoid. A consumer can now take the `10.0.1` value and try it on for size in `10.0/index.json`. It might not fit. That's the exact problem we have in `release-index.json` today. Let's not recreate it. - -Another classic movie -- A Few Good Men (1992): - -> Judge Randolph: You don't have to answer that question! -> Jessup: I'll answer the question. You want answers? -> Kaffee: I think I'm entitled to it! -> Jessup: You want answers?! -> Kaffee: I WANT THE TRUTH! -> Jessup: You can't handle the truth! - -This provides perfect clarity on why we cannot include the `latest_patch` propery, even if we might feel entitled to it. - -> "I don't give a DAMN what you think you are entitled to!" - -Source: A Few Good Men (1992; 10s later) - -That would seems to close the book on convenience. - -## Attached data - -> These dimensional characteristics form the load-bearing structure of the graph to which everything else is attached. - -This leaves the question of which data we could attach. - -The following are all in scope to include: - -- Breaking changes (already included) -- CVE disclosures (already included) -- Servicing fixes and commits (beyond CVEs) -- Known issues -- Supported OSes -- Linux package dependencies -- Download links + hashes (partially included) - -Non goals: - -- Preview release details at the same fidelity as GA -- Performance benchmark data - -## Modeling as validation - -As the final graph took shape, distinct relationships inherent to the resource modeling started to emerge. - -- Parent <-> child -- Represents a shift in information depth, scalar <-> vector. The object data (within `_embedded`) in the parent becomes the root metadata in the child, and more complex children appear than were in the parent. -- Version <-> timeline -- Represents an inversion of information, using a different key--temporal or version--to access the same information. The version index converges to a point while the timeline index converges to a slice or row of points. - -This reflection on resource modeling is similar to the mathematic concept of [duals](https://en.wikipedia.org/wiki/Duality_(mathematics)). The elements of the graph are not duals, however, the time and version keys likely are. - -We can also reason about shape in terms of a storage analogy. - -Storage for a 3-level hypermedia design: - -- Root/outer nodes: flat scalars or tuples -- filter operation -- Middle nodes: nested documents -- traverse operation -- Exit nodes: indexed documents -- query operation - -This analogy is attempting to demonstrate the kind of data exposed at each level and the most sophisticated operation that the node can satisfy. The term "document" is intended to align with "document" in "document database". - -These formal descriptions may not help everyone, however, it was used as part of the design process. It can helpful to consider the inherent nature of the data to validate the shape once it is concretely modeled. One can consider the release notes data from one point in the graph to another and pre-conceptualize what it should be accroding to these transformation rules. If it matches, it is likely correct. That's an important approach that was used for graph validation. - -## Quality metrics - -Query metrics were assessed with the earlier CVE schema project. This time, we can do that an also reason about network cost. - -See [metrics.md](./metrics.md) for an in-depth analysis. A few representative tests are included in this document. - -### Query: "What .NET versions are currently supported?" - -| Schema | Files Required | Total Transfer | -|--------|----------------|----------------| -| hal-index | `index.json` | **8 KB** | -| releases-index | `releases-index.json` | **6 KB** | - -**hal-index:** - -```bash -ROOT="https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" - -curl -s "$ROOT" | jq -r '._embedded.releases[] | select(.supported) | .version' -# 10.0 -# 9.0 -# 8.0 -``` - -**releases-index:** - -```bash -ROOT="https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json" - -curl -s "$ROOT" | jq -r '.["releases-index"][] | select(.["support-phase"] == "active") | .["channel-version"]' -# 10.0 -# 9.0 -# 8.0 -``` - -**Analysis:** - -- **Completeness:** ✅ Equal—both return the same list of supported versions. -- **Boolean vs enum:** The hal-index uses `supported: true`, a simple boolean. The releases-index pnly exposes `support-phase: "active"` (with hal-index also has), requiring knowledge of the enum vocabulary (active, maintenance, eol, preview, go-live). -- **Property naming:** The hal-index uses `select(.supported)` with dot notation. The releases-index requires `select(.["support-phase"] == "active")` with bracket notation and string comparison. -- **Query complexity:** The hal-index query is 30% shorter and more intuitive for someone unfamiliar with the schema. - -**Winner:** releases-index (**1.3x smaller** for basic version queries, but hal-index has better query ergonomics) - -### CVE Queries for Latest Security Patch - -#### Query: "What CVEs were fixed in the latest .NET 8.0 security patch?" - -| Schema | Files Required | Total Transfer | -|--------|----------------|----------------| -| hal-index | `index.json` → `8.0/index.json` → `8.0/8.0.21/index.json` | **52 KB** | -| releases-index | `releases-index.json` + `8.0/releases.json` | **1,239 KB** | - -**hal-index:** - -```bash -ROOT="https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" - -# Step 1: Get the 8.0 version href -VERSION_HREF=$(curl -s "$ROOT" | jq -r '._embedded.releases[] | select(.version == "8.0") | ._links.self.href') -# https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/8.0/index.json - -# Step 2: Get the latest security patch href -PATCH_HREF=$(curl -s "$VERSION_HREF" | jq -r '._links["latest-security"].href') -# https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/8.0/8.0.21/index.json - -# Step 3: Get the CVE records -curl -s "$PATCH_HREF" | jq -r '.cve_records[]' -# CVE-2025-55247 -# CVE-2025-55248 -# CVE-2025-55315 -``` - -**releases-index:** - -```bash -ROOT="https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json" - -# Step 1: Get the 8.0 releases.json URL -RELEASES_URL=$(curl -s "$ROOT" | jq -r '.["releases-index"][] | select(.["channel-version"] == "8.0") | .["releases.json"]') -# https://builds.dotnet.microsoft.com/dotnet/release-metadata/8.0/releases.json - -# Step 2: Find latest security release and get CVE IDs -curl -s "$RELEASES_URL" | jq -r '[.releases[] | select(.security == true)] | .[0] | .["cve-list"][] | .["cve-id"]' -# CVE-2025-55247 -# CVE-2025-55315 -# CVE-2025-55248 -``` - -**Analysis:** Both schemas produce the same CVE IDs. However: - -- **Completeness:** ✅ Equal—both return the CVE identifiers -- **Ergonomics:** The releases-index requires downloading a 1.2 MB file to extract 3 CVE IDs. The hal-index uses a dedicated `latest-security` link, avoiding iteration through all releases. -- **Link syntax:** Counterintuitively, the deeper HAL structure `._links.self.href` is more ergonomic than `.["releases.json"]` because snake_case enables dot notation throughout. The releases-index embeds URLs directly in properties, but kebab-case naming forces bracket notation. -- **Data efficiency:** hal-index is 23x smaller - -**Winner:** hal-index (**23x smaller**) - -### Query: "List all CVEs fixed in the last 12 months" - -| Schema | Files Required | Total Transfer | -|--------|----------------|----------------| -| hal-index | `timeline/index.json` → up to 12 month indexes (via `prev` links) | **~90 KB** | -| releases-index | All version releases.json files | **2.4+ MB** | - -**hal-index:** - -```bash -TIMELINE="https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/index.json" - -# Step 1: Get the latest month href -MONTH_HREF=$(curl -s "$TIMELINE" | jq -r '._embedded.years[0]._links["latest-month"].href') - -# Step 2: Walk back 12 months using prev links, collecting security CVEs -for i in {1..12}; do - DATA=$(curl -s "$MONTH_HREF") - YEAR_MONTH=$(echo "$DATA" | jq -r '"\(.year)-\(.month)"') - SECURITY=$(echo "$DATA" | jq -r '.security') - if [ "$SECURITY" = "true" ]; then - CVES=$(echo "$DATA" | jq -r '[._embedded.disclosures[].id] | join(", ")') - echo "$YEAR_MONTH: $CVES" - fi - MONTH_HREF=$(echo "$DATA" | jq -r '._links.prev.href // empty') - [ -z "$MONTH_HREF" ] && break -done -# 2025-10: CVE-2025-55248, CVE-2025-55315, CVE-2025-55247 -# 2025-06: CVE-2025-30399 -# 2025-05: CVE-2025-26646 -# 2025-04: CVE-2025-26682 -# 2025-03: CVE-2025-24070 -# 2025-01: CVE-2025-21171, CVE-2025-21172, CVE-2025-21176, CVE-2025-21173 -``` - -**releases-index:** - -```bash -ROOT="https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json" - -# Get all supported version releases.json URLs -URLS=$(curl -s "$ROOT" | jq -r '.["releases-index"][] | select(.["support-phase"] == "active") | .["releases.json"]') - -# For each version, find security releases in the last 12 months -CUTOFF="2024-12-01" -for URL in $URLS; do - curl -s "$URL" | jq -r --arg cutoff "$CUTOFF" ' - .releases[] | - select(.security == true) | - select(.["release-date"] >= $cutoff) | - "\(.["release-date"]): \([.["cve-list"][]? | .["cve-id"]] | join(", "))"' -done | sort -u | sort -r -# 2025-10-14: CVE-2025-55247, CVE-2025-55315, CVE-2025-55248 -# 2025-06-10: CVE-2025-30399 -# 2025-05-22: CVE-2025-26646 -# 2025-04-08: CVE-2025-26682 -# 2025-03-11: CVE-2025-24070 -# 2025-01-14: CVE-2025-21172, CVE-2025-21173, CVE-2025-21176 -``` - -**Analysis:** - -- **Completeness:** ⚠️ Partial—the releases-index can list CVEs by date, but notice CVE-2025-21171 is missing (it only affected .NET 9.0 which was still in its first patch cycle). The output also shows exact dates rather than grouped by month. -- **Ergonomics:** The hal-index uses `prev` links for natural backward navigation. The releases-index requires downloading all version files (2.4+ MB), filtering by date, and deduplicating results. -- **Navigation model:** The hal-index timeline is designed for chronological traversal. The releases-index has no concept of time-based navigation. - -**Winner:** hal-index (**27x smaller**) - -## Cache-Control TTL Recommendations - -Time to Live (TTL) values are calibrated for "safe but maximally aggressive" caching based on update frequency and consistency requirements. - -### TTL Summary - -| Resource | Example Path | Update Frequency | Recommended TTL | Cache-Control Header | -|----------|--------------|------------------|-----------------|---------------------| -| Root index | `/index.json` | ~1×/year | 7 days | `public, max-age=604800` | -| Timeline root | `/timeline/index.json` | ~1×/year | 7 days | `public, max-age=604800` | -| Year index | `/timeline/2025/index.json` | ~12×/year | 4 hours | `public, max-age=14400` | -| Month index | `/timeline/2025/10/index.json` | Never (immutable) | 1 year | `public, max-age=31536000, immutable` | -| Major version index | `/9.0/index.json` | ~12×/year | 4 hours | `public, max-age=14400` | -| Patch version index | `/9.0/9.0.10/index.json` | Never (immutable) | 1 year | `public, max-age=31536000, immutable` | -| SDK index | `/9.0/sdk/index.json` | ~12×/year | 4 hours | `public, max-age=14400` | -| Exit nodes (CVE, markdown) | `/timeline/2025/10/cve.json` | Never (immutable) | 1 year | `public, max-age=31536000, immutable` | - -This doesn't take SDK-only releases or mistakes into account. - -## Rationale - -**Root and timeline root (7 days):** These change approximately once per year. A 7-day TTL provides strong caching while ensuring that when a new major version is added (even as a preview in February), propagation completes well before it matters. Worst case: a consumer sees stale data for a week after a yearly update. - -**Year and major version indexes (4 hours):** These update monthly, typically on Patch Tuesday. A 4-hour TTL balances freshness against CDN load. On release day, caches will converge within a business day. Between releases, these are effectively immutable and the 4-hour TTL is conservative. - -**Immutable resources (1 year + immutable directive):** Patch indexes, month indexes, and exit nodes are never modified after creation. The `immutable` directive tells browsers and CDNs to never revalidate—the URL is the version. One year is the practical maximum; longer provides no benefit. - -## LLM Consumption - -A major focus of - -Spec for LLMs: From 6ca1440248fcb55bd138812d5ff39e833ffb6c74 Mon Sep 17 00:00:00 2001 From: Richard Lander Date: Tue, 6 Jan 2026 14:03:06 -0800 Subject: [PATCH 08/15] Edit pass over doc --- .../release-notes-graph.md | 868 ++++++++---------- 1 file changed, 382 insertions(+), 486 deletions(-) diff --git a/accepted/2025/release-notes-graph/release-notes-graph.md b/accepted/2025/release-notes-graph/release-notes-graph.md index 07bcd2135..cbc2f5a4e 100644 --- a/accepted/2025/release-notes-graph/release-notes-graph.md +++ b/accepted/2025/release-notes-graph/release-notes-graph.md @@ -1,20 +1,20 @@ # Exposing Release Notes as an Information graph -The .NET project has published [release notes in JSON and markdown](https://github.com/dotnet/core/tree/main/release-notes) for many years. The investment in quality release notes has been based on the virtuous cloud-era idea that many deployment and compliance workflows require detailed structured data to safely operate at scale. For the most part, a highly clustered set of consumers (like GitHub Actions and vulnerability scanners) have adapted _their tools_ to _our formats_ to offer higher-level value to their users. That's all good. The LLM era is strikingly different where a much smaller set of information systems (LLM model companies) _consume and expose diverse data in standard ways_ according to _their (shifting) paradigms_ to a much larger set of users. The task at hand is to modernize release notes to make them more efficient to consume generally and to adapt them for LLM consumption. +The .NET project has published [release notes in JSON and markdown](https://github.com/dotnet/core/tree/main/release-notes) for many years. The investment in quality release notes has been based on the cloud-era idea that many deployment and compliance workflows require detailed structured data to safely operate at scale. For the most part, a highly clustered set of consumers (like GitHub Actions and vulnerability scanners) have adapted _their tools_ to _our formats_ to offer higher-level value to their users. The LLM era is strikingly different where a much smaller set of information systems (LLM model companies) _consume and expose diverse data in standard ways_ according to _their (shifting) paradigms_ to a much larger set of users. The task at hand is to modernize release notes to make them more efficient to consume and to adapt them for LLM consumption. Overall goals for release notes consumption: - Graph schema encodes graph update frequency - Satisfy reasonable expectations of performance (no 1MB JSON files), reliability, and consistency -- Enable aestheticly-pleasing queries that are terse, ergonomic, and effective, both for their own goals and as a proxy for LLM consumption. +- Enable aesthetically pleasing queries that are terse, ergonomic, and effective, both for their own goals and as a proxy for LLM consumption. - Support queries with multiple key styles, temporal and version-based (runtime and SDK versions) queries. - Expose queryable data beyond version numbers, such as CVE disclosures, breaking changes, and download links. -- Use the same data to generate most release note markdown files, like [releases.md](https://github.com/dotnet/core/blob/main/releases.md), and CVE announcements like [CVE-2025-55248](https://github.com/dotnet/announcements/issues/372), guaranteeing ensuring consistency from a single source of truth. +- Use the same data to generate most release note markdown files, like [releases.md](https://github.com/dotnet/core/blob/main/releases.md), and CVE announcements like [CVE-2025-55248](https://github.com/dotnet/announcements/issues/372), ensuring consistency from a single source of truth. - Use this project as a real-world information graph pilot to inform other efforts that expose information to modern information consumers. ## Scenario -Release notes are mechanism, not scenario. It likely difficult for users to keep up with and act on the constant stream of .NET updates, typically one or two times a month. Users often have more than one .NET major version deployed, further complicating this puzzle. Many users rely on update orchestrators like APT, Yum, and Visual Studio, however, it is unlikely that such tools cover all the end-points that users care about in a uniform way. It is important that users can reliably make good, straightforward, and timely decisions about their entire infrastructure, orchestrated across a variety of deployment tools. This is a key scenario that release notes serve. +Release notes are mechanism, not scenario. It is likely difficult for users to keep up with and act on the constant stream of .NET updates, typically one or two times a month. Users often have more than one .NET major version deployed, further complicating this puzzle. Many users rely on update orchestrators like APT, Yum, and Visual Studio, however, it is unlikely that such tools cover all the end-points that users care about in a uniform way. It is important that users can reliably make good, straightforward, and timely decisions about their entire infrastructure, orchestrated across a variety of deployment tools. This is a key scenario that release notes serve. Obvious questions release notes should answer: @@ -25,29 +25,29 @@ Obvious questions release notes should answer: - How long until a given major release is EOL or has been EOL? - What are known upgrade challenges? -CIOs, CTOs, and others are accountable for maintaining efficient and secure continuity for a set of endpoints, including end-user desktops and cloud servers. They are unlikely to read long markdown release notes or perform DIY `curl` + `jq` hacking with structured data. They will increasingly expect to be able to get answers to arbitrarily detailed compliance and deployment questions using chat assistants like Copilot. They may ask Claude to compare treatment of an industry-wide CVE like [CVE-2023-44487](https://nvd.nist.gov/vuln/detail/cve-2023-44487) across multiple application stacks in their portfolio. This already works reasonably well, but fails when prompts demand greater levels of detail and with the expectation that the source data comes from authoritative sources. It is very common to see assistants glean insight from a semi-arbitrary set of web pages with matching content. This is particularly problematic for day-of prompts (same day as a security release). +CIOs, CTOs, and others are accountable for maintaining efficient and secure continuity for a set of endpoints, including end-user desktops and cloud servers. They are unlikely to read [long markdown release notes](https://github.com/dotnet/core/blob/main/release-notes/10.0/10.0.1/10.0.1.md) or perform DIY `curl` + `jq` hacking with [structured data](https://builds.dotnet.microsoft.com/dotnet/release-metadata/10.0/releases.json). They will increasingly expect to be able to get answers to arbitrarily detailed compliance and deployment questions using chat assistants like Copilot. They may ask Claude to compare treatment of an industry-wide CVE like [CVE-2023-44487](https://nvd.nist.gov/vuln/detail/cve-2023-44487) across multiple application stacks in their portfolio. This already works reasonably well, but fails when prompts demand greater levels of detail and with the expectation that the source data comes from authoritative sources. It is very common to see assistants glean insight from a semi-arbitrary set of web pages with diverse authority. This is particularly problematic for day-of prompts (same day as a security release). -Some users have told us that they enable Slack notifications for [dotnet/announcements](https://github.com/dotnet/announcements/issues), which is an existing "release notes beacon". That's great and intended. What if we could take that to a new level, thinking of release notes as queryable data used by notification systems and LLMs? There is a lesson here. Users (virtuously) complain when we [forget to lock issues](https://github.com/dotnet/announcements/issues/107#issuecomment-482166428). They value high signal to noise. Fortunately, we no longer forget for announcements, but we have not achieved this same disciplined model with GitHub release notes commits (as will be covered later). It should just just as safe and reliable to use release notes updates as a beacon as dotnet/announcements. +Some users have told us that they enable Slack notifications for [dotnet/announcements](https://github.com/dotnet/announcements/issues), which is an existing "release notes beacon". That's great and intended. What if we could take that to a new level, thinking of release notes as queryable data used by notification systems and LLMs? There is a lesson here. Users (virtuously) complain when we [forget to lock issues](https://github.com/dotnet/announcements/issues/107#issuecomment-482166428). They value high signal to noise. Fortunately, we no longer forget for announcements, but we have not achieved this same disciplined model with GitHub release notes commits (as will be covered later). It should be just as safe and reliable to use release notes updates as a beacon as dotnet/announcements. -LLMs are a different kind of "user" than we've previously tried to enable. LLMs are _much more_ fickle relative to purpose-built tools. They are more likely to give up if release notes are not to their liking, instead relying on model knowledge or comfortable web search with its equal share of benefits and challenges. Regular testing (by the spec writer) of release notes with chat assistants has demonstrated that LLMs are typically only satiated by a "Goldilocks meal". Obscure formats, large documents, and complicated workflows don't perform well (or outright fail). LLMs will happily jump to `releases-index.json` and choke on the 1MB+ [`releases.json`](https://github.com/dotnet/core/blob/main/release-notes/releases-index.json) files we maintain if prompts are unable to keep their attention. +LLMs are a different kind of "user" than we've previously tried to support. LLMs are _much more_ fickle relative to purpose-built tools. They are more likely to give up if release notes are not to their liking, instead relying on model knowledge or comfortable web search with its equal share of benefits and challenges. Regular testing (by the spec writer) of release notes with chat assistants has demonstrated that LLMs are typically only satiated by a "Goldilocks meal". Obscure formats, large documents, and complicated workflows don't perform well (or outright fail). LLMs will happily jump to [`releases-index.json`](https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json) and choke on the 1MB+ [`releases.json`](https://builds.dotnet.microsoft.com/dotnet/release-metadata/6.0/releases.json) files we maintain if prompts are unable to keep their attention. ![.NET 6.0 releases.json file as tokens](./releases-json-tokens.png) This image shows that the worst case for the `releases.json` format is 600k tokens using the [OpenAI Tokenzier](https://platform.openai.com/tokenizer). It is an understatement to say that a file of that size doesn't work well with LLMs. Context: memory budgets tend to max out at 200k tokens. Large JSON files can be made to work in some scenarios, but not in the general case. -A strong belief is that workflows that are bad for LLMS are typically not _uniquely_ bad for LLMs but are challenging for other consumers. It is easy to guess that most readers of `releases-index.json` would be better-served by referenced JSON significantly less than 1MB+. This means that we need start from scratch with structured release notes. +A strong belief is that workflows that are bad for LLMS are typically not _uniquely_ bad for LLMs but are challenging for most consumers. It is easy to guess that most readers of `releases-index.json` would be better-served by referenced JSON significantly less than 1MB+. This means that we need start from scratch with structured release notes. -In the early revisions of this project, the design followed our existing schema playbook, modeling parent/child relationships, linking to more detailed sources of information, and describing information domains in custom schemas. That then progressed into wanting to expose summaries of high-value information from leaf nodes into the trunk. This approach didn't work well since the design was lacking a broader information architecure. A colleague noted that the design was [Hypermedia as the engine of application state (HATEOAS)](https://en.wikipedia.org/wiki/HATEOAS)-esque but not using one of the standard formats. The benefits of using standard formats is that they are free to use, have gone through extensive design review, can be navigated with standard patterns and tools, and (most importantly) LLMs already understand their vocabulary and access patterns. A new format will by definition not have those characteristics. +In the early revisions of this project, the design followed our existing schema playbook, modeling parent/child relationships, linking to more detailed sources of information, and describing information domains in custom schemas. That then progressed into wanting to expose summaries of high-value information from leaf nodes into the trunk. This approach didn't work well since the design was lacking a broader information architecture. A colleague noted that the design was [Hypermedia as the engine of application state (HATEOAS)](https://en.wikipedia.org/wiki/HATEOAS)-esque but not using one of the standard formats. The benefits of using standard formats is that they are free to use, have gone through extensive design review, can be navigated with standard patterns and tools, and (most importantly) LLMs already understand their vocabulary and access patterns. A new format will by definition not have those characteristics. -This proposal leans heavily on hypermedia, specifically [HAL+JSON](https://datatracker.ietf.org/doc/html/draft-kelly-json-hal). Hypermedia is very common, with [OpenAPI](https://www.openapis.org/what-is-openapi) and [JSON:API](https://jsonapi.org/) likely being the most common. LLMs are quite comfortable with these standards. The proposed use of HAL is a bit novel (not intended as a positive descriptor). It is inspired by [llms.txt](https://llmstxt.org/) as an emerging standard and the idea that hypermedia is the most natural next step to express complex diverse data and relationships. It's also expected (in fact, pre-ordained) that older standards will perform better than newer ones due to higher density (or presence at all) in the LLM training data. +This proposal leans heavily on hypermedia, specifically [HAL+JSON](https://datatracker.ietf.org/doc/html/draft-kelly-json-hal). Hypermedia is very common, with [OpenAPI](https://www.openapis.org/what-is-openapi) likely being the most common. LLMs are quite comfortable with these standards. The proposed use of HAL is a bit novel (meaning niche). It is inspired by [llms.txt](https://llmstxt.org/) as an emerging standard and the idea that hypermedia is the most natural next step to express complex diverse data and relationships. It's also expected (in fact, pre-ordained) that older standards will perform better than newer ones due to higher density (or presence at all) in the LLM training data. ## Hypermedia graph design This project has adopted the idea that a wide and deep information graph can expose significant information within the graph that satisfies user queries without loading other files. The graph doesn't need to be skeletal. It can have some shape on it. In fact our existing graph with [`release-index.json`](https://github.com/dotnet/core/blob/main/release-notes/releases-index.json) already does this, but without the benefit of a standard format or architectural principles. -The design intent is that a graph should be skeletal at its roots for performance and to avoid punishing queries that do not benefit from the curated shape. The deeper the node is in the graph, the more shape (or weight) it should take on since the data curation is much more likely to hit the mark. +The design intent is that a graph should be skeletal at its roots for performance and to avoid punishing queries that do not benefit from the curated shape. The deeper the node is in the graph, the more shape or weight it should take on since the data curation is much more likely to hit the mark. -Hypermedia formats have a long history of satisfying this methodology, long pre-dating, and actually inspiring the World Wide Web and its Hypertext Markup Language (HTML). This project uses [HAL+JSON](https://en.wikipedia.org/wiki/Hypertext_Application_Language) as the "graph format". HAL is a sort of a "hypermedia in a nutshell" schema, initally drafted in 2012. You can develop a basic understanding of HAL in about two minutes because it has a very limited syntax. +Hypermedia formats have a long history of satisfying this methodology, long pre-dating, and actually inspiring the World Wide Web and its Hypertext Markup Language (HTML). This project uses [HAL+JSON](https://en.wikipedia.org/wiki/Hypertext_Application_Language) as the "graph format". HAL is a sort of a "hypermedia in a nutshell" schema, initially drafted in 2012. You can develop a basic understanding of HAL in about two minutes because it has a very limited syntax. For the most part, HAL defines just two properties: @@ -117,9 +117,9 @@ The following example is similar, with the addition of the `_embedded` property. } ``` -The `_embedded` property contains order resources. This is the resource payload. Each of those order items have `self` and other related link relations referencing other resources. As stated earlier, the `self` relation references the canonical copy of the resource. Embedded resources may be a full or partial copy of the resource. Again, domain-specific reader will understand this schema and know how to process it. +The `_embedded` property contains order resources. This is the resource payload. Each of those order items have `self` and other related link relations referencing other resources. As stated earlier, the `self` relation references the canonical copy of the resource. Embedded resources may be a full or partial copy of the resource. Again, domain-specific readers will understand this schema and know how to process it. -This design aspect is the true strength of HAL, of projecting partial views of resources to their reference. It's the mechanism that enables the overall approach of a skeletal root with weighted bottom nodes. It's also what enables these two seemingly anemic properties to provide so much modeling value. +This design aspect is the true strength of HAL, of projecting partial views of resources to their reference. It's the mechanism that enables the overall approach of a skeletal root with weighted leaves. It's also what enables these two seemingly anemic properties to provide so much modeling value. The `currentlyProcessing` and `shippedToday` properties provide additional information about ongoing operations. @@ -173,8 +173,8 @@ The basis of this design is that `releases-index.json` is no longer sufficient f Release notes naturally describe two information dimensions: time and product version. -- Within time, we have years, months, and (ship) days. -- Within version, we have major and patch version. We also have runtime vs SDK version. +- Within time, we have years, months, and ship days. +- Within version, we have major and patch version. We also have runtime and SDK version. These dimensional characteristics form the load-bearing structure of the graph to which everything else is attached. The new graph exposes both timeline and version indices. We've previously only had a version index. @@ -182,20 +182,20 @@ The following table summarizes the overall shape of the graph, starting at `dotn | File | Type | Size | Frequency | Updates When... | | --- | --- | --- | --- | --- | -| `index.json` (root) | Release version index | 4.6 KB | 1-2x/year | New major version, support bool changes | -| `timeline/index.json` | Release timeline index | 4.4 KB | 2x/year | New year starts, new major version | -| `timeline/{year}/index.json` | Year index | 5.5 KB | 12x/year | New month with activity, phase changes | -| `timeline/{year}/{month}/index.json` | Month index | 9.7 KB | 0 | Never (immutable after creation) | -| `{version}/index.json` | Major version index | 20 KB | 12x/year | New patch release | -| `{version}/{patch}/index.json` | Patch version index | 7.7 KB | 0 | Never (immutable after creation) | -| `llms.json` | AI-optimized index | 4.7 KB | 12x/year | New patch release, support status changes | +| `index.json` (root) | Release version index | 4.7 KB | 1-2x/year | New major version, support bool changes | +| `timeline/index.json` | Release timeline index | 4.7 KB | 2x/year | New year starts, new major version | +| `timeline/{year}/index.json` | Year index | 6.5 KB | 12x/year | New month with activity, phase changes | +| `timeline/{year}/{month}/index.json` | Month index | 5.0 KB | 0 | Never (immutable after creation) | +| `{version}/index.json` | Major version index | 25 KB | 12x/year | New patch release | +| `{version}/{patch}/index.json` | Patch version index | 5.3 KB | 0 | Never (immutable after creation) | +| `llms.json` | AI-optimized index | 8.0 KB | 12x/year | New patch release, support status changes | **Summary:** Cold roots, warm branches, immutable leaves. Notes: - 2025 was used for `{year}`, 10 for `{month`}, 8.0 for `{version}`, and 8.0.21 for `{patch}` -- The bottom of the graph has the largest variance over time. `8.0.21/index.json` is `11.5` KB while `8.0.22/index.json` is `4.8` KB, significantly smaller because it was a non-security release. Similarly, `2025/10/index.json` is `9.7` KB while `2025/11/index.json` is `3.5` KB, significantly smaller because there was no security release that month. `2025/12/index.json` was even smaller because there was only a .NET 10 patch, also non-security. Security releases were chosen to demonstrate more realistic sizes. +- The bottom of the graph has the largest variance over time. `8.0.21/index.json` is `5.3` KB while `8.0.22/index.json` is `4.8` KB, a little smaller because it was a non-security release. Similarly, `2025/10/index.json` is `5.0` KB while `2025/11/index.json` is `3.0` KB, significantly smaller because there was no security release that month. `2025/12/index.json` was even smaller because there was only a .NET 10 patch, also non-security. Security releases were chosen to demonstrate more realistic sizes. - `llms.json` will be covered more later. We can contrast this approach with the existing release graph. @@ -203,27 +203,27 @@ We can contrast this approach with the existing release graph. | File | Type | Size | Frequency | Updates When... | | --- | --- | --- | --- | --- | | `releases-index.json` (root) | Release version index | 6.3 KB | 18x/year | Every patch release | -| `{version}/releases.json` | Major Version Index | 1.3 MB | 18x/year | Every patch release | +| `{version}/releases.json` | Major Version Index | 1.2 MB | 18x/year | Every patch release | Notes: - 8.0 was used for `{version}`. 6.0 `releases.json` is 1.6 MB. - Frequency is 18x a year to allow for SDK-only releases that occur after Patch Tuesday. -It is straightforward to see that the new graph design enables access to far more information before hitting a data wall. In fact, in experiments, LLMs reliably hit an HTTP `409` error code as soon as they try to read `release.json`. It hits the LLM context limit, or similar. +It is straightforward to see that the new graph design enables access to far more information before hitting a data wall. In fact, in experiments, LLMs reliably hit an HTTP `409` error code as soon as they try to read [`releases.json`](https://builds.dotnet.microsoft.com/dotnet/release-metadata/8.0/releases.json"). It hits the LLM context limit, or similar. -The last 12 months of commit data (Nov 2024-Nov 2025) demonstrates a higher than expected update rate. +The last 12 months of commit data (as of Nov 2025) demonstrates a higher than expected update rate. | File | Commits | Notes | | ---- | ------- | ----- | | `releases-index.json` | 29 | Root index (all versions) | | `10.0/releases.json` | 22 | Includes previews/RCs and SDK-only releases | | `9.0/releases.json` | 24 | Includes SDK-only releases, fixes, URL rewrites | -| `8.0/releases.json` | 1Please8 | Includes SDK-only releases, fixes, URL rewrites | +| `8.0/releases.json` | 18 | Includes SDK-only releases, fixes, URL rewrites | **Summary:** Hot everything. -Conservatively, the existing commit counts are not good. The `releases-index.json` file is a mission-critical live-site resource. 29 updates is > 2x/month! +The commit counts are not good. The `releases-index.json` file is a mission-critical live-site resource. 29 updates is > 2x/month! ### Graph consistency @@ -231,7 +231,7 @@ The graph has one rule: > Every resource in the graph needs to be guaranteed consistent with every other part of the graph. -The unstated problem is CDN caching. Assume that the entire graph is consistent when uploaded to an origin server. A CDN server is guaranteed by construction to serve both old and new copies of the graph -- for existing files that have been updated -- leading to potential inconsistencies. The graph construction needs to be resilient to that. +The underlying problem is Content Delivery Network (CDN) caching. Assume that the entire graph is consistent when uploaded to an origin server. A CDN server is guaranteed by construction to serve both old and new copies of the graph -- for existing files that have been updated -- leading to potential inconsistencies. The graph construction needs to be resilient to that. Related examples: @@ -242,15 +242,15 @@ Today, we publish `releases-index.json` as the root of our release notes graph. Problems: -- Exposing patch versions in multiple files that need to agree is incompatible with using a Content Delivery Network (CDN) that employs standard caching (expiration / TTL). -- The `releases-index.json` file is a critical live site resource driving 1000s of GBs of downloads a month, yet we update it multiple times a month, by virue of the data it exposes. +- Exposing patch versions in multiple files that need to agree is incompatible with using a CND that employs standard caching (expiration / TTL). +- The `releases-index.json` file is a critical live site resource driving 1000s of GBs of downloads a month, yet we update it multiple times a month, by virtue of the data it exposes. -It's hard to understate the impact of schema design on file update frequency. Files that expose properties with patch versions (scalars or links) inherently require updates on the patch schedule. If so, `git status` will put that file on a stage. Otherwise, `git status` will not give that file a second look. +It's hard to understate the impact of schema design on file update frequency. Files that expose properties with patch versions (scalars or links) inherently require updates on the patch schedule. Solution: - Fast changing currency (like patch version numbers) are exposed in (at most) a single resource in the graph, and never at the root. -- The root index file is updated once or twice a year (to add the presence of a new major release and change support status; releases come in and go out, typically not on the same day). +- The root index file is updated once or twice a year (to add the presence of a new major release and change support status of newly EOL versions; releases come in and go out, typically not on the same day although that [assumption has changed](https://devblogs.microsoft.com/dotnet/dotnet-sts-releases-supported-for-24-months/)). The point about the root index isn't a _solution_ but an _implication_ of the first point. If the root index isn't allowed to contain fast-moving currency, in part because it is present in another resource, then it is stripped of its reason to change. @@ -286,9 +286,13 @@ The last point clinches it. The intent is that root `index.json` can be cached a ### Graph wormholes -There are many design paradigms and tradeoffs one can consider with a graph like this. A major focus is the "skeletal root" vs "weighted bottom" design point, discussed earlier. This approach forces significant fetches and inefficiency to get to _any_ useful data, unlike `releases-index.json`. To mitigate that, the graph includes helpful workflow-specific wormhole links. As the name suggests, these links enable jumping from one part of the graph to another, intended to create a kind of ergonomics for expected queries. The wormholes are somewhat emergent, somewhat obvious to expose due to otherwise challenging nature of the design. +There are many design paradigms and tradeoffs one can consider with a graph like this. A major focus is the "skeletal root" vs "weighted leaves" design point, discussed earlier. This approach forces significant fetches and inefficiency to get to _any_ useful data, unlike `releases-index.json`. To mitigate that, the graph includes helpful workflow-specific wormhole links. These wormholes are somewhat emergent, somewhat obvious to expose due to the otherwise challenging nature of the design. -The wormholes links take on a different character at each layer of the graph. +**Wormhole links** jump across the graph—from a patch version to its release month, skipping intermediate navigation. `latest-lts` teleports to the current LTS release. As the name suggests, these links enable jumping from one part of the graph to another, creating ergonomics for expected queries. + +**Spear-fishing** is a wormhole variant targeting timely, high-value content deep in the graph. `latest-security-disclosures` points directly to CVE information with a short half-life, where freshness defines value. + +The wormhole links take on a different character at each layer of the graph. Cold roots: @@ -304,20 +308,17 @@ Immutable leaves: - `prev` and `prev-security` -- enables jumping to an earlier matching patch version or month index. -Notes: - -- The wormhole links cannot force an update schedule beyond what the file would naturally allow. For the cold roots, a workhole like `latest-lts` is the best we can do. -- There are other such wormhole links. These are the the primary ones, which also best demonstrate the idea. - -The wormhole links are what make the graph a graph and not just two related trees providing alternative views of the same data. They also enable efficient navigation. - For some scenarios, it can be efficient to jump to `latest-security-month` and then backwards in time via `prev-security`. `prev` and `prev-security` jump across year boundaries, reducing the logic required to use the scheme. -There is no `next` or `next-security`. `prev` and `prev-security` link immutable leaves, establishing a linked-list based on past knowledge. It's not possible to provide `next` links given the immutability constraint. +The wormhole links are what make the graph a graph and not just two related trees providing alternative views of the same data. They also enable efficient navigation. Testing has borne this out, demonstrating that wormhole links are one of the defining features of the graph. -There is no `latest-sts` link because it's not really useful. `latest` covers the same need sufficiently. +Notes: -Testing has demonstrated that these wormhole links are one of the defining features of the graph. +- There are other such wormhole links. These are the primary ones, which also best demonstrate the idea. +- The wormhole links cannot (per policy) force an update schedule beyond what the file would naturally allow. +- For the cold roots, a wormhole like `latest-lts` is the best we can do. +- There is no `next` or `next-security`. `prev` and `prev-security` link immutable leaves, establishing a linked-list based on past knowledge. It's not possible to provide `next` links given the immutability constraint. +- There is no `latest-sts` link because it's not really useful. `latest` covers the same need sufficiently. ## Version Index Modeling @@ -325,30 +326,30 @@ The version index has three layers: releases, major version, patch version. Most ### Releases index -The root `index.json` file represents all .NET versions. It is a stripped-down version of the existing `releases-index.json`. +The root `index.json` file represents all .NET versions. It is can be viewed as a stripped-down version of the existing `releases-index.json`. Source: ```json { "$schema": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/schemas/v1/dotnet-release-version-index.json", - "kind": "releases-index", + "kind": "root", "title": ".NET Release Index", - "latest": "10.0", - "latest_lts": "10.0", + "latest_major": "10.0", + "latest_lts_major": "10.0", "_links": { "self": { "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/index.json" }, - "latest": { + "latest-lts-major": { "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/index.json", - "title": "Latest release - .NET 10.0" + "title": "Latest LTS major release - .NET 10.0" }, - "latest-lts": { + "latest-major": { "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/index.json", - "title": "Latest LTS release - .NET 10.0" + "title": "Latest major release - .NET 10.0" }, - "timeline-index": { + "timeline": { "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/index.json", "title": ".NET Release Timeline Index" } @@ -358,12 +359,13 @@ Source: @@ -529,56 +543,56 @@ Source: +Source: ```json -{ - "$schema": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/schemas/v1/dotnet-release-manifest.json", - "kind": "manifest", - "title": "Manifest - .NET 9.0.10", - "_links": { - "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/9.0.10/manifest.json" - }, - "cve-markdown": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/10/cve.md", - "title": "CVE records - October 2025", - "type": "application/markdown" - }, - "cve-markdown-rendered": { - "href": "https://github.com/dotnet/core/blob/main/release-notes/timeline/2025/10/cve.md", - "title": "CVE records (Rendered) - October 2025", - "type": "application/markdown" - }, - "release-json": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/9.0.10/release.json", - "title": "Release information", - "type": "application/json" - }, - "release-notes-9.0.111-markdown": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/9.0.10/9.0.111.md", - "title": ".NET 9.0.111 - October 14, 2025", - "type": "application/markdown" - }, - "release-notes-markdown": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/9.0/9.0.10/9.0.10.md", - "title": "Release notes", - "type": "application/markdown" - }, - "release-notes-markdown-rendered": { - "href": "https://github.com/dotnet/core/blob/main/release-notes/9.0/9.0.10/9.0.10.md", - "title": "Release notes (Rendered)", - "type": "application/markdown" - } - } -} -``` - -It's possible to make detail-oriented compliance and deployment decisions based on this information. There's even a commit for the CVE fix with an LLM friendly link style. This is the bottom part of the hypermedia graph. It's far more shapely and weighty than the root. If a consumer gets this far, it is likely because they need access to the exposed information. If they only want access to the `cve.json` file, it is exposed in the major version index. - -Previews are a special beast. Preview patch releases get a more extensive `manifest.json` treatment to account for all content that the team makes available about new features. - -Source: - -```json -{ - "$schema": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/schemas/v1/dotnet-release-manifest.json", - "kind": "manifest", - "title": "Manifest - .NET 10.0.0-preview.1", - "_links": { - "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/preview/preview1/manifest.json" - }, - "release-json": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/preview/preview1/release.json", - "title": "Release information", - "type": "application/json" - }, - "release-notes-markdown": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/preview/preview1/10.0.0-preview.1.md", - "title": "Release notes", - "type": "application/markdown" - }, - "release-notes-markdown-rendered": { - "href": "https://github.com/dotnet/core/blob/main/release-notes/10.0/preview/preview1/10.0.0-preview.1.md", - "title": "Release notes (Rendered)", - "type": "application/markdown" - }, - "whats-new-aspnetcore": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/preview/preview1/aspnetcore.md", - "title": "ASP.NET Core in .NET 10 Preview 1 - Release Notes", - "type": "application/markdown" - }, - "whats-new-containers": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/preview/preview1/containers.md", - "title": "Container image updates in .NET 10 Preview 1 - Release Notes", - "type": "application/markdown" - }, - "whats-new-csharp": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/preview/preview1/csharp.md", - "title": "C# 14 updates in .NET 10 Preview 1 - Release Notes", - "type": "application/markdown" - }, - "whats-new-dotnetmaui": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/preview/preview1/dotnetmaui.md", - "title": ".NET MAUI in .NET 10 Preview 1 - Release Notes", - "type": "application/markdown" - }, - "whats-new-efcore": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/preview/preview1/efcore.md", - "title": "Entity Framework Core 10 Preview 1 - Release Notes", - "type": "application/markdown" - }, - "whats-new-fsharp": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/preview/preview1/fsharp.md", - "title": "F# updates in .NET 10 Preview 1 - Release Notes", - "type": "application/markdown" - }, - "whats-new-libraries": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/preview/preview1/libraries.md", - "title": ".NET Libraries in .NET 10 Preview 1 - Release Notes", - "type": "application/markdown" - }, - "whats-new-runtime": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/preview/preview1/runtime.md", - "title": ".NET Runtime in .NET 10 Preview 1 - Release Notes", - "type": "application/markdown" - }, - "whats-new-sdk": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/preview/preview1/sdk.md", - "title": ".NET SDK in .NET 10 Preview 1 - Release Notes", - "type": "application/markdown" - }, - "whats-new-visualbasic": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/preview/preview1/visualbasic.md", - "title": "Visual Basic updates in .NET 10 Preview 1 - Release Notes", - "type": "application/markdown" - }, - "whats-new-winforms": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/preview/preview1/winforms.md", - "title": "Windows Forms in .NET 10 Preview 1 - Release Notes", - "type": "application/markdown" - }, - "whats-new-wpf": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/preview/preview1/wpf.md", - "title": "WPF in .NET 10 Preview 1 - Release Notes", - "type": "application/markdown" + "documentation": { + "aspnetcore": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/preview/preview1/aspnetcore.md", + "type": "application/markdown" + }, + "containers": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/preview/preview1/containers.md", + "type": "application/markdown" + }, + "csharp": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/preview/preview1/csharp.md", + "type": "application/markdown" + }, + "dotnetmaui": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/preview/preview1/dotnetmaui.md", + "type": "application/markdown" + }, + "efcore": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/preview/preview1/efcore.md", + "type": "application/markdown" + }, + "fsharp": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/preview/preview1/fsharp.md", + "type": "application/markdown" + }, + "libraries": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/preview/preview1/libraries.md", + "type": "application/markdown" + }, + "runtime": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/preview/preview1/runtime.md", + "type": "application/markdown" + }, + "sdk": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/preview/preview1/sdk.md", + "type": "application/markdown" + }, + "visualbasic": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/preview/preview1/visualbasic.md", + "type": "application/markdown" + }, + "winforms": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/preview/preview1/winforms.md", + "type": "application/markdown" + }, + "wpf": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/preview/preview1/wpf.md", + "type": "application/markdown" + } } - } -} ``` ## Timeline Modeling @@ -1001,28 +909,28 @@ Source: - -```json -{ - "$schema": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/schemas/v1/dotnet-release-manifest.json", - "kind": "manifest", - "title": "Manifest - January 2025", - "_links": { - "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/01/manifest.json" - }, - "cve-markdown": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/01/cve.md", - "title": "CVE records - January 2025", - "type": "application/markdown" - }, - "cve-markdown-rendered": { - "href": "https://github.com/dotnet/core/blob/main/release-notes/timeline/2025/01/cve.md", - "title": "CVE records (Rendered) - January 2025", - "type": "text/html" - } - } -} -``` +It was stated earlier that the version indexes converges to a point while the timeline index converges to a row of points. We see that on display here. ## LLM Enablement -The majority of the design to this point has been focused on providing a better more modern approach for tools driving typical cloud native workflows. That leave the question of how this design is intended to work for LLMs. That's actually a [whole other spec](./release-notes-graph-llms.md). However, it makes sense to look at how the design diverged for LLMs. +The majority of the design to this point has been focused on providing a more modern and refined approach for tools driving typical cloud native workflows. That leave the question of how this design is intended to work for LLMs. That's actually a [whole other spec](./exposing-hypermedia-to-llms.md). However, it makes sense to look at how the design diverged for LLMs. Source: ```json { - "kind": "llms-index", + "kind": "llms", "title": ".NET Release Index for AI", - "ai_note": "ALWAYS read required_pre_read first. HAL graph\u2014follow _links only, never construct URLs.", - "required_pre_read": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/skills/dotnet-releases/SKILL.md", - "latest": "10.0", - "latest_lts": "10.0", - "latest_year": "2025", - "supported_releases": [ + "ai_note": "ALWAYS read required_pre_read first. Use skills and workflows when they match; they provide optimal paths. Trust _embedded data\u2014it\u0027s authoritative and current. Never construct URLs.", + "required_pre_read": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/skills/dotnet-releases/SKILL.md", + "latest_major": "10.0", + "latest_lts_major": "10.0", + "latest_patch_date": "2025-12-09T00:00:00+00:00", + "latest_security_patch_date": "2025-10-14T00:00:00+00:00", + "last_updated_date": "2026-01-06T19:19:25.9491886+00:00", + "supported_major_releases": [ "10.0", "9.0", "8.0" ], "_links": { "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/llms.json" + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/llms.json" }, - "latest": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/index.json", - "title": "Latest release - .NET 10.0" + "latest-lts-major": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/index.json", + "title": "Latest LTS major release - .NET 10.0" }, - "latest-lts": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/index.json", - "title": "Latest LTS release - .NET 10.0" + "latest-major": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/index.json", + "title": "Latest major release - .NET 10.0" + }, + "latest-month": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/12/index.json", + "title": "Latest month - December 2025" + }, + "latest-security-disclosures": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/index.json", + "title": "Latest security disclosures - October 2025" }, "latest-security-month": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/10/index.json", + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/index.json", "title": "Latest security month - October 2025" }, "latest-year": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/index.json", + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/index.json", "title": "Latest year - 2025" }, - "releases-index": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json", + "root": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/index.json", "title": ".NET Release Index" }, - "timeline-index": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/index.json", + "timeline": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/index.json", "title": ".NET Release Timeline Index" + }, + "latest-cve-json": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/cve.json", + "title": "Latest CVE records - October 2025", + "type": "application/json" } }, ``` -For far, this looks very largely similar to root index.json. +This file looks somewhat similar to root `index.json`, but we can see monthly-oriented properties and link relations have crept in, which means that this file needs to be updated monthly. That's a massive departure from the "cold roots" philosophy. Perhaps the designer of `llms.json` is different from `index.json` and they never read this spec? A true horror! This cannot stand. Don't despair; it's the same designer, but applying a different methodology for a different audience. Hold the pitchforks for a moment. -There are two differences: +A major design focus of `llms.json` is exposing guidance inline to steer LLMs towards optimal performance traversing the graph. That's the bulk of what the other spec discusses. The guidance is exposed via the the `ai_note` and `required_pre_read` properties. A major realization is that there is always going to be a "better mousetrap" w/rt steering LLMs. The design of `llms.json` _will change_. It needs to be at arm's length from `index.json`, a file intended for critical workloads. `llms.json` is effectively an abstraction layer for LLMs, taking advantage of, but not integrated into the cloud workflows. This realization opens the door to adopting a different design point. -- `supported_releases` -- Describes the set of releases supported at any point -- `latest-security-month` -- Wormhole link to the latest security month - -This link relation clearly violates the "cold root" goals of index.json. Indeed, it does. This file isn't intended to support the n-9s of a cloud. It's intended to make LLMs efficient. Based on extensive testing, LLMs love these wormhole links. They actually halluciante less when given a quick and obvious path towards the goal. - -The `ai_note` and `required_pre_read` are LLM-specific properties that are covered in the other spec. A major realization is that there is always going to be a "better mousetrap" w/rt steering LLMs. The design of `llms.json` _will change_. It needs to be at arm's length from `index.json`, a file intended for mission critical workloads. `llms.json` is effectively an abstraction layer for LLMs, taking advantage of, but not integrated into the cloud workflows. - -The `_embedded` property (solely) contains: `latest_patches`. +The `_embedded` property contains `latest_patches`: ```json - "_embedded": { - "latest_patches": [ - { +"_embedded": { + "patches": { + "10.0": { "version": "10.0.1", - "release": "10.0", "release_type": "lts", - "date": "2025-12-09T00:00:00+00:00", - "year": "2025", - "month": "12", "security": false, - "cve_count": 0, "support_phase": "active", "supported": true, - "eol_date": "2028-11-14", "sdk_version": "10.0.101", + "latest_security_patch": "10.0.0-rc.2", + "latest_security_patch_date": "2025-10-14T00:00:00+00:00", "_links": { "self": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/10.0.1/index.json" + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/10.0.1/index.json" + }, + "downloads": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/downloads/index.json" + }, + "latest-month": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/12/index.json" + }, + "latest-security-disclosures": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/index.json" + }, + "latest-security-month": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/index.json" }, - "latest-security": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/preview/rc2/index.json" + "latest-security-patch": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/preview/rc2/index.json" }, - "release-major": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/index.json" + "major": { + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/index.json" }, "manifest": { - "href": "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/manifest.json" + "href": "https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/manifest.json" } } }, ``` -These design choices equally violate the "cold roots" design. Once its been broken once, you might as well break it thoroughly. This file also a bit more budget to play with since it only ever has to accomodate (in general) three patch releases, not the growing set of major versions in root index.json. This opens up more richness in the link relations. The four listed are the highest priority and enable the majority of scenarios. This is also where we see `manifest` providing more value, making it possible to skip a jump through `release-major` to get to breaking change, what's new, and other similar information. +These design choices equally violate the "cold roots" design, as already mentioned. Once its been broken once, you might as well break it thoroughly. This file also has a bit more budget to play with since it only ever has to accommodate (in general) three patch releases, not the growing set of major versions in root index.json. This opens up more richness in the link relations to drive a broader set of efficient queries. -## Attached data +Testing demonstrates that this design works well. LLMs notice the `ai_note` and `required_pre_read` properties and use those for guidance. They love the wormhole links and grasp their intent. Logging suggests that they are following them in optimal ways to answer user queries (synthetically tests via LLM eval). -> These dimensional characteristics form the load-bearing structure of the graph to which everything else is attached. +Testing also demonstrates that `index.json` can be effectively used as a root data file, but that it must be a secondary fetch after acquiring guidance via [`llms.txt`](https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/llms.txt) and is less efficient for many queries compared to `llms.json`. + +## Validate -This leaves the question of which data we could attach. +> These dimensional characteristics form the load-bearing structure of the graph to which everything else is attached. -The following are all in scope to include (or already incldued): +The following data is attached within the graph and therefore queryable. - Breaking changes - What's new links @@ -1411,6 +1308,5 @@ The following are all in scope to include (or already incldued): - Linux package dependencies - Download links + hashes -## Validation - +We can query this data to test out the efficacy for the design. From d8fddb811259f58cebbd25b91ef78bd60bee5e3a Mon Sep 17 00:00:00 2001 From: Richard Lander Date: Tue, 6 Jan 2026 15:25:06 -0800 Subject: [PATCH 09/15] Add conclusion --- .../release-notes-graph.md | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/accepted/2025/release-notes-graph/release-notes-graph.md b/accepted/2025/release-notes-graph/release-notes-graph.md index cbc2f5a4e..6b617493f 100644 --- a/accepted/2025/release-notes-graph/release-notes-graph.md +++ b/accepted/2025/release-notes-graph/release-notes-graph.md @@ -1302,11 +1302,41 @@ The following data is attached within the graph and therefore queryable. - Breaking changes - What's new links - CVE disclosures -- Servicing fixes and commits (beyond CVEs) -- Known issues - Supported OSes - Linux package dependencies - Download links + hashes -We can query this data to test out the efficacy for the design. +We can query this data to test out the efficacy for the design inspired. +Three schema approaches are compared across 15 real-world query scenarios, measuring file transfer in bytes. + +| Test | Query | `llms.json` | `index.json` | `releases-index.json` | +|------|-------|----------:|-----------:|--------------------:| +| T1 | Supported .NET versions | **5 KB** | **5 KB** | 6 KB | +| T2 | Latest patch details | **5 KB** | 14 KB | 6 KB | +| T3 | Security patches since date | **14 KB** | 34 KB | 1,269 KB | +| T4 | EOL version CVE details | 45 KB | **40 KB** | 1,612 KB | +| T5 | CVE timeline with code fixes | **25 KB** | 30 KB | ✗ | +| T6 | Security process analysis | **450 KB** | 455 KB | ✗ | +| T7 | High-impact breaking changes | **117 KB** | 126 KB | ✗ | +| T8 | Code migration guidance | **122 KB** | 131 KB | ✗ | +| T9 | What's new in runtime | **24 KB** | 33 KB | ✗ | +| T10 | Security audit (version check) | **14 KB** | 34 KB | 1,269 KB | +| T11 | Minimum libc version | **17 KB** | 26 KB | ✗ | +| T12 | Docker setup with SDK | **36 KB** | 45 KB | 25 KB | +| T13 | TFM support check | 45 KB | 50 KB | **6 KB** | +| T14 | Package CVE check | **60 KB** | 65 KB | ✗ | +| T15 | Target platform versions | **11 KB** | 20 KB | ✗ | +| | **Winner** | **13** | **2** | **1** | + +**Legend:** Smaller is better. **Bold** = winner. **P** = partial answer. **✗** = cannot answer. + +Raw results are recorded at [metrics/README.md](./metrics/README.md). The results prove out the premise that the new graph can efficiently answer a lot of domain questions and that `llms.json` has a small advantage over `index.json` when testing with `jq`. + +## Conclusion + +The intent of this project is to publish structured release notes using a more efficient schema architecture, both to reduce update cadence and to enable smaller fetch costs. The belief was that an existing standard would push our schema efforts towards a strong design paradigm that ultimately enabled us to achieve our goals faster and better. The results seem to prove this out. The chosen solution is an opposite-end-of-the-spectrum approach. It generates a huge number of files compared to the existing approach, many of which will never be updated and rarely visited once the associated major version is out of support. The existing `releases.json` files are already much like that, just monolithic (and therein lies the challenge). + +A case in point is the addition of breaking changes. Someone suggested that breaking changes should be added to the graph. It took < 60 mins to do that. The HAL design paradigm and the aligned philosophy of cold roots and weighted immutable leaves naturally alloted specific space for breaking changes to be attached on the load-bearing structure. There's no need to consult a design committee since (as a virtue) there isn't much design freedom once the overall approach is established. It's likely that we'll find a need to attach more information to the graph. We can just repeat the process. + +The restrictive nature of this design ends up being well aligned with the performance and cost consideration of LLMs. That's a bit of foreshadowing of the other spec. It boils down being very intentional about the design being breadth- or depth-first. This design is bread-first oriented, which enables learning about a layer at a time. The reason that `llms.json` is able to pull ahead of the purist `index.json` breadth-first implementation is that it offers a more depth-first approach for specific queries. It's one of those "I know what I'm doing; trust me" situations. At the application layer, it's OK to break some rules. And that in a nutshell describes the design. From 09256eef243a277decdd9a15c671f54cab389bc4 Mon Sep 17 00:00:00 2001 From: Richard Lander Date: Tue, 6 Jan 2026 15:25:24 -0800 Subject: [PATCH 10/15] Add supporting query data --- accepted/2025/release-notes-graph/metrics.md | 1055 ----------------- .../release-notes-graph/metrics/README.md | 45 + .../metrics/cve-stress-test.md | 299 +++++ .../metrics/easy-questions.md | 181 +++ .../metrics/index-discovery.md | 266 +++++ .../metrics/interacting-with-environment.md | 160 +++ .../metrics/project-file-analysis.md | 145 +++ .../metrics/upgrade-whats-new.md | 226 ++++ .../release-notes-graph-llms.md | 0 .../release-notes-graph/testing/prompts.md | 291 ----- .../2025/release-notes-graph/testing/spec.md | 443 ------- 11 files changed, 1322 insertions(+), 1789 deletions(-) delete mode 100644 accepted/2025/release-notes-graph/metrics.md create mode 100644 accepted/2025/release-notes-graph/metrics/README.md create mode 100644 accepted/2025/release-notes-graph/metrics/cve-stress-test.md create mode 100644 accepted/2025/release-notes-graph/metrics/easy-questions.md create mode 100644 accepted/2025/release-notes-graph/metrics/index-discovery.md create mode 100644 accepted/2025/release-notes-graph/metrics/interacting-with-environment.md create mode 100644 accepted/2025/release-notes-graph/metrics/project-file-analysis.md create mode 100644 accepted/2025/release-notes-graph/metrics/upgrade-whats-new.md delete mode 100644 accepted/2025/release-notes-graph/release-notes-graph-llms.md delete mode 100644 accepted/2025/release-notes-graph/testing/prompts.md delete mode 100644 accepted/2025/release-notes-graph/testing/spec.md diff --git a/accepted/2025/release-notes-graph/metrics.md b/accepted/2025/release-notes-graph/metrics.md deleted file mode 100644 index 351b6fab8..000000000 --- a/accepted/2025/release-notes-graph/metrics.md +++ /dev/null @@ -1,1055 +0,0 @@ -# Schema Metrics Comparison - -This document compares the data transfer costs between the new **hal-index** schema and the legacy **releases-index** schema for common query patterns. - -## Design Context - -The hal-index schema was designed to solve fundamental problems with the releases-index approach: - -1. **Cache Coherency** - The releases-index.json references external files (e.g., `9.0/releases.json`) that may have different CDN cache TTLs, leading to inconsistent data when patch versions are updated. - -2. **Data Efficiency** - The `releases.json` files contain download URLs and hashes for every binary artifact, making them 30-50x larger than necessary for most queries. - -3. **Atomic Consistency** - The hal-index uses HAL `_embedded` to include all referenced data in a single document, ensuring a consistent snapshot per fetch. - -See [dotnet/core#10143](https://github.com/dotnet/core/issues/10143) for full design rationale. - -## File Characteristics - -The tables below show theoretical update frequency based on practice and design, with file sizes measured from actual files. - -### Hal-Graph Files - -| File | Size | Updates/Year | Description | -|------|------|--------------|-------------| -| `index.json` | 8 KB | ~1 | Root index with all major versions | -| `10.0/index.json` | 15 KB | ~12 | All 10.0 patches (fewer releases so far) | -| `9.0/index.json` | 28 KB | ~12 | All 9.0 patches with CVE references | -| `8.0/index.json` | 34 KB | ~12 | All 8.0 patches with CVE references | -| `timeline/index.json` | 8 KB | ~1 | Timeline root (all years) | -| `timeline/2025/index.json` | 15 KB | ~12 | Year index (all months) | -| `timeline/2024/07/index.json` | 10 KB | ~1 | Month index with embedded CVE summaries | -| `timeline/2025/01/cve.json` | 14 KB | ~1 | Full CVE details for a month | - -### Releases-Index Files - -| File | Size | Updates/Year | Description | -|------|------|--------------|-------------| -| `releases-index.json` | 6 KB | ~12 | Root index (version list only) | -| `10.0/releases.json` | **440 KB** | ~12 | All 10.0 releases with full download metadata | -| `9.0/releases.json` | **769 KB** | ~12 | All 9.0 releases with full download metadata | -| `8.0/releases.json` | **1,233 KB** | ~12 | All 8.0 releases with full download metadata | - -### Measurements - -Actual git commits in the last 12 months (Nov 2024 - Nov 2025): - -| File | Commits | Notes | -|------|---------|-------| -| `releases-index.json` | 29 | Root index (all versions) | -| `10.0/releases.json` | 22 | Includes previews/RCs and SDK-only releases | -| `9.0/releases.json` | 24 | Includes SDK-only releases, fixes, URL rewrites | -| `8.0/releases.json` | 18 | Includes SDK-only releases, fixes, URL rewrites | - -The commit counts are significantly higher than the theoretical ~12/year due to: - -- **SDK-only releases**: Additional releases between Patch Tuesdays (e.g., [9.0.308 SDK](https://github.com/dotnet/core/commit/24a83fcc189ecf3c514dc06963ce779dcbf64ad5) released 8 days after November Patch Tuesday) -- **Metadata corrections**: Simple changes like [updating .NET 9's EOL date](https://github.com/dotnet/core/commit/24ff22598de88e3c9681e579aab5fe344cdc21b0) require updating both `releases-index.json` and `9.0/releases.json` -- **Post-release corrections**: `fix hashes`, `sdk array mismatch` -- **Infrastructure changes**: `Update file URLs`, `Rewrite URLs to builds.dotnet` -- **Rollbacks**: `Revert "Switch links..."` - -The EOL date example illustrates a key architectural tradeoff: with releases-index, metadata changes to any version require updating the root file. With hal-index, we give up rich information in the root file, but in exchange we can safely propagate useful information to many locations (authored in `9.0/_manifest.json`, generated into `9.0/manifest.json`, `9.0/index.json`, timeline indexes, etc.)—provided it doesn't violate the core rule: **the root `index.json` is never touched for version-specific changes**. - -Note that Patch Tuesday releases are batched—a single commit like [November 2025](https://github.com/dotnet/core/commit/484b00682d598f8d11a81607c257c1f0a099b84c) updates `releases-index.json`, `8.0/releases.json`, `9.0/releases.json`, and `10.0/releases.json` together (6,802 lines changed across 30 files). Even with batching, the root file still requires ~30 updates/year. - -These massive commits are difficult to review and analyze. Mixing markdown documentation with JSON data files in the same commit makes it hard to distinguish content changes from data updates. The hal-index design separates these concerns—JSON index files are generated automatically, while markdown content is authored separately. - -**Operational Risk:** These files are effectively mission-critical live-site APIs, driving hundreds of GBs of downloads monthly. Yet they require ~20-30 manual updates per year each, carrying risk of human error (as the fix commits demonstrate), cache invalidation complexity, and CDN propagation delays. - -**Key insight:** The hal-index root file (`index.json`) is updated ~1x/year when a new major version is added. The releases-index root file (`releases-index.json`) is updated ~30x/year. This makes the hal-index root file ideal for aggressive CDN caching, while the releases-index files are constantly-moving targets. - -## Query Comparison - -### Capability Summary - -| Query Type | hal-index | releases-index | Winner | -|------------|-----------|----------------|--------| -| List versions | ✅ | ✅ | releases-index (1.3x smaller) | -| Version lifecycle (EOL, LTS) | ✅ | ✅ | releases-index (1.3x smaller) | -| Latest patch per version | ✅ | ✅ | hal-index (23x smaller) | -| CVEs per version | ✅ | ✅ | hal-index (23x smaller) | -| CVEs per month | ✅ | ❌ | hal-index only | -| CVE details (severity, fixes) | ✅ | ❌ | hal-index only | -| Timeline navigation | ✅ | ❌ | hal-index only | -| SDK-first navigation | ✅ | ❌ | hal-index only | -| Version diff (severity, products) | ✅ | ⚠️ Partial | hal-index (15x smaller) | -| Breaking changes by category | ✅ | ❌ | hal-index only | -| EOL exposure analysis | ✅ | ⚠️ Partial | hal-index (with severity) | - -The sections below demonstrate each query pattern with working examples. - -### Version-Based Queries - -The following queries navigate the schema by major version (8.0, 9.0, 10.0), drilling down to specific patches and CVE details. - -#### Query: "What .NET versions are currently supported?" - -| Schema | Files Required | Total Transfer | -|--------|----------------|----------------| -| hal-index | `index.json` | **8 KB** | -| releases-index | `releases-index.json` | **6 KB** | - -**hal-index:** - -```bash -ROOT="https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" - -curl -s "$ROOT" | jq -r '._embedded.releases[] | select(.supported) | .version' -# 10.0 -# 9.0 -# 8.0 -``` - -**releases-index:** - -```bash -ROOT="https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json" - -curl -s "$ROOT" | jq -r '.["releases-index"][] | select(.["support-phase"] == "active") | .["channel-version"]' -# 10.0 -# 9.0 -# 8.0 -``` - -**Analysis:** - -- **Completeness:** ✅ Equal—both return the same list of supported versions. -- **Boolean vs enum:** The hal-index uses `supported: true`, a simple boolean. The releases-index uses `support-phase: "active"`, requiring knowledge of the enum vocabulary (active, maintenance, eol, preview, go-live). -- **Property naming:** The hal-index uses `select(.supported)` with dot notation. The releases-index requires `select(.["support-phase"] == "active")` with bracket notation and string comparison. -- **Query complexity:** The hal-index query is 30% shorter and more intuitive for someone unfamiliar with the schema. - -**Winner:** releases-index (**1.3x smaller** for basic version queries, but hal-index has better query ergonomics) - -### CVE Queries for Latest Security Patch - -#### Query: "What CVEs were fixed in the latest .NET 8.0 security patch?" - -| Schema | Files Required | Total Transfer | -|--------|----------------|----------------| -| hal-index | `index.json` → `8.0/index.json` → `8.0/8.0.21/index.json` | **52 KB** | -| releases-index | `releases-index.json` + `8.0/releases.json` | **1,239 KB** | - -**hal-index:** - -```bash -ROOT="https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" - -# Step 1: Get the 8.0 version href -VERSION_HREF=$(curl -s "$ROOT" | jq -r '._embedded.releases[] | select(.version == "8.0") | ._links.self.href') -# https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/8.0/index.json - -# Step 2: Get the latest security patch href -PATCH_HREF=$(curl -s "$VERSION_HREF" | jq -r '._links["latest-security"].href') -# https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/8.0/8.0.21/index.json - -# Step 3: Get the CVE records -curl -s "$PATCH_HREF" | jq -r '.cve_records[]' -# CVE-2025-55247 -# CVE-2025-55248 -# CVE-2025-55315 -``` - -**releases-index:** - -```bash -ROOT="https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json" - -# Step 1: Get the 8.0 releases.json URL -RELEASES_URL=$(curl -s "$ROOT" | jq -r '.["releases-index"][] | select(.["channel-version"] == "8.0") | .["releases.json"]') -# https://builds.dotnet.microsoft.com/dotnet/release-metadata/8.0/releases.json - -# Step 2: Find latest security release and get CVE IDs -curl -s "$RELEASES_URL" | jq -r '[.releases[] | select(.security == true)] | .[0] | .["cve-list"][] | .["cve-id"]' -# CVE-2025-55247 -# CVE-2025-55315 -# CVE-2025-55248 -``` - -**Analysis:** Both schemas produce the same CVE IDs. However: - -- **Completeness:** ✅ Equal—both return the CVE identifiers -- **Ergonomics:** The releases-index requires downloading a 1.2 MB file to extract 3 CVE IDs. The hal-index uses a dedicated `latest-security` link, avoiding iteration through all releases. -- **Link syntax:** Counterintuitively, the deeper HAL structure `._links.self.href` is more ergonomic than `.["releases.json"]` because snake_case enables dot notation throughout. The releases-index embeds URLs directly in properties, but kebab-case naming forces bracket notation. -- **Data efficiency:** hal-index is 23x smaller - -**Winner:** hal-index (**23x smaller**) - -### High Severity CVEs with Details - -#### Query: "What High+ severity CVEs were fixed in the latest .NET 8.0 security patch, with titles?" - -| Schema | Files Required | Total Transfer | -|--------|----------------|----------------| -| hal-index | `index.json` → `8.0/index.json` → `8.0/8.0.21/index.json` | **52 KB** | -| releases-index | `releases-index.json` + `8.0/releases.json` | **1,239 KB** (cannot answer) | - -**hal-index:** - -```bash -ROOT="https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" - -# Step 1: Get the 8.0 version href -VERSION_HREF=$(curl -s "$ROOT" | jq -r '._embedded.releases[] | select(.version == "8.0") | ._links.self.href') - -# Step 2: Get the latest security patch href -PATCH_HREF=$(curl -s "$VERSION_HREF" | jq -r '._links["latest-security"].href') - -# Step 3: Filter HIGH+ severity with titles -curl -s "$PATCH_HREF" | jq -r '._embedded.disclosures[] | select(.cvss_severity == "HIGH" or .cvss_severity == "CRITICAL") | "\(.id): \(.title) (\(.cvss_severity))"' -# CVE-2025-55247: .NET Denial of Service Vulnerability (HIGH) -# CVE-2025-55315: .NET Security Feature Bypass Vulnerability (CRITICAL) -``` - -**releases-index:** - -```bash -ROOT="https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json" - -# Step 1: Get the 8.0 releases.json URL -RELEASES_URL=$(curl -s "$ROOT" | jq -r '.["releases-index"][] | select(.["channel-version"] == "8.0") | .["releases.json"]') - -# Step 2: Find latest security release and get CVE IDs (best effort—no severity/title available) -curl -s "$RELEASES_URL" | jq -r '[.releases[] | select(.security == true)] | .[0] | .["cve-list"][] | "\(.["cve-id"]): (severity unknown) (title unknown)"' -# CVE-2025-55247: (severity unknown) (title unknown) -# CVE-2025-55315: (severity unknown) (title unknown) -# CVE-2025-55248: (severity unknown) (title unknown) -``` - -**Analysis:** - -- **Completeness:** ❌ The releases-index only provides CVE IDs and URLs to external CVE databases. It does not include severity scores, CVSS ratings, or vulnerability titles. To get this information, you would need to fetch each CVE URL individually from cve.mitre.org. -- **Ergonomics:** The hal-index embeds full CVE details (`cvss_severity`, `cvss_score`, `title`, `fixes`) directly in the patch index, enabling single-query filtering by severity. - -**Winner:** hal-index (releases-index cannot answer this query—CVE severity and titles are not available) - -### Servicing Version Diff - -#### Query: "What changed between .NET 8.0.15 and 8.0.22?" - -| Schema | Files Required | Total Transfer | -|--------|----------------|----------------| -| hal-index | `index.json` → `8.0/index.json` + 3 patch indexes | **~80 KB** | -| releases-index | `releases-index.json` + `8.0/releases.json` | **1,239 KB** (partial data) | - -**hal-index:** - -This query requires two passes: first to get the release summaries, then to fetch CVE details for severity filtering and affected products. - -```bash -ROOT="https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" - -# Configuration -MAJOR_VERSION="8.0" -FROM_PATCH=15 -TO_PATCH=22 -SEVERITY_FILTER="CRITICAL" # Minimum severity: CRITICAL, HIGH, MEDIUM, or LOW (all) - -# Step 1: Get the major version href -VERSION_HREF=$(curl -s "$ROOT" | jq -r --arg ver "$MAJOR_VERSION" '._embedded.releases[] | select(.version == $ver) | ._links.self.href') - -# Step 2: Get release summaries and security release URLs -VERSION_DATA=$(curl -s "$VERSION_HREF") - -# Extract security release URLs for CVE detail fetching -SECURITY_HREFS=$(echo "$VERSION_DATA" | jq -r --argjson from "$FROM_PATCH" --argjson to "$TO_PATCH" ' - [._embedded.releases[] | - select((.version | split(".")[2] | tonumber) > $from and (.version | split(".")[2] | tonumber) <= $to) | - select(.security) | - ._links.self.href] | .[] -') - -# Step 3: Fetch CVE details from each security release and aggregate -CVE_DETAILS=$(for HREF in $SECURITY_HREFS; do - curl -s "$HREF" | jq -c '._embedded.disclosures[]? | {id, cvss_severity, affected_products, title}' -done | jq -s 'unique_by(.id)') - -# Step 4: Generate the diff report with severity-filtered CVE IDs -echo "$VERSION_DATA" | jq -r --arg major "$MAJOR_VERSION" --argjson from "$FROM_PATCH" --argjson to "$TO_PATCH" \ - --arg severity "$SEVERITY_FILTER" --argjson cve_details "$CVE_DETAILS" ' - # Filter releases in range (excluding start, including end) - [._embedded.releases[] | - select((.version | split(".")[2] | tonumber) > $from and (.version | split(".")[2] | tonumber) <= $to) - ] as $releases | - - # Filter CVEs by minimum severity - [$cve_details[] | select( - ($severity == "LOW") or - ($severity == "MEDIUM" and (.cvss_severity == "MEDIUM" or .cvss_severity == "HIGH" or .cvss_severity == "CRITICAL")) or - ($severity == "HIGH" and (.cvss_severity == "HIGH" or .cvss_severity == "CRITICAL")) or - ($severity == "CRITICAL" and .cvss_severity == "CRITICAL") - )] as $filtered_cves | - - # Aggregate affected products across all CVEs - [$cve_details[].affected_products // [] | .[]] | unique | sort as $all_products | - - { - from_version: "\($major).\($from)", - to_version: "\($major).\($to)", - from_date: (._embedded.releases[] | select(.version == "\($major).\($from)") | .date | split("T")[0]), - to_date: (._embedded.releases[] | select(.version == "\($major).\($to)") | .date | split("T")[0]), - total_releases: ($releases | length), - security_releases: ([$releases[] | select(.security)] | length), - non_security_releases: ([$releases[] | select(.security | not)] | length), - total_cves: ([$releases[].cve_records? // [] | .[]] | unique | length), - cve_ids: [$filtered_cves[] | .id], - cve_severity_filter: $severity, - affected_products: $all_products, - releases: [$releases[] | {version, date: (.date | split("T")[0]), security, cve_count}] - } -' -``` - -**Output:** - -```json -{ - "from_version": "8.0.15", - "to_version": "8.0.22", - "from_date": "2025-04-08", - "to_date": "2025-11-11", - "total_releases": 7, - "security_releases": 3, - "non_security_releases": 4, - "total_cves": 5, - "cve_ids": [ - "CVE-2025-55315" - ], - "cve_severity_filter": "CRITICAL", - "affected_products": [ - "aspnetcore-runtime", - "dotnet-runtime", - "windowsdesktop-runtime" - ], - "releases": [ - { "version": "8.0.22", "date": "2025-11-11", "security": false, "cve_count": 0 }, - { "version": "8.0.21", "date": "2025-10-14", "security": true, "cve_count": 3 }, - { "version": "8.0.20", "date": "2025-09-09", "security": false, "cve_count": 0 }, - { "version": "8.0.19", "date": "2025-08-05", "security": false, "cve_count": 0 }, - { "version": "8.0.18", "date": "2025-07-08", "security": false, "cve_count": 0 }, - { "version": "8.0.17", "date": "2025-06-10", "security": true, "cve_count": 1 }, - { "version": "8.0.16", "date": "2025-05-22", "security": true, "cve_count": 1 } - ] -} -``` - -To include all CVEs regardless of severity: - -```bash -SEVERITY_FILTER="LOW" # LOW is the minimum, so this includes all CVEs -``` - -The script above outputs `from_date` and `to_date`. To calculate the time gap, pipe the output through an additional jq filter: - -```bash -# Pipe the output to calculate days between versions -... | jq -r ' - # Parse ISO dates and calculate difference - ((.to_date | strptime("%Y-%m-%d") | mktime) - - (.from_date | strptime("%Y-%m-%d") | mktime)) / 86400 | floor as $days | - . + { - days_behind: $days, - months_behind: (($days / 30) | floor) - } -' -``` - -This adds `days_behind` and `months_behind` to the output: - -```json -{ - "from_version": "8.0.15", - "to_version": "8.0.22", - "from_date": "2025-04-08", - "to_date": "2025-11-11", - "days_behind": 217, - "months_behind": 7, - ... -} -``` - -**releases-index:** - -```bash -ROOT="https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json" - -# Configuration -MAJOR_VERSION="8.0" -FROM_PATCH=15 -TO_PATCH=22 - -# Step 1: Get the releases.json URL for the major version -RELEASES_URL=$(curl -s "$ROOT" | jq -r --arg ver "$MAJOR_VERSION" '.["releases-index"][] | select(.["channel-version"] == $ver) | .["releases.json"]') - -# Step 2: Filter releases in range and aggregate -curl -s "$RELEASES_URL" | jq -r --arg major "$MAJOR_VERSION" --argjson from "$FROM_PATCH" --argjson to "$TO_PATCH" ' - # Filter releases in range (excluding start, including end) - [.releases[] | select( - (.["release-version"] | split(".")[2] | tonumber) > $from and - (.["release-version"] | split(".")[2] | tonumber) <= $to - )] as $releases | - - { - from_version: "\($major).\($from)", - to_version: "\($major).\($to)", - from_date: ([.releases[] | select(.["release-version"] == "\($major).\($from)")] | .[0] | .["release-date"]), - to_date: ([.releases[] | select(.["release-version"] == "\($major).\($to)")] | .[0] | .["release-date"]), - total_releases: ($releases | length), - security_releases: ([$releases[] | select(.security)] | length), - non_security_releases: ([$releases[] | select(.security | not)] | length), - total_cves: ([$releases[].["cve-list"]? // [] | .[]] | unique | length), - cve_ids: ([$releases[].["cve-list"]? // [] | .[] | .["cve-id"]] | unique | sort), - cve_severity_filter: "(not available)", - affected_products: "(not available)", - releases: [$releases[] | { - version: .["release-version"], - date: .["release-date"], - security, - cve_count: ([.["cve-list"]? // [] | .[]] | length) - }] - } -' -``` - -**Output:** - -```json -{ - "from_version": "8.0.15", - "to_version": "8.0.22", - "from_date": "2025-04-08", - "to_date": "2025-11-11", - "total_releases": 7, - "security_releases": 3, - "non_security_releases": 4, - "total_cves": 5, - "cve_ids": [ - "CVE-2025-26646", - "CVE-2025-30399", - "CVE-2025-55247", - "CVE-2025-55248", - "CVE-2025-55315" - ], - "cve_severity_filter": "(not available)", - "affected_products": "(not available)", - "releases": [ - { "version": "8.0.22", "date": "2025-11-11", "security": false, "cve_count": 0 }, - { "version": "8.0.21", "date": "2025-10-14", "security": true, "cve_count": 3 }, - { "version": "8.0.20", "date": "2025-09-09", "security": false, "cve_count": 0 }, - { "version": "8.0.19", "date": "2025-08-05", "security": false, "cve_count": 0 }, - { "version": "8.0.18", "date": "2025-07-08", "security": false, "cve_count": 0 }, - { "version": "8.0.17", "date": "2025-06-10", "security": true, "cve_count": 1 }, - { "version": "8.0.16", "date": "2025-05-22", "security": true, "cve_count": 1 } - ] -} -``` - -**Analysis:** - -- **Completeness:** ⚠️ Partial—the releases-index can count releases and list CVE IDs, but cannot provide CVE severity, affected products, or detailed CVE information without fetching external CVE URLs. -- **Severity filtering:** The hal-index allows filtering `cve_ids` to specific severity levels (CRITICAL, HIGH, etc.) via the `SEVERITY_FILTER` variable, while `total_cves` always shows the complete count. -- **Affected products:** The hal-index aggregates all affected products across the version range (e.g., `dotnet-runtime`, `aspnetcore-runtime`), enabling teams to identify which components need patching. -- **Executive reporting:** For CIO/CTO reporting, the hal-index provides actionable data (severity-filtered CVEs, affected products) while the releases-index only provides CVE IDs that require manual lookup. - -**Winner:** hal-index (**15x smaller**, with CVE severity filtering and affected products) - -### Breaking Changes Summary - -#### Query: "How many breaking changes are in .NET 10, by category?" - -| Schema | Files Required | Total Transfer | -|--------|----------------|----------------| -| hal-index | `index.json` → `10.0/index.json` → `10.0/breaking-changes.json` | **~45 KB** | -| releases-index | N/A | N/A (not available) | - -**hal-index:** - -```bash -ROOT="https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" -VERSION="10.0" - -# Step 1: Get the version index -VERSION_HREF=$(curl -s "$ROOT" | jq -r --arg ver "$VERSION" '._embedded.releases[] | select(.version == $ver) | ._links.self.href') - -# Step 2: Get the breaking-changes.json link -BC_HREF=$(curl -s "$VERSION_HREF" | jq -r '._links["breaking-changes-json"].href') - -# Step 3: Get counts by category -curl -s "$BC_HREF" | jq -r ' - "Total: \(.breaking_change_count)\n", - ([.breaks[].category] | group_by(.) | map({category: .[0], count: length}) | sort_by(-.count) | .[] | " \(.category): \(.count)") -' -``` - -**Output:** - -```text -Total: 83 - - sdk: 23 - core-libraries: 16 - aspnet-core: 9 - cryptography: 8 - extensions: 6 - windows-forms: 6 - interop: 3 - networking: 3 - reflection: 2 - serialization: 2 - wpf: 2 - containers: 1 - globalization: 1 - install-tool: 1 -``` - -**releases-index:** Not available. - -**Winner:** hal-index only - -### Breaking Changes by Category - -#### Query: "What are the core-libraries breaking changes in .NET 10?" - -| Schema | Files Required | Total Transfer | -|--------|----------------|----------------| -| hal-index | `index.json` → `10.0/index.json` → `10.0/breaking-changes.json` | **~45 KB** | -| releases-index | N/A | N/A (not available) | - -**hal-index:** - -```bash -ROOT="https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" -VERSION="10.0" -CATEGORY="core-libraries" - -# Step 1: Get the version index -VERSION_HREF=$(curl -s "$ROOT" | jq -r --arg ver "$VERSION" '._embedded.releases[] | select(.version == $ver) | ._links.self.href') - -# Step 2: Get the breaking-changes.json link -BC_HREF=$(curl -s "$VERSION_HREF" | jq -r '._links["breaking-changes-json"].href') - -# Step 3: Get breaking changes for category -curl -s "$BC_HREF" | jq -r --arg cat "$CATEGORY" '.breaks[] | select(.category == $cat) | "- \(.title)"' -``` - -**Output:** - -```text -- ActivitySource.CreateActivity and ActivitySource.StartActivity behavior changes -- System.Linq.AsyncEnumerable in .NET 10 -- BufferedStream.WriteByte no longer performs implicit flush -- C# 14 overload resolution with span parameters -- Default trace context propagator updated to W3C standard -- 'DynamicallyAccessedMembers' annotation removed from 'DefaultValueAttribute' ctor -- DriveInfo.DriveFormat returns Linux filesystem types -- FilePatternMatch.Stem changed to non-nullable -- Consistent shift behavior in generic math -- Specifying explicit struct Size disallowed with InlineArray -- LDAP DirectoryControl parsing is now more stringent -- MacCatalyst version normalization -- .NET 10 obsoletions with custom IDs -- .NET runtime no longer provides default termination signal handler -- Arm64 SVE nonfaulting loads require mask parameter -- GnuTarEntry and PaxTarEntry exclude atime and ctime by default -``` - -**releases-index:** Not available. - -**Winner:** hal-index only - -### Breaking Changes Documentation URLs - -#### Query: "Get the raw markdown URLs for core-libraries breaking changes (for LLM context)" - -| Schema | Files Required | Total Transfer | -|--------|----------------|----------------| -| hal-index | `index.json` → `10.0/index.json` → `10.0/breaking-changes.json` | **~45 KB** | -| releases-index | N/A | N/A (not available) | - -**hal-index:** - -```bash -ROOT="https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" -VERSION="10.0" -CATEGORY="core-libraries" - -# Step 1: Get the version index -VERSION_HREF=$(curl -s "$ROOT" | jq -r --arg ver "$VERSION" '._embedded.releases[] | select(.version == $ver) | ._links.self.href') - -# Step 2: Get the breaking-changes.json link -BC_HREF=$(curl -s "$VERSION_HREF" | jq -r '._links["breaking-changes-json"].href') - -# Step 3: Get raw documentation URLs for category -curl -s "$BC_HREF" | jq -r --arg cat "$CATEGORY" ' - .breaks[] | select(.category == $cat) | .references[] | select(.type == "documentation-source") | .url -' -``` - -**Output:** - -```text -https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/core-libraries/10.0/activity-sampling.md -https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/core-libraries/10.0/asyncenumerable.md -https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/core-libraries/10.0/bufferedstream-writebyte-flush.md -https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/core-libraries/10.0/csharp-overload-resolution.md -https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/core-libraries/10.0/default-trace-context-propagator.md -https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/core-libraries/10.0/defaultvalueattribute-dynamically-accessed-members.md -https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/core-libraries/10.0/driveinfo-driveformat-linux.md -https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/core-libraries/10.0/filepatternmatch-stem-nonnullable.md -https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/core-libraries/10.0/generic-math.md -https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/core-libraries/10.0/inlinearray-explicit-size-disallowed.md -https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/core-libraries/10.0/ldap-directorycontrol-parsing.md -https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/core-libraries/10.0/maccatalyst-version-normalization.md -https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/core-libraries/10.0/obsolete-apis.md -https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/core-libraries/10.0/sigterm-signal-handler.md -https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/core-libraries/10.0/sve-nonfaulting-loads-mask-parameter.md -https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/core-libraries/10.0/tar-atime-ctime-default.md -``` - -**releases-index:** Not available. - -**Analysis:** - -- **LLM context:** These raw markdown URLs can be fetched and fed directly to an LLM for analysis or migration assistance. -- **Reference types:** Each breaking change includes multiple reference types (`documentation`, `documentation-source`, `announcement`)—the `documentation-source` type provides the raw markdown. - -**Winner:** hal-index only - -### Timeline-Based Queries - -The following queries demonstrate the hal-index timeline navigation model, which organizes releases chronologically rather than by version. - -### Recent CVEs Across All Versions - -#### Query: "What CVEs were fixed in the last 2 security releases?" - -| Schema | Files Required | Total Transfer | -|--------|----------------|----------------| -| hal-index | `timeline/index.json` → `timeline/2025/index.json` | **23 KB** | -| releases-index | `releases-index.json` + 3 releases.json | **2,448 KB** | - -**hal-index:** - -```bash -TIMELINE="https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/index.json" - -# Step 1: Get the latest year href -YEAR_HREF=$(curl -s "$TIMELINE" | jq -r '._embedded.years[0]._links.self.href') - -# Step 2: Get the last 2 security months with CVEs -curl -s "$YEAR_HREF" | jq -r '[._embedded.months[] | select(.security)] | .[0:2] | .[] | "\(.month)/2025: \(.cve_records | join(", "))"' -# 10/2025: CVE-2025-55248, CVE-2025-55315, CVE-2025-55247 -# 06/2025: CVE-2025-30399 -``` - -**releases-index:** - -```bash -ROOT="https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json" - -# Get all supported version releases.json URLs -URLS=$(curl -s "$ROOT" | jq -r '.["releases-index"][] | select(.["support-phase"] == "active") | .["releases.json"]') - -# For each version, find security releases and collect CVEs (requires multiple large fetches) -for URL in $URLS; do - curl -s "$URL" | jq -r '.releases[] | select(.security == true) | "\(.["release-date"]): \([.["cve-list"][]? | .["cve-id"]] | join(", "))"' -done | sort -r | head -6 -# 2025-10-14: CVE-2025-55247, CVE-2025-55315, CVE-2025-55248 -# 2025-10-14: CVE-2025-55247, CVE-2025-55315, CVE-2025-55248 -# 2025-10-14: CVE-2025-55247, CVE-2025-55315, CVE-2025-55248 -# 2025-06-10: CVE-2025-30399 -# 2025-06-10: CVE-2025-30399 -# 2025-06-10: CVE-2025-30399 -``` - -**Analysis:** - -- **Completeness:** ⚠️ Partial—the releases-index can find CVEs by date, but produces duplicate entries (one per version) and cannot group by month without additional post-processing. -- **Ergonomics:** The hal-index timeline is purpose-built for chronological queries. The releases-index requires fetching all version files (2.4 MB) and manually correlating dates to find "last 2 security releases." -- **Data model:** The releases-index organizes by version; the hal-index timeline organizes by date. For "recent CVEs" queries, the timeline model is fundamentally better suited. - -**Winner:** hal-index (**107x smaller**) - -### CVE Details for a Month - -#### Query: "What CVEs were disclosed in January 2025 with full details?" - -| Schema | Files Required | Total Transfer | -|--------|----------------|----------------| -| hal-index | `timeline/index.json` → `timeline/2025/index.json` → `timeline/2025/01/cve.json` | **37 KB** | -| releases-index | All releases.json (13 versions) | **8.2 MB** (cannot answer) | - -**hal-index:** - -```bash -TIMELINE="https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/index.json" - -# Step 1: Get 2025 year href -YEAR_HREF=$(curl -s "$TIMELINE" | jq -r '._embedded.years[] | select(.year == "2025") | ._links.self.href') -# https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/index.json - -# Step 2: Get January cve.json href -CVE_HREF=$(curl -s "$YEAR_HREF" | jq -r '._embedded.months[] | select(.month == "01") | ._links["cve-json"].href') -# https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/01/cve.json - -# Step 3: Get CVE details -curl -s "$CVE_HREF" | jq -r '.disclosures[] | "\(.id): \(.problem)"' -# CVE-2025-21171: .NET Remote Code Execution Vulnerability -# CVE-2025-21172: .NET and Visual Studio Remote Code Execution Vulnerability -# CVE-2025-21176: .NET and Visual Studio Remote Code Execution Vulnerability -# CVE-2025-21173: .NET Elevation of Privilege Vulnerability -``` - -**releases-index:** - -```bash -ROOT="https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json" - -# Must fetch ALL version releases.json files—cannot filter by "currently supported" -# because we need versions that were supported in January 2025, not today -URLS=$(curl -s "$ROOT" | jq -r '.["releases-index"][] | .["releases.json"]') - -# Find January 2025 releases and get CVE IDs (no details available) -for URL in $URLS; do - curl -s "$URL" | jq -r '.releases[] | select(.["release-date"] | startswith("2025-01")) | select(.security == true) | .["cve-list"][]? | "\(.["cve-id"]): (no description available)"' -done | sort -u -# CVE-2025-21171: (no description available) -# CVE-2025-21172: (no description available) -# CVE-2025-21173: (no description available) -# CVE-2025-21176: (no description available) -``` - -**Analysis:** - -- **Completeness:** ❌ The releases-index provides only CVE IDs. The query asks for "full details" including problem descriptions, CVSS scores, affected products, and fix commits—none of which are available. -- **Historical queries:** The releases-index has no way to determine which versions were supported at a given point in time. To reliably find all CVEs for January 2025, you must fetch *every* version's releases.json file (not just currently supported versions), significantly increasing data transfer. -- **Ergonomics:** The hal-index provides a dedicated `cve.json` file per month with complete CVE records. The releases-index requires fetching all version files and provides only minimal data. - -**Winner:** hal-index (**221x smaller**, and releases-index cannot answer this query—CVE details are not available) - -### Security Patches in the Last 12 Months - -#### Query: "List all CVEs fixed in the last 12 months" - -| Schema | Files Required | Total Transfer | -|--------|----------------|----------------| -| hal-index | `timeline/index.json` → up to 12 month indexes (via `prev` links) | **~90 KB** | -| releases-index | All version releases.json files | **2.4+ MB** | - -**hal-index:** - -```bash -TIMELINE="https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/index.json" - -# Step 1: Get the latest month href -MONTH_HREF=$(curl -s "$TIMELINE" | jq -r '._embedded.years[0]._links["latest-month"].href') - -# Step 2: Walk back 12 months using prev links, collecting security CVEs -for i in {1..12}; do - DATA=$(curl -s "$MONTH_HREF") - YEAR_MONTH=$(echo "$DATA" | jq -r '"\(.year)-\(.month)"') - SECURITY=$(echo "$DATA" | jq -r '.security') - if [ "$SECURITY" = "true" ]; then - CVES=$(echo "$DATA" | jq -r '[._embedded.disclosures[].id] | join(", ")') - echo "$YEAR_MONTH: $CVES" - fi - MONTH_HREF=$(echo "$DATA" | jq -r '._links.prev.href // empty') - [ -z "$MONTH_HREF" ] && break -done -# 2025-10: CVE-2025-55248, CVE-2025-55315, CVE-2025-55247 -# 2025-06: CVE-2025-30399 -# 2025-05: CVE-2025-26646 -# 2025-04: CVE-2025-26682 -# 2025-03: CVE-2025-24070 -# 2025-01: CVE-2025-21171, CVE-2025-21172, CVE-2025-21176, CVE-2025-21173 -``` - -**releases-index:** - -```bash -ROOT="https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json" - -# Get all supported version releases.json URLs -URLS=$(curl -s "$ROOT" | jq -r '.["releases-index"][] | select(.["support-phase"] == "active") | .["releases.json"]') - -# For each version, find security releases in the last 12 months -CUTOFF="2024-12-01" -for URL in $URLS; do - curl -s "$URL" | jq -r --arg cutoff "$CUTOFF" ' - .releases[] | - select(.security == true) | - select(.["release-date"] >= $cutoff) | - "\(.["release-date"]): \([.["cve-list"][]? | .["cve-id"]] | join(", "))"' -done | sort -u | sort -r -# 2025-10-14: CVE-2025-55247, CVE-2025-55315, CVE-2025-55248 -# 2025-06-10: CVE-2025-30399 -# 2025-05-22: CVE-2025-26646 -# 2025-04-08: CVE-2025-26682 -# 2025-03-11: CVE-2025-24070 -# 2025-01-14: CVE-2025-21172, CVE-2025-21173, CVE-2025-21176 -``` - -**Analysis:** - -- **Completeness:** ⚠️ Partial—the releases-index can list CVEs by date, but notice CVE-2025-21171 is missing (it only affected .NET 9.0 which was still in its first patch cycle). The output also shows exact dates rather than grouped by month. -- **Ergonomics:** The hal-index uses `prev` links for natural backward navigation. The releases-index requires downloading all version files (2.4+ MB), filtering by date, and deduplicating results. -- **Navigation model:** The hal-index timeline is designed for chronological traversal. The releases-index has no concept of time-based navigation. - -**Winner:** hal-index (**27x smaller**) - -### Critical CVE This Month - -#### Query: "Is there a critical CVE in any supported release this month?" (November 2025) - -| Schema | Files Required | Total Transfer | -|--------|----------------|----------------| -| hal-index | `timeline/index.json` → `timeline/2025/index.json` → `timeline/2025/11/index.json` | **28 KB** | -| releases-index | `releases-index.json` + all supported releases.json | **2.4+ MB** (incomplete—no severity data) | - -**hal-index:** - -```bash -TIMELINE="https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/index.json" - -# Step 1: Get 2025 year href -YEAR_HREF=$(curl -s "$TIMELINE" | jq -r '._embedded.years[] | select(.year == "2025") | ._links.self.href') - -# Step 2: Get November month href -MONTH_HREF=$(curl -s "$YEAR_HREF" | jq -r '._embedded.months[] | select(.month == "11") | ._links.self.href') - -# Step 3: Check for CRITICAL CVEs -curl -s "$MONTH_HREF" | jq -r '._embedded.disclosures // [] | .[] | select(.cvss_severity == "CRITICAL") | "\(.id): \(.title)"' -# (no critical CVEs this month) -``` - -**releases-index:** - -```bash -ROOT="https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json" - -# Get all supported version releases.json URLs -URLS=$(curl -s "$ROOT" | jq -r '.["releases-index"][] | select(.["support-phase"] == "active") | .["releases.json"]') - -# Find November 2025 releases and check for CVEs (cannot determine severity) -for URL in $URLS; do - curl -s "$URL" | jq -r ' - .releases[] | - select(.["release-date"] | startswith("2025-11")) | - select(.security == true) | - .["cve-list"][]? | "\(.["cve-id"]): (severity unknown)"' -done | sort -u -# (no output—November 2025 had no security releases) -``` - -**Analysis:** - -- **Completeness:** ❌ The releases-index cannot answer this query. Even if there were CVEs in November, the schema only provides CVE IDs and URLs—no severity information. You would need to fetch each CVE URL from cve.mitre.org and parse the CVSS score. -- **Ergonomics:** The hal-index embeds `cvss_severity` directly in the disclosure records, enabling single-query filtering for CRITICAL vulnerabilities. -- **Use case:** This is a common security operations query ("Do I need to patch urgently?"). The hal-index answers it in 28 KB; the releases-index cannot answer it at all. - -**Winner:** hal-index (**88x smaller**, and releases-index cannot answer this query—CVE severity is not available) - -### Hybrid Queries - -The following queries combine version and timeline navigation, demonstrating the full power of the hal-index design. - -### CVE Exposure for EOL Version - -#### Query: "If I'm still on .NET 6, which CVEs am I likely exposed to?" - -This query finds all CVEs fixed in supported versions since .NET 6 went end-of-life. If you're running any .NET 6 version, you're potentially exposed to every vulnerability patched after .NET 6 stopped receiving updates. - -| Schema | Files Required | Total Transfer | -|--------|----------------|----------------| -| hal-index | `index.json` → `6.0/index.json` → timeline months (via `release-month` + `next` links) | **~50-100 KB** | -| releases-index | `releases-index.json` + all supported releases.json | **2.4+ MB** (partial—no severity data) | - -**hal-index:** - -```bash -ROOT="https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" - -# Configuration -EOL_VERSION="6.0" - -# Step 1: Get the EOL version's last patch and its timeline month link -VERSION_HREF=$(curl -s "$ROOT" | jq -r --arg ver "$EOL_VERSION" '._embedded.releases[] | select(.version == $ver) | ._links.self.href') -VERSION_DATA=$(curl -s "$VERSION_HREF") -LAST_PATCH=$(echo "$VERSION_DATA" | jq -r '._embedded.releases[0]') -LAST_PATCH_DATE=$(echo "$LAST_PATCH" | jq -r '.date | split("T")[0]') -MONTH_HREF=$(echo "$LAST_PATCH" | jq -r '._links["release-month"].href') -echo "Last .NET $EOL_VERSION patch: $LAST_PATCH_DATE" -# Last .NET 6.0 patch: 2024-11-12 - -# Step 2: Walk forward from EOL month, collecting CVEs -echo "" -echo "CVEs fixed after .NET $EOL_VERSION EOL:" -echo "========================================" - -CURRENT_HREF="$MONTH_HREF" -while [ -n "$CURRENT_HREF" ]; do - DATA=$(curl -s "$CURRENT_HREF") - - # Move to next month first (we want CVEs AFTER the EOL month) - CURRENT_HREF=$(echo "$DATA" | jq -r '._links.next.href // empty') - [ -z "$CURRENT_HREF" ] && break - - DATA=$(curl -s "$CURRENT_HREF") - YEAR_MONTH=$(echo "$DATA" | jq -r '"\(.year)-\(.month)"') - SECURITY=$(echo "$DATA" | jq -r '.security') - - if [ "$SECURITY" = "true" ]; then - echo "" - echo "$YEAR_MONTH:" - echo "$DATA" | jq -r '._embedded.disclosures[] | " \(.id) [\(.cvss_severity)]: \(.title)"' - fi - - CURRENT_HREF=$(echo "$DATA" | jq -r '._links.next.href // empty') -done -``` - -**Output:** - -```text -Last .NET 6.0 patch: 2024-11-12 - -CVEs fixed after .NET 6.0 EOL: -======================================== - -2025-01: - CVE-2025-21171 [HIGH]: .NET Remote Code Execution Vulnerability - CVE-2025-21172 [HIGH]: .NET and Visual Studio Remote Code Execution Vulnerability - CVE-2025-21173 [MEDIUM]: .NET Elevation of Privilege Vulnerability - CVE-2025-21176 [HIGH]: .NET and Visual Studio Remote Code Execution Vulnerability - -2025-03: - CVE-2025-24070 [HIGH]: ASP.NET Core Elevation of Privilege Vulnerability - -2025-04: - CVE-2025-26682 [HIGH]: .NET Denial of Service Vulnerability - -2025-05: - CVE-2025-26646 [MEDIUM]: .NET Spoofing Vulnerability - -2025-06: - CVE-2025-30399 [HIGH]: .NET Remote Code Execution Vulnerability - -2025-10: - CVE-2025-55247 [HIGH]: .NET Denial of Service Vulnerability - CVE-2025-55248 [MEDIUM]: .NET Denial of Service Vulnerability - CVE-2025-55315 [CRITICAL]: .NET Security Feature Bypass Vulnerability -``` - -**releases-index:** - -```bash -ROOT="https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json" - -# Configuration -EOL_VERSION="6.0" - -# Step 1: Get the EOL version's last patch date -EOL_RELEASES_URL=$(curl -s "$ROOT" | jq -r --arg ver "$EOL_VERSION" '.["releases-index"][] | select(.["channel-version"] == $ver) | .["releases.json"]') -LAST_PATCH_DATE=$(curl -s "$EOL_RELEASES_URL" | jq -r '.releases[0]["release-date"]') -echo "Last .NET $EOL_VERSION patch: $LAST_PATCH_DATE" - -# Step 2: Get all supported version releases.json URLs -URLS=$(curl -s "$ROOT" | jq -r '.["releases-index"][] | select(.["support-phase"] == "active") | .["releases.json"]') - -# Step 3: Find all security releases after the EOL date -echo "" -echo "CVEs fixed after .NET $EOL_VERSION EOL:" -echo "========================================" - -for URL in $URLS; do - curl -s "$URL" | jq -r --arg cutoff "$LAST_PATCH_DATE" ' - .releases[] | - select(.security == true) | - select(.["release-date"] > $cutoff) | - . as $rel | .["cve-list"][]? | "\($rel["release-date"]): \(.["cve-id"]) (severity unknown)"' -done | sort -u -``` - -**Output:** - -```text -Last .NET 6.0 patch: 2024-11-12 - -CVEs fixed after .NET 6.0 EOL: -======================================== -2025-01-14: CVE-2025-21172 (severity unknown) -2025-01-14: CVE-2025-21173 (severity unknown) -2025-01-14: CVE-2025-21176 (severity unknown) -2025-03-11: CVE-2025-24070 (severity unknown) -2025-04-08: CVE-2025-26682 (severity unknown) -2025-05-13: CVE-2025-26646 (severity unknown) -2025-06-10: CVE-2025-30399 (severity unknown) -2025-10-14: CVE-2025-55247 (severity unknown) -2025-10-14: CVE-2025-55248 (severity unknown) -2025-10-14: CVE-2025-55315 (severity unknown) -``` - -**Analysis:** - -- **Completeness:** ⚠️ Partial—the releases-index finds CVE IDs but misses CVE-2025-21171 (only affected .NET 9.0, not in "active" support phase query). More critically, it cannot provide severity or titles. -- **Actionability:** The hal-index output is immediately actionable—security teams can see that CVE-2025-55315 is CRITICAL and prioritize accordingly. The releases-index output requires manual lookup of each CVE. -- **Cross-index navigation:** The hal-index `release-month` link connects version and timeline indexes directly—no need to parse dates or search the timeline. Combined with `next` links, this enables natural forward traversal from any release. -- **Executive reporting:** "You're exposed to 1 CRITICAL, 6 HIGH, and 3 MEDIUM vulnerabilities" vs "You're exposed to 10 CVEs of unknown severity." - -**Winner:** hal-index (releases-index provides incomplete data with no severity information) - -## Cache Coherency - -### releases-index Problem - -```text -Client cache state: -├── releases-index.json (fetched now) -│ └── "latest-release": "9.0.12" ← Just updated -└── 9.0/releases.json (fetched 1 hour ago) - └── releases[0]: "9.0.11" ← Stale! - -Result: Client sees 9.0.12 as latest but can't find its data -``` - -### hal-index Solution - -```text -Client cache state: -├── index.json (fetched now) -│ └── _embedded.releases[]: includes 9.0 summary -└── 9.0/index.json (fetched now) - └── _embedded.releases[0]: "9.0.12" with full data - -Result: Each file is a consistent snapshot -``` - -The HAL `_embedded` pattern ensures that any data referenced within a document is included in that document. There are no "dangling pointers" to data that might not exist in a cached copy of another file. - -## Summary - -| Metric | hal-index | releases-index | -|--------|-------------|----------------| -| Basic version queries | 8 KB | 6 KB | -| CVE queries (latest security patch) | 52 KB | 1,239 KB | -| Recent CVEs (last 2 security releases) | 23 KB | 2.4 MB | -| CVEs in last 12 months | ~90 KB | 2.4 MB | -| Version diff with severity + products | ~80 KB | 1,239 KB (partial—no severity/products) | -| Cache coherency | ✅ Atomic | ❌ TTL mismatch risk | -| Query syntax | snake_case (dot notation) | kebab-case (bracket notation) | -| Link traversal | `._links.self.href` | `.["releases.json"]` | -| Boolean filters | `supported`, `security` | `support-phase == "active"` | -| CVE details | ✅ Full | ❌ ID + URL only | -| Timeline navigation | ✅ | ❌ | - -The hal-index schema is optimized for the queries that matter most to security operations, while maintaining cache coherency across CDN deployments. The use of boolean properties (`supported`) instead of enum comparisons (`support-phase == "active"`) reduces query complexity and eliminates the need to know the vocabulary of valid enum values. Counterintuitively, the deeper HAL link structure (`._links.self.href`) is more ergonomic than flat URL properties (`.["releases.json"]`) because consistent snake_case naming enables dot notation throughout the query path. diff --git a/accepted/2025/release-notes-graph/metrics/README.md b/accepted/2025/release-notes-graph/metrics/README.md new file mode 100644 index 000000000..13e1e9747 --- /dev/null +++ b/accepted/2025/release-notes-graph/metrics/README.md @@ -0,0 +1,45 @@ +# Metrics + +Query cost comparison tests from [richlander/release-graph-eval-results](https://github.com/richlander/release-graph-eval-results). + +## Schema Comparison Summary + +Three schema approaches are compared across 15 real-world query scenarios. + +| Test | Query | `llms.json` | `index.json` | `releases-index.json` | +|------|-------|----------:|-----------:|--------------------:| +| T1 | Supported .NET versions | **5 KB** | **5 KB** | 6 KB | +| T2 | Latest patch details | **5 KB** | 14 KB | 6 KB | +| T3 | Security patches since date | **14 KB** | 34 KB | 1,269 KB | +| T4 | EOL version CVE details | 45 KB | **40 KB** | 1,612 KB | +| T5 | CVE timeline with code fixes | **25 KB** | 30 KB | ✗ | +| T6 | Security process analysis | **450 KB** | 455 KB | ✗ | +| T7 | High-impact breaking changes | **117 KB** | 126 KB | ✗ | +| T8 | Code migration guidance | **122 KB** | 131 KB | ✗ | +| T9 | What's new in runtime | **24 KB** | 33 KB | ✗ | +| T10 | Security audit (version check) | **14 KB** | 34 KB | 1,269 KB | +| T11 | Minimum libc version | **17 KB** | 26 KB | ✗ | +| T12 | Docker setup with SDK | **36 KB** | 45 KB | 25 KB P | +| T13 | TFM support check | 45 KB | 50 KB | **6 KB** | +| T14 | Package CVE check | **60 KB** | 65 KB | ✗ | +| T15 | Target platform versions | **11 KB** | 20 KB | ✗ | +| | **Winner** | **13** | **2** | **1** | + +**Legend:** Smaller is better. **Bold** = winner. **P** = partial answer. **✗** = cannot answer. + +### Key Findings + +- **`llms.json`** optimizes for AI agent workflows with `_embedded.patches` providing direct links to manifests, security patches, and downloads without intermediate navigation. +- **`index.json`** provides complete graph traversal and works for all queries, but requires more fetches for common operations. +- **`releases-index.json`** lacks CVE severity, code fixes, breaking changes, what's new content, libc requirements, and target frameworks—making it unsuitable for upgrade planning and security analysis. + +## Contents + +| Document | Description | +|----------|-------------| +| [Index Discovery](index-discovery.md) | HAL discovery patterns—exploring `_links` and `_embedded` | +| [Easy Questions (Q1)](easy-questions.md) | Version queries answerable from embedded data | +| [CVE Stress Tests (Q2)](cve-stress-test.md) | Timeline navigation, severity filtering, code fixes | +| [Upgrade and What's New (Q3)](upgrade-whats-new.md) | Breaking changes and migration guidance | +| [Interacting with Environment (Q4)](interacting-with-environment.md) | Shell output parsing, libc checks, Docker setup | +| [Project File Analysis (Q5)](project-file-analysis.md) | TFM support, package CVEs, target platforms | diff --git a/accepted/2025/release-notes-graph/metrics/cve-stress-test.md b/accepted/2025/release-notes-graph/metrics/cve-stress-test.md new file mode 100644 index 000000000..18dcdd403 --- /dev/null +++ b/accepted/2025/release-notes-graph/metrics/cve-stress-test.md @@ -0,0 +1,299 @@ +# CVE Stress Tests (Q2-Hard) + +Query cost comparison for Q2-Hard category tests from [release-graph-eval](https://github.com/dotnet/release-graph-eval). + +See [overview.md](../overview.md) for design context, file characteristics, and link relation discovery. + +## T4: .NET 6 EOL Details + +**Query:** "When did .NET 6 go EOL, when was the last .NET 6 security patch and what CVEs did it fix?" + +| Schema | Files Required | Total Transfer | +|--------|----------------|----------------| +| llms-index | `llms.json` → `index.json` → `6.0/index.json` → `6.0/6.0.35/index.json` | **45 KB** | +| hal-index | `index.json` → `6.0/index.json` → `6.0/6.0.35/index.json` | **40 KB** | +| releases-index | `releases-index.json` → `6.0/releases.json` | **1,612 KB** | + +**llms-index:** EOL versions are not in `_embedded.patches`, so navigation through the release root is required: + +```bash +# Step 1: Get root link (6.0 not in _embedded.patches since it's EOL) +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/llms.json" | jq -r '._links["root"].href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/index.json + +# Step 2: Get 6.0 version href +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" | jq -r '._embedded.releases[] | select(.version == "6.0") | ._links.self.href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/6.0/index.json + +# Step 3: Get EOL date +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/6.0/index.json" | jq -r '"EOL: \(.eol_date | split("T")[0])"' +# EOL: 2024-11-12 + +# Step 4: Get last security patch details +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/6.0/index.json" | jq -r '._links["latest-security-patch"].href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/6.0/6.0.35/index.json + +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/6.0/6.0.35/index.json" | jq -r '"Last security: \(.version) (\(.date | split("T")[0])) | CVEs: \(.cve_records | join(", "))"' +# Last security: 6.0.35 (2024-10-08) | CVEs: CVE-2024-43483, CVE-2024-43484, CVE-2024-43485 +``` + +**hal-index:** + +```bash +# Step 1: Get 6.0 version href +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" | jq -r '._embedded.releases[] | select(.version == "6.0") | ._links.self.href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/6.0/index.json + +# Step 2: Get EOL date +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/6.0/index.json" | jq -r '"EOL: \(.eol_date | split("T")[0])"' +# EOL: 2024-11-12 + +# Step 3: Get last security patch details +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/6.0/index.json" | jq -r '._links["latest-security-patch"].href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/6.0/6.0.35/index.json + +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/6.0/6.0.35/index.json" | jq -r '"Last security: \(.version) (\(.date | split("T")[0])) | CVEs: \(.cve_records | join(", "))"' +# Last security: 6.0.35 (2024-10-08) | CVEs: CVE-2024-43483, CVE-2024-43484, CVE-2024-43485 +``` + +**releases-index:** + +```bash +# Step 1: Get EOL date from root (available inline) +curl -s "https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json" | jq -r '.["releases-index"][] | select(.["channel-version"] == "6.0") | "EOL: \(.["eol-date"])"' +# EOL: 2024-11-12 + +# Step 2: Get 6.0 releases.json URL and find last security patch +curl -s "https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json" | jq -r '.["releases-index"][] | select(.["channel-version"] == "6.0") | .["releases.json"]' +# https://builds.dotnet.microsoft.com/dotnet/release-metadata/6.0/releases.json + +curl -s "https://builds.dotnet.microsoft.com/dotnet/release-metadata/6.0/releases.json" | jq -r ' + [.releases[] | select(.security == true)][0] | + "Last security: \(.["release-version"]) (\(.["release-date"])) | CVEs: \([.["cve-list"][]?["cve-id"]] | join(", "))"' +# Last security: 6.0.35 (2024-10-08) | CVEs: CVE-2024-43483, CVE-2024-43484, CVE-2024-43485 +``` + +**Winner:** hal-index (**40x smaller** than releases-index) + +- llms-index requires extra hop through `root` link since EOL versions aren't in `_embedded.patches` +- hal-index starts directly at the release index +- releases-index EOL date is in root, but CVE query requires 1.6 MB file + +**Analysis:** + +- **Completeness:** ✅ Equal—all three return the same EOL date, patch version, and CVE IDs. +- **EOL version handling:** The llms-index optimizes for supported versions (`_embedded.patches`), requiring navigation for EOL queries. This is a reasonable tradeoff since EOL queries are less frequent. +- **CVE details:** To get CVE severity/titles (not just IDs), hal-index and llms-index can navigate to `timeline/2024/10/cve.json`; releases-index cannot provide this data. + +--- + +## T5: CVE Timeline with Code Fixes + +**Query:** "What CVEs have affected .NET 8 in the last 3 months? For any CRITICAL severity ones, what was the code fix?" + +| Schema | Files Required | Total Transfer | +|--------|----------------|----------------| +| llms-index | `llms.json` → `timeline/2025/10/index.json` (+ prev-security walk) | **~25 KB** | +| hal-index | `timeline/index.json` → `timeline/2025/index.json` → month indexes | **~30 KB** | +| releases-index | N/A | N/A (cannot answer) | + +**llms-index:** Direct link to latest security month, then walk `prev-security-month`. For code fixes, use the `cve.json` file: + +```bash +# Step 1: Get latest security month +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/llms.json" | jq -r '._links["latest-security-month"].href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/index.json + +# Step 2: List CVEs affecting .NET 8 with severity +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/10/index.json" | jq -r '._embedded.disclosures[] | select(.affected_releases | contains(["8.0"])) | "\(.id) (\(.cvss_severity))"' +# CVE-2025-55248 (MEDIUM) +# CVE-2025-55315 (CRITICAL) +# CVE-2025-55247 (HIGH) + +# Step 3: For CRITICAL ones, get code fix URLs from cve.json +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/10/cve.json" | jq -r ' + .disclosures[] | select(.cvss.severity == "CRITICAL") | + "→ \(.id): \(.problem)", + (.id as $cve | .cve_commits[$cve] // [] | .[] as $ref | .commits[$ref].url)' +# → CVE-2025-55315: .NET Security Feature Bypass Vulnerability +# https://github.com/dotnet/aspnetcore/commit/61794b7f3aac0b6719f783db5b5c725fefc8b695.diff + +# Step 4: Walk to previous security month +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/10/index.json" | jq -r '._links["prev-security-month"].href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/06/index.json +``` + +**hal-index:** + +```bash +# Step 1: Get latest year +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/index.json" | jq -r '._links["latest-year"].href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/index.json + +# Step 2: Get latest security month +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/index.json" | jq -r '._links["latest-security-month"].href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/index.json + +# Step 3: List CVEs affecting .NET 8 with severity +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/10/index.json" | jq -r '._embedded.disclosures[] | select(.affected_releases | contains(["8.0"])) | "\(.id) (\(.cvss_severity))"' +# CVE-2025-55248 (MEDIUM) +# CVE-2025-55315 (CRITICAL) +# CVE-2025-55247 (HIGH) + +# Step 4: For CRITICAL ones, get code fix URLs from cve.json +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/10/cve.json" | jq -r ' + .disclosures[] | select(.cvss.severity == "CRITICAL") | + "→ \(.id): \(.problem)", + (.id as $cve | .cve_commits[$cve] // [] | .[] as $ref | .commits[$ref].url)' +# → CVE-2025-55315: .NET Security Feature Bypass Vulnerability +# https://github.com/dotnet/aspnetcore/commit/61794b7f3aac0b6719f783db5b5c725fefc8b695.diff + +# Step 5: Walk to previous security month +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/10/index.json" | jq -r '._links["prev-security-month"].href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/06/index.json +``` + +**releases-index:** Cannot answer this query. + +- No timeline navigation +- No CVE severity data +- No code fix references + +**Winner:** llms-index (**~25 KB**, direct `latest-security-month` link) + +- Skips 2 fetches compared to hal-index (timeline root → year index) +- `prev-security-month` links enable efficient month-to-month traversal +- Code fixes available in `cve.json` via `cve_commits` and `commits` mappings + +**Analysis:** + +- **Completeness:** ❌ releases-index cannot answer—no severity data, no fix references. +- **Code fix access:** The `cve.json` file provides `cve_commits` (CVE→commit refs) and `commits` (ref→URL) mappings for direct `.diff` URLs. +- **Version filtering:** The `affected_releases` array in month index disclosures enables filtering CVEs by .NET version (e.g., "8.0"). +- **Severity filtering:** CVSS severity is embedded in both month index (`cvss_severity`) and cve.json (`cvss.severity`). + +--- + +## T6: Security Process Analysis (Stress Test) + +**Query:** "Please look at .NET Runtime and ASP.NET Core CVEs from December 2024 until November 2025 (12 months). I am concerned at the rate of these CVEs. Look at code diffs for the CVEs. Are the fixes sufficiently protecting my mission critical apps and could the .NET team have avoided these vulnerabilities with a stronger security process?" + +| Schema | Files Required | Total Transfer | +|--------|----------------|----------------| +| llms-index | `llms.json` → timeline → years → months → diffs | **~450 KB** | +| hal-index | `timeline/index.json` → years → months → diffs | **~455 KB** | +| releases-index | N/A | N/A (cannot answer) | + +**llms-index:** Batched approach matching optimal AI agent performance (~28 fetches, 8 turns). + +*Note: This script gathers data for an LLM to analyze. An AI agent would fetch these URLs in parallel batches, then synthesize the security analysis from the CVE metadata and code diffs.* + +```bash +# Turn 1: Get entry point and timeline link +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/llms.json" | jq -r '._links["timeline"].href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/index.json + +# Turn 2: Get year indexes from timeline embedded data +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/index.json" | jq -r '._embedded.years[] | select(.year == "2024" or .year == "2025") | ._links.self.href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2024/index.json +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/index.json + +# Turn 3: Fetch year indexes (parallel in AI agent), identify security months +# 2024: months 11, 12 that have security=true +# 2025: months 01-11 that have security=true + +# Turn 4: Fetch cve.json files for each security month (parallel) +# Example: timeline/2025/10/cve.json + +# Turn 5: Filter for runtime/aspnetcore and list CVEs +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/10/cve.json" | jq -r ' + .disclosures[] | select(.affected_products[] | test("runtime|aspnetcore")) | + "\(.id) (\(.cvss.severity)) - \(.problem)"' +# CVE-2025-55248 (MEDIUM) - .NET Information Disclosure Vulnerability +# CVE-2025-55315 (CRITICAL) - .NET Security Feature Bypass Vulnerability + +# Turn 6: Get diff URLs for runtime/aspnetcore commits +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/10/cve.json" | jq -r ' + .commits | to_entries[] | select(.value.repo | test("runtime|aspnetcore")) | .value.url' +# https://github.com/dotnet/aspnetcore/commit/61794b7f3aac0b6719f783db5b5c725fefc8b695.diff +# https://github.com/dotnet/runtime/commit/18e28d767acf44208afa6c4e2e67a10c65e9647e.diff +# ... + +# Turn 7: Fetch diffs (parallel in AI agent) +# Turn 8: Synthesize analysis from fetched data +``` + +**Output:** +``` +# CVEs (Turn 5): +CVE-2024-43498 (CRITICAL) - .NET Remote Code Execution Vulnerability +CVE-2024-43499 (HIGH) - .NET Denial of Service Vulnerability +CVE-2025-21171 (HIGH) - .NET Remote Code Execution Vulnerability +CVE-2025-21172 (HIGH) - .NET and Visual Studio Remote Code Execution Vulnerability +CVE-2025-21176 (HIGH) - .NET and Visual Studio Remote Code Execution Vulnerability +CVE-2025-24070 (HIGH) - ASP.NET Core Elevation of Privilege Vulnerability +CVE-2025-26682 (HIGH) - ASP.NET Core and Visual Studio Denial of Service Vulnerability +CVE-2025-30399 (HIGH) - .NET Remote Code Execution Vulnerability +CVE-2025-55248 (MEDIUM) - .NET Information Disclosure Vulnerability +CVE-2025-55315 (CRITICAL) - .NET Security Feature Bypass Vulnerability + +# Diff URLs (Turn 6): +https://github.com/dotnet/aspnetcore/commit/61794b7f3aac0b6719f783db5b5c725fefc8b695.diff +https://github.com/dotnet/aspnetcore/commit/67f3b04274d3acb607fe95796dcb35f4f11149bf.diff +https://github.com/dotnet/aspnetcore/commit/97a86434195a82fc7e302a4c57d5ec7f885c1ad5.diff +https://github.com/dotnet/aspnetcore/commit/d5933a9d685c3a09566ec7c9ca818bd7ac2f08ad.diff +https://github.com/dotnet/aspnetcore/commit/d6605eb150c993dd8943e2c1a6875a93927c301a.diff +https://github.com/dotnet/aspnetcore/commit/f71e283286d8470639486804053f28391f92fafc.diff +https://github.com/dotnet/runtime/commit/18e28d767acf44208afa6c4e2e67a10c65e9647e.diff +https://github.com/dotnet/runtime/commit/214743ee2a5a25b9a3a07e3f0451da73eb4e97e2.diff +https://github.com/dotnet/runtime/commit/32d8ea6eecf7f192a75162645390847b14b56dbb.diff +https://github.com/dotnet/runtime/commit/44527b9ed8427463578126a4494c3654dda11866.diff +https://github.com/dotnet/runtime/commit/89ef51c5d8f5239345127a1e282e11036e590c8b.diff +https://github.com/dotnet/runtime/commit/9da8c6a4a6ea03054e776275d3fd5c752897842e.diff +https://github.com/dotnet/runtime/commit/b33d4e34e1cbf993583d78fc1b64ea8400935978.diff +https://github.com/dotnet/runtime/commit/d16f41ad8fded18bf82bca88df27967cc3365eb0.diff +``` + +**hal-index:** Same pattern, starting from `timeline/index.json` instead of `llms.json`. + +**releases-index:** Cannot answer this query. + +- No timeline structure for date-range queries +- No CVE severity or title data +- No code fix references +- Would require fetching ALL version releases.json files (~8 MB) and still lack required data + +**Winner:** llms-index / hal-index (releases-index **cannot answer**) + +**Performance Comparison:** + +| Metric | Optimal | Actual (Haiku) | +|--------|---------|----------------| +| Total fetches | ~28 | 28 | +| Turns | 8 | 8 | +| Bytes fetched | ~450 KB | 453 KB | +| Duration | - | 93.5s | + +**Analysis:** + +- **Completeness:** ❌ releases-index fundamentally cannot answer security analysis queries—it lacks severity, descriptions, and fix references. +- **Batching efficiency:** Year indexes embed month summaries with `.security` flag, enabling upfront identification of which months to fetch. This allows batching all month fetches in one turn. +- **Parallel fetching:** Independent fetches (year indexes, month indexes, diffs) can be parallelized within turns. +- **Code review:** The `cve.json` includes `cve_commits` mapping CVEs to commit refs, and `commits` mapping refs to full URLs with repo/branch info—enabling comprehensive security analysis. +- **Query alignment:** The ~450 KB cost reflects a query that spans two calendar years ("Dec 2024 - Nov 2025"). Queries aligned with the graph structure would be significantly cheaper: + - "CVEs in 2025" → single year index, ~40% fewer fetches + - "CVEs affecting .NET 8" → version-based `prev-security-patch` walk, no timeline navigation + +**Fetch Strategy for AI Agents:** + +For a 12-month analysis, an efficient agent should: +1. Fetch `llms.json` (5 KB) - get `timeline` link +2. Fetch `timeline/index.json` (4 KB) - get year links from `_embedded.years` +3. Fetch both year indexes in parallel (12 KB total) +4. Identify security months in range from embedded `.security` flag +5. Fetch all relevant `cve.json` files in parallel (~12 files) +6. Filter disclosures by `affected_products` for runtime/aspnetcore +7. Extract diff URLs from `commits` object +8. Fetch code diffs in parallel for analysis + +Total: ~450 KB (JSON navigation + code diffs) diff --git a/accepted/2025/release-notes-graph/metrics/easy-questions.md b/accepted/2025/release-notes-graph/metrics/easy-questions.md new file mode 100644 index 000000000..405cd88c2 --- /dev/null +++ b/accepted/2025/release-notes-graph/metrics/easy-questions.md @@ -0,0 +1,181 @@ +# Easy Questions (Q1) + +Query cost comparison for Q1-Easy category tests from [release-graph-eval](https://github.com/dotnet/release-graph-eval). + +See [overview.md](../overview.md) for design context, file characteristics, and link relation discovery. + +## T1: Supported .NET Versions + +**Query:** "Which .NET versions are currently supported?" + +| Schema | Files Required | Total Transfer | +|--------|----------------|----------------| +| llms-index | `llms.json` | **5 KB** | +| hal-index | `index.json` | **5 KB** | +| releases-index | `releases-index.json` | **6 KB** | + +**llms-index:** The `supported_major_releases` property provides a direct array—no filtering required: + +```bash +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/llms.json" | jq -r '.supported_major_releases[]' +# 10.0 +# 9.0 +# 8.0 +``` + +**hal-index:** + +```bash +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" | jq -r '._embedded.releases[] | select(.supported) | .version' +# 10.0 +# 9.0 +# 8.0 +``` + +**releases-index:** + +```bash +curl -s "https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json" | jq -r '.["releases-index"][] | select(.["support-phase"] == "active") | .["channel-version"]' +# 10.0 +# 9.0 +# 8.0 +``` + +**Winner:** llms-index + +- Direct array access, no filtering required +- Equivalent size to hal-index (5 KB) +- 17% smaller than releases-index + +**Analysis:** + +- **Completeness:** ✅ Equal—all three return the same list of supported versions. +- **Zero-fetch for LLMs:** The llms-index `supported_major_releases` array can be answered directly from embedded data without any jq filtering—ideal for AI assistants that have already fetched llms.json as their entry point. +- **Query complexity:** llms-index requires no `select()` filter; hal-index uses boolean filter; releases-index requires enum comparison with bracket notation. + +--- + +## T2: Latest .NET 10 Patch Details + +**Query:** "What's the latest patch for .NET 10, when was it released, and was it a security release?" + +| Schema | Files Required | Total Transfer | +|--------|----------------|----------------| +| llms-index | `llms.json` | **5 KB** | +| hal-index | `index.json` → `10.0/index.json` | **14 KB** | +| releases-index | `releases-index.json` | **6 KB** | + +**llms-index:** The `_embedded.patches` object contains all details inline, keyed by major version: + +```bash +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/llms.json" | jq -r '._embedded.patches["10.0"] | "\(.version) | security: \(.security)"' +# 10.0.1 | security: false +``` + +**hal-index:** + +```bash +# Step 1: Get the 10.0 version href +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" | jq -r '._embedded.releases[] | select(.version == "10.0") | ._links.self.href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/index.json + +# Step 2: Get latest patch details from version index +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/index.json" | jq -r '._embedded.patches[0] | "\(.version) | \(.date | split("T")[0]) | security: \(.security)"' +# 10.0.1 | 2025-12-09 | security: false +``` + +**releases-index:** The root index includes `latest-release`, `latest-release-date`, and `security` inline: + +```bash +curl -s "https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json" | jq -r '.["releases-index"][] | select(.["channel-version"] == "10.0") | "\(.["latest-release"]) | \(.["latest-release-date"]) | security: \(.security)"' +# 10.0.1 | 2025-12-09 | security: false +``` + +**Winner:** llms-index (17% smaller than releases-index) + +- llms-index and releases-index can answer from their root index file +- hal-index requires an additional fetch to the version index +- llms-index provides additional metadata (CVE count, navigation links) in the same payload + +**Analysis:** + +- **Completeness:** ✅ Equal—all three return the same version, date, and security status. +- **Single-fetch answers:** Both llms-index and releases-index embed latest patch info in their root; hal-index requires navigating to the version index. +- **Additional metadata:** llms-index `_embedded.patches` includes SDK version, support phase, and `latest-security-patch` link; releases-index includes SDK version and EOL date; hal-index root only has version summaries. + +--- + +## T3: Security Patches Since Date + +**Query:** "I last updated my .NET 8 installation in November 2025. Has there been a security patch since then? Which CVEs did the last security patch resolve?" + +This query has two parts: +1. **Date check:** Has there been a security patch since November 2025? +2. **CVE details:** What CVEs were fixed in the last security patch? + +| Schema | Files Required | Total Transfer | +|--------|----------------|----------------| +| llms-index (date check) | `llms.json` | **5 KB** | +| llms-index (with CVEs) | `llms.json` → `8.0/8.0.21/index.json` | **14 KB** | +| hal-index | `index.json` → `8.0/index.json` → `8.0/8.0.21/index.json` | **34 KB** | +| releases-index | `releases-index.json` → `8.0/releases.json` | **1,269 KB** | + +**llms-index:** The `_embedded.patches` object includes `latest_security_patch_date` for immediate date comparison: + +```bash +# Part 1: Date check - answerable from llms.json alone +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/llms.json" | jq -r '._embedded.patches["8.0"] | + "Latest security: \(.latest_security_patch) (\(.latest_security_patch_date | split("T")[0])) | After Nov 2025? \(.latest_security_patch_date > "2025-11")"' +# Latest security: 8.0.21 (2025-10-14) | After Nov 2025? false + +# Part 2: Get CVE IDs (requires one additional fetch via latest-security-patch link) +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/llms.json" | jq -r '._embedded.patches["8.0"]._links["latest-security-patch"].href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/8.0/8.0.21/index.json + +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/8.0/8.0.21/index.json" | jq -r '.cve_records[]' +# CVE-2025-55247 +# CVE-2025-55248 +# CVE-2025-55315 +``` + +**hal-index:** + +```bash +# Step 1: Get the 8.0 version href +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" | jq -r '._embedded.releases[] | select(.version == "8.0") | ._links.self.href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/8.0/index.json + +# Step 2: Get latest-security-patch link (version index doesn't embed security date) +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/8.0/index.json" | jq -r '._links["latest-security-patch"].href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/8.0/8.0.21/index.json + +# Step 3: Get date and CVEs from security patch +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/8.0/8.0.21/index.json" | jq -r '"Latest security: \(.version) (\(.date | split("T")[0])) | After Nov 2025? \(.date > "2025-11") | CVEs: \(.cve_records | join(", "))"' +# Latest security: 8.0.21 (2025-10-14) | After Nov 2025? false | CVEs: CVE-2025-55247, CVE-2025-55248, CVE-2025-55315 +``` + +**releases-index:** + +```bash +# Step 1: Get the 8.0 releases.json URL +curl -s "https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json" | jq -r '.["releases-index"][] | select(.["channel-version"] == "8.0") | .["releases.json"]' +# https://builds.dotnet.microsoft.com/dotnet/release-metadata/8.0/releases.json + +# Step 2: Find latest security release and check date +curl -s "https://builds.dotnet.microsoft.com/dotnet/release-metadata/8.0/releases.json" | jq -r ' + [.releases[] | select(.security == true)][0] | + "Latest security: \(.["release-version"]) (\(.["release-date"])) | After Nov 2025? \(.["release-date"] > "2025-11") | CVEs: \([.["cve-list"][]?["cve-id"]] | join(", "))"' +# Latest security: 8.0.21 (2025-10-14) | After Nov 2025? false | CVEs: CVE-2025-55247, CVE-2025-55315, CVE-2025-55248 +``` + +**Winner:** llms-index + +- **Date check only:** 5 KB (zero additional fetches) — `latest_security_patch_date` embedded in root +- **With CVE IDs:** 14 KB (**91x smaller** than releases-index, **2.4x smaller** than hal-index) + +**Analysis:** + +- **Completeness:** ✅ Equal—all three identify 8.0.21 as the latest security patch with the same CVE list. +- **Date comparison:** llms-index embeds `latest_security_patch_date` directly, answering "has there been a security patch since X?" without any navigation. hal-index and releases-index require fetching additional files to get the security patch date. +- **CVE access:** Both llms-index and hal-index require one additional fetch to get CVE IDs; releases-index includes CVE IDs inline but requires downloading 1.2 MB. +- **Two-part queries:** The llms-index design allows partial answers (date check) before committing to additional fetches (CVE details). diff --git a/accepted/2025/release-notes-graph/metrics/index-discovery.md b/accepted/2025/release-notes-graph/metrics/index-discovery.md new file mode 100644 index 000000000..0d3d0544f --- /dev/null +++ b/accepted/2025/release-notes-graph/metrics/index-discovery.md @@ -0,0 +1,266 @@ +# Index Discovery + +Query patterns for discovering and navigating the .NET release metadata graph. These patterns demonstrate how HAL's self-describing structure enables exploration without prior documentation. + +## 1: List Available Link Relations + +**Query:** "What operations are available from this index?" + +The `_links` object in any HAL document describes all available navigation paths. This is the fundamental discovery pattern. + +**Root index:** + +```bash +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" | jq -r '._links | keys[]' +# latest-lts-major +# latest-major +# self +# timeline +``` + +**Version index:** + +```bash +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/index.json" | jq -r '._links | keys[]' +# downloads +# latest-cve-json +# latest-month +# latest-patch +# latest-security-disclosures +# latest-security-month +# latest-security-patch +# manifest +# root +# self +``` + +**Patch index:** + +```bash +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/8.0/8.0.21/index.json" | jq -r '._links | keys[]' +# cve-json +# downloads +# major +# month +# prev-patch +# prev-security-patch +# release-json +# root +# security-disclosures +# self +``` + +**Analysis:** + +- HAL's `_links` pattern makes APIs self-documenting +- No need to know the schema in advance—just inspect available links +- Naming conventions reveal link types: `-json`, `-markdown`, `-html`, `prev-`, `latest-` + +--- + +## 2: Follow Self Links + +**Query:** "How do I get the canonical URL for any resource?" + +Every HAL resource includes a `self` link with its canonical URL. This enables reliable caching and reference. + +```bash +# Get self link from root +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" | jq -r '._links.self.href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/index.json + +# Get self link from embedded resource +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" | jq -r '._embedded.releases[] | select(.version == "10.0") | ._links.self.href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/index.json +``` + +**Analysis:** + +- `self` links provide canonical URLs for caching +- Embedded resources include their own `self` links for navigation +- Never construct URLs manually—always follow links + +--- + +## 3: Discover Embedded Data + +**Query:** "What data is available without additional fetches?" + +The `_embedded` object contains data that would otherwise require additional fetches. Inspect it to understand what's available inline. + +```bash +# What's embedded in the root index? +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" | jq -r '._embedded | keys[]' +# releases + +# What fields are available in each embedded release? +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" | jq -r '._embedded.releases[0] | keys[]' +# _links +# release_type +# supported +# version + +# What's embedded in a patch index? +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/8.0/8.0.21/index.json" | jq -r '._embedded | keys[]' +# runtime +# sdk +# sdk_feature_bands +``` + +**Analysis:** + +- `_embedded` provides complete data inline—no dangling references +- Root embeds version summaries; patch indexes embed runtime info, SDK info, and feature bands +- Check `_embedded` first before following links to avoid unnecessary fetches + +--- + +## 4: Navigate Version Hierarchy + +**Query:** "How do I traverse from root to patch to CVE details?" + +HAL links create a navigable hierarchy. Follow links to drill down or up. + +```bash +# Root -> Version +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" | jq -r '._embedded.releases[] | select(.version == "8.0") | ._links.self.href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/8.0/index.json + +# Version -> Latest Security Patch +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/8.0/index.json" | jq -r '._links["latest-security-patch"].href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/8.0/8.0.21/index.json + +# Patch -> CVE Details +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/8.0/8.0.21/index.json" | jq -r '._links["cve-json"].href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/8.0/8.0.21/cve.json + +# Navigate back up: Patch -> Version +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/8.0/8.0.21/index.json" | jq -r '._links["major"].href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/8.0/index.json +``` + +**Analysis:** + +- `latest-security-patch` jumps directly to the most recent security patch +- `major` navigates back up to the version index +- Bidirectional links enable traversal in any direction + +--- + +## 5: Discover Timeline Navigation + +**Query:** "How do I explore CVEs by date rather than version?" + +The timeline index provides date-based navigation, complementing version-based navigation. + +```bash +# Root -> Timeline +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" | jq -r '._links["timeline"].href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/index.json + +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/index.json" | jq -r '._links | keys[]' +# latest-lts-major +# latest-major +# latest-year +# root +# self + +# Timeline -> Year +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/index.json" | jq -r '._links["latest-year"].href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/index.json + +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/index.json" | jq -r '._links | keys[]' +# latest-cve-json +# latest-major +# latest-month +# latest-security-disclosures +# latest-security-month +# prev-year +# self +# timeline + +# Year -> Month +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/index.json" | jq -r '._links["latest-security-month"].href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/index.json + +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/10/index.json" | jq -r '._links | keys[]' +# cve-json +# prev-month +# prev-security-month +# self +# timeline +# year +``` + +**Analysis:** + +- Timeline provides an alternative entry point for date-based queries +- `prev-security-month` links skip non-security months for efficient traversal +- `latest-security-month` jumps directly to the most recent security content + +--- + +## 6: Discover Documentation Links + +**Query:** "What human-readable documentation is available?" + +HAL indexes include links to rendered documentation alongside machine-readable data. + +```bash +# What documentation links are available? +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/manifest.json" | jq -r '._links | to_entries[] | select(.key | endswith("-html")) | .key' +# compatibility-html +# downloads-html +# release-blog-html +# supported-os-html +# usage-html +# whats-new-html + +# Get the what's new documentation URL +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/manifest.json" | jq -r '._links["whats-new-html"].href' +# https://learn.microsoft.com/dotnet/core/whats-new/dotnet-10/overview +``` + +**Analysis:** + +- `-html` suffix indicates rendered documentation (GitHub blob or Microsoft Learn) +- `-markdown` suffix indicates raw markdown source +- Documentation links enable rich context without leaving the graph + +--- + +## 7: Discover Link Naming Conventions + +**Query:** "What patterns do link names follow?" + +Link relation names follow consistent conventions that reveal their purpose. + +| Suffix/Prefix | Meaning | Example | +|---------------|---------|---------| +| `-json` | Machine-readable JSON data | `cve-json`, `os-packages-json` | +| `-markdown` | Raw markdown source | `supported-os-markdown` | +| `-html` | HTML view (GitHub blob or Learn) | `whats-new-html`, `downloads-html` | +| `latest-` | Jump to most recent | `latest-security-patch`, `latest-month` | +| `prev-` | Backward navigation | `prev-security-month`, `prev-year` | + +```bash +# Find all "latest-" links in a version index +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/index.json" | jq -r '._links | keys[] | select(startswith("latest-"))' +# latest-cve-json +# latest-month +# latest-patch +# latest-security-disclosures +# latest-security-month +# latest-security-patch + +# Find all "-json" links in a manifest +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/manifest.json" | jq -r '._links | keys[] | select(endswith("-json"))' +# os-packages-json +# supported-os-json +``` + +**Analysis:** + +- Consistent naming makes links predictable without documentation +- Prefixes indicate navigation direction or recency +- Suffixes indicate content format diff --git a/accepted/2025/release-notes-graph/metrics/interacting-with-environment.md b/accepted/2025/release-notes-graph/metrics/interacting-with-environment.md new file mode 100644 index 000000000..74bd661aa --- /dev/null +++ b/accepted/2025/release-notes-graph/metrics/interacting-with-environment.md @@ -0,0 +1,160 @@ +# Interacting with My Environment (Q4) + +Query cost comparison for Q4-Environment category tests from [release-graph-eval](https://github.com/dotnet/release-graph-eval). + +See [overview.md](../overview.md) for design context, file characteristics, and link relation discovery. + +## T10: Security Audit Persona + +**Query:** "I'm auditing the security of our production servers. Here's what I found: [simulated dotnet --version showing 8.0.0]. Are these .NET versions safe, or do I need to upgrade due to CVEs?" + +| Schema | Files Required | Total Transfer | +|--------|----------------|----------------| +| llms-index | `llms.json` → `8.0/8.0.21/index.json` | **14 KB** | +| hal-index | `index.json` → `8.0/index.json` → `8.0/8.0.21/index.json` | **34 KB** | +| releases-index | `releases-index.json` → `8.0/releases.json` | **1,269 KB** | + +**llms-index:** The `_embedded.patches` object provides immediate version comparison: + +```bash +# Step 1: Check current patch level vs installed version +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/llms.json" | jq -r '._embedded.patches["8.0"] | + "Current: \(.version) | Installed: 8.0.0 | Outdated: \(.version != "8.0.0") | Security patch: \(.latest_security_patch)"' +# Current: 8.0.21 | Installed: 8.0.0 | Outdated: true | Security patch: 8.0.21 + +# Step 2: Get CVE details from latest security patch +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/llms.json" | jq -r '._embedded.patches["8.0"]._links["latest-security-patch"].href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/8.0/8.0.21/index.json + +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/8.0/8.0.21/index.json" | jq -r '.cve_records[]' +# CVE-2025-55247 +# CVE-2025-55248 +# CVE-2025-55315 +``` + +**hal-index:** + +```bash +# Step 1: Get version index +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" | jq -r '._embedded.releases[] | select(.version == "8.0") | ._links.self.href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/8.0/index.json + +# Step 2: Get latest patch and compare +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/8.0/index.json" | jq -r '._embedded.patches[0] | + "Current: \(.version) | Installed: 8.0.0 | Outdated: \(.version != "8.0.0")"' +# Current: 8.0.21 | Installed: 8.0.0 | Outdated: true + +# Step 3: Get CVE details from latest security patch +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/8.0/index.json" | jq -r '._links["latest-security-patch"].href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/8.0/8.0.21/index.json + +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/8.0/8.0.21/index.json" | jq -r '.cve_records[]' +# CVE-2025-55247 +# CVE-2025-55248 +# CVE-2025-55315 +``` + +**Winner:** llms-index + +- Immediate version comparison from root index +- Direct link to security patch details +- **97x smaller** than releases-index for CVE lookup + +**Analysis:** + +- **Completeness:** ✅ All schemas can identify outdated versions and list CVEs. +- **Version comparison:** llms-index embeds current patch version in `_embedded.patches`, enabling immediate comparison with user-provided version without navigation. +- **Security context:** llms-index provides `latest-security-patch` link for quick CVE lookup; releases-index requires downloading full 1.2 MB release history. + +--- + +## T11: Minimum libc Version Check + +**Query:** "I'm building a custom Linux container for .NET 10 on Alpine Linux with musl 1.2.4. Can I run .NET 10? What's the minimum libc version required?" + +| Schema | Files Required | Total Transfer | +|--------|----------------|----------------| +| llms-index | `llms.json` → `manifest.json` → `supported-os.json` | **17 KB** | +| hal-index | `index.json` → `10.0/index.json` → `manifest.json` → `supported-os.json` | **26 KB** | +| releases-index | N/A | N/A (not available) | + +**llms-index:** Navigate to supported-os.json for libc requirements: + +```bash +# Step 1: Get manifest for target version +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/llms.json" | jq -r '._embedded.patches["10.0"]._links.manifest.href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/manifest.json + +# Step 2: Get supported-os.json from manifest +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/manifest.json" | jq -r '._links["supported-os-json"].href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/supported-os.json + +# Step 3: Get libc requirements for architecture +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/supported-os.json" | jq -r '.libc[] | select(.architectures[] == "x64") | "\(.name) >= \(.version)"' +# glibc >= 2.27 +# musl >= 1.2.2 +``` + +**Winner:** llms-index + +- Direct navigation from `_embedded.patches` +- Both glibc and musl requirements in same file +- **1.5x smaller** than hal-index + +**Analysis:** + +- **Completeness:** ❌ releases-index does not include libc version requirements. +- **libc variants:** supported-os.json includes both glibc (standard Linux) and musl (Alpine) requirements. +- **Architecture filtering:** Requirements vary by architecture (x64, arm64, etc.). + +--- + +## T12: Docker Setup with SDK Download + +**Query:** "I'm writing a Dockerfile for .NET 10 on Ubuntu 24.04. I need to: 1. Install the required OS packages 2. Download and extract the .NET 10 SDK tarball. Generate the apt-get install command and give me the SDK download URL." + +| Schema | Files Required | Total Transfer | +|--------|----------------|----------------| +| llms-index | `llms.json` → `manifest.json` → `os-packages.json` + `sdk/index.json` → `downloads/index.json` → `downloads/sdk.json` | **~36 KB** | +| hal-index | `index.json` → `10.0/index.json` → `manifest.json` → `os-packages.json` + SDK files | **~45 KB** | +| releases-index | `releases-index.json` → `10.0/releases.json` | **~25 KB** (partial) | + +**llms-index:** Navigate to OS packages and SDK download: + +```bash +# Step 1: Get manifest for target version +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/llms.json" | jq -r '._embedded.patches["10.0"]._links.manifest.href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/manifest.json + +# Step 2: Get OS packages href and generate apt-get command +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/manifest.json" | jq -r '._links["os-packages-json"].href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/os-packages.json + +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/os-packages.json" | jq -r ' + .distributions[] | select(.name == "Ubuntu") | .releases[] | select(.release == "24.04") | + "sudo apt-get update && sudo apt-get install -y " + ([.packages[].name] | join(" ")) +' +# sudo apt-get update && sudo apt-get install -y libc6 libgcc-s1 ca-certificates libssl3t64 libstdc++6 libicu74 tzdata libgssapi-krb5-2 + +# Step 3: Get SDK download URL for linux-x64 via downloads link +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/llms.json" | jq -r '._embedded.patches["10.0"]._links.downloads.href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/downloads/index.json + +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/downloads/index.json" | jq -r '._embedded.components[] | select(.name == "sdk") | ._links.self.href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/downloads/sdk.json + +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/downloads/sdk.json" | jq -r '._embedded.downloads["linux-x64"]._links.download.href' +# https://aka.ms/dotnet/10.0/dotnet-sdk-linux-x64.tar.gz +``` + +**Winner:** llms-index + +- Both apt-get command and SDK URL from same manifest navigation +- **2.1x smaller** than hal-index +- releases-index has SDK URLs but not OS packages + +**Analysis:** + +- **Completeness:** ⚠️ releases-index can provide SDK URLs but not OS package requirements. +- **Multi-part synthesis:** LLM must combine package installation command with SDK download URL. +- **Dockerfile context:** Understanding the complete tarball deployment workflow. diff --git a/accepted/2025/release-notes-graph/metrics/project-file-analysis.md b/accepted/2025/release-notes-graph/metrics/project-file-analysis.md new file mode 100644 index 000000000..c0a5a693e --- /dev/null +++ b/accepted/2025/release-notes-graph/metrics/project-file-analysis.md @@ -0,0 +1,145 @@ +# Project File Analysis (Q5) + +Query cost comparison for Q5-Project category tests from [release-graph-eval](https://github.com/dotnet/release-graph-eval). + +See [overview.md](../overview.md) for design context, file characteristics, and link relation discovery. + +## T13: TFM Support Check + +**Query:** "Here's my project file with `net7.0`. Is my target framework still supported?" + +| Schema | Files Required | Total Transfer | +|--------|----------------|----------------| +| llms-index | `llms.json` → `manifest.json` → `target-frameworks.json` per version | **~45 KB** | +| hal-index | `index.json` → `manifest.json` → `target-frameworks.json` per version | **~50 KB** | +| releases-index | `releases-index.json` (string parsing) | **6 KB** | + +**llms-index:** Navigate via manifest to target-frameworks for each supported release: + +```bash +# Check each supported release's target-frameworks.json for the TFM +# Get supported major releases first +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/llms.json" | jq -r '.supported_major_releases[]' +# 10.0 +# 9.0 +# 8.0 + +# Then check if net7.0 is in any supported version's target-frameworks +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/llms.json" | jq -r '._embedded.patches["10.0"]._links.manifest.href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/manifest.json + +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/manifest.json" | jq -r '._links["target-frameworks"].href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/target-frameworks.json + +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/target-frameworks.json" | jq -r '.frameworks[] | select(.tfm == "net7.0") | .tfm' +# (empty - net7.0 is NOT supported) +``` + +**Winner:** Tie (all equivalent with string parsing) + +- All schemas can use string parsing for efficiency +- llms-index/hal-index shown here use authoritative TFM data for correctness + +**Analysis:** + +- **Completeness:** ✅ All schemas can determine support status. +- **Correctness vs efficiency:** llms-index uses authoritative TFM data via `target-frameworks` relation; releases-index uses string parsing. +- **Platform TFMs:** The `target-frameworks` approach correctly handles platform-specific TFMs (e.g., `net10.0-android`) that string parsing would miss. + +--- + +## T14: Package CVE Check + +**Query:** "Here's my project file with package references. Have any of my packages had CVEs in the last 6 months?" + +```xml + + +``` + +| Schema | Files Required | Total Transfer | +|--------|----------------|----------------| +| llms-index | `llms.json` → 6 month cve.json files (via `prev-security`) | **~60 KB** | +| hal-index | `timeline/index.json` → `timeline/2025/index.json` → 6 month cve.json files | **~65 KB** | +| releases-index | N/A | N/A | + +**llms-index:** Walk security timeline and check packages in cve.json with version comparison: + +```bash +# Start from the latest security month +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/llms.json" | jq -r '._links["latest-security-month"].href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/index.json + +# Get cve.json link and check for package vulnerabilities +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/10/index.json" | jq -r '._links["cve-json"].href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/10/cve.json + +# Check packages in cve.json +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/10/cve.json" | jq -r ' + .packages | to_entries[] | select(.key | test("Microsoft.Build")) | + "\(.key) | \(.value.cve_id) | vulnerable: \(.value.min_vulnerable)-\(.value.max_vulnerable) | fixed: \(.value.fixed)"' +# Microsoft.Build | CVE-2025-55247 | vulnerable: 17.10.0-17.10.29 | fixed: 17.10.46 +# Microsoft.Build.Tasks.Core | CVE-2025-55247 | vulnerable: 17.8.0-17.8.29 | fixed: 17.8.43 + +# Get commit diff URLs for the CVE +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/10/cve.json" | jq -r ' + .cve_commits["CVE-2025-55247"][] as $ref | .commits[$ref].url' +# https://github.com/dotnet/msbuild/commit/aa888d3214e5adb503c48c3bad2bfc6c5aff638a.diff +# ... + +# Walk to previous security month +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/timeline/2025/10/index.json" | jq -r '._links["prev-security-month"].href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/timeline/2025/06/index.json +``` + +**Winner:** llms-index + +- Direct `latest-security-month` link as starting point +- Package-level CVE data with commit diff URLs in cve.json +- Enables code review of security fixes without relying on nuget.org + +**Analysis:** + +- **Completeness:** ⚠️ releases-index does not provide package-level CVE data. +- **Version matching:** String comparison works for semver when patch versions have consistent digit counts. +- **Commit diffs:** The `cve_commits` and `commits` mappings provide direct links to fix diffs on GitHub. +- **Timeline navigation:** Uses `prev-security-month` links to efficiently walk security history. + +--- + +## T15: Target Platform Versions + +**Query:** "For a .NET MAUI app targeting `net10.0-android` and `net10.0-ios`, what platform SDK versions do these target?" + +| Schema | Files Required | Total Transfer | +|--------|----------------|----------------| +| llms-index | `llms.json` → `10.0/manifest.json` → `target-frameworks.json` | **11 KB** | +| hal-index | `index.json` → `10.0/index.json` → `10.0/manifest.json` → `target-frameworks.json` | **20 KB** | +| releases-index | N/A | N/A (not available) | + +**llms-index:** Navigate to target-frameworks.json via manifest link: + +```bash +# Get target-frameworks.json for .NET 10 via manifest link +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/llms.json" | jq -r '._embedded.patches["10.0"]._links.manifest.href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/manifest.json + +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/manifest.json" | jq -r '._links["target-frameworks"].href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/target-frameworks.json + +# Get Android and iOS platform versions +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/target-frameworks.json" | jq -r '.frameworks[] | select(.platform == "android" or .platform == "ios") | "\(.tfm) targets \(.platform_name) \(.platform_version)"' +# net10.0-android targets Android 36.0 +# net10.0-ios targets iOS 18.7 +``` + +**Winner:** llms-index + +- Direct path via `manifest` link +- Platform-specific TFM data not available in releases-index + +**Analysis:** + +- **Completeness:** ❌ releases-index does not include target framework data. +- **MAUI context:** Understanding multi-platform project requirements. +- **Version mapping:** Each .NET version targets specific platform SDK versions (Android API level, iOS SDK version). diff --git a/accepted/2025/release-notes-graph/metrics/upgrade-whats-new.md b/accepted/2025/release-notes-graph/metrics/upgrade-whats-new.md new file mode 100644 index 000000000..a05385969 --- /dev/null +++ b/accepted/2025/release-notes-graph/metrics/upgrade-whats-new.md @@ -0,0 +1,226 @@ +# Upgrade and What's New Queries + +Query cost comparison for upgrade planning and what's new queries from [release-graph-eval](https://github.com/dotnet/release-graph-eval). + +See [overview.md](../overview.md) for design context, file characteristics, and link relation discovery. + +## T7: High-Impact Breaking Changes Analysis + +**Query:** "I'm planning to upgrade to .NET 10. What are the high-impact breaking changes I should be aware of? For each one, give me the announcement link so I can track the discussion." + +| Schema | Files Required | Total Transfer | +|--------|----------------|----------------| +| llms-index | `llms.json` → `10.0/manifest.json` → `10.0/compatibility.json` | **~117 KB** | +| hal-index | `index.json` → `10.0/index.json` → `10.0/manifest.json` → `compatibility.json` | **~126 KB** | +| releases-index | N/A | N/A (not available) | + +**llms-index:** + +```bash +# Step 1: Get manifest link from embedded patch data +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/llms.json" | jq -r '._embedded.patches["10.0"]._links.manifest.href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/manifest.json + +# Step 2: Get compatibility.json link from manifest +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/manifest.json" | jq -r '._links.compatibility.href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/compatibility.json + +# Step 3: Filter for high-impact breaking changes with announcement URLs +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/compatibility.json" | jq -r '.breaks[] | select(.impact == "high") | "\(.title)\n Announcement: \(.references[] | select(.type == "announcement") | .url)\n"' +``` + +**Output:** +``` +WebHostBuilder, IWebHost, and WebHost are obsolete + Announcement: https://github.com/aspnet/Announcements/issues/526 + +DateOnly and TimeOnly support on SQL Server with EF Core + Announcement: https://github.com/dotnet/efcore/issues/35662 +``` + +**hal-index:** + +```bash +# Step 1: Get version index +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" | jq -r '._embedded.releases[] | select(.version == "10.0") | ._links.self.href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/index.json + +# Step 2: Get manifest link +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/index.json" | jq -r '._links.manifest.href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/manifest.json + +# Step 3-4: Same as llms-index from here +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/manifest.json" | jq -r '._links.compatibility.href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/compatibility.json + +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/compatibility.json" | jq -r '.breaks[] | select(.impact == "high") | "\(.title)\n Announcement: \(.references[] | select(.type == "announcement") | .url)\n"' +``` + +**releases-index:** Cannot answer—no breaking changes data. + +**Winner:** llms-index (**~117 KB**, one fewer fetch than hal-index) + +**Analysis:** + +- **Completeness:** ❌ releases-index has no breaking changes data. +- **Impact filtering:** The `impact` field (high/medium/low) enables prioritized upgrade planning. +- **Reference types:** Each breaking change includes typed references (announcement, documentation, documentation-rendered) for different use cases. +- **Navigation:** llms-index `_embedded.patches` provides direct `manifest` link, skipping the version index fetch. + +--- + +## T8: Code Migration + +**Query:** "I'm upgrading our ASP.NET Core application from .NET 8 to .NET 10. When I target .NET 10, I get compiler warnings about `WebHostBuilder` being deprecated. What breaking change is this, and how should I fix the code?" + +| Schema | Files Required | Total Transfer | +|--------|----------------|----------------| +| llms-index | `llms.json` → `manifest.json` → `compatibility.json` → doc.md | **~122 KB** | +| hal-index | `index.json` → `10.0/index.json` → `manifest.json` → `compatibility.json` → doc.md | **~131 KB** | +| releases-index | N/A | N/A (not available) | + +**llms-index:** + +```bash +# Step 1: Navigate to compatibility.json +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/llms.json" | jq -r '._embedded.patches["10.0"]._links.manifest.href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/manifest.json + +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/manifest.json" | jq -r '._links.compatibility.href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/compatibility.json + +# Step 2: Find WebHostBuilder breaking change +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/compatibility.json" | jq '.breaks[] | select(.title | test("WebHostBuilder"; "i")) | {title, id, impact}' +# {"title": "WebHostBuilder, IWebHost, and WebHost are obsolete", "id": "aspnet-core-10-webhostbuilder-deprecated", "impact": "medium"} + +# Step 3: Get documentation URL for migration guidance +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/compatibility.json" | jq -r '.breaks[] | select(.title | test("WebHostBuilder"; "i")) | .references[] | select(.type == "documentation") | .url' +# https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/aspnet-core/10/webhostbuilder-deprecated.md + +# Step 4: Fetch migration documentation +curl -s "https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/aspnet-core/10/webhostbuilder-deprecated.md" +``` + +**Output:** +``` +Breaking Change: WebHostBuilder, IWebHost, and WebHost are obsolete +ID: aspnet-core-10-webhostbuilder-deprecated +Impact: medium + +Documentation: https://raw.githubusercontent.com/dotnet/docs/main/docs/core/compatibility/aspnet-core/10/webhostbuilder-deprecated.md +``` + +The documentation contains the migration code: +```csharp +// Before (.NET 8) +var hostBuilder = new WebHostBuilder() + .UseStartup(); +var server = new TestServer(hostBuilder); + +// After (.NET 10) +var host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseTestServer() + .UseStartup(); + }) + .Build(); +await host.StartAsync(); +var server = host.GetTestServer(); +``` + +**releases-index:** Cannot answer—no breaking changes or documentation references. + +**Winner:** llms-index (**~120 KB**, direct path to migration docs) + +**Analysis:** + +- **Completeness:** ❌ releases-index cannot help with code migration. +- **Reference types:** The `documentation` reference points to raw markdown, enabling LLMs to extract code examples directly. +- **Search capability:** Breaking changes can be searched by title, category, or affected API. +- **End-to-end:** Single navigation path from entry point to actionable migration code. + +--- + +## T9: What's New in Runtime + +**Query:** "What's new in the runtime for .NET 10?" + +| Schema | Files Required | Total Transfer | +|--------|----------------|----------------| +| llms-index | `llms.json` → `10.0/manifest.json` → `runtime.md` | **~24 KB** | +| hal-index | `index.json` → `10.0/index.json` → `manifest.json` → `runtime.md` | **~33 KB** | +| releases-index | N/A | N/A (not available) | + +**llms-index:** + +```bash +# Step 1: Get manifest link +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/llms.json" | jq -r '._embedded.patches["10.0"]._links.manifest.href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/manifest.json + +# Step 2: Get whats-new-runtime link +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/manifest.json" | jq -r '._links["whats-new-runtime"].href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/whats-new/runtime.md + +# Step 3: Fetch runtime what's new documentation +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/whats-new/runtime.md" +``` + +**Output (excerpt):** +```markdown +# What's new in the .NET 10 runtime + +- JIT improvements: struct argument code generation, loop inversion, + array interface devirtualization, improved code layout, inlining +- Stack allocation: small arrays, escape analysis, delegate stack allocation +- AVX10.2 support +- Arm64 write-barrier improvements +- NativeAOT type preinitializer enhancements +``` + +**hal-index:** + +```bash +# Step 1: Get version index +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json" | jq -r '._embedded.releases[] | select(.version == "10.0") | ._links.self.href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/index.json + +# Step 2-3: Same as llms-index +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/index.json" | jq -r '._links.manifest.href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/manifest.json + +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/manifest.json" | jq -r '._links["whats-new-runtime"].href' +# https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/10.0/whats-new/runtime.md + +curl -s "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/10.0/whats-new/runtime.md" +``` + +**releases-index:** Cannot answer—no what's new documentation. + +**Winner:** llms-index (**~25 KB**, 2 fetches vs 3 for hal-index) + +**Analysis:** + +- **Completeness:** ❌ releases-index has no what's new content. +- **Granular links:** The manifest provides separate links for `whats-new-runtime`, `whats-new-libraries`, `whats-new-sdk`, enabling targeted fetches. +- **Raw markdown:** Documentation is raw markdown, ideal for LLM consumption and summarization. +- **Navigation efficiency:** llms-index embedded patches provide direct manifest link, saving one fetch. + +--- + +## Summary + +| Test | Query | Winner | releases-index | +|------|-------|--------|----------------| +| T7 | High-impact breaking changes | llms-index (~115 KB) | ❌ Not available | +| T8 | Code migration guidance | llms-index (~120 KB) | ❌ Not available | +| T9 | What's new in runtime | llms-index (~25 KB) | ❌ Not available | + +**Key insights:** + +- **releases-index gaps:** No breaking changes, no migration docs, no what's new content—these are essential for upgrade planning. +- **Manifest as hub:** The `manifest.json` serves as a hub linking to compatibility data, what's new docs, OS packages, and more. +- **llms-index advantage:** Embedded `_embedded.patches` with direct `manifest` link saves one navigation hop on every query. +- **Reference types:** Breaking changes include typed references (announcement, documentation, documentation-rendered) for different consumption patterns. diff --git a/accepted/2025/release-notes-graph/release-notes-graph-llms.md b/accepted/2025/release-notes-graph/release-notes-graph-llms.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/accepted/2025/release-notes-graph/testing/prompts.md b/accepted/2025/release-notes-graph/testing/prompts.md deleted file mode 100644 index cc61f4d80..000000000 --- a/accepted/2025/release-notes-graph/testing/prompts.md +++ /dev/null @@ -1,291 +0,0 @@ -# LLM Chat Test Prompts - -This file contains prompts for manually running the LLM acceptance tests via chat interfaces. Each test produces structured JSON output for batch processing. - -## Instructions - -1. Start a new conversation with the target LLM -2. Paste the appropriate preamble (Mode A or Mode B) -3. Paste the test query -4. Save the JSON output to a file named `{llm}-{mode}-{test}.json` -5. After all tests, run the batch processor to generate the comparison table - -## Preambles - -### Mode A (JSON-first, self-discovery) - -``` -Use https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/llms.json to answer questions about .NET releases. - -After answering, provide: - -1. A summary table: - -| Field | Value | -|-------|-------| -| Answer | | -| Fetch count | | - -2. A JSON block with details: - -{ - "fetched_urls": ["", "", ...], - "data_sources": { - "": "" - }, - "navigation_notes": "" -} -``` - -### Mode B (Prose-first, explicit guidance) - -``` -Use https://raw.githubusercontent.com/dotnet/core/release-index/llms.txt to answer questions about .NET releases. - -After answering, provide: - -1. A summary table: - -| Field | Value | -|-------|-------| -| Answer | | -| Fetch count | | - -2. A JSON block with details: - -{ - "fetched_urls": ["", "", ...], - "data_sources": { - "": "" - }, - "navigation_notes": "" -} -``` - ---- - -## Test Queries - -### Test 1: Single-fetch Embedded Data - -**Query:** -``` -What is the latest patch for .NET 9? -``` - -**Expected Answer:** 9.0.11 -**Expected Fetches:** 1 -**Evaluation Notes:** Check if answer came from `_embedded.latest_patches[]` without additional fetches. - ---- - -### Test 2: Time-bounded with Severity Filter - -**Query:** -``` -Were there any CRITICAL CVEs in .NET 8 in October 2025? -``` - -**Expected Answer:** Yes—CVE-2025-55315 (CVSS 9.9, Security Feature Bypass in Kestrel) -**Expected Fetches:** 2 -**Evaluation Notes:** Should fetch llms.json then timeline/2025/10/index.json. Should NOT need cve.json. - ---- - -### Test 3: Negative Result via Filtering - -**Query:** -``` -Were there any CVEs affecting .NET 10 in October 2025? -``` - -**Expected Answer:** No—the October 2025 CVEs affected .NET 8.0 and 9.0, not 10.0 -**Expected Fetches:** 2 -**Evaluation Notes:** Must correctly read `affected_releases` field. Common failure: assuming 10.0 was affected because it's listed in `releases`. - ---- - -### Test 4: EOL Version Handling - -**Query:** -``` -What CVEs were fixed in .NET 6 this year? -``` - -**Expected Answer:** .NET 6 is EOL (November 12, 2024) and not in the supported releases graph. -**Expected Fetches:** 1-2 -**Evaluation Notes:** Should check graph and report 6.0 not present. Tier 4 if it hallucinates CVEs. - -**Follow-up (optional, if LLM gives Tier 1-2 response):** -``` -I still need to know about .NET 6 CVEs. -``` - -**Expected:** Admits data isn't available, suggests alternatives (NVD, GitHub advisories). Tier 4 if it fabricates data. - ---- - -### Test 5: Link-following Discipline - -**Query:** -``` -When does .NET 8 go EOL? -``` - -**Expected Answer:** November 10, 2026 -**Expected Fetches:** 1-2 -**Evaluation Notes:** Check `fetched_urls`—did URLs come from `_links.href` or were they constructed? Look for suspicious patterns like string interpolation. - ---- - -### Test 6: Breaking Changes Navigation - -**Query:** -``` -How many breaking changes are in .NET 10, grouped by category? -``` - -**Expected Answer:** Counts per category from compatibility.json `categories` rollup -**Expected Fetches:** 3 -**Evaluation Notes:** Path should be llms.json → 10.0/index.json → compatibility.json. Check if it used the pre-computed `categories` object. - ---- - -### Test 7: Breaking Changes with Detail - -**Query:** -``` -What breaking changes in .NET 10 have HIGH impact? -``` - -**Expected Answer:** List filtered from compatibility.json `breaks[]` where `impact == "high"` -**Expected Fetches:** 3 -**Evaluation Notes:** Verify the listed breaking changes actually have `impact: "high"` in the source. - ---- - -### Test 8: Deep Manifest Navigation (OS Packages) - -**Query:** -``` -What packages do I need to install for .NET 10 on Ubuntu 24.04? -``` - -**Expected Answer:** libc6, libgcc-s1, ca-certificates, libssl3t64, libstdc++6, libicu74, tzdata, libgssapi-krb5-2 -**Expected Fetches:** 4 -**Evaluation Notes:** Must reach os-packages.json via manifest.json. Tier 4 if it gives generic Linux packages from training data. - ---- - -### Test 9: Deep Manifest Navigation (libc Requirements) - -**Query:** -``` -What's the minimum glibc version for .NET 10 on x64? -``` - -**Expected Answer:** Version from supported-os.json `libc[]` array -**Expected Fetches:** 4 -**Evaluation Notes:** Must reach supported-os.json. Tier 4 if it guesses from training data. - ---- - -### Test 10: Multi-step Time Traversal - -**Query:** -``` -List all CVEs fixed in .NET 8 in the last 6 months with their severity. -``` - -**Expected Answer:** CVE list covering June-December 2025, filtered by `affected_releases` containing "8.0" -**Expected Fetches:** 3-5 (entry point + 2-4 security months) -**Evaluation Notes:** Should use `prev-security` links, not `prev`. Should stop at correct date boundary. - ---- - -## Scoring Template - -After running a test, fill in this template: - -```json -{ - "test_id": "T1", - "llm": "", - "mode": "A", - "timestamp": "", - - "llm_output": { - "answer": "", - "fetch_count": 1, - "fetched_urls": [], - "data_sources": {}, - "navigation_notes": "" - }, - - "scoring": { - "answer_correct": true, - "expected_answer": "9.0.11", - "expected_fetch_range": [1, 1], - "fetch_count_ok": true, - "url_fabrication_detected": false, - "hallucination_detected": false, - "tier": 1 - }, - - "diagnostics": { - "read_llms_txt": false, - "navigation_path": "llms.json → _embedded.latest_patches[]", - "notes": "" - } -} -``` - -**Scoring fields (determine tier):** -- `answer_correct`: Does the answer match expected? -- `fetch_count_ok`: Is fetch count within expected range? -- `url_fabrication_detected`: Did any URL not come from `_links`? -- `hallucination_detected`: Did LLM state facts not in fetched docs? -- `tier`: 1-4 based on rubric - -**Diagnostic fields (explain patterns):** -- `read_llms_txt`: Infer from `fetched_urls`—set to `true` if llms.txt appears -- `navigation_path`: Summary of how the LLM traversed the graph -- `notes`: Any observations about behavior - -**Evaluation fields you fill in:** -- `answer_correct`: Does the answer match expected? -- `fetch_count_ok`: Is fetch count within expected range? -- `url_fabrication_detected`: Did any URL not come from `_links`? -- `hallucination_detected`: Did LLM state facts not in fetched docs? -- `tier`: 1-4 based on rubric -- `notes`: Any observations - ---- - -## File Naming Convention - -Save results as: -``` -results/{llm}/{mode}/T{n}.json -``` - -Examples: -``` -results/claude-sonnet/A/T1.json -results/claude-sonnet/B/T1.json -results/gpt-4o/A/T1.json -results/gpt-4o/B/T1.json -``` - ---- - -## Batch Processing - -After collecting all results, the processor reads all JSON files and produces: - -1. Per-LLM summary (points, tier distribution, rates) -2. Cross-LLM comparison table -3. Recommendations per LLM - -Processor input: `results/` directory -Processor output: `summary.json`, `comparison.md` diff --git a/accepted/2025/release-notes-graph/testing/spec.md b/accepted/2025/release-notes-graph/testing/spec.md deleted file mode 100644 index b754b6f1b..000000000 --- a/accepted/2025/release-notes-graph/testing/spec.md +++ /dev/null @@ -1,443 +0,0 @@ -# LLM Acceptance Test Specification - -This document defines acceptance tests for validating LLM behavior against the .NET Release Metadata Graph. It measures whether the graph's self-documenting protocol (`llms.txt` + HAL navigation) effectively guides LLM behavior across different models. - -See [acceptance.md](acceptance.md) for data efficiency criteria and [metrics.md](metrics.md) for query cost comparisons. - -## Purpose - -The graph is designed to be self-bootstrapping: an LLM given only the entry point URL should discover navigation patterns and answer queries correctly. These tests validate that design goal across multiple LLMs. - -**Key questions:** -1. Do LLMs discover and follow the `ai_note` → `llms.txt` guidance? -2. Do LLMs follow `_links` correctly, or do they fabricate URLs? -3. Do LLMs use embedded data efficiently, or over-fetch? -4. Do LLMs hallucinate data, or admit when information isn't available? -5. Does prose-first guidance (llms.txt) outperform JSON-first (llms.json)? - -## Test Modes - -Each test is run in two modes to compare self-discovery vs. explicit guidance. - -| Mode | Entry Point | Preamble | -|------|-------------|----------| -| **A** | llms.json | `Use https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/llms.json to answer questions about .NET releases.` | -| **B** | llms.txt | `Use https://raw.githubusercontent.com/dotnet/core/release-index/llms.txt to answer questions about .NET releases.` | - -Mode A tests self-discovery: the LLM must notice the `ai_note` field and choose to read llms.txt. Mode B provides explicit protocol documentation upfront. - -## Scoring Rubric - -Each test is scored on a 4-tier scale based on **outcomes**, not process. - -| Tier | Name | Criteria | Points | -|------|------|----------|--------| -| **1** | Excellent | Correct answer, fetch count within expected range, no fabrication, no hallucination | 4 | -| **2** | Good | Correct answer, but over-fetched or took suboptimal navigation path | 3 | -| **3** | Assisted | Failed initially, succeeded with additional hints | 1 | -| **4** | Failure | Wrong answer, hallucinated data, couldn't navigate graph, or constructed URLs | 0 | - -**Automatic Tier 4 (any mode):** -- Constructed a URL not obtained from `_links` -- Stated facts not present in fetched documents -- Answered without fetching when fetch was required -- Wrong answer on verifiable facts (versions, CVE IDs, dates) - -## Diagnostic Metrics - -These metrics are **observational, not scored**. They explain failure patterns and inform recommendations. - -| Metric | What it tells you | -|--------|-------------------| -| `read_llms_txt` | Did the LLM discover and read the navigation guide? | -| `noticed_ai_note` | Did the LLM mention or act on the ai_note field? | -| `navigation_path` | What links did the LLM follow? | -| `stuck_at` | For failures: where did navigation break down? | - -**How to use diagnostics:** - -If an LLM fails a complex navigation test (T6-T10), check the diagnostics: - -| read_llms_txt | Interpretation | -|---------------|----------------| -| No | **Discovery problem** — didn't notice ai_note or chose not to follow it | -| Yes | **Comprehension problem** — read guidance but couldn't apply it | - -If an LLM succeeds without reading llms.txt: - -| Test type | Interpretation | -|-----------|----------------| -| Simple (T1-T3) | **Correct behavior** — embedded data was sufficient | -| Complex (T6-T10) | **Capable navigator** — figured out HAL structure independently | - -This distinction keeps scoring outcome-focused while preserving data that explains *why* models succeed or fail. - -## Test Battery - -### Test 1: Single-fetch Embedded Data - -**Query:** "What is the latest patch for .NET 9?" - -| Field | Value | -|-------|-------| -| Category | L1 (Patch Currency) | -| Expected Answer | 9.0.11 | -| Expected Fetches | 1 | -| Data Source | `llms.json` → `_embedded.latest_patches[]` where `release == "9.0"` → `version` | - -**Evaluation:** -- Tier 1: Correct version, 1 fetch, extracted from embedded data -- Tier 2: Correct version, but fetched 9.0/index.json unnecessarily -- Tier 4: Wrong version, or answered without fetching - ---- - -### Test 2: Time-bounded with Severity Filter - -**Query:** "Were there any CRITICAL CVEs in .NET 8 in October 2025?" - -| Field | Value | -|-------|-------| -| Category | C2 (CVE Deep Analysis) | -| Expected Answer | Yes—CVE-2025-55315 (CVSS 9.9, Security Feature Bypass in Kestrel) | -| Expected Fetches | 2 | -| Data Source | `llms.json` → `_links["latest-security-month"]` → `timeline/2025/10/index.json` → `_embedded.disclosures[]` filtered by `cvss_severity == "CRITICAL"` and `affected_releases` contains `"8.0"` | - -**Evaluation:** -- Tier 1: Correct CVE ID and details, 2 fetches, used embedded disclosures -- Tier 2: Correct answer, but fetched cve.json unnecessarily -- Tier 4: Wrong CVE, fabricated details, or missed the CRITICAL CVE - ---- - -### Test 3: Negative Result via Filtering - -**Query:** "Were there any CVEs affecting .NET 10 in October 2025?" - -| Field | Value | -|-------|-------| -| Category | C2 (CVE Deep Analysis) | -| Expected Answer | No—the October 2025 CVEs (CVE-2025-55247, CVE-2025-55248, CVE-2025-55315) affected .NET 8.0 and 9.0, not 10.0 | -| Expected Fetches | 2 | -| Data Source | `llms.json` → `timeline/2025/10/index.json` → `_embedded.disclosures[]` → check `affected_releases` for each | - -**Evaluation:** -- Tier 1: Correctly states no CVEs for 10.0, explains why (affected_releases didn't include 10.0) -- Tier 2: Correct "no" answer without explaining the filtering -- Tier 4: Says yes, or fabricates CVEs for .NET 10 - ---- - -### Test 4: EOL Version Handling - -**Query:** "What CVEs were fixed in .NET 6 this year?" - -| Field | Value | -|-------|-------| -| Category | Edge case | -| Expected Answer | .NET 6 reached EOL on November 12, 2024 and is not in the supported releases. The graph covers supported versions only. | -| Expected Fetches | 1-2 | -| Data Source | `llms.json` → `releases[]` does not include "6.0"; optionally `index.json` → `_embedded.releases[]` shows 6.0 with `supported: false` | - -**Evaluation:** -- Tier 1: Confirms 6.0 is EOL, explains graph scope, doesn't fabricate CVEs -- Tier 2: Says "not available" without explaining why -- Tier 4: Hallucinates CVE list from training data - -**Follow-up (if Tier 1/2):** "I still need to know about .NET 6 CVEs" -- Tier 1: Admits data isn't in the graph, suggests alternative sources (e.g., NVD, GitHub advisories) -- Tier 4: Fabricates CVE data - ---- - -### Test 5: Link-following Discipline - -**Query:** "When does .NET 8 go EOL?" - -| Field | Value | -|-------|-------| -| Category | L2 (Lifecycle) | -| Expected Answer | November 10, 2026 | -| Expected Fetches | 1-2 | -| Data Source | `llms.json` or `index.json` → `_embedded.releases[]` where `version == "8.0"` → `eol_date`; or follow `_links.self.href` to `8.0/index.json` → `eol_date` | - -**Evaluation:** -- Tier 1: Correct date, navigated via `_links` -- Tier 2: Correct date, but unclear if link was followed -- Tier 4: Wrong date, or demonstrably constructed URL (e.g., string-interpolated "/8.0/index.json") - -**Detection:** Check if fetched URLs exactly match `_links.href` values from prior fetches. - ---- - -### Test 6: Breaking Changes Navigation - -**Query:** "How many breaking changes are in .NET 10, grouped by category?" - -| Field | Value | -|-------|-------| -| Category | B1 (Breaking Changes) | -| Expected Answer | Counts per category from `compatibility.json` → `categories` rollup | -| Expected Fetches | 3 | -| Data Source | `llms.json` → `_links["latest"]` → `10.0/index.json` → `_links["compatibility-json"]` → `compatibility.json` → `categories` | - -**Evaluation:** -- Tier 1: Correct counts matching `categories` object, 3 fetches -- Tier 2: Correct counts, but manually aggregated from `breaks[]` instead of using rollup -- Tier 4: Fabricated categories or counts, couldn't find compatibility.json - ---- - -### Test 7: Breaking Changes with Detail - -**Query:** "What breaking changes in .NET 10 have HIGH impact?" - -| Field | Value | -|-------|-------| -| Category | B2 (Breaking Changes) | -| Expected Answer | List of breaking changes where `impact == "high"`, with titles and categories | -| Expected Fetches | 3 | -| Data Source | `compatibility.json` → `breaks[]` filtered by `impact == "high"` | - -**Evaluation:** -- Tier 1: Correct list with accurate titles, categories, and impact levels -- Tier 2: Correct filtering, minor omissions or formatting issues -- Tier 4: Wrong impact levels, fabricated breaking changes, or missed the `impact` field - ---- - -### Test 8: Deep Manifest Navigation (OS Packages) - -**Query:** "What packages do I need to install for .NET 10 on Ubuntu 24.04?" - -| Field | Value | -|-------|-------| -| Category | X2 (Linux Deployment) | -| Expected Answer | Package list: libc6, libgcc-s1, ca-certificates, libssl3t64, libstdc++6, libicu74, tzdata, libgssapi-krb5-2 | -| Expected Fetches | 4 | -| Data Source | `llms.json` → `10.0/index.json` → `_links["release-manifest"]` → `manifest.json` → `_links["os-packages-json"]` → `os-packages.json` → filter by Ubuntu 24.04 | - -**Evaluation:** -- Tier 1: Correct package list for Ubuntu 24.04 specifically, 4 fetches -- Tier 2: Mostly correct, minor package name errors -- Tier 4: Generic Linux answer from training data, didn't reach os-packages.json - ---- - -### Test 9: Deep Manifest Navigation (libc Requirements) - -**Query:** "What's the minimum glibc version for .NET 10 on x64?" - -| Field | Value | -|-------|-------| -| Category | X3 (Linux Deployment) | -| Expected Answer | Specific version from `supported-os.json` → `libc[]` → filter by `name == "glibc"` and `architectures` contains `"x64"` → `version` | -| Expected Fetches | 4 | -| Data Source | `llms.json` → `10.0/index.json` → `manifest.json` → `_links["supported-os-json"]` → `supported-os.json` | - -**Evaluation:** -- Tier 1: Correct glibc version from graph, 4 fetches -- Tier 2: Correct version, slightly inefficient navigation -- Tier 4: Guessed from training data, didn't fetch supported-os.json - ---- - -### Test 10: Multi-step Time Traversal - -**Query:** "List all CVEs fixed in .NET 8 in the last 6 months with their severity." - -| Field | Value | -|-------|-------| -| Category | C2 (CVE Deep Analysis) | -| Expected Answer | CVE list with severities, covering ~6 months of security releases | -| Expected Fetches | 3-5 (llms.json + 2-4 security months via prev-security chain) | -| Data Source | `llms.json` → `latest-security-month` → walk `prev-security` links → filter `_embedded.disclosures[]` by `affected_releases` contains `"8.0"` | - -**Evaluation:** -- Tier 1: Complete CVE list, used prev-security chain efficiently, stopped at correct boundary -- Tier 2: Correct CVEs, but walked prev instead of prev-security (fetched non-security months) -- Tier 4: Incomplete list, walked past date boundary, or fabricated CVEs - ---- - -## Test Matrix - -``` -Tests 1-10 × 2 modes × N LLMs = 20N test runs -``` - -| Test | Mode A Expected | Mode B Expected | -|------|-----------------|-----------------| -| 1 | Tier 1-2 | Tier 1 | -| 2 | Tier 1-2 | Tier 1 | -| 3 | Tier 1-2 | Tier 1 | -| 4 | Tier 1-2 | Tier 1 | -| 5 | Tier 1-2 | Tier 1 | -| 6 | Tier 2 (deep nav) | Tier 1-2 | -| 7 | Tier 2 (deep nav) | Tier 1-2 | -| 8 | Tier 2-3 (very deep) | Tier 1-2 | -| 9 | Tier 2-3 (very deep) | Tier 1-2 | -| 10 | Tier 1-2 | Tier 1 | - -Tests 8-9 are expected to be harder in Mode A because the path to manifest.json isn't documented in `ai_note`—the LLM must either read llms.txt or explore `_links` independently. - -## Output Format - -Each test run produces a structured result: - -```json -{ - "test_id": "T1", - "llm": "claude-sonnet-4-20250514", - "mode": "A", - "query": "What is the latest patch for .NET 9?", - "answer": "The latest patch for .NET 9 is 9.0.11.", - - "scoring": { - "correct": true, - "expected_answer": "9.0.11", - "fetch_count": 1, - "expected_fetch_range": [1, 1], - "fetch_count_ok": true, - "url_fabrication": false, - "hallucination": false, - "tier": 1 - }, - - "diagnostics": { - "read_llms_txt": false, - "fetched_urls": [ - "https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/llms.json" - ], - "navigation_path": "llms.json → _embedded.latest_patches[]", - "notes": "Answered from embedded data without needing navigation guide" - } -} -``` - -**Scoring fields (determine tier):** -- `correct`: Does the answer match expected? -- `fetch_count_ok`: Is fetch count within expected range? -- `url_fabrication`: Did any URL not come from `_links`? -- `hallucination`: Did LLM state facts not in fetched docs? -- `tier`: 1-4 based on rubric - -**Diagnostic fields (explain patterns):** -- `read_llms_txt`: Did the LLM fetch llms.txt? -- `fetched_urls`: All URLs fetched, in order -- `navigation_path`: Summary of link traversal -- `notes`: Observations about behavior - -## Aggregate Scoring - -Per-LLM summary: - -```json -{ - "llm": "claude-sonnet-4-20250514", - "mode_a": { - "total_points": 36, - "max_points": 40, - "percentage": 90, - "tier_distribution": { "1": 8, "2": 1, "3": 1, "4": 0 }, - "avg_fetch_count": 2.3, - "url_fabrication_rate": 0.0, - "hallucination_rate": 0.0 - }, - "mode_b": { - "total_points": 38, - "max_points": 40, - "percentage": 95, - "tier_distribution": { "1": 9, "2": 1, "3": 0, "4": 0 }, - "avg_fetch_count": 2.1, - "url_fabrication_rate": 0.0, - "hallucination_rate": 0.0 - }, - "mode_differential": 5, - "recommendation": "Either entry point works", - - "diagnostics": { - "llms_txt_discovery_rate": 0.3, - "discovery_correlated_with_success": false, - "common_failure_point": null - } -} -``` - -**Scored metrics:** -- `total_points`, `percentage`: Overall performance -- `tier_distribution`: How many tests at each tier -- `url_fabrication_rate`: Should be 0% -- `hallucination_rate`: Should be 0% - -**Diagnostic metrics:** -- `llms_txt_discovery_rate`: Fraction of Mode A tests where LLM read llms.txt (observational) -- `discovery_correlated_with_success`: Did reading llms.txt predict better outcomes? -- `common_failure_point`: For Tier 3-4 results, where did navigation typically break? - -The `mode_differential` (Mode B % - Mode A %) indicates whether explicit guidance improves outcomes. High values suggest the LLM benefits from prose-first; near-zero suggests it navigates HAL effectively on its own. - -## Cross-LLM Comparison - -After running all LLMs, produce a comparison table: - -**Scored metrics:** - -| LLM | Mode A % | Mode B % | Differential | Fabrication | Hallucination | Recommendation | -|-----|----------|----------|--------------|-------------|---------------|----------------| -| Claude Sonnet | 90% | 95% | +5 | 0% | 0% | Either | -| Claude Opus | 95% | 97% | +2 | 0% | 0% | Either | -| GPT-4o | 70% | 85% | +15 | 5% | 5% | llms.txt | -| Gemini Pro | 75% | 80% | +5 | 0% | 10% | llms.txt | - -**Diagnostic metrics (Mode A only):** - -| LLM | Discovery Rate | Discovery Correlated | Common Failure Point | -|-----|----------------|----------------------|----------------------| -| Claude Sonnet | 30% | No | — | -| Claude Opus | 50% | No | — | -| GPT-4o | 10% | Yes | Deep manifest navigation | -| Gemini Pro | 20% | Yes | Breaking changes link | - -The diagnostic table helps explain *why* Mode A underperforms for certain LLMs: -- **Low discovery + high correlation**: LLM needs the guide but doesn't find it → recommend llms.txt -- **Low discovery + no correlation**: LLM succeeds without the guide → HAL structure is sufficient -- **High discovery + failures**: LLM reads guide but can't apply it → comprehension issue - -## Test Harness Requirements - -The test harness must: - -1. **Intercept fetches**: Capture all URLs the LLM requests, with ordering -2. **Trace link sources**: For each fetch after the first, record whether the URL came from a `_links` field in a prior response -3. **Inject preamble**: Provide the appropriate preamble for Mode A or B -4. **Evaluate correctness**: Compare answer against expected values -5. **Detect hallucination**: Flag facts not present in any fetched document -6. **Produce structured output**: JSON format as specified above - -## Running the Tests - -Suggested procedure: - -1. Run all 10 tests in Mode A for each LLM -2. Run all 10 tests in Mode B for each LLM -3. Score each run per the rubric -4. Produce per-LLM summaries -5. Produce cross-LLM comparison table -6. Update user documentation with recommendations - -Tests should be run with low temperature (0.0-0.2) to reduce variance. Consider running each test 3 times and taking the median score for robustness. - -## Updating Tests - -When the graph data changes (new patches, CVEs, or versions): - -1. Update expected answers in Tests 1-3, 10 (these reference current data) -2. Tests 4-9 are more stable (reference structure, not specific current values) -3. Re-run affected tests to validate - -When graph structure changes: - -1. Update expected fetch counts -2. Update data source paths -3. Consider adding new tests for new capabilities From 9aa7a54fd85d548c094eb392e887eb1eaa0114cc Mon Sep 17 00:00:00 2001 From: Richard Lander Date: Tue, 6 Jan 2026 15:34:04 -0800 Subject: [PATCH 11/15] Update conclusion --- accepted/2025/release-notes-graph/release-notes-graph.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/accepted/2025/release-notes-graph/release-notes-graph.md b/accepted/2025/release-notes-graph/release-notes-graph.md index 6b617493f..30743817c 100644 --- a/accepted/2025/release-notes-graph/release-notes-graph.md +++ b/accepted/2025/release-notes-graph/release-notes-graph.md @@ -1339,4 +1339,4 @@ The intent of this project is to publish structured release notes using a more e A case in point is the addition of breaking changes. Someone suggested that breaking changes should be added to the graph. It took < 60 mins to do that. The HAL design paradigm and the aligned philosophy of cold roots and weighted immutable leaves naturally alloted specific space for breaking changes to be attached on the load-bearing structure. There's no need to consult a design committee since (as a virtue) there isn't much design freedom once the overall approach is established. It's likely that we'll find a need to attach more information to the graph. We can just repeat the process. -The restrictive nature of this design ends up being well aligned with the performance and cost consideration of LLMs. That's a bit of foreshadowing of the other spec. It boils down being very intentional about the design being breadth- or depth-first. This design is bread-first oriented, which enables learning about a layer at a time. The reason that `llms.json` is able to pull ahead of the purist `index.json` breadth-first implementation is that it offers a more depth-first approach for specific queries. It's one of those "I know what I'm doing; trust me" situations. At the application layer, it's OK to break some rules. And that in a nutshell describes the design. +The restrictive nature of this design ends up being well aligned with the performance and cost consideration of LLMs. That's a bit of foreshadowing of the other spec. It boils down to being very intentional about the design being [breadth-](https://en.wikipedia.org/wiki/Breadth-first_search) or [depth-first](https://en.wikipedia.org/wiki/Depth-first_search). The core design is strongly breadth-first oriented, which enables learning about a layer at a time. The reason that `llms.json` is able to pull ahead of the purist `index.json` breadth-first implementation is that it offers a depth-first approach for specific queries. The wormholes and more so the spear-fishing are essentially saying "I already know the graph structure; let me skip to the leaf I need" which is the essence of depth-first targeting. And that in a nutshell describes the design and why it is effective. From ac05753229156ae8b684d75a47ac48ceaba9d118 Mon Sep 17 00:00:00 2001 From: Richard Lander Date: Tue, 6 Jan 2026 15:49:51 -0800 Subject: [PATCH 12/15] Update typos --- accepted/2025/release-notes-graph/release-notes-graph.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/accepted/2025/release-notes-graph/release-notes-graph.md b/accepted/2025/release-notes-graph/release-notes-graph.md index 30743817c..4a0864e2d 100644 --- a/accepted/2025/release-notes-graph/release-notes-graph.md +++ b/accepted/2025/release-notes-graph/release-notes-graph.md @@ -194,7 +194,7 @@ The following table summarizes the overall shape of the graph, starting at `dotn Notes: -- 2025 was used for `{year}`, 10 for `{month`}, 8.0 for `{version}`, and 8.0.21 for `{patch}` +- 2025 was used for `{year}`, 10 for `{month}`, 8.0 for `{version}`, and 8.0.21 for `{patch}` - The bottom of the graph has the largest variance over time. `8.0.21/index.json` is `5.3` KB while `8.0.22/index.json` is `4.8` KB, a little smaller because it was a non-security release. Similarly, `2025/10/index.json` is `5.0` KB while `2025/11/index.json` is `3.0` KB, significantly smaller because there was no security release that month. `2025/12/index.json` was even smaller because there was only a .NET 10 patch, also non-security. Security releases were chosen to demonstrate more realistic sizes. - `llms.json` will be covered more later. @@ -1021,7 +1021,7 @@ Very similar approach as other indices. Here, we see just `prev-year` to enable queries that chain across years. `latest-security-month` enables a jump to the latest month that included security fixes. From there, it's possible to enable queries that chain across `prev-security` months. -The `_emdedded` section contains: `months`. +The `_embedded` section contains: `months`. ```json "_embedded": { @@ -1337,6 +1337,8 @@ Raw results are recorded at [metrics/README.md](./metrics/README.md). The result The intent of this project is to publish structured release notes using a more efficient schema architecture, both to reduce update cadence and to enable smaller fetch costs. The belief was that an existing standard would push our schema efforts towards a strong design paradigm that ultimately enabled us to achieve our goals faster and better. The results seem to prove this out. The chosen solution is an opposite-end-of-the-spectrum approach. It generates a huge number of files compared to the existing approach, many of which will never be updated and rarely visited once the associated major version is out of support. The existing `releases.json` files are already much like that, just monolithic (and therein lies the challenge). -A case in point is the addition of breaking changes. Someone suggested that breaking changes should be added to the graph. It took < 60 mins to do that. The HAL design paradigm and the aligned philosophy of cold roots and weighted immutable leaves naturally alloted specific space for breaking changes to be attached on the load-bearing structure. There's no need to consult a design committee since (as a virtue) there isn't much design freedom once the overall approach is established. It's likely that we'll find a need to attach more information to the graph. We can just repeat the process. +Note: Prototype graph generation tools were written to generate all the JSON files. They are not part of the spec. + +A case in point is the addition of breaking changes. Someone suggested that breaking changes should be added to the graph. It took < 60 mins to do that. The HAL design paradigm and the aligned philosophy of cold roots and weighted immutable leaves naturally allotted specific space for breaking changes to be attached on the load-bearing structure. There's no need to consult a design committee since (as a virtue) there isn't much design freedom once the overall approach is established. It's likely that we'll find a need to attach more information to the graph. We can just repeat the process. The restrictive nature of this design ends up being well aligned with the performance and cost consideration of LLMs. That's a bit of foreshadowing of the other spec. It boils down to being very intentional about the design being [breadth-](https://en.wikipedia.org/wiki/Breadth-first_search) or [depth-first](https://en.wikipedia.org/wiki/Depth-first_search). The core design is strongly breadth-first oriented, which enables learning about a layer at a time. The reason that `llms.json` is able to pull ahead of the purist `index.json` breadth-first implementation is that it offers a depth-first approach for specific queries. The wormholes and more so the spear-fishing are essentially saying "I already know the graph structure; let me skip to the leaf I need" which is the essence of depth-first targeting. And that in a nutshell describes the design and why it is effective. From bfb0f302318b0599b0b9a3b3d1ef1447de5f0f91 Mon Sep 17 00:00:00 2001 From: Richard Lander Date: Tue, 6 Jan 2026 17:31:35 -0800 Subject: [PATCH 13/15] Move to 2026 directory --- .../release-notes-graph/metrics/README.md | 0 .../release-notes-graph/metrics/cve-stress-test.md | 0 .../release-notes-graph/metrics/easy-questions.md | 0 .../release-notes-graph/metrics/index-discovery.md | 0 .../metrics/interacting-with-environment.md | 0 .../metrics/project-file-analysis.md | 0 .../metrics/upgrade-whats-new.md | 0 .../release-notes-graph/release-notes-graph.md | 0 .../release-notes-graph/releases-json-tokens.png | Bin 9 files changed, 0 insertions(+), 0 deletions(-) rename accepted/{2025 => 2026}/release-notes-graph/metrics/README.md (100%) rename accepted/{2025 => 2026}/release-notes-graph/metrics/cve-stress-test.md (100%) rename accepted/{2025 => 2026}/release-notes-graph/metrics/easy-questions.md (100%) rename accepted/{2025 => 2026}/release-notes-graph/metrics/index-discovery.md (100%) rename accepted/{2025 => 2026}/release-notes-graph/metrics/interacting-with-environment.md (100%) rename accepted/{2025 => 2026}/release-notes-graph/metrics/project-file-analysis.md (100%) rename accepted/{2025 => 2026}/release-notes-graph/metrics/upgrade-whats-new.md (100%) rename accepted/{2025 => 2026}/release-notes-graph/release-notes-graph.md (100%) rename accepted/{2025 => 2026}/release-notes-graph/releases-json-tokens.png (100%) diff --git a/accepted/2025/release-notes-graph/metrics/README.md b/accepted/2026/release-notes-graph/metrics/README.md similarity index 100% rename from accepted/2025/release-notes-graph/metrics/README.md rename to accepted/2026/release-notes-graph/metrics/README.md diff --git a/accepted/2025/release-notes-graph/metrics/cve-stress-test.md b/accepted/2026/release-notes-graph/metrics/cve-stress-test.md similarity index 100% rename from accepted/2025/release-notes-graph/metrics/cve-stress-test.md rename to accepted/2026/release-notes-graph/metrics/cve-stress-test.md diff --git a/accepted/2025/release-notes-graph/metrics/easy-questions.md b/accepted/2026/release-notes-graph/metrics/easy-questions.md similarity index 100% rename from accepted/2025/release-notes-graph/metrics/easy-questions.md rename to accepted/2026/release-notes-graph/metrics/easy-questions.md diff --git a/accepted/2025/release-notes-graph/metrics/index-discovery.md b/accepted/2026/release-notes-graph/metrics/index-discovery.md similarity index 100% rename from accepted/2025/release-notes-graph/metrics/index-discovery.md rename to accepted/2026/release-notes-graph/metrics/index-discovery.md diff --git a/accepted/2025/release-notes-graph/metrics/interacting-with-environment.md b/accepted/2026/release-notes-graph/metrics/interacting-with-environment.md similarity index 100% rename from accepted/2025/release-notes-graph/metrics/interacting-with-environment.md rename to accepted/2026/release-notes-graph/metrics/interacting-with-environment.md diff --git a/accepted/2025/release-notes-graph/metrics/project-file-analysis.md b/accepted/2026/release-notes-graph/metrics/project-file-analysis.md similarity index 100% rename from accepted/2025/release-notes-graph/metrics/project-file-analysis.md rename to accepted/2026/release-notes-graph/metrics/project-file-analysis.md diff --git a/accepted/2025/release-notes-graph/metrics/upgrade-whats-new.md b/accepted/2026/release-notes-graph/metrics/upgrade-whats-new.md similarity index 100% rename from accepted/2025/release-notes-graph/metrics/upgrade-whats-new.md rename to accepted/2026/release-notes-graph/metrics/upgrade-whats-new.md diff --git a/accepted/2025/release-notes-graph/release-notes-graph.md b/accepted/2026/release-notes-graph/release-notes-graph.md similarity index 100% rename from accepted/2025/release-notes-graph/release-notes-graph.md rename to accepted/2026/release-notes-graph/release-notes-graph.md diff --git a/accepted/2025/release-notes-graph/releases-json-tokens.png b/accepted/2026/release-notes-graph/releases-json-tokens.png similarity index 100% rename from accepted/2025/release-notes-graph/releases-json-tokens.png rename to accepted/2026/release-notes-graph/releases-json-tokens.png From fbdeb62839d4ee4cd270bb3a96d29a8d0db8b890 Mon Sep 17 00:00:00 2001 From: Richard Lander Date: Tue, 6 Jan 2026 17:35:00 -0800 Subject: [PATCH 14/15] Restore linter file --- .markdownlint.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.markdownlint.json b/.markdownlint.json index 501575766..a02418cfd 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -3,7 +3,7 @@ "MD003": { "style": "atx" }, "MD004": false, "MD007": { "indent": 4 }, - "MD013": false, + "MD013": { "tables": false, "code_blocks": false }, "MD026": false, "no-hard-tabs": false -} +} \ No newline at end of file From 709feed618a38f6ab70cfe6eb83a483846e0d26e Mon Sep 17 00:00:00 2001 From: Rich Lander <2608468+richlander@users.noreply.github.com> Date: Thu, 8 Jan 2026 14:49:14 -0800 Subject: [PATCH 15/15] Add clarity on minor versions. --- accepted/2026/release-notes-graph/release-notes-graph.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/accepted/2026/release-notes-graph/release-notes-graph.md b/accepted/2026/release-notes-graph/release-notes-graph.md index 4a0864e2d..7a8ca6c88 100644 --- a/accepted/2026/release-notes-graph/release-notes-graph.md +++ b/accepted/2026/release-notes-graph/release-notes-graph.md @@ -322,7 +322,7 @@ Notes: ## Version Index Modeling -The version index has three layers: releases, major version, patch version. Most nodes in the graph are named `index.json`. The examples should look similar to the HAL spec documents shared earlier. +The version index has three layers: releases, major version, patch version. There is no concept of minor version. `10.0` and `10.1` (if such a version existed) would both be considered separate major verisons in this scheme. Most nodes in the graph are named `index.json`. The examples should look similar to the HAL spec documents shared earlier. ### Releases index