Skip to content

feat(spec): declare publicPicker on FormFieldSchema (#7467) - #7487

Merged
os-zhuang merged 5 commits into
mainfrom
claude/issue-7467-declare-public-picker
Aug 11, 2026
Merged

feat(spec): declare publicPicker on FormFieldSchema (#7467)#7487
os-zhuang merged 5 commits into
mainfrom
claude/issue-7467-declare-public-picker

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #7467

The ruling, and what lands

Maintainer ruling on the card, verbatim: 「宣告」 — option 1 of the card's fork: declare publicPicker in packages/spec. The retirement direction is closed, and the route's picker branch is untouched (packages/rest carries test-only changes).

GET /forms/:slug/lookup/:field has always gated the anonymous public-form picker on a publicPicker block that no schema declared. FormFieldSchema is strict (ADR-0089 D3a), so ViewMetadataSchema refused any form carrying one (unrecognized_keys → 422 at saveMetaItem; code-authored forms hit the same wall at FormViewSchema.parse). The route's whole picker branch was live code no spec-valid form could turn on — ADR-0049's "declared ≠ enforced" in the mirror direction.

The schema mirrors the route's reads exactly, and nothing wider

Every key and constraint of the new FormFieldPublicPickerSchema is derived from the lookup handler's actual reads in packages/rest/src/rest-server.ts — this block opens an unauthenticated search surface, so the schema admits nothing the route does not enforce, and the route's hard bounds are encoded instead of left to silent request-time adjustment:

Key Route read Schema
displayFields slice(0, 5), fallback ['name'], first entry is the contains search target z.array(z.string()).min(1).max(5).optional() — a 6th entry (or [], which the route treats as absent) is a parse error, not a silent rewrite
maxResults Math.min(Math.max(1, Number(v) || 20), 50) z.number().int().min(1).max(50).optional()51, 0, negatives, fractions refused at authoring time
filter Array.isArray → spread into the same filters list as the route's own { field, operator: 'contains', value: q } row z.array(ViewFilterRuleSchema).optional() — the repo's one filter-rule dialect, #6227 shape coupling included
object referenceTo = picker.object (preferred before the field-def fallback) z.string().optional()

An unknown subkey is still a loud unrecognized_keys (ADR-0089 D3a). An empty block {} parses — the route treats any truthy picker as the opt-in and supplies its own defaults.

The route's fifth read. The handler also reads picker.sort (fallback: first display field, ascending). That key is outside the ruling's four-key enumeration, so it stays deliberately undeclared — pinned as intentional in view-public-picker.test.ts — and is filed as its own decision card, #7485. A second route-side finding from implementation: the field-def fallback for the picker target reads only legacy spellings (referenceTo/target/options.objectName), never the canonical reference, making publicPicker.object de-facto required today — filed as #7486.

The two pins the card names

  1. public-form-routes.test.ts 'refuses a publicPicker declared on owner_id' — rebuilt through the schema. The fixture now parses through the real ViewMetadataSchema first (asserted spec-valid — before this PR that assertion is red), so the pin finally says what it meant: the anchor refusal is the ROUTE's own boundary, not a side effect of the schema refusing the form.
  2. public-form-routes.stored-row.test.ts BOUNDARY: … STILL 403⚠️ that file lives in PR fix(metadata-protocol): the groupssections fold reaches the stored row (#7134) #7468 (A Studio-saved form authored with groups still degrades on the REST public-form routes — the producer fold does not reach stored rows #7134), which is still open; it does not exist on main, so there is nothing to flip in this PR's base. The flip's content lands here instead as a new file, public-form-lookup-picker.test.ts: the real saveMetaItem (stub engine) persists a spec-valid form carrying a picker — the exact save that was a 422 before — and the real route handler answers with projected data (request composition pinned key-for-key: object override, limit, ['id', ...displayFields] select, declared filter rows ahead of the search predicate, offset 0), plus the picker-less GUARD (still 403). If fix(metadata-protocol): the groupssections fold reaches the stored row (#7134) #7468 merges while this PR is open, I will merge main and rewrite its BOUNDARY case; note its fixture carries no picker, so it stays mechanically green either way — the "goes red" lives in its comment's revisit instruction.

ADR-0049 liveness ledger

packages/spec/liveness/view.json form.children.sections — the row carrying the blanket verdict for the FormField subtree (undrilled inheritance, the instrument's declared granularity) — re-verified 2026-08-11, cross-repo, with the lookup route cited as the measured reader for the four new keys and the sort exception recorded. The seeding audit doc (docs/audits/2026-06-viewschema-property-liveness.md) gets the matching post-audit entry.

Generated artifacts (four-step os-regen)

pnpm --filter @objectstack/spec build FIRST (#7122 — one probe build with OS_SKIP_DTS=1 was caught by the generator's own stale-DTS refusal and redone full), then gen:api-surface, gen:export-origins, gen:docs, gen:strictness-ledgercheck:generated all 13 green; check:authorable-surface and check:liveness green. No generated file hand-edited.

Docs

content/docs/ui/forms.mdx documents the lookup endpoint (the page covered the two sibling routes only): the publicPicker table, response shape, error table, and the lookup route's own auth context. No content/docs/releases/ or docs/adr/** touched.

Changeset

.changeset/declare-public-picker.md@objectstack/spec minor: a new authorable property on the public acceptance surface (metadata that yesterday was a 422 parses today); precedent #7387 per #3405/#5583. Nothing previously accepted changes shape.

Reverse verification (predictions written before running)

Probe Predicted Measured
A — delete the publicPicker key wiring 5 positive spec cases red; negatives green 4 positive cases red + the unknown-subkey case red (missed prediction, the good kind: its assertion names the nested key, so it is fix-dependent too). Stronger: the spec build itself refuses — the authorable-surface ratchet reports ui/FormField:publicPicker disappeared from the contract, so a dist without the key cannot even be produced; the REST e2e red is therefore enforced upstream of the test layer (same parse door, proven directly by the spec-layer probes)
B — raise the schema ceiling to .max(51) exactly the maxResults: 51 case red exactly that one case red
C — widen displayFields to .max(6) exactly the 6th-displayField case red exactly that one case red
Picker-less form on the flipped path still 403, green both directions green both directions (GUARD in public-form-lookup-picker.test.ts)

Gates run locally

Gate Result
@objectstack/spec suite ✅ 374 files, 9801 tests
@objectstack/rest suite ✅ 81 files, 1333 tests
@objectstack/spec typecheck (src + scripts + test layer)
@objectstack/rest typecheck
check:generated ✅ all 13 artifacts current
check:authorable-surface / check:liveness
check:doc-authoring ✅ 374 files clean

🤖 Generated with Claude Code

https://claude.ai/code/session_01CnMk7vfrt2zW7xvwLS3JDf


Generated by Claude Code

The REST public-lookup route (GET /forms/:slug/lookup/:field) has always
gated the anonymous picker on a publicPicker block that no schema declared:
FormFieldSchema is strict (ADR-0089 D3a), so every authoring path refused a
form carrying one and the capability was unreachable — ADR-0049's
'declared ≠ enforced' in the mirror direction. Per the maintainer ruling on
the card (declare, option 1), FormFieldSchema now carries an optional
publicPicker block mirroring exactly the route's four reads:

- displayFields (≤5, the route's projection cap; omitted → ['name'])
- maxResults (int 1..50, encoding the route's hard ceiling; default 20)
- filter (ViewFilterRuleSchema[], the dialect the route composes)
- object (referenced-object override)

An unknown subkey stays a loud unrecognized_keys error; picker.sort — a
fifth route read outside the ruling's enumeration — stays deliberately
undeclared and pinned as such (follow-up filed from #7467).

Also: ADR-0049 liveness ledger (view.json sections row + viewschema audit),
the owner_id pin rebuilt through the real schema, a stored-row e2e proving
a spec-valid form with a picker gets a real lookup answer, docs for the
lookup route, regen artifacts, minor changeset.

Closes #7467

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CnMk7vfrt2zW7xvwLS3JDf
@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 11, 2026 2:16am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/permissions/system-context.mdx (via packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…the dispatch predicates (#7467)

check:engine-double-contract flagged the new stub: a fake looser than
ObjectQL's update/delete is how #4434 shipped a dead route green. Both
verbs now open with assertEngineUpdateDispatch/assertEngineDeleteDispatch
from @objectstack/metadata-core (#5619).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CnMk7vfrt2zW7xvwLS3JDf
claude added 3 commits August 11, 2026 01:42
… debt (#7467)

The rest TEST_DEBT ledger is a 155-error ratchet; the new file added a
TS2835 (extensionless relative import under NodeNext) and a TS7006
(implicit-any callback). Explicit .js extension + typed callback — raw
tsc over the test layer measures 155 again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CnMk7vfrt2zW7xvwLS3JDf
…ow answers (#7467)

PR #7468 landed the stored-row fold with a 'BOUNDARY: STILL 403' case
annotated to be revisited the day publicPicker became declarable. This is
the revisit: the same real saveMetaItem persists a groups-authored form
whose field carries a spec-valid publicPicker, and the same real route
handler answers 200 with projected data. The picker-less 403 half stays
pinned in public-form-lookup-picker.test.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CnMk7vfrt2zW7xvwLS3JDf
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants