Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ jobs:
echo "any=false" >> "$GITHUB_OUTPUT"
else
echo "any=true" >> "$GITHUB_OUTPUT"
echo "versions=$(git diff --name-only -- data | grep '\.json\.gz$' | sed 's#data/##; s#/# v#; s#\.json\.gz##' | paste -sd', ' -)" >> "$GITHUB_OUTPUT"
# `v` only where there is a version to prefix: an untagged source is
# dated, and `v2026-09-01` names a tag that does not exist.
echo "versions=$(git diff --name-only -- data | grep '\.json\.gz$' | sed -E 's#^data/##; s#\.json\.gz$##; s#/([0-9]+\.[0-9]+)# v\1#; s#/# #' | paste -sd', ' -)" >> "$GITHUB_OUTPUT"
fi

# Build first: the MCP worker tests read dist/. The tests assert against
Expand Down
34 changes: 27 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ bun run build

`bun run ingest --force` re-normalizes every tracked release. It should produce a byte-identical
`data/` — snapshots are fully sorted and gzipped at a fixed level precisely so that "the file
changed" is a reliable signal that upstream moved.
changed" is a reliable signal that upstream moved. For an untagged source it re-walks the history
from the floor and drops the existing snapshots first, because which commits produce a distinct
model is exactly what a changed normalizer changes.

## The GenAI registry is untagged

Expand All @@ -62,9 +64,27 @@ tags, a towncrier CHANGELOG reading only "Unreleased", and `stability: developme
itself. Waiting for a tag would mean tracking nothing while the attributes are already in
production use.

So it is tracked from `main`, versioned by commit date (`2026-09-03`) with the short SHA kept as the
tag, and labelled unreleased everywhere it appears. Snapshots are written only when the *normalized
model* changes, not when the branch moves — otherwise `data/` would grow by the calendar.
So its history is the commits themselves. Every commit touching `model/` is normalized, versioned
by commit date (`2026-09-01`) with the short SHA kept as the tag, and labelled unreleased
everywhere it appears. Two rules keep that proportional to real change:

- A commit is kept only when the **differ** finds something in it. Most do not — a Weaver bump, a
link pin, a reflowed brief — and gating on the normalized bytes instead would be stricter than
the diff and publish release pages listing no changes at all.
- Upstream merges several model changes on a busy day, so the second one that day takes
`2026-05-05.2`. The suffix sorts after the bare date, and without it the second commit would
overwrite the first snapshot.

The walk floors at `ebe3d1f`, the commit that made this a repository of its own. One commit earlier
the filtered history still carries the 52 `aws.*` attributes the split dropped, so crossing that
boundary would report 52 removals the project never made. Everything older is already covered by
the tagged semantic-conventions snapshots, which carried `gen_ai.*` until v1.44.0 deprecated it in
place.

Because the registry is `development` throughout, nothing in it can rank `breaking` — see
[Severity](#severity). Its editorial commits are also kept off the front page and the RSS feed: a
reworded brief on an untagged branch is not news, and forty of them in a row would bury every
tagged release.

The consequence worth knowing: ~59 attributes exist in **both** registries — deprecated
"Moved to..." stubs in semantic-conventions, live definitions in GenAI. Anything answering "is this
Expand Down Expand Up @@ -143,9 +163,9 @@ matching asset (`/mcp`, 404s). Asset paths are served on `www` too, with every p

## Data

Snapshots start at semconv v1.30.0, specification v1.42.0 and OTLP v1.4.0. Earlier releases used
schemas and layouts different enough that diffing across them would report changes the projects
never made.
Snapshots start at semconv v1.30.0, specification v1.42.0, OTLP v1.4.0 and GenAI `ebe3d1f`
(2026-05-05). Earlier releases used schemas and layouts different enough that diffing across them
would report changes the projects never made.

## Licence

Expand Down
153 changes: 151 additions & 2 deletions bun.lock

Large diffs are not rendered by default.

Binary file added data/genai/2026-05-05.2.json.gz
Binary file not shown.
Binary file added data/genai/2026-05-05.3.json.gz
Binary file not shown.
Binary file added data/genai/2026-05-05.4.json.gz
Binary file not shown.
Binary file added data/genai/2026-05-05.json.gz
Binary file not shown.
Binary file added data/genai/2026-05-08.json.gz
Binary file not shown.
Binary file added data/genai/2026-05-11.2.json.gz
Binary file not shown.
Binary file added data/genai/2026-05-11.json.gz
Binary file not shown.
Binary file added data/genai/2026-05-16.json.gz
Binary file not shown.
Binary file added data/genai/2026-05-18.json.gz
Binary file not shown.
Binary file added data/genai/2026-05-19.json.gz
Binary file not shown.
Binary file added data/genai/2026-05-26.json.gz
Binary file not shown.
Binary file added data/genai/2026-06-04.2.json.gz
Binary file not shown.
Binary file added data/genai/2026-06-04.json.gz
Binary file not shown.
Binary file added data/genai/2026-06-05.json.gz
Binary file not shown.
Binary file added data/genai/2026-06-08.json.gz
Binary file not shown.
Binary file added data/genai/2026-06-10.json.gz
Binary file not shown.
Binary file added data/genai/2026-06-17.2.json.gz
Binary file not shown.
Binary file added data/genai/2026-06-17.json.gz
Binary file not shown.
Binary file added data/genai/2026-06-18.2.json.gz
Binary file not shown.
Binary file added data/genai/2026-06-18.json.gz
Binary file not shown.
Binary file added data/genai/2026-06-19.json.gz
Binary file not shown.
Binary file added data/genai/2026-06-23.json.gz
Binary file not shown.
Binary file added data/genai/2026-06-24.json.gz
Binary file not shown.
Binary file added data/genai/2026-06-25.2.json.gz
Binary file not shown.
Binary file added data/genai/2026-06-25.json.gz
Binary file not shown.
Binary file added data/genai/2026-07-08.json.gz
Binary file not shown.
Binary file added data/genai/2026-07-16.json.gz
Binary file not shown.
Binary file added data/genai/2026-07-23.json.gz
Binary file not shown.
Binary file added data/genai/2026-07-28.json.gz
Binary file not shown.
Binary file added data/genai/2026-07-31.json.gz
Binary file not shown.
Binary file added data/genai/2026-08-01.json.gz
Binary file not shown.
Binary file added data/genai/2026-08-04.json.gz
Binary file not shown.
Binary file added data/genai/2026-08-05.2.json.gz
Binary file not shown.
Binary file added data/genai/2026-08-05.json.gz
Binary file not shown.
Binary file added data/genai/2026-08-06.json.gz
Binary file not shown.
Binary file added data/genai/2026-08-07.json.gz
Binary file not shown.
Binary file added data/genai/2026-08-10.json.gz
Binary file not shown.
Binary file added data/genai/2026-08-20.json.gz
Binary file not shown.
Binary file added data/genai/2026-08-27.json.gz
Binary file not shown.
Binary file added data/genai/2026-09-01.json.gz
Binary file not shown.
Binary file removed data/genai/2026-09-03.json.gz
Binary file not shown.
324 changes: 318 additions & 6 deletions data/index.json

Large diffs are not rendered by default.

Binary file modified data/semconv/1.30.0.json.gz
Binary file not shown.
Binary file modified data/semconv/1.31.0.json.gz
Binary file not shown.
Binary file modified data/semconv/1.32.0.json.gz
Binary file not shown.
Binary file modified data/semconv/1.33.0.json.gz
Binary file not shown.
Binary file modified data/semconv/1.34.0.json.gz
Binary file not shown.
Binary file modified data/semconv/1.35.0.json.gz
Binary file not shown.
Binary file modified data/semconv/1.36.0.json.gz
Binary file not shown.
Binary file modified data/semconv/1.37.0.json.gz
Binary file not shown.
Binary file modified data/semconv/1.38.0.json.gz
Binary file not shown.
Binary file modified data/semconv/1.39.0.json.gz
Binary file not shown.
Binary file modified data/semconv/1.40.0.json.gz
Binary file not shown.
Binary file modified data/semconv/1.41.0.json.gz
Binary file not shown.
Binary file modified data/semconv/1.41.1.json.gz
Binary file not shown.
Binary file modified data/semconv/1.42.0.json.gz
Binary file not shown.
Binary file modified data/semconv/1.43.0.json.gz
Binary file not shown.
Binary file modified data/semconv/1.44.0.json.gz
Binary file not shown.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"scripts": {
"ingest": "bun run src/ingest/main.ts",
"test": "bun test",
"typecheck": "tsc --noEmit",
"typecheck": "tsc --noEmit && astro check",
"dev": "astro dev --port 4321",
"build": "astro build",
"preview": "astro preview --port 4321",
Expand All @@ -24,6 +24,7 @@
"yaml": "^2.8.1"
},
"devDependencies": {
"@astrojs/check": "^0.9.10",
"@cloudflare/workers-types": "^5.20260903.1",
"@types/bun": "^1.3.11",
"typescript": "^5.9.3",
Expand Down
1 change: 1 addition & 0 deletions site/lib/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ const KIND_LABEL: Record<string, string> = {
"brief-changed": "wording",
"note-changed": "guidance",
"examples-changed": "examples",
"usage-changed": "usage",
"first-seen": "added",
"requirement-added": "new rule",
"requirement-removed": "rule dropped",
Expand Down
13 changes: 10 additions & 3 deletions site/pages/about.astro
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ const data = await catalog()
<strong>The GenAI conventions</strong> are the same registry model in a separate repository,
split out of the conventions in v1.44.0. That repository has never cut a release &mdash; no
tags, a changelog reading only "Unreleased" &mdash; so it is tracked from its default branch,
dated rather than versioned, and marked unreleased everywhere it appears.
dated rather than versioned, and marked unreleased everywhere it appears. Its history is the
commits themselves: every commit touching the model tree is normalized, and the ones that
leave the model unchanged &mdash; a tooling bump, a link pin &mdash; are dropped rather than
published as a release with nothing in it.
</p>
<p class="m-0">
The split leaves roughly 59 attributes in both registries at once: deprecated
Expand Down Expand Up @@ -91,8 +94,12 @@ const data = await catalog()
<li>
History starts at semantic conventions v{data.semconv.versions.at(-1)}, specification
v{data.spec.versions.at(-1)} and OTLP v{data.proto.versions.at(-1)}. Anything described as
first seen there may be much older. The GenAI registry has no history at all yet &mdash;
tracking began {data.genai.versions.at(-1)}.
first seen there may be much older. The GenAI registry is walked commit by commit from
{" "}{data.genai.versions.at(-1)}, the point at which it became a repository of its own;
one commit earlier its filtered history still carries the 52 <code
class="font-mono text-[0.92em]">aws.*</code
> attributes the split dropped, and reading across that boundary would report 52 removals
the project never made.
</li>
<li>
Specification changes are tracked as requirements, not as prose diffs. A paragraph rewritten
Expand Down
9 changes: 6 additions & 3 deletions site/pages/diff.astro
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ const defaults = Object.fromEntries(
const summary = document.getElementById("summary") as HTMLElement
const out = document.getElementById("out") as HTMLElement

/** Mirrors `versionLabel`: a dated, untagged source has no `v` to prefix. */
const label = (version: string) => (version.includes("-") ? version : `v${version}`)

const versions = JSON.parse(form.dataset["versions"] ?? "{}") as Record<string, string[]>
const defaults = JSON.parse(form.dataset["defaults"] ?? "{}") as Record<string, { from: string; to: string }>

Expand All @@ -85,7 +88,7 @@ const defaults = Object.fromEntries(
function fillVersions(preferred?: { from: string; to: string }) {
const list = versions[sourceSelect.value] ?? []
for (const select of [fromSelect, toSelect]) {
select.innerHTML = list.map((v) => `<option value="${v}">v${v}</option>`).join("")
select.innerHTML = list.map((v) => `<option value="${v}">${label(v)}</option>`).join("")
}
const fallback = defaults[sourceSelect.value] ?? { from: list[0] ?? "", to: list.at(-1) ?? "" }
fromSelect.value = preferred?.from && list.includes(preferred.from) ? preferred.from : fallback.from
Expand Down Expand Up @@ -132,7 +135,7 @@ const defaults = Object.fromEntries(
// naming rather than a 404.
const list = versions[source] ?? []
if (list.indexOf(from) > list.indexOf(to)) {
summary.textContent = `v${from} is newer than v${to} — swap them to see what changed.`
summary.textContent = `${label(from)} is newer than ${label(to)} — swap them to see what changed.`
out.innerHTML = ""
return
}
Expand All @@ -144,7 +147,7 @@ const defaults = Object.fromEntries(
if (!diff) {
const response = await fetch(`/api/diff/${key}.json`)
if (!response.ok) {
summary.textContent = `No diff available for v${from} to v${to}.`
summary.textContent = `No diff available for ${label(from)} to ${label(to)}.`
out.innerHTML = ""
return
}
Expand Down
39 changes: 23 additions & 16 deletions site/pages/feed.xml.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { APIRoute } from "astro"
import { catalog, SOURCE_LABEL } from "../../src/model/catalog.ts"
import { catalog, SOURCE_LABEL, UNRELEASED } from "../../src/model/catalog.ts"
import { versionLabel } from "../lib/format.ts"
import { SITE } from "../lib/site.ts"

const escape = (s: string) =>
Expand All @@ -9,27 +10,33 @@ export const GET: APIRoute = async ({ site }) => {
const data = await catalog()
const origin = site?.origin ?? SITE.origin

const items = data.feed.map(({ source, version, diff, release }) => {
const significant = diff.changes.filter((c) => c.severity !== "informational")
const lines = significant
.slice(0, 30)
.map((c) => `${c.severity.toUpperCase()} · ${c.entity} ${c.kind} · ${c.id} — ${c.detail}`)
const body = [
`${diff.counts.breaking} breaking, ${diff.counts.notable} notable, ${diff.counts.informational} editorial changes since v${diff.from}.`,
...lines,
significant.length > 30 ? `…and ${significant.length - 30} more.` : "",
]
.filter(Boolean)
.join("\n")
const items = data.feed
.filter(
// Same rule as the front page: a subscriber wants releases and the
// commits that changed something, not every wording fix on a branch.
({ source, diff }) => !UNRELEASED[source] || diff.changes.some((c) => c.severity !== "informational"),
)
.map(({ source, version, diff, release }) => {
const significant = diff.changes.filter((c) => c.severity !== "informational")
const lines = significant
.slice(0, 30)
.map((c) => `${c.severity.toUpperCase()} · ${c.entity} ${c.kind} · ${c.id} — ${c.detail}`)
const body = [
`${diff.counts.breaking} breaking, ${diff.counts.notable} notable, ${diff.counts.informational} editorial changes since ${versionLabel(diff.from)}.`,
...lines,
significant.length > 30 ? `…and ${significant.length - 30} more.` : "",
]
.filter(Boolean)
.join("\n")

return ` <item>
<title>${SOURCE_LABEL[source]} v${version} — ${diff.counts.breaking} breaking, ${diff.counts.notable} notable</title>
return ` <item>
<title>${SOURCE_LABEL[source]} ${versionLabel(version)} — ${diff.counts.breaking} breaking, ${diff.counts.notable} notable</title>
<link>${origin}/releases/${source}/${version}</link>
<guid isPermaLink="true">${origin}/releases/${source}/${version}</guid>
<pubDate>${new Date(release?.publishedAt ?? Date.now()).toUTCString()}</pubDate>
<description>${escape(body)}</description>
</item>`
})
})

const xml = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
Expand Down
24 changes: 17 additions & 7 deletions site/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { formatDate, severityClass, versionLabel } from "../lib/format.ts"
import { SITE } from "../lib/site.ts"

const data = await catalog()
const linkable = new Set(data.attributes.map((a) => a.id))
const linkable = data.linkableAttributes

const requirements = data.spec.latest.sections.reduce((n, section) => n + section.normative.length, 0)

Expand Down Expand Up @@ -49,10 +49,18 @@ const domains = buildDomains(data).map((domain) => ({
signals: domain.metrics.length + domain.spans.length + domain.events.length + domain.entities.length,
}))

const feed = data.feed.map((entry) => ({
...entry,
significant: entry.diff.changes.filter((c) => c.severity !== "informational"),
}))
/**
* An untagged source moves by commit rather than by release, so its editorial
* churn — a reworded brief, a new example — is not front-page news, and forty
* consecutive rows of it would bury every tagged release under the fold. Those
* entries keep their own page and stay reachable from the neighbouring ones.
*/
const feed = data.feed
.map((entry) => ({
...entry,
significant: entry.diff.changes.filter((c) => c.severity !== "informational"),
}))
.filter((entry) => !UNRELEASED[entry.source] || entry.significant.length > 0)
---

<Shell title={SITE.name} description={SITE.description}>
Expand Down Expand Up @@ -158,10 +166,12 @@ const feed = data.feed.map((entry) => ({
<a href={`/releases/${source}/${version}`} class="text-muted">
{significant.length > 6
? `All ${significant.length} notable changes`
: `Full diff from v${diff.from}`}
: `Full diff from ${versionLabel(diff.from)}`}
{diff.counts.informational > 0 && ` + ${diff.counts.informational} editorial`}
</a>
{release && <a href={release.url}>Upstream release notes</a>}
{release && (
<a href={release.url}>{UNRELEASED[source] ? "Upstream commit" : "Upstream release notes"}</a>
)}
</p>
</article>
))
Expand Down
62 changes: 43 additions & 19 deletions site/pages/releases/[source]/[version].astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import ChangeList from "../../../components/ChangeList.astro"
import Pill from "../../../components/Pill.astro"
import Shell from "../../../layouts/Shell.astro"
import { catalog, SOURCES, SOURCE_LABEL } from "../../../../src/model/catalog.ts"
import { catalog, SOURCES, SOURCE_LABEL, UNRELEASED } from "../../../../src/model/catalog.ts"
import type { Severity } from "../../../../src/model/change.ts"
import type { SourceId } from "../../../../src/model/types.ts"
import { formatDate, SEVERITY_LABEL, severityClass, versionLabel } from "../../../lib/format.ts"
Expand All @@ -19,7 +19,14 @@ const data = await catalog()
const catalogue = data.source(source)

const release = catalogue.release(diff.to)
const linkable = new Set(data.attributes.map((a) => a.id))
/**
* What upstream calls this snapshot. A tagged source's release tag is `v1.44.0`,
* but an untagged one is dated — `v2026-09-01` names no ref, and every deep link
* built from it 404s. The commit SHA the record carries is the tree the snapshot
* was actually read from.
*/
const upstreamTag = release?.tag ?? `v${diff.to}`
const linkable = data.linkableAttributes
const index = catalogue.diffs.findIndex((d) => d.to === diff.to)
const newer = index > 0 ? catalogue.diffs[index - 1] : undefined
const older = catalogue.diffs[index + 1]
Expand All @@ -32,6 +39,10 @@ const BREAKING_BLURB: Record<SourceId, string> = {
semconv: "Changes to definitions the project had already marked stable or release candidate.",
spec: "Requirements added to, removed from, or restrengthened in a document marked Stable.",
proto: "Changes to a released wire definition. Development packages are ranked notable instead.",
// Never rendered in practice: the GenAI registry is development throughout, so
// nothing in it can rank breaking. Kept so the promise is written down rather
// than implied by an absent key.
genai: "Changes to definitions already marked stable or release candidate.",
}

/**
Expand All @@ -41,25 +52,38 @@ const BREAKING_BLURB: Record<SourceId, string> = {
* because they enumerate the same renames our own diff already lists, and the
* trailing issue references are noise at this size.
*/
const upstreamBullets = (release?.body ?? "")
.split("\n")
.filter((line) => line.startsWith("- "))
.map((line) =>
line
.slice(2)
.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
.replace(/\s*\(#\d+\)\s*$/, "")
.replace(/`/g, "")
.replace(/\s+/g, " ")
.trim(),
)
const clean = (line: string) =>
line
.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
.replace(/\s*\(#\d+\)\s*$/, "")
.replace(/`/g, "")
.replace(/\s+/g, " ")
.trim()

const body = release?.body ?? ""

/**
* An untagged source has no release notes to lift from: its record carries the
* commit subject instead, which is the one sentence its author wrote about the
* change and worth the same box.
*/
const untagged = UNRELEASED[source] === true

const upstreamBullets = (
untagged
? [clean(body)]
: body
.split("\n")
.filter((line) => line.startsWith("- "))
.map((line) => clean(line.slice(2)))
)
.filter((line) => line.length > 0)
.slice(0, 10)
---

<Shell
title={`${SOURCE_LABEL[source]} v${diff.to}`}
description={`What changed in OpenTelemetry ${SOURCE_LABEL[source].toLowerCase()} v${diff.to}: ${diff.counts.breaking} breaking, ${diff.counts.notable} notable and ${diff.counts.informational} editorial changes since v${diff.from}.`}
title={`${SOURCE_LABEL[source]} ${versionLabel(diff.to)}`}
description={`What changed in OpenTelemetry ${SOURCE_LABEL[source].toLowerCase()} ${versionLabel(diff.to)}: ${diff.counts.breaking} breaking, ${diff.counts.notable} notable and ${diff.counts.informational} editorial changes since ${versionLabel(diff.from)}.`}
>
<nav class="text-[13px] text-faint"><a href="/">Changes</a></nav>

Expand Down Expand Up @@ -93,7 +117,7 @@ const upstreamBullets = (release?.body ?? "")
upstreamBullets.length > 0 && (
<section class="mt-7 rounded-md border border-rule bg-surface px-4 py-3">
<h2 class="m-0 text-[11px] uppercase tracking-[0.12em] text-faint font-semibold">
From the upstream release notes
{untagged ? "From the upstream commit" : "From the upstream release notes"}
</h2>
<ul class="mt-2 m-0 pl-4 flex flex-col gap-1.5 text-[13.5px] text-muted">
{upstreamBullets.map((line) => (
Expand All @@ -102,7 +126,7 @@ const upstreamBullets = (release?.body ?? "")
</ul>
{release && (
<p class="mt-3 mb-0 text-[13px]">
<a href={release.url}>Read the full release notes</a>
<a href={release.url}>{untagged ? "Read the commit" : "Read the full release notes"}</a>
</p>
)}
</section>
Expand All @@ -120,7 +144,7 @@ const upstreamBullets = (release?.body ?? "")
<p class="mt-1 mb-0 text-[13.5px] text-muted max-w-[68ch]">{BREAKING_BLURB[source]}</p>
)}
<div class="mt-2">
<ChangeList changes={section.changes} linkable={linkable} source={source} tag={`v${diff.to}`} />
<ChangeList changes={section.changes} linkable={linkable} source={source} tag={upstreamTag} />
</div>
</section>
))
Expand Down
Loading
Loading