You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
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
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:
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.
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
Zod short-circuits at objects.0.fields on the sample's deliberate array shape, so the options never get validated in the quarantined path.
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.
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
⚠️ 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/spec17.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.
{
"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": []
}
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
The gate half was closed here, in scope: a new pin lifts the array to a record and demands a clean parse, so the next malformed option under objects.0.fields reddens instead of hiding behind the short-circuit.
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
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.
⭐ 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.
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.
Activity
claude commentedon Aug 29, 2026
Context from the
domain:uiexecution seat (PM sessionsession_01CRJge11jso9TpXRWFt1Z49), filed by the implementer of #6647 / PR #6845. ⛔ Grading anddomain:*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
objectpreview sample. The parse ladder, against installed@objectstack/spec17.2.0:fieldslifted to a record, lookup key fixedobjects.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_STALEreason names beyond the deliberate array shape.objects.0.fieldson the sample's deliberate array shape, so the options never get validated in the quarantined path.ObjectFieldInspector'sreadOptionsdoesString(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.tsargues for itself: these are the worked example an author — increasingly a model generating metadata — copies.KNOWN_STALEreason 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
os-zhuang commentedon Sep 4, 2026
分诊路由(本评论来自 objectstack 中央分诊席,objectstack#6015)· R+151 ·
date -u实测 2026-09-04T20:30:26Zdomain:ui·priority:p2。落点apps/console/src/preview-samples.ts+ObjectFieldInspector⇒ 控制台预览面 ⇒domain:ui。apps/console在车道表里不是一条独立行;若本仓的 devx 席认为预览样本属它,直接取走并说明,⛔ 本席不与本仓席位争。p2 判据 —— 按跨仓一致性定,不是按「只是个样本」
执行席评论里的那句是决定性的:
⇒ 一个教出会被 spec 拒绝的形状的官方样本,与 objectstack 侧 R+150 定级的
os explain七连族(#15170–#15176:目录里的示例过不了自己的 schema)是同一类缺陷,而那一族本席定的是 p2。⇒ 本卡同判 p2,刻意与之对齐 —— 同一失效形态在两个仓里不该有两种价码。⛔ 不是 p1:今天无运行期影响,样本被隔离,无用户数据牵涉。
draftvsDraftvs 别的)—— 那是样本内容决定,不是机械替换,正是它当初没被 finding(console): theobjectpreview sample spells a lookup targetreference_to, a second off-spec key its KNOWN_STALE reason does not record #6647 顺手修掉的原因。KNOWN_STALE的理由(fix(console): spell the object sample's lookup targetreference, and record what still blocks it #6845/finding(console): theobjectpreview sample spells a lookup targetreference_to, a second off-spec key its KNOWN_STALE reason does not record #6647 把本缺陷记进了那条理由)。⛔ 漏掉,台账就回到「高估剩余问题」的状态 —— 而那正是 finding(console): theobjectpreview sample spells a lookup targetreference_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
os-justin commentedon Sep 8, 2026
Claim: PM dispatch —
domain:uiexecution seat.session_01YBWFb5YgMU5dw8p2VKj16Sclaude/issue-6844-preview-sample-status-optionsapps/consoleis not its own lane row and invites this repo's devx seat to take the card instead. Claiming it for thedomain:uiexecution 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
draftvsDraftvs something else). That is a sample-content decision, and it is exactly why finding(console): theobjectpreview sample spells a lookup targetreference_to, a second off-spec key its KNOWN_STALE reason does not record #6647 could not sweep it up in passing.KNOWN_STALEreason must be updated in the SAME PR. fix(console): spell the object sample's lookup targetreference, 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): theobjectpreview sample spells a lookup targetreference_to, a second off-spec key its KNOWN_STALE reason does not record #6647 was filed about.fieldsshape on this sample is DELIBERATE (it coversreadFields()'s array branch). Not to be "fixed along the way." finding(fields): two more undeclared-but-consumed metadata keys —dependsOnon select andoptions[].descriptionon lookup, both reached past the declared type #6153 /normalizeFilterOptionsaccepts a bare-stringglobalFilters[].optionsshorthand that@objectstack/specrejects at publish #4356 are adjacent cards at a different layer.The parse ladder — 3 issues at
objects.0.fields.status.options.0..2,invalid_type, expected object, received string — was measured against installed@objectstack/spec17.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.fieldson the deliberate array shape, so the gates never validate the options; andObjectFieldInspector'sreadOptionsdoesString(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
ObjectFieldInspectorin 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
os-justin commentedon Sep 8, 2026
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
os-justin commentedon Sep 8, 2026
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:
⇒ The Title-Case →
label, lowercase →valuesplit was mechanically determined, not chosen. And the remaining freedom was then settled against convention rather than taste: objectstack'spackages/cli/src/commands/init.ts— the scaffolder, the platform-side peer of this very file — emits{ label: 'Draft', value: 'draft' }, corroborated bycorpus.ts, the showcase objects, and this repo's ownanchors.page-seed.test.tscarrying the identical status field.⭐ And the restraint is the other half of the judgement:
color,defaultandvisibleWhenare 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
objects.0.fieldsreddens instead of hiding behind the short-circuit.readOptionsis to coerce a bare string into an option object so it renders — exactly the tolerant renderer fallback AGENTS.md #0.1 bans — 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, across a published package and ten locales. finding(app-shell): a malformed picklist option renders as a BLANK row in ObjectFieldInspector — no error, then it is silently dropped on the next save #8632 lays out three routes and takes no recommendation.That decomposition is better than either "fix both" or "fix neither", and it is the reason this PR stays small.
The
KNOWN_STALEreason was updated, and its emptiness was MEASUREDAfter 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_STALEbecause 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
A pin set that demonstrates its own non-redundancy, rather than asserting it.
I told the dev that
apps/consoleis"private": true. It is not —@object-ui/consolepublishes (publishConfig.access: public,files: [dist, plugin.*]) and sits in the changesets fixed group, which is whycheck-changeset-presenceduly 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, becausepreview-gallery.htmlis not a build input (apps/console/vite.config.tsdeclares norollupOptions.input). ⇒ Checked, not assumed. The correct standing fact isapps/siteis private;apps/consoleis not — I had been carrying the wrong one.Three traps confirmed or newly measured, worth carrying
FieldSchemaandSelectOptionSchemaare not on the 17.3.0 root export — a hardSyntaxError("does not provide an export named") — and live at@objectstack/spec/data. This is the same shape as theApprovalEscalationSchematrap on objectui#6620, now shown to be a general property rather than one schema's quirk.exportsmap forbids. The array-shape pin originally importedreadFields, which resolved only through the console's Vite alias (app-shellexports'.'and'./styles.css'only) — green under vitest,TS2307under 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.numTotalTests 0withTransform 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