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.

Both field designers author placeholder, a key FieldSchema rejects by name — but the designer's own preview renders it, so this is a contract question not a deletion #4676

Closed

Description

@yinlianghui

Found while implementing #4644 (retire the Indexed toggle); filed unassigned rather than folded into that PR, because the correct form is not mechanically pinned the way indexed's was. Backlink: PR #4675.

Measured

Against the @objectstack/spec 17.0.0-rc.6 installed in this repo, probing FieldSchema.safeParse({ name:'foo', label:'Foo', type:'text', }):

key verdict
(control, no extra key) success
indexed unrecognized_keys — handled by #4644
placeholder unrecognized_keys
externalId success
trackHistory success
hidden, readonly, unique success

The placeholder rejection carries its own prescription:

Unrecognized key(s) on this field: `placeholder`.
  • never a FieldSchema key. Author hint text through `inlineHelpText` or
    `description`. Until #4001 closed this shape these were dropped silently —
    the field was still created, minus whatever the key was meant to
    constrain, protect or compute.

So the same 422 (INVALID_METADATA) that #4644 documents for indexed is reachable by typing into the Placeholder box, on the same PUT /api/v1/meta/object/:name route.

Where it is authored

Both field designers, plus the surrounding write paths:

  • packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx — the designer.field.placeholder text field in the ADVANCED section, patchDef({ placeholder: … })
  • packages/plugin-designer/src/FieldDesigner.tsx — the placeholder entry in the type-specific section
  • packages/plugin-designer/src/MetadataFieldsPage.tsx — toDesignerField / fromDesignerField
  • packages/app-shell/src/services/MetadataService.ts — FieldMetadataPayload.placeholder, toFieldPayload
  • packages/app-shell/src/utils/metadataConverters.ts — MetadataField.placeholder, toFieldDefinition
  • packages/types/src/designer.ts — DesignerFieldDefinition.placeholder
  • label keys: designer.field.placeholder, appDesigner.fieldDesigner.placeholder, and placeholder in all ten locale packs

Why this is NOT the #4644 treatment

indexed was inert everywhere in this repo — nothing read it, so retiring the control cost no capability. placeholder is different: it is genuinely consumed by the designer's own form preview.

packages/app-shell/src/views/metadata-admin/previews/object-fields-bridge.ts maps it in and back out:

// buildFieldsBridge
placeholder: typeof def?.placeholder === 'string' ? def.placeholder : undefined,
// serializeDesignerField
if (f.placeholder) out.placeholder = f.placeholder;

so an author who types a placeholder sees it render in the preview, and only discovers on save that it cannot be persisted. Deleting the control therefore removes a visibly-working affordance, which is a product decision rather than a contract clean-up.

The options (not decided here)

Deciding needs a measurement this card does not have: whether anything outside the designer preview applies a field-level placeholder at render time (the form/field renderers in packages/fields, packages/plugin-form), and whether any example app or real deployment authors one.

Whatever is chosen, the stale-draft half of #4644 applies identically: objects already carrying placeholder keep failing every save until the key is stripped on load.

Refs: #4644 (sibling retirement, same ADVANCED section), PR #4675 (where this was measured), objectstack#4001 (silent drop → loud rejection), objectstack#2377.


2026-08-16 ruling: Option C (see ruling comment) — the producer moves; implementation filed at destination.

Blocked-by: objectstack-ai/objectstack#9019


Generated by Claude Code

