Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/spotty-buttons-narrow-their-row.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
---
'@objectstack/spec': patch
---

Correct the `EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT` docblock so it stops promising coverage the lookup does not deliver.

The `button` row cited two objectui renderers as its evidence — `form/button.tsx` and `action/action-button.tsx` — but the map is keyed on the type string exactly as authored, and `action-button.tsx` registers under `action:button`, a different key with no row. Prose only: the map, `expressionBindableTextKeysFor` and every exported type are byte-identical, `action:button` still answers the empty set, and no accept surface widens.

- The `button` row now cites `form/button.tsx` alone.
- A new docblock section records that the table is keyed on the authored type string, so namespace-prefixed spellings (`action:button`, `ui:button`, the `mcp:` family) answer the empty set by construction rather than by oversight — and that prefix-stripping is the wrong repair, because it would in the same motion grant rows to `element:button` and `page:card`, whose renderers never read these keys at the node's top level.
- `action:button` and `ui:button` are recorded as deliberately out on two measured grounds — zero pull in the objectui corpus, and the module's own admission rule that each row arrives with its own measurement — alongside the unchanged reopen path for a named requirement.
28 changes: 27 additions & 1 deletion content/docs/references/ui/expression-bindable-text-keys.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,7 @@ the evaluated-but-blank half of the objectui#4795 table:
|-------------|----------------------------------------------------------|
| `statistic` | `schema.label`, `schema.value`, `schema.description` (`data-display/statistic.tsx`) |
| `card` | `schema.title`, `schema.description` (`layout/card.tsx`) |
| `button` | `schema.label` (`form/button.tsx`, `action/action-button.tsx`) |
| `button` | `schema.label` (`form/button.tsx`) |

These are the measured motivating cases from objectui#4795 (dashboard
workhorses). Other registered renderers also read keys from this closed set
Expand All@@ -78,6 +78,32 @@ channel as `content` alone and declared `text.value` OUT on those grounds —
this omission is deliberate, not pending measurement. Adding a row is
additive and spec-first; do it here, never as a renderer-side inference.

## Rows are keyed on the AUTHORED type string, prefix and all

`expressionBindableTextKeysFor` does an own-property lookup on the
type string exactly as authored, so a namespace-prefixed spelling —
`action:button`, `ui:button`, the `mcp:` family — is a DIFFERENT key from
the bare name and answers the empty set BY CONSTRUCTION, not by oversight.
Nothing strips a prefix between the two, and nothing should: a stripping
rule would in the same motion grant rows to `element:button` and
`page:card`, whose renderers take their config from the evaluated
`properties` bag and never read these keys at the node's top level.

