feat(types)!: retire MarkdownSchema.sanitize and .components as ADR-0049 tombstones (objectui#6972) - #7718
Conversation
`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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BAZFhALsQsGqxui8sNqM8s
`components` was declared `?: Record<string, any>` 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BAZFhALsQsGqxui8sNqM8s
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
|
Generated by Claude Code |
…arkdown 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BAZFhALsQsGqxui8sNqM8s
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Fixes #6972
Dev of
session_01BAZFhALsQsGqxui8sNqM8s's dispatch (PM loop round R1). Carriesneeds:contract-review— two declared keys become refusals on two published faces — and stays draft for in-seat contract review. Placeholders in this body are spelled as UPPERCASE WORDS rather than angle-bracket shapes, because the GitHub body sanitizer eats tag-shaped fragments.What
Both inert
MarkdownSchemakeys become ADR-0049 retirement tombstones on both published faces:?: neveron the TypeScript interface (packages/types/src/data-display.ts) and aretirementTombstone()named refusal on the Zod mirror (packages/types/src/zod/data-display.zod.ts) — the route PR #7639 (ComponentInput.inputType) and PR #7669 (FloatingChatbotConfig.triggerIcon) established. The member is not deleted:BaseSchemais.passthrough()on the Zod side and carries a string index signature on the TS side, so a deleted key would parse green and compile asanyagain — the silent no-op this card exists to end. The "deleted" row is pinned live as a control. ThehandlerKeyRefusalregion oftombstone.zod.tsis untouched; no new helper was needed.Per-key disposition (Clause ②)
sanitize?: boolean,@default true; mirrorz.boolean().optional()?: never; refused by name; guidance states sanitization is unconditional and says delete the keycomponents?: Record of STRING to ANY; mirrorz.record(...).optional()?: never; refused by name; guidance states nothing reads it and a React override map is not JSON-authorableGuidance strings as shipped (parse message and
.describe()are the same string):Measurements on the branch base
4ce14f1grep -rn "schema.sanitize\|schema.components" packages/ apps/ --include=*.ts --include=*.tsxreturns nothing (exit 1). Control in the same query shape:schema.contentis read in 20.tsxfiles (28 across ts+tsx). The zero is a reading.rehypePluginsinpackages/plugin-markdown/src/MarkdownImpl.tsxis a module-levelconstarray (lines 99–107) whose last link is[rehypeSanitize, sanitizeSchema], handed straight toReactMarkdownat line 226. No ternary, noif, no runtime assembly. That sentence is in the guidance string.componentspremise (no host path consumes acomponentsmap):MarkdownImplProps(lines 27–37) accepts onlycontentandclassName;MarkdownImpldestructures exactly those two (line 196); thecomponents={mdComponents}it passes (line 227) is the plugin's own module-level map (line 173 — the mermaid / metadata fence overrides), never merged with anything off the schema;LazyMarkdowninindex.tsxreceives onlycontentandclassName; a repo-widegit grepforMarkdownImpl/LazyMarkdownfinds no other host site (thefieldspackage'sLazyMarkdownContentis a different component with its ownvalueprop). No plugin API, app-shell or runner site passes such a map.runtime-slotdoes not apply: nothing reaches a renderer, so there is no TS twin to keep callable. Premise holds; both halves executed.packages/plugin-markdown/src/types.tsisexport type { MarkdownSchema } from '@object-ui/types'(one authority, objectui#6172) — no plugin source edit needed; its published face narrows through the same declaration. The plugin's ownindex.test.tsgains a two-face pin: both keys are atscerror through the re-exported authority.MarkdownSchemahas zero other consumers outsidepackages/typesandpackages/plugin-markdown(git grepover packages/ apps/ examples/, CHANGELOGs excluded).packages/types/examples/data-display-examples.json("sanitize": trueonexamples.markdown) — the key is deleted there and the fixture is now pinned to parse green. Repo-widegit grep -w sanitizeand"sanitize": every other hit isExpressionEvaluator's own option,plugin-form'ssanitizeFormData, prose, or the historical objectui#6172 changeset. No catalog entry, doc snippet, skill or app authoredcomponentson a markdown node.content/docs/plugins/plugin-markdown.mdx— not undercontent/docs/components/**as the dispatch assumed. The two rows are removed and replaced with aRetired (objectui#6972)blockquote in the formcontent/docs/core/report-schema.mdxuses. Thecheck:doc-*gates cannot see table rows (they readtypeliterals and ts fences;component-docs-retired-handler-keys-7340.test.tsrecords the same hole), so the docs disposition is carried by prose, not by a gate.zod-mirror-parity.test.tsneeds no row (same as PR feat(types)!: retire ComponentInput.inputType as an ADR-0049 tombstone (objectui#5905, ruled option B) #7639); it passes unchanged.@objectstack/specdeclares noMarkdownSchemaand no markdownsanitize;check:spec-symbolsis green.Pins
packages/types/src/__tests__/markdown-inert-keys-retired-6972.test.ts— 19 tests. Per key: refusal of every plausible value with the envelope asserted (path names the key,code: 'invalid_type',expected: 'never'), the message equals the guidance literal and contains the prescriptive sentence, message equals.describe()(one string, two channels), and the key stays in.shape. Boundary controls: absent parses green,classNameaccepted, a wrongcontentis still refused (the mirror did not stop validating), the shipped fixture parses green, an undeclared key still rides.passthrough()(the DELETED row, live). TS half per key: fresh literal, document form, widened value — all@ts-expect-error, enforced bytsc -p tsconfig.test.json.packages/plugin-markdown/src/index.test.ts— 3 new tests: each key is atscerror through the plugin's re-exported face, plus a non-vacuity control thatcontent/classNamestay writable.Reverse verification (ablation), fix committed first
Both declaration files reverted to
4ce14f1(git checkout BASE -- PATH PATH), on-disk markers counted (sanitize?: never0,sanitize?: boolean1,components?: never0, zod tombstones 0, zod boolean/record 1 each),@object-ui/typesrebuilt, dist markers counted (dist/data-display.d.ts: never 0, boolean 1;dist/zod/data-display.zod.d.ts: ZodNever 0, ZodBoolean 2). Direction: RED, as expected — 8 of 19 pins failed (the 4 refusal legs, 2 guidance legs, 2 keeps-declared legs),type-checkof@object-ui/typesexit 2 with 6 TS2578 (unused directives),type-checkof@object-ui/plugin-markdownexit 2 with 2 TS2578 through the re-exported face. Restore:git checkout HEAD -- PATH PATH,git diff HEAD --statempty,git hash-objectof both files equal to the HEAD blobs (e3ef905ac426…,5657e60c825d…), rebuilt, dist markers back (never 1, boolean 0; ZodNever 2, ZodBoolean 0), pins green 19/19, plugin-markdown type-check exit 0. Trap-guarded script (absolute paths); no dist marker left behind.Gates — exit codes captured before any pipe; all on HEAD
61eccaa; the patch-round re-runs are on5fe2a1f(see Deviations)pnpm exec vitest run --maxWorkers=2 packages/types/ scripts/__tests__/one-authority-per-exported-name-6273.test.ts(verify lock)Test Files 113 passed (113)·Tests 1955 passed (1955)pnpm exec vitest run --maxWorkers=2 packages/plugin-markdown/(verify lock)Test Files 5 passed (5)·Tests 49 passed (49)pnpm --filter @object-ui/types buildthentype-check(tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json, script name echoed)pnpm --filter @object-ui/plugin-markdown type-check(tsc --noEmit && tsc -p tsconfig.test.json, against the rebuilt types dist)node scripts/check-changeset-presence.mjs4 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)node scripts/check-changeset-fixed.mjsAll workspace packages are in the changeset fixed group.node scripts/check-changeset-no-major.mjsNo changeset declares a major bump.node scripts/check-changeset-overwrite.mjsNo pre-existing changeset was modified or deleted.pnpm check:control-bytesOK (scanned 6293 tracked text file(s))pnpm check:spec-symbolsno comment cites a key its spec symbol does not declarepnpm check:doc-typesEvery documented component type is registered.pnpm check:doc-fencesevery TypeScript block in 227 document(s) is fenced ts/tsx/typescriptnode scripts/check-doc-links.mjsLinks are valid across 17 scan roots.pnpm check:doc-snippetsPRECONDITION NOT MET: the gate's own text says "I could not run, NOT I ran and found errors"; it needs 20+ plugin packages built. Declared to CI. The only doc edit here is a table row removal and a blockquote — no fence changed.node scripts/check-governed-queue-guard.mjs --test PATHSNOT GOVERNED — 7 path(s) checked against 5 governed surface(s); none matched.pnpm exec vitest run --maxWorkers=2 scripts/__tests__/check-doc-snippet-types.test.ts(patch round, on5fe2a1f)Test Files 1 passed (1)·Tests 70 passed (70)Vitest spelling note: the dispatch's
pnpm --filter @object-ui/types exec vitest runform puts vitest's root in the package directory, which this repo's invocation guard refuses (AGENTS.md, objectui#3378); the root-relative path form above is the sanctioned equivalent and runs the same config CI runs.origin/mainwas merged before opening: still4ce14f1, "Already up to date".Affected-package census and the declared narrowing
TURBO_SCM_BASE=4ce14f1 pnpm exec turbo ls --affectedlists 43 packages — everything downstream of@object-ui/types. Run locally:packages/typesin full,packages/plugin-markdownin full, both type-checks, and the one-authority pin underscripts/__tests__. The remaining 41 are declared to CI with this narrowing argument: the diff changes two members of one interface/mirror pair; a downstream package can only be affected by namingMarkdownSchema, or by authoringsanitize/componentson a markdown node, and all three censuses above are zero outside the two packages run here.Changeset
.changeset/6972-markdown-inert-keys-retired.md—minorfor@object-ui/typesand@object-ui/plugin-markdown(the plugin's published face narrows through the re-export, no plugin source change; named so its CHANGELOG carries the note, as objectui#6172's did when the face widened). Notmajor, per the fixed-group conventioncheck-changeset-no-majorenforces.skip-changesetis a phantom label in this repo and is not used.Deviations from the claim's file surface
packages/plugin-markdown/src/index.test.tsgained the two-face pin (the claim listed the plugin as expected read-only source; the pin is the only honest place to assert the second face, and PR feat(types)!: retire ComponentInput.inputType as an ADR-0049 tombstone (objectui#5905, ruled option B) #7639 set the precedent of pinning there).packages/types/examples/data-display-examples.jsonlost its"sanitize": true(the one fixture that authored the key; under the tombstone it would parse red).5fe2a1f):scripts/__tests__/check-doc-snippet-types.test.tsanchors the plugin-markdown README sample fence by LINE on purpose (an edit above it forces a re-declaration rather than a row that silently covers nothing), and the retirement blockquote moved that fence 195 → 206, which turned CITest (shard 1/4)red on61eccaa. The constant is re-declared; assertion shape unchanged; the blockquote is not shrunk or moved. Anchoring that pin by content is its own card, not a rider here. Re-run on5fe2a1f: the pin file exit 0 (70 passed, table above),pnpm check:control-bytesexit 0,pnpm check:doc-snippetsexit 2 (precondition, NOT MEASURED — unchanged).Out of scope
Nothing unrelated was found; no issues filed.
🤖 Generated with Claude Code
https://claude.ai/code/session_01BAZFhALsQsGqxui8sNqM8s