Commit 3dc33b2
Fixes #21180
Clause-②: yes (narrowing)
Retires the form field's `publicPicker` block and deletes the anonymous
lookup route `GET /forms/:slug/lookup/:field`. This is the second half
of ruling E on #21079 (record `5933054144`, maintainer 「同意E」,
2026-10-01). Anonymous public forms no longer take lookup,
`master_detail` or `user` fields. The ruling reverses the #7467 model
("declare `publicPicker`"). 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
| surface | change |
| --- | --- |
| spec | `FormFieldBaseSchema.publicPicker` is a `retiredKey()`
tombstone carrying the card's prescription (FROM → TO below).
`FormFieldPublicPickerSchema`, `FormFieldPublicPicker` and
`FormFieldPublicPickerParsed` are deleted (no other reader). |
| ADR-0087 | D2 conversion `form-field-public-picker-removed` (protocol
18, `retiredFromLoadPath`, `retiredAfter: 17.5.0`, order 53). One
`STEP18_RATIONALE` fragment. D3 semantic entry
`form-field-public-picker-retired`. `RETIRED_KEYS_BY_MAJOR[18]` gets
`ui/FormField:publicPicker` and `RETIRED_DEFS_BY_MAJOR[18]` gets
`ui/FormFieldPublicPicker`. The registry is regenerated by
`gen:migration-registry`, never hand-typed inside its markers. |
| rest | The picker handler is deleted, with its literal `guest_portal`
picker context and its picker-only helper `view-filter-rule-lowering.ts`
(no other importer, not exported). The ledger row
(`rest-route-ledger.ts`) goes too. The resolve route's strip loses its
`publicPicker` condition: lookup / `master_detail` / `user` fields are
always left off the anonymous rendering. |
| error codes | `LOOKUP_NOT_PUBLIC` and `LOOKUP_TARGET_MISSING` leave
`error-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. |
| lint | The `publicPicker` claiming reader in
`validate-preset-comparands.ts` is removed, with its cases. The
now-unused `graph` parameter of `boundObjectOf` / `bindAncestors` goes
too (`noUnusedParameters`). |
| tests | Deleted: `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 of `zero-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`). |
| docs | The picker section of `content/docs/ui/forms.mdx` is replaced
by a short statement of current behaviour. The references regenerate.
The platform checklist item `access-security.public-form-intake` moves
to revision 2: its 403 clause now asserts the route's absence. |
| generated / ledgers | Regenerated: api-surface, export-origins,
declaration-map, json-schema manifest, authorable-surface (the def's
four lines deleted deliberately, the key marked `[RETIRED]`), strictness
counts and references. `dropped-refinements.baseline.json` is corrected
as the build printed it. `engine-double-contract.pinned.json` is
regenerated with `--write`: 6 losses, all from the two deleted test
files. The `check:route-envelope` pins are banked: 43 → 39 and 58 → 54,
the four `{ code, error }` answers the deleted handler carried. |
| changeset | `.changeset/21180-retire-public-picker.md`: BREAKING,
`Clause-②: yes (narrowing)`, the card's FROM → TO, and the ADR-0087
marker `registered form-field-public-picker-removed,
form-field-public-picker-retired`. |
**FROM → TO:** delete the `publicPicker` block; an anonymous public form
no longer offers record search. Use a `select` field with static
`options`, or put the form behind sign-in.
## The PM's hypotheses, measured
- **H1, the census.** At BASE `b9087d77e9`: 280 lines. By pattern:
`publicPicker` 129, `FormFieldPublicPicker` 43, `LOOKUP_NOT_PUBLIC` 19,
`/lookup/:field` 35, `guest_portal` 65. No producer exists outside spec,
tests, docs and the REST route; no example declares one. After, at
`dafa22868`: `publicPicker` 106, `FormFieldPublicPicker` 10,
`LOOKUP_NOT_PUBLIC` 0, `/lookup/:field` 15, `guest_portal` 59. 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_portal` remains as
permission-set names (examples, plugin-security tests, the published
skill's resolve/submit text) and as the submit route's context. In
`rest-server.ts` the 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.
- **H2, the strip depended on the key.** It did: `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, a `master_detail` and a `user`
field: only the text field `subject` renders.
- **H3, gone, not refused.** On the in-process `HonoHttpServer`, with
the unmatched-request seam installed as `HonoServerPlugin.start()`
installs it, `GET /api/v1/forms/test/lookup/owner_id` answers `404` with
`error.code` `ENDPOINT_NOT_FOUND`. That body is byte-identical (path
aside) to a never-registered sibling path, and `findData` is never
called. The registered resolve route on the same harness answers 200,
the lit control.
- **H4, the ledger rule.** Deletion, per the rule's own text. The
`objectui` consumer check: zero readers at the old pin `e420df310f` and
the new pin `31971ff1e2` (only an exemption reason string in a parity
test). The `cloud` consumer check is NOT MEASURED: code search returned
zero for both the codes and a lit control (`"@objectstack/spec"`), so it
cannot see that repository.
- **H5, `os validate` on a fixture.** Before, at BASE: the fixture
authoring `publicPicker: { displayFields: ['name'], maxResults: 10 }`
gave exit 0, `valid: true`. After: exit 1, `valid: false`. The text face
prints the prescription at
`views.0.formViews.contact.sections.0.fields.1.publicPicker`. The
control fixture, identical without the key, gives exit 0 both times.
- **Pin stop condition.** objectui at `.objectui-sha` imports neither
name at `e420df310f` nor, after #21149 moved it, at `31971ff1e2`: 7
string mentions and 0 import lines, with a lit control of 392 files
importing `@objectstack/spec/ui`. Its spec-parity test enumerates
`FormFieldSchema.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.ts` and the lowering module restored from BASE
(blob `c673773e46`, verified on disk by hash). The pins import source,
so no rebuild was needed. All three new pins went red in the expected
direction:
- strip: `expected [ 'subject', 'contact_id', …(2) ] to deeply equal [
'subject' ]`
- route registered: `expected true to be false`
- route answer: `expected 403 to be 404`
The other 20 cases in the file stayed green. Restore: back to the HEAD
blob `b52e360ec3`, `git diff HEAD` empty, `git status` clean, with a
trap on the script.
## Tests and gates (head `dafa22868`, after merging `main` at
`5e5ce48ce`)
- `@objectstack/rest`, whole package: 252 files, 4776 passed. Typecheck
green, including `check:test-typecheck`.
- `@objectstack/lint`, whole package (pre-merge, untouched by `main`):
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,
including `check:scripts-typecheck` and `check:test-typecheck`, which
compiles the new test's `@ts-expect-error`.
- `@objectstack/metadata-protocol`: `protocol.save-union-issues.test.ts`
25/25. Typecheck green.
- `@objectstack/cli`: unit tier 242 files, 3435 passed. The integration
tier is declared to CI.
- `@objectstack/dogfood`: `zero-set-masking` and
`expression-conformance`, 8/8. Typecheck green.
- Derived gates: `dispatch-gates --commands` on this head gives 138
families. All 138 ran to exit 0. `--ran` reconciliation: 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-examples` and `check:dual-build-cjs-loads` exited 3 until
seven packages outside this diff were built. `check:pm-dispatch-gates`
and `check:type-check-debt` were re-run without the runner's 480 s cap.
- Lint, as a proven narrowing: eslint's own config lints
`**/*.{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-wide `pnpm lint` is CI's.
- Changeset gates, against BASE: `check:adr-0087-registration`
(registered, both ids new here), `check:empty-changeset`,
`check-changeset-no-major` and `check:doc-authoring` all 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, `main` gets merged here and these
deletions are kept. `security-plugin.ts` and `security-service.ts` are
untouched.
## Acceptance notes (observed, not filed)
- `.changeset/21062-picker-queryable-key.md` is 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.
- `objectSchema` from `GET /forms/:slug` still publishes the definitions
of declared lookup / `master_detail` / `user` fields. The ruled strip
covers the rendered sections only, and widening it would be a new gate.
- `ISecurityService.getQueryableFields` loses its only REST reader with
the picker (`#20935`). The method stays: it lives in #21079's surface.
- objectui's parity test carries an exemption reason that describes the
retired route. It stays accurate until objectui moves to the spec
release that carries this; then its prose is stale (the key stays in the
shape).
- Two dated audits (`docs/audits/2026-06-*`) and the ADR-0096 table name
the route as history and are left as written.
## Deviations
- Changeset grade is `minor`, not `major`: the skill and
`check-changeset-no-major` refuse `major` in the launch window. BREAKING
is carried by the banner, the `Clause-②` line and the ADR-0087 marker.
- `LOOKUP_TARGET_MISSING` also leaves the ledger. The card names only
`LOOKUP_NOT_PUBLIC`, but the ledger's rule applies to both, and the
deleted handler was the only producer of each.
- The claim's file surface did not name `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 from
`gen:migration-registry`. The one hand edit is the `STEP18_RATIONALE`
fragment outside the markers, which the retirement skill requires.
---
_Generated by [Claude
Code](https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent be5a83c commit 3dc33b2
46 files changed
Lines changed: 1066 additions & 3241 deletions
File tree
- .changeset
- content/docs
- references
- api
- ui
- ui
- docs
- audits/2026-07-unknown-key-strictness-ledger.counts
- qa/platform-checklist/areas
- packages
- lint/src
- metadata-protocol/src
- qa/dogfood/test
- rest/src
- spec
- api-surface
- authorable-surface
- declaration-map
- export-origins
- json-schema.manifest
- liveness
- src
- api
- conversions
- migrations
- entries
- retired-defs
- retired-keys
- semantic
- ui
- scripts
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
31 | | - | |
| 31 | + | |
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
| |||
218 | 218 | | |
219 | 219 | | |
220 | 220 | | |
221 | | - | |
222 | | - | |
223 | 221 | | |
224 | 222 | | |
225 | 223 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
385 | 385 | | |
386 | 386 | | |
387 | 387 | | |
388 | | - | |
389 | | - | |
390 | 388 | | |
391 | 389 | | |
392 | 390 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| |||
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
36 | | - | |
37 | | - | |
| 36 | + | |
| 37 | + | |
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
| |||
364 | 364 | | |
365 | 365 | | |
366 | 366 | | |
367 | | - | |
| 367 | + | |
368 | 368 | | |
369 | 369 | | |
370 | 370 | | |
| |||
385 | 385 | | |
386 | 386 | | |
387 | 387 | | |
388 | | - | |
| 388 | + | |
389 | 389 | | |
390 | 390 | | |
391 | 391 | | |
| |||
0 commit comments