Activity

  1. added
    bugSomething isn't working
    and removed
    bugSomething isn't working
    on Aug 15, 2026
  2. added theissue type on Aug 15, 2026
  3. yinlianghui commented on Aug 15, 2026

    @yinlianghui
    ContributorAuthor

    First-touch grading (seat repo:objectui; session session_01RnQd8iMMUwXQEV1crFmQiQ, 2026-08-15): pm:queue, type Bug — the 422 trap is reachable by typing into a shipped control, same route and refusal shape as #4644, and correctly filed as its own card because the preview-consumption fact changes the class.

    Conditional ruling (delegated; maintainer veto window OPEN), measurement first: the dispatch measures whether anything outside the designer's own preview applies a field-level placeholder at render time — the form/field renderers (packages/fields, packages/plugin-form), example apps, catalog schemas.

    Not dispatched yet — in-flight at the concurrency cap; queued. Whatever is ruled, the stale-draft half applies identically, as the card records.


    Generated by Claude Code

  4. self-assigned this
    on Aug 15, 2026
  5. yinlianghui commented on Aug 15, 2026

    @yinlianghui
    ContributorAuthor

    Claim: PM loop round 8
    Session: session_01RnQd8iMMUwFQEV1crFmQiQ
    Branch: claude/issue-4676-placeholder-key
    Worktree: objectui-issue-4676
    Domain: repo:objectui
    File surface (conditional on the ruled measurement): read-only measurement across packages/fields, packages/plugin-form, examples/catalog first; if Option A arms — packages/app-shell/src/views/metadata-admin/ (inspector + object-fields-io.ts RETIRED_FIELD_KEYS), packages/plugin-designer/ (FieldDesigner + MetadataFieldsPage), MetadataService.ts, metadataConverters.ts, packages/types/src/designer.ts, i18n label keys (ten packs) + tests — the #4644 / PR #4675 surface, now free (merged) (stop on breach; explain in the report)
    Container & model: S–M, mode:subagent, model: opus
    Serial constraints cleared: #4644's PR #4675 is MERGED, so the metadata-admin/plugin-designer surface is unheld; #4672 holds plugin-charts cartesian click, #4673 holds core chart-series — disjoint; no open dev PR touches the field designers (checked this round).


    Generated by Claude Code

  6. yinlianghui commented on Aug 15, 2026

    @yinlianghui
    ContributorAuthor

    Correction to the claim comment above: the session ID contains a typo — it reads session_01RnQd8iMMUwFQEV1crFmQiQ but the claiming seat session is session_01RnQd8iMMUwXQEV1crFmQiQ (the same session that posted every other comment in this thread). Branch, worktree, and surface are unchanged.


    Generated by Claude Code

  7. yinlianghui commented on Aug 15, 2026

    @yinlianghui
    ContributorAuthor

    Measurement verdict: a real render consumer exists OUTSIDE the designer preview. Option A does NOT arm. Per the conditional ruling (comment 5301215867), this stops here — no retirement, no partial edits, no PR.

    {
      "issue": 4676,
      "status": "needs_decision",
      "branch": "claude/issue-4676-placeholder-key (created, no commits, torn down — nothing pushed)",
      "pr": null,
      "premise_still_valid": true,
      "summary": "Premise A verified on origin/main @ 9ce096fb0: FieldSchema (@objectstack/spec 17.0.0-rc.6, exported from @objectstack/spec/data) still rejects `placeholder` with unrecognized_keys and the prescription 'never a FieldSchema key. Author hint text through inlineHelpText or description', and both designers still author it (ObjectFieldInspector.tsx:676-678 patchDef, FieldDesigner.tsx:314 + 220/241/349, MetadataFieldsPage.tsx:94/144, types/src/designer.ts:781). Premise C — the decisive one — REFUTES the zero-read-points branch. An OBJECT-FIELD-level placeholder is read and applied at render time in FOUR packages plus apps/console, none of them the designer preview: plugin-form builds it into every auto-generated and sectioned form (ObjectForm.tsx:601, sectionFields.ts:122 fromObjectSchema, ModalForm.tsx:425, DrawerForm.tsx:350 — all reading objectSchema.fields[name]); plugin-detail/RecordDetailDrawer.tsx:253 maps def.placeholder off objectSchema.fields into DetailView inline edit; app-shell/resolveActionParams.ts:512 has a field-backed action param INHERIT field.placeholder, documented as intended in that module's header ('Field-backed params inherit label, type, options, placeholder, and help text from the object's field definition'); apps/console/FormPage.tsx:343 resolves override.placeholder ?? def.placeholder from ObjectSchemaPayload.fields and renders it at :830. Underneath, the @object-ui/fields widgets read field.placeholder straight off the metadata carrier (TextField, TextAreaField and peers), because every one of those hosts also passes `field: field` — the raw object field definition — as the widget's single metadata carrier. So this is NOT #4644's inert key: it is a broad, deliberate, multi-package renderer capability whose producer the spec refuses. That is a producer/consumer contract split, which is Options B/C territory and above the delegation gate. The honest counterweight for the maintainer: on the ObjectStack backend path these consumers are STARVED, because PUT /api/v1/meta/object/:name 422s the key, so no server-stored object field can carry it; ObjectUI is however backend-agnostic (AGENTS.md section 1) and data-objectstack passes served metadata through untouched (pinned by getObjectSchema.test.ts), so any non-ObjectStack DataSource supplying its own object schema feeds these read points live today.",
      "tests": "No production code changed, so no gate union is owed; everything below is measurement run against an unmodified origin/main @ 9ce096fb0 with two THROWAWAY probes, both deleted afterwards (worktree verified clean, `git status --porcelain` empty, `git log origin/main..HEAD` empty, then torn down). All heavy runs wrapped in `flock -E 99 -w 540 /tmp/os-heavy-verify.lock` with NODE_OPTIONS=--max-old-space-size=4096, vitest --maxWorkers=2, invoked from the repo root per AGENTS.md. (1) Spec probe, `node --input-type=module` in the worktree: control {name,label,type:'text'} => SUCCESS; +indexed => FAIL unrecognized_keys; +placeholder => FAIL unrecognized_keys; +inlineHelpText => SUCCESS; +description => SUCCESS; +externalId => SUCCESS — i.e. the card's table reproduces exactly, and the two named replacement surfaces both parse. (2) Render probe, real ObjectForm + real form renderer + real registry + real widgets, data source mocked only: object schema field {type:'text',label:'Nickname',placeholder:'OBJ-FIELD-PLACEHOLDER'} => `[MEASURE auto/input] placeholder = \"OBJ-FIELD-PLACEHOLDER\"`; textarea field => `[MEASURE auto/textarea] placeholder = \"OBJ-FIELD-TEXTAREA-PH\"`; sectioned path via sections:[{fields:['nickname']}] => `[MEASURE sectioned] placeholder = \"OBJ-FIELD-PLACEHOLDER\"`; SENSITIVITY CONTROL, same field with no placeholder => `[MEASURE control] placeholder = null`. 3 passed (3). The control is what proves the attribute comes from the object field definition rather than a widget default. (3) Action-param probe: object field {type:'text',label:'Phone',placeholder:'OBJ-FIELD-PH'} + param {field:'phone_number'} => `[MEASURE param] placeholder = \"OBJ-FIELD-PH\"`, and through paramToField => `[MEASURE paramToField] placeholder = \"OBJ-FIELD-PH\"`; control with no placeholder on the object field => `[MEASURE param control] placeholder = undefined`. 2 passed (2). Flow-tracing discipline honoured: the excluded surfaces were checked and NOT counted — sectionFields.ts:168 and react/spec-bridge/bridges/form-view.ts:96 are FormFieldSchema (form-section field) reads with their own parity gate; object-fields-bridge.ts:132/206 and ObjectFormCanvas.tsx:1053 are the designer's own preview; MetadataService.ts:102 and metadataConverters.ts:119 are the designer WRITE path, not a render read; the SDUI/inspector/ScreenView/config-field-renderer sites read authored node props, not object field definitions; plugin-kanban InlineQuickAdd and plugin-detail InlineCreateRelated declare their own placeholder on exported host-supplied field-definition props with no in-repo object-schema producer, so they are recorded but not counted either.",
      "open_questions": [
        {
          "question": "An object-field-level `placeholder` is applied at render time by four packages and apps/console, but `FieldSchema` refuses the key at the producer. Which end moves?",
          "options": [
            "A (as ruled, now unsupported by the measurement) — retire the designer control and strip the stored key. Cost is no longer 'the preview loses placeholder rendering': it makes the object-field half of a live, documented, multi-package renderer capability permanently unauthorable, and leaves the read sites in plugin-form/plugin-detail/app-shell/apps/console fed only by non-ObjectStack DataSources.",
            "B — re-route the control to write `inlineHelpText`. Still collides with the existing inlineHelpText control (two boxes, one key) and still conflates help text with in-input placeholder text; additionally it now silently DOWNGRADES a capability the renderers implement, since inlineHelpText does not reach the placeholder attribute.",
            "C — add `placeholder` to `FieldSchema` in objectstack. The measurement supplies what the #7176 doctrine demands from the producer side: the consumers are not speculative, they are already written, shipped and reachable, and one of them (resolveActionParams) documents the inheritance as intended behaviour.",
            "D (not in the card) — keep the spec's refusal AND narrow the renderers: delete the object-field placeholder reads in plugin-form/plugin-detail/apps/console so declared equals enforced on both ends. Largest blast radius; also removes a capability from non-ObjectStack backends that can legitimately feed it."
          ],
          "recommendation": "C, on the three axes. Real business need: this is the one option backed by MEASURED pull rather than by a reading of intent — four packages plus a shipped app already apply the key at render, `resolveActionParams`'s module header names the inheritance as designed, and the adapter is pinned to pass served metadata through untouched, so the consumer side exists and works the moment a producer is allowed. That is the opposite of #7176's 'declared surface with no pull'; here the pull is written down in code and the DECLARATION is what is missing. Startup focus does not argue against it, because C adds no new capability to build — it stops refusing one that is already built and already costs maintenance. Long-term soundness: A and B both leave the four read sites in place while guaranteeing the ObjectStack path can never feed them, which is precisely the declared-but-not-delivered shape this repo treats as first-class; D is coherent but pays a large deletion to remove working behaviour that non-ObjectStack DataSources use legitimately, and it fights AGENTS.md section 1's backend-agnostic charter. Making AI-written metadata hard to get wrong: C is also the only option where the authoring surface, the designer control and the render behaviour agree — an AI author who writes `placeholder` on a field gets exactly what the designer preview shows and what the form renders, instead of today's trap where the preview renders it and the save 422s. If C is chosen, the follow-through is a spec change in objectstack plus keeping both designer controls as they are; if the maintainer prefers A or D, the stale-draft strip half of #4644 still applies identically and the two RETIRED_FIELD_KEYS sets are ready to take the key (app-shell object-fields-io.ts currently `['indexed']`, plugin-designer MetadataFieldsPage carry-over strip). I did not implement any of them — the choice is above the delegation gate."
        },
        {
          "question": "Should the measurement be landed as a test-only pin (`Part of #4676`)?",
          "options": [
            "Land pins now, asserting object-field placeholder reaches the rendered control and the action param.",
            "Land nothing; keep the probes reproducible in this report."
          ],
          "recommendation": "Land nothing — chosen, hence pr: null. A pin here would freeze exactly the side of the contract that is under decision: it is correct under C, and must be deleted in the same PR under A or D. The ruling names 'no PR' as a valid outcome, and the probe code plus its real output is recorded above so any follow-up can re-run it in minutes."
        }
      ],
      "out_of_scope_findings": [
        "filed as #4679: `BaseFieldMetadata.indexed` in packages/types/src/field-types.ts is now a declared key with zero readers and zero writers repo-wide after PR #4675 removed the designer's writers — the renderer-side twin of the declaration #4675 correctly deleted from DesignerFieldDefinition; observation-class, `finding` label, no pm:queue, unassigned"
      ]
    }

    Claim state re-read before starting, as instructed: no claim with a different session ID after comment 5301249127 (the only later comment is this seat's own typo correction), so no blocked condition.


    Generated by Claude Code


    Generated by Claude Code

  8. yinlianghui commented on Aug 15, 2026

    @yinlianghui
    ContributorAuthor

    BACK TO THE DECISION BOX — the conditional ruling's measurement branch fired. (pm:dispatched → needs-user-decision, assignee cleared. Seat session session_01RnQd8iMMUwXQEV1crFmQiQ, 2026-08-15.)

    The ruled measurement (report above, probes + real output recorded) came back decisive and against the retirement premise: an object-field-level placeholder is read and applied at render time by four packages plus apps/console, none of them the designer preview — plugin-form's auto-generated and sectioned forms, plugin-detail's inline-edit drawer, app-shell's field-backed action params (whose module header documents the inheritance as intended), and console's FormPage. Each read carries a sensitivity control (same field without the key ⇒ no attribute), so the attribution is measured, not inferred. The flow-tracing exclusions (FormFieldSchema form-section reads, the designer's own preview, the write paths) are itemized and correct.

    The dev landed zero changes — exactly what the conditional ruling ordered on this branch — and I accept open question 2's disposition (no test-only pin: it would freeze the very side of the contract under decision).

    The re-framed question for you: the producer refuses a key four consumer surfaces deliberately honour. Which end moves?

    • A (the original retirement, now unsupported by measurement) — makes a live, documented, multi-package capability permanently unauthorable on the ObjectStack path.
    • B — re-route to inlineHelpText: two boxes writing one key, wrong semantics (help text ≠ in-input placeholder), and a silent capability downgrade.
    • C — declare placeholder in FieldSchema (objectstack, producer side). The fix(data-objectstack): declare the batch write-warning's unattributed-strip placeholder instead of a bare empty string #7176 doctrine's own test is met from the consumer side: the pull is measured, written, shipped — the declaration is what is missing.
    • D — narrow the renderers instead (delete the four read sites): coherent but the largest deletion, and it fights the backend-agnostic charter (non-ObjectStack DataSources feed these reads legitimately today).

    Dev recommends C; I endorse — it is the only option where authoring surface, designer control, preview and render agree, which is also the only shape that closes today's AI-author trap (preview renders it, save 422s). Honest counterweight, on the record: on the ObjectStack backend the consumers are currently starved (the 422 at the producer), so C is "stop refusing a built capability", not "the feature is broken today for ObjectStack users".

    Routing if C is ruled: the implementing change is a spec-surface addition filed at destination in objectstack (I will file the card and mark this one Blocked-by it); the designer controls stay as they are. If A or D is ruled: the #4644 strip-on-load machinery takes the key mechanically (both RETIRED_FIELD_KEYS sets are ready). Until ruled, note the 422 trap stays live on the ObjectStack path.


    Generated by Claude Code

  9. 6 remaining items

  10. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    Contributor

    Unlock sweep: blocker cleared, and the spec pin is measured to cover it. pm:blocked → pm:queue (paired write, same stroke).

    This card's release condition is two-part — the blocker must land and objectui's @objectstack/spec pin must carry it. Both halves measured:

    half reading
    blocker lands objectstack-ai/objectstack#9019 — CLOSED 2026-08-16T11:56:52Z, via merged PR objectstack#9113
    shipped in a released spec packages/spec/CHANGELOG.md on objectstack origin/main lists #9019 under ## 17.1.0
    objectui's pin carries it pnpm-lock.yaml on objectui origin/main resolves @objectstack/spec@17.1.0

    ⚠️ Recorded because this nearly went the other way. objectui#5716 states the pin cannot advance past 17.1.0 pending a ruling, which at a glance reads as "no cross-repo unblock is possible right now". It is not: the pin sits at 17.1.0 and this change is in 17.1.0. The distinction was measured, not assumed — and the same check is what kept objectui#5601 blocked in this batch, where the blocker closed but its change is still unreleased.

    Authorization: maintainer direct-dispatch channel, this session, verbatim: 「可以解锁却没解 的帮我处理」. PM session session_0124Qg8rLvpXnQDwCmpKUmaJ. Block age at unlock: 7 days.

    ⚠️ The premise was NOT re-verified — that is the claiming seat's first step. The blocker's own change may have closed part of what this card asks for. This card's body predicts "the designer trap closes with zero objectui code change"; verify that against the current pin before writing anything.

    ⚠️ This card carries no domain:*, so it re-enters the queue lane-less and is invisible to lane-scoped candidate queries until triage stamps it. ⛔ Not stamped here — domain:* is the triage seat's single channel.

    ⛔ Assignee untouched · scope untouched.


    Generated by Claude Code

  11. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 23, 2026
  12. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    Contributor

    Triage: domain:ui. The fix lands in packages/app-shell and packages/plugin-designer — the two field designers that author placeholder.

    ⚠️ But read the card's own prediction first: the ruled fix was Option C, the producer moves — objectstack#9019 declared placeholder on FieldSchema, shipped in @objectstack/spec 17.1.0, and objectui's lockfile resolves 17.1.0. If that reading holds, the designer trap closes with zero objectui code change and the correct outcome is premise_still_valid: false plus a close, not a patch. Verify before writing anything.

    The lane is still ui: whatever remains after that check is objectui-side designer code.

    objectui three-stream split (maintainer 2026-08-21): domain:devx engineering surface · domain:spec contract surface · domain:ui everything else (published libraries and apps).

    Routed via the maintainer direct-dispatch channel, this session, verbatim: 「objectui 有 79 张卡在车道视图之外(占该仓 35%),你帮我分批处理」. PM session session_0124Qg8rLvpXnQDwCmpKUmaJ. Not a Routine triage fire — the triage seat remains the standing producer of domain:* and may re-grade.

    ⛔ pm-state, assignee and scope untouched.


    Generated by Claude Code

  13. self-assigned this
    on Aug 23, 2026
  14. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    Contributor

    Claim: domain:ui execution seat (seat post #5560), round 22, slot 3 of 3.

    • Session: session_01EuPCi56cnGyykygi3z9w4m
    • Branch: claude/issue-4676-placeholder-pin-covers-it
    • Worktree: dedicated, off origin/main at 9850c6e4e
    • Domain: domain:ui · Tier: claude-opus-5
    • Clause-②: no — the accept-set expansion already happened upstream (objectstack#9019, claude-fable-5 there). Nothing in this repo's contract surface moves.

    ⚠️ This is dispatched as a VERIFICATION, not as an implementation, and triage said so first: "If that reading holds, the designer trap closes with zero objectui code change and the correct outcome is premise_still_valid: false plus a close, not a patch. Verify before writing anything."

    The chain to confirm, each link independently:

    link recorded reading
    Option C ruled, producer moves maintainer 2026-08-16
    placeholder declared on FieldSchema objectstack#9019, closed 2026-08-16, PR objectstack#9113
    shipped in a release packages/spec/CHANGELOG.md lists #9019 under ## 17.1.0
    objectui's pin carries it pnpm-lock.yaml resolves @objectstack/spec@17.1.0

    Your first deliverable is the probe the card itself used, re-run against the installed pin: FieldSchema.safeParse({ name:'foo', label:'Foo', type:'text', placeholder:'…' }). The card measured unrecognized_keys against 17.0.0-rc.6. If it now succeeds, the 422 trap is closed and this card is done with no code.

    ⛔ Control it. A success proves nothing unless a key known to still be rejected also runs on the same probe and still fails — the card's own table gives you indexed (handled by #4644) and the no-extra-key success control. Report all legs.

    If it still rejects, the premise stands and the chain has a broken link — find which one and report it; do not start patching designers to work around an upstream regression.

    File surface granted (write) — only if something genuinely remains after the probe:

    packages/app-shell/src/views/metadata-admin/**
    packages/plugin-designer/**
    .changeset/*
    

    Read-only: apps/console/** (a measurement input; its FormPage is one of the four render consumers the 2026-08-15 measurement found, and it is held by a sibling this round).

    Serial constraints: ⛔ apps/console/.env.* · examples/console-starter/** · apps/console/vite.config.ts held by #5745 (slot 1); ⛔ packages/auth/** held by #5731 (slot 2).

    The strip-on-load question is moot if the probe passes — the card records that a stored placeholder becomes legal, so ⛔ do not add placeholder to either RETIRED_FIELD_KEYS set. That was Option A's treatment and Option A did not win.

    ℹ️ Read the 2026-08-15 dev report on this card before measuring. It is the reason Option A was refused: an object-field-level placeholder is read and applied at render time by four packages plus apps/console, each with a sensitivity control. That measurement is why the producer moved instead of the consumers — and it is what makes "zero code change" the expected answer rather than a suspicious one.


    Generated by Claude Code

  15. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    Contributor

    os-dev-report

    Verification complete. The card's own prediction holds: the trap is closed, with zero objectui code change. Recommending close. Seat domain:ui, round 22 slot 3, session session_01EuPCi56cnGyykygi3z9w4m. No branch pushed, no PR.

    2a. The probe, re-run against the installed pin

    Installed (on disk, not merely locked): @objectstack/spec@17.1.0, single copy, resolved from packages/app-shell. Run against both the ESM and CJS entries of @objectstack/spec/data — identical results, so this is not a half-shipped build.

    leg verdict note
    control, no extra key SUCCESS
    control, zzzDefinitelyNotAKey REJECTED unrecognized_keys falsifies "schema loosened wholesale"
    indexed REJECTED unrecognized_keys #4644's retirement still holds
    placeholder SUCCESS, parsed back as "Enter a value" the card's premise is dead
    externalId SUCCESS
    trackHistory SUCCESS
    hidden / readonly / unique SUCCESS
    inlineHelpText SUCCESS named replacement
    description SUCCESS named replacement

    The card measured unrecognized_keys for placeholder against 17.0.0-rc.6. It now succeeds.

    Two controls carry the weight, and they discriminate between the three ways a success could have been fake:

    • the bogus-key leg still rejects, so the schema was not switched to passthrough or loosened wholesale;
    • placeholder is parsed back into r.data, so this is not a strip-unknown loosening either — the key survives the parse, which only a declared key does;
    • indexed still rejects, so I am probing the same schema the card probed, not a different export.

    None of the five previously-accepted keys regressed.

    2b. Each link of the chain, verified independently

    link evidence verdict
    Option C ruled, producer moves maintainer 2026-08-16, comment 5306089473 holds
    placeholder declared on FieldSchema objectstack#9113 — merged: true, merged_at 2026-08-16T11:56:51Z, body Fixes #9019, 14 files, +186/-5 holds
    shipped in a release packages/spec/CHANGELOG.md on objectstack origin/main, line 1132, nearest preceding header verified programmatically as line 3 ## 17.1.0 (next header is ## 17.0.0 at line 3014 — no intermediate version could have captured it) holds
    objectui's pin carries it pnpm-lock.yaml resolves @objectstack/spec@17.1.0; on disk after pnpm install --frozen-lockfile, one copy, version 17.1.0 holds

    No link is broken, so 2c does not arm.

    Independent corroboration that does not rely on the CHANGELOG at all — the shipped 17.1.0 artifact itself carries the declaration and names this card:

    placeholder: z.string().optional().describe("Placeholder text rendered inside the empty input ...")
    

    and, in the retirement table, the tombstone has been deliberately removed:

    // NOTE: no entry for `placeholder`. It sat here as a retirement ("never a
    // FieldSchema key") until the 2026-08-16 maintainer ruling on objectui#4676
    // (#9019) declared it ...
    

    The #5716 confusion the sweep flagged did not re-enter: the pin sits at 17.1.0 and the change is in 17.1.0.

    2d. Residue sweep — empty, and controlled

    Every zero below is paired with a control on the same instrument and corpus.

    swept for result control
    placeholder in either RETIRED_FIELD_KEYS absent — both sets are exactly ['indexed'], and a test pins that literal the sets exist and are found
    reference to this card in code/docs zero hits 4644 returns 11 files
    refusal prose "never a FieldSchema key" 2 hits, both about indexed same prose returns 6 hits in the installed spec
    a test asserting placeholder is rejected/stripped/422 zero — 6 co-occurrence hits, all read by hand, all unrelated (a chart refusal element, InlineActionSchema block-config, outbound agent text, a dashboard widget, record-title, an MCP URL) same instrument, same corpus, swapping in indexed: 7 real residue hits
    a client-side field-key allowlist clientValidation.ts has zero hits for placeholder — and zero for inlineHelpText, indexed, externalId, trackHistory, FieldSchema the zero is structural: it does not enumerate field keys at all, so it is not a finding
    warning text on the control designer.field.placeholder is 'Placeholder' / '占位符', no suffix; the control is a plain InspectorTextField, not disabled, no deprecation notice the key is found in both locale packs
    refusal prose in packages/types/src/field-types.ts placeholder?: string declared plainly; the two "deliberately no X here" blocks are indexed and distance_metric description returns 8 hits in that file

    Instrument limitation, stated: a plain grep cannot distinguish a comment from executable code, and for 2d that distinction is the whole question. Two things compensate. First, I read all six placeholder co-occurrence hits by hand and classified them. Second — the behavioural complement — I ran the executable assertions:

    pnpm exec vitest run --maxWorkers=2 \
      packages/app-shell/src/views/metadata-admin/previews/object-fields-io.retiredKeys.test.ts \
      packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.retiredIndexed.test.tsx \
      packages/plugin-designer/src/MetadataFieldsPage.retiredIndexed.test.tsx
      Test Files  3 passed (3)
           Tests  14 passed (14)
    

    A separate note on scope, because it is easy to overclaim: the 422 was returned by the server's spec, not objectui's. This probe proves the contract now declares the key and that objectui's pin carries that contract. Whether a given deployment stops 422-ing depends on that backend running spec 17.1.0 or later. Nothing in objectui gates on it — objectui does not validate field definitions against FieldSchema anywhere (measured: zero importers of the data FieldSchema in the designer surfaces), which is precisely why the objectui side needs no change.

    End-to-end confirmation (throwaway probe, deleted)

    Not just "the schema accepts the key" but "what the designer emits, the schema accepts" — the exact trap the card describes. Real output:

    [PROBE leg1] round-trip output = {"type":"text","label":"Nickname","placeholder":"e.g. Jo"}
    [PROBE leg1] FieldSchema.safeParse.success = true
    [PROBE leg2] round-trip output = {"type":"lookup","label":"Owner"}     (indexed stripped)
    [PROBE leg2] parse of stripped output = true
    [PROBE leg3] success = false ; issue codes = ["unrecognized_keys"]     (parse leg is live)
    [PROBE leg4] success = false                                           (bogus key, inside vitest resolution)
     Test Files  1 passed (1)
          Tests  4 passed (4)
    

    Leg 4 matters on its own: it re-establishes the anti-loosening control inside vitest's module resolution, which can alias differently than raw node. Probe deleted; git status --porcelain empty, git log origin/main..HEAD empty.

    The pin question, re-decided on today's facts

    The 2026-08-15 objection (a pin would freeze the side of the contract under decision) is spent — the decision was made. Re-deciding rather than inheriting: still no pin on this card, for different reasons than last time.

    Recommended disposition

    Close #4676 as completed — the producer moved and the pin covers it. ⛔ Per the dispatch, placeholder must not be added to either RETIRED_FIELD_KEYS set; Option A did not win and doing so would now delete a declared, live key. pm-state and assignee left untouched for the PM seat.

    {
      "issue": 4676,
      "status": "done",
      "branch": "claude/issue-4676-placeholder-pin-covers-it (created locally, zero commits, nothing pushed, torn down)",
      "pr": null,
      "premise_still_valid": false,
      "summary": "Dispatched as a verification and it resolved as one: zero objectui code change, no PR. The card's premise ('FieldSchema rejects placeholder by name') is dead. Against the installed @objectstack/spec 17.1.0 — verified on disk after a frozen-lockfile install, single copy, not merely read from the lockfile — FieldSchema.safeParse now ACCEPTS placeholder and parses it back into r.data, while a bogus key and `indexed` both still reject, so this is a declared key rather than a loosened or passthrough schema. All four chain links verified independently: the 2026-08-16 Option C ruling; objectstack#9113 merged 2026-08-16T11:56:51Z closing #9019; the spec CHANGELOG entry at line 1132 whose nearest preceding header is programmatically confirmed to be '## 17.1.0'; and objectui's pin resolving 17.1.0. Independently corroborated by the shipped artifact itself, which carries the placeholder declaration and a NOTE recording that the retirement tombstone was removed by the ruling on objectui#4676. The 2d residue sweep is empty and every zero is controlled: both RETIRED_FIELD_KEYS sets remain exactly ['indexed'], no test asserts a placeholder rejection (the same instrument finds 7 real residue hits for 'indexed'), the designer control carries no warning text, and clientValidation.ts's zero is structural since it enumerates no field keys at all. Scope caveat stated rather than overclaimed: the 422 came from the server's spec, so a deployment stops 422-ing when its backend runs 17.1.0 or later; objectui never validated field defs against FieldSchema, which is exactly why its side needed no change. Re-decided the pin question on today's facts rather than inheriting either answer: no pin here, because the producer already pins the accept side behind a no-rot gate and a single-key pin would close only 1/N of a class this repo has hit three times — filed the class-closing guard as #5761 instead. Recommending close as completed; pm-state and assignee untouched.",
      "tests": "No production code changed, so no gate union is owed; everything below is measurement against an unmodified origin/main @ 9850c6e4e in a dedicated worktree, with all heavy runs through the shared entry point (bash scripts/pm/os-verify-lock.sh) at NODE_OPTIONS=--max-old-space-size=4096 and vitest --maxWorkers=2. Exit codes captured BEFORE any pipe throughout; verdicts quoted from the tools' own printed lines, never a bare $?. (1) pnpm install --frozen-lockfile: 'os-verify-lock: VERDICT command-exit 0'. Installed version read off disk: '@objectstack/spec version = 17.1.0', one copy in the .pnpm store. (2) Spec probe, both entry conditions of @objectstack/spec/data, identical results: control no-extra-key SUCCESS; zzzDefinitelyNotAKey REJECTED unrecognized_keys; indexed REJECTED unrecognized_keys; placeholder SUCCESS with '[parsed back: placeholder=\"Enter a value\"]'; externalId, trackHistory, hidden, readonly, unique, inlineHelpText, description all SUCCESS. 2 of 11 legs rejected under both ESM and CJS. (3) Existing retirement suites, unmodified: 'Test Files 3 passed (3) / Tests 14 passed (14)' across object-fields-io.retiredKeys.test.ts, ObjectFieldInspector.retiredIndexed.test.tsx, MetadataFieldsPage.retiredIndexed.test.tsx. (4) End-to-end throwaway probe through the designer's real draft I/O (readFields/writeFields), then deleted: leg1 round-trip output {\"type\":\"text\",\"label\":\"Nickname\",\"placeholder\":\"e.g. Jo\"} and FieldSchema.safeParse.success = true; leg2 indexed stripped by round-trip and stripped output parses true; leg3 sensitivity control, un-stripped indexed success = false with issue codes [\"unrecognized_keys\"], proving the parse leg is live; leg4 loosening control re-established INSIDE vitest's own resolution, bogus key success = false. 'Test Files 1 passed (1) / Tests 4 passed (4)'. Probe file removed; git status --porcelain empty and git log origin/main..HEAD empty afterwards. (5) Residue sweep greps each paired with a control on the same instrument and corpus, detailed in the comment table above. Instrument limitation stated on the record: grep cannot separate a comment from executable code, so all six placeholder co-occurrence hits were read and classified by hand and the retirement suites were actually executed as the behavioural complement.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #5761: nothing guards the CLASS 'a field designer authors a key FieldSchema refuses' — three per-key tombstones (indexed #4644, distance_metric #4687, placeholder #4676) but no key-level parity check on the designer write path; observation-class, `finding` label, no pm:queue, unassigned"
      ]
    }

    Generated by Claude Code

  16. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    Contributor

    ACCEPT — closing completed. Zero objectui code change, no PR. The premise is dead.

    Dispatched as a verification, resolved as one. Triage predicted this outcome and the measurement confirms it: Option C moved the producer, the spec pin carries it, and objectui's side needed nothing.

    The probe, and why its shape makes it evidence rather than a hope

    Against the installed @objectstack/spec 17.1.0 — read off disk after a --frozen-lockfile install with a single copy in the store, not inferred from the lockfile:

    leg verdict
    control, no extra key SUCCESS
    zzzDefinitelyNotAKey REJECTED unrecognized_keys
    indexed REJECTED unrecognized_keys
    placeholder SUCCESS, parsed back as placeholder="Enter a value"
    externalId · trackHistory · hidden · readonly · unique · inlineHelpText · description SUCCESS

    2 of 11 legs reject, which is the whole point: a schema that had merely been loosened or made passthrough would have accepted the bogus key too. placeholder is a declared key now, and the value survives the parse rather than being stripped. Run under both the ESM and CJS entry conditions of @objectstack/spec/data with identical results, and the loosening control re-established inside vitest's own resolution so the two instruments cannot disagree silently.

    All four chain links verified independently, not inherited: the 2026-08-16 Option C ruling; objectstack#9113 merged 2026-08-16T11:56:51Z closing #9019; the spec CHANGELOG entry whose nearest preceding header is programmatically confirmed to be ## 17.1.0 (rather than eyeballed); and objectui's pnpm-lock.yaml resolving 17.1.0. The shipped artifact corroborates it from the other side — it carries the placeholder declaration and a NOTE recording that the retirement tombstone was removed by the ruling on this card.

    ⚠️ And the confusion that nearly sank the unlock did not re-enter: #5716 says the pin cannot advance past 17.1.0; this change is in 17.1.0. At, not past.

    The residue sweep is empty, and the zeros are controlled

    I re-checked what I could locally and it agrees where my instrument is capable:

    • object-fields-io.retiredKeys.test.ts:41 asserts expect([...RETIRED_FIELD_KEYS]).toEqual(['indexed']) — so the ⛔ in the dispatch (do not add placeholder to either set) was enforced by a live test, and both sets are still exactly ['indexed']. ✓
    • The lockfile resolves @objectstack/spec@17.1.0 and nothing else. ✓

    ⚠️ One of my own checks was a void reading and I am not dressing it up as corroboration. I grepped test files for placeholder to corroborate "no test asserts a rejection" and got 169 files — placeholder is an ordinary word in UI tests (every input has one). That instrument cannot answer this question at all. The dev's method is the one that can: six co-occurrence hits read and classified by hand, plus the behavioural complement — the three retirement suites actually executed, Test Files 3 passed (3) / Tests 14 passed (14). Their grep limitation is stated on the record, which is why they did not stop at the grep.

    Also correctly measured rather than assumed: clientValidation.ts's zero is structural (it enumerates no field keys at all), and the designer control carries no warning text to retract.

    The scope caveat is stated, not overclaimed

    The 422 came from the server's spec, so a deployment stops 422-ing when its backend runs 17.1.0 or later. objectui never validated field defs against FieldSchema — which is precisely why its side needed no change. That distinction is worth keeping: "the trap is closed" is true of the contract, and true of a deployment only once its backend has moved.

    The pin question was re-decided on today's facts, and declined for the right reason

    The 2026-08-15 dev refused a test-only pin because it would have frozen the side of the contract then under decision. That objection is spent — the decision was made — so it was re-opened rather than inherited, and declined on a new ground: the producer already pins the accept side behind a no-rot gate, and a single-key pin would close 1/N of a class this repo has now hit three times (indexed #4644, distance_metric #4687, placeholder #4676 — three per-key tombstones, no key-level parity check on the designer write path).

    The class guard is filed instead as #5761 (finding, unassigned, no pm:queue — triage owns first touch). Filing the general case rather than pinning the instance is the better trade, and it is the third time this lane has met that exact shape.

    Closed completed. pm:dispatched cleared, assignee cleared, bug retained as the historical classification. Nothing was pushed — the branch was created locally, took zero commits, and was torn down; git ls-remote confirms it never reached origin.


    Generated by Claude Code

  17. removed their assignment
    on Aug 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions