Skip to content

finding: nothing guards the CLASS "a field designer authors a key FieldSchema refuses" — three per-key tombstones, no key-level parity check #5761

Description

@os-zhuang

Observation-class finding, surfaced while verifying #4676 (the placeholder chain). No live defect today — all three known instances are resolved. Filed unassigned; finding, deliberately notpm:queue.

The class

A field designer offers a control that writes a key FieldSchema refuses by name. The author sees the control work (and, in metadata-admin, sees the preview render it), then PUT /api/v1/meta/object/:name returns a hard 422 INVALID_METADATA that blocks every subsequent save of that object until the key is stripped.

This repo has now filed that same shape three times:

cardkeyresolution
#4644indexedcontrol retired + strip-on-load
#4687distance_metricdeclaration removed (zero readers/writers)
#4676placeholderproducer moved — declared upstream, objectstack#9019 / PR objectstack#9113, shipped in @objectstack/spec 17.1.0

Three instances, three different correct answers. That is a class, not a coincidence.

What exists today

Per-key tombstones, each keyed to one literal:

  • packages/app-shell/src/views/metadata-admin/previews/object-fields-io.tsRETIRED_FIELD_KEYS = ['indexed']
  • packages/plugin-designer/src/MetadataFieldsPage.tsx — a second, independently-maintained RETIRED_FIELD_KEYS = ['indexed']
  • prose tombstones in packages/types/src/field-types.ts, MetadataService.ts, metadataConverters.ts, FieldDesigner.tsx

Every one of these is written after an instance is discovered in production. Nothing detects the next one.

What is missing

No test or gate asserts: every key the field designers can emit is a key FieldSchema accepts.

Measured on origin/main @ 9850c6e4e, with controls:

  • grep -rn "FieldSchema" packages/app-shell packages/plugin-designer — 30 hits, all prose about indexed plus a local structural interface ServerFieldSchema (plugin-designer's own subset type, not the spec's schema). Zero executable assertions against the spec's FieldSchema.
  • Control: grep -c "@objectstack/spec/data" across packages/apps returns 127 hits, so the repo does import that subpath heavily — the zero above is a real absence, not a broken instrument.
  • scripts/check-spec-symbol-derivation.mjs is not this guard: it checks symbol NAME collisions between app-shell and the spec, a different dimension entirely.
  • Sibling gap already on record: finding: no gate reads a doc snippet's schema KEYS against the spec — #4823's deferred "second dimension", closed unbuilt #5138 ("no gate reads a doc snippet's schema KEYS against the spec") is the same key-level blind spot in the docs corpus, and was closed unbuilt.

Shape of the fix

A parity test on the designer write path, not a per-key allowlist. The tractable version: take what the designer draft I/O actually emits and parse it. Measured working during the #4676 verification (throwaway probe, not landed):

readFields -> writeFields round-trip of { nickname: { type:'text', label:'Nickname', placeholder:'e.g. Jo' } }
=> {"type":"text","label":"Nickname","placeholder":"e.g. Jo"}
=> FieldSchema.safeParse(...).success = true
control: un-stripped `indexed` => success = false, issue codes ["unrecognized_keys"]
control: bogus key `zzzDefinitelyNotAKey` => success = false

The controls matter: without them a green parity test proves nothing, because a loosened or wrongly-resolved schema accepts everything.

Note the harder half, stated honestly rather than hidden: enumerating the full set of keys the two designers can emit is not mechanical — ObjectFieldInspector writes through many conditional patchDef({...}) calls, and fromDesignerField spreads prev verbatim to preserve unknown keys. A guard over the statically declared payload shapes (FieldMetadataPayload, DesignerFieldDefinition, ServerFieldSchema) is the achievable version and would have caught all three instances above.

Why this is not urgent

All three known instances are closed, and the two RETIRED_FIELD_KEYS sets are correct as they stand. The cost of the gap is that instance four is found by a user hitting a save-blocking 422, not by CI.

Refs: #4676 (where this surfaced), #4644, #4687, #5138, objectstack#9019.

Metadata

Metadata

Labels

domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repopm:dispatchedtooling

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions