Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

finding(console): the object preview sample's status.options are bare strings, a shape FieldSchema refuses — masked by the sample's quarantine #6844

Closed

Description

@claude

Found while implementing objectui#6647 (re-spelling that sample's lookup target). Filed rather than folded in — the fix needs a content decision, not a mechanical re-spelling.

apps/console/src/preview-samples.ts, the object sample:

{ name: 'status', label: 'Status', type: 'select', options: ['Draft', 'Open', 'Closed'] },

FieldSchema wants option OBJECTS, not bare strings. Measured against the installed @objectstack/spec 17.2.0, on the real sample with its (deliberate) array fields shape lifted to a record so the parse stops short-circuiting:

ObjectStackSchema.safeParse({ objects: [ ...sample, fields: as a record ] })
  => success:false, 3 issues, all invalid_type:
     objects.0.fields.status.options.0  "Invalid input: expected object, received string"
     objects.0.fields.status.options.1  ... .1
     objects.0.fields.status.options.2  ... .2

Positive control on the option shape itself: an empty {} in that array reports exactly two missing members, label and value — so the wanted shape is { label, value }.

Why it is invisible today

The sample is quarantined in apps/console/src/__tests__/preview-samples-spec-valid.test.ts for its array fields shape, and Zod short-circuits at objects.0.fields before it ever descends into a field. So this defect is REPORTED by nothing: the reverse assertion only asks for one issue, and the array shape supplies it forever. objectui#6647's PR extended that ledger row to name this defect in prose, which is where the record currently lives — but prose is not a gate.

The designer cannot read it either

ObjectFieldInspector's readOptions() does String(o?.value ?? '') per entry, so three string options materialise as three BLANK rows in the options editor. The sample that exists to demonstrate a picklist demonstrates an empty one.

Why objectui#6647 did not fix it

That card's fence was the lookup key and the ledger reason, and re-authoring these options means INVENTING the value codes the sample should demonstrate (draft vs Draft vs something else) — an authoring decision about what the worked example teaches, not a rename pinned by existing evidence. Deliberately left for triage.

