') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); finding(components): a deprecated type's migration guidance is now stated twice — the console notice literal and `deprecated.replacement` — with nothing asserting they agree · Issue #6823 · objectstack-ai/objectui · GitHub
Skip to content

finding(components): a deprecated type's migration guidance is now stated twice — the console notice literal and deprecated.replacement — with nothing asserting they agree #6823

Description

@claude

Found while implementing #6674 (the machine-readable deprecation declaration). Filed unassigned, recording only — not graded, no type. domain:* and grading are triage's to produce.

Measured on objectui branch claude/issue-6674-registry-deprecation-declaration @ eb852a052.

The fact

After #6674, the migration guidance for a deprecated component type is stated in two places that must agree and that nothing holds together:

  1. the human-facing console notice — DIV_DEPRECATION_NOTICE / SPAN_DEPRECATION_NOTICE, string literals in packages/components/src/renderers/basic/{div,span}.tsx, and
  2. the machine-readable deprecated.replacement on the same file's registration, which finding(registry): component deprecation is not declared anywhere machine-readable — the only statements of it are a console.warn literal and a human label, so no gate can ask "is this type deprecated?" #6674 added so a gate can tell an author what to write instead.

Both say the same thing today, in different words, because the second was transcribed from the first by hand. Nothing asserts they still agree, so a reword of either leaves the other stale — and the stale one is the copy an automated gate reads and repeats to authors.

This is the shape #4580 ruled about for a type and #6067 / #5671 / #5893 executed three times since: a structural copy would reproduce the defect the moment either side moved. The unit here is a sentence rather than a type, but the failure mode is identical, and unlike those it fails silently — there is no tsc to notice.

Why #6674 did not fix it

Deliberately out of scope there, and the reason is a real cost, not a shrug: the notice text is pinned byte-for-byte by four existing tests —

  • packages/components/src/__tests__/div-deprecation-provenance.test.tsx (asserts "card", "flex", or semantic layout components and "container", "stack", or "grid" verbatim)
  • packages/components/src/__tests__/div-deprecation-warn-once.test.tsx
  • packages/components/src/__tests__/span-deprecation-provenance.test.tsx
  • packages/components/src/__tests__/span-deprecation-warn-once.test.tsx

Those pins are correct and deliberate (#4000 records that the guidance is "byte-for-byte what it was"). Converging the two statements means moving them, which is its own judged change and does not belong on a card whose scope was the declaration.

Suggested disposition (not a decision)

Two shapes, both bounded, listed cheapest first:

  • Assert the agreement. One case per renderer: the notice contains the tokens deprecated.replacement names. Keeps both texts, catches the drift, costs no churn to the existing pins.
  • Derive the notice's guidance line from the declaration. One statement, the direction the rulings above all took. Costs a rewrite of the four pins, and the notice's two-bullet layout would have to survive the rewrite or be deliberately dropped.

⚠️ Not proposed here: touching the scope sentence in either notice. surfaces and the isHtmlTierNode exemption are already pinned to each other by #6674, so that half is covered.

Related


Generated by Claude Code

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions