spec: retire ChartConfigSchema.aria — no renderer ever applied it, and its two alias spellings become refusals - #18300
Conversation
Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
…D3 chain + two retired-key entries) Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
…mily Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
…eletes Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
…ent prose Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 4bac193b4280556ee84172d567dff8025a615769 && git checkout 4bac193b4280556ee84172d567dff8025a615769
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 827cacbf4b79088926f98bba25cccfb0f369ebbc 16a8de837aec5791ba8976e80272f066bd818118 && git checkout -B drift-repro 827cacbf4b79088926f98bba25cccfb0f369ebbc && git merge --no-ff 16a8de837aec5791ba8976e80272f066bd818118
node scripts/docs-audit/affected-docs.mjs --json 827cacbf4b79088926f98bba25cccfb0f369ebbc
|
… decision Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Reviewed against merge-base Fence checks first. ① Derived judgments
② Semver level
③ Boundary flags
Two evidence-quality findings of my own, neither verdict-affecting:
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #17751
Clause-②: yes
Retires
ChartConfigSchema.ariaunder ADR-0049 enforce-or-remove, executing the maintainer's decision batch #118 item 2 (2026-09-12) — 「其他同意」 to the director seat's recommendation C, judge the protocol wrong for this one key. The standing principle the card cites, verbatim and untranslated:The premise, re-measured first-hand before writing anything
Every row below was taken in this worktree, not read off the card.
grep -nonpackages/spec/src/ui/chart.zod.ts:630 aria: AriaPropsSchema.optional()— controlshowLegend2 hits in the same filegit show 53ded82bf7a4:packages/core/src/utils/chart-presentation.tsariaappears only in prose, incl. its own docblock calling it "the one declared key with no reader at all" — controlshowLegend3 hits in the same filegit grep '\.aria\b' HEADover the chart pathsplugin-view/src/ObjectView.tsxstill has 1 real.ariaread, the VIEW'sgrep -n 'retiredKey('onapp.zod.ts/dashboard.zod.tsapp.zod.ts:1571,dashboard.zod.ts:829and:1190— andsrc/ui/aria-carrier-tombstones.test.tsalready pins the classliveness/dashboard.json,chartConfigPresentationdocblockdescriptionis lowered onto the chart graphic asrole="img"+aria-label, pinned in objectui's DOM testsScope: why this registers TWO retired keys, and why that is not a widening
The card is titled for
ChartConfigSchema.ariaand that is exactly what this retires — one property, on one schema.ui/ReportChart:ariaappears beside it as arithmetic, not as a second decision:ReportChartSchemaisChartConfigSchema.extend({ xAxis, yAxis }), so the extension copies the retired property into its own walked shape andauthorable-surface/marks the copy[RETIRED]separately. There is no second declaration to leave alone — the extension has noariaof its own to keep — so the two keys are not separable.The build says so before any reader does. Tombstoning the one property failed
build-schemas.tscheck (b) on the first run, naming both keys unprompted, and stayed red until both were registered:This is the
shared/FieldMapping:transformshape, where one tombstone produced three keys;RETIRED_KEYS_BY_MAJOR's own docblock states the rule — registered per key, as the gate reads them; nothing radiates from the base. The fact is written intosrc/ui/aria-carrier-tombstones.test.tsbeside the test that exercises it, so a later reader meets it where the behaviour is.Two things the card did not spell out, and what was decided
1. The replacement vocabulary is NOT what the card's shorthand says, and the tombstone must not repeat it. The card describes the live channel as "
BaseSchema.ariaLabel/ariaDescribedBy/role". Measured:BaseSchemadoes not exist inpackages/spec— it is objectui's own node interface inpackages/types, andSchemaRenderer.resolveAriaProps(objectuipackages/react/src/SchemaRenderer.tsx:122) is what reads it. A metadata author cannot writeBaseSchema.ariaLabelanywhere in this protocol, so shipping that sentence in a tombstone would have been plausible and false. What the prescription names instead is measured: the same three words live in the sharedAriaPropsblock, still carried bypage.aria,page.components[].ariaand the list viewaria; and on a chart the accessible name that is actually applied is the siblingdescription.2. The two alias spellings.
chart.zod.tsmappedaccessibility: 'aria'andariaProps: 'aria'. Both moved fromaliasestoguidance, carrying the retirement instead of a rename. This was not a judgement call in the end — the alias map's own rules decide it, and the decision was ablated in both directions:src/shared/alias-integrity.test.tsred, naming both by name:`accessibility` -> `aria` — `aria` is a tombstone; it accepts nothingand the same forariaProps. An alias runs before the edit-distance fallback and bypasses the tombstone filter, so it would send an author to the one key the shape is now guaranteed to reject.accessibilitygets a bareUnrecognized key(s) on this chart config…with no retirement text in it — becauseknownKeysexcludes tombstones from the distance candidates, so nothing points anywhere.The retirement kit
Route:
retiredKey()tombstone, not a bare deletion — even thoughChartConfigSchemais astrictObject. A bare delete stays loud but only as a generic unrecognized-key report that cannot carry the prescription; that is the exact distinctionaria-carrier-tombstones.test.tsalready asserts by name for the widget twin. Consequences, all of them followed:ReportChartSchemais aChartConfigSchema.extend(...), so the retired property copies into its own walked shape. Registered per key, nothing radiating from the base:ui/ChartConfig:ariaandui/ReportChart:aria, each its own file undersrc/migrations/entries/retired-keys/, under major 18 (launch-window convention — the tombstone ships on the 17.x line, the prescription lives at the major boundary wheremigrate metausers look).chart-config-aria-removed(toMajor: 18,retiredFromLoadPath: true) strips all three authored coordinates —dashboards[].widgets[].chartConfig,reports[].chart,reports[].blocks[].chart— with a fixture asserting 3 notices. A separate entry rather than more keys ondashboard-widget-action-aria-removed, for the reason that entry itself gives.MIGRATIONS_BY_MAJOR[18].conversionIdsand the step'srationaleis extended.dashboard/widgets.chartConfig.ariarow stays (the tombstone keeps the key in the walked shape — therls.priorityprecedent), regraded with aREMOVEDnote and a freshverifiedAt. Itsundrilled-containers.baseline.jsonrow is deleted, which the gate itself asked for: "the container now drills (or its property is gone) — delete it."authorable-surface/ui.jsonnow carries both rows as[RETIRED]; the generatedchart/report/dashboardreferences regenerate with the prescription in place of the old nested-shape table.*.form.ts, so there is no false-compliant UI half to remove.pnpm check:i18nconfirms: 9 packages, all bundles in sync.minor, with the FROM → TO table, the one-line fix and the ADR-0087 disposition. ⛔ Notmajor:scripts/check-changeset-no-major.mjsrefuses it during the launch window and breaking ships asminoruntil GA.The ablation — the pin proven red before, green after
Both legs mutated on disk, proved on disk, and restored by blob identity under a
trap.Leg 1 — the tombstone. Mutation put
aria: AriaPropsSchema.optional()back:Leg 2 — the alias decision. Mutation restored the two alias entries and deleted the two guidance entries:
Restore proven both times by blob identity, not by an exit code:
RESTORED_BLOB=cbd0147784ead1076f2148671af25b9809172e34equalsHEAD:packages/spec/src/ui/chart.zod.ts, withgit diff HEADempty andgit status --porcelainclean. Green after: 7/7 in the family pin, 84/84 acrosschart.test.ts+aria-carrier-tombstones.test.ts.One fixture was inverted, not respelled
src/ui/chart.test.tshadChart ARIA Integration > should accept chart with ARIA attributes, which pinned exactly the acceptance this change removes. There is no spelling ofariathis shape accepts any more, so the block is inverted in place — it now asserts the refusal and carries the prescription, with a positive control assertingdescriptionstill parses so the negative cannot pass on a shape that lost its accessibility story altogether.Verification
All exit codes captured by redirect-then-
$?, never through a pipe.pnpm --filter @objectstack/spec build(underscripts/pm/os-verify-lock.sh)VERDICT command-exit 0pnpm --filter @objectstack/spec typecheck && … testTest Files 481 passed (481),Tests 13698 passed (13698)pnpm --filter @objectstack/spec check:generated--fix, reviewed)check:generatednames as not runpnpm check:i18npnpm lint(repo-wideeslint . --no-inline-config)node scripts/pm/dispatch-gates.mjs --ranThe one NOT-MEASURED is
pnpm check:dual-build-cjs-loads, recorded exit 3 (PREREQUISITE NOT MET — it reads built output for studio, the connectors and several plugins, none of which this diff touches). That is a whole-monorepo build, which is CI's run; it is declared here rather than counted as green.Acceptance notes
content/docs/ui/dashboards.mdx, on a fixture string literal (revenue_by_region) and not on the retired key. Re-measured independently onorigin/main827cacbf4b(not on the drift run's merge tree): word-boundaryariain that page is 0 hits —grep -nE '(^|[^a-zA-Z])aria([^a-zA-Z]|$)', exit 1 — against a lit control ofchartConfigat 1 hit, which is the single table row| chartConfig | object | optional | Advanced chart configuration |. That row does not enumerate the keys, so retiringariafalsifies nothing on it. The boundary form matters: a naivegrep -i ariareturns two false positives in this same file,colorVariantandvariable.18.ui__ChartConfig__aria.ts— and warned that its own checkout carried uncommitted changes, so pages documenting the retirement are NOT MEASURED by it, never cleared by it.chartConfig.ariaat.objectui-sha53ded82bf7a4; the two places that do are objectui's own negative-pin test files, andConsole Pin Gatebuilds the Console SPA rather than running objectui's tests. The consumer half — objectui's contract twins gaining the tombstone pin — is objectui#4044's, which ispm:blockedon this card by design.majorchangeset andClause-②: no. Both are stale against what the tree enforces —scripts/check-changeset-no-major.mjsrefusesmajorduring the launch window, and clause ② covers accept-set movement in both directions (packages/client/CHANGELOG.mdrecords an "Accept-set narrowing on a published SDK (clause-②)"). This PR follows the dispatch order and the gates:minor,Clause-②: yes. No card edited — the delivering seat does not rewrite the card it was dispatched from..claude/skills/spec-property-retirement/SKILL.md§4 still instructs@objectstack/specretirements to take amajorchangeset, whichcheck:changeset-no-majorrefuses during the launch window. Governed surface, and not this card's to edit. Successor: the next retirement card through that playbook..strict()⇒ delete the key + guidance map", while everyaria-family precedent on this tree is aretiredKey()tombstone inside astrictObject— whichshared/strict-object.tsdocuments as "deliberate and strictly stronger than aguidanceentry". The table is a default, not a rule, but it points a reader the wrong way on exactly this class. Same successor as above.Delivered by the
domain:specdev seat with Claude Code, sessionsession_01KB5PFtxuy1x3dcR5gxudx6; head16a8de837a.Generated by Claude Code