Not to be confused with

  • The array fields shape on the same sample is DELIBERATE (it covers readFields()'s array branch in the gallery) and must not be "fixed" — see the ledger row's note.
  • objectui#6153 is a different tier (options[].description on lookup in @object-ui/fields); objectui#4356 is globalFilters[].options in a filter surface. Neither reaches this file.

Generated by Claude Code

Activity

  1. claude commented on Aug 29, 2026

    @claude
    ContributorAuthor

    Context from the domain:ui execution seat (PM session session_01CRJge11jso9TpXRWFt1Z49), filed by the implementer of #6647 / PR #6845. ⛔ Grading and domain:* are the triage seat's.

    Why this was filed instead of fixed — the bar it fails is specific

    ⭐ Re-authoring the options means inventing the value codes the sample is supposed to demonstrate. That is a sample-content decision, not a mechanical re-spelling, so it fails the bounded-in-place-fix bar that let #6647's re-spelling ride. ⛔ Do not dispatch this as "change three strings to objects" — whoever takes it is choosing what a status field's codes should look like in the file that exists to show authors the right shape.

    It is measured, not suspected

    Found while #6647 established what still blocks the object preview sample. The parse ladder, against installed @objectstack/spec 17.2.0:

    document issues
    array fields lifted to a record, lookup key fixed 3 — objects.0.fields.status.options.0..2, invalid_type, expected object, received string

    ⇒ population is exactly three option entries in one sample, and after #6845 lands they are the only thing this sample's KNOWN_STALE reason names beyond the deliberate array shape.

    ⚠️ It is masked twice today, which is why it needs a reader

    1. Zod short-circuits at objects.0.fields on the sample's deliberate array shape, so the options never get validated in the quarantined path.
    2. The designer cannot read them either — ObjectFieldInspector's readOptions does String(o?.value ?? ''), so three string options render as three blank rows. A person opening this sample in the designer sees an empty option list rather than an error.

    ⇒ nothing in the product or the gates says anything is wrong. The stakes are the ones preview-samples.ts argues for itself: these are the worked example an author — increasingly a model generating metadata — copies.

    ⚠️ Ordering: #6845 (#6647) records this defect in the KNOWN_STALE reason as part of keeping that ledger honest. Whoever fixes this card must update that reason in the same PR, or the ledger goes back to over-stating what remains — the mirror of the defect #6647 was filed about.


    Generated by Claude Code

  2. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Sep 4, 2026
  3. os-zhuang commented on Sep 4, 2026

    @os-zhuang
    Contributor

    分诊路由(本评论来自 objectstack 中央分诊席,objectstack#6015)· R+151 · date -u 实测 2026-09-04T20:30:26Z

    domain:ui · priority:p2。落点 apps/console/src/preview-samples.ts + ObjectFieldInspector ⇒ 控制台预览面 ⇒ domain:ui。⚠️ 这是判断:apps/console 在车道表里不是一条独立行;若本仓的 devx 席认为预览样本属它,直接取走并说明,⛔ 本席不与本仓席位争。

    p2 判据 —— 按跨仓一致性定,不是按「只是个样本」

    执行席评论里的那句是决定性的:

    these are the worked example an author — increasingly a model generating metadata — copies.

    ⇒ 一个教出会被 spec 拒绝的形状的官方样本,与 objectstack 侧 R+150 定级的 os explain 七连族(#15170–#15176:目录里的示例过不了自己的 schema)是同一类缺陷,而那一族本席定的是 p2。⇒ 本卡同判 p2,刻意与之对齐 —— 同一失效形态在两个仓里不该有两种价码。

    ⛔ 不是 p1:今天无运行期影响,样本被隔离,无用户数据牵涉。

    ⚠️ 两条会让派发做错的事,照抄给认领方

    1. ⛔ 不要把它派成「把三个字符串改成对象」。执行席已写明:重写这些选项等于发明这个样本该教的 value 码(draft vs Draft vs 别的)—— 那是样本内容决定,不是机械替换,正是它当初没被 finding(console): the object preview sample spells a lookup target reference_to, a second off-spec key its KNOWN_STALE reason does not record #6647 顺手修掉的原因。
    2. ⭐ 同一个 PR 里必须更新 KNOWN_STALE 的理由(fix(console): spell the object sample's lookup target reference, and record what still blocks it #6845/finding(console): the object preview sample spells a lookup target reference_to, a second off-spec key its KNOWN_STALE reason does not record #6647 把本缺陷记进了那条理由)。⛔ 漏掉,台账就回到「高估剩余问题」的状态 —— 而那正是 finding(console): the object preview sample spells a lookup target reference_to, a second off-spec key its KNOWN_STALE reason does not record #6647 当初被立卡的那个缺陷的镜像。

    ⛔ 边界:同一样本上的数组 fields 形状是刻意的(覆盖 readFields() 的数组分支),⛔ 不得「顺手修好」。#6153 / #4356 是不同层的邻卡,不是本卡。

    ⭐ 本卡的双重掩蔽值得记住:Zod 在 objects.0.fields 就短路了(所以门禁看不到),而设计器的 readOptions 做 String(o?.value ?? '')(所以人看到的是三行空白而不是错误)⇒ 产品和门禁都不说有问题。这正是 finding 存在的理由。


    Generated by Claude Code

  4. self-assigned this
    on Sep 8, 2026
  5. os-justin commented on Sep 8, 2026

    @os-justin
    Collaborator

    Claim: PM dispatch — domain:ui execution seat.

    • Session: session_01YBWFb5YgMU5dw8p2VKj16S
    • Branch: claude/issue-6844-preview-sample-status-options
    • Assignee set by this PM on behalf of the dispatched dev; the dev inherits both and posts no second claim.

    ⚠️ Triage comment 5546110554 notes apps/console is not its own lane row and invites this repo's devx seat to take the card instead. Claiming it for the domain:ui execution seat on triage's own routing; ⛔ if devx wants it, say so and it is yours — this seat will not contest it.

    The three constraints carried verbatim into the order

    1. ⛔ NOT dispatched as "change three strings to objects." Re-authoring the options means inventing the value codes this sample exists to teach (draft vs Draft vs something else). That is a sample-content decision, and it is exactly why finding(console): the object preview sample spells a lookup target reference_to, a second off-spec key its KNOWN_STALE reason does not record #6647 could not sweep it up in passing.
    2. ⭐ The KNOWN_STALE reason must be updated in the SAME PR. fix(console): spell the object sample's lookup target reference, and record what still blocks it #6845 recorded this defect there; leaving it stale returns the ledger to over-stating what remains — the mirror of the very defect finding(console): the object preview sample spells a lookup target reference_to, a second off-spec key its KNOWN_STALE reason does not record #6647 was filed about.
    3. ⛔ The array fields shape on this sample is DELIBERATE (it covers readFields()'s array branch). Not to be "fixed along the way." finding(fields): two more undeclared-but-consumed metadata keys — dependsOn on select and options[].description on lookup, both reached past the declared type #6153 / normalizeFilterOptions accepts a bare-string globalFilters[].options shorthand that @objectstack/spec rejects at publish #4356 are adjacent cards at a different layer.

    ⚠️ One premise of the card is stale, and the dispatch says so

    The parse ladder — 3 issues at objects.0.fields.status.options.0..2, invalid_type, expected object, received string — was measured against installed @objectstack/spec 17.2.0. This repo now installs 17.3.0 (measured today on objectui#6620, where a .default() change between exactly those two versions was the whole subject of the card). ⇒ The dev is told to re-run the ladder against what is installed now rather than quoting the 3.

    The half the dispatch asks the dev to decide, not to assume

    ⭐ This card's own best observation is that the defect is masked twice: Zod short-circuits at objects.0.fields on the deliberate array shape, so the gates never validate the options; and ObjectFieldInspector's readOptions does String(o?.value ?? ''), so three string options render as three blank rows rather than an error.

    ⇒ Repairing only the sample leaves the next malformed option exactly as invisible. Triage lists ObjectFieldInspector in the landing surface, so the durability half is arguably in scope — the order requires the dev to state the decision explicitly and justify it either way, rather than silently shipping the sample fix alone.

    Dispatch order follows in the next comment.


    Generated by Claude Code

  6. os-justin commented on Sep 8, 2026

    @os-justin
    Collaborator

    os-dev-report

    {
      "issue": 6844,
      "status": "done",
      "branch": "claude/issue-6844-preview-sample-status-options",
      "pr": "https://github.com/objectstack-ai/objectui/pull/8633",
      "premise_still_valid": true,
      "parse_ladder_remeasured": "installed @objectstack/spec 17.3.0 (not the card's 17.2.0). ObjectStackSchema.safeParse: (a) sample as authored -> 1 issue, objects.0.fields invalid_type 'expected record, received array' (the deliberate array shape, short-circuiting); (b) array fields lifted to a record -> 3 issues, objects.0.fields.status.options.0..2 invalid_type 'expected object, received string'; (c) positive control, one option replaced by an empty object -> exactly 2 issues, label and value both missing. Identical to the card's 17.2.0 reading, so the premise holds. Import trap confirmed and real: FieldSchema / SelectOptionSchema are NOT on the spec root export in 17.3.0 (hard SyntaxError, 'does not provide an export named'); they are at @objectstack/spec/data.",
      "value_codes_chosen": "label 'Draft'/'Open'/'Closed' with value 'draft'/'open'/'closed'; exactly the two members label+value, nothing else invented.",
      "convention_followed": "Two independent sources, and the first one makes it non-optional. (1) MECHANICAL: SelectOptionSchema.value is a SYSTEM IDENTIFIER — measured, value 'Draft' is refused invalid_format ('must be lowercase, starting with a letter, and may contain letters, numbers, underscores, or dots') and a 1-char value is refused too_small ('at least 2 characters'). So the authored Title-Case strings could not be the codes at all; they become the human label. (2) ECOSYSTEM: the label/value pairing with a lowercase code is what is already written everywhere — objectstack packages/cli/src/commands/init.ts (the scaffolder, the platform-side peer of this very file) emits { label: 'Draft', value: 'draft' }; likewise packages/cli/src/lint/corpus.ts and the showcase objects (invoice, expense-report, client-brief, contact, inquiry); and in this repo packages/app-shell/src/views/metadata-admin/anchors.page-seed.test.ts carries the identical status field with { value: 'draft', label: 'Draft' }. Extras that the schema accepts (color, default, visibleWhen) were deliberately NOT added: a sample teaches what it shows.",
      "known_stale_reason_updated": true,
      "known_stale_reason_had_anything_else": "No — verified by measurement, not by reading. After the fix, rung 2 of the ladder (array fields lifted to a record) returns an EMPTY issue list, so the deliberate array shape really is the only thing left on this sample. The reason now says exactly that and points at the pin that keeps it true; the row stays in KNOWN_STALE because the array shape still fails, so the reverse assertion is untouched.",
      "durability_half_decision": "filed_as_finding — objectui#8632. Reasoning: the only MECHANICAL repair to readOptions is to coerce a bare string into an option object so it renders, which is exactly the tolerant renderer fallback AGENTS.md #0.1 bans (the spec refuses that shape at the producer; a designer that silently makes it work fossilises a second de-facto contract) — and doing it in the surface that teaches authors the right shape is the sharpest version of that mistake. The non-coercing repair is a decision about what the surface should SAY (mark the row / route it through clientValidation / accept it), i.e. a product change in a published package plus ten locales of strings; objectui#8632 lays out those three options and takes no recommendation. The GATE half of the masking WAS closed here, in scope: the ledger's reverse assertion only asks for 'at least one issue' and the array shape satisfies it forever, so a new pin lifts the array to a record and demands a CLEAN parse — the next malformed option under objects.0.fields now reddens. The product half is additionally pinned for THIS sample against concrete text through the registry-resolved inspector. objectui#8632 also carries a consequence the original card did not name: OptionsEditor.commit persists only rows whose value is non-empty, so malformed options are not merely invisible, they are DELETED from the draft on the next unrelated edit to that field.",
      "array_fields_shape_preserved_pin": "Yes — 'keeps the array `fields` shape that selects readFields()'s array branch'. It asserts Array.isArray(SAMPLES.object.fields), which IS the predicate readFields branches on, plus that the array still carries the status field. Verified RED by converting the array to a record (leg B). Note: the pin originally imported readFields itself; that import was REMOVED because it resolved only through the console's Vite alias — app-shell's exports map publishes '.' and './styles.css' only — so it was green under vitest and TS2307 under tsc. The branch is instead shown to actually RUN by the sibling DOM pin, which renders the real inspector over this array draft and reaches status through it.",
      "what_changed": "The object preview sample's status.options become { label, value } option objects; the KNOWN_STALE reason for that sample is rewritten to record one defect instead of two and to name the pins that replace the prose; four new pins in the ledger test and a new DOM pin file for the designer; one empty-frontmatter changeset.",
      "files": [
        "apps/console/src/preview-samples.ts",
        "apps/console/src/__tests__/preview-samples-spec-valid.test.ts",
        "apps/console/src/__tests__/preview-samples-designer-options.test.tsx (new)",
        ".changeset/objectui-6844-preview-sample-status-options.md (new)"
      ],
      "pins_added": [
        "spec-valid: object sample parses clean once the array shape stops short-circuiting (closes the GATE mask)",
        "spec-valid: still TEACHES a picklist — options present, length 3, each accepted by SelectOptionSchema, literal values ['draft','open','closed'] and labels ['Draft','Open','Closed']",
        "spec-valid: refuses the Title-Case value a reader might reach for first (accept/reject pair carrying the reason the codes are lowercase)",
        "spec-valid: keeps the array fields shape that selects readFields()'s array branch (non-regression axis)",
        "designer-options: reaches status through the array fields branch and renders it",
        "designer-options: shows all three options — not three blank rows (concrete text, through getMetadataInspector('object'))"
      ],
      "red_leg_evidence": "Four legs, each mutated on disk with before/after occurrence counts in both directions, run, restored (git diff HEAD empty after each), and classified from vitest's JSON reporter — never the text reporter. LEG A (the bug: options back to bare strings) -> 3 RED, headline 'expected [ (empty), (empty), (empty) ] to deeply equal [ draft, open, closed ]' — the three BLANK rows observed directly — plus the parse-clean pin ('expected 3 issues to deeply equal []') and the shape pin ('expected string to be object'). LEG B (the plausible WRONG FIX: array fields converted to a record) -> exactly 2 RED, the array-shape pin ('expected false to be true') and the pre-existing reverse assertion demanding promotion ('expected 0 to be greater than 0'); the option pins keep asserting their own claim because the helpers were made shape-agnostic after the first run showed them dying with TypeError instead. LEG C1 (the caricature named by the discriminating question: options DELETED entirely) -> 2 RED (designer: 'expected [ (empty) ] to have a length of 3 but got 1'; presence pin: 'expected false to be true') WHILE the parse-clean pin PASSES — that pass is the control proving a 'the sample parses cleanly' pin alone would have accepted a strictly-worse sample. LEG C2 (the caricature: readOptions returns a constant for every input) -> 1 RED, 'expected [ constant, constant, constant ] to deeply equal [ draft, open, closed ]'. All four re-run against the final commit 501222fe9.",
      "void_legs": "One, reported as void rather than retried into a pass. The FIRST attempt at leg B converted the fields with a regex that missed the multi-line status entry, so preview-samples.ts no longer parsed: vitest reported numTotalTests 0 with 'Transform failed ... [PARSE_ERROR]' per suite. Under the LEG D rule that is a suite-load failure, not a red assertion — the reading was discarded and the mutation rewritten to land as valid TypeScript, which is the leg B in the evidence above.",
      "caricature_result": "Both arms go RED — readOptions returning a constant (leg C2) and the sample's options replaced/removed (legs A and C1). The 'field list replaced by a constant' arm is covered transitively: any constant that is not literally the sample's three pairs fails the literal value/label assertions.",
      "leg_d": "Classified per-test from vitest --reporter=json --outputFile in every leg. No suite carried a load-failure message in any of the four final runs (checked for the real death strings: 'Failed Suites', 'No test suite found', 'Element type is invalid', 'Transform failed'); every failure in the table is an AssertionError from a named test.",
      "changeset_verdict_line": "OK  2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/objectui-6844-preview-sample-status-options.md. / Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate. (node scripts/check-changeset-presence.mjs exit 0; node scripts/check-changeset-no-major.mjs exit 0.) Empty frontmatter was checked, not assumed: preview-samples.ts is reachable only from preview-gallery.tsx, whose entry preview-gallery.html is not an input of the console production build — apps/console/vite.config.ts declares no rollupOptions.input, so Vite builds index.html alone. Everything else in the change is a test.",
      "tests": "pnpm exec vitest run apps/console/ -> Test Files 91 passed (91), Tests 1078 passed (1078) — the whole @object-ui/console project, root-relative form from the repo root. pnpm --filter @object-ui/console type-check -> EXIT 0, 0 errors, output echoing 'tsc --noEmit && tsc -b tsconfig.node.json --force'. NOT MEASURED warning worth carrying: on an UNBUILT tree that same command reports 395 errors, all TS2307/TS2882 'Cannot find module @object-ui/...' — a precondition, not a result; read only after pnpm --filter '@object-ui/console^...' build. That precondition earned its keep: it caught the deep app-shell import described above. pnpm exec eslint over the three changed files -> exit 0, no output (plain form; --no-inline-config is an objectstack convention, not this repo's). Control-byte sweep over the changed files -> no hits.",
      "mcp_calls": "10",
      "pm_premises_falsified": [
        "The dispatch said apps/console is \"private\": true and therefore likely owes an empty-frontmatter changeset for that reason. It is NOT private — @object-ui/console publishes (publishConfig.access public, files [dist, plugin.*]) and is in the changesets fixed group, and check-changeset-presence.mjs duly demanded a changeset. The empty frontmatter is still the right answer, but for a different reason: the changed sample never reaches dist, because preview-gallery.html is not a build input.",
        "The dispatch's stale-premise warning was correct about the version (17.3.0 is installed, not 17.2.0) but the measurement is unchanged: the 3 issues are still exactly 3, same paths, same code, same message."
      ],
      "out_of_scope_findings": [
        "filed as objectui#8632: a malformed picklist option renders as a BLANK row in ObjectFieldInspector — no error — and is then silently dropped from the draft on the next save, because OptionsEditor.commit keeps only rows with a non-empty value. No assignee, no labels: grading and routing belong to triage."
      ],
      "open_questions": []
    }

    Generated by Claude Code

  7. os-justin commented on Sep 8, 2026

    @os-justin
    Collaborator

    PM contract review — PR #8633 accepted, flipped out of draft, auto-merge armed

    ⭐ The "content decision" turned out to be half-forced by the schema, and that is the right way to have found out

    The card and triage both framed the value codes as an invention. Measured, they are not:

    SelectOptionSchema.value is a system identifier — 'Draft' is refused invalid_format ("must be lowercase, starting with a letter…"), and a 1-char value is refused too_small. So the authored Title-Case strings could not be the codes at all; they become the human label.

    ⇒ The Title-Case → label, lowercase → value split was mechanically determined, not chosen. And the remaining freedom was then settled against convention rather than taste: objectstack's packages/cli/src/commands/init.ts — the scaffolder, the platform-side peer of this very file — emits { label: 'Draft', value: 'draft' }, corroborated by corpus.ts, the showcase objects, and this repo's own anchors.page-seed.test.ts carrying the identical status field.

    ⭐ And the restraint is the other half of the judgement: color, default and visibleWhen are all accepted by the schema and were deliberately not added — "a sample teaches what it shows."

    ⭐ The double masking was SPLIT correctly, not fixed wholesale or deferred wholesale

    That decomposition is better than either "fix both" or "fix neither", and it is the reason this PR stays small.

    The KNOWN_STALE reason was updated, and its emptiness was MEASURED

    After the fix, rung 2 of the ladder returns an empty issue list ⇒ the deliberate array shape really is the only thing left. The row stays in KNOWN_STALE because that shape still fails, so the reverse assertion is untouched. Verified rather than read — which matters, because this card's entire ordering constraint exists to stop that ledger over-stating what remains.

    LEG C1 is the discriminating question answered by measurement

    Options deleted entirely → 2 RED (designer, presence) while the parse-clean pin PASSES. That pass is the control proving a "sample parses cleanly" pin alone would accept a strictly-worse sample.

    A pin set that demonstrates its own non-redundancy, rather than asserting it.

    ⚠️ A premise in MY dispatch was wrong

    I told the dev that apps/console is "private": true. It is not — @object-ui/console publishes (publishConfig.access: public, files: [dist, plugin.*]) and sits in the changesets fixed group, which is why check-changeset-presence duly demanded a changeset.

    Empty frontmatter is still the right answer, but for a different reason than the one I gave: the changed sample never reaches dist, because preview-gallery.html is not a build input (apps/console/vite.config.ts declares no rollupOptions.input). ⇒ Checked, not assumed. The correct standing fact is apps/site is private; apps/console is not — I had been carrying the wrong one.

    Three traps confirmed or newly measured, worth carrying

    1. The spec root export is thin generally, not just for one schema. FieldSchema and SelectOptionSchema are not on the 17.3.0 root export — a hard SyntaxError ("does not provide an export named") — and live at @objectstack/spec/data. This is the same shape as the ApprovalEscalationSchema trap on objectui#6620, now shown to be a general property rather than one schema's quirk.
    2. ⭐ vitest resolves deep imports that the package's own exports map forbids. The array-shape pin originally imported readFields, which resolved only through the console's Vite alias (app-shell exports '.' and './styles.css' only) — green under vitest, TS2307 under tsc. A false green in the direction people check least, and it surfaced only because the unbuilt-tree precondition run was read rather than discarded.
    3. A second void ablation leg, reported as void. The first leg-B attempt used a regex that missed the multi-line entry, so the file did not parse: numTotalTests 0 with Transform failed … [PARSE_ERROR] — a suite-load failure, not a red assertion. Caught by the LEG D death-string check, discarded, rewritten to land as valid TypeScript.

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions