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
35 changes: 35 additions & 0 deletions .changeset/7014-select-option-spec-claim-correction.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
---
'@object-ui/types': patch
---

Correct three false `@objectstack/spec` alignment claims on field metadata, and pin the
real boundary (objectui#7014).

**No contract change.** No type, schema, export or runtime path moves. What changes is
published JSDoc — the text that reaches your editor tooltips through `.d.ts` — which was
asserting the opposite of what the spec does.

Three doc comments claimed the installed `@objectstack/spec` DECLARES a key that it in
fact **refuses by name**. Measured on `@objectstack/spec@17.2.0`, each paired with a
control that accepts the same payload minus the key:

- `SelectOptionMetadata.description` said it "Aligns `@objectstack/spec`
`SelectOptionSchema.description`". That schema is `.strict()` over exactly
`{label, value, color, default, visibleWhen}`; `description` fails with
`unrecognized_keys`.
- `MarkdownFieldMetadata.rows` and `HtmlFieldMetadata.rows` said `@objectstack/spec`
`FieldSchema.rows` declares the key "authorable on exactly the multiline editor
types". `FieldSchema` refuses `rows` by name on all four of
textarea/markdown/html/richtext.

The keys themselves stay declared and stay consumed — `LookupField` searches an option's
`description` (objectui#6153) and `RichTextField` reads `rows` (objectui#6140). Only the
attribution was wrong, and it mattered in a specific way: `FieldSchema` routes a select
field's `options` through the strict option schema, so authoring `description` on an
option makes `PUT /api/v1/meta/object/:name` fail the **whole field** with a 422
`INVALID_METADATA`. The comments were inviting exactly that write. They now say these are
objectui-side read-model extensions that must never reach authored object metadata.

A new pin (`select-option-spec-extension-7014.test.ts`) asserts the spec's option key set
and each by-name refusal, so if the spec ever adopts one of these names the claim is
re-opened loudly instead of silently becoming true.
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,8 +8,9 @@
* `opt.description` alongside the label, and `recordToOption` emits the same
* key for fetched records. What the card changed is the CONTRACT:
* `SelectOptionMetadata` (the declared type of `LookupFieldMetadata.options`)
* now declares `description?: string`, aligned with `@objectstack/spec`'s
* `SelectOptionSchema.description`, so the fixture below is an ANNOTATED
* now declares `description?: string` as an objectui-side extension - the
* installed `@objectstack/spec` 17.2.0 has no such key and REFUSES it BY NAME
* on `SelectOptionSchema` (objectui#7014) - so the fixture below is an ANNOTATED
* literal — the excess-property check that used to refuse this exact document
* is the compile half of the pin, and the search behaviour is the runtime
* half. Behaviour unchanged by design; the test would have passed before the
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* The objectui-side select-option / editor keys are extensions the spec
* REFUSES BY NAME — pinned so the comments that say so cannot rot (objectui#7014).
*
* Why this exists. `SelectOptionMetadata` (packages/types/src/field-types.ts)
* and the two `rows` declarations beside it each carried a doc comment
* asserting the installed `@objectstack/spec` DECLARES the key:
*
* "Aligns `@objectstack/spec` `SelectOptionSchema.description`"
* "`@objectstack/spec` `FieldSchema.rows` (a positive integer, authorable …)"
*
* Measured on `@objectstack/spec@17.2.0`, all three are false: the spec has no
* such key and rejects it BY NAME. A false canonical claim is not stale
* documentation — it is a planted premise for the next agent, which is the
* whole failure class `scripts/check-spec-symbol-derivation.mjs` exists to
* prevent. That gate could not see these, because it reads only the comment
* block attached to a DECLARATION and validates a citation only at SYMBOL
* granularity; both claims sit on MEMBERS and dangle at the member
* (`SelectOptionSchema` is a live export, `.description` is not a key of it).
*
* The keys themselves are legitimate and consumed — objectui#6153 for the
* option `description` (LookupField searches it), objectui#6140 for `rows`
* (RichTextField reads it). What was wrong was the attribution. So this pin
* asserts the BOUNDARY rather than removing anything: these are read-model
* extensions that must never reach authored object metadata.
*
* Every assertion below pairs the refusal with a CONTROL that accepts the same
* payload minus the key, so a red here means "the key's status changed", never
* "the fixture drifted".
*/

import { describe, it, expect } from 'vitest';
import { SelectOptionSchema as SpecSelectOptionSchema, FieldSchema } from '@objectstack/spec/data';

/** Keys of the spec's select option, as installed. */
const SPEC_OPTION_KEYS = Object.keys(SpecSelectOptionSchema.shape).sort();

/** A valid option — `value` is a system identifier, min length 2. */
const validOption = { label: 'High', value: 'high' } as const;

/** Pull the `unrecognized_keys` issue naming `key`, or undefined. */
const refusedByName = (result: { success: boolean; error?: { issues: readonly any[] } }, key: string) =>
result.success
? undefined
: result.error!.issues.find(
(i) => i.code === 'unrecognized_keys' && (i.keys ?? []).includes(key)
);

describe('spec SelectOptionSchema is the boundary these extensions sit outside', () => {
it('declares exactly the five keys the corrected comments name', () => {
// If the spec ever ADDS `description`/`icon`/`disabled`, this fails and the
// comments in field-types.ts must be re-corrected rather than left stale.
expect(SPEC_OPTION_KEYS).toEqual(['color', 'default', 'label', 'value', 'visibleWhen']);
});

it('accepts the control option', () => {
expect(SpecSelectOptionSchema.safeParse(validOption).success).toBe(true);
});

for (const key of ['description', 'icon', 'disabled'] as const) {
it(`refuses the objectui-only key \`${key}\` BY NAME`, () => {
const res = SpecSelectOptionSchema.safeParse({ ...validOption, [key]: key === 'disabled' ? true : 'x' });
expect(res.success).toBe(false);
expect(refusedByName(res, key), `expected unrecognized_keys naming '${key}'`).toBeDefined();
});
}
});

describe('FieldSchema routes options through that strict schema', () => {
const field = (options: unknown[]) => ({ name: 'status', type: 'select', label: 'Status', options });

it('accepts a field whose options carry only spec keys', () => {
expect(FieldSchema.safeParse(field([validOption])).success).toBe(true);
});

it('fails the WHOLE field when an option carries `description`', () => {
const res = FieldSchema.safeParse(field([{ ...validOption, description: 'help' }]));
expect(res.success).toBe(false);
expect(refusedByName(res, 'description')).toBeDefined();
});
});

describe('FieldSchema refuses `rows` by name on every multiline editor type', () => {
const base = (type: string) => ({ name: 'body', type, label: 'Body' });

for (const type of ['textarea', 'markdown', 'html', 'richtext'] as const) {
it(`control: \`${type}\` without \`rows\` is accepted`, () => {
expect(FieldSchema.safeParse(base(type)).success).toBe(true);
});

it(`\`${type}\` with \`rows\` is refused BY NAME`, () => {
const res = FieldSchema.safeParse({ ...base(type), rows: 4 });
expect(res.success).toBe(false);
expect(refusedByName(res, 'rows'), `expected unrecognized_keys naming 'rows' on ${type}`).toBeDefined();
});
}
});
28 changes: 20 additions & 8 deletions packages/types/src/field-types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -205,9 +205,13 @@ export interface MarkdownFieldMetadata extends BaseFieldMetadata {
* `RichTextField` — the one widget behind the `markdown`/`html`/`richtext`
* registry keys — has always read `rows` off this metadata (default 8) while
* no rich-content type declared it, so the running widget honoured a key an
* annotated literal rejected. Aligns the `TextareaFieldMetadata` precedent
* and `@objectstack/spec` `FieldSchema.rows` (a positive integer, authorable
* on exactly the multiline editor types textarea/markdown/html/richtext).
* annotated literal rejected. Follows the `TextareaFieldMetadata` precedent.
*
* WARNING - NOT a spec key. Measured on the installed `@objectstack/spec`
* 17.2.0: `FieldSchema` REFUSES `rows` BY NAME (`unrecognized_keys`) on all
* four of textarea/markdown/html/richtext, with the same payload minus
* `rows` accepted as the control. It is an objectui render hint and must not
* be written into authored object metadata (objectui#7014).
* The four inert editor keys the same ruling measured (`toolbar`/`preview`/
* `minHeight`/`maxHeight`) stay deliberately undeclared — nothing reads them.
*/
Expand All@@ -224,8 +228,8 @@ export interface HtmlFieldMetadata extends BaseFieldMetadata {
* Height of the INLINE editor, in text rows. Same declaration as
* `MarkdownFieldMetadata.rows` (objectui#6140 Option A ruling — see the
* docblock there): `RichTextField` reads it for all three registry keys it
* serves, and `@objectstack/spec` `FieldSchema.rows` declares it for the
* multiline editor types.
* serves. WARNING - NOT a spec key either; see the measured refusal in the
* docblock there (objectui#7014).
*/
rows?: number;
}
Expand DownExpand Up@@ -320,9 +324,17 @@ export interface SelectOptionMetadata {
* (`opt.description && opt.description.toLowerCase().includes(q)`) and its
* `recordToOption` emits the same key for fetched records — while this type
* never declared it, so the behaviour was real for a key no annotated
* literal could carry. Aligns `@objectstack/spec`
* `SelectOptionSchema.description`; renderers may show it as supporting
* text.
* literal could carry. Renderers may show it as supporting text.
*
* WARNING - objectui-side extension, NOT a spec key. Measured on the
* installed `@objectstack/spec` 17.2.0: `SelectOptionSchema` is `.strict()`
* over exactly `{label, value, color, default, visibleWhen}` and REFUSES
* `description` BY NAME (`unrecognized_keys`), with the same option minus
* the key accepted as the control. `FieldSchema` routes `options` through
* that schema, so writing this key into authored object metadata fails the
* whole field. It lives on the runtime READ model the renderers consume and
* must never reach the metadata payload. Pinned by
* `__tests__/select-option-spec-extension-7014.test.ts` (objectui#7014).
*/
description?: string;
color?: string;
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
35 changes: 35 additions & 0 deletions .changeset/7014-select-option-spec-claim-correction.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
---
'@object-ui/types': patch
---

Correct three false `@objectstack/spec` alignment claims on field metadata, and pin the
real boundary (objectui#7014).

**No contract change.** No type, schema, export or runtime path moves. What changes is
published JSDoc — the text that reaches your editor tooltips through `.d.ts` — which was
asserting the opposite of what the spec does.

Three doc comments claimed the installed `@objectstack/spec` DECLARES a key that it in
fact **refuses by name**. Measured on `@objectstack/spec@17.2.0`, each paired with a
control that accepts the same payload minus the key:

- `SelectOptionMetadata.description` said it "Aligns `@objectstack/spec`
`SelectOptionSchema.description`". That schema is `.strict()` over exactly
`{label, value, color, default, visibleWhen}`; `description` fails with
`unrecognized_keys`.
- `MarkdownFieldMetadata.rows` and `HtmlFieldMetadata.rows` said `@objectstack/spec`
`FieldSchema.rows` declares the key "authorable on exactly the multiline editor
types". `FieldSchema` refuses `rows` by name on all four of
textarea/markdown/html/richtext.

The keys themselves stay declared and stay consumed — `LookupField` searches an option's
`description` (objectui#6153) and `RichTextField` reads `rows` (objectui#6140). Only the
attribution was wrong, and it mattered in a specific way: `FieldSchema` routes a select
field's `options` through the strict option schema, so authoring `description` on an
option makes `PUT /api/v1/meta/object/:name` fail the **whole field** with a 422
`INVALID_METADATA`. The comments were inviting exactly that write. They now say these are
objectui-side read-model extensions that must never reach authored object metadata.

A new pin (`select-option-spec-extension-7014.test.ts`) asserts the spec's option key set
and each by-name refusal, so if the spec ever adopts one of these names the claim is
re-opened loudly instead of silently becoming true.
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,8 +8,9 @@
* `opt.description` alongside the label, and `recordToOption` emits the same
* key for fetched records. What the card changed is the CONTRACT:
* `SelectOptionMetadata` (the declared type of `LookupFieldMetadata.options`)
* now declares `description?: string`, aligned with `@objectstack/spec`'s
* `SelectOptionSchema.description`, so the fixture below is an ANNOTATED
* now declares `description?: string` as an objectui-side extension - the
* installed `@objectstack/spec` 17.2.0 has no such key and REFUSES it BY NAME
* on `SelectOptionSchema` (objectui#7014) - so the fixture below is an ANNOTATED
* literal — the excess-property check that used to refuse this exact document
* is the compile half of the pin, and the search behaviour is the runtime
* half. Behaviour unchanged by design; the test would have passed before the
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* The objectui-side select-option / editor keys are extensions the spec
* REFUSES BY NAME — pinned so the comments that say so cannot rot (objectui#7014).
*
* Why this exists. `SelectOptionMetadata` (packages/types/src/field-types.ts)
* and the two `rows` declarations beside it each carried a doc comment
* asserting the installed `@objectstack/spec` DECLARES the key:
*
* "Aligns `@objectstack/spec` `SelectOptionSchema.description`"
* "`@objectstack/spec` `FieldSchema.rows` (a positive integer, authorable …)"
*
* Measured on `@objectstack/spec@17.2.0`, all three are false: the spec has no
* such key and rejects it BY NAME. A false canonical claim is not stale
* documentation — it is a planted premise for the next agent, which is the
* whole failure class `scripts/check-spec-symbol-derivation.mjs` exists to
* prevent. That gate could not see these, because it reads only the comment
* block attached to a DECLARATION and validates a citation only at SYMBOL
* granularity; both claims sit on MEMBERS and dangle at the member
* (`SelectOptionSchema` is a live export, `.description` is not a key of it).
*
* The keys themselves are legitimate and consumed — objectui#6153 for the
* option `description` (LookupField searches it), objectui#6140 for `rows`
* (RichTextField reads it). What was wrong was the attribution. So this pin
* asserts the BOUNDARY rather than removing anything: these are read-model
* extensions that must never reach authored object metadata.
*
* Every assertion below pairs the refusal with a CONTROL that accepts the same
* payload minus the key, so a red here means "the key's status changed", never
* "the fixture drifted".
*/

import { describe, it, expect } from 'vitest';
import { SelectOptionSchema as SpecSelectOptionSchema, FieldSchema } from '@objectstack/spec/data';

/** Keys of the spec's select option, as installed. */
const SPEC_OPTION_KEYS = Object.keys(SpecSelectOptionSchema.shape).sort();

/** A valid option — `value` is a system identifier, min length 2. */
const validOption = { label: 'High', value: 'high' } as const;

/** Pull the `unrecognized_keys` issue naming `key`, or undefined. */
const refusedByName = (result: { success: boolean; error?: { issues: readonly any[] } }, key: string) =>
result.success
? undefined
: result.error!.issues.find(
(i) => i.code === 'unrecognized_keys' && (i.keys ?? []).includes(key)
);

describe('spec SelectOptionSchema is the boundary these extensions sit outside', () => {
it('declares exactly the five keys the corrected comments name', () => {
// If the spec ever ADDS `description`/`icon`/`disabled`, this fails and the
// comments in field-types.ts must be re-corrected rather than left stale.
expect(SPEC_OPTION_KEYS).toEqual(['color', 'default', 'label', 'value', 'visibleWhen']);
});

it('accepts the control option', () => {
expect(SpecSelectOptionSchema.safeParse(validOption).success).toBe(true);
});

for (const key of ['description', 'icon', 'disabled'] as const) {
it(`refuses the objectui-only key \`${key}\` BY NAME`, () => {
const res = SpecSelectOptionSchema.safeParse({ ...validOption, [key]: key === 'disabled' ? true : 'x' });
expect(res.success).toBe(false);
expect(refusedByName(res, key), `expected unrecognized_keys naming '${key}'`).toBeDefined();
});
}
});

describe('FieldSchema routes options through that strict schema', () => {
const field = (options: unknown[]) => ({ name: 'status', type: 'select', label: 'Status', options });

it('accepts a field whose options carry only spec keys', () => {
expect(FieldSchema.safeParse(field([validOption])).success).toBe(true);
});

it('fails the WHOLE field when an option carries `description`', () => {
const res = FieldSchema.safeParse(field([{ ...validOption, description: 'help' }]));
expect(res.success).toBe(false);
expect(refusedByName(res, 'description')).toBeDefined();
});
});

describe('FieldSchema refuses `rows` by name on every multiline editor type', () => {
const base = (type: string) => ({ name: 'body', type, label: 'Body' });

for (const type of ['textarea', 'markdown', 'html', 'richtext'] as const) {
it(`control: \`${type}\` without \`rows\` is accepted`, () => {
expect(FieldSchema.safeParse(base(type)).success).toBe(true);
});

it(`\`${type}\` with \`rows\` is refused BY NAME`, () => {
const res = FieldSchema.safeParse({ ...base(type), rows: 4 });
expect(res.success).toBe(false);
expect(refusedByName(res, 'rows'), `expected unrecognized_keys naming 'rows' on ${type}`).toBeDefined();
});
}
});
28 changes: 20 additions & 8 deletions packages/types/src/field-types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -205,9 +205,13 @@ export interface MarkdownFieldMetadata extends BaseFieldMetadata {
* `RichTextField` — the one widget behind the `markdown`/`html`/`richtext`
* registry keys — has always read `rows` off this metadata (default 8) while
* no rich-content type declared it, so the running widget honoured a key an
* annotated literal rejected. Aligns the `TextareaFieldMetadata` precedent
* and `@objectstack/spec` `FieldSchema.rows` (a positive integer, authorable
* on exactly the multiline editor types textarea/markdown/html/richtext).
* annotated literal rejected. Follows the `TextareaFieldMetadata` precedent.
*
* WARNING - NOT a spec key. Measured on the installed `@objectstack/spec`
* 17.2.0: `FieldSchema` REFUSES `rows` BY NAME (`unrecognized_keys`) on all
* four of textarea/markdown/html/richtext, with the same payload minus
* `rows` accepted as the control. It is an objectui render hint and must not
* be written into authored object metadata (objectui#7014).
* The four inert editor keys the same ruling measured (`toolbar`/`preview`/
* `minHeight`/`maxHeight`) stay deliberately undeclared — nothing reads them.
*/
Expand All@@ -224,8 +228,8 @@ export interface HtmlFieldMetadata extends BaseFieldMetadata {
* Height of the INLINE editor, in text rows. Same declaration as
* `MarkdownFieldMetadata.rows` (objectui#6140 Option A ruling — see the
* docblock there): `RichTextField` reads it for all three registry keys it
* serves, and `@objectstack/spec` `FieldSchema.rows` declares it for the
* multiline editor types.
* serves. WARNING - NOT a spec key either; see the measured refusal in the
* docblock there (objectui#7014).
*/
rows?: number;
}
Expand DownExpand Up@@ -320,9 +324,17 @@ export interface SelectOptionMetadata {
* (`opt.description && opt.description.toLowerCase().includes(q)`) and its
* `recordToOption` emits the same key for fetched records — while this type
* never declared it, so the behaviour was real for a key no annotated
* literal could carry. Aligns `@objectstack/spec`
* `SelectOptionSchema.description`; renderers may show it as supporting
* text.
* literal could carry. Renderers may show it as supporting text.
*
* WARNING - objectui-side extension, NOT a spec key. Measured on the
* installed `@objectstack/spec` 17.2.0: `SelectOptionSchema` is `.strict()`
* over exactly `{label, value, color, default, visibleWhen}` and REFUSES
* `description` BY NAME (`unrecognized_keys`), with the same option minus
* the key accepted as the control. `FieldSchema` routes `options` through
* that schema, so writing this key into authored object metadata fails the
* whole field. It lives on the runtime READ model the renderers consume and
* must never reach the metadata payload. Pinned by
* `__tests__/select-option-spec-extension-7014.test.ts` (objectui#7014).
*/
description?: string;
color?: string;
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
35 changes: 35 additions & 0 deletions .changeset/7014-select-option-spec-claim-correction.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
---
'@object-ui/types': patch
---

Correct three false `@objectstack/spec` alignment claims on field metadata, and pin the
real boundary (objectui#7014).

**No contract change.** No type, schema, export or runtime path moves. What changes is
published JSDoc — the text that reaches your editor tooltips through `.d.ts` — which was
asserting the opposite of what the spec does.

Three doc comments claimed the installed `@objectstack/spec` DECLARES a key that it in
fact **refuses by name**. Measured on `@objectstack/spec@17.2.0`, each paired with a
control that accepts the same payload minus the key:

- `SelectOptionMetadata.description` said it "Aligns `@objectstack/spec`
`SelectOptionSchema.description`". That schema is `.strict()` over exactly
`{label, value, color, default, visibleWhen}`; `description` fails with
`unrecognized_keys`.
- `MarkdownFieldMetadata.rows` and `HtmlFieldMetadata.rows` said `@objectstack/spec`
`FieldSchema.rows` declares the key "authorable on exactly the multiline editor
types". `FieldSchema` refuses `rows` by name on all four of
textarea/markdown/html/richtext.

The keys themselves stay declared and stay consumed — `LookupField` searches an option's
`description` (objectui#6153) and `RichTextField` reads `rows` (objectui#6140). Only the
attribution was wrong, and it mattered in a specific way: `FieldSchema` routes a select
field's `options` through the strict option schema, so authoring `description` on an
option makes `PUT /api/v1/meta/object/:name` fail the **whole field** with a 422
`INVALID_METADATA`. The comments were inviting exactly that write. They now say these are
objectui-side read-model extensions that must never reach authored object metadata.

A new pin (`select-option-spec-extension-7014.test.ts`) asserts the spec's option key set
and each by-name refusal, so if the spec ever adopts one of these names the claim is
re-opened loudly instead of silently becoming true.
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,8 +8,9 @@
* `opt.description` alongside the label, and `recordToOption` emits the same
* key for fetched records. What the card changed is the CONTRACT:
* `SelectOptionMetadata` (the declared type of `LookupFieldMetadata.options`)
* now declares `description?: string`, aligned with `@objectstack/spec`'s
* `SelectOptionSchema.description`, so the fixture below is an ANNOTATED
* now declares `description?: string` as an objectui-side extension - the
* installed `@objectstack/spec` 17.2.0 has no such key and REFUSES it BY NAME
* on `SelectOptionSchema` (objectui#7014) - so the fixture below is an ANNOTATED
* literal — the excess-property check that used to refuse this exact document
* is the compile half of the pin, and the search behaviour is the runtime
* half. Behaviour unchanged by design; the test would have passed before the
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* The objectui-side select-option / editor keys are extensions the spec
* REFUSES BY NAME — pinned so the comments that say so cannot rot (objectui#7014).
*
* Why this exists. `SelectOptionMetadata` (packages/types/src/field-types.ts)
* and the two `rows` declarations beside it each carried a doc comment
* asserting the installed `@objectstack/spec` DECLARES the key:
*
* "Aligns `@objectstack/spec` `SelectOptionSchema.description`"
* "`@objectstack/spec` `FieldSchema.rows` (a positive integer, authorable …)"
*
* Measured on `@objectstack/spec@17.2.0`, all three are false: the spec has no
* such key and rejects it BY NAME. A false canonical claim is not stale
* documentation — it is a planted premise for the next agent, which is the
* whole failure class `scripts/check-spec-symbol-derivation.mjs` exists to
* prevent. That gate could not see these, because it reads only the comment
* block attached to a DECLARATION and validates a citation only at SYMBOL
* granularity; both claims sit on MEMBERS and dangle at the member
* (`SelectOptionSchema` is a live export, `.description` is not a key of it).
*
* The keys themselves are legitimate and consumed — objectui#6153 for the
* option `description` (LookupField searches it), objectui#6140 for `rows`
* (RichTextField reads it). What was wrong was the attribution. So this pin
* asserts the BOUNDARY rather than removing anything: these are read-model
* extensions that must never reach authored object metadata.
*
* Every assertion below pairs the refusal with a CONTROL that accepts the same
* payload minus the key, so a red here means "the key's status changed", never
* "the fixture drifted".
*/

import { describe, it, expect } from 'vitest';
import { SelectOptionSchema as SpecSelectOptionSchema, FieldSchema } from '@objectstack/spec/data';

/** Keys of the spec's select option, as installed. */
const SPEC_OPTION_KEYS = Object.keys(SpecSelectOptionSchema.shape).sort();

/** A valid option — `value` is a system identifier, min length 2. */
const validOption = { label: 'High', value: 'high' } as const;

/** Pull the `unrecognized_keys` issue naming `key`, or undefined. */
const refusedByName = (result: { success: boolean; error?: { issues: readonly any[] } }, key: string) =>
result.success
? undefined
: result.error!.issues.find(
(i) => i.code === 'unrecognized_keys' && (i.keys ?? []).includes(key)
);

describe('spec SelectOptionSchema is the boundary these extensions sit outside', () => {
it('declares exactly the five keys the corrected comments name', () => {
// If the spec ever ADDS `description`/`icon`/`disabled`, this fails and the
// comments in field-types.ts must be re-corrected rather than left stale.
expect(SPEC_OPTION_KEYS).toEqual(['color', 'default', 'label', 'value', 'visibleWhen']);
});

it('accepts the control option', () => {
expect(SpecSelectOptionSchema.safeParse(validOption).success).toBe(true);
});

for (const key of ['description', 'icon', 'disabled'] as const) {
it(`refuses the objectui-only key \`${key}\` BY NAME`, () => {
const res = SpecSelectOptionSchema.safeParse({ ...validOption, [key]: key === 'disabled' ? true : 'x' });
expect(res.success).toBe(false);
expect(refusedByName(res, key), `expected unrecognized_keys naming '${key}'`).toBeDefined();
});
}
});

describe('FieldSchema routes options through that strict schema', () => {
const field = (options: unknown[]) => ({ name: 'status', type: 'select', label: 'Status', options });

it('accepts a field whose options carry only spec keys', () => {
expect(FieldSchema.safeParse(field([validOption])).success).toBe(true);
});

it('fails the WHOLE field when an option carries `description`', () => {
const res = FieldSchema.safeParse(field([{ ...validOption, description: 'help' }]));
expect(res.success).toBe(false);
expect(refusedByName(res, 'description')).toBeDefined();
});
});

describe('FieldSchema refuses `rows` by name on every multiline editor type', () => {
const base = (type: string) => ({ name: 'body', type, label: 'Body' });

for (const type of ['textarea', 'markdown', 'html', 'richtext'] as const) {
it(`control: \`${type}\` without \`rows\` is accepted`, () => {
expect(FieldSchema.safeParse(base(type)).success).toBe(true);
});

it(`\`${type}\` with \`rows\` is refused BY NAME`, () => {
const res = FieldSchema.safeParse({ ...base(type), rows: 4 });
expect(res.success).toBe(false);
expect(refusedByName(res, 'rows'), `expected unrecognized_keys naming 'rows' on ${type}`).toBeDefined();
});
}
});
28 changes: 20 additions & 8 deletions packages/types/src/field-types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -205,9 +205,13 @@ export interface MarkdownFieldMetadata extends BaseFieldMetadata {
* `RichTextField` — the one widget behind the `markdown`/`html`/`richtext`
* registry keys — has always read `rows` off this metadata (default 8) while
* no rich-content type declared it, so the running widget honoured a key an
* annotated literal rejected. Aligns the `TextareaFieldMetadata` precedent
* and `@objectstack/spec` `FieldSchema.rows` (a positive integer, authorable
* on exactly the multiline editor types textarea/markdown/html/richtext).
* annotated literal rejected. Follows the `TextareaFieldMetadata` precedent.
*
* WARNING - NOT a spec key. Measured on the installed `@objectstack/spec`
* 17.2.0: `FieldSchema` REFUSES `rows` BY NAME (`unrecognized_keys`) on all
* four of textarea/markdown/html/richtext, with the same payload minus
* `rows` accepted as the control. It is an objectui render hint and must not
* be written into authored object metadata (objectui#7014).
* The four inert editor keys the same ruling measured (`toolbar`/`preview`/
* `minHeight`/`maxHeight`) stay deliberately undeclared — nothing reads them.
*/
Expand All@@ -224,8 +228,8 @@ export interface HtmlFieldMetadata extends BaseFieldMetadata {
* Height of the INLINE editor, in text rows. Same declaration as
* `MarkdownFieldMetadata.rows` (objectui#6140 Option A ruling — see the
* docblock there): `RichTextField` reads it for all three registry keys it
* serves, and `@objectstack/spec` `FieldSchema.rows` declares it for the
* multiline editor types.
* serves. WARNING - NOT a spec key either; see the measured refusal in the
* docblock there (objectui#7014).
*/
rows?: number;
}
Expand DownExpand Up@@ -320,9 +324,17 @@ export interface SelectOptionMetadata {
* (`opt.description && opt.description.toLowerCase().includes(q)`) and its
* `recordToOption` emits the same key for fetched records — while this type
* never declared it, so the behaviour was real for a key no annotated
* literal could carry. Aligns `@objectstack/spec`
* `SelectOptionSchema.description`; renderers may show it as supporting
* text.
* literal could carry. Renderers may show it as supporting text.
*
* WARNING - objectui-side extension, NOT a spec key. Measured on the
* installed `@objectstack/spec` 17.2.0: `SelectOptionSchema` is `.strict()`
* over exactly `{label, value, color, default, visibleWhen}` and REFUSES
* `description` BY NAME (`unrecognized_keys`), with the same option minus
* the key accepted as the control. `FieldSchema` routes `options` through
* that schema, so writing this key into authored object metadata fails the
* whole field. It lives on the runtime READ model the renderers consume and
* must never reach the metadata payload. Pinned by
* `__tests__/select-option-spec-extension-7014.test.ts` (objectui#7014).
*/
description?: string;
color?: string;
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
35 changes: 35 additions & 0 deletions .changeset/7014-select-option-spec-claim-correction.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
---
'@object-ui/types': patch
---

Correct three false `@objectstack/spec` alignment claims on field metadata, and pin the
real boundary (objectui#7014).

**No contract change.** No type, schema, export or runtime path moves. What changes is
published JSDoc — the text that reaches your editor tooltips through `.d.ts` — which was
asserting the opposite of what the spec does.

Three doc comments claimed the installed `@objectstack/spec` DECLARES a key that it in
fact **refuses by name**. Measured on `@objectstack/spec@17.2.0`, each paired with a
control that accepts the same payload minus the key:

- `SelectOptionMetadata.description` said it "Aligns `@objectstack/spec`
`SelectOptionSchema.description`". That schema is `.strict()` over exactly
`{label, value, color, default, visibleWhen}`; `description` fails with
`unrecognized_keys`.
- `MarkdownFieldMetadata.rows` and `HtmlFieldMetadata.rows` said `@objectstack/spec`
`FieldSchema.rows` declares the key "authorable on exactly the multiline editor
types". `FieldSchema` refuses `rows` by name on all four of
textarea/markdown/html/richtext.

The keys themselves stay declared and stay consumed — `LookupField` searches an option's
`description` (objectui#6153) and `RichTextField` reads `rows` (objectui#6140). Only the
attribution was wrong, and it mattered in a specific way: `FieldSchema` routes a select
field's `options` through the strict option schema, so authoring `description` on an
option makes `PUT /api/v1/meta/object/:name` fail the **whole field** with a 422
`INVALID_METADATA`. The comments were inviting exactly that write. They now say these are
objectui-side read-model extensions that must never reach authored object metadata.

A new pin (`select-option-spec-extension-7014.test.ts`) asserts the spec's option key set
and each by-name refusal, so if the spec ever adopts one of these names the claim is
re-opened loudly instead of silently becoming true.
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,8 +8,9 @@
* `opt.description` alongside the label, and `recordToOption` emits the same
* key for fetched records. What the card changed is the CONTRACT:
* `SelectOptionMetadata` (the declared type of `LookupFieldMetadata.options`)
* now declares `description?: string`, aligned with `@objectstack/spec`'s
* `SelectOptionSchema.description`, so the fixture below is an ANNOTATED
* now declares `description?: string` as an objectui-side extension - the
* installed `@objectstack/spec` 17.2.0 has no such key and REFUSES it BY NAME
* on `SelectOptionSchema` (objectui#7014) - so the fixture below is an ANNOTATED
* literal — the excess-property check that used to refuse this exact document
* is the compile half of the pin, and the search behaviour is the runtime
* half. Behaviour unchanged by design; the test would have passed before the
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* The objectui-side select-option / editor keys are extensions the spec
* REFUSES BY NAME — pinned so the comments that say so cannot rot (objectui#7014).
*
* Why this exists. `SelectOptionMetadata` (packages/types/src/field-types.ts)
* and the two `rows` declarations beside it each carried a doc comment
* asserting the installed `@objectstack/spec` DECLARES the key:
*
* "Aligns `@objectstack/spec` `SelectOptionSchema.description`"
* "`@objectstack/spec` `FieldSchema.rows` (a positive integer, authorable …)"
*
* Measured on `@objectstack/spec@17.2.0`, all three are false: the spec has no
* such key and rejects it BY NAME. A false canonical claim is not stale
* documentation — it is a planted premise for the next agent, which is the
* whole failure class `scripts/check-spec-symbol-derivation.mjs` exists to
* prevent. That gate could not see these, because it reads only the comment
* block attached to a DECLARATION and validates a citation only at SYMBOL
* granularity; both claims sit on MEMBERS and dangle at the member
* (`SelectOptionSchema` is a live export, `.description` is not a key of it).
*
* The keys themselves are legitimate and consumed — objectui#6153 for the
* option `description` (LookupField searches it), objectui#6140 for `rows`
* (RichTextField reads it). What was wrong was the attribution. So this pin
* asserts the BOUNDARY rather than removing anything: these are read-model
* extensions that must never reach authored object metadata.
*
* Every assertion below pairs the refusal with a CONTROL that accepts the same
* payload minus the key, so a red here means "the key's status changed", never
* "the fixture drifted".
*/

import { describe, it, expect } from 'vitest';
import { SelectOptionSchema as SpecSelectOptionSchema, FieldSchema } from '@objectstack/spec/data';

/** Keys of the spec's select option, as installed. */
const SPEC_OPTION_KEYS = Object.keys(SpecSelectOptionSchema.shape).sort();

/** A valid option — `value` is a system identifier, min length 2. */
const validOption = { label: 'High', value: 'high' } as const;

/** Pull the `unrecognized_keys` issue naming `key`, or undefined. */
const refusedByName = (result: { success: boolean; error?: { issues: readonly any[] } }, key: string) =>
result.success
? undefined
: result.error!.issues.find(
(i) => i.code === 'unrecognized_keys' && (i.keys ?? []).includes(key)
);

describe('spec SelectOptionSchema is the boundary these extensions sit outside', () => {
it('declares exactly the five keys the corrected comments name', () => {
// If the spec ever ADDS `description`/`icon`/`disabled`, this fails and the
// comments in field-types.ts must be re-corrected rather than left stale.
expect(SPEC_OPTION_KEYS).toEqual(['color', 'default', 'label', 'value', 'visibleWhen']);
});

it('accepts the control option', () => {
expect(SpecSelectOptionSchema.safeParse(validOption).success).toBe(true);
});

for (const key of ['description', 'icon', 'disabled'] as const) {
it(`refuses the objectui-only key \`${key}\` BY NAME`, () => {
const res = SpecSelectOptionSchema.safeParse({ ...validOption, [key]: key === 'disabled' ? true : 'x' });
expect(res.success).toBe(false);
expect(refusedByName(res, key), `expected unrecognized_keys naming '${key}'`).toBeDefined();
});
}
});

describe('FieldSchema routes options through that strict schema', () => {
const field = (options: unknown[]) => ({ name: 'status', type: 'select', label: 'Status', options });

it('accepts a field whose options carry only spec keys', () => {
expect(FieldSchema.safeParse(field([validOption])).success).toBe(true);
});

it('fails the WHOLE field when an option carries `description`', () => {
const res = FieldSchema.safeParse(field([{ ...validOption, description: 'help' }]));
expect(res.success).toBe(false);
expect(refusedByName(res, 'description')).toBeDefined();
});
});

describe('FieldSchema refuses `rows` by name on every multiline editor type', () => {
const base = (type: string) => ({ name: 'body', type, label: 'Body' });

for (const type of ['textarea', 'markdown', 'html', 'richtext'] as const) {
it(`control: \`${type}\` without \`rows\` is accepted`, () => {
expect(FieldSchema.safeParse(base(type)).success).toBe(true);
});

it(`\`${type}\` with \`rows\` is refused BY NAME`, () => {
const res = FieldSchema.safeParse({ ...base(type), rows: 4 });
expect(res.success).toBe(false);
expect(refusedByName(res, 'rows'), `expected unrecognized_keys naming 'rows' on ${type}`).toBeDefined();
});
}
});
28 changes: 20 additions & 8 deletions packages/types/src/field-types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -205,9 +205,13 @@ export interface MarkdownFieldMetadata extends BaseFieldMetadata {
* `RichTextField` — the one widget behind the `markdown`/`html`/`richtext`
* registry keys — has always read `rows` off this metadata (default 8) while
* no rich-content type declared it, so the running widget honoured a key an
* annotated literal rejected. Aligns the `TextareaFieldMetadata` precedent
* and `@objectstack/spec` `FieldSchema.rows` (a positive integer, authorable
* on exactly the multiline editor types textarea/markdown/html/richtext).
* annotated literal rejected. Follows the `TextareaFieldMetadata` precedent.
*
* WARNING - NOT a spec key. Measured on the installed `@objectstack/spec`
* 17.2.0: `FieldSchema` REFUSES `rows` BY NAME (`unrecognized_keys`) on all
* four of textarea/markdown/html/richtext, with the same payload minus
* `rows` accepted as the control. It is an objectui render hint and must not
* be written into authored object metadata (objectui#7014).
* The four inert editor keys the same ruling measured (`toolbar`/`preview`/
* `minHeight`/`maxHeight`) stay deliberately undeclared — nothing reads them.
*/
Expand All@@ -224,8 +228,8 @@ export interface HtmlFieldMetadata extends BaseFieldMetadata {
* Height of the INLINE editor, in text rows. Same declaration as
* `MarkdownFieldMetadata.rows` (objectui#6140 Option A ruling — see the
* docblock there): `RichTextField` reads it for all three registry keys it
* serves, and `@objectstack/spec` `FieldSchema.rows` declares it for the
* multiline editor types.
* serves. WARNING - NOT a spec key either; see the measured refusal in the
* docblock there (objectui#7014).
*/
rows?: number;
}
Expand DownExpand Up@@ -320,9 +324,17 @@ export interface SelectOptionMetadata {
* (`opt.description && opt.description.toLowerCase().includes(q)`) and its
* `recordToOption` emits the same key for fetched records — while this type
* never declared it, so the behaviour was real for a key no annotated
* literal could carry. Aligns `@objectstack/spec`
* `SelectOptionSchema.description`; renderers may show it as supporting
* text.
* literal could carry. Renderers may show it as supporting text.
*
* WARNING - objectui-side extension, NOT a spec key. Measured on the
* installed `@objectstack/spec` 17.2.0: `SelectOptionSchema` is `.strict()`
* over exactly `{label, value, color, default, visibleWhen}` and REFUSES
* `description` BY NAME (`unrecognized_keys`), with the same option minus
* the key accepted as the control. `FieldSchema` routes `options` through
* that schema, so writing this key into authored object metadata fails the
* whole field. It lives on the runtime READ model the renderers consume and
* must never reach the metadata payload. Pinned by
* `__tests__/select-option-spec-extension-7014.test.ts` (objectui#7014).
*/
description?: string;
color?: string;
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
35 changes: 35 additions & 0 deletions .changeset/7014-select-option-spec-claim-correction.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
---
'@object-ui/types': patch
---

Correct three false `@objectstack/spec` alignment claims on field metadata, and pin the
real boundary (objectui#7014).

**No contract change.** No type, schema, export or runtime path moves. What changes is
published JSDoc — the text that reaches your editor tooltips through `.d.ts` — which was
asserting the opposite of what the spec does.

Three doc comments claimed the installed `@objectstack/spec` DECLARES a key that it in
fact **refuses by name**. Measured on `@objectstack/spec@17.2.0`, each paired with a
control that accepts the same payload minus the key:

- `SelectOptionMetadata.description` said it "Aligns `@objectstack/spec`
`SelectOptionSchema.description`". That schema is `.strict()` over exactly
`{label, value, color, default, visibleWhen}`; `description` fails with
`unrecognized_keys`.
- `MarkdownFieldMetadata.rows` and `HtmlFieldMetadata.rows` said `@objectstack/spec`
`FieldSchema.rows` declares the key "authorable on exactly the multiline editor
types". `FieldSchema` refuses `rows` by name on all four of
textarea/markdown/html/richtext.

The keys themselves stay declared and stay consumed — `LookupField` searches an option's
`description` (objectui#6153) and `RichTextField` reads `rows` (objectui#6140). Only the
attribution was wrong, and it mattered in a specific way: `FieldSchema` routes a select
field's `options` through the strict option schema, so authoring `description` on an
option makes `PUT /api/v1/meta/object/:name` fail the **whole field** with a 422
`INVALID_METADATA`. The comments were inviting exactly that write. They now say these are
objectui-side read-model extensions that must never reach authored object metadata.

A new pin (`select-option-spec-extension-7014.test.ts`) asserts the spec's option key set
and each by-name refusal, so if the spec ever adopts one of these names the claim is
re-opened loudly instead of silently becoming true.
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,8 +8,9 @@
* `opt.description` alongside the label, and `recordToOption` emits the same
* key for fetched records. What the card changed is the CONTRACT:
* `SelectOptionMetadata` (the declared type of `LookupFieldMetadata.options`)
* now declares `description?: string`, aligned with `@objectstack/spec`'s
* `SelectOptionSchema.description`, so the fixture below is an ANNOTATED
* now declares `description?: string` as an objectui-side extension - the
* installed `@objectstack/spec` 17.2.0 has no such key and REFUSES it BY NAME
* on `SelectOptionSchema` (objectui#7014) - so the fixture below is an ANNOTATED
* literal — the excess-property check that used to refuse this exact document
* is the compile half of the pin, and the search behaviour is the runtime
* half. Behaviour unchanged by design; the test would have passed before the
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* The objectui-side select-option / editor keys are extensions the spec
* REFUSES BY NAME — pinned so the comments that say so cannot rot (objectui#7014).
*
* Why this exists. `SelectOptionMetadata` (packages/types/src/field-types.ts)
* and the two `rows` declarations beside it each carried a doc comment
* asserting the installed `@objectstack/spec` DECLARES the key:
*
* "Aligns `@objectstack/spec` `SelectOptionSchema.description`"
* "`@objectstack/spec` `FieldSchema.rows` (a positive integer, authorable …)"
*
* Measured on `@objectstack/spec@17.2.0`, all three are false: the spec has no
* such key and rejects it BY NAME. A false canonical claim is not stale
* documentation — it is a planted premise for the next agent, which is the
* whole failure class `scripts/check-spec-symbol-derivation.mjs` exists to
* prevent. That gate could not see these, because it reads only the comment
* block attached to a DECLARATION and validates a citation only at SYMBOL
* granularity; both claims sit on MEMBERS and dangle at the member
* (`SelectOptionSchema` is a live export, `.description` is not a key of it).
*
* The keys themselves are legitimate and consumed — objectui#6153 for the
* option `description` (LookupField searches it), objectui#6140 for `rows`
* (RichTextField reads it). What was wrong was the attribution. So this pin
* asserts the BOUNDARY rather than removing anything: these are read-model
* extensions that must never reach authored object metadata.
*
* Every assertion below pairs the refusal with a CONTROL that accepts the same
* payload minus the key, so a red here means "the key's status changed", never
* "the fixture drifted".
*/

import { describe, it, expect } from 'vitest';
import { SelectOptionSchema as SpecSelectOptionSchema, FieldSchema } from '@objectstack/spec/data';

/** Keys of the spec's select option, as installed. */
const SPEC_OPTION_KEYS = Object.keys(SpecSelectOptionSchema.shape).sort();

/** A valid option — `value` is a system identifier, min length 2. */
const validOption = { label: 'High', value: 'high' } as const;

/** Pull the `unrecognized_keys` issue naming `key`, or undefined. */
const refusedByName = (result: { success: boolean; error?: { issues: readonly any[] } }, key: string) =>
result.success
? undefined
: result.error!.issues.find(
(i) => i.code === 'unrecognized_keys' && (i.keys ?? []).includes(key)
);

describe('spec SelectOptionSchema is the boundary these extensions sit outside', () => {
it('declares exactly the five keys the corrected comments name', () => {
// If the spec ever ADDS `description`/`icon`/`disabled`, this fails and the
// comments in field-types.ts must be re-corrected rather than left stale.
expect(SPEC_OPTION_KEYS).toEqual(['color', 'default', 'label', 'value', 'visibleWhen']);
});

it('accepts the control option', () => {
expect(SpecSelectOptionSchema.safeParse(validOption).success).toBe(true);
});

for (const key of ['description', 'icon', 'disabled'] as const) {
it(`refuses the objectui-only key \`${key}\` BY NAME`, () => {
const res = SpecSelectOptionSchema.safeParse({ ...validOption, [key]: key === 'disabled' ? true : 'x' });
expect(res.success).toBe(false);
expect(refusedByName(res, key), `expected unrecognized_keys naming '${key}'`).toBeDefined();
});
}
});

describe('FieldSchema routes options through that strict schema', () => {
const field = (options: unknown[]) => ({ name: 'status', type: 'select', label: 'Status', options });

it('accepts a field whose options carry only spec keys', () => {
expect(FieldSchema.safeParse(field([validOption])).success).toBe(true);
});

it('fails the WHOLE field when an option carries `description`', () => {
const res = FieldSchema.safeParse(field([{ ...validOption, description: 'help' }]));
expect(res.success).toBe(false);
expect(refusedByName(res, 'description')).toBeDefined();
});
});

describe('FieldSchema refuses `rows` by name on every multiline editor type', () => {
const base = (type: string) => ({ name: 'body', type, label: 'Body' });

for (const type of ['textarea', 'markdown', 'html', 'richtext'] as const) {
it(`control: \`${type}\` without \`rows\` is accepted`, () => {
expect(FieldSchema.safeParse(base(type)).success).toBe(true);
});

it(`\`${type}\` with \`rows\` is refused BY NAME`, () => {
const res = FieldSchema.safeParse({ ...base(type), rows: 4 });
expect(res.success).toBe(false);
expect(refusedByName(res, 'rows'), `expected unrecognized_keys naming 'rows' on ${type}`).toBeDefined();
});
}
});
28 changes: 20 additions & 8 deletions packages/types/src/field-types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -205,9 +205,13 @@ export interface MarkdownFieldMetadata extends BaseFieldMetadata {
* `RichTextField` — the one widget behind the `markdown`/`html`/`richtext`
* registry keys — has always read `rows` off this metadata (default 8) while
* no rich-content type declared it, so the running widget honoured a key an
* annotated literal rejected. Aligns the `TextareaFieldMetadata` precedent
* and `@objectstack/spec` `FieldSchema.rows` (a positive integer, authorable
* on exactly the multiline editor types textarea/markdown/html/richtext).
* annotated literal rejected. Follows the `TextareaFieldMetadata` precedent.
*
* WARNING - NOT a spec key. Measured on the installed `@objectstack/spec`
* 17.2.0: `FieldSchema` REFUSES `rows` BY NAME (`unrecognized_keys`) on all
* four of textarea/markdown/html/richtext, with the same payload minus
* `rows` accepted as the control. It is an objectui render hint and must not
* be written into authored object metadata (objectui#7014).
* The four inert editor keys the same ruling measured (`toolbar`/`preview`/
* `minHeight`/`maxHeight`) stay deliberately undeclared — nothing reads them.
*/
Expand All@@ -224,8 +228,8 @@ export interface HtmlFieldMetadata extends BaseFieldMetadata {
* Height of the INLINE editor, in text rows. Same declaration as
* `MarkdownFieldMetadata.rows` (objectui#6140 Option A ruling — see the
* docblock there): `RichTextField` reads it for all three registry keys it
* serves, and `@objectstack/spec` `FieldSchema.rows` declares it for the
* multiline editor types.
* serves. WARNING - NOT a spec key either; see the measured refusal in the
* docblock there (objectui#7014).
*/
rows?: number;
}
Expand DownExpand Up@@ -320,9 +324,17 @@ export interface SelectOptionMetadata {
* (`opt.description && opt.description.toLowerCase().includes(q)`) and its
* `recordToOption` emits the same key for fetched records — while this type
* never declared it, so the behaviour was real for a key no annotated
* literal could carry. Aligns `@objectstack/spec`
* `SelectOptionSchema.description`; renderers may show it as supporting
* text.
* literal could carry. Renderers may show it as supporting text.
*
* WARNING - objectui-side extension, NOT a spec key. Measured on the
* installed `@objectstack/spec` 17.2.0: `SelectOptionSchema` is `.strict()`
* over exactly `{label, value, color, default, visibleWhen}` and REFUSES
* `description` BY NAME (`unrecognized_keys`), with the same option minus
* the key accepted as the control. `FieldSchema` routes `options` through
* that schema, so writing this key into authored object metadata fails the
* whole field. It lives on the runtime READ model the renderers consume and
* must never reach the metadata payload. Pinned by
* `__tests__/select-option-spec-extension-7014.test.ts` (objectui#7014).
*/
description?: string;
color?: string;
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
35 changes: 35 additions & 0 deletions .changeset/7014-select-option-spec-claim-correction.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
---
'@object-ui/types': patch
---

Correct three false `@objectstack/spec` alignment claims on field metadata, and pin the
real boundary (objectui#7014).

**No contract change.** No type, schema, export or runtime path moves. What changes is
published JSDoc — the text that reaches your editor tooltips through `.d.ts` — which was
asserting the opposite of what the spec does.

Three doc comments claimed the installed `@objectstack/spec` DECLARES a key that it in
fact **refuses by name**. Measured on `@objectstack/spec@17.2.0`, each paired with a
control that accepts the same payload minus the key:

- `SelectOptionMetadata.description` said it "Aligns `@objectstack/spec`
`SelectOptionSchema.description`". That schema is `.strict()` over exactly
`{label, value, color, default, visibleWhen}`; `description` fails with
`unrecognized_keys`.
- `MarkdownFieldMetadata.rows` and `HtmlFieldMetadata.rows` said `@objectstack/spec`
`FieldSchema.rows` declares the key "authorable on exactly the multiline editor
types". `FieldSchema` refuses `rows` by name on all four of
textarea/markdown/html/richtext.

The keys themselves stay declared and stay consumed — `LookupField` searches an option's
`description` (objectui#6153) and `RichTextField` reads `rows` (objectui#6140). Only the
attribution was wrong, and it mattered in a specific way: `FieldSchema` routes a select
field's `options` through the strict option schema, so authoring `description` on an
option makes `PUT /api/v1/meta/object/:name` fail the **whole field** with a 422
`INVALID_METADATA`. The comments were inviting exactly that write. They now say these are
objectui-side read-model extensions that must never reach authored object metadata.

A new pin (`select-option-spec-extension-7014.test.ts`) asserts the spec's option key set
and each by-name refusal, so if the spec ever adopts one of these names the claim is
re-opened loudly instead of silently becoming true.
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,8 +8,9 @@
* `opt.description` alongside the label, and `recordToOption` emits the same
* key for fetched records. What the card changed is the CONTRACT:
* `SelectOptionMetadata` (the declared type of `LookupFieldMetadata.options`)
* now declares `description?: string`, aligned with `@objectstack/spec`'s
* `SelectOptionSchema.description`, so the fixture below is an ANNOTATED
* now declares `description?: string` as an objectui-side extension - the
* installed `@objectstack/spec` 17.2.0 has no such key and REFUSES it BY NAME
* on `SelectOptionSchema` (objectui#7014) - so the fixture below is an ANNOTATED
* literal — the excess-property check that used to refuse this exact document
* is the compile half of the pin, and the search behaviour is the runtime
* half. Behaviour unchanged by design; the test would have passed before the
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* The objectui-side select-option / editor keys are extensions the spec
* REFUSES BY NAME — pinned so the comments that say so cannot rot (objectui#7014).
*
* Why this exists. `SelectOptionMetadata` (packages/types/src/field-types.ts)
* and the two `rows` declarations beside it each carried a doc comment
* asserting the installed `@objectstack/spec` DECLARES the key:
*
* "Aligns `@objectstack/spec` `SelectOptionSchema.description`"
* "`@objectstack/spec` `FieldSchema.rows` (a positive integer, authorable …)"
*
* Measured on `@objectstack/spec@17.2.0`, all three are false: the spec has no
* such key and rejects it BY NAME. A false canonical claim is not stale
* documentation — it is a planted premise for the next agent, which is the
* whole failure class `scripts/check-spec-symbol-derivation.mjs` exists to
* prevent. That gate could not see these, because it reads only the comment
* block attached to a DECLARATION and validates a citation only at SYMBOL
* granularity; both claims sit on MEMBERS and dangle at the member
* (`SelectOptionSchema` is a live export, `.description` is not a key of it).
*
* The keys themselves are legitimate and consumed — objectui#6153 for the
* option `description` (LookupField searches it), objectui#6140 for `rows`
* (RichTextField reads it). What was wrong was the attribution. So this pin
* asserts the BOUNDARY rather than removing anything: these are read-model
* extensions that must never reach authored object metadata.
*
* Every assertion below pairs the refusal with a CONTROL that accepts the same
* payload minus the key, so a red here means "the key's status changed", never
* "the fixture drifted".
*/

import { describe, it, expect } from 'vitest';
import { SelectOptionSchema as SpecSelectOptionSchema, FieldSchema } from '@objectstack/spec/data';

/** Keys of the spec's select option, as installed. */
const SPEC_OPTION_KEYS = Object.keys(SpecSelectOptionSchema.shape).sort();

/** A valid option — `value` is a system identifier, min length 2. */
const validOption = { label: 'High', value: 'high' } as const;

/** Pull the `unrecognized_keys` issue naming `key`, or undefined. */
const refusedByName = (result: { success: boolean; error?: { issues: readonly any[] } }, key: string) =>
result.success
? undefined
: result.error!.issues.find(
(i) => i.code === 'unrecognized_keys' && (i.keys ?? []).includes(key)
);

describe('spec SelectOptionSchema is the boundary these extensions sit outside', () => {
it('declares exactly the five keys the corrected comments name', () => {
// If the spec ever ADDS `description`/`icon`/`disabled`, this fails and the
// comments in field-types.ts must be re-corrected rather than left stale.
expect(SPEC_OPTION_KEYS).toEqual(['color', 'default', 'label', 'value', 'visibleWhen']);
});

it('accepts the control option', () => {
expect(SpecSelectOptionSchema.safeParse(validOption).success).toBe(true);
});

for (const key of ['description', 'icon', 'disabled'] as const) {
it(`refuses the objectui-only key \`${key}\` BY NAME`, () => {
const res = SpecSelectOptionSchema.safeParse({ ...validOption, [key]: key === 'disabled' ? true : 'x' });
expect(res.success).toBe(false);
expect(refusedByName(res, key), `expected unrecognized_keys naming '${key}'`).toBeDefined();
});
}
});

describe('FieldSchema routes options through that strict schema', () => {
const field = (options: unknown[]) => ({ name: 'status', type: 'select', label: 'Status', options });

it('accepts a field whose options carry only spec keys', () => {
expect(FieldSchema.safeParse(field([validOption])).success).toBe(true);
});

it('fails the WHOLE field when an option carries `description`', () => {
const res = FieldSchema.safeParse(field([{ ...validOption, description: 'help' }]));
expect(res.success).toBe(false);
expect(refusedByName(res, 'description')).toBeDefined();
});
});

describe('FieldSchema refuses `rows` by name on every multiline editor type', () => {
const base = (type: string) => ({ name: 'body', type, label: 'Body' });

for (const type of ['textarea', 'markdown', 'html', 'richtext'] as const) {
it(`control: \`${type}\` without \`rows\` is accepted`, () => {
expect(FieldSchema.safeParse(base(type)).success).toBe(true);
});

it(`\`${type}\` with \`rows\` is refused BY NAME`, () => {
const res = FieldSchema.safeParse({ ...base(type), rows: 4 });
expect(res.success).toBe(false);
expect(refusedByName(res, 'rows'), `expected unrecognized_keys naming 'rows' on ${type}`).toBeDefined();
});
}
});
28 changes: 20 additions & 8 deletions packages/types/src/field-types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -205,9 +205,13 @@ export interface MarkdownFieldMetadata extends BaseFieldMetadata {
* `RichTextField` — the one widget behind the `markdown`/`html`/`richtext`
* registry keys — has always read `rows` off this metadata (default 8) while
* no rich-content type declared it, so the running widget honoured a key an
* annotated literal rejected. Aligns the `TextareaFieldMetadata` precedent
* and `@objectstack/spec` `FieldSchema.rows` (a positive integer, authorable
* on exactly the multiline editor types textarea/markdown/html/richtext).
* annotated literal rejected. Follows the `TextareaFieldMetadata` precedent.
*
* WARNING - NOT a spec key. Measured on the installed `@objectstack/spec`
* 17.2.0: `FieldSchema` REFUSES `rows` BY NAME (`unrecognized_keys`) on all
* four of textarea/markdown/html/richtext, with the same payload minus
* `rows` accepted as the control. It is an objectui render hint and must not
* be written into authored object metadata (objectui#7014).
* The four inert editor keys the same ruling measured (`toolbar`/`preview`/
* `minHeight`/`maxHeight`) stay deliberately undeclared — nothing reads them.
*/
Expand All@@ -224,8 +228,8 @@ export interface HtmlFieldMetadata extends BaseFieldMetadata {
* Height of the INLINE editor, in text rows. Same declaration as
* `MarkdownFieldMetadata.rows` (objectui#6140 Option A ruling — see the
* docblock there): `RichTextField` reads it for all three registry keys it
* serves, and `@objectstack/spec` `FieldSchema.rows` declares it for the
* multiline editor types.
* serves. WARNING - NOT a spec key either; see the measured refusal in the
* docblock there (objectui#7014).
*/
rows?: number;
}
Expand DownExpand Up@@ -320,9 +324,17 @@ export interface SelectOptionMetadata {
* (`opt.description && opt.description.toLowerCase().includes(q)`) and its
* `recordToOption` emits the same key for fetched records — while this type
* never declared it, so the behaviour was real for a key no annotated
* literal could carry. Aligns `@objectstack/spec`
* `SelectOptionSchema.description`; renderers may show it as supporting
* text.
* literal could carry. Renderers may show it as supporting text.
*
* WARNING - objectui-side extension, NOT a spec key. Measured on the
* installed `@objectstack/spec` 17.2.0: `SelectOptionSchema` is `.strict()`
* over exactly `{label, value, color, default, visibleWhen}` and REFUSES
* `description` BY NAME (`unrecognized_keys`), with the same option minus
* the key accepted as the control. `FieldSchema` routes `options` through
* that schema, so writing this key into authored object metadata fails the
* whole field. It lives on the runtime READ model the renderers consume and
* must never reach the metadata payload. Pinned by
* `__tests__/select-option-spec-extension-7014.test.ts` (objectui#7014).
*/
description?: string;
color?: string;
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
35 changes: 35 additions & 0 deletions .changeset/7014-select-option-spec-claim-correction.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
---
'@object-ui/types': patch
---

Correct three false `@objectstack/spec` alignment claims on field metadata, and pin the
real boundary (objectui#7014).

**No contract change.** No type, schema, export or runtime path moves. What changes is
published JSDoc — the text that reaches your editor tooltips through `.d.ts` — which was
asserting the opposite of what the spec does.

Three doc comments claimed the installed `@objectstack/spec` DECLARES a key that it in
fact **refuses by name**. Measured on `@objectstack/spec@17.2.0`, each paired with a
control that accepts the same payload minus the key:

- `SelectOptionMetadata.description` said it "Aligns `@objectstack/spec`
`SelectOptionSchema.description`". That schema is `.strict()` over exactly
`{label, value, color, default, visibleWhen}`; `description` fails with
`unrecognized_keys`.
- `MarkdownFieldMetadata.rows` and `HtmlFieldMetadata.rows` said `@objectstack/spec`
`FieldSchema.rows` declares the key "authorable on exactly the multiline editor
types". `FieldSchema` refuses `rows` by name on all four of
textarea/markdown/html/richtext.

The keys themselves stay declared and stay consumed — `LookupField` searches an option's
`description` (objectui#6153) and `RichTextField` reads `rows` (objectui#6140). Only the
attribution was wrong, and it mattered in a specific way: `FieldSchema` routes a select
field's `options` through the strict option schema, so authoring `description` on an
option makes `PUT /api/v1/meta/object/:name` fail the **whole field** with a 422
`INVALID_METADATA`. The comments were inviting exactly that write. They now say these are
objectui-side read-model extensions that must never reach authored object metadata.

A new pin (`select-option-spec-extension-7014.test.ts`) asserts the spec's option key set
and each by-name refusal, so if the spec ever adopts one of these names the claim is
re-opened loudly instead of silently becoming true.
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,8 +8,9 @@
* `opt.description` alongside the label, and `recordToOption` emits the same
* key for fetched records. What the card changed is the CONTRACT:
* `SelectOptionMetadata` (the declared type of `LookupFieldMetadata.options`)
* now declares `description?: string`, aligned with `@objectstack/spec`'s
* `SelectOptionSchema.description`, so the fixture below is an ANNOTATED
* now declares `description?: string` as an objectui-side extension - the
* installed `@objectstack/spec` 17.2.0 has no such key and REFUSES it BY NAME
* on `SelectOptionSchema` (objectui#7014) - so the fixture below is an ANNOTATED
* literal — the excess-property check that used to refuse this exact document
* is the compile half of the pin, and the search behaviour is the runtime
* half. Behaviour unchanged by design; the test would have passed before the
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* The objectui-side select-option / editor keys are extensions the spec
* REFUSES BY NAME — pinned so the comments that say so cannot rot (objectui#7014).
*
* Why this exists. `SelectOptionMetadata` (packages/types/src/field-types.ts)
* and the two `rows` declarations beside it each carried a doc comment
* asserting the installed `@objectstack/spec` DECLARES the key:
*
* "Aligns `@objectstack/spec` `SelectOptionSchema.description`"
* "`@objectstack/spec` `FieldSchema.rows` (a positive integer, authorable …)"
*
* Measured on `@objectstack/spec@17.2.0`, all three are false: the spec has no
* such key and rejects it BY NAME. A false canonical claim is not stale
* documentation — it is a planted premise for the next agent, which is the
* whole failure class `scripts/check-spec-symbol-derivation.mjs` exists to
* prevent. That gate could not see these, because it reads only the comment
* block attached to a DECLARATION and validates a citation only at SYMBOL
* granularity; both claims sit on MEMBERS and dangle at the member
* (`SelectOptionSchema` is a live export, `.description` is not a key of it).
*
* The keys themselves are legitimate and consumed — objectui#6153 for the
* option `description` (LookupField searches it), objectui#6140 for `rows`
* (RichTextField reads it). What was wrong was the attribution. So this pin
* asserts the BOUNDARY rather than removing anything: these are read-model
* extensions that must never reach authored object metadata.
*
* Every assertion below pairs the refusal with a CONTROL that accepts the same
* payload minus the key, so a red here means "the key's status changed", never
* "the fixture drifted".
*/

import { describe, it, expect } from 'vitest';
import { SelectOptionSchema as SpecSelectOptionSchema, FieldSchema } from '@objectstack/spec/data';

/** Keys of the spec's select option, as installed. */
const SPEC_OPTION_KEYS = Object.keys(SpecSelectOptionSchema.shape).sort();

/** A valid option — `value` is a system identifier, min length 2. */
const validOption = { label: 'High', value: 'high' } as const;

/** Pull the `unrecognized_keys` issue naming `key`, or undefined. */
const refusedByName = (result: { success: boolean; error?: { issues: readonly any[] } }, key: string) =>
result.success
? undefined
: result.error!.issues.find(
(i) => i.code === 'unrecognized_keys' && (i.keys ?? []).includes(key)
);

describe('spec SelectOptionSchema is the boundary these extensions sit outside', () => {
it('declares exactly the five keys the corrected comments name', () => {
// If the spec ever ADDS `description`/`icon`/`disabled`, this fails and the
// comments in field-types.ts must be re-corrected rather than left stale.
expect(SPEC_OPTION_KEYS).toEqual(['color', 'default', 'label', 'value', 'visibleWhen']);
});

it('accepts the control option', () => {
expect(SpecSelectOptionSchema.safeParse(validOption).success).toBe(true);
});

for (const key of ['description', 'icon', 'disabled'] as const) {
it(`refuses the objectui-only key \`${key}\` BY NAME`, () => {
const res = SpecSelectOptionSchema.safeParse({ ...validOption, [key]: key === 'disabled' ? true : 'x' });
expect(res.success).toBe(false);
expect(refusedByName(res, key), `expected unrecognized_keys naming '${key}'`).toBeDefined();
});
}
});

describe('FieldSchema routes options through that strict schema', () => {
const field = (options: unknown[]) => ({ name: 'status', type: 'select', label: 'Status', options });

it('accepts a field whose options carry only spec keys', () => {
expect(FieldSchema.safeParse(field([validOption])).success).toBe(true);
});

it('fails the WHOLE field when an option carries `description`', () => {
const res = FieldSchema.safeParse(field([{ ...validOption, description: 'help' }]));
expect(res.success).toBe(false);
expect(refusedByName(res, 'description')).toBeDefined();
});
});

describe('FieldSchema refuses `rows` by name on every multiline editor type', () => {
const base = (type: string) => ({ name: 'body', type, label: 'Body' });

for (const type of ['textarea', 'markdown', 'html', 'richtext'] as const) {
it(`control: \`${type}\` without \`rows\` is accepted`, () => {
expect(FieldSchema.safeParse(base(type)).success).toBe(true);
});

it(`\`${type}\` with \`rows\` is refused BY NAME`, () => {
const res = FieldSchema.safeParse({ ...base(type), rows: 4 });
expect(res.success).toBe(false);
expect(refusedByName(res, 'rows'), `expected unrecognized_keys naming 'rows' on ${type}`).toBeDefined();
});
}
});
28 changes: 20 additions & 8 deletions packages/types/src/field-types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -205,9 +205,13 @@ export interface MarkdownFieldMetadata extends BaseFieldMetadata {
* `RichTextField` — the one widget behind the `markdown`/`html`/`richtext`
* registry keys — has always read `rows` off this metadata (default 8) while
* no rich-content type declared it, so the running widget honoured a key an
* annotated literal rejected. Aligns the `TextareaFieldMetadata` precedent
* and `@objectstack/spec` `FieldSchema.rows` (a positive integer, authorable
* on exactly the multiline editor types textarea/markdown/html/richtext).
* annotated literal rejected. Follows the `TextareaFieldMetadata` precedent.
*
* WARNING - NOT a spec key. Measured on the installed `@objectstack/spec`
* 17.2.0: `FieldSchema` REFUSES `rows` BY NAME (`unrecognized_keys`) on all
* four of textarea/markdown/html/richtext, with the same payload minus
* `rows` accepted as the control. It is an objectui render hint and must not
* be written into authored object metadata (objectui#7014).
* The four inert editor keys the same ruling measured (`toolbar`/`preview`/
* `minHeight`/`maxHeight`) stay deliberately undeclared — nothing reads them.
*/
Expand All@@ -224,8 +228,8 @@ export interface HtmlFieldMetadata extends BaseFieldMetadata {
* Height of the INLINE editor, in text rows. Same declaration as
* `MarkdownFieldMetadata.rows` (objectui#6140 Option A ruling — see the
* docblock there): `RichTextField` reads it for all three registry keys it
* serves, and `@objectstack/spec` `FieldSchema.rows` declares it for the
* multiline editor types.
* serves. WARNING - NOT a spec key either; see the measured refusal in the
* docblock there (objectui#7014).
*/
rows?: number;
}
Expand DownExpand Up@@ -320,9 +324,17 @@ export interface SelectOptionMetadata {
* (`opt.description && opt.description.toLowerCase().includes(q)`) and its
* `recordToOption` emits the same key for fetched records — while this type
* never declared it, so the behaviour was real for a key no annotated
* literal could carry. Aligns `@objectstack/spec`
* `SelectOptionSchema.description`; renderers may show it as supporting
* text.
* literal could carry. Renderers may show it as supporting text.
*
* WARNING - objectui-side extension, NOT a spec key. Measured on the
* installed `@objectstack/spec` 17.2.0: `SelectOptionSchema` is `.strict()`
* over exactly `{label, value, color, default, visibleWhen}` and REFUSES
* `description` BY NAME (`unrecognized_keys`), with the same option minus
* the key accepted as the control. `FieldSchema` routes `options` through
* that schema, so writing this key into authored object metadata fails the
* whole field. It lives on the runtime READ model the renderers consume and
* must never reach the metadata payload. Pinned by
* `__tests__/select-option-spec-extension-7014.test.ts` (objectui#7014).
*/
description?: string;
color?: string;
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
35 changes: 35 additions & 0 deletions .changeset/7014-select-option-spec-claim-correction.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
---
'@object-ui/types': patch
---

Correct three false `@objectstack/spec` alignment claims on field metadata, and pin the
real boundary (objectui#7014).

**No contract change.** No type, schema, export or runtime path moves. What changes is
published JSDoc — the text that reaches your editor tooltips through `.d.ts` — which was
asserting the opposite of what the spec does.

Three doc comments claimed the installed `@objectstack/spec` DECLARES a key that it in
fact **refuses by name**. Measured on `@objectstack/spec@17.2.0`, each paired with a
control that accepts the same payload minus the key:

- `SelectOptionMetadata.description` said it "Aligns `@objectstack/spec`
`SelectOptionSchema.description`". That schema is `.strict()` over exactly
`{label, value, color, default, visibleWhen}`; `description` fails with
`unrecognized_keys`.
- `MarkdownFieldMetadata.rows` and `HtmlFieldMetadata.rows` said `@objectstack/spec`
`FieldSchema.rows` declares the key "authorable on exactly the multiline editor
types". `FieldSchema` refuses `rows` by name on all four of
textarea/markdown/html/richtext.

The keys themselves stay declared and stay consumed — `LookupField` searches an option's
`description` (objectui#6153) and `RichTextField` reads `rows` (objectui#6140). Only the
attribution was wrong, and it mattered in a specific way: `FieldSchema` routes a select
field's `options` through the strict option schema, so authoring `description` on an
option makes `PUT /api/v1/meta/object/:name` fail the **whole field** with a 422
`INVALID_METADATA`. The comments were inviting exactly that write. They now say these are
objectui-side read-model extensions that must never reach authored object metadata.

A new pin (`select-option-spec-extension-7014.test.ts`) asserts the spec's option key set
and each by-name refusal, so if the spec ever adopts one of these names the claim is
re-opened loudly instead of silently becoming true.
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,8 +8,9 @@
* `opt.description` alongside the label, and `recordToOption` emits the same
* key for fetched records. What the card changed is the CONTRACT:
* `SelectOptionMetadata` (the declared type of `LookupFieldMetadata.options`)
* now declares `description?: string`, aligned with `@objectstack/spec`'s
* `SelectOptionSchema.description`, so the fixture below is an ANNOTATED
* now declares `description?: string` as an objectui-side extension - the
* installed `@objectstack/spec` 17.2.0 has no such key and REFUSES it BY NAME
* on `SelectOptionSchema` (objectui#7014) - so the fixture below is an ANNOTATED
* literal — the excess-property check that used to refuse this exact document
* is the compile half of the pin, and the search behaviour is the runtime
* half. Behaviour unchanged by design; the test would have passed before the
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* The objectui-side select-option / editor keys are extensions the spec
* REFUSES BY NAME — pinned so the comments that say so cannot rot (objectui#7014).
*
* Why this exists. `SelectOptionMetadata` (packages/types/src/field-types.ts)
* and the two `rows` declarations beside it each carried a doc comment
* asserting the installed `@objectstack/spec` DECLARES the key:
*
* "Aligns `@objectstack/spec` `SelectOptionSchema.description`"
* "`@objectstack/spec` `FieldSchema.rows` (a positive integer, authorable …)"
*
* Measured on `@objectstack/spec@17.2.0`, all three are false: the spec has no
* such key and rejects it BY NAME. A false canonical claim is not stale
* documentation — it is a planted premise for the next agent, which is the
* whole failure class `scripts/check-spec-symbol-derivation.mjs` exists to
* prevent. That gate could not see these, because it reads only the comment
* block attached to a DECLARATION and validates a citation only at SYMBOL
* granularity; both claims sit on MEMBERS and dangle at the member
* (`SelectOptionSchema` is a live export, `.description` is not a key of it).
*
* The keys themselves are legitimate and consumed — objectui#6153 for the
* option `description` (LookupField searches it), objectui#6140 for `rows`
* (RichTextField reads it). What was wrong was the attribution. So this pin
* asserts the BOUNDARY rather than removing anything: these are read-model
* extensions that must never reach authored object metadata.
*
* Every assertion below pairs the refusal with a CONTROL that accepts the same
* payload minus the key, so a red here means "the key's status changed", never
* "the fixture drifted".
*/

import { describe, it, expect } from 'vitest';
import { SelectOptionSchema as SpecSelectOptionSchema, FieldSchema } from '@objectstack/spec/data';

/** Keys of the spec's select option, as installed. */
const SPEC_OPTION_KEYS = Object.keys(SpecSelectOptionSchema.shape).sort();

/** A valid option — `value` is a system identifier, min length 2. */
const validOption = { label: 'High', value: 'high' } as const;

/** Pull the `unrecognized_keys` issue naming `key`, or undefined. */
const refusedByName = (result: { success: boolean; error?: { issues: readonly any[] } }, key: string) =>
result.success
? undefined
: result.error!.issues.find(
(i) => i.code === 'unrecognized_keys' && (i.keys ?? []).includes(key)
);

describe('spec SelectOptionSchema is the boundary these extensions sit outside', () => {
it('declares exactly the five keys the corrected comments name', () => {
// If the spec ever ADDS `description`/`icon`/`disabled`, this fails and the
// comments in field-types.ts must be re-corrected rather than left stale.
expect(SPEC_OPTION_KEYS).toEqual(['color', 'default', 'label', 'value', 'visibleWhen']);
});

it('accepts the control option', () => {
expect(SpecSelectOptionSchema.safeParse(validOption).success).toBe(true);
});

for (const key of ['description', 'icon', 'disabled'] as const) {
it(`refuses the objectui-only key \`${key}\` BY NAME`, () => {
const res = SpecSelectOptionSchema.safeParse({ ...validOption, [key]: key === 'disabled' ? true : 'x' });
expect(res.success).toBe(false);
expect(refusedByName(res, key), `expected unrecognized_keys naming '${key}'`).toBeDefined();
});
}
});

describe('FieldSchema routes options through that strict schema', () => {
const field = (options: unknown[]) => ({ name: 'status', type: 'select', label: 'Status', options });

it('accepts a field whose options carry only spec keys', () => {
expect(FieldSchema.safeParse(field([validOption])).success).toBe(true);
});

it('fails the WHOLE field when an option carries `description`', () => {
const res = FieldSchema.safeParse(field([{ ...validOption, description: 'help' }]));
expect(res.success).toBe(false);
expect(refusedByName(res, 'description')).toBeDefined();
});
});

describe('FieldSchema refuses `rows` by name on every multiline editor type', () => {
const base = (type: string) => ({ name: 'body', type, label: 'Body' });

for (const type of ['textarea', 'markdown', 'html', 'richtext'] as const) {
it(`control: \`${type}\` without \`rows\` is accepted`, () => {
expect(FieldSchema.safeParse(base(type)).success).toBe(true);
});

it(`\`${type}\` with \`rows\` is refused BY NAME`, () => {
const res = FieldSchema.safeParse({ ...base(type), rows: 4 });
expect(res.success).toBe(false);
expect(refusedByName(res, 'rows'), `expected unrecognized_keys naming 'rows' on ${type}`).toBeDefined();
});
}
});
28 changes: 20 additions & 8 deletions packages/types/src/field-types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -205,9 +205,13 @@ export interface MarkdownFieldMetadata extends BaseFieldMetadata {
* `RichTextField` — the one widget behind the `markdown`/`html`/`richtext`
* registry keys — has always read `rows` off this metadata (default 8) while
* no rich-content type declared it, so the running widget honoured a key an
* annotated literal rejected. Aligns the `TextareaFieldMetadata` precedent
* and `@objectstack/spec` `FieldSchema.rows` (a positive integer, authorable
* on exactly the multiline editor types textarea/markdown/html/richtext).
* annotated literal rejected. Follows the `TextareaFieldMetadata` precedent.
*
* WARNING - NOT a spec key. Measured on the installed `@objectstack/spec`
* 17.2.0: `FieldSchema` REFUSES `rows` BY NAME (`unrecognized_keys`) on all
* four of textarea/markdown/html/richtext, with the same payload minus
* `rows` accepted as the control. It is an objectui render hint and must not
* be written into authored object metadata (objectui#7014).
* The four inert editor keys the same ruling measured (`toolbar`/`preview`/
* `minHeight`/`maxHeight`) stay deliberately undeclared — nothing reads them.
*/
Expand All@@ -224,8 +228,8 @@ export interface HtmlFieldMetadata extends BaseFieldMetadata {
* Height of the INLINE editor, in text rows. Same declaration as
* `MarkdownFieldMetadata.rows` (objectui#6140 Option A ruling — see the
* docblock there): `RichTextField` reads it for all three registry keys it
* serves, and `@objectstack/spec` `FieldSchema.rows` declares it for the
* multiline editor types.
* serves. WARNING - NOT a spec key either; see the measured refusal in the
* docblock there (objectui#7014).
*/
rows?: number;
}
Expand DownExpand Up@@ -320,9 +324,17 @@ export interface SelectOptionMetadata {
* (`opt.description && opt.description.toLowerCase().includes(q)`) and its
* `recordToOption` emits the same key for fetched records — while this type
* never declared it, so the behaviour was real for a key no annotated
* literal could carry. Aligns `@objectstack/spec`
* `SelectOptionSchema.description`; renderers may show it as supporting
* text.
* literal could carry. Renderers may show it as supporting text.
*
* WARNING - objectui-side extension, NOT a spec key. Measured on the
* installed `@objectstack/spec` 17.2.0: `SelectOptionSchema` is `.strict()`
* over exactly `{label, value, color, default, visibleWhen}` and REFUSES
* `description` BY NAME (`unrecognized_keys`), with the same option minus
* the key accepted as the control. `FieldSchema` routes `options` through
* that schema, so writing this key into authored object metadata fails the
* whole field. It lives on the runtime READ model the renderers consume and
* must never reach the metadata payload. Pinned by
* `__tests__/select-option-spec-extension-7014.test.ts` (objectui#7014).
*/
description?: string;
color?: string;
Expand Down
Loading