Found while implementing #7134 (the groups → sections 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'] } } |
rejected — unrecognized_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:
- 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.
- 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.
Found while implementing #7134 (the
groups→sectionssave-path fold). Filed, not fixed: closing it means deciding where the key belongs inpackages/spec, which is a spec-acceptance change and out of #7134's lane.The gap
GET /forms/:slug/lookup/:fieldinpackages/rest/src/rest-server.tsgates the public lookup on an opt-inpublicPickerblock on the field's declaration in the form spec. Without one it answers403 LOOKUP_NOT_PUBLIC— deliberately loud rather than silent (#3022).That key is declared in no schema:
FormFieldSchemais strict, and ADR-0089 D3a made an undeclared key a loud parse error rather than a silent drop. So a form carrying apublicPickeris refused byViewMetadataSchema:Measured on
origin/main@08363a09f, through the realViewMetadataSchema.safeParse, on theViewItembranch:{ field: 'owner', publicPicker: { displayFields: ['name'] } }unrecognized_keys{ field: 'owner' }saveMetaItemvalidates against that same schema, so a form declaring a picker is a 422 and never becomes asys_metadatarow. Code-authored forms hit the same wall —FormViewSchema.parseis the same door.Consequence
The public-lookup capability is unreachable through any authoring path this repo accepts. The route, its
displayFieldsprojection, itsmaxResultscap (hard ceiling 50), itspublicPicker.filterpre-filter and itspublicPicker.objectoverride 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/:fieldanswering 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'ssectionswalk (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 inpackages/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:
publicPickerblock toFormFieldSchema(displayFields,maxResults,filter,object) matching what the route already reads. Makes the capability reachable; adds an authorable property to the ADR-0049 liveness surface.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.