From 08dfd8426284715f4efef5a29480808f0e1f5efe Mon Sep 17 00:00:00 2001 From: Thomas Berdy Date: Wed, 5 Aug 2026 15:49:48 +0200 Subject: [PATCH] feat(docs): switch to the namespaced documented-data-type marker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The engine renamed its documented-data-type flag from `x-has-data-type` to `x-mergify-has-data-type`, matching the namespacing already used for `x-mergify-enum`. An unprefixed `x-` key is a claim on a name nobody owns, which is how `x-enum-descriptions` ended up meaning a positional array of strings to openapi-generator and a map to us. Read the new spelling only. The engine publishes both today, so this is safe now — and reading only one is what makes dropping the other possible: while a consumer accepts either, nothing can tell which it actually depends on, so the removal can never be shown to be safe. This is the step that unblocks it. Only `isDataType` reads the flag — the anchor collector, both config tables and the build gate all go through it — so this is one predicate, not a sweep. The comments and the anchor-check message that named the old key are updated with it; a marker's name appearing in prose that no longer matches what the code looks for is how the next reader gets sent after the wrong thing. Part of MRGFY-8330 Co-Authored-By: Claude Opus 5 (1M context) Change-Id: I1575ebfdb9f97f7a6ecfa25815036b07836d026c --- src/components/Tables/ConfigOptions.test.tsx | 22 ++++++++++------- src/components/Tables/ConfigOptions.tsx | 2 +- src/util/dataType.test.ts | 26 ++++++++++++++------ src/util/dataType.ts | 25 +++++++++++-------- src/util/dataTypeAnchors.ts | 4 +-- src/util/enumChoices.ts | 2 +- src/util/schemaToMarkdown.ts | 2 +- 7 files changed, 52 insertions(+), 31 deletions(-) diff --git a/src/components/Tables/ConfigOptions.test.tsx b/src/components/Tables/ConfigOptions.test.tsx index 31c84f694a..635739f287 100644 --- a/src/components/Tables/ConfigOptions.test.tsx +++ b/src/components/Tables/ConfigOptions.test.tsx @@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest'; import { getValueTypeText } from '~/util/schemaToMarkdown'; import { getValueType } from './ConfigOptions'; -// The x-has-data-type contract, render side: a node flagged as a documented data +// The x-mergify-has-data-type contract, render side: a node flagged as a documented data // type becomes a link to its data-types section instead of an expansion of // its shape, for both the HTML tables (getValueType) and the markdown/LLM // export (getValueTypeText). The node's `title` drives the label and the @@ -15,7 +15,7 @@ const HREF = '/configuration/data-types#queue-dequeue-reason'; const inlineMarked = { title: 'Queue dequeue reason', - 'x-has-data-type': true, + 'x-mergify-has-data-type': true, anyOf: [{ enum: ['PR_MERGED', 'PR_DEQUEUED'], type: 'string' }, { type: 'null' }], }; @@ -24,7 +24,7 @@ function render(schema: object, definition: object): string { return element === null ? '' : renderToStaticMarkup(element); } -describe('getValueType with x-has-data-type', () => { +describe('getValueType with x-mergify-has-data-type', () => { it('links an inline-flagged node instead of expanding its enum', () => { const html = render({}, inlineMarked); expect(html).toContain(`href="${HREF}"`); @@ -34,7 +34,7 @@ describe('getValueType with x-has-data-type', () => { it('links a flag published as a $ref sibling, taking the title from the target', () => { const schema = { $defs: { QueueCode: { title: 'Queue dequeue reason', type: 'string' } } }; - const html = render(schema, { $ref: '#/$defs/QueueCode', 'x-has-data-type': true }); + const html = render(schema, { $ref: '#/$defs/QueueCode', 'x-mergify-has-data-type': true }); expect(html).toContain(`href="${HREF}"`); expect(html).toContain('Queue dequeue reason'); }); @@ -42,7 +42,11 @@ describe('getValueType with x-has-data-type', () => { it('links a flag on a $defs entry reached through a plain $ref', () => { const schema = { $defs: { - QueueCode: { 'x-has-data-type': true, title: 'Queue dequeue reason', type: 'string' }, + QueueCode: { + 'x-mergify-has-data-type': true, + title: 'Queue dequeue reason', + type: 'string', + }, }, }; const html = render(schema, { $ref: '#/$defs/QueueCode' }); @@ -53,7 +57,7 @@ describe('getValueType with x-has-data-type', () => { const schema = { $defs: { ReportMode: { - 'x-has-data-type': true, + 'x-mergify-has-data-type': true, title: 'Report Modes', enum: ['check', 'comment'], type: 'string', @@ -68,7 +72,7 @@ describe('getValueType with x-has-data-type', () => { }); it('falls back to the previous rendering when a flagged node has no title', () => { - const html = render({}, { 'x-has-data-type': true, enum: ['check', 'comment'] }); + const html = render({}, { 'x-mergify-has-data-type': true, enum: ['check', 'comment'] }); expect(html).not.toContain('href'); expect(html).toContain('check'); }); @@ -137,7 +141,7 @@ describe('getValueTypeText with an untitled $ref target', () => { }); }); -describe('getValueTypeText with x-has-data-type', () => { +describe('getValueTypeText with x-mergify-has-data-type', () => { it('emits a markdown link instead of the enum dump', () => { expect(getValueTypeText({} as never, inlineMarked)).toBe(`[Queue dequeue reason](${HREF})`); }); @@ -146,7 +150,7 @@ describe('getValueTypeText with x-has-data-type', () => { const schema = { $defs: { ReportMode: { - 'x-has-data-type': true, + 'x-mergify-has-data-type': true, title: 'Report Modes', enum: ['check', 'comment'], type: 'string', diff --git a/src/components/Tables/ConfigOptions.tsx b/src/components/Tables/ConfigOptions.tsx index 33c67b0a5a..2378e16612 100644 --- a/src/components/Tables/ConfigOptions.tsx +++ b/src/components/Tables/ConfigOptions.tsx @@ -113,7 +113,7 @@ function getTitle(schema: object, ref: OptionDefinitionRef): string { return item?.title || item?.name || ''; } -// A node flagged `x-has-data-type: true` is a documented data type: link to its +// A node flagged `x-mergify-has-data-type: true` is a documented data type: link to its // data-types section instead of expanding the node's shape — an enum such as // `queue-dequeue-reason` would otherwise dump its forty codes into the type // cell. The node's `title` drives both the label and the anchor (slugified diff --git a/src/util/dataType.test.ts b/src/util/dataType.test.ts index c68bf2172d..8120b11e67 100644 --- a/src/util/dataType.test.ts +++ b/src/util/dataType.test.ts @@ -4,9 +4,18 @@ import { collectDataTypeTitles, getDataTypeHref, isDataType } from './dataType'; import { dataTypesHeadingAnchors, missingDataTypeAnchors } from './dataTypeAnchors'; describe('isDataType', () => { - it('recognizes flagged nodes only', () => { - expect(isDataType({ 'x-has-data-type': true })).toBe(true); - expect(isDataType({ 'x-has-data-type': 'queue-dequeue-reason' })).toBe(false); + it('recognizes the namespaced marker', () => { + expect(isDataType({ 'x-mergify-has-data-type': true })).toBe(true); + expect(isDataType({ 'x-mergify-has-data-type': 'queue-dequeue-reason' })).toBe(false); + }); + + // Reading only the new spelling is what lets the engine drop the old one: + // a consumer that accepted either would leave the removal unprovable. + it('ignores the legacy marker', () => { + expect(isDataType({ 'x-has-data-type': true })).toBe(false); + }); + + it('rejects everything else', () => { expect(isDataType({ type: 'string' })).toBe(false); expect(isDataType(null)).toBe(false); expect(isDataType('string')).toBe(false); @@ -26,14 +35,17 @@ describe('collectDataTypeTitles', () => { it('finds flagged nodes wherever they appear in a schema', () => { const schema = { $defs: { - ReportMode: { 'x-has-data-type': true, title: 'Report Mode' }, + ReportMode: { 'x-mergify-has-data-type': true, title: 'Report Mode' }, }, properties: { reason: { - anyOf: [{ 'x-has-data-type': true, title: 'Queue dequeue reason' }, { type: 'null' }], + anyOf: [ + { 'x-mergify-has-data-type': true, title: 'Queue dequeue reason' }, + { type: 'null' }, + ], }, report_mode: { type: 'array', items: { $ref: '#/$defs/ReportMode' } }, - untitled: { 'x-has-data-type': true }, + untitled: { 'x-mergify-has-data-type': true }, }, }; expect(collectDataTypeTitles(schema).sort()).toEqual([ @@ -52,7 +64,7 @@ describe('data-types page anchors', () => { it('exposes every anchor the site links to', () => { const anchors = dataTypesHeadingAnchors(); for (const expected of [ - // derived from titles the engine marks with x-has-data-type + // derived from titles the engine marks with x-mergify-has-data-type 'queue-dequeue-reason', 'priority', 'report-mode', diff --git a/src/util/dataType.ts b/src/util/dataType.ts index a6a72ff9c5..e46dcd23c9 100644 --- a/src/util/dataType.ts +++ b/src/util/dataType.ts @@ -1,15 +1,20 @@ import { slugify } from './slugify'; // The engine flags a schema node that corresponds to a documented Mergify -// data type with `x-has-data-type: true`. Everything else derives from the node's -// standard `title`: the link label is the title, and the link target is the -// slugified title, which by convention equals the section heading anchor on -// /configuration/data-types. The convention is enforced where drift can +// data type with `x-mergify-has-data-type: true`. Everything else derives from +// the node's standard `title`: the link label is the title, and the link target +// is the slugified title, which by convention equals the section heading anchor +// on /configuration/data-types. The convention is enforced where drift can // actually arrive: the Astro build fails when a marked title has no matching // heading anchor (schema syncs land as direct pushes to main, so the deploy // build is the gate), and dataType.test.ts gives the same signal earlier, in // PR CI. -const DATA_TYPE_KEY = 'x-has-data-type'; +// +// The engine published this as `x-has-data-type` until it was namespaced. It +// emits both spellings today, so reading only the new one is safe — and it is +// what lets the engine drop the old one: while a consumer accepts either, you +// cannot tell which it depends on, so nothing can prove the removal safe. +const DATA_TYPE_KEY = 'x-mergify-has-data-type'; const DATA_TYPES_PAGE = '/configuration/data-types'; @@ -26,11 +31,11 @@ export function getDataTypeHref(title: string): string { } /** - * Walk a JSON schema and return the `title` of every node flagged - * `x-has-data-type: true`, wherever the flag appears (inline property nodes, - * `$ref` siblings, `$defs` entries, array items, `anyOf` branches). A marked - * node without a title yields `undefined` — an invalid marker the anchor - * check reports. + * Walk a JSON schema and return the `title` of every node flagged as a + * documented data type, in either spelling, wherever the flag appears (inline + * property nodes, `$ref` siblings, `$defs` entries, array items, `anyOf` + * branches). A marked node without a title yields `undefined` — an invalid + * marker the anchor check reports. */ export function collectDataTypeTitles( node: unknown, diff --git a/src/util/dataTypeAnchors.ts b/src/util/dataTypeAnchors.ts index 211514efd5..18afe7ae77 100644 --- a/src/util/dataTypeAnchors.ts +++ b/src/util/dataTypeAnchors.ts @@ -1,4 +1,4 @@ -// Node-only helpers for the x-has-data-type title↔anchor convention (they read +// Node-only helpers for the x-mergify-has-data-type title↔anchor convention (they read // the data-types page from disk). Kept separate from dataType.ts so // components can import the marker readers without dragging node:fs into a // client bundle. @@ -52,7 +52,7 @@ export function missingDataTypeAnchors(schema: unknown): string[] { const problems = new Set(); for (const title of collectDataTypeTitles(schema)) { if (title === undefined) { - problems.add('(x-has-data-type node without a title)'); + problems.add('(x-mergify-has-data-type node without a title)'); } else if (!anchors.has(slugify(title))) { problems.add(`"${title}" → #${slugify(title)}`); } diff --git a/src/util/enumChoices.ts b/src/util/enumChoices.ts index cad8333ea7..c1be06421a 100644 --- a/src/util/enumChoices.ts +++ b/src/util/enumChoices.ts @@ -108,7 +108,7 @@ function ownValues(node: SchemaNode): string[] { * Pydantic publishes an annotation inline for an inlined type but as a * *sibling of `$ref`* for a type hoisted into `$defs`, so both the raw node * and the resolved target have to be consulted — the same walk - * `ConfigOptions.getDataTypeLink` performs for `x-has-data-type`. Nearest wins: + * `ConfigOptions.getDataTypeLink` performs for `x-mergify-has-data-type`. Nearest wins: * a sibling on the referring node overrides the shared component. */ function collectMetadata(root: unknown, node: unknown): SchemaNode { diff --git a/src/util/schemaToMarkdown.ts b/src/util/schemaToMarkdown.ts index c42e24a796..7bbbb5c907 100644 --- a/src/util/schemaToMarkdown.ts +++ b/src/util/schemaToMarkdown.ts @@ -17,7 +17,7 @@ function getTitle(schema: Schema, ref: string): string { } // Markdown twin of ConfigOptions.getDataTypeLink: a node flagged -// `x-has-data-type: true` links to its data-types section instead of expanding +// `x-mergify-has-data-type: true` links to its data-types section instead of expanding // its shape, with the node's `title` driving both label and anchor. The flag // may sit inline or beside a `$ref`, so check each node along the chain. function getDataTypeLinkText(schema: Schema, definition: unknown): string | undefined {