Repository navigation
publicPicker is enforced by the REST lookup route but declared nowhere in packages/spec — no saved form can ever enable it #7467
Description
Activity
Triage:
needs-user-decision+domain:spec;domain:metadataremoved (mislabel correction, single-producer lane).- Classification: the body itself states the fork — "this needs a ruling, not a guess": (1) declare
publicPickerinFormFieldSchema(adds an authorable property to the ADR-0049 liveness surface) vs (2) retire the route's picker branch under the spec-property-retirement playbook. Both directions change what the platform's public contract admits — a maintainer call, not dispatchable. - Routing: the decision is an acceptance-surface question — direction 1 changes the set of metadata
ViewMetadataSchemaaccepts, and the reverse red line routes any accept/reject-behavior change todomain:spec(precedents meta: bind Zod schemas for webhook / connector / sharing_rule WITHOUT registering the kinds — close the unvalidatedPUT /metawrite hole (#2657 audit, option A) #6245/feat(spec): action param 的 options[] 声明逐选项 visibleWhen (#5016) #6235; boundary-question precedent conversions/registry.ts is now the last hand-authored append registry — extend the #7297 per-entry split? #7464/Splitmigrations/registry.ts's two append tables into per-entry files (registry half of #6957's ruling) #7297). The fix will land inpackages/spec(declare) orpackages/rest(retire), not inpackages/metadata*— the fileddomain:metadata(applied at filing, 14:27:52Z) doesn't match any landing site, so it is removed rather than left to route the card into a lane that can't act on it. - Premise check on
origin/main@2c28df9:git grep publicPicker -- packages/spec/— zero hits insrc/(only CHANGELOG prose at :31184); counter-check with known-present neighbors (maxResults,FormFieldSchema) confirms the scanner. Enforcement side live inpackages/rest/src/rest-server.ts(opt-in gate anddisplayFields/maxResults/filterreads at :7710–:7936). Enforced-never-declarable premise holds. - Dup check: no other open issue/PR in the three repos covers
publicPickerdeclarability (local filter over cached open lists; only this card hits). target:v17: not applied — the capability has never been reachable by any spec-valid form, so no shipped user hits this today; it is a declared/enforced mirror-gap awaiting a direction, not a released-surface defect. If the maintainer rules "declare", the execution card can be re-judged then.
本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
- Classification: the body itself states the fork — "this needs a ruling, not a guess": (1) declare
Maintainer ruling (2026-08-10, spec-lane PM session), verbatim and untranslated: 「宣告」 — option 1 of the card's fork: declare
publicPickerinpackages/spec. The retirement direction is closed.needs-user-decision→pm:queueexecuted on the ruling (keptdomain:spec).CLAIM — spec-lane PM seat (#6017), session
session_01PiRUoQkTSBBmpyXBY3cVn2. Branch:claude/issue-7467-declare-public-picker. Dispatching a cloud dev session (model: Fable — judged design-heavy: a new authorable block on the public acceptance surface, gating PUBLIC unauthenticated lookups, plus two cross-package pin flips the card names).Direction bounds recorded at dispatch:
- Mirror EXACTLY what the route reads (
displayFields,maxResultswith the hard ceiling 50 encoded,filter,object) — derive fromrest-server.ts's actual reads, never wider; this is a security-relevant public-exposure gate. - ADR-0049 ledger entries for the new keys (live — the route is the measured reader).
- Flip both pins: A Studio-saved form authored with
groupsstill degrades on the REST public-form routes — the producer fold does not reach stored rows #7134's stored-rowBOUNDARY: … STILL 403test (written to go red on this fix) and the raw-fixturerefuses a publicPicker declared on owner_idpin (rebuild through the schema). - Four-step os-regen for the moved spec surface (build spec FIRST per gen:api-surface / gen:export-origins read the dist without asserting it is FRESH — a stale dist launders a phantom breaking removal into the committed baseline #7122); changeset judged minor (new authorable surface, feat(spec): declare the settings
visiblegrammar the evaluator actually implements (#7327) #7387/动作参数弹窗:内联 lookup 参数无法声明引用目标(配置被静默剥离 + 文案谎报「即将上线」) #3405/spec:ChartAggregateSchema与ChartGroupBySchemaobject 分支转 strictObject —— #5020 parse 接通后的收紧半边(#4001 后续批) #5583 precedent). - Draft PR, no auto-merge — the PM lands it; OS-DEV-REPORT on this card is the return channel.
Generated by Claude Code
- Mirror EXACTLY what the route reads (
DEV CLAIM — dev seat for this card, dispatched by the spec-lane PM (#6017,
session_01PiRUoQkTSBBmpyXBY3cVn2).- Session:
session_01CnMk7vfrt2zW7xvwLS3JDf - Branch:
claude/issue-7467-declare-public-picker - Scope, per the maintainer ruling 「宣告」: declare
publicPickeronFormFieldSchemainpackages/spec(displayFields,maxResultsceiling 50,filter,object— mirroring exactly what the REST lookup route reads), ADR-0049 ledger entries, flip the two named pin tests, os-regen, minor changeset, draft PR. Retirement direction closed; the route's picker branch stays untouched.
Generated by Claude Code
- Session:
OS-DEV-REPORT — dev seat
session_01CnMk7vfrt2zW7xvwLS3JDf{ "issue": 7467, "outcome": "ci-green", "branch": "claude/issue-7467-declare-public-picker", "pr_number": 7487, "head": "26c8f41", "premise_check": "Re-verified on origin/main@afdc6ea before implementing: zero publicPicker hits in packages/spec/src; the card's fixture { field: 'owner', publicPicker: { displayFields: ['name'] } } measured rejected (unrecognized_keys) through the real ViewMetadataSchema; enforcement live in packages/rest/src/rest-server.ts lookup handler. Premise held.", "changes": [ "packages/spec/src/ui/view.zod.ts: FormFieldPublicPickerSchema (strict) + optional publicPicker key on FormFieldBaseSchema; exported FormFieldPublicPicker / FormFieldPublicPickerParsed (ADR-0122); TSDoc states the block gates the public lookup route and that absence answers 403 LOOKUP_NOT_PUBLIC loud-by-design (#3022). Keys mirror the route's reads exactly: displayFields (min 1, max 5 - the route's slice), maxResults (int 1..50 - the route's hard ceiling encoded), filter (ViewFilterRuleSchema[], the dialect the route composes with its own contains-predicate), object (override the route prefers).", "packages/spec/src/ui/view-public-picker.test.ts: 12 acceptance cases through the real ViewMetadataSchema.safeParse / FormViewSchema.parse - the card's measured-rejected fixture now parses; empty block parses (opt-in with route defaults); unknown subkey still unrecognized_keys; maxResults 51/0/-5/2.5 rejected; 6th displayField and [] rejected; malformed filter row rejected via #6227 coupling; picker.sort pinned NOT declarable.", "packages/rest/src/public-form-routes.test.ts: the 'refuses a publicPicker declared on owner_id' pin rebuilt through the schema - fixture asserted spec-valid via the real ViewMetadataSchema before the route's 403 is asserted (the raw-object bypass that made the gap invisible is gone).", "packages/rest/src/public-form-lookup-picker.test.ts (new): stored-row e2e - the real saveMetaItem (stub engine, dispatch-predicate-pinned update/delete) persists a picker-carrying form that used to 422, and the real lookup handler answers 200 with server-side projection pinned (leaked driver column stripped) and the composed query pinned key-for-key; picker-less GUARD still 403.", "packages/rest/src/public-form-routes.stored-row.test.ts: #7134's 'BOUNDARY: STILL 403' case flipped after PR #7468 merged into main mid-PR - now 'FLIPPED [#7467]': a groups-authored stored form carrying a spec-valid publicPicker gets real lookup data end-to-end (fold + declaration composing); header and fixture comments updated to stop claiming the key is undeclarable.", "packages/spec/liveness/view.json: sections row (the blanket-verdict carrier for the FormField subtree) re-verified 2026-08-11, cross-repo, evidence names the lookup route as the measured reader for the four new keys and records the deliberate sort exception.", "docs/audits/2026-06-viewschema-property-liveness.md: post-audit entry for FormField publicPicker{displayFields,maxResults,filter,object}, live, consumer cited.", "content/docs/ui/forms.mdx: lookup endpoint documented (publicPicker table, response shape, error table incl. 403 LOOKUP_NOT_PUBLIC and 500 LOOKUP_TARGET_MISSING, per-route auth contexts).", "Generated: api-surface/ui.json, export-origins, authorable-surface/ui.json, json-schema.manifest, content/docs/references (gen:docs), strictness-ledger counts - all via generators, none hand-edited." ], "gates": { "ci": "all 26 checks green on 26c8f41 (Console Pin Gate legitimately skipped)", "local": "spec suite 374 files / 9801 tests; rest suite green incl. all three public-form files (32 tests); spec+rest typecheck; check:generated 13/13; check:authorable-surface; check:liveness; check:engine-double-contract; check:doc-authoring; rest test-layer tsc debt held at the frozen 155" }, "reverse_verification": { "probe_A_delete_schema_block": "predicted 5 spec cases red / negatives green; measured 4 positives red + unknown-subkey case red (missed prediction, recorded - its assertion names the nested key so it is fix-dependent), AND the spec build itself refuses: gen:schema's authorable-surface ratchet reports 'ui/FormField:publicPicker disappeared from the contract', so a dist without the key cannot be produced - the REST e2e red is enforced upstream of the test layer", "probe_B_ceiling_51": "predicted exactly the maxResults-51 case red; measured exactly that", "probe_C_displayFields_6": "predicted exactly the 6th-displayField case red; measured exactly that", "guard_pickerless_403": "green in both directions (public-form-lookup-picker.test.ts GUARD)" }, "changeset": { "file": ".changeset/declare-public-picker.md", "bump": "minor", "justification": "new authorable property on the public acceptance surface - metadata that was a 422 yesterday parses today; precedent #7387 per #3405/#5583; nothing previously accepted changes shape; REST route untouched" }, "deviations": [ "The card's stored-row pin file did not exist on main at dispatch (it lived in then-open PR #7468). Handled in two steps: the flip's content shipped first as my own e2e file, then #7468 merged mid-PR (01:32Z) and I merged origin/main (no rebase) and rewrote its BOUNDARY case properly. Note the original BOUNDARY fixture carried no picker, so it would NOT have gone mechanically red on this fix - the 'goes red' lived in its comment; the rewrite honors the intent.", "displayFields also encodes min(1) and max(5), and maxResults is int-restricted - constraints derived from the route's slice/clamp behavior rather than listed verbatim in the card; per the narrower-schema instruction, authoring a value the route would silently rewrite is refused instead.", "One destructive tooling mistake during reverse verification, self-recovered: a git checkout during probe cleanup reverted the not-yet-committed view.zod.ts; re-applied from context, verified by the 12/12 suite and probe reruns, and all subsequent probes ran against committed state." ], "findings_filed": [ "#7485 - publicPicker.sort: the route's fifth read stays enforced-never-declarable after this card; needs its own ruling (declare vs retire the read)", "#7486 - lookup target fallback reads only legacy field spellings (referenceTo/target/options.objectName), never canonical reference: omitted publicPicker.object answers 500 LOOKUP_TARGET_MISSING against canonical schemas, making the override de-facto required" ], "docs_adr_touched": "content/docs/ui/forms.mdx (hand-written), docs/audits/2026-06-viewschema-property-liveness.md, packages/spec/liveness/view.json, generated content/docs/references/**. No content/docs/releases/**, no docs/adr/**.", "open_questions": [ "maxResults is declared integer-only; the route's Number() coercion would technically accept fractions - narrower schema chosen per instruction, flagged here rather than widened.", "filter is typed as ViewFilterRuleSchema[] (the repo's authoring dialect, matching the shape the route itself composes); the route's own guard is only Array.isArray, so stored pre-schema rows with exotic element shapes remain the route's concern, not the schema's." ] }PR #7487 is a draft with CI green; auto-merge NOT enabled — landing is the PM's call per the claim comment.
Generated by Claude Code
- added 3 commits that reference this issue
on Aug 17, 2026
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.