`action:button` is therefore deliberately OUT, and `ui:button` with it
(objectstack#13672); the `button` row above covers the bare `button`
spelling alone, which is why its citation names `form/button.tsx` only. Two
measured grounds, the same two kinds every other row runs on — zero pull
(the objectui corpus census of 736 JSON documents / 2747 typed nodes finds 5
`action:button` nodes, 0 of them carrying an expression in `label`, and 0
`ui:`-prefixed spellings of any kind), and this module's own admission rule
that a row arrives with its own measurement while a type string with no row
IS the empty set. Not a judgement that the key is inert:
`action/action-button.tsx` renders the author-written `schema.label`
directly (measured 2026-08-31), so the read-back half is real and only the
pull is missing. The reopen path is the ordinary one: a named requirement
for an expression-bound `action:button` label adds a row here, carrying that
row's own measurement.

<Callout type="info">
**Source:** `packages/spec/src/ui/expression-bindable-text-keys.zod.ts`
</Callout>
Expand Down
23 changes: 19 additions & 4 deletions packages/spec/src/ui/expression-bindable-text-keys.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,10 @@ describe('EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT — measured carriage', ()
it('matches the renderer read points measured at the objectui pin', () => {
// data-display/statistic.tsx reads schema.label / .value / .description;
// layout/card.tsx reads schema.title / .description;
// form/button.tsx (and action/action-button.tsx) read schema.label.
// form/button.tsx reads schema.label — and the row covers that BARE
// `button` spelling alone: action/action-button.tsx registers as
// `action:button`, a different key this map deliberately does not
// carry (objectstack#13672; see the module docblock).
expect(EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT).toEqual({
statistic: ['label', 'value', 'description'],
card: ['title', 'description'],
Expand DownExpand Up@@ -110,9 +113,21 @@ describe('expressionBindableTextKeysFor — the mechanical per-type answer', ()

it('answers the empty set for every unlisted type — closed, never inferred', () => {
// `text` binds through its own `content` leg; `element:*` / `page:*`
// config rides the evaluated `properties` bag — none of them get rows
// inferred from what their renderers happen to read.
for (const type of ['text', 'element:text', 'page:card', 'alert', '']) {
// config rides the evaluated `properties` bag; and a namespace-prefixed
// spelling is a DIFFERENT key from the bare name, so `action:button` and
// `ui:button` answer empty by construction — the pair the objectstack#13672
// ruling recorded as deliberately out, pinned here so a later prefix-
// stripping "fix" cannot move the machine face in silence. None of them get
// rows inferred from what their renderers happen to read.
for (const type of [
'text',
'element:text',
'page:card',
'action:button',
'ui:button',
'alert',
'',
]) {
const keys = expressionBindableTextKeysFor(type);
expect(keys).toEqual([]);
expect(Object.isFrozen(keys)).toBe(true);
Expand Down
28 changes: 27 additions & 1 deletion packages/spec/src/ui/expression-bindable-text-keys.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,7 +55,7 @@
* |-------------|----------------------------------------------------------|
* | `statistic` | `schema.label`, `schema.value`, `schema.description` (`data-display/statistic.tsx`) |
* | `card` | `schema.title`, `schema.description` (`layout/card.tsx`) |
* | `button` | `schema.label` (`form/button.tsx`, `action/action-button.tsx`) |
* | `button` | `schema.label` (`form/button.tsx`) |
*
* These are the measured motivating cases from objectui#4795 (dashboard
* workhorses). Other registered renderers also read keys from this closed set
Expand All@@ -73,6 +73,32 @@
* channel as `content` alone and declared `text.value` OUT on those grounds —
* this omission is deliberate, not pending measurement. Adding a row is
* additive and spec-first; do it here, never as a renderer-side inference.
*
* ## Rows are keyed on the AUTHORED type string, prefix and all
*
* {@link expressionBindableTextKeysFor} does an own-property lookup on the
* type string exactly as authored, so a namespace-prefixed spelling —
* `action:button`, `ui:button`, the `mcp:` family — is a DIFFERENT key from
* the bare name and answers the empty set BY CONSTRUCTION, not by oversight.
* Nothing strips a prefix between the two, and nothing should: a stripping
* rule would in the same motion grant rows to `element:button` and
* `page:card`, whose renderers take their config from the evaluated
* `properties` bag and never read these keys at the node's top level.
*
* `action:button` is therefore deliberately OUT, and `ui:button` with it
* (objectstack#13672); the `button` row above covers the bare `button`
* spelling alone, which is why its citation names `form/button.tsx` only. Two
* measured grounds, the same two kinds every other row runs on — zero pull
* (the objectui corpus census of 736 JSON documents / 2747 typed nodes finds 5
* `action:button` nodes, 0 of them carrying an expression in `label`, and 0
* `ui:`-prefixed spellings of any kind), and this module's own admission rule
* that a row arrives with its own measurement while a type string with no row
* IS the empty set. Not a judgement that the key is inert:
* `action/action-button.tsx` renders the author-written `schema.label`
* directly (measured 2026-08-31), so the read-back half is real and only the
* pull is missing. The reopen path is the ordinary one: a named requirement
* for an expression-bound `action:button` label adds a row here, carrying that
* row's own measurement.
*/

import { z } from 'zod';
Expand Down
Loading
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/spotty-buttons-narrow-their-row.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
---
'@objectstack/spec': patch
---

Correct the `EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT` docblock so it stops promising coverage the lookup does not deliver.

The `button` row cited two objectui renderers as its evidence — `form/button.tsx` and `action/action-button.tsx` — but the map is keyed on the type string exactly as authored, and `action-button.tsx` registers under `action:button`, a different key with no row. Prose only: the map, `expressionBindableTextKeysFor` and every exported type are byte-identical, `action:button` still answers the empty set, and no accept surface widens.

- The `button` row now cites `form/button.tsx` alone.
- A new docblock section records that the table is keyed on the authored type string, so namespace-prefixed spellings (`action:button`, `ui:button`, the `mcp:` family) answer the empty set by construction rather than by oversight — and that prefix-stripping is the wrong repair, because it would in the same motion grant rows to `element:button` and `page:card`, whose renderers never read these keys at the node's top level.
- `action:button` and `ui:button` are recorded as deliberately out on two measured grounds — zero pull in the objectui corpus, and the module's own admission rule that each row arrives with its own measurement — alongside the unchanged reopen path for a named requirement.
28 changes: 27 additions & 1 deletion content/docs/references/ui/expression-bindable-text-keys.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,7 @@ the evaluated-but-blank half of the objectui#4795 table:
|-------------|----------------------------------------------------------|
| `statistic` | `schema.label`, `schema.value`, `schema.description` (`data-display/statistic.tsx`) |
| `card` | `schema.title`, `schema.description` (`layout/card.tsx`) |
| `button` | `schema.label` (`form/button.tsx`, `action/action-button.tsx`) |
| `button` | `schema.label` (`form/button.tsx`) |

These are the measured motivating cases from objectui#4795 (dashboard
workhorses). Other registered renderers also read keys from this closed set
Expand All@@ -78,6 +78,32 @@ channel as `content` alone and declared `text.value` OUT on those grounds —
this omission is deliberate, not pending measurement. Adding a row is
additive and spec-first; do it here, never as a renderer-side inference.

## Rows are keyed on the AUTHORED type string, prefix and all

`expressionBindableTextKeysFor` does an own-property lookup on the
type string exactly as authored, so a namespace-prefixed spelling —
`action:button`, `ui:button`, the `mcp:` family — is a DIFFERENT key from
the bare name and answers the empty set BY CONSTRUCTION, not by oversight.
Nothing strips a prefix between the two, and nothing should: a stripping
rule would in the same motion grant rows to `element:button` and
`page:card`, whose renderers take their config from the evaluated
`properties` bag and never read these keys at the node's top level.

`action:button` is therefore deliberately OUT, and `ui:button` with it
(objectstack#13672); the `button` row above covers the bare `button`
spelling alone, which is why its citation names `form/button.tsx` only. Two
measured grounds, the same two kinds every other row runs on — zero pull
(the objectui corpus census of 736 JSON documents / 2747 typed nodes finds 5
`action:button` nodes, 0 of them carrying an expression in `label`, and 0
`ui:`-prefixed spellings of any kind), and this module's own admission rule
that a row arrives with its own measurement while a type string with no row
IS the empty set. Not a judgement that the key is inert:
`action/action-button.tsx` renders the author-written `schema.label`
directly (measured 2026-08-31), so the read-back half is real and only the
pull is missing. The reopen path is the ordinary one: a named requirement
for an expression-bound `action:button` label adds a row here, carrying that
row's own measurement.

<Callout type="info">
**Source:** `packages/spec/src/ui/expression-bindable-text-keys.zod.ts`
</Callout>
Expand Down
23 changes: 19 additions & 4 deletions packages/spec/src/ui/expression-bindable-text-keys.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,10 @@ describe('EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT — measured carriage', ()
it('matches the renderer read points measured at the objectui pin', () => {
// data-display/statistic.tsx reads schema.label / .value / .description;
// layout/card.tsx reads schema.title / .description;
// form/button.tsx (and action/action-button.tsx) read schema.label.
// form/button.tsx reads schema.label — and the row covers that BARE
// `button` spelling alone: action/action-button.tsx registers as
// `action:button`, a different key this map deliberately does not
// carry (objectstack#13672; see the module docblock).
expect(EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT).toEqual({
statistic: ['label', 'value', 'description'],
card: ['title', 'description'],
Expand DownExpand Up@@ -110,9 +113,21 @@ describe('expressionBindableTextKeysFor — the mechanical per-type answer', ()

it('answers the empty set for every unlisted type — closed, never inferred', () => {
// `text` binds through its own `content` leg; `element:*` / `page:*`
// config rides the evaluated `properties` bag — none of them get rows
// inferred from what their renderers happen to read.
for (const type of ['text', 'element:text', 'page:card', 'alert', '']) {
// config rides the evaluated `properties` bag; and a namespace-prefixed
// spelling is a DIFFERENT key from the bare name, so `action:button` and
// `ui:button` answer empty by construction — the pair the objectstack#13672
// ruling recorded as deliberately out, pinned here so a later prefix-
// stripping "fix" cannot move the machine face in silence. None of them get
// rows inferred from what their renderers happen to read.
for (const type of [
'text',
'element:text',
'page:card',
'action:button',
'ui:button',
'alert',
'',
]) {
const keys = expressionBindableTextKeysFor(type);
expect(keys).toEqual([]);
expect(Object.isFrozen(keys)).toBe(true);
Expand Down
28 changes: 27 additions & 1 deletion packages/spec/src/ui/expression-bindable-text-keys.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,7 +55,7 @@
* |-------------|----------------------------------------------------------|
* | `statistic` | `schema.label`, `schema.value`, `schema.description` (`data-display/statistic.tsx`) |
* | `card` | `schema.title`, `schema.description` (`layout/card.tsx`) |
* | `button` | `schema.label` (`form/button.tsx`, `action/action-button.tsx`) |
* | `button` | `schema.label` (`form/button.tsx`) |
*
* These are the measured motivating cases from objectui#4795 (dashboard
* workhorses). Other registered renderers also read keys from this closed set
Expand All@@ -73,6 +73,32 @@
* channel as `content` alone and declared `text.value` OUT on those grounds —
* this omission is deliberate, not pending measurement. Adding a row is
* additive and spec-first; do it here, never as a renderer-side inference.
*
* ## Rows are keyed on the AUTHORED type string, prefix and all
*
* {@link expressionBindableTextKeysFor} does an own-property lookup on the
* type string exactly as authored, so a namespace-prefixed spelling —
* `action:button`, `ui:button`, the `mcp:` family — is a DIFFERENT key from
* the bare name and answers the empty set BY CONSTRUCTION, not by oversight.
* Nothing strips a prefix between the two, and nothing should: a stripping
* rule would in the same motion grant rows to `element:button` and
* `page:card`, whose renderers take their config from the evaluated
* `properties` bag and never read these keys at the node's top level.
*
* `action:button` is therefore deliberately OUT, and `ui:button` with it
* (objectstack#13672); the `button` row above covers the bare `button`
* spelling alone, which is why its citation names `form/button.tsx` only. Two
* measured grounds, the same two kinds every other row runs on — zero pull
* (the objectui corpus census of 736 JSON documents / 2747 typed nodes finds 5
* `action:button` nodes, 0 of them carrying an expression in `label`, and 0
* `ui:`-prefixed spellings of any kind), and this module's own admission rule
* that a row arrives with its own measurement while a type string with no row
* IS the empty set. Not a judgement that the key is inert:
* `action/action-button.tsx` renders the author-written `schema.label`
* directly (measured 2026-08-31), so the read-back half is real and only the
* pull is missing. The reopen path is the ordinary one: a named requirement
* for an expression-bound `action:button` label adds a row here, carrying that
* row's own measurement.
*/

import { z } from 'zod';
Expand Down
Loading
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/spotty-buttons-narrow-their-row.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
---
'@objectstack/spec': patch
---

Correct the `EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT` docblock so it stops promising coverage the lookup does not deliver.

The `button` row cited two objectui renderers as its evidence — `form/button.tsx` and `action/action-button.tsx` — but the map is keyed on the type string exactly as authored, and `action-button.tsx` registers under `action:button`, a different key with no row. Prose only: the map, `expressionBindableTextKeysFor` and every exported type are byte-identical, `action:button` still answers the empty set, and no accept surface widens.

- The `button` row now cites `form/button.tsx` alone.
- A new docblock section records that the table is keyed on the authored type string, so namespace-prefixed spellings (`action:button`, `ui:button`, the `mcp:` family) answer the empty set by construction rather than by oversight — and that prefix-stripping is the wrong repair, because it would in the same motion grant rows to `element:button` and `page:card`, whose renderers never read these keys at the node's top level.
- `action:button` and `ui:button` are recorded as deliberately out on two measured grounds — zero pull in the objectui corpus, and the module's own admission rule that each row arrives with its own measurement — alongside the unchanged reopen path for a named requirement.
28 changes: 27 additions & 1 deletion content/docs/references/ui/expression-bindable-text-keys.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,7 @@ the evaluated-but-blank half of the objectui#4795 table:
|-------------|----------------------------------------------------------|
| `statistic` | `schema.label`, `schema.value`, `schema.description` (`data-display/statistic.tsx`) |
| `card` | `schema.title`, `schema.description` (`layout/card.tsx`) |
| `button` | `schema.label` (`form/button.tsx`, `action/action-button.tsx`) |
| `button` | `schema.label` (`form/button.tsx`) |

These are the measured motivating cases from objectui#4795 (dashboard
workhorses). Other registered renderers also read keys from this closed set
Expand All@@ -78,6 +78,32 @@ channel as `content` alone and declared `text.value` OUT on those grounds —
this omission is deliberate, not pending measurement. Adding a row is
additive and spec-first; do it here, never as a renderer-side inference.

## Rows are keyed on the AUTHORED type string, prefix and all

`expressionBindableTextKeysFor` does an own-property lookup on the
type string exactly as authored, so a namespace-prefixed spelling —
`action:button`, `ui:button`, the `mcp:` family — is a DIFFERENT key from
the bare name and answers the empty set BY CONSTRUCTION, not by oversight.
Nothing strips a prefix between the two, and nothing should: a stripping
rule would in the same motion grant rows to `element:button` and
`page:card`, whose renderers take their config from the evaluated
`properties` bag and never read these keys at the node's top level.

`action:button` is therefore deliberately OUT, and `ui:button` with it
(objectstack#13672); the `button` row above covers the bare `button`
spelling alone, which is why its citation names `form/button.tsx` only. Two
measured grounds, the same two kinds every other row runs on — zero pull
(the objectui corpus census of 736 JSON documents / 2747 typed nodes finds 5
`action:button` nodes, 0 of them carrying an expression in `label`, and 0
`ui:`-prefixed spellings of any kind), and this module's own admission rule
that a row arrives with its own measurement while a type string with no row
IS the empty set. Not a judgement that the key is inert:
`action/action-button.tsx` renders the author-written `schema.label`
directly (measured 2026-08-31), so the read-back half is real and only the
pull is missing. The reopen path is the ordinary one: a named requirement
for an expression-bound `action:button` label adds a row here, carrying that
row's own measurement.

<Callout type="info">
**Source:** `packages/spec/src/ui/expression-bindable-text-keys.zod.ts`
</Callout>
Expand Down
23 changes: 19 additions & 4 deletions packages/spec/src/ui/expression-bindable-text-keys.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,10 @@ describe('EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT — measured carriage', ()
it('matches the renderer read points measured at the objectui pin', () => {
// data-display/statistic.tsx reads schema.label / .value / .description;
// layout/card.tsx reads schema.title / .description;
// form/button.tsx (and action/action-button.tsx) read schema.label.
// form/button.tsx reads schema.label — and the row covers that BARE
// `button` spelling alone: action/action-button.tsx registers as
// `action:button`, a different key this map deliberately does not
// carry (objectstack#13672; see the module docblock).
expect(EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT).toEqual({
statistic: ['label', 'value', 'description'],
card: ['title', 'description'],
Expand DownExpand Up@@ -110,9 +113,21 @@ describe('expressionBindableTextKeysFor — the mechanical per-type answer', ()

it('answers the empty set for every unlisted type — closed, never inferred', () => {
// `text` binds through its own `content` leg; `element:*` / `page:*`
// config rides the evaluated `properties` bag — none of them get rows
// inferred from what their renderers happen to read.
for (const type of ['text', 'element:text', 'page:card', 'alert', '']) {
// config rides the evaluated `properties` bag; and a namespace-prefixed
// spelling is a DIFFERENT key from the bare name, so `action:button` and
// `ui:button` answer empty by construction — the pair the objectstack#13672
// ruling recorded as deliberately out, pinned here so a later prefix-
// stripping "fix" cannot move the machine face in silence. None of them get
// rows inferred from what their renderers happen to read.
for (const type of [
'text',
'element:text',
'page:card',
'action:button',
'ui:button',
'alert',
'',
]) {
const keys = expressionBindableTextKeysFor(type);
expect(keys).toEqual([]);
expect(Object.isFrozen(keys)).toBe(true);
Expand Down
28 changes: 27 additions & 1 deletion packages/spec/src/ui/expression-bindable-text-keys.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,7 +55,7 @@
* |-------------|----------------------------------------------------------|
* | `statistic` | `schema.label`, `schema.value`, `schema.description` (`data-display/statistic.tsx`) |
* | `card` | `schema.title`, `schema.description` (`layout/card.tsx`) |
* | `button` | `schema.label` (`form/button.tsx`, `action/action-button.tsx`) |
* | `button` | `schema.label` (`form/button.tsx`) |
*
* These are the measured motivating cases from objectui#4795 (dashboard
* workhorses). Other registered renderers also read keys from this closed set
Expand All@@ -73,6 +73,32 @@
* channel as `content` alone and declared `text.value` OUT on those grounds —
* this omission is deliberate, not pending measurement. Adding a row is
* additive and spec-first; do it here, never as a renderer-side inference.
*
* ## Rows are keyed on the AUTHORED type string, prefix and all
*
* {@link expressionBindableTextKeysFor} does an own-property lookup on the
* type string exactly as authored, so a namespace-prefixed spelling —
* `action:button`, `ui:button`, the `mcp:` family — is a DIFFERENT key from
* the bare name and answers the empty set BY CONSTRUCTION, not by oversight.
* Nothing strips a prefix between the two, and nothing should: a stripping
* rule would in the same motion grant rows to `element:button` and
* `page:card`, whose renderers take their config from the evaluated
* `properties` bag and never read these keys at the node's top level.
*
* `action:button` is therefore deliberately OUT, and `ui:button` with it
* (objectstack#13672); the `button` row above covers the bare `button`
* spelling alone, which is why its citation names `form/button.tsx` only. Two
* measured grounds, the same two kinds every other row runs on — zero pull
* (the objectui corpus census of 736 JSON documents / 2747 typed nodes finds 5
* `action:button` nodes, 0 of them carrying an expression in `label`, and 0
* `ui:`-prefixed spellings of any kind), and this module's own admission rule
* that a row arrives with its own measurement while a type string with no row
* IS the empty set. Not a judgement that the key is inert:
* `action/action-button.tsx` renders the author-written `schema.label`
* directly (measured 2026-08-31), so the read-back half is real and only the
* pull is missing. The reopen path is the ordinary one: a named requirement
* for an expression-bound `action:button` label adds a row here, carrying that
* row's own measurement.
*/

import { z } from 'zod';
Expand Down
Loading
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/spotty-buttons-narrow-their-row.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
---
'@objectstack/spec': patch
---

Correct the `EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT` docblock so it stops promising coverage the lookup does not deliver.

The `button` row cited two objectui renderers as its evidence — `form/button.tsx` and `action/action-button.tsx` — but the map is keyed on the type string exactly as authored, and `action-button.tsx` registers under `action:button`, a different key with no row. Prose only: the map, `expressionBindableTextKeysFor` and every exported type are byte-identical, `action:button` still answers the empty set, and no accept surface widens.

- The `button` row now cites `form/button.tsx` alone.
- A new docblock section records that the table is keyed on the authored type string, so namespace-prefixed spellings (`action:button`, `ui:button`, the `mcp:` family) answer the empty set by construction rather than by oversight — and that prefix-stripping is the wrong repair, because it would in the same motion grant rows to `element:button` and `page:card`, whose renderers never read these keys at the node's top level.
- `action:button` and `ui:button` are recorded as deliberately out on two measured grounds — zero pull in the objectui corpus, and the module's own admission rule that each row arrives with its own measurement — alongside the unchanged reopen path for a named requirement.
28 changes: 27 additions & 1 deletion content/docs/references/ui/expression-bindable-text-keys.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,7 @@ the evaluated-but-blank half of the objectui#4795 table:
|-------------|----------------------------------------------------------|
| `statistic` | `schema.label`, `schema.value`, `schema.description` (`data-display/statistic.tsx`) |
| `card` | `schema.title`, `schema.description` (`layout/card.tsx`) |
| `button` | `schema.label` (`form/button.tsx`, `action/action-button.tsx`) |
| `button` | `schema.label` (`form/button.tsx`) |

These are the measured motivating cases from objectui#4795 (dashboard
workhorses). Other registered renderers also read keys from this closed set
Expand All@@ -78,6 +78,32 @@ channel as `content` alone and declared `text.value` OUT on those grounds —
this omission is deliberate, not pending measurement. Adding a row is
additive and spec-first; do it here, never as a renderer-side inference.

## Rows are keyed on the AUTHORED type string, prefix and all

`expressionBindableTextKeysFor` does an own-property lookup on the
type string exactly as authored, so a namespace-prefixed spelling —
`action:button`, `ui:button`, the `mcp:` family — is a DIFFERENT key from
the bare name and answers the empty set BY CONSTRUCTION, not by oversight.
Nothing strips a prefix between the two, and nothing should: a stripping
rule would in the same motion grant rows to `element:button` and
`page:card`, whose renderers take their config from the evaluated
`properties` bag and never read these keys at the node's top level.

`action:button` is therefore deliberately OUT, and `ui:button` with it
(objectstack#13672); the `button` row above covers the bare `button`
spelling alone, which is why its citation names `form/button.tsx` only. Two
measured grounds, the same two kinds every other row runs on — zero pull
(the objectui corpus census of 736 JSON documents / 2747 typed nodes finds 5
`action:button` nodes, 0 of them carrying an expression in `label`, and 0
`ui:`-prefixed spellings of any kind), and this module's own admission rule
that a row arrives with its own measurement while a type string with no row
IS the empty set. Not a judgement that the key is inert:
`action/action-button.tsx` renders the author-written `schema.label`
directly (measured 2026-08-31), so the read-back half is real and only the
pull is missing. The reopen path is the ordinary one: a named requirement
for an expression-bound `action:button` label adds a row here, carrying that
row's own measurement.

<Callout type="info">
**Source:** `packages/spec/src/ui/expression-bindable-text-keys.zod.ts`
</Callout>
Expand Down
23 changes: 19 additions & 4 deletions packages/spec/src/ui/expression-bindable-text-keys.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,10 @@ describe('EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT — measured carriage', ()
it('matches the renderer read points measured at the objectui pin', () => {
// data-display/statistic.tsx reads schema.label / .value / .description;
// layout/card.tsx reads schema.title / .description;
// form/button.tsx (and action/action-button.tsx) read schema.label.
// form/button.tsx reads schema.label — and the row covers that BARE
// `button` spelling alone: action/action-button.tsx registers as
// `action:button`, a different key this map deliberately does not
// carry (objectstack#13672; see the module docblock).
expect(EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT).toEqual({
statistic: ['label', 'value', 'description'],
card: ['title', 'description'],
Expand DownExpand Up@@ -110,9 +113,21 @@ describe('expressionBindableTextKeysFor — the mechanical per-type answer', ()

it('answers the empty set for every unlisted type — closed, never inferred', () => {
// `text` binds through its own `content` leg; `element:*` / `page:*`
// config rides the evaluated `properties` bag — none of them get rows
// inferred from what their renderers happen to read.
for (const type of ['text', 'element:text', 'page:card', 'alert', '']) {
// config rides the evaluated `properties` bag; and a namespace-prefixed
// spelling is a DIFFERENT key from the bare name, so `action:button` and
// `ui:button` answer empty by construction — the pair the objectstack#13672
// ruling recorded as deliberately out, pinned here so a later prefix-
// stripping "fix" cannot move the machine face in silence. None of them get
// rows inferred from what their renderers happen to read.
for (const type of [
'text',
'element:text',
'page:card',
'action:button',
'ui:button',
'alert',
'',
]) {
const keys = expressionBindableTextKeysFor(type);
expect(keys).toEqual([]);
expect(Object.isFrozen(keys)).toBe(true);
Expand Down
28 changes: 27 additions & 1 deletion packages/spec/src/ui/expression-bindable-text-keys.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,7 +55,7 @@
* |-------------|----------------------------------------------------------|
* | `statistic` | `schema.label`, `schema.value`, `schema.description` (`data-display/statistic.tsx`) |
* | `card` | `schema.title`, `schema.description` (`layout/card.tsx`) |
* | `button` | `schema.label` (`form/button.tsx`, `action/action-button.tsx`) |
* | `button` | `schema.label` (`form/button.tsx`) |
*
* These are the measured motivating cases from objectui#4795 (dashboard
* workhorses). Other registered renderers also read keys from this closed set
Expand All@@ -73,6 +73,32 @@
* channel as `content` alone and declared `text.value` OUT on those grounds —
* this omission is deliberate, not pending measurement. Adding a row is
* additive and spec-first; do it here, never as a renderer-side inference.
*
* ## Rows are keyed on the AUTHORED type string, prefix and all
*
* {@link expressionBindableTextKeysFor} does an own-property lookup on the
* type string exactly as authored, so a namespace-prefixed spelling —
* `action:button`, `ui:button`, the `mcp:` family — is a DIFFERENT key from
* the bare name and answers the empty set BY CONSTRUCTION, not by oversight.
* Nothing strips a prefix between the two, and nothing should: a stripping
* rule would in the same motion grant rows to `element:button` and
* `page:card`, whose renderers take their config from the evaluated
* `properties` bag and never read these keys at the node's top level.
*
* `action:button` is therefore deliberately OUT, and `ui:button` with it
* (objectstack#13672); the `button` row above covers the bare `button`
* spelling alone, which is why its citation names `form/button.tsx` only. Two
* measured grounds, the same two kinds every other row runs on — zero pull
* (the objectui corpus census of 736 JSON documents / 2747 typed nodes finds 5
* `action:button` nodes, 0 of them carrying an expression in `label`, and 0
* `ui:`-prefixed spellings of any kind), and this module's own admission rule
* that a row arrives with its own measurement while a type string with no row
* IS the empty set. Not a judgement that the key is inert:
* `action/action-button.tsx` renders the author-written `schema.label`
* directly (measured 2026-08-31), so the read-back half is real and only the
* pull is missing. The reopen path is the ordinary one: a named requirement
* for an expression-bound `action:button` label adds a row here, carrying that
* row's own measurement.
*/

import { z } from 'zod';
Expand Down
Loading
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/spotty-buttons-narrow-their-row.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
---
'@objectstack/spec': patch
---

Correct the `EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT` docblock so it stops promising coverage the lookup does not deliver.

The `button` row cited two objectui renderers as its evidence — `form/button.tsx` and `action/action-button.tsx` — but the map is keyed on the type string exactly as authored, and `action-button.tsx` registers under `action:button`, a different key with no row. Prose only: the map, `expressionBindableTextKeysFor` and every exported type are byte-identical, `action:button` still answers the empty set, and no accept surface widens.

- The `button` row now cites `form/button.tsx` alone.
- A new docblock section records that the table is keyed on the authored type string, so namespace-prefixed spellings (`action:button`, `ui:button`, the `mcp:` family) answer the empty set by construction rather than by oversight — and that prefix-stripping is the wrong repair, because it would in the same motion grant rows to `element:button` and `page:card`, whose renderers never read these keys at the node's top level.
- `action:button` and `ui:button` are recorded as deliberately out on two measured grounds — zero pull in the objectui corpus, and the module's own admission rule that each row arrives with its own measurement — alongside the unchanged reopen path for a named requirement.
28 changes: 27 additions & 1 deletion content/docs/references/ui/expression-bindable-text-keys.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,7 @@ the evaluated-but-blank half of the objectui#4795 table:
|-------------|----------------------------------------------------------|
| `statistic` | `schema.label`, `schema.value`, `schema.description` (`data-display/statistic.tsx`) |
| `card` | `schema.title`, `schema.description` (`layout/card.tsx`) |
| `button` | `schema.label` (`form/button.tsx`, `action/action-button.tsx`) |
| `button` | `schema.label` (`form/button.tsx`) |

These are the measured motivating cases from objectui#4795 (dashboard
workhorses). Other registered renderers also read keys from this closed set
Expand All@@ -78,6 +78,32 @@ channel as `content` alone and declared `text.value` OUT on those grounds —
this omission is deliberate, not pending measurement. Adding a row is
additive and spec-first; do it here, never as a renderer-side inference.

## Rows are keyed on the AUTHORED type string, prefix and all

`expressionBindableTextKeysFor` does an own-property lookup on the
type string exactly as authored, so a namespace-prefixed spelling —
`action:button`, `ui:button`, the `mcp:` family — is a DIFFERENT key from
the bare name and answers the empty set BY CONSTRUCTION, not by oversight.
Nothing strips a prefix between the two, and nothing should: a stripping
rule would in the same motion grant rows to `element:button` and
`page:card`, whose renderers take their config from the evaluated
`properties` bag and never read these keys at the node's top level.

`action:button` is therefore deliberately OUT, and `ui:button` with it
(objectstack#13672); the `button` row above covers the bare `button`
spelling alone, which is why its citation names `form/button.tsx` only. Two
measured grounds, the same two kinds every other row runs on — zero pull
(the objectui corpus census of 736 JSON documents / 2747 typed nodes finds 5
`action:button` nodes, 0 of them carrying an expression in `label`, and 0
`ui:`-prefixed spellings of any kind), and this module's own admission rule
that a row arrives with its own measurement while a type string with no row
IS the empty set. Not a judgement that the key is inert:
`action/action-button.tsx` renders the author-written `schema.label`
directly (measured 2026-08-31), so the read-back half is real and only the
pull is missing. The reopen path is the ordinary one: a named requirement
for an expression-bound `action:button` label adds a row here, carrying that
row's own measurement.

<Callout type="info">
**Source:** `packages/spec/src/ui/expression-bindable-text-keys.zod.ts`
</Callout>
Expand Down
23 changes: 19 additions & 4 deletions packages/spec/src/ui/expression-bindable-text-keys.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,10 @@ describe('EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT — measured carriage', ()
it('matches the renderer read points measured at the objectui pin', () => {
// data-display/statistic.tsx reads schema.label / .value / .description;
// layout/card.tsx reads schema.title / .description;
// form/button.tsx (and action/action-button.tsx) read schema.label.
// form/button.tsx reads schema.label — and the row covers that BARE
// `button` spelling alone: action/action-button.tsx registers as
// `action:button`, a different key this map deliberately does not
// carry (objectstack#13672; see the module docblock).
expect(EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT).toEqual({
statistic: ['label', 'value', 'description'],
card: ['title', 'description'],
Expand DownExpand Up@@ -110,9 +113,21 @@ describe('expressionBindableTextKeysFor — the mechanical per-type answer', ()

it('answers the empty set for every unlisted type — closed, never inferred', () => {
// `text` binds through its own `content` leg; `element:*` / `page:*`
// config rides the evaluated `properties` bag — none of them get rows
// inferred from what their renderers happen to read.
for (const type of ['text', 'element:text', 'page:card', 'alert', '']) {
// config rides the evaluated `properties` bag; and a namespace-prefixed
// spelling is a DIFFERENT key from the bare name, so `action:button` and
// `ui:button` answer empty by construction — the pair the objectstack#13672
// ruling recorded as deliberately out, pinned here so a later prefix-
// stripping "fix" cannot move the machine face in silence. None of them get
// rows inferred from what their renderers happen to read.
for (const type of [
'text',
'element:text',
'page:card',
'action:button',
'ui:button',
'alert',
'',
]) {
const keys = expressionBindableTextKeysFor(type);
expect(keys).toEqual([]);
expect(Object.isFrozen(keys)).toBe(true);
Expand Down
28 changes: 27 additions & 1 deletion packages/spec/src/ui/expression-bindable-text-keys.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,7 +55,7 @@
* |-------------|----------------------------------------------------------|
* | `statistic` | `schema.label`, `schema.value`, `schema.description` (`data-display/statistic.tsx`) |
* | `card` | `schema.title`, `schema.description` (`layout/card.tsx`) |
* | `button` | `schema.label` (`form/button.tsx`, `action/action-button.tsx`) |
* | `button` | `schema.label` (`form/button.tsx`) |
*
* These are the measured motivating cases from objectui#4795 (dashboard
* workhorses). Other registered renderers also read keys from this closed set
Expand All@@ -73,6 +73,32 @@
* channel as `content` alone and declared `text.value` OUT on those grounds —
* this omission is deliberate, not pending measurement. Adding a row is
* additive and spec-first; do it here, never as a renderer-side inference.
*
* ## Rows are keyed on the AUTHORED type string, prefix and all
*
* {@link expressionBindableTextKeysFor} does an own-property lookup on the
* type string exactly as authored, so a namespace-prefixed spelling —
* `action:button`, `ui:button`, the `mcp:` family — is a DIFFERENT key from
* the bare name and answers the empty set BY CONSTRUCTION, not by oversight.
* Nothing strips a prefix between the two, and nothing should: a stripping
* rule would in the same motion grant rows to `element:button` and
* `page:card`, whose renderers take their config from the evaluated
* `properties` bag and never read these keys at the node's top level.
*
* `action:button` is therefore deliberately OUT, and `ui:button` with it
* (objectstack#13672); the `button` row above covers the bare `button`
* spelling alone, which is why its citation names `form/button.tsx` only. Two
* measured grounds, the same two kinds every other row runs on — zero pull
* (the objectui corpus census of 736 JSON documents / 2747 typed nodes finds 5
* `action:button` nodes, 0 of them carrying an expression in `label`, and 0
* `ui:`-prefixed spellings of any kind), and this module's own admission rule
* that a row arrives with its own measurement while a type string with no row
* IS the empty set. Not a judgement that the key is inert:
* `action/action-button.tsx` renders the author-written `schema.label`
* directly (measured 2026-08-31), so the read-back half is real and only the
* pull is missing. The reopen path is the ordinary one: a named requirement
* for an expression-bound `action:button` label adds a row here, carrying that
* row's own measurement.
*/

import { z } from 'zod';
Expand Down
Loading
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/spotty-buttons-narrow-their-row.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
---
'@objectstack/spec': patch
---

Correct the `EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT` docblock so it stops promising coverage the lookup does not deliver.

The `button` row cited two objectui renderers as its evidence — `form/button.tsx` and `action/action-button.tsx` — but the map is keyed on the type string exactly as authored, and `action-button.tsx` registers under `action:button`, a different key with no row. Prose only: the map, `expressionBindableTextKeysFor` and every exported type are byte-identical, `action:button` still answers the empty set, and no accept surface widens.

- The `button` row now cites `form/button.tsx` alone.
- A new docblock section records that the table is keyed on the authored type string, so namespace-prefixed spellings (`action:button`, `ui:button`, the `mcp:` family) answer the empty set by construction rather than by oversight — and that prefix-stripping is the wrong repair, because it would in the same motion grant rows to `element:button` and `page:card`, whose renderers never read these keys at the node's top level.
- `action:button` and `ui:button` are recorded as deliberately out on two measured grounds — zero pull in the objectui corpus, and the module's own admission rule that each row arrives with its own measurement — alongside the unchanged reopen path for a named requirement.
28 changes: 27 additions & 1 deletion content/docs/references/ui/expression-bindable-text-keys.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,7 @@ the evaluated-but-blank half of the objectui#4795 table:
|-------------|----------------------------------------------------------|
| `statistic` | `schema.label`, `schema.value`, `schema.description` (`data-display/statistic.tsx`) |
| `card` | `schema.title`, `schema.description` (`layout/card.tsx`) |
| `button` | `schema.label` (`form/button.tsx`, `action/action-button.tsx`) |
| `button` | `schema.label` (`form/button.tsx`) |

These are the measured motivating cases from objectui#4795 (dashboard
workhorses). Other registered renderers also read keys from this closed set
Expand All@@ -78,6 +78,32 @@ channel as `content` alone and declared `text.value` OUT on those grounds —
this omission is deliberate, not pending measurement. Adding a row is
additive and spec-first; do it here, never as a renderer-side inference.

## Rows are keyed on the AUTHORED type string, prefix and all

`expressionBindableTextKeysFor` does an own-property lookup on the
type string exactly as authored, so a namespace-prefixed spelling —
`action:button`, `ui:button`, the `mcp:` family — is a DIFFERENT key from
the bare name and answers the empty set BY CONSTRUCTION, not by oversight.
Nothing strips a prefix between the two, and nothing should: a stripping
rule would in the same motion grant rows to `element:button` and
`page:card`, whose renderers take their config from the evaluated
`properties` bag and never read these keys at the node's top level.

`action:button` is therefore deliberately OUT, and `ui:button` with it
(objectstack#13672); the `button` row above covers the bare `button`
spelling alone, which is why its citation names `form/button.tsx` only. Two
measured grounds, the same two kinds every other row runs on — zero pull
(the objectui corpus census of 736 JSON documents / 2747 typed nodes finds 5
`action:button` nodes, 0 of them carrying an expression in `label`, and 0
`ui:`-prefixed spellings of any kind), and this module's own admission rule
that a row arrives with its own measurement while a type string with no row
IS the empty set. Not a judgement that the key is inert:
`action/action-button.tsx` renders the author-written `schema.label`
directly (measured 2026-08-31), so the read-back half is real and only the
pull is missing. The reopen path is the ordinary one: a named requirement
for an expression-bound `action:button` label adds a row here, carrying that
row's own measurement.

<Callout type="info">
**Source:** `packages/spec/src/ui/expression-bindable-text-keys.zod.ts`
</Callout>
Expand Down
23 changes: 19 additions & 4 deletions packages/spec/src/ui/expression-bindable-text-keys.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,10 @@ describe('EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT — measured carriage', ()
it('matches the renderer read points measured at the objectui pin', () => {
// data-display/statistic.tsx reads schema.label / .value / .description;
// layout/card.tsx reads schema.title / .description;
// form/button.tsx (and action/action-button.tsx) read schema.label.
// form/button.tsx reads schema.label — and the row covers that BARE
// `button` spelling alone: action/action-button.tsx registers as
// `action:button`, a different key this map deliberately does not
// carry (objectstack#13672; see the module docblock).
expect(EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT).toEqual({
statistic: ['label', 'value', 'description'],
card: ['title', 'description'],
Expand DownExpand Up@@ -110,9 +113,21 @@ describe('expressionBindableTextKeysFor — the mechanical per-type answer', ()

it('answers the empty set for every unlisted type — closed, never inferred', () => {
// `text` binds through its own `content` leg; `element:*` / `page:*`
// config rides the evaluated `properties` bag — none of them get rows
// inferred from what their renderers happen to read.
for (const type of ['text', 'element:text', 'page:card', 'alert', '']) {
// config rides the evaluated `properties` bag; and a namespace-prefixed
// spelling is a DIFFERENT key from the bare name, so `action:button` and
// `ui:button` answer empty by construction — the pair the objectstack#13672
// ruling recorded as deliberately out, pinned here so a later prefix-
// stripping "fix" cannot move the machine face in silence. None of them get
// rows inferred from what their renderers happen to read.
for (const type of [
'text',
'element:text',
'page:card',
'action:button',
'ui:button',
'alert',
'',
]) {
const keys = expressionBindableTextKeysFor(type);
expect(keys).toEqual([]);
expect(Object.isFrozen(keys)).toBe(true);
Expand Down
28 changes: 27 additions & 1 deletion packages/spec/src/ui/expression-bindable-text-keys.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,7 +55,7 @@
* |-------------|----------------------------------------------------------|
* | `statistic` | `schema.label`, `schema.value`, `schema.description` (`data-display/statistic.tsx`) |
* | `card` | `schema.title`, `schema.description` (`layout/card.tsx`) |
* | `button` | `schema.label` (`form/button.tsx`, `action/action-button.tsx`) |
* | `button` | `schema.label` (`form/button.tsx`) |
*
* These are the measured motivating cases from objectui#4795 (dashboard
* workhorses). Other registered renderers also read keys from this closed set
Expand All@@ -73,6 +73,32 @@
* channel as `content` alone and declared `text.value` OUT on those grounds —
* this omission is deliberate, not pending measurement. Adding a row is
* additive and spec-first; do it here, never as a renderer-side inference.
*
* ## Rows are keyed on the AUTHORED type string, prefix and all
*
* {@link expressionBindableTextKeysFor} does an own-property lookup on the
* type string exactly as authored, so a namespace-prefixed spelling —
* `action:button`, `ui:button`, the `mcp:` family — is a DIFFERENT key from
* the bare name and answers the empty set BY CONSTRUCTION, not by oversight.
* Nothing strips a prefix between the two, and nothing should: a stripping
* rule would in the same motion grant rows to `element:button` and
* `page:card`, whose renderers take their config from the evaluated
* `properties` bag and never read these keys at the node's top level.
*
* `action:button` is therefore deliberately OUT, and `ui:button` with it
* (objectstack#13672); the `button` row above covers the bare `button`
* spelling alone, which is why its citation names `form/button.tsx` only. Two
* measured grounds, the same two kinds every other row runs on — zero pull
* (the objectui corpus census of 736 JSON documents / 2747 typed nodes finds 5
* `action:button` nodes, 0 of them carrying an expression in `label`, and 0
* `ui:`-prefixed spellings of any kind), and this module's own admission rule
* that a row arrives with its own measurement while a type string with no row
* IS the empty set. Not a judgement that the key is inert:
* `action/action-button.tsx` renders the author-written `schema.label`
* directly (measured 2026-08-31), so the read-back half is real and only the
* pull is missing. The reopen path is the ordinary one: a named requirement
* for an expression-bound `action:button` label adds a row here, carrying that
* row's own measurement.
*/

import { z } from 'zod';
Expand Down
Loading
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/spotty-buttons-narrow-their-row.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
---
'@objectstack/spec': patch
---

Correct the `EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT` docblock so it stops promising coverage the lookup does not deliver.

The `button` row cited two objectui renderers as its evidence — `form/button.tsx` and `action/action-button.tsx` — but the map is keyed on the type string exactly as authored, and `action-button.tsx` registers under `action:button`, a different key with no row. Prose only: the map, `expressionBindableTextKeysFor` and every exported type are byte-identical, `action:button` still answers the empty set, and no accept surface widens.

- The `button` row now cites `form/button.tsx` alone.
- A new docblock section records that the table is keyed on the authored type string, so namespace-prefixed spellings (`action:button`, `ui:button`, the `mcp:` family) answer the empty set by construction rather than by oversight — and that prefix-stripping is the wrong repair, because it would in the same motion grant rows to `element:button` and `page:card`, whose renderers never read these keys at the node's top level.
- `action:button` and `ui:button` are recorded as deliberately out on two measured grounds — zero pull in the objectui corpus, and the module's own admission rule that each row arrives with its own measurement — alongside the unchanged reopen path for a named requirement.
28 changes: 27 additions & 1 deletion content/docs/references/ui/expression-bindable-text-keys.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,7 @@ the evaluated-but-blank half of the objectui#4795 table:
|-------------|----------------------------------------------------------|
| `statistic` | `schema.label`, `schema.value`, `schema.description` (`data-display/statistic.tsx`) |
| `card` | `schema.title`, `schema.description` (`layout/card.tsx`) |
| `button` | `schema.label` (`form/button.tsx`, `action/action-button.tsx`) |
| `button` | `schema.label` (`form/button.tsx`) |

These are the measured motivating cases from objectui#4795 (dashboard
workhorses). Other registered renderers also read keys from this closed set
Expand All@@ -78,6 +78,32 @@ channel as `content` alone and declared `text.value` OUT on those grounds —
this omission is deliberate, not pending measurement. Adding a row is
additive and spec-first; do it here, never as a renderer-side inference.

## Rows are keyed on the AUTHORED type string, prefix and all

`expressionBindableTextKeysFor` does an own-property lookup on the
type string exactly as authored, so a namespace-prefixed spelling —
`action:button`, `ui:button`, the `mcp:` family — is a DIFFERENT key from
the bare name and answers the empty set BY CONSTRUCTION, not by oversight.
Nothing strips a prefix between the two, and nothing should: a stripping
rule would in the same motion grant rows to `element:button` and
`page:card`, whose renderers take their config from the evaluated
`properties` bag and never read these keys at the node's top level.

`action:button` is therefore deliberately OUT, and `ui:button` with it
(objectstack#13672); the `button` row above covers the bare `button`
spelling alone, which is why its citation names `form/button.tsx` only. Two
measured grounds, the same two kinds every other row runs on — zero pull
(the objectui corpus census of 736 JSON documents / 2747 typed nodes finds 5
`action:button` nodes, 0 of them carrying an expression in `label`, and 0
`ui:`-prefixed spellings of any kind), and this module's own admission rule
that a row arrives with its own measurement while a type string with no row
IS the empty set. Not a judgement that the key is inert:
`action/action-button.tsx` renders the author-written `schema.label`
directly (measured 2026-08-31), so the read-back half is real and only the
pull is missing. The reopen path is the ordinary one: a named requirement
for an expression-bound `action:button` label adds a row here, carrying that
row's own measurement.

<Callout type="info">
**Source:** `packages/spec/src/ui/expression-bindable-text-keys.zod.ts`
</Callout>
Expand Down
23 changes: 19 additions & 4 deletions packages/spec/src/ui/expression-bindable-text-keys.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,10 @@ describe('EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT — measured carriage', ()
it('matches the renderer read points measured at the objectui pin', () => {
// data-display/statistic.tsx reads schema.label / .value / .description;
// layout/card.tsx reads schema.title / .description;
// form/button.tsx (and action/action-button.tsx) read schema.label.
// form/button.tsx reads schema.label — and the row covers that BARE
// `button` spelling alone: action/action-button.tsx registers as
// `action:button`, a different key this map deliberately does not
// carry (objectstack#13672; see the module docblock).
expect(EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT).toEqual({
statistic: ['label', 'value', 'description'],
card: ['title', 'description'],
Expand DownExpand Up@@ -110,9 +113,21 @@ describe('expressionBindableTextKeysFor — the mechanical per-type answer', ()

it('answers the empty set for every unlisted type — closed, never inferred', () => {
// `text` binds through its own `content` leg; `element:*` / `page:*`
// config rides the evaluated `properties` bag — none of them get rows
// inferred from what their renderers happen to read.
for (const type of ['text', 'element:text', 'page:card', 'alert', '']) {
// config rides the evaluated `properties` bag; and a namespace-prefixed
// spelling is a DIFFERENT key from the bare name, so `action:button` and
// `ui:button` answer empty by construction — the pair the objectstack#13672
// ruling recorded as deliberately out, pinned here so a later prefix-
// stripping "fix" cannot move the machine face in silence. None of them get
// rows inferred from what their renderers happen to read.
for (const type of [
'text',
'element:text',
'page:card',
'action:button',
'ui:button',
'alert',
'',
]) {
const keys = expressionBindableTextKeysFor(type);
expect(keys).toEqual([]);
expect(Object.isFrozen(keys)).toBe(true);
Expand Down
28 changes: 27 additions & 1 deletion packages/spec/src/ui/expression-bindable-text-keys.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,7 +55,7 @@
* |-------------|----------------------------------------------------------|
* | `statistic` | `schema.label`, `schema.value`, `schema.description` (`data-display/statistic.tsx`) |
* | `card` | `schema.title`, `schema.description` (`layout/card.tsx`) |
* | `button` | `schema.label` (`form/button.tsx`, `action/action-button.tsx`) |
* | `button` | `schema.label` (`form/button.tsx`) |
*
* These are the measured motivating cases from objectui#4795 (dashboard
* workhorses). Other registered renderers also read keys from this closed set
Expand All@@ -73,6 +73,32 @@
* channel as `content` alone and declared `text.value` OUT on those grounds —
* this omission is deliberate, not pending measurement. Adding a row is
* additive and spec-first; do it here, never as a renderer-side inference.
*
* ## Rows are keyed on the AUTHORED type string, prefix and all
*
* {@link expressionBindableTextKeysFor} does an own-property lookup on the
* type string exactly as authored, so a namespace-prefixed spelling —
* `action:button`, `ui:button`, the `mcp:` family — is a DIFFERENT key from
* the bare name and answers the empty set BY CONSTRUCTION, not by oversight.
* Nothing strips a prefix between the two, and nothing should: a stripping
* rule would in the same motion grant rows to `element:button` and
* `page:card`, whose renderers take their config from the evaluated
* `properties` bag and never read these keys at the node's top level.
*
* `action:button` is therefore deliberately OUT, and `ui:button` with it
* (objectstack#13672); the `button` row above covers the bare `button`
* spelling alone, which is why its citation names `form/button.tsx` only. Two
* measured grounds, the same two kinds every other row runs on — zero pull
* (the objectui corpus census of 736 JSON documents / 2747 typed nodes finds 5
* `action:button` nodes, 0 of them carrying an expression in `label`, and 0
* `ui:`-prefixed spellings of any kind), and this module's own admission rule
* that a row arrives with its own measurement while a type string with no row
* IS the empty set. Not a judgement that the key is inert:
* `action/action-button.tsx` renders the author-written `schema.label`
* directly (measured 2026-08-31), so the read-back half is real and only the
* pull is missing. The reopen path is the ordinary one: a named requirement
* for an expression-bound `action:button` label adds a row here, carrying that
* row's own measurement.
*/

import { z } from 'zod';
Expand Down
Loading
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/spotty-buttons-narrow-their-row.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
---
'@objectstack/spec': patch
---

Correct the `EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT` docblock so it stops promising coverage the lookup does not deliver.

The `button` row cited two objectui renderers as its evidence — `form/button.tsx` and `action/action-button.tsx` — but the map is keyed on the type string exactly as authored, and `action-button.tsx` registers under `action:button`, a different key with no row. Prose only: the map, `expressionBindableTextKeysFor` and every exported type are byte-identical, `action:button` still answers the empty set, and no accept surface widens.

- The `button` row now cites `form/button.tsx` alone.
- A new docblock section records that the table is keyed on the authored type string, so namespace-prefixed spellings (`action:button`, `ui:button`, the `mcp:` family) answer the empty set by construction rather than by oversight — and that prefix-stripping is the wrong repair, because it would in the same motion grant rows to `element:button` and `page:card`, whose renderers never read these keys at the node's top level.
- `action:button` and `ui:button` are recorded as deliberately out on two measured grounds — zero pull in the objectui corpus, and the module's own admission rule that each row arrives with its own measurement — alongside the unchanged reopen path for a named requirement.
28 changes: 27 additions & 1 deletion content/docs/references/ui/expression-bindable-text-keys.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,7 @@ the evaluated-but-blank half of the objectui#4795 table:
|-------------|----------------------------------------------------------|
| `statistic` | `schema.label`, `schema.value`, `schema.description` (`data-display/statistic.tsx`) |
| `card` | `schema.title`, `schema.description` (`layout/card.tsx`) |
| `button` | `schema.label` (`form/button.tsx`, `action/action-button.tsx`) |
| `button` | `schema.label` (`form/button.tsx`) |

These are the measured motivating cases from objectui#4795 (dashboard
workhorses). Other registered renderers also read keys from this closed set
Expand All@@ -78,6 +78,32 @@ channel as `content` alone and declared `text.value` OUT on those grounds —
this omission is deliberate, not pending measurement. Adding a row is
additive and spec-first; do it here, never as a renderer-side inference.

## Rows are keyed on the AUTHORED type string, prefix and all

`expressionBindableTextKeysFor` does an own-property lookup on the
type string exactly as authored, so a namespace-prefixed spelling —
`action:button`, `ui:button`, the `mcp:` family — is a DIFFERENT key from
the bare name and answers the empty set BY CONSTRUCTION, not by oversight.
Nothing strips a prefix between the two, and nothing should: a stripping
rule would in the same motion grant rows to `element:button` and
`page:card`, whose renderers take their config from the evaluated
`properties` bag and never read these keys at the node's top level.

`action:button` is therefore deliberately OUT, and `ui:button` with it
(objectstack#13672); the `button` row above covers the bare `button`
spelling alone, which is why its citation names `form/button.tsx` only. Two
measured grounds, the same two kinds every other row runs on — zero pull
(the objectui corpus census of 736 JSON documents / 2747 typed nodes finds 5
`action:button` nodes, 0 of them carrying an expression in `label`, and 0
`ui:`-prefixed spellings of any kind), and this module's own admission rule
that a row arrives with its own measurement while a type string with no row
IS the empty set. Not a judgement that the key is inert:
`action/action-button.tsx` renders the author-written `schema.label`
directly (measured 2026-08-31), so the read-back half is real and only the
pull is missing. The reopen path is the ordinary one: a named requirement
for an expression-bound `action:button` label adds a row here, carrying that
row's own measurement.

<Callout type="info">
**Source:** `packages/spec/src/ui/expression-bindable-text-keys.zod.ts`
</Callout>
Expand Down
23 changes: 19 additions & 4 deletions packages/spec/src/ui/expression-bindable-text-keys.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,10 @@ describe('EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT — measured carriage', ()
it('matches the renderer read points measured at the objectui pin', () => {
// data-display/statistic.tsx reads schema.label / .value / .description;
// layout/card.tsx reads schema.title / .description;
// form/button.tsx (and action/action-button.tsx) read schema.label.
// form/button.tsx reads schema.label — and the row covers that BARE
// `button` spelling alone: action/action-button.tsx registers as
// `action:button`, a different key this map deliberately does not
// carry (objectstack#13672; see the module docblock).
expect(EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT).toEqual({
statistic: ['label', 'value', 'description'],
card: ['title', 'description'],
Expand DownExpand Up@@ -110,9 +113,21 @@ describe('expressionBindableTextKeysFor — the mechanical per-type answer', ()

it('answers the empty set for every unlisted type — closed, never inferred', () => {
// `text` binds through its own `content` leg; `element:*` / `page:*`
// config rides the evaluated `properties` bag — none of them get rows
// inferred from what their renderers happen to read.
for (const type of ['text', 'element:text', 'page:card', 'alert', '']) {
// config rides the evaluated `properties` bag; and a namespace-prefixed
// spelling is a DIFFERENT key from the bare name, so `action:button` and
// `ui:button` answer empty by construction — the pair the objectstack#13672
// ruling recorded as deliberately out, pinned here so a later prefix-
// stripping "fix" cannot move the machine face in silence. None of them get
// rows inferred from what their renderers happen to read.
for (const type of [
'text',
'element:text',
'page:card',
'action:button',
'ui:button',
'alert',
'',
]) {
const keys = expressionBindableTextKeysFor(type);
expect(keys).toEqual([]);
expect(Object.isFrozen(keys)).toBe(true);
Expand Down
28 changes: 27 additions & 1 deletion packages/spec/src/ui/expression-bindable-text-keys.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,7 +55,7 @@
* |-------------|----------------------------------------------------------|
* | `statistic` | `schema.label`, `schema.value`, `schema.description` (`data-display/statistic.tsx`) |
* | `card` | `schema.title`, `schema.description` (`layout/card.tsx`) |
* | `button` | `schema.label` (`form/button.tsx`, `action/action-button.tsx`) |
* | `button` | `schema.label` (`form/button.tsx`) |
*
* These are the measured motivating cases from objectui#4795 (dashboard
* workhorses). Other registered renderers also read keys from this closed set
Expand All@@ -73,6 +73,32 @@
* channel as `content` alone and declared `text.value` OUT on those grounds —
* this omission is deliberate, not pending measurement. Adding a row is
* additive and spec-first; do it here, never as a renderer-side inference.
*
* ## Rows are keyed on the AUTHORED type string, prefix and all
*
* {@link expressionBindableTextKeysFor} does an own-property lookup on the
* type string exactly as authored, so a namespace-prefixed spelling —
* `action:button`, `ui:button`, the `mcp:` family — is a DIFFERENT key from
* the bare name and answers the empty set BY CONSTRUCTION, not by oversight.
* Nothing strips a prefix between the two, and nothing should: a stripping
* rule would in the same motion grant rows to `element:button` and
* `page:card`, whose renderers take their config from the evaluated
* `properties` bag and never read these keys at the node's top level.
*
* `action:button` is therefore deliberately OUT, and `ui:button` with it
* (objectstack#13672); the `button` row above covers the bare `button`
* spelling alone, which is why its citation names `form/button.tsx` only. Two
* measured grounds, the same two kinds every other row runs on — zero pull
* (the objectui corpus census of 736 JSON documents / 2747 typed nodes finds 5
* `action:button` nodes, 0 of them carrying an expression in `label`, and 0
* `ui:`-prefixed spellings of any kind), and this module's own admission rule
* that a row arrives with its own measurement while a type string with no row
* IS the empty set. Not a judgement that the key is inert:
* `action/action-button.tsx` renders the author-written `schema.label`
* directly (measured 2026-08-31), so the read-back half is real and only the
* pull is missing. The reopen path is the ordinary one: a named requirement
* for an expression-bound `action:button` label adds a row here, carrying that
* row's own measurement.
*/

import { z } from 'zod';
Expand Down
Loading