From c1cace7a545336f645a86f89983d95e59123d141 Mon Sep 17 00:00:00 2001 From: Andy Stark Date: Thu, 6 Aug 2026 15:38:06 +0100 Subject: [PATCH] DOC-6939 Add schema_version and since to the AI outputs Two fields the applied AI team asked for, both now agreed. schema_version is their candidate improvement 1 and since is their observation 01, which they called their only hard blocker: they could not move to the feed without losing version filtering, because the Markdown metadata block carried since for command pages and the JSON did not. schema_version is a single integer, emitted in every per-page JSON record and in the Markdown metadata block, currently 1. It lives in one place, aiSchemaVersion in config.toml, so there is one thing to bump rather than four templates to keep in step. The semantics are the part that makes it useful, and they are the team's own definition: it increments only when the shape of a record changes -- a field added, removed or renamed, or a new value entering the role vocabulary -- and never when content changes. A version that moved with content would be a second content hash, and they were explicit that they would learn to ignore it. That distinction is written into both the config comment and the published documentation, because the field is worthless without it. Worth noting what does NOT bump it, since the temptation will be there: changing what a field contains is not a shape change. The page id changing from a filename to a path is a value change, so version 1 covers both. since comes straight from the command pages' frontmatter, present on 574 pages and absent elsewhere, matching what the Markdown metadata block already published. Section pages do not gain a spurious empty field. The transform declares both fields on its input and output interfaces rather than letting them ride through on a spread. They were already surviving, because the spread carries unknown keys at runtime, but nothing said so -- and a future refactor that replaced the spread with explicit field copying would have dropped the one field consumers gate their parsing on, silently. Verified on a full build and transform: schema_version is 1 on all 5,733 records and in the Markdown metadata block, since appears on exactly 574 records against 574 source files declaring it, and content_hash still verifies for all 5,687 content pages. Constraint: aiSchemaVersion in config.toml must increment only for a change to the record's shape -- a field added, removed or renamed, or a new role value -- and never for a content change or a change to what an existing field contains, or consumers stop trusting it and gate on content_hash alone Constraint: schema_version and since are declared on both interfaces in build/transform_json_sections.ts so they survive deliberately rather than incidentally -- the spread would carry them either way today, but replacing it with explicit copying would drop them without failing Ticket: DOC-6939 Co-Authored-By: Claude Opus 5 (1M context) --- build/transform_json_sections.ts | 8 ++++++++ config.toml | 11 +++++++++++ content/ai-agent-resources.md | 13 +++++++++++++ layouts/_default/section.json | 4 +++- layouts/_default/section.md | 1 + layouts/_default/single.json | 4 +++- layouts/_default/single.md | 1 + 7 files changed, 40 insertions(+), 2 deletions(-) diff --git a/build/transform_json_sections.ts b/build/transform_json_sections.ts index daf7512007..65ea758bad 100644 --- a/build/transform_json_sections.ts +++ b/build/transform_json_sections.ts @@ -47,10 +47,16 @@ interface CodeExample { } interface PageJsonInput { + // Emitted by the Hugo templates and carried through untouched. Declared so the + // spread below preserves them explicitly rather than by accident: schema_version is + // what consumers gate re-parsing on, and since is the only version information the + // feed carries. + schema_version?: number; id: string; title: string; url: string; summary: string; + since?: string; content?: string; tags: string[]; last_updated: string; @@ -60,10 +66,12 @@ interface PageJsonInput { type PageType = 'content' | 'index'; interface PageJsonOutput { + schema_version?: number; id: string; title: string; url: string; summary: string; + since?: string; page_type: PageType; content_hash?: string; tags: string[]; diff --git a/config.toml b/config.toml index bd7c208cde..480907858b 100644 --- a/config.toml +++ b/config.toml @@ -62,6 +62,17 @@ anchor = "smart" tagManagerId = "GTM-TKZ6J9R" gitHubRepo = "https://github.com/redis/docs" +# Schema version for the AI-facing JSON and Markdown outputs. Emitted as +# `schema_version` in each per-page JSON record and in the Markdown metadata block. +# +# Increment this ONLY when the shape of a record changes: a field added, removed or +# renamed, or a new value entering the `role` vocabulary. It must NOT change when page +# content changes -- consumers gate re-parsing on it, and a version that moves with +# content is a second content hash, which they will learn to ignore. +# +# Changing what a field *contains* is not a shape change and does not bump this. +aiSchemaVersion = 1 + # Display and sort order for client examples clientsExamples = ["Python", "Node.js", "ioredis", "Java-Sync", "Lettuce-Sync", "Java-Async", "Java-Reactive", "Go", "C", "C#-Sync (NRedisStack)", "C#-Async (NRedisStack)", "C#-Sync (SE.Redis)", "C#-Async (SE.Redis)", "RedisVL", "PHP", "Ruby", "Rust-Sync", "Rust-Async"] searchService = "/convai/api/search-service" diff --git a/content/ai-agent-resources.md b/content/ai-agent-resources.md index 6ae34a4aee..b5c579f26f 100644 --- a/content/ai-agent-resources.md +++ b/content/ai-agent-resources.md @@ -65,16 +65,29 @@ Two consequences worth knowing if you diff the feed against the sitemap: - Both figures move. The feed is rebuilt at least daily and the corpus grows, so treat any page count as a snapshot. +### Schema version + +Every record carries a `schema_version` integer, and the same value appears in the +`json metadata` block of the Markdown output. It is currently **1**. + +It increments only when the **shape** of a record changes: a field added, removed or +renamed, or a new value entering the [role vocabulary](#section-roles). It does **not** +change when page content changes, and it does not change when the value inside a field +changes without the field itself changing. Use `content_hash` to detect content changes; +use `schema_version` to detect when your parser might need attention. + ### JSON schema Each document contains: | Field | Type | Description | |-------|------|-------------| +| `schema_version` | integer | Version of the record format. See [Schema version](#schema-version). | | `id` | string | Unique identifier, the page's path without a file extension (for example `develop/clients/redis-py`) | | `title` | string | Page title | | `url` | string | Canonical URL | | `summary` | string | Short description | +| `since` | string | Redis version the command was introduced in. Present on command pages only. | | `page_type` | string | `"content"` (has prose) or `"index"` (navigation only) | | `content_hash` | string | SHA256 hash for cache invalidation (content pages only) | | `sections` | array | Content split by headings with semantic roles | diff --git a/layouts/_default/section.json b/layouts/_default/section.json index f88dafde72..95915770b7 100644 --- a/layouts/_default/section.json +++ b/layouts/_default/section.json @@ -22,10 +22,12 @@ {{- end -}} { + "schema_version": {{ site.Params.aiSchemaVersion | jsonify }}, "id": {{ $id | jsonify }}, "title": {{ .Title | jsonify }}, "url": {{ .Permalink | jsonify }}, - "summary": {{ $summary | jsonify }}, + "summary": {{ $summary | jsonify }},{{ with .Params.since }} + "since": {{ . | jsonify }},{{ end }} "content": {{ $content | jsonify }}, "tags": {{ $tags | jsonify }}, "last_updated": {{ $lastUpdated | jsonify }}, diff --git a/layouts/_default/section.md b/layouts/_default/section.md index 800ebf5e4b..139c2741f6 100644 --- a/layouts/_default/section.md +++ b/layouts/_default/section.md @@ -2,6 +2,7 @@ ```json metadata { + "schema_version": {{ site.Params.aiSchemaVersion | jsonify }}, "title": {{ .Title | jsonify }}, "description": {{ (.Params.description | default .Description) | plainify | replaceRE "\\s+" " " | strings.TrimSpace | jsonify }}, "categories": {{ .Params.categories | jsonify }}{{ if .Params.arguments }}, diff --git a/layouts/_default/single.json b/layouts/_default/single.json index a9325cbf43..30727f7fed 100644 --- a/layouts/_default/single.json +++ b/layouts/_default/single.json @@ -11,10 +11,12 @@ {{- $lastUpdated := .Lastmod.Format "2006-01-02T15:04:05Z07:00" -}} { + "schema_version": {{ site.Params.aiSchemaVersion | jsonify }}, "id": {{ $id | jsonify }}, "title": {{ .Title | jsonify }}, "url": {{ .Permalink | jsonify }}, - "summary": {{ $summary | jsonify }}, + "summary": {{ $summary | jsonify }},{{ with .Params.since }} + "since": {{ . | jsonify }},{{ end }} "content": {{ $content | jsonify }}, "tags": {{ $tags | jsonify }}, "last_updated": {{ $lastUpdated | jsonify }} diff --git a/layouts/_default/single.md b/layouts/_default/single.md index 800ebf5e4b..139c2741f6 100644 --- a/layouts/_default/single.md +++ b/layouts/_default/single.md @@ -2,6 +2,7 @@ ```json metadata { + "schema_version": {{ site.Params.aiSchemaVersion | jsonify }}, "title": {{ .Title | jsonify }}, "description": {{ (.Params.description | default .Description) | plainify | replaceRE "\\s+" " " | strings.TrimSpace | jsonify }}, "categories": {{ .Params.categories | jsonify }}{{ if .Params.arguments }},