EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT's button row cites action-button.tsx, but the map is keyed on the bare name — action:button resolves to the empty set #13672

Description

@os-sam

Found while implementing the objectui half of the expression-bindable text keys (objectui#4795, consuming this module; PR objectstack-ai/objectui#6981). A row-vs-key-spelling mismatch, filed rather than papered over renderer-side.

The mismatch

src/ui/expression-bindable-text-keys.zod.ts documents the button row's evidence as two files (verbatim from the docblock table):

| button | schema.label (form/button.tsx, action/action-button.tsx) |

But the map is keyed on the bare registry name:

EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT={statistic: [...],card: [...],button: ['label']}

and expressionBindableTextKeysFor does an own-property lookup on the type string it is handed. In objectui those two files register under different type strings:

fileregistrationexpressionBindableTextKeysFor(type)
packages/components/src/renderers/form/button.tsxregister('button', …, { namespace: 'ui' })ui:buttonand the bare button fallbackbutton['label']
packages/components/src/renderers/action/action-button.tsxregister('button', …, { namespace: 'action', skipFallback: true })action:button onlyaction:button[]

Measured against the installed 17.2.0:

fn('button') -> [ 'label' ]
fn('action:button') -> []
fn('ui:statistic') -> []

ActionButtonRenderer reads schema.label at the node top level (two sites), exactly like the ui:button renderer — so the row's own stated evidence covers it, but the lookup does not reach it.

Why the consumer will not paper over it

The objectui memo passes the authored type string verbatim, deliberately. Prefix-stripping (action:buttonbutton) is the obvious patch and is wrong: it would in the same motion grant rows to element:button and page:card, whose renderers read config out of the bag via readProps() and never touch these keys on the node — manufacturing the evaluated-but-not-read-back half objectui#4795 exists to close. And the module's docblock forbids exactly that shape of fix ("never inferred from what a renderer happens to read").

So the correct place to resolve it is here.

Measured impact today

Small but non-zero: the objectui corpus census (736 JSON documents plus json doc fences, 2747 typed nodes) counts 5action:button nodes, against 158 bare button, 135 card, 42 statistic, and zeroui:-prefixed spellings. None of the 5 currently carries a ${…} in label, so nothing is visibly broken — this is a latent gap, not a live defect.

Options

  1. Add an action:button row (['label']). Faithful to the evidence the docblock already cites, keeps the map's "key is the type string" semantics exact and mechanically answerable.
  2. Declare the map's keys to be bare registry names and give the lookup a documented normalization rule with an explicit deny-list for the namespaces that read from the bag (element:, page:). More machinery, and the deny-list is the inference the module is trying to avoid.
  3. Narrow the docblock to cite only form/button.tsx, recording that action:button is deliberately out.

Recommendation: (1) — one line, no new concepts, and it makes the declaration say what its own evidence table already claims. (3) is acceptable if action:button's label is considered action-declared rather than author-written, but then the docblock should say so rather than citing the file as evidence for a row that does not cover it.

Provenance

Measured on objectui origin/main7a5da1414 with @objectstack/spec@17.2.0 installed. Registration sites and read points are reproducible from that tree.

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
     blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
    }
    } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
    })();
    (function(){
    try {
    var __m = "github.com";
    var __re = new RegExp('^' + "github\\.com" + '
    
    Skip to content

    EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT's button row cites action-button.tsx, but the map is keyed on the bare name — action:button resolves to the empty set #13672

    Description

    @os-sam

    Found while implementing the objectui half of the expression-bindable text keys (objectui#4795, consuming this module; PR objectstack-ai/objectui#6981). A row-vs-key-spelling mismatch, filed rather than papered over renderer-side.

    The mismatch

    src/ui/expression-bindable-text-keys.zod.ts documents the button row's evidence as two files (verbatim from the docblock table):

    | button | schema.label (form/button.tsx, action/action-button.tsx) |

    But the map is keyed on the bare registry name:

    EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT={statistic: [...],card: [...],button: ['label']}

    and expressionBindableTextKeysFor does an own-property lookup on the type string it is handed. In objectui those two files register under different type strings:

    fileregistrationexpressionBindableTextKeysFor(type)
    packages/components/src/renderers/form/button.tsxregister('button', …, { namespace: 'ui' })ui:buttonand the bare button fallbackbutton['label']
    packages/components/src/renderers/action/action-button.tsxregister('button', …, { namespace: 'action', skipFallback: true })action:button onlyaction:button[]

    Measured against the installed 17.2.0:

    fn('button') -> [ 'label' ]
    fn('action:button') -> []
    fn('ui:statistic') -> []
    

    ActionButtonRenderer reads schema.label at the node top level (two sites), exactly like the ui:button renderer — so the row's own stated evidence covers it, but the lookup does not reach it.

    Why the consumer will not paper over it

    The objectui memo passes the authored type string verbatim, deliberately. Prefix-stripping (action:buttonbutton) is the obvious patch and is wrong: it would in the same motion grant rows to element:button and page:card, whose renderers read config out of the bag via readProps() and never touch these keys on the node — manufacturing the evaluated-but-not-read-back half objectui#4795 exists to close. And the module's docblock forbids exactly that shape of fix ("never inferred from what a renderer happens to read").

    So the correct place to resolve it is here.

    Measured impact today

    Small but non-zero: the objectui corpus census (736 JSON documents plus json doc fences, 2747 typed nodes) counts 5action:button nodes, against 158 bare button, 135 card, 42 statistic, and zeroui:-prefixed spellings. None of the 5 currently carries a ${…} in label, so nothing is visibly broken — this is a latent gap, not a live defect.

    Options

    1. Add an action:button row (['label']). Faithful to the evidence the docblock already cites, keeps the map's "key is the type string" semantics exact and mechanically answerable.
    2. Declare the map's keys to be bare registry names and give the lookup a documented normalization rule with an explicit deny-list for the namespaces that read from the bag (element:, page:). More machinery, and the deny-list is the inference the module is trying to avoid.
    3. Narrow the docblock to cite only form/button.tsx, recording that action:button is deliberately out.

    Recommendation: (1) — one line, no new concepts, and it makes the declaration say what its own evidence table already claims. (3) is acceptable if action:button's label is considered action-declared rather than author-written, but then the docblock should say so rather than citing the file as evidence for a row that does not cover it.

    Provenance

    Measured on objectui origin/main7a5da1414 with @objectstack/spec@17.2.0 installed. Registration sites and read points are reproducible from that tree.

    Metadata

    Metadata

    Assignees

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
      Skip to content

      EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT's button row cites action-button.tsx, but the map is keyed on the bare name — action:button resolves to the empty set #13672

      Description

      @os-sam

      Found while implementing the objectui half of the expression-bindable text keys (objectui#4795, consuming this module; PR objectstack-ai/objectui#6981). A row-vs-key-spelling mismatch, filed rather than papered over renderer-side.

      The mismatch

      src/ui/expression-bindable-text-keys.zod.ts documents the button row's evidence as two files (verbatim from the docblock table):

      | button | schema.label (form/button.tsx, action/action-button.tsx) |

      But the map is keyed on the bare registry name:

      EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT={statistic: [...],card: [...],button: ['label']}

      and expressionBindableTextKeysFor does an own-property lookup on the type string it is handed. In objectui those two files register under different type strings:

      fileregistrationexpressionBindableTextKeysFor(type)
      packages/components/src/renderers/form/button.tsxregister('button', …, { namespace: 'ui' })ui:buttonand the bare button fallbackbutton['label']
      packages/components/src/renderers/action/action-button.tsxregister('button', …, { namespace: 'action', skipFallback: true })action:button onlyaction:button[]

      Measured against the installed 17.2.0:

      fn('button') -> [ 'label' ]
      fn('action:button') -> []
      fn('ui:statistic') -> []
      

      ActionButtonRenderer reads schema.label at the node top level (two sites), exactly like the ui:button renderer — so the row's own stated evidence covers it, but the lookup does not reach it.

      Why the consumer will not paper over it

      The objectui memo passes the authored type string verbatim, deliberately. Prefix-stripping (action:buttonbutton) is the obvious patch and is wrong: it would in the same motion grant rows to element:button and page:card, whose renderers read config out of the bag via readProps() and never touch these keys on the node — manufacturing the evaluated-but-not-read-back half objectui#4795 exists to close. And the module's docblock forbids exactly that shape of fix ("never inferred from what a renderer happens to read").

      So the correct place to resolve it is here.

      Measured impact today

      Small but non-zero: the objectui corpus census (736 JSON documents plus json doc fences, 2747 typed nodes) counts 5action:button nodes, against 158 bare button, 135 card, 42 statistic, and zeroui:-prefixed spellings. None of the 5 currently carries a ${…} in label, so nothing is visibly broken — this is a latent gap, not a live defect.

      Options

      1. Add an action:button row (['label']). Faithful to the evidence the docblock already cites, keeps the map's "key is the type string" semantics exact and mechanically answerable.
      2. Declare the map's keys to be bare registry names and give the lookup a documented normalization rule with an explicit deny-list for the namespaces that read from the bag (element:, page:). More machinery, and the deny-list is the inference the module is trying to avoid.
      3. Narrow the docblock to cite only form/button.tsx, recording that action:button is deliberately out.

      Recommendation: (1) — one line, no new concepts, and it makes the declaration say what its own evidence table already claims. (3) is acceptable if action:button's label is considered action-declared rather than author-written, but then the docblock should say so rather than citing the file as evidence for a row that does not cover it.

      Provenance

      Measured on objectui origin/main7a5da1414 with @objectstack/spec@17.2.0 installed. Registration sites and read points are reproducible from that tree.

      Metadata

      Metadata

      Assignees

      Projects

      No projects

        Milestone

        No milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions

        , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
        Skip to content

        EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT's button row cites action-button.tsx, but the map is keyed on the bare name — action:button resolves to the empty set #13672

        Description

        @os-sam

        Found while implementing the objectui half of the expression-bindable text keys (objectui#4795, consuming this module; PR objectstack-ai/objectui#6981). A row-vs-key-spelling mismatch, filed rather than papered over renderer-side.

        The mismatch

        src/ui/expression-bindable-text-keys.zod.ts documents the button row's evidence as two files (verbatim from the docblock table):

        | button | schema.label (form/button.tsx, action/action-button.tsx) |

        But the map is keyed on the bare registry name:

        EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT={statistic: [...],card: [...],button: ['label']}

        and expressionBindableTextKeysFor does an own-property lookup on the type string it is handed. In objectui those two files register under different type strings:

        fileregistrationexpressionBindableTextKeysFor(type)
        packages/components/src/renderers/form/button.tsxregister('button', …, { namespace: 'ui' })ui:buttonand the bare button fallbackbutton['label']
        packages/components/src/renderers/action/action-button.tsxregister('button', …, { namespace: 'action', skipFallback: true })action:button onlyaction:button[]

        Measured against the installed 17.2.0:

        fn('button') -> [ 'label' ]
        fn('action:button') -> []
        fn('ui:statistic') -> []
        

        ActionButtonRenderer reads schema.label at the node top level (two sites), exactly like the ui:button renderer — so the row's own stated evidence covers it, but the lookup does not reach it.

        Why the consumer will not paper over it

        The objectui memo passes the authored type string verbatim, deliberately. Prefix-stripping (action:buttonbutton) is the obvious patch and is wrong: it would in the same motion grant rows to element:button and page:card, whose renderers read config out of the bag via readProps() and never touch these keys on the node — manufacturing the evaluated-but-not-read-back half objectui#4795 exists to close. And the module's docblock forbids exactly that shape of fix ("never inferred from what a renderer happens to read").

        So the correct place to resolve it is here.

        Measured impact today

        Small but non-zero: the objectui corpus census (736 JSON documents plus json doc fences, 2747 typed nodes) counts 5action:button nodes, against 158 bare button, 135 card, 42 statistic, and zeroui:-prefixed spellings. None of the 5 currently carries a ${…} in label, so nothing is visibly broken — this is a latent gap, not a live defect.

        Options

        1. Add an action:button row (['label']). Faithful to the evidence the docblock already cites, keeps the map's "key is the type string" semantics exact and mechanically answerable.
        2. Declare the map's keys to be bare registry names and give the lookup a documented normalization rule with an explicit deny-list for the namespaces that read from the bag (element:, page:). More machinery, and the deny-list is the inference the module is trying to avoid.
        3. Narrow the docblock to cite only form/button.tsx, recording that action:button is deliberately out.

        Recommendation: (1) — one line, no new concepts, and it makes the declaration say what its own evidence table already claims. (3) is acceptable if action:button's label is considered action-declared rather than author-written, but then the docblock should say so rather than citing the file as evidence for a row that does not cover it.

        Provenance

        Measured on objectui origin/main7a5da1414 with @objectstack/spec@17.2.0 installed. Registration sites and read points are reproducible from that tree.

        Metadata

        Metadata

        Assignees

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
          Skip to content

          EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT's button row cites action-button.tsx, but the map is keyed on the bare name — action:button resolves to the empty set #13672

          Description

          @os-sam

          Found while implementing the objectui half of the expression-bindable text keys (objectui#4795, consuming this module; PR objectstack-ai/objectui#6981). A row-vs-key-spelling mismatch, filed rather than papered over renderer-side.

          The mismatch

          src/ui/expression-bindable-text-keys.zod.ts documents the button row's evidence as two files (verbatim from the docblock table):

          | button | schema.label (form/button.tsx, action/action-button.tsx) |

          But the map is keyed on the bare registry name:

          EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT={statistic: [...],card: [...],button: ['label']}

          and expressionBindableTextKeysFor does an own-property lookup on the type string it is handed. In objectui those two files register under different type strings:

          fileregistrationexpressionBindableTextKeysFor(type)
          packages/components/src/renderers/form/button.tsxregister('button', …, { namespace: 'ui' })ui:buttonand the bare button fallbackbutton['label']
          packages/components/src/renderers/action/action-button.tsxregister('button', …, { namespace: 'action', skipFallback: true })action:button onlyaction:button[]

          Measured against the installed 17.2.0:

          fn('button') -> [ 'label' ]
          fn('action:button') -> []
          fn('ui:statistic') -> []
          

          ActionButtonRenderer reads schema.label at the node top level (two sites), exactly like the ui:button renderer — so the row's own stated evidence covers it, but the lookup does not reach it.

          Why the consumer will not paper over it

          The objectui memo passes the authored type string verbatim, deliberately. Prefix-stripping (action:buttonbutton) is the obvious patch and is wrong: it would in the same motion grant rows to element:button and page:card, whose renderers read config out of the bag via readProps() and never touch these keys on the node — manufacturing the evaluated-but-not-read-back half objectui#4795 exists to close. And the module's docblock forbids exactly that shape of fix ("never inferred from what a renderer happens to read").

          So the correct place to resolve it is here.

          Measured impact today

          Small but non-zero: the objectui corpus census (736 JSON documents plus json doc fences, 2747 typed nodes) counts 5action:button nodes, against 158 bare button, 135 card, 42 statistic, and zeroui:-prefixed spellings. None of the 5 currently carries a ${…} in label, so nothing is visibly broken — this is a latent gap, not a live defect.

          Options

          1. Add an action:button row (['label']). Faithful to the evidence the docblock already cites, keeps the map's "key is the type string" semantics exact and mechanically answerable.
          2. Declare the map's keys to be bare registry names and give the lookup a documented normalization rule with an explicit deny-list for the namespaces that read from the bag (element:, page:). More machinery, and the deny-list is the inference the module is trying to avoid.
          3. Narrow the docblock to cite only form/button.tsx, recording that action:button is deliberately out.

          Recommendation: (1) — one line, no new concepts, and it makes the declaration say what its own evidence table already claims. (3) is acceptable if action:button's label is considered action-declared rather than author-written, but then the docblock should say so rather than citing the file as evidence for a row that does not cover it.

          Provenance

          Measured on objectui origin/main7a5da1414 with @objectstack/spec@17.2.0 installed. Registration sites and read points are reproducible from that tree.

          Metadata

          Metadata

          Assignees

          Projects

          No projects

            Milestone

            No milestone

            Relationships

            None yet

            Development

            No branches or pull requests

            Issue actions

            , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
            Skip to content

            EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT's button row cites action-button.tsx, but the map is keyed on the bare name — action:button resolves to the empty set #13672

            Description

            @os-sam

            Found while implementing the objectui half of the expression-bindable text keys (objectui#4795, consuming this module; PR objectstack-ai/objectui#6981). A row-vs-key-spelling mismatch, filed rather than papered over renderer-side.

            The mismatch

            src/ui/expression-bindable-text-keys.zod.ts documents the button row's evidence as two files (verbatim from the docblock table):

            | button | schema.label (form/button.tsx, action/action-button.tsx) |

            But the map is keyed on the bare registry name:

            EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT={statistic: [...],card: [...],button: ['label']}

            and expressionBindableTextKeysFor does an own-property lookup on the type string it is handed. In objectui those two files register under different type strings:

            fileregistrationexpressionBindableTextKeysFor(type)
            packages/components/src/renderers/form/button.tsxregister('button', …, { namespace: 'ui' })ui:buttonand the bare button fallbackbutton['label']
            packages/components/src/renderers/action/action-button.tsxregister('button', …, { namespace: 'action', skipFallback: true })action:button onlyaction:button[]

            Measured against the installed 17.2.0:

            fn('button') -> [ 'label' ]
            fn('action:button') -> []
            fn('ui:statistic') -> []
            

            ActionButtonRenderer reads schema.label at the node top level (two sites), exactly like the ui:button renderer — so the row's own stated evidence covers it, but the lookup does not reach it.

            Why the consumer will not paper over it

            The objectui memo passes the authored type string verbatim, deliberately. Prefix-stripping (action:buttonbutton) is the obvious patch and is wrong: it would in the same motion grant rows to element:button and page:card, whose renderers read config out of the bag via readProps() and never touch these keys on the node — manufacturing the evaluated-but-not-read-back half objectui#4795 exists to close. And the module's docblock forbids exactly that shape of fix ("never inferred from what a renderer happens to read").

            So the correct place to resolve it is here.

            Measured impact today

            Small but non-zero: the objectui corpus census (736 JSON documents plus json doc fences, 2747 typed nodes) counts 5action:button nodes, against 158 bare button, 135 card, 42 statistic, and zeroui:-prefixed spellings. None of the 5 currently carries a ${…} in label, so nothing is visibly broken — this is a latent gap, not a live defect.

            Options

            1. Add an action:button row (['label']). Faithful to the evidence the docblock already cites, keeps the map's "key is the type string" semantics exact and mechanically answerable.
            2. Declare the map's keys to be bare registry names and give the lookup a documented normalization rule with an explicit deny-list for the namespaces that read from the bag (element:, page:). More machinery, and the deny-list is the inference the module is trying to avoid.
            3. Narrow the docblock to cite only form/button.tsx, recording that action:button is deliberately out.

            Recommendation: (1) — one line, no new concepts, and it makes the declaration say what its own evidence table already claims. (3) is acceptable if action:button's label is considered action-declared rather than author-written, but then the docblock should say so rather than citing the file as evidence for a row that does not cover it.

            Provenance

            Measured on objectui origin/main7a5da1414 with @objectstack/spec@17.2.0 installed. Registration sites and read points are reproducible from that tree.

            Metadata

            Metadata

            Assignees

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT's button row cites action-button.tsx, but the map is keyed on the bare name — action:button resolves to the empty set #13672

              Description

              @os-sam

              Found while implementing the objectui half of the expression-bindable text keys (objectui#4795, consuming this module; PR objectstack-ai/objectui#6981). A row-vs-key-spelling mismatch, filed rather than papered over renderer-side.

              The mismatch

              src/ui/expression-bindable-text-keys.zod.ts documents the button row's evidence as two files (verbatim from the docblock table):

              | button | schema.label (form/button.tsx, action/action-button.tsx) |

              But the map is keyed on the bare registry name:

              EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT={statistic: [...],card: [...],button: ['label']}

              and expressionBindableTextKeysFor does an own-property lookup on the type string it is handed. In objectui those two files register under different type strings:

              fileregistrationexpressionBindableTextKeysFor(type)
              packages/components/src/renderers/form/button.tsxregister('button', …, { namespace: 'ui' })ui:buttonand the bare button fallbackbutton['label']
              packages/components/src/renderers/action/action-button.tsxregister('button', …, { namespace: 'action', skipFallback: true })action:button onlyaction:button[]

              Measured against the installed 17.2.0:

              fn('button') -> [ 'label' ]
              fn('action:button') -> []
              fn('ui:statistic') -> []
              

              ActionButtonRenderer reads schema.label at the node top level (two sites), exactly like the ui:button renderer — so the row's own stated evidence covers it, but the lookup does not reach it.

              Why the consumer will not paper over it

              The objectui memo passes the authored type string verbatim, deliberately. Prefix-stripping (action:buttonbutton) is the obvious patch and is wrong: it would in the same motion grant rows to element:button and page:card, whose renderers read config out of the bag via readProps() and never touch these keys on the node — manufacturing the evaluated-but-not-read-back half objectui#4795 exists to close. And the module's docblock forbids exactly that shape of fix ("never inferred from what a renderer happens to read").

              So the correct place to resolve it is here.

              Measured impact today

              Small but non-zero: the objectui corpus census (736 JSON documents plus json doc fences, 2747 typed nodes) counts 5action:button nodes, against 158 bare button, 135 card, 42 statistic, and zeroui:-prefixed spellings. None of the 5 currently carries a ${…} in label, so nothing is visibly broken — this is a latent gap, not a live defect.

              Options

              1. Add an action:button row (['label']). Faithful to the evidence the docblock already cites, keeps the map's "key is the type string" semantics exact and mechanically answerable.
              2. Declare the map's keys to be bare registry names and give the lookup a documented normalization rule with an explicit deny-list for the namespaces that read from the bag (element:, page:). More machinery, and the deny-list is the inference the module is trying to avoid.
              3. Narrow the docblock to cite only form/button.tsx, recording that action:button is deliberately out.

              Recommendation: (1) — one line, no new concepts, and it makes the declaration say what its own evidence table already claims. (3) is acceptable if action:button's label is considered action-declared rather than author-written, but then the docblock should say so rather than citing the file as evidence for a row that does not cover it.

              Provenance

              Measured on objectui origin/main7a5da1414 with @objectstack/spec@17.2.0 installed. Registration sites and read points are reproducible from that tree.

              Metadata

              Metadata

              Assignees

              Projects

              No projects

                Milestone

                No milestone

                Relationships

                None yet

                Development

                No branches or pull requests

                Issue actions

                , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                Skip to content

                EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT's button row cites action-button.tsx, but the map is keyed on the bare name — action:button resolves to the empty set #13672

                Description

                @os-sam

                Found while implementing the objectui half of the expression-bindable text keys (objectui#4795, consuming this module; PR objectstack-ai/objectui#6981). A row-vs-key-spelling mismatch, filed rather than papered over renderer-side.

                The mismatch

                src/ui/expression-bindable-text-keys.zod.ts documents the button row's evidence as two files (verbatim from the docblock table):

                | button | schema.label (form/button.tsx, action/action-button.tsx) |

                But the map is keyed on the bare registry name:

                EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT={statistic: [...],card: [...],button: ['label']}

                and expressionBindableTextKeysFor does an own-property lookup on the type string it is handed. In objectui those two files register under different type strings:

                fileregistrationexpressionBindableTextKeysFor(type)
                packages/components/src/renderers/form/button.tsxregister('button', …, { namespace: 'ui' })ui:buttonand the bare button fallbackbutton['label']
                packages/components/src/renderers/action/action-button.tsxregister('button', …, { namespace: 'action', skipFallback: true })action:button onlyaction:button[]

                Measured against the installed 17.2.0:

                fn('button') -> [ 'label' ]
                fn('action:button') -> []
                fn('ui:statistic') -> []
                

                ActionButtonRenderer reads schema.label at the node top level (two sites), exactly like the ui:button renderer — so the row's own stated evidence covers it, but the lookup does not reach it.

                Why the consumer will not paper over it

                The objectui memo passes the authored type string verbatim, deliberately. Prefix-stripping (action:buttonbutton) is the obvious patch and is wrong: it would in the same motion grant rows to element:button and page:card, whose renderers read config out of the bag via readProps() and never touch these keys on the node — manufacturing the evaluated-but-not-read-back half objectui#4795 exists to close. And the module's docblock forbids exactly that shape of fix ("never inferred from what a renderer happens to read").

                So the correct place to resolve it is here.

                Measured impact today

                Small but non-zero: the objectui corpus census (736 JSON documents plus json doc fences, 2747 typed nodes) counts 5action:button nodes, against 158 bare button, 135 card, 42 statistic, and zeroui:-prefixed spellings. None of the 5 currently carries a ${…} in label, so nothing is visibly broken — this is a latent gap, not a live defect.

                Options

                1. Add an action:button row (['label']). Faithful to the evidence the docblock already cites, keeps the map's "key is the type string" semantics exact and mechanically answerable.
                2. Declare the map's keys to be bare registry names and give the lookup a documented normalization rule with an explicit deny-list for the namespaces that read from the bag (element:, page:). More machinery, and the deny-list is the inference the module is trying to avoid.
                3. Narrow the docblock to cite only form/button.tsx, recording that action:button is deliberately out.

                Recommendation: (1) — one line, no new concepts, and it makes the declaration say what its own evidence table already claims. (3) is acceptable if action:button's label is considered action-declared rather than author-written, but then the docblock should say so rather than citing the file as evidence for a row that does not cover it.

                Provenance

                Measured on objectui origin/main7a5da1414 with @objectstack/spec@17.2.0 installed. Registration sites and read points are reproducible from that tree.

                Metadata

                Metadata

                Assignees

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions