From 9155102398c28ce69d75330ed91ab8ee62bc2b09 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 11:12:05 +0000 Subject: [PATCH 1/3] feat(types)!: retire MarkdownSchema.sanitize as an ADR-0049 tombstone `sanitize` was declared `?: boolean` (`@default true`) on both published faces and read by nothing -- and it implied a switch that does not exist. Sanitization is unconditional: `rehypePlugins` in plugin-markdown/src/MarkdownImpl.tsx is a module-level const whose last link is `[rehypeSanitize, sanitizeSchema]`, with no conditional path, and MarkdownRenderer forwards exactly `content` and `className`. The enforce arm would be an XSS-off switch, which is not acceptable, so enforce-or-remove collapses to remove for this key. - `sanitize?: never` on the interface, `retirementTombstone()` named refusal on the Zod mirror, guidance stating that sanitization is unconditional - refusal pin in packages/types (envelope: path, invalid_type, expected never, the guidance text; accepting controls; the fixture parses green) - two-face pin in plugin-markdown's own test: the tombstone is a tsc error through the re-exported authority - the one in-repo fixture that authored the key (packages/types/examples/data-display-examples.json) drops it - docs row replaced by a retirement note; changeset (minor) Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01BAZFhALsQsGqxui8sNqM8s --- .../6972-markdown-inert-keys-retired.md | 69 +++++ content/docs/plugins/plugin-markdown.mdx | 10 +- packages/plugin-markdown/src/index.test.ts | 29 ++ .../types/examples/data-display-examples.json | 3 +- .../markdown-inert-keys-retired-6972.test.ts | 262 ++++++++++++++++++ packages/types/src/data-display.ts | 40 ++- packages/types/src/zod/data-display.zod.ts | 17 +- 7 files changed, 422 insertions(+), 8 deletions(-) create mode 100644 .changeset/6972-markdown-inert-keys-retired.md create mode 100644 packages/types/src/__tests__/markdown-inert-keys-retired-6972.test.ts diff --git a/.changeset/6972-markdown-inert-keys-retired.md b/.changeset/6972-markdown-inert-keys-retired.md new file mode 100644 index 0000000000..3cc45cccf2 --- /dev/null +++ b/.changeset/6972-markdown-inert-keys-retired.md @@ -0,0 +1,69 @@ +--- +'@object-ui/types': minor +'@object-ui/plugin-markdown': minor +--- + +**Breaking for authored metadata:** `MarkdownSchema.sanitize` is RETIRED +(objectui#6972, ADR-0049 enforce-or-remove). A `markdown` node that authors +`sanitize` — either value — no longer validates: the parse fails loudly on the +`sanitize` path with the explanation in the message, and the TS member is a +`?: never` tombstone, so the same document is refused at compile time. + +**What was measured, on this branch's base.** `sanitize` was declared +`?: boolean` with `@default true` on both published faces — `data-display.ts` +and the Zod mirror — documented, and read by NOTHING. Worse than an ordinary +inert key, it implied a switch that does not exist: sanitization is +**unconditional**. `rehypePlugins` in `plugin-markdown/src/MarkdownImpl.tsx` is +a module-level `const` array whose last link is `[rehypeSanitize, sanitizeSchema]`, +handed to `ReactMarkdown` as-is — no ternary, no `if`, no runtime assembly. +`MarkdownRenderer` forwards exactly `content` and `className`, and +`MarkdownImplProps` accepts only those two. A repo-wide grep for +`schema.sanitize` over `packages/` and `apps/` returns nothing, against a +control of 20 `.tsx` files reading `schema.content` in the same query shape, so +the zero is a reading, not a blind query. An author writing `sanitize: false` +believed they turned XSS filtering off; one writing `sanitize: true` believed +they turned it on. Neither was true. + +**Why remove and not enforce.** The enforce arm of enforce-or-remove for this +key is a switch that DISABLES XSS sanitization, which is not an acceptable +outcome; for `sanitize` the ruling collapses to remove (triage on +objectui#6972). + +**Who is affected — a `sanitize` authored onto a `markdown` node:** + +```json +{ "type": "markdown", + "content": "# Hello", + "sanitize": false } // ← was tolerated, changed nothing +``` + +now fails validation with: + +> RETIRED (objectui#6972) — sanitization is unconditional: rehype-sanitize is a +> fixed last link of the markdown renderer's rehype chain, and no value of this +> key ever switched it. There is no authored spelling that disables XSS +> sanitization; delete the key. + +**Two published faces.** `@object-ui/plugin-markdown` re-exports `MarkdownSchema` +from `@object-ui/types` (one authority since objectui#6172) rather than +declaring a copy, so the retirement reaches its consumers through the same +declaration — which is why this changeset names the plugin as well: no plugin +source changes, but the type its published face exposes narrows, and its own +test now pins that the refusal arrives there. + +**Who is NOT affected.** A document that never wrote the key is untouched +(`absent` stays valid), `content` and `className` are unchanged, and the +renderer's behaviour is byte-identical — it sanitized unconditionally before +and does now. One in-repo fixture authored the key +(`packages/types/examples/data-display-examples.json#examples.markdown`, +`"sanitize": true`); the key is deleted from it and the fixture is now pinned +to parse green. + +**Migration:** delete the key. There is nothing to replace it with — the +behaviour it claimed to control is always on. + +Graded `minor`, not `patch`: this narrows the accepted input set, which is +breaking for any author who wrote the tolerated key. It is not `major` per +this repo's fixed-group convention (objectui's own breaking changes ship as +`minor`; the group's major tracks `@objectstack` — AGENTS.md 版本号策略, +mechanically enforced by `scripts/check-changeset-no-major.mjs`). diff --git a/content/docs/plugins/plugin-markdown.mdx b/content/docs/plugins/plugin-markdown.mdx index 4fb75dbc0c..a675505c6d 100644 --- a/content/docs/plugins/plugin-markdown.mdx +++ b/content/docs/plugins/plugin-markdown.mdx @@ -72,9 +72,17 @@ renderer's own props type takes a non-optional `string`, and the Zod mirror |----------|------|---------|-------------| | `content` | string | — (required) | Markdown content to render | | `className` | string | `''` | Additional Tailwind CSS classes | -| `sanitize` | boolean | `true` | Declared on the schema; the renderer sanitizes unconditionally | | `components` | Record | — | Declared on the schema; not read by this renderer | +> **Retired (objectui#6972):** `sanitize` used to be listed here as a boolean +> defaulting to `true`. It was never read: sanitization is **unconditional** — +> `rehype-sanitize` is a fixed last link of the renderer's rehype chain, and no +> value of the key ever switched it, so `sanitize: false` changed nothing while +> reading as a security control. The key is now `never` on the TypeScript face +> and is refused **by name** by the Zod mirror, with that explanation in the +> parse message. There is no authored spelling that disables XSS sanitization; +> delete the key. + ## Supported Markdown Features ### Headers diff --git a/packages/plugin-markdown/src/index.test.ts b/packages/plugin-markdown/src/index.test.ts index 50978070de..5bbcfc55f8 100644 --- a/packages/plugin-markdown/src/index.test.ts +++ b/packages/plugin-markdown/src/index.test.ts @@ -9,6 +9,7 @@ import { describe, it, expect } from 'vitest'; import { ComponentRegistry } from '@object-ui/core'; import type { ComponentInput } from '@object-ui/types'; +import type { MarkdownSchema } from './types'; // Imports all renderers to register them. Module scope, NOT awaited inside a // `beforeAll` — there the cold transform of the renderer graph is billed to the // hook, against `hookTimeout`. That is what made the sibling plugin-kanban test @@ -97,4 +98,32 @@ describe('Plugin Markdown', () => { expect(defaults?.content).toContain('#'); }); }); + + describe('MarkdownSchema retirements reach this package\'s published face (objectui#6972)', () => { + // `./types` re-exports `MarkdownSchema` from `@object-ui/types` — ONE + // authority (objectui#6172), not a local copy — so an ADR-0049 tombstone + // declared there must be a `tsc` error through THIS package's import + // spelling too. That is the "two published faces" half of the retirement, + // pinned where the second face lives. REAL enforcement: `type-check` runs + // `tsc -p tsconfig.test.json`, so an unused directive fails the build — + // if the plugin ever re-declared a local `MarkdownSchema` that carried the + // key, this leg goes red before anything else does. + it('`sanitize` is a `tsc` error through the re-exported authority — sanitization is unconditional', () => { + const node: MarkdownSchema = { + type: 'markdown', + content: '# Hello', + // @ts-expect-error `sanitize` is an ADR-0049 retirement tombstone (objectui#6972): the renderer sanitizes unconditionally, no value switches it + sanitize: false, + }; + expect(node.content).toBe('# Hello'); + }); + + it('the two values the renderer DOES forward stay writable — the non-vacuity control', () => { + // `content` and `className` are exactly what `MarkdownRenderer` hands to + // `MarkdownImpl`; without this leg the directive above could be satisfied + // by a face that lost the whole interface. + const node: MarkdownSchema = { type: 'markdown', content: '# Hello', className: 'prose-lg' }; + expect(node.className).toBe('prose-lg'); + }); + }); }); diff --git a/packages/types/examples/data-display-examples.json b/packages/types/examples/data-display-examples.json index 73e3e63dd1..a5793f2ad6 100644 --- a/packages/types/examples/data-display-examples.json +++ b/packages/types/examples/data-display-examples.json @@ -234,8 +234,7 @@ "markdown": { "type": "markdown", "id": "readme", - "content": "# Welcome\n\nThis is a **markdown** component.\n\n- Feature 1\n- Feature 2\n- Feature 3", - "sanitize": true + "content": "# Welcome\n\nThis is a **markdown** component.\n\n- Feature 1\n- Feature 2\n- Feature 3" }, "html": { "type": "html", diff --git a/packages/types/src/__tests__/markdown-inert-keys-retired-6972.test.ts b/packages/types/src/__tests__/markdown-inert-keys-retired-6972.test.ts new file mode 100644 index 0000000000..9737ab5f83 --- /dev/null +++ b/packages/types/src/__tests__/markdown-inert-keys-retired-6972.test.ts @@ -0,0 +1,262 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * Retirement pin — `MarkdownSchema.sanitize` is REFUSED, not silently ignored + * (objectui#6972, ADR-0049 enforce-or-remove). + * + * ## The failure this pin exists to prevent + * + * `sanitize` was declared `?: boolean` with `@default true` on both published + * faces (`data-display.ts` and the Zod mirror; `@object-ui/plugin-markdown` + * re-exports the same authority since objectui#6172), documented, and read by + * NOTHING. Worse than an ordinary inert key, it implied a switch that does not + * exist: sanitization is UNCONDITIONAL. `rehypePlugins` in + * `plugin-markdown/src/MarkdownImpl.tsx` is a module-level `const` array whose + * last link is `[rehypeSanitize, sanitizeSchema]`, handed to `ReactMarkdown` + * as-is — no ternary, no `if`, no runtime assembly. `MarkdownRenderer` forwards + * exactly `content` and `className`; `MarkdownImplProps` accepts only those + * two. So `sanitize: false` type-checked, parsed green and changed nothing + * while reading as a security-relevant control, and `sanitize: true` promised + * a gate the author never controlled either. Both readings lied. + * + * The enforce arm of enforce-or-remove would be a switch that DISABLES XSS + * sanitization — not an acceptable outcome — so for this key the ruling + * collapses to remove (triage on objectui#6972). The deliverable is therefore + * not "sanitize works"; it is: **an authored `sanitize` is refused loudly at + * the authoring boundary, and the refusal says why** (sanitization is + * unconditional; there is no spelling that disables it). + * + * ## Why the tombstone, and not simply deleting the key + * + * `BaseSchema` is `.passthrough()` on the Zod side and carries a + * `[key: string]: any` index signature on the TS side. An UNDECLARED key is + * accepted by both halves, unvalidated — deleting `sanitize` outright would + * hand the authored spelling exactly the silent no-op this card exists to + * close. `?: never` / `retirementTombstone()` is this package's convention — + * {@link StaticTableColumn} (objectui#5474), `DataTableSchema.toolbar` + * (objectui#6881), `ComponentInput.inputType` (objectui#5905) — and it is + * lockstep: both halves or neither. The "deleted" row is pinned live below as + * a control, so the contrast cannot rot into prose. + * + * The `@ts-expect-error` directives are REAL enforcement: this package + * type-checks its tests through `tsconfig.test.json`, so re-widening the + * declaration fails the build on the unused directive. A green `vitest` run is + * NOT evidence about them — type assertions are erased before it runs. + */ + +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, it, expect } from 'vitest'; +import { MarkdownSchema } from '../zod/data-display.zod.js'; +import type { MarkdownSchema as MarkdownSchemaTS } from '../data-display.js'; + +/** + * The retired keys, each with the values an author would plausibly have + * written, the FULL guidance string (pinned as a literal so the derived + * assertions below cannot all drift together), and the prescriptive half — + * the sentence that tells the author what is true and what to do. + */ +const RETIRED = { + sanitize: { + values: [false, true] as const, + guidance: + 'RETIRED (objectui#6972) — sanitization is unconditional: rehype-sanitize is a fixed last link of the ' + + 'markdown renderer\'s rehype chain, and no value of this key ever switched it. There is no authored ' + + 'spelling that disables XSS sanitization; delete the key.', + prescriptive: 'There is no authored spelling that disables XSS sanitization; delete the key.', + }, +} as const; + +type RetiredKey = keyof typeof RETIRED; + +/** A minimal document that is valid TODAY and stays valid — the inside of the boundary. */ +const VALID_MARKDOWN = { + type: 'markdown', + content: '# Hello\n\nSome **markdown**.', +} as const; + +const shapeOf = (schema: unknown): Record => + (schema as { shape: Record }).shape; + +const describeOf = (schema: unknown, key: string): string | undefined => + (shapeOf(schema)[key] as { description?: string } | undefined)?.description; + +/* ── the Zod half: refused BY NAME, with the guidance in the message ─────── */ + +describe.each(Object.keys(RETIRED) as RetiredKey[])( + 'MarkdownSchema.%s is RETIRED — the Zod half of the tombstone (objectui#6972)', + (key) => { + const { values, guidance, prescriptive } = RETIRED[key]; + + it.each(values.map((v) => [String(v), v] as const))( + 'REFUSES `%s`, naming the retired key in the path — every value, not one spelling', + (_label, value) => { + // The pin. Before the retirement this document parsed GREEN (the key + // was `z.boolean().optional()`), measured ACCEPTED on the retiring PR's + // base. Asserting the ENVELOPE — not merely `success:false` — so the pin + // cannot be satisfied by an unrelated rejection. + const result = MarkdownSchema.safeParse({ ...VALID_MARKDOWN, [key]: value }); + expect( + result.success, + `an authored \`${key}: ${String(value)}\` was ACCEPTED — it parses green and changes nothing`, + ).toBe(false); + if (result.success) return; + + const issue = result.error.issues.find((i) => i.path[0] === key); + expect(issue, `parse failed, but not on the \`${key}\` path`).toBeTruthy(); + // The accept-set contract: same address, same code a bare `z.never()` + // reports — `retirementTombstone()` customises the MESSAGE only. + expect(issue?.code).toBe('invalid_type'); + expect((issue as { expected?: string } | undefined)?.expected).toBe('never'); + expect(issue?.path).toEqual([key]); + }, + ); + + it('the refusal CARRIES the guidance — the prescriptive sentence, not zod\'s generic message', () => { + const result = MarkdownSchema.safeParse({ ...VALID_MARKDOWN, [key]: values[0] }); + expect(result.success).toBe(false); + if (result.success) return; + + const issue = result.error.issues.find((i) => i.path[0] === key); + expect(issue?.message).not.toContain('Invalid input: expected never, received '); + // The half an author acts on: what is TRUE (sanitization is unconditional) + // and what to DO (delete the key). Pinned as text, because the wording is + // the contract here — a message that only said "retired" would leave the + // security-shaped misreading in place. + expect(issue?.message).toContain(prescriptive); + expect(issue?.message).toBe(guidance); + // ONE string, BOTH channels — asserted derived, so the parse message and + // the generated-docs metadata cannot drift apart (objectui#6931). + expect(issue?.message).toBe(describeOf(MarkdownSchema, key)); + }); + + it('keeps the key DECLARED — a tombstone, not a deletion', () => { + // The route guard. `BaseSchema` is `.passthrough()`, so removing the key + // from the mirror would make the authored spelling parse green again and + // do nothing — the silent no-op reintroduced by the very edit meant to + // remove it. + expect( + Object.keys(MarkdownSchema.shape), + `${key} left the mirror — under .passthrough() the retired key becomes a SILENT no-op again`, + ).toContain(key); + expect(describeOf(MarkdownSchema, key)).toContain('RETIRED (objectui#6972)'); + }); + }, +); + +/* ── the inside of the boundary: everything else is untouched ────────────── */ + +describe('the retirement narrows exactly the retired keys and nothing else (objectui#6972)', () => { + it('a document that never wrote the key parses GREEN — `absent` stays valid', () => { + // `.optional()` on the tombstone. The retirement narrows exactly one key. + const result = MarkdownSchema.safeParse(VALID_MARKDOWN); + expect(result.success ? null : result.error.issues).toBe(null); + }); + + it('still ACCEPTS `className` — the live sibling the renderer actually forwards', () => { + // `className` is the OTHER value `MarkdownRenderer` forwards (with + // `content`). Without this leg the refusals above would be satisfied by a + // schema that refuses every optional key — a narrowing that refuses too + // much would pass a refusal-only test. + const result = MarkdownSchema.safeParse({ ...VALID_MARKDOWN, className: 'prose-lg' }); + expect(result.success ? null : result.error.issues).toBe(null); + }); + + it('still REFUSES a wrong `content` — the mirror did not stop validating', () => { + // Counter-probe in the other direction: the schema is not `z.any()` in + // disguise, so the green results above are readings. + const result = MarkdownSchema.safeParse({ type: 'markdown', content: 42 }); + expect(result.success).toBe(false); + if (result.success) return; + expect(result.error.issues.find((i) => i.path[0] === 'content')).toBeTruthy(); + }); + + it('the shipped example document parses GREEN — the fixture that used to author the key', () => { + // `packages/types/examples/data-display-examples.json#examples.markdown` + // authored `"sanitize": true` — the one in-repo write of the key, dropped + // by the retiring PR because the tombstone refuses it. Pinned here so the + // fixture cannot silently regress into a document the published mirror + // rejects: it is documentation consumers copy from. + const ROOT = resolve(__dirname, '../../../..'); + const doc = JSON.parse( + readFileSync(resolve(ROOT, 'packages/types/examples/data-display-examples.json'), 'utf8'), + ) as { examples: { markdown: Record } }; + const markdown = doc.examples.markdown; + expect(markdown.type).toBe('markdown'); + for (const key of Object.keys(RETIRED)) { + expect(markdown, `the fixture still authors the retired \`${key}\``).not.toHaveProperty(key); + } + const result = MarkdownSchema.safeParse(markdown); + expect(result.success ? null : result.error.issues).toBe(null); + }); + + it('an UNDECLARED key still rides `.passthrough()` — the DELETED row, measured live', () => { + // This is the contrast that justifies `?: never` over deletion, pinned + // rather than argued: a key the mirror does not declare is neither refused + // nor stripped, it is KEPT. Had `sanitize` been deleted instead of + // tombstoned, an authored value would sit exactly where this one sits — + // green, forwarded, and read by nothing. + const result = MarkdownSchema.safeParse({ ...VALID_MARKDOWN, notAKeyAtAll: 'anything' }); + expect(result.success).toBe(true); + if (result.success) expect(result.data).toHaveProperty('notAKeyAtAll', 'anything'); + }); +}); + +/* ── the TS half: the `tsc` channel ──────────────────────────────────────── */ + +describe('MarkdownSchema.sanitize is RETIRED — the TS half of the tombstone (objectui#6972)', () => { + it('refuses the retired key at compile time', () => { + // On the pre-fix tree `sanitize` is `boolean | undefined`, so the + // assignment is LEGAL, the directive below is unused, and `tsc` fails the + // build with TS2578 naming the key — this leg is red before the fix in + // `type-check`, not in vitest, which strips types. + + // @ts-expect-error — `sanitize` is RETIRED (objectui#6972): declared `?: never`, so no value is authorable. + const retired: MarkdownSchemaTS['sanitize'] = false; + + // Counter-probe on the same surface: the live sibling still accepts its + // value, so the directive above pins the KEY's retirement and not a + // blanket narrowing of the interface. + const sibling: MarkdownSchemaTS['content'] = '# Hello'; + + expect([retired, sibling]).toHaveLength(2); + }); + + it('refuses the retired key in the form authors actually write', () => { + // The leg that proves the tombstone survives `BaseSchema`'s + // `[key: string]: any`: if the index signature won, `sanitize` would widen + // back to `any` here and the directive would go unused (TS2578). + const retiredDocument: MarkdownSchemaTS = { + type: 'markdown', + content: '# Hello', + // @ts-expect-error — `sanitize` is RETIRED (objectui#6972); sanitization is unconditional, there is no spelling that disables it. + sanitize: false, + }; + + // The migrated document — the key simply deleted — still type-checks. + const migratedDocument: MarkdownSchemaTS = { + type: 'markdown', + content: '# Hello', + className: 'prose-lg', + }; + + expect([retiredDocument, migratedDocument]).toHaveLength(2); + }); + + it('refuses it through a WIDENED value too — the half a deletion would have missed', () => { + // Excess-property checking only reaches a FRESH literal (objectui#7654 + // measured the contrast): a deleted key would ride a widened value + // silently. The declared `never` makes the assignment itself ill-typed, + // so freshness stops mattering. + const raw = { type: 'markdown' as const, content: '# Hello', sanitize: true }; + // @ts-expect-error — `sanitize` is RETIRED (objectui#6972), reached through a non-fresh value. + const document: MarkdownSchemaTS = raw; + expect(document.type).toBe('markdown'); + }); +}); diff --git a/packages/types/src/data-display.ts b/packages/types/src/data-display.ts index 9b356dc979..75aa6ea44b 100644 --- a/packages/types/src/data-display.ts +++ b/packages/types/src/data-display.ts @@ -1161,10 +1161,42 @@ export interface MarkdownSchema extends BaseSchema { */ content: string; /** - * Whether to sanitize HTML - * @default true - */ - sanitize?: boolean; + * ADR-0049 RETIREMENT TOMBSTONE — `sanitize` (objectui#6972). + * + * Declared `?: boolean` with `@default true` and read by NOTHING — and, worse + * than an ordinary inert key, it implied a switch that does not exist. + * Sanitization is UNCONDITIONAL: `rehypePlugins` in + * `packages/plugin-markdown/src/MarkdownImpl.tsx` is a module-level `const` + * array whose last link is `[rehypeSanitize, sanitizeSchema]`, handed to + * `ReactMarkdown` as-is — no ternary, no `if`, no runtime assembly. So + * `sanitize: false` type-checked, passed the Zod mirror and changed nothing + * while reading as a security-relevant control, and `sanitize: true` + * promised a gate the author never controlled either. Both readings lied. + * The enforce arm of enforce-or-remove would be a switch that DISABLES XSS + * sanitization, which is not an acceptable outcome, so for this key the + * ruling collapses to remove (triage on objectui#6972). + * + * Measured on the retiring PR's base: `MarkdownRenderer` + * (`plugin-markdown/src/index.tsx`) forwards exactly `content` and + * `className` to `MarkdownImpl`, whose props type accepts only those two; + * `grep -rn "schema.sanitize"` over `packages/` and `apps/` returns nothing, + * against a control of 20 `.tsx` files reading `schema.content` in the same + * query shape — the zero is a reading, not a blind query. + * + * `?: never` is this package's tombstone convention (see `crud.ts` + * `confirm`, {@link StaticTableColumn}, `DataTableSchema.toolbar` above), + * NOT a deletion: `BaseSchema`'s `[key: string]: any` would admit a deleted + * key as `any` again — the same silence one layer over. The Zod twin refuses + * it loudly via `retirementTombstone()` (`zod/data-display.zod.ts`). Both + * published faces carry the refusal: `@object-ui/plugin-markdown` re-exports + * this one authority (objectui#6172), so its consumers meet the same + * declaration. + * + * RETIRED (objectui#6972, ADR-0049) — sanitization is unconditional; there + * is no authored spelling that disables it. Delete the key. + * @deprecated Not part of `MarkdownSchema`'s contract — the value was inert. + */ + sanitize?: never; /** * Custom components for markdown elements */ diff --git a/packages/types/src/zod/data-display.zod.ts b/packages/types/src/zod/data-display.zod.ts index 011cf68189..ff63c39083 100644 --- a/packages/types/src/zod/data-display.zod.ts +++ b/packages/types/src/zod/data-display.zod.ts @@ -285,11 +285,26 @@ export const DataTableSchema = BaseSchema.extend({ /** * Markdown Schema - Markdown content renderer + * + * `sanitize` is an ADR-0049 tombstone (objectui#6972). It implied a switch + * that does not exist: sanitization is UNCONDITIONAL — the `rehypeSanitize` + * link is a fixed last member of a module-level `const` chain in + * `plugin-markdown/src/MarkdownImpl.tsx`, with no conditional path — so the + * enforce arm (an XSS-off switch) was refused and the key retired. It refuses + * BY NAME through `retirementTombstone()` (objectui#6931), with the remedy in + * the message, rather than parsing green and doing nothing. The TS twin is + * `?: never` in `../data-display.ts`; both published faces carry the refusal + * (`@object-ui/types`, and `@object-ui/plugin-markdown`'s re-export of the + * same authority — objectui#6172). */ export const MarkdownSchema = BaseSchema.extend({ type: z.literal('markdown'), content: z.string().describe('Markdown content'), - sanitize: z.boolean().optional().describe('Sanitize HTML'), + sanitize: retirementTombstone( + 'RETIRED (objectui#6972) — sanitization is unconditional: rehype-sanitize is a fixed last link of the ' + + 'markdown renderer\'s rehype chain, and no value of this key ever switched it. There is no authored ' + + 'spelling that disables XSS sanitization; delete the key.', + ), components: z.record(z.string(), z.any()).optional().describe('Custom component overrides'), }); From 61eccaa2028193027e4c059910a181cffe0c6820 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 11:16:06 +0000 Subject: [PATCH 2/3] feat(types)!: retire MarkdownSchema.components as an ADR-0049 tombstone `components` was declared `?: Record` on both published faces and read by nothing: MarkdownRenderer forwards only `content` and `className`, MarkdownImplProps has no such prop, and the map MarkdownImpl hands to ReactMarkdown is its own module-level `mdComponents`, never merged with anything off the schema. The falsifiable premise -- no host path consumes a `components` map -- was re-measured on the branch base before this half was written; it holds, so there is no runtime-slot twin to keep. Removed under the PM's declared veto window on objectui#6972: a map of React component overrides is not a JSON-authorable value (the objectui#6124 class). - `components?: never` on the interface, `retirementTombstone()` named refusal on the Zod mirror - the refusal pin gains the second key (same envelope, its own guidance) and the plugin-markdown two-face pin gains a `components` leg - docs row folded into the shared retirement note; changeset extended Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01BAZFhALsQsGqxui8sNqM8s --- .../6972-markdown-inert-keys-retired.md | 66 +++++++++-- content/docs/plugins/plugin-markdown.mdx | 17 +-- packages/plugin-markdown/src/index.test.ts | 10 ++ .../markdown-inert-keys-retired-6972.test.ts | 108 +++++++++++++++--- packages/types/src/data-display.ts | 29 ++++- packages/types/src/zod/data-display.zod.ts | 29 +++-- 6 files changed, 210 insertions(+), 49 deletions(-) diff --git a/.changeset/6972-markdown-inert-keys-retired.md b/.changeset/6972-markdown-inert-keys-retired.md index 3cc45cccf2..1f19c51e0c 100644 --- a/.changeset/6972-markdown-inert-keys-retired.md +++ b/.changeset/6972-markdown-inert-keys-retired.md @@ -3,11 +3,15 @@ '@object-ui/plugin-markdown': minor --- -**Breaking for authored metadata:** `MarkdownSchema.sanitize` is RETIRED -(objectui#6972, ADR-0049 enforce-or-remove). A `markdown` node that authors -`sanitize` — either value — no longer validates: the parse fails loudly on the -`sanitize` path with the explanation in the message, and the TS member is a -`?: never` tombstone, so the same document is refused at compile time. +**Breaking for authored metadata:** `MarkdownSchema.sanitize` and +`MarkdownSchema.components` are RETIRED (objectui#6972, ADR-0049 +enforce-or-remove). A `markdown` node that authors either key no longer +validates: the parse fails loudly on that key's path with the explanation in +the message, and both TS members are `?: never` tombstones, so the same document +is refused at compile time. The two keys do not share a disposition — triage +recorded the asymmetry — so each is argued below. + +## `sanitize` **What was measured, on this branch's base.** `sanitize` was declared `?: boolean` with `@default true` on both published faces — `data-display.ts` @@ -44,6 +48,45 @@ now fails validation with: > key ever switched it. There is no authored spelling that disables XSS > sanitization; delete the key. +## `components` + +**What was measured, on this branch's base.** `components` was declared +`?: Record` ("custom components for markdown elements") on both +faces and read by NOTHING: the `components` map `MarkdownImpl` hands to +`ReactMarkdown` is its own module-level `mdComponents` (the mermaid / metadata +fence overrides), never merged with anything off the schema, and +`grep -rn "schema.components"` over `packages/` and `apps/` returns nothing +against the same `schema.content` control. The premise the PM declared +falsifiable — *no host path consumes a `components` map* — was re-measured +before this half was written: `MarkdownImplProps` has no such prop, `LazyMarkdown` +receives only `content` and `className`, and no plugin API, app-shell or runner +site passes one. + +**Why remove and not wire, and why not a runtime slot.** A map of React +component overrides is not a value a JSON document can author — the same shape +as the handler keys objectui#6124 retired ("JSON has no function value"). The +`runtime-slot` disposition keeps a TypeScript twin callable when a host-supplied +value actually reaches a renderer; nothing reaches this one, so there is no twin +to keep and the TS face refuses it outright. This half is the PM's disposition +under a declared veto window on objectui#6972, not a maintainer ruling; the PR +stays draft for contract review. A real override slot must arrive as a proposal +WITH its enforcing reader, not by reviving this key. + +```json +{ "type": "markdown", + "content": "# Hello", + "components": { "h1": "h2" } } // ← was tolerated, changed nothing +``` + +now fails validation with: + +> RETIRED (objectui#6972) — never read: the markdown renderer forwards only +> `content` and `className`, and a map of React component overrides is not a +> JSON-authorable value. Delete the key; the fenced mermaid / metadata block +> overrides are the renderer's own fixed map, not an authoring surface. + +## Both keys + **Two published faces.** `@object-ui/plugin-markdown` re-exports `MarkdownSchema` from `@object-ui/types` (one authority since objectui#6172) rather than declaring a copy, so the retirement reaches its consumers through the same @@ -51,16 +94,19 @@ declaration — which is why this changeset names the plugin as well: no plugin source changes, but the type its published face exposes narrows, and its own test now pins that the refusal arrives there. -**Who is NOT affected.** A document that never wrote the key is untouched +**Who is NOT affected.** A document that never wrote either key is untouched (`absent` stays valid), `content` and `className` are unchanged, and the renderer's behaviour is byte-identical — it sanitized unconditionally before -and does now. One in-repo fixture authored the key +and does now, and its fenced-block overrides are the same fixed map. One +in-repo fixture authored either key (`packages/types/examples/data-display-examples.json#examples.markdown`, `"sanitize": true`); the key is deleted from it and the fixture is now pinned -to parse green. +to parse green. No fixture, catalog entry, doc snippet, skill or app in this +repository authored `components` on a markdown node. -**Migration:** delete the key. There is nothing to replace it with — the -behaviour it claimed to control is always on. +**Migration:** delete the keys. There is nothing to replace either with — the +behaviour `sanitize` claimed to control is always on, and no authored spelling +overrides markdown elements. Graded `minor`, not `patch`: this narrows the accepted input set, which is breaking for any author who wrote the tolerated key. It is not `major` per diff --git a/content/docs/plugins/plugin-markdown.mdx b/content/docs/plugins/plugin-markdown.mdx index a675505c6d..9e7dd0475e 100644 --- a/content/docs/plugins/plugin-markdown.mdx +++ b/content/docs/plugins/plugin-markdown.mdx @@ -72,16 +72,19 @@ renderer's own props type takes a non-optional `string`, and the Zod mirror |----------|------|---------|-------------| | `content` | string | — (required) | Markdown content to render | | `className` | string | `''` | Additional Tailwind CSS classes | -| `components` | Record | — | Declared on the schema; not read by this renderer | -> **Retired (objectui#6972):** `sanitize` used to be listed here as a boolean -> defaulting to `true`. It was never read: sanitization is **unconditional** — +> **Retired (objectui#6972):** `sanitize` and `components` used to be listed +> here. Neither was ever read. `sanitize` (a boolean defaulting to `true`) +> implied a switch that does not exist: sanitization is **unconditional** — > `rehype-sanitize` is a fixed last link of the renderer's rehype chain, and no > value of the key ever switched it, so `sanitize: false` changed nothing while -> reading as a security control. The key is now `never` on the TypeScript face -> and is refused **by name** by the Zod mirror, with that explanation in the -> parse message. There is no authored spelling that disables XSS sanitization; -> delete the key. +> reading as a security control. `components` (a `Record` of React component +> overrides) is not a value a JSON document can author, and the renderer's own +> fenced-block overrides (mermaid / metadata) are a fixed internal map, not an +> authoring surface. Both keys are now `never` on the TypeScript face and are +> refused **by name** by the Zod mirror, with that explanation in the parse +> message. There is no authored spelling that disables XSS sanitization, and no +> authored spelling that overrides markdown elements; delete the keys. ## Supported Markdown Features diff --git a/packages/plugin-markdown/src/index.test.ts b/packages/plugin-markdown/src/index.test.ts index 5bbcfc55f8..b772b7dab4 100644 --- a/packages/plugin-markdown/src/index.test.ts +++ b/packages/plugin-markdown/src/index.test.ts @@ -118,6 +118,16 @@ describe('Plugin Markdown', () => { expect(node.content).toBe('# Hello'); }); + it('`components` is a `tsc` error through the re-exported authority — nothing reads an override map', () => { + const node: MarkdownSchema = { + type: 'markdown', + content: '# Hello', + // @ts-expect-error `components` is an ADR-0049 retirement tombstone (objectui#6972): the renderer forwards only content and className + components: { h1: 'h2' }, + }; + expect(node.content).toBe('# Hello'); + }); + it('the two values the renderer DOES forward stay writable — the non-vacuity control', () => { // `content` and `className` are exactly what `MarkdownRenderer` hands to // `MarkdownImpl`; without this leg the directive above could be satisfied diff --git a/packages/types/src/__tests__/markdown-inert-keys-retired-6972.test.ts b/packages/types/src/__tests__/markdown-inert-keys-retired-6972.test.ts index 9737ab5f83..e66476feb5 100644 --- a/packages/types/src/__tests__/markdown-inert-keys-retired-6972.test.ts +++ b/packages/types/src/__tests__/markdown-inert-keys-retired-6972.test.ts @@ -7,10 +7,13 @@ */ /** - * Retirement pin — `MarkdownSchema.sanitize` is REFUSED, not silently ignored - * (objectui#6972, ADR-0049 enforce-or-remove). + * Retirement pin — `MarkdownSchema.sanitize` and `MarkdownSchema.components` + * are REFUSED, not silently ignored (objectui#6972, ADR-0049 + * enforce-or-remove). The two keys do NOT share a disposition — triage recorded + * the asymmetry — so each is argued on its own below, then pinned by one + * mechanism. * - * ## The failure this pin exists to prevent + * ## The failure this pin exists to prevent — `sanitize` * * `sanitize` was declared `?: boolean` with `@default true` on both published * faces (`data-display.ts` and the Zod mirror; `@object-ui/plugin-markdown` @@ -32,13 +35,29 @@ * the authoring boundary, and the refusal says why** (sanitization is * unconditional; there is no spelling that disables it). * - * ## Why the tombstone, and not simply deleting the key + * ## The failure this pin exists to prevent — `components` + * + * `components` was declared `?: Record` ("custom components for + * markdown elements") and read by NOTHING: the `components` map `MarkdownImpl` + * hands to `ReactMarkdown` is its OWN module-level `mdComponents` (the mermaid + * / metadata fence overrides), never merged with anything off the schema, and + * no host path — no `MarkdownImpl` prop, no plugin API, no app-shell or runner + * site — consumes such a map. Triage left both arms open here; the PM's + * disposition (declared veto window on objectui#6972) is remove: a map of + * React component overrides is not a JSON-authorable value, the same shape as + * the handler keys objectui#6124 retired, and with no host consumer there is + * no `runtime-slot` twin to keep callable. The falsifiable premise — "no host + * path consumes a `components` map" — was re-measured on the retiring PR's + * base before this half was written. + * + * ## Why the tombstone, and not simply deleting the keys * * `BaseSchema` is `.passthrough()` on the Zod side and carries a * `[key: string]: any` index signature on the TS side. An UNDECLARED key is * accepted by both halves, unvalidated — deleting `sanitize` outright would * hand the authored spelling exactly the silent no-op this card exists to - * close. `?: never` / `retirementTombstone()` is this package's convention — + * close (and would hand `components` the same). `?: never` / + * `retirementTombstone()` is this package's convention — * {@link StaticTableColumn} (objectui#5474), `DataTableSchema.toolbar` * (objectui#6881), `ComponentInput.inputType` (objectui#5905) — and it is * lockstep: both halves or neither. The "deleted" row is pinned live below as @@ -71,6 +90,16 @@ const RETIRED = { + 'spelling that disables XSS sanitization; delete the key.', prescriptive: 'There is no authored spelling that disables XSS sanitization; delete the key.', }, + components: { + // The shapes an author reaching for react-markdown's `components` prop + // would write: a tag remap, and an empty map. + values: [{ h1: 'h2' }, {}] as const, + guidance: + 'RETIRED (objectui#6972) — never read: the markdown renderer forwards only `content` and `className`, ' + + 'and a map of React component overrides is not a JSON-authorable value. Delete the key; the fenced ' + + 'mermaid / metadata block overrides are the renderer\'s own fixed map, not an authoring surface.', + prescriptive: 'Delete the key; the fenced mermaid / metadata block overrides are the renderer\'s own fixed map, not an authoring surface.', + }, } as const; type RetiredKey = keyof typeof RETIRED; @@ -94,17 +123,19 @@ describe.each(Object.keys(RETIRED) as RetiredKey[])( (key) => { const { values, guidance, prescriptive } = RETIRED[key]; - it.each(values.map((v) => [String(v), v] as const))( + it.each(values.map((v) => [JSON.stringify(v), v] as const))( 'REFUSES `%s`, naming the retired key in the path — every value, not one spelling', (_label, value) => { - // The pin. Before the retirement this document parsed GREEN (the key - // was `z.boolean().optional()`), measured ACCEPTED on the retiring PR's - // base. Asserting the ENVELOPE — not merely `success:false` — so the pin - // cannot be satisfied by an unrelated rejection. + // The pin. Before the retirement this document parsed GREEN (`sanitize` + // was `z.boolean().optional()`, `components` was + // `z.record(z.string(), z.any()).optional()`), measured ACCEPTED on the + // retiring PR's base. Asserting the ENVELOPE — not merely + // `success:false` — so the pin cannot be satisfied by an unrelated + // rejection. const result = MarkdownSchema.safeParse({ ...VALID_MARKDOWN, [key]: value }); expect( result.success, - `an authored \`${key}: ${String(value)}\` was ACCEPTED — it parses green and changes nothing`, + `an authored \`${key}: ${JSON.stringify(value)}\` was ACCEPTED — it parses green and changes nothing`, ).toBe(false); if (result.success) return; @@ -125,10 +156,11 @@ describe.each(Object.keys(RETIRED) as RetiredKey[])( const issue = result.error.issues.find((i) => i.path[0] === key); expect(issue?.message).not.toContain('Invalid input: expected never, received '); - // The half an author acts on: what is TRUE (sanitization is unconditional) - // and what to DO (delete the key). Pinned as text, because the wording is - // the contract here — a message that only said "retired" would leave the - // security-shaped misreading in place. + // The half an author acts on: what is TRUE (sanitization is unconditional; + // nothing reads an override map) and what to DO (delete the key). Pinned + // as text, because the wording is the contract here — a message that only + // said "retired" would leave the security-shaped misreading of `sanitize` + // in place. expect(issue?.message).toContain(prescriptive); expect(issue?.message).toBe(guidance); // ONE string, BOTH channels — asserted derived, so the parse message and @@ -153,8 +185,8 @@ describe.each(Object.keys(RETIRED) as RetiredKey[])( /* ── the inside of the boundary: everything else is untouched ────────────── */ describe('the retirement narrows exactly the retired keys and nothing else (objectui#6972)', () => { - it('a document that never wrote the key parses GREEN — `absent` stays valid', () => { - // `.optional()` on the tombstone. The retirement narrows exactly one key. + it('a document that never wrote either key parses GREEN — `absent` stays valid', () => { + // `.optional()` on both tombstones. The retirement narrows exactly two keys. const result = MarkdownSchema.safeParse(VALID_MARKDOWN); expect(result.success ? null : result.error.issues).toBe(null); }); @@ -179,7 +211,7 @@ describe('the retirement narrows exactly the retired keys and nothing else (obje it('the shipped example document parses GREEN — the fixture that used to author the key', () => { // `packages/types/examples/data-display-examples.json#examples.markdown` - // authored `"sanitize": true` — the one in-repo write of the key, dropped + // authored `"sanitize": true` — the one in-repo write of either key, dropped // by the retiring PR because the tombstone refuses it. Pinned here so the // fixture cannot silently regress into a document the published mirror // rejects: it is documentation consumers copy from. @@ -199,7 +231,7 @@ describe('the retirement narrows exactly the retired keys and nothing else (obje it('an UNDECLARED key still rides `.passthrough()` — the DELETED row, measured live', () => { // This is the contrast that justifies `?: never` over deletion, pinned // rather than argued: a key the mirror does not declare is neither refused - // nor stripped, it is KEPT. Had `sanitize` been deleted instead of + // nor stripped, it is KEPT. Had either key been deleted instead of // tombstoned, an authored value would sit exactly where this one sits — // green, forwarded, and read by nothing. const result = MarkdownSchema.safeParse({ ...VALID_MARKDOWN, notAKeyAtAll: 'anything' }); @@ -260,3 +292,41 @@ describe('MarkdownSchema.sanitize is RETIRED — the TS half of the tombstone (o expect(document.type).toBe('markdown'); }); }); + +describe('MarkdownSchema.components is RETIRED — the TS half of the tombstone (objectui#6972)', () => { + it('refuses the retired key at compile time', () => { + // On the pre-fix tree `components` is `Record | undefined`, + // so this assignment is LEGAL and the directive is unused (TS2578) — + // red before the fix in `type-check`. + + // @ts-expect-error — `components` is RETIRED (objectui#6972): declared `?: never`, so no value is authorable. + const retired: MarkdownSchemaTS['components'] = { h1: 'h2' }; + + const sibling: MarkdownSchemaTS['className'] = 'prose-lg'; + expect([retired, sibling]).toHaveLength(2); + }); + + it('refuses the retired key in the form authors actually write', () => { + // Survives `BaseSchema`'s `[key: string]: any`: if the index signature + // won, `components` would widen back to `any` here and the directive + // would go unused (TS2578). + const retiredDocument: MarkdownSchemaTS = { + type: 'markdown', + content: '# Hello', + // @ts-expect-error — `components` is RETIRED (objectui#6972); the renderer forwards only `content` and `className`. + components: { h1: 'h2' }, + }; + expect(retiredDocument.type).toBe('markdown'); + }); + + it('refuses it through a WIDENED value too — and there is NO runtime-slot twin to keep callable', () => { + // The `runtime-slot` arm (`handlerKeyRefusal`, objectui#6124) keeps a TS + // twin callable when a host-supplied value actually REACHES a renderer. + // Nothing reaches this one — `MarkdownImplProps` has no such prop — so the + // TS face refuses it outright, on the widened path as well. + const raw = { type: 'markdown' as const, content: '# Hello', components: { h1: 'h2' } }; + // @ts-expect-error — `components` is RETIRED (objectui#6972), reached through a non-fresh value. + const document: MarkdownSchemaTS = raw; + expect(document.type).toBe('markdown'); + }); +}); diff --git a/packages/types/src/data-display.ts b/packages/types/src/data-display.ts index 75aa6ea44b..e3ef905ac4 100644 --- a/packages/types/src/data-display.ts +++ b/packages/types/src/data-display.ts @@ -1198,9 +1198,34 @@ export interface MarkdownSchema extends BaseSchema { */ sanitize?: never; /** - * Custom components for markdown elements + * ADR-0049 RETIREMENT TOMBSTONE — `components` (objectui#6972). + * + * Declared `?: Record` ("custom components for markdown + * elements") and read by NOTHING: `MarkdownRenderer` forwards only `content` + * and `className`, `MarkdownImplProps` accepts only those two, and the + * `components` map `MarkdownImpl` hands to `ReactMarkdown` is its OWN + * module-level `mdComponents` (the mermaid / metadata fence overrides), + * never merged with anything off the schema. `grep -rn "schema.components"` + * over `packages/` and `apps/` returns nothing, against the same + * `schema.content` control as `sanitize` above. + * + * Removed rather than wired, under the PM's declared veto window on + * objectui#6972: a map of React component overrides is not a + * JSON-authorable value — the same shape as the handler keys objectui#6124 + * retired ("JSON has no function value"). It is NOT a `runtime-slot` + * either: no host path (no `MarkdownImpl` prop, no plugin API, no app-shell + * or runner site) consumes such a map, so there is no TypeScript twin to + * keep callable for hosts. A real override slot must arrive as a proposal + * WITH its enforcing reader, not by reviving this key. + * + * Same convention as `sanitize` above: `?: never` here, + * `retirementTombstone()` on the Zod twin, both published faces. + * + * RETIRED (objectui#6972, ADR-0049) — never read by the markdown renderer. + * Delete the key. + * @deprecated Not part of `MarkdownSchema`'s contract — the value was inert. */ - components?: Record; + components?: never; } /** diff --git a/packages/types/src/zod/data-display.zod.ts b/packages/types/src/zod/data-display.zod.ts index ff63c39083..5657e60c82 100644 --- a/packages/types/src/zod/data-display.zod.ts +++ b/packages/types/src/zod/data-display.zod.ts @@ -286,16 +286,19 @@ export const DataTableSchema = BaseSchema.extend({ /** * Markdown Schema - Markdown content renderer * - * `sanitize` is an ADR-0049 tombstone (objectui#6972). It implied a switch - * that does not exist: sanitization is UNCONDITIONAL — the `rehypeSanitize` - * link is a fixed last member of a module-level `const` chain in - * `plugin-markdown/src/MarkdownImpl.tsx`, with no conditional path — so the - * enforce arm (an XSS-off switch) was refused and the key retired. It refuses - * BY NAME through `retirementTombstone()` (objectui#6931), with the remedy in - * the message, rather than parsing green and doing nothing. The TS twin is - * `?: never` in `../data-display.ts`; both published faces carry the refusal - * (`@object-ui/types`, and `@object-ui/plugin-markdown`'s re-export of the - * same authority — objectui#6172). + * `sanitize` / `components` are ADR-0049 tombstones (objectui#6972). + * `sanitize` implied a switch that does not exist: sanitization is + * UNCONDITIONAL — the `rehypeSanitize` link is a fixed last member of a + * module-level `const` chain in `plugin-markdown/src/MarkdownImpl.tsx`, with + * no conditional path — so the enforce arm (an XSS-off switch) was refused + * and the key retired. `components` was a `Record` of React + * overrides nothing read — not a JSON-authorable value, and no host path + * consumes such a map either, so there is no runtime slot to keep. Both + * refuse BY NAME through `retirementTombstone()` (objectui#6931), with the + * remedy in the message, rather than parsing green and doing nothing. The TS + * twins are `?: never` in `../data-display.ts`; both published faces carry the + * refusal (`@object-ui/types`, and `@object-ui/plugin-markdown`'s re-export of + * the same authority — objectui#6172). */ export const MarkdownSchema = BaseSchema.extend({ type: z.literal('markdown'), @@ -305,7 +308,11 @@ export const MarkdownSchema = BaseSchema.extend({ + 'markdown renderer\'s rehype chain, and no value of this key ever switched it. There is no authored ' + 'spelling that disables XSS sanitization; delete the key.', ), - components: z.record(z.string(), z.any()).optional().describe('Custom component overrides'), + components: retirementTombstone( + 'RETIRED (objectui#6972) — never read: the markdown renderer forwards only `content` and `className`, ' + + 'and a map of React component overrides is not a JSON-authorable value. Delete the key; the fenced ' + + 'mermaid / metadata block overrides are the renderer\'s own fixed map, not an authoring surface.', + ), }); /** From 5fe2a1f8d7370ecdf070514429bf92560d1f8dcb Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 11:40:45 +0000 Subject: [PATCH 3/3] test(scripts): re-declare the README sample fence line moved by the markdown docs edit `check-doc-snippet-types.test.ts` pins the plugin-markdown README sample by its fence LINE on purpose (an edit above it forces a re-declaration rather than a row that silently covers nothing). The objectui#6972 retirement blockquote replaced two prop-table rows above that fence with 13 lines, so the fence moved 195 -> 206. Re-declared; assertion shape unchanged. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01BAZFhALsQsGqxui8sNqM8s --- scripts/__tests__/check-doc-snippet-types.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/__tests__/check-doc-snippet-types.test.ts b/scripts/__tests__/check-doc-snippet-types.test.ts index 0c89fb6340..519be59c7f 100644 --- a/scripts/__tests__/check-doc-snippet-types.test.ts +++ b/scripts/__tests__/check-doc-snippet-types.test.ts @@ -97,7 +97,9 @@ const FENCE = '```'; * `npm install project-name`. */ const README_SAMPLE_DOC = 'content/docs/plugins/plugin-markdown.mdx'; -const README_SAMPLE_FENCE_LINE = 195; +// 195 until objectui#6972 replaced two prop-table rows above it with a +// retirement blockquote (+11 lines); re-declared here, as this pin intends. +const README_SAMPLE_FENCE_LINE = 206; /** * The regex reader objectui#7555 removed from both gates, kept HERE and only