Skip to content

publicPicker is enforced by the REST lookup route but declared nowhere in packages/spec — no saved form can ever enable it #7467

Description

@os-zhuang

Found while implementing #7134 (the groupssections save-path fold). Filed, not fixed: closing it means deciding where the key belongs in packages/spec, which is a spec-acceptance change and out of #7134's lane.

The gap

GET /forms/:slug/lookup/:field in packages/rest/src/rest-server.ts gates the public lookup on an opt-in publicPicker block on the field's declaration in the form spec. Without one it answers 403 LOOKUP_NOT_PUBLIC — deliberately loud rather than silent (#3022).

That key is declared in no schema:

$ grep -rn "publicPicker" packages/spec/src/
(no hits)

FormFieldSchema is strict, and ADR-0089 D3a made an undeclared key a loud parse error rather than a silent drop. So a form carrying a publicPicker is refused by ViewMetadataSchema:

Unrecognized key(s) on this view/page schema: publicPicker. Before ADR-0089 D3a these were dropped silently, shipping inert metadata; a mis-layered or stale key is now a loud parse error.

Measured on origin/main @ 08363a09f, through the real ViewMetadataSchema.safeParse, on the ViewItem branch:

field declaration parse
{ field: 'owner', publicPicker: { displayFields: ['name'] } } rejectedunrecognized_keys
{ field: 'owner' } accepted

saveMetaItem validates against that same schema, so a form declaring a picker is a 422 and never becomes a sys_metadata row. Code-authored forms hit the same wall — FormViewSchema.parse is the same door.

Consequence

The public-lookup capability is unreachable through any authoring path this repo accepts. The route, its displayFields projection, its maxResults cap (hard ceiling 50), its publicPicker.filter pre-filter and its publicPicker.object override are all live code that no spec-valid form can turn on. That is Prime Directive #10's "declared ≠ enforced" in the mirror direction: enforced, never declarable.

It is also why #7134 could close only two of the three degradations #6926 listed. The third — GET /forms/:slug/lookup/:field answering 403 for every field — does not clear on the stored-row path, and not because the fold missed it: the fold does reach the route's sections walk (the two sibling routes are the same walk and they changed), but there is no picker in those sections to find. #7134 pins that boundary explicitly in packages/rest/src/public-form-routes.stored-row.test.ts (BOUNDARY: the lookup route is STILL 403 — for a different reason), and that assertion is written to go red the day this is fixed, so whoever lands it is told to revisit.

Decision needed before implementing

Two directions, and they are opposite — this needs a ruling, not a guess:

  1. Declare it. Add a publicPicker block to FormFieldSchema (displayFields, maxResults, filter, object) matching what the route already reads. Makes the capability reachable; adds an authorable property to the ADR-0049 liveness surface.
  2. Retire it. Treat an undeclarable opt-in as a capability nobody can use and remove the route's picker branch under the spec-property-retirement playbook.

Note the existing pin in packages/rest/src/public-form-routes.test.ts ('refuses a publicPicker declared on owner_id') builds its fixture as a raw object that never passes through the schema, which is why the gap was invisible from that side.

Related: #7134, #6926, #3022, #6601, #6920, ADR-0089, ADR-0049, ADR-0106.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions