Repository navigation
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
Description
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— thedesigner.field.placeholdertext field in the ADVANCED section,patchDef({ placeholder: … })packages/plugin-designer/src/FieldDesigner.tsx— theplaceholderentry in the type-specific sectionpackages/plugin-designer/src/MetadataFieldsPage.tsx—toDesignerField/fromDesignerFieldpackages/app-shell/src/services/MetadataService.ts—FieldMetadataPayload.placeholder,toFieldPayloadpackages/app-shell/src/utils/metadataConverters.ts—MetadataField.placeholder,toFieldDefinitionpackages/types/src/designer.ts—DesignerFieldDefinition.placeholder- label keys:
designer.field.placeholder,appDesigner.fieldDesigner.placeholder, andplaceholderin 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)
- A. Retire the control, matching console: field designer still offers an "Indexed" toggle for a key FieldSchema rejects by name — ticking it hard-blocks the save #4644 / the examples/schema-catalog: 13 个 grid 示例用了未声明的
cols键,列数被静默丢弃 —— 要 3/4 列的示例在 docs 站实际渲染成 2 列 #4001 / feat(studio): Capabilities section in the object Settings panel (framework#2707/#2727) #2377 doctrine — a designer must not offer a key the contract refuses. The spec's own message names the replacements, andObjectFieldInspectoralready rendersinlineHelpTextright there, so authors are not left without a hint-text surface. Cost: the preview loses per-field placeholder rendering. - B. Re-route the control to write
inlineHelpText. Keeps the affordance, but it would collide with the existinginlineHelpTextcontrol (two boxes writing one key), andinlineHelpTextis help text, not in-input placeholder text — different rendering semantics. - C. Add
placeholdertoFieldSchemainobjectstack. Only correct if there is measured pull for field-level placeholder as real, applied behaviour; otherwise it grows the declared surface ahead of any consumer, which the fix(data-objectstack): declare the batch write-warning's unattributed-strip placeholder instead of a bare empty string #7176 doctrine refuses from the producer side.
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
yinlianghui commentedon Aug 15, 2026
First-touch grading (seat
repo:objectui; sessionsession_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
placeholderat render time — the form/field renderers (packages/fields,packages/plugin-form), example apps, catalog schemas.placeholderto the twoRETIRED_FIELD_KEYSsets). Same doctrine as console: field designer still offers an "Indexed" toggle for a key FieldSchema rejects by name — ticking it hard-blocks the save #4644's landed ruling — a preview affordance that renders what the contract refuses to persist is a trap, not a capability, and the spec's own rejection message names the replacement surfaces (inlineHelpText/description), which the inspector already renders.FieldSchemain objectstack) are both above the delegation gate (C is a spec-surface change; B rewrites hint-text semantics).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
yinlianghui commentedon Aug 15, 2026
Claim: PM loop round 8
Session:
session_01RnQd8iMMUwFQEV1crFmQiQBranch:
claude/issue-4676-placeholder-keyWorktree:
objectui-issue-4676Domain:
repo:objectuiFile 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.tsRETIRED_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: opusSerial 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
yinlianghui commentedon Aug 15, 2026
Correction to the claim comment above: the session ID contains a typo — it reads
session_01RnQd8iMMUwFQEV1crFmQiQbut the claiming seat session issession_01RnQd8iMMUwXQEV1crFmQiQ(the same session that posted every other comment in this thread). Branch, worktree, and surface are unchanged.Generated by Claude Code
yinlianghui commentedon Aug 15, 2026
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
yinlianghui commentedon Aug 15, 2026
BACK TO THE DECISION BOX — the conditional ruling's measurement branch fired. (
pm:dispatched→needs-user-decision, assignee cleared. Seat sessionsession_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
placeholderis read and applied at render time by four packages plusapps/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?
inlineHelpText: two boxes writing one key, wrong semantics (help text ≠ in-input placeholder), and a silent capability downgrade.placeholderinFieldSchema(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.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 (bothRETIRED_FIELD_KEYSsets are ready). Until ruled, note the 422 trap stays live on the ObjectStack path.Generated by Claude Code
6 remaining items
os-zhuang commentedon Aug 23, 2026
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/specpin must carry it. Both halves measured:objectstack-ai/objectstack#9019— CLOSED 2026-08-16T11:56:52Z, via merged PR objectstack#9113packages/spec/CHANGELOG.mdon objectstackorigin/mainlists#9019under## 17.1.0pnpm-lock.yamlon objectuiorigin/mainresolves@objectstack/spec@17.1.0Authorization: maintainer direct-dispatch channel, this session, verbatim: 「可以解锁却没解 的帮我处理」. PM session
session_0124Qg8rLvpXnQDwCmpKUmaJ. Block age at unlock: 7 days.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
os-zhuang commentedon Aug 23, 2026
Triage:
domain:ui. The fix lands inpackages/app-shellandpackages/plugin-designer— the two field designers that authorplaceholder.placeholderonFieldSchema, shipped in@objectstack/spec17.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 ispremise_still_valid: falseplus 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:devxengineering surface ·domain:speccontract surface ·domain:uieverything 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 ofdomain:*and may re-grade.⛔ pm-state, assignee and scope untouched.
Generated by Claude Code
os-zhuang commentedon Aug 23, 2026
Claim:
domain:uiexecution seat (seat post #5560), round 22, slot 3 of 3.session_01EuPCi56cnGyykygi3z9w4mclaude/issue-4676-placeholder-pin-covers-itorigin/mainat9850c6e4edomain:ui· Tier:claude-opus-5claude-fable-5there). Nothing in this repo's contract surface moves.premise_still_valid: falseplus a close, not a patch. Verify before writing anything."The chain to confirm, each link independently:
placeholderdeclared onFieldSchemapackages/spec/CHANGELOG.mdlists #9019 under## 17.1.0pnpm-lock.yamlresolves@objectstack/spec@17.1.0Your 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 measuredunrecognized_keysagainst 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
successproves nothing unless a key known to still be rejected also runs on the same probe and still fails — the card's own table gives youindexed(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:
Read-only:
apps/console/**(a measurement input; itsFormPageis 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.tsheld 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
placeholderbecomes legal, so ⛔ do not addplaceholderto eitherRETIRED_FIELD_KEYSset. 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
placeholderis read and applied at render time by four packages plusapps/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
os-zhuang commentedon Aug 23, 2026
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, sessionsession_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 frompackages/app-shell. Run against both the ESM and CJS entries of@objectstack/spec/data— identical results, so this is not a half-shipped build.zzzDefinitelyNotAKeyunrecognized_keysindexedunrecognized_keysplaceholder"Enter a value"externalIdtrackHistoryhidden/readonly/uniqueinlineHelpTextdescriptionThe card measured
unrecognized_keysforplaceholderagainst 17.0.0-rc.6. It now succeeds.Two controls carry the weight, and they discriminate between the three ways a
successcould have been fake:placeholderis parsed back intor.data, so this is not a strip-unknown loosening either — the key survives the parse, which only a declared key does;indexedstill 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
placeholderdeclared onFieldSchemamerged: true,merged_at2026-08-16T11:56:51Z, bodyFixes #9019, 14 files, +186/-5packages/spec/CHANGELOG.mdon objectstackorigin/main, line 1132, nearest preceding header verified programmatically as line 3## 17.1.0(next header is## 17.0.0at line 3014 — no intermediate version could have captured it)pnpm-lock.yamlresolves@objectstack/spec@17.1.0; on disk afterpnpm install --frozen-lockfile, one copy, version 17.1.0No 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:
and, in the retirement table, the tombstone has been deliberately removed:
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.
placeholderin eitherRETIRED_FIELD_KEYS['indexed'], and a test pins that literal4644returns 11 filesindexedplaceholderis rejected/stripped/422InlineActionSchemablock-config, outbound agent text, a dashboard widget, record-title, an MCP URL)indexed: 7 real residue hitsclientValidation.tshas zero hits forplaceholder— and zero forinlineHelpText,indexed,externalId,trackHistory,FieldSchemadesigner.field.placeholderis'Placeholder'/'占位符', no suffix; the control is a plainInspectorTextField, not disabled, no deprecation noticepackages/types/src/field-types.tsplaceholder?: stringdeclared plainly; the two "deliberately no X here" blocks areindexedanddistance_metricdescriptionreturns 8 hits in that fileInstrument limitation, stated: a plain
grepcannot distinguish a comment from executable code, and for 2d that distinction is the whole question. Two things compensate. First, I read all sixplaceholderco-occurrence hits by hand and classified them. Second — the behavioural complement — I ran the executable assertions: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
FieldSchemaanywhere (measured: zero importers of the dataFieldSchemain 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:
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 --porcelainempty,git log origin/main..HEADempty.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.
.describe()prose pin, and a no-rot gate ("no guidance entry names a key the schema now declares") that makes re-adding the tombstone a red build. A consumer-side copy duplicates a producer-owned assertion.indexed(console: field designer still offers an "Indexed" toggle for a key FieldSchema rejects by name — ticking it hard-blocks the save #4644),distance_metric(finding(types): VectorFieldMetadata has two declared keys FieldSchema rejects —indexedanddistance_metric#4687),placeholder(Both field designers authorplaceholder, a key FieldSchema rejects by name — but the designer's own preview renders it, so this is a contract question not a deletion #4676) — with three different correct answers. Aplaceholder-shaped pin would leave the trap open for key four.finding, nopm:queue, unassigned): nothing asserts that the keys the designers can emit are keysFieldSchemaaccepts; the guard that exists (check-spec-symbol-derivation.mjs) checks symbol NAMES, a different dimension, and finding: no gate reads a doc snippet's schema KEYS against the spec — #4823's deferred "second dimension", closed unbuilt #5138 already recorded the same key-level blind spot for doc snippets.Recommended disposition
Close #4676 as completed — the producer moved and the pin covers it. ⛔ Per the dispatch,
placeholdermust not be added to eitherRETIRED_FIELD_KEYSset; 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
os-zhuang commentedon Aug 23, 2026
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/spec17.1.0 — read off disk after a--frozen-lockfileinstall with a single copy in the store, not inferred from the lockfile:zzzDefinitelyNotAKeyunrecognized_keysindexedunrecognized_keysplaceholderplaceholder="Enter a value"externalId·trackHistory·hidden·readonly·unique·inlineHelpText·description2 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.
placeholderis 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/datawith 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'spnpm-lock.yamlresolving 17.1.0. The shipped artifact corroborates it from the other side — it carries theplaceholderdeclaration and a NOTE recording that the retirement tombstone was removed by the ruling on this card.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:41assertsexpect([...RETIRED_FIELD_KEYS]).toEqual(['indexed'])— so the ⛔ in the dispatch (do not addplaceholderto either set) was enforced by a live test, and both sets are still exactly['indexed']. ✓@objectstack/spec@17.1.0and nothing else. ✓placeholderto corroborate "no test asserts a rejection" and got 169 files —placeholderis 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, nopm: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:dispatchedcleared, assignee cleared,bugretained as the historical classification. Nothing was pushed — the branch was created locally, took zero commits, and was torn down;git ls-remoteconfirms it never reached origin.Generated by Claude Code
feat(tooling): gate designer field payload keys against the installed…
feat(tooling): gate designer field payload keys against the installed…