Repository navigation
feat(spec,rest,lint)!: retire the form field's publicPicker and the anonymous lookup route (#21180) - #21222
Conversation
…e the anonymous lookup route (WIP) Ruling E: anonymous public forms no longer take lookup / master_detail / user fields. The key becomes a retiredKey() tombstone with an ADR-0087 D2 conversion and registry entries; GET /forms/:slug/lookup/:field and its helper module are deleted; the resolve route's strip is unconditional; the lint reader and the picker tests go. Claude-Session: https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU Co-authored-by: Claude <noreply@anthropic.com>
…erate the spec artefacts The def leaves with its only carrier: a RETIRED_DEFS_BY_MAJOR[18] entry, its manifest and authorable-surface lines deleted deliberately, the dropped-refinements ledger corrected as the build printed it, and the api-surface, export-origins, declaration-map, reference docs and strictness counts regenerated by check:generated --fix. Claude-Session: https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU Co-authored-by: Claude <noreply@anthropic.com>
…ed-route pin, as the served composition does Claude-Session: https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU Co-authored-by: Claude <noreply@anthropic.com>
…ADR-0087 registered Also moves the dropped-refinements ledger's header totals with its body (212 → 210 schemas, 617 → 613 sites). Claude-Session: https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU Co-authored-by: Claude <noreply@anthropic.com>
…of the file Claude-Session: https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU Co-authored-by: Claude <noreply@anthropic.com>
…route took four legacy bodies with it (43 → 39, 58 → 54) Claude-Session: https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU Co-authored-by: Claude <noreply@anthropic.com>
…tire-public-picker
Claude-Session: https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU Co-authored-by: Claude <noreply@anthropic.com>
…tire-public-picker # Conflicts: # packages/spec/dropped-refinements.baseline.json
…s counts from the merged tree Claude-Session: https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 4 package(s): 36 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 140 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 839406895e3f06efc5beda5d102196e12140aaec && git checkout 839406895e3f06efc5beda5d102196e12140aaec
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3a7b6eb0635827442fa248baffa14187a60f5a22 89bfe194274678c602899e5e5fa4efdd383720e1 && git checkout -B drift-repro 3a7b6eb0635827442fa248baffa14187a60f5a22 && git merge --no-ff 89bfe194274678c602899e5e5fa4efdd383720e1
node scripts/docs-audit/affected-docs.mjs --json 3a7b6eb0635827442fa248baffa14187a60f5a22
|
…nd-spot census and the query-slot floor after the picker route's deletion The matrix docblock's rest ledger figure 83 -> 82 rows (18 families). The blind-spot census: rest-route-ledger 83/83/0 -> 82/82/0, rest-server 72/19/53 -> 71/19/52 (the picker route was a blind spot, outside registerMetadataEndpoints), totals 67/72 -> 66/71. The canonical-query-AST floor for rest-server.ts 5 -> 4 query slots. Every figure re-measured. Claude-Session: https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU Co-authored-by: Claude <noreply@anthropic.com>
…tire-public-picker # Conflicts: # content/docs/ui/forms.mdx # packages/qa/dogfood/test/public-picker-queryable-key.dogfood.test.ts # packages/qa/dogfood/test/zero-set-masking.dogfood.test.ts # packages/rest/src/public-form-lookup-picker-queryable-key.test.ts
… main's new key on top of the retirement's deliberate deletions Claude-Session: https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs: card #21180 (body and all six comments, the claim ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each named right or wrong:
② Semver level
③ Boundary flagsThe dev's
Check-runs on Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #21180
Clause-②: yes (narrowing)
Retires the form field's
publicPickerblock and deletes the anonymous lookup routeGET /forms/:slug/lookup/:field. This is the second half of ruling E on #21079 (record5933054144, maintainer 「同意E」, 2026-10-01). Anonymous public forms no longer take lookup,master_detailoruserfields. The ruling reverses the #7467 model ("declarepublicPicker"). It is an immediate retirement under ADR-0087 D2: no alias window and no dual spelling. No new gate: the resolve route's existing strip becomes unconditional.The ADR-0061 note rides a separate docs-only PR (Tier H), not this one.
What changed
FormFieldBaseSchema.publicPickeris aretiredKey()tombstone carrying the card's prescription (FROM → TO below).FormFieldPublicPickerSchema,FormFieldPublicPickerandFormFieldPublicPickerParsedare deleted (no other reader).form-field-public-picker-removed(protocol 18,retiredFromLoadPath,retiredAfter: 17.5.0, order 53). OneSTEP18_RATIONALEfragment. D3 semantic entryform-field-public-picker-retired.RETIRED_KEYS_BY_MAJOR[18]getsui/FormField:publicPickerandRETIRED_DEFS_BY_MAJOR[18]getsui/FormFieldPublicPicker. The registry is regenerated bygen:migration-registry, never hand-typed inside its markers.guest_portalpicker context and its picker-only helperview-filter-rule-lowering.ts(no other importer, not exported). The ledger row (rest-route-ledger.ts) goes too. The resolve route's strip loses itspublicPickercondition: lookup /master_detail/userfields are always left off the anonymous rendering.LOOKUP_NOT_PUBLICandLOOKUP_TARGET_MISSINGleaveerror-code-ledger.zod.ts. This follows the ledger's own retirement rule ("A row whose last EMITTER is deleted comes out with it"), so the row is deleted, with no graded tombstone. The picker handler was the only producer of both codes.publicPickerclaiming reader invalidate-preset-comparands.tsis removed, with its cases. The now-unusedgraphparameter ofboundObjectOf/bindAncestorsgoes too (noUnusedParameters).public-form-lookup-picker.test.ts,public-form-lookup-picker-queryable-key.test.ts,public-form-lookup-filter-lowering.test.ts,view-public-picker.test.ts,public-picker-queryable-key.dogfood.test.ts. Re-pinned:public-form-routes.test.ts,public-form-routes.stored-row.test.ts,rest-server-query-number-census.test.ts,rest-server-canonical-query-ast.test.ts,view-union-branch-focus.test.ts,protocol.save-union-issues.test.ts, and the picker door case ofzero-set-masking.dogfood.test.ts. New:form-field-public-picker-retirement.test.ts(tombstone at four doors, the conversion, registration, and a tree-scoped absence pin over the radius already declared for@objectstack/spec).content/docs/ui/forms.mdxis replaced by a short statement of current behaviour. The references regenerate. The platform checklist itemaccess-security.public-form-intakemoves to revision 2: its 403 clause now asserts the route's absence.[RETIRED]), strictness counts and references.dropped-refinements.baseline.jsonis corrected as the build printed it.engine-double-contract.pinned.jsonis regenerated with--write: 6 losses, all from the two deleted test files. Thecheck:route-envelopepins are banked: 43 → 39 and 58 → 54, the four{ code, error }answers the deleted handler carried..changeset/21180-retire-public-picker.md: BREAKING,Clause-②: yes (narrowing), the card's FROM → TO, and the ADR-0087 markerregistered form-field-public-picker-removed, form-field-public-picker-retired.FROM → TO: delete the
publicPickerblock; an anonymous public form no longer offers record search. Use aselectfield with staticoptions, or put the form behind sign-in.The PM's hypotheses, measured
b9087d77e9: 280 lines. By pattern:publicPicker129,FormFieldPublicPicker43,LOOKUP_NOT_PUBLIC19,/lookup/:field35,guest_portal65. No producer exists outside spec, tests, docs and the REST route; no example declares one. After, atdafa22868:publicPicker106,FormFieldPublicPicker10,LOOKUP_NOT_PUBLIC0,/lookup/:field15,guest_portal59. The kit accounts for the residue: the tombstone, the conversion and registry entries, the retirement and union pins, the H2/H3 pins, and generated artefacts (references, authorable-surface, and the base anchor, which only its own generator writes). Untouched history also remains: two dated audits,releases/v15.mdx, the ADR-0061 sentence, and the pending.changeset/21062-picker-queryable-key.md.guest_portalremains as permission-set names (examples, plugin-security tests, the published skill's resolve/submit text) and as the submit route's context. Inrest-server.tsthe picker's literal context was 1 of the 7 lines; the other 6 are the submit route and its docblock. All of those are outside this card.if (t !== 'lookup' && t !== 'master_detail' && t !== 'user') return true; return !!cfg?.publicPicker;. The condition is deleted; the function now returns the type test, with no new branch. Pinned by a stored row carrying the old block on a lookup, amaster_detailand auserfield: only the text fieldsubjectrenders.HonoHttpServer, with the unmatched-request seam installed asHonoServerPlugin.start()installs it,GET /api/v1/forms/test/lookup/owner_idanswers404witherror.codeENDPOINT_NOT_FOUND. That body is byte-identical (path aside) to a never-registered sibling path, andfindDatais never called. The registered resolve route on the same harness answers 200, the lit control.objectuiconsumer check: zero readers at the old pine420df310fand the new pin31971ff1e2(only an exemption reason string in a parity test). Thecloudconsumer check is NOT MEASURED: code search returned zero for both the codes and a lit control ("@objectstack/spec"), so it cannot see that repository.os validateon a fixture. Before, at BASE: the fixture authoringpublicPicker: { displayFields: ['name'], maxResults: 10 }gave exit 0,valid: true. After: exit 1,valid: false. The text face prints the prescription atviews.0.formViews.contact.sections.0.fields.1.publicPicker. The control fixture, identical without the key, gives exit 0 both times..objectui-shaimports neither name ate420df310fnor, after chore(objectui): bump the console pin to 31971ff1e28f (one zod instance in the vendored Console), add a single-zod canary to build-console.sh, and key the release console cache on the spec zod range #21149 moved it, at31971ff1e2: 7 string mentions and 0 import lines, with a lit control of 392 files importing@objectstack/spec/ui. Its spec-parity test enumeratesFormFieldSchema.in.shape, and the tombstone keeps the key there. No pin bump rides this PR.Reverse verification (the fix committed first, then BASE's route code restored)
Run against
rest-server.tsand the lowering module restored from BASE (blobc673773e46, verified on disk by hash). The pins import source, so no rebuild was needed. All three new pins went red in the expected direction:expected [ 'subject', 'contact_id', …(2) ] to deeply equal [ 'subject' ]expected true to be falseexpected 403 to be 404The other 20 cases in the file stayed green. Restore: back to the HEAD blob
b52e360ec3,git diff HEADempty,git statusclean, with a trap on the script.Tests and gates (head
dafa22868, after mergingmainat5e5ce48ce)@objectstack/rest, whole package: 252 files, 4776 passed. Typecheck green, includingcheck:test-typecheck.@objectstack/lint, whole package (pre-merge, untouched bymain): 118 files, 5486 passed. Typecheck green.@objectstack/spec: the local project ran 593 files with one failure, the ledger's header totals (fixed: 212 → 210 schemas, 617 → 613 sites), then 27/27. The repo project: 48 files, 849 passed. After the merge:src/ui,src/conversions,src/migrations, error-code ledger, migrate-sentence, alias-integrity and the merge-shape scripts give 120 files, 4299 passed.check:generated: 15/15 current. Typecheck green, includingcheck:scripts-typecheckandcheck:test-typecheck, which compiles the new test's@ts-expect-error.@objectstack/metadata-protocol:protocol.save-union-issues.test.ts25/25. Typecheck green.@objectstack/cli: unit tier 242 files, 3435 passed. The integration tier is declared to CI.@objectstack/dogfood:zero-set-maskingandexpression-conformance, 8/8. Typecheck green.dispatch-gates --commandson this head gives 138 families. All 138 ran to exit 0.--ranreconciliation: 138 run, 0 NOT-MEASURED, a derived zero with every exit code recorded. Three refusals were cleared by building their prerequisites, not by skipping:check:skill-examplesandcheck:dual-build-cjs-loadsexited 3 until seven packages outside this diff were built.check:pm-dispatch-gatesandcheck:type-check-debtwere re-run without the runner's 480 s cap.**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}and enables no type-aware linting. The 21 touched code files were all linted (--format json): 0 errors, 0 warnings. The repo-widepnpm lintis CI's.check:adr-0087-registration(registered, both ids new here),check:empty-changeset,check-changeset-no-majorandcheck:doc-authoringall pass. The level axis needs this PR's payload, so CI judges it.Serial with #21079 (PR #21217)
At open time PR #21217 is a draft and not merged, so this PR is first of the pair. This branch deletes both of #21062's picker pins and removes the picker door case of
zero-set-masking.dogfood.test.ts, along with the public form and inquiry object only that case booted, since the fixture can no longer carry the retired key. The record-door case is byte-identical. If #21217 lands first,maingets merged here and these deletions are kept.security-plugin.tsandsecurity-service.tsare untouched.Acceptance notes (observed, not filed)
.changeset/21062-picker-queryable-key.mdis an unreleased changeset describing a change to the route this PR deletes. If both ship in one release, the compiled notes will describe a route that no longer exists. Left for the release compiler; this PR does not edit another PR's changeset.objectSchemafromGET /forms/:slugstill publishes the definitions of declared lookup /master_detail/userfields. The ruled strip covers the rendered sections only, and widening it would be a new gate.ISecurityService.getQueryableFieldsloses its only REST reader with the picker (#20935). The method stays: it lives in security(plugin-security): a non-system caller who resolves no permission set is admitted to every object and read with no row scope; an empty set list grants by absence instead of answering the deny baseline #21079's surface.docs/audits/2026-06-*) and the ADR-0096 table name the route as history and are left as written.Deviations
minor, notmajor: the skill andcheck-changeset-no-majorrefusemajorin the launch window. BREAKING is carried by the banner, theClause-②line and the ADR-0087 marker.LOOKUP_TARGET_MISSINGalso leaves the ledger. The card names onlyLOOKUP_NOT_PUBLIC, but the ledger's rule applies to both, and the deleted handler was the only producer of each.view-filter-rule-lowering.ts(the picker-only helper),retired-defs/18.ui__FormFieldPublicPicker.ts(required by the build's manifest-deletion gate),dropped-refinements.baseline.json,engine-double-contract.pinned.json,scripts/check-route-envelope.mjs(ratchet banking),metadata-protocol(a stale comment and a test that rode the key), or the platform checklist item. Each follows mechanically from the deletion.migrations/registry.ts: the generated regions come fromgen:migration-registry. The one hand edit is theSTEP18_RATIONALEfragment outside the markers, which the retirement skill requires.Generated by Claude Code