Skip to content

feat(spec,rest,lint)!: retire the form field's publicPicker and the anonymous lookup route (#21180) - #21222

Merged
objectstack-fleet[bot] merged 13 commits into
mainfrom
claude/issue-21180-retire-public-picker
Oct 1, 2026
Merged

objectstack-fleet[bot] merged 13 commits into
mainfrom
claude/issue-21180-retire-public-picker

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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 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, 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 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.
  • 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

claude added 8 commits October 1, 2026 14:46
…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>
…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

# 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>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation protocol:ui tests tooling labels Oct 1, 2026
@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/lint, @objectstack/metadata-protocol, @objectstack/rest, @objectstack/spec, touching 34 documentable anchor(s). ⚠️ 9 changed file(s) yielded no anchor (packages/spec/api-surface/ui.json, packages/spec/authorable-surface/ui.json, packages/spec/declaration-map/ui.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

36 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 3a7b6eb0635827442fa248baffa14187a60f5a22.

⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 9 changed file(s) yielded no anchor (packages/spec/api-surface/ui.json, packages/spec/authorable-surface/ui.json, packages/spec/declaration-map/ui.json, …) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: master_detail (literal, 30 pages)
  • 10 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 140 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 3a7b6eb0635827442fa248baffa14187a60f5a22 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 839406895e3f06efc5beda5d102196e12140aaec — the merge of head 89bfe194274678c602899e5e5fa4efdd383720e1 into base 3a7b6eb0635827442fa248baffa14187a60f5a22, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 3a7b6eb0635827442fa248baffa14187a60f5a22 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…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>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 89bfe194274678c602899e5e5fa4efdd383720e1
Local-runs: none

Inputs: card #21180 (body and all six comments, the claim 5933259335 with its amendments 5937680095 and 5940031210, the pointer 5937051116, the dev report 5937598550 and the ADR-PR record 5937859102), PR #21222 (body, 46-file list, the net diff against main at the merge-base 62b90d74f7, which is #21217's landing), and the head's check-runs read last. Files at the head were read from the fetched ref; nothing was built, run or re-run.

① Derived judgments

Every accept-set and public-surface change the diff implies, each named right or wrong:

  1. FormFieldBaseSchema.publicPicker becomes a retiredKey() tombstone (view.zod.ts): the input type is never, the parse refuses every value with the prescription at publicPicker. RIGHT — card scope 1, the skill's tombstone route for a strict shape. The guidance string keeps the five house conventions (fully-qualified key in backticks first; was removed in @objectstack/spec 17.6.0 (ADR-0087 D2); a dash clause saying why; Delete the key plus the mechanism now in effect; the os migrate meta --from 17 sentence). The version wording follows the precedent: the view.pageName tombstone was written at package version 17.4.0 and says 17.5.0, so a tombstone written at 17.5.0 says 17.6.0.
  2. FormFieldPublicPickerSchema, FormFieldPublicPicker, FormFieldPublicPickerParsed leave @objectstack/spec/ui (api-surface minus 3, export-origins and declaration-map minus the same, ui/FormFieldPublicPicker out of the json-schema manifest and into RETIRED_DEFS_BY_MAJOR[18]). RIGHT — card scope 1 and the skill's orphan-value-schema line; no other reader in the tree at the head (grep: only changelogs, the base anchor the generator owns, and the kit).
  3. ERROR_CODE_LEDGER loses LOOKUP_NOT_PUBLIC and LOOKUP_TARGET_MISSING (the published ErrorCode / ApiErrorSchema.code enum narrows from plus-322 to plus-320 in the regenerated contract reference). RIGHT — the ledger's own rule, quoted at the head: "A row whose last EMITTER is deleted comes out with it ... What still retires a row is the FIRST ground only: no producer left anywhere in packages/** source." The deleted handler was the only producer of both; at the head neither code is spelled outside changelogs, the changeset and a ratchet comment. The card names only the first; the second is the same rule applied to the same deletion, not a widening (leaving it would be exactly the "registered but unemittable row" the rule forbids). No graded tombstone is owed: retired-error-codes.ts is for StandardErrorCode catalogue members, and these were ledger rows (the OVERLAY_PERSISTENCE_FAILED precedent was a plain delete).
  4. GET /api/v1/forms/:slug/lookup/:field is unregistered (handler, its literal guest_portal picker context, the picker-only view-filter-rule-lowering.ts helper with no other importer, and the ledger row all deleted). RIGHT — card scope 2, ruling E's "the anonymous picker route goes". The pin in public-form-routes.test.ts asserts absence from getRoutes(), then on the real HonoHttpServer with installNotFoundSeam() (the seam HonoServerPlugin installs at adapter.ts:1087) a 404 with error.code ENDPOINT_NOT_FOUND, byte-identical path aside to a never-registered sibling, findData never called, and the registered resolve route as lit control. That is what "deleted" means on the served composition: the adapter's answer, not a handler's refusal.
  5. GET /forms/:slug strips lookup / master_detail / user fields unconditionally: the diff deletes the return !!cfg?.publicPicker arm and the cfg parameter; the function is now the type test alone. RIGHT — card scope 2, no new branch, no new gate. The stored-row pin (a pre-retirement row still carrying the block on all three types; only subject renders) proves the strip no longer consults the key. objectSchema still carries those fields' definitions; widening the strip to it would be a new gate, which the card forbids, so leaving it is right and is recorded as an acceptance note.
  6. ADR-0087 registration: D2 form-field-public-picker-removed (toMajor: 18, retiredFromLoadPath: true, retiredAfter: '17.5.0' equal to the package label at the head, order 53 in MAJOR_18_CONVERSIONS, stripKeys over sections[] / groups[] / top-level fields[] with nested recursion, fixture expectedNotices: 2 equal to the stripped-key count); D3 form-field-public-picker-retired with non-empty reason and acceptanceCriteria; ui/FormField:publicPicker under RETIRED_KEYS_BY_MAJOR[18]; ui/FormFieldPublicPicker under RETIRED_DEFS_BY_MAJOR[18]. RIGHT. All three table insertions sit inside the os-generated regions of migrations/registry.ts (semantic:18 at 6116 to 19327, retired-key:18 at 19710 to 23554, retired-def:18 at 23811 to 25655) and mirror the three new entries/ files, so they are the generator's output. The one hand edit, the STEP18_RATIONALE fragment, is what the skill section 3 requires for an 18-step retirement ("rationale: add exactly one STEP18_RATIONALE fragment, inserted at the sort position of your D3 semantic id"), and that table is hand-written by the registry's own header. It is inserted at its id's sort position. Its order: 56 collides with ui-object-master-detail-form-details-closed (also 56, on main since before the merge); the table's header allows it in so many words ("Two retirements in flight may both take the same number, and then render in id order") and step18-rationale-merge.test.ts pins that case. Cosmetic only: the fragment opens with "Finally," and renders before the other 56 by id.
  7. Lint: the publicPicker claiming reader and the PUBLIC_PICKER_KEY constant leave validate-preset-comparands.ts; the graph parameter of the module-private boundObjectOf / bindAncestors goes with it (noUnusedParameters). No exported signature moves. RIGHT — card scope 3; the three B1 / position cases leave with the key they exercised.
  8. Tests: five picker files deleted; public-form-routes.test.ts gains the strip pin and the gone-route pin; public-form-routes.stored-row.test.ts loses its flipped picker case and its two picker stubs; rest-server-query-number-census.test.ts loses the picker ledger row (no count pinned); rest-server-canonical-query-ast.test.ts loses the picker pair and its refusal control (the property that control also held, 400 INVALID_FILTER for an array of condition objects, is the normalizer's and is pinned in metadata-protocol's malformed-filter suite, as the new docblock says). RIGHT.
  9. view-union-branch-focus.test.ts and protocol.save-union-issues.test.ts: the nested-unknown-subkey repro moves from publicPicker.sort to keyField.sort. keyField is the other strictObject block on the form field (view.zod.ts:3346), so the property under test (an unknown subkey in a nested strict block surfaces the ViewItem branch and names sort at the block's path) is carried unchanged, with the same assertions. The picker-carrying ACCEPTED body moves to REFUSED, and the retired key itself becomes a MISDIRECTED case asserting its prescription reaches the author through that door. RIGHT — that is the tombstone's intended verdict, not a pin bent to pass: a retiredKey() body must be refused, and the file records it as a ruled move.
  10. zero-set-masking.dogfood.test.ts: the picker-door case, the zsmask_inquiry object, the view it booted and the defineView import leave; the record-door case fix(plugin-security,spec)!: a non-system caller that carries a principal and resolves no permission set gets the deny baseline at object admission and at the row scope #21217 rewrote is untouched (no hunk reaches it). RIGHT — the second lander takes this case per the card's Serial line and pointer 5937051116.
  11. Census re-pins (numbers only, each equal to a count I took at the head): rest-route-ledger.ts has 82 route: rows over 18 distinct families (was 83); rest-server.ts has 64 this.routeManager.register( sites (was 65), 8 registerPerItemRoute( calls, 16 private register*Endpoints(, 56 enforceAuth (unchanged), so population 64 minus 1 plus 8 equals 71, reachable 19, blind spot 52; BLIND_SPOT_TOTAL_STATIC 66 equals 52 plus 13 plus 1 and RUNTIME 71 equals 57 plus 13 plus 1; authz-conformance.matrix.ts docblock 82 rows / 18 families; minQuerySlots for rest-server.ts 4 equals the 4 query: slots at the head (5 at the base). No assertion is loosened: each floor moved to the measured count, as it sat at the measured count before. RIGHT.
  12. Ratchets banked: check-route-envelope.mjs stringError 43 to 39 and siblingCode 58 to 54, the deleted handler's four { code, error } answers (400 INVALID_REQUEST, 404 FORM_NOT_FOUND, 403 LOOKUP_NOT_PUBLIC, 500 LOOKUP_TARGET_MISSING; the PERMISSION_DENIED arm went through mapDataError and was never a literal). engine-double-contract.pinned.json loses six rows, three verbs for each of the two deleted test files. dropped-refinements.baseline.json loses the two schemas and four sites that carried publicPicker.filter.element (213 to 211, 618 to 614). RIGHT, each a mechanical consequence of the deletion.
  13. Generated and ledger artefacts: authorable-surface/ui.json marks ui/FormField:publicPicker [RETIRED] and drops the def's four rows (the skill's tombstone-plus-whole-def reading); the strictness audit counts drop one strict site (189 to 188); the references regenerate (1522 to 1521 schemas; publicPicker rendered as never with the [REMOVED] prescription). liveness/view.json: the key never had a per-key row (undrilled sections blanket row), so none is removed or added; that row's evidence and note are corrected to record the removal, and verifiedAt stays 2026-08-11 because its cross-repo objectui evidence was not re-verified, which is the honest reading. RIGHT; check:liveness and check:generated are CI's.
  14. Docs: forms.mdx loses the picker section (both tables, the curl and the JSON) and the section-2 picker sentences, replaced by a short statement of the current behaviour and the two alternatives; "None of the three routes" becomes "Neither route". content/docs/releases/ untouched. The platform checklist item moves to revision 2 with its 403 clause now asserting the route's absence. RIGHT — card scope 4.
  15. ADR-0061 is not in this diff. RIGHT — card scope 5 puts it in the separate docs-only PR docs(adr): ADR-0061 D5 — dated note: the anonymous publicPicker search model is retired by ruling E (#21180) #21223 (accepted at 5937859102).
  16. Serial with fix(plugin-security,spec)!: a non-system caller that carries a principal and resolves no permission set gets the deny baseline at object admission and at the row scope #21217 (the second lander): at the merge-base fix(plugin-security,spec)!: a non-system caller that carries a principal and resolves no permission set gets the deny baseline at object admission and at the row scope #21217 had added the section-2 picker prose and a 403 PERMISSION_DENIED row to the picker error table in forms.mdx, and in zero-set-masking the expectRefusedAtAdmission helper and the rewritten record-door case. This diff keeps fix(plugin-security,spec)!: a non-system caller that carries a principal and resolves no permission set gets the deny baseline at object admission and at the row scope #21217's section-2 rewording ("The submit's authorization ...", "For the submit you only need ...") and the helper and record-door case byte for byte; what it removes is the picker-only text (the picker sentences, the picker table including that 403 row, the picker-door case and its fixtures), which is what ruling E assigns to this card. security-plugin.ts and security-service.ts are untouched. RIGHT.
  17. File surface beyond claim 5933259335 as amended at 5937680095 and 5940031210: every one of the 46 files is named by the claim or an amendment. The additions beyond the original claim (view-filter-rule-lowering.ts, the canonical-AST test, LOOKUP_TARGET_MISSING, the ADR-0087 kit and vitest.repo-tests.json, dropped-refinements.baseline.json, the union-branch and save-union repros, protocol.ts's comment, the new retirement pin, the two ratchet files, the checklist item, the strictness audit counts, and the two authz census files) are each a mechanical consequence of the deletion or a re-pin of a count that included the route. None widens the accept set or the public surface beyond the card. RIGHT.
  18. The new tree-scoped absence pin (form-field-public-picker-retirement.test.ts, in the repo project): key-position matcher with an anti-vacuity set, structural exclusions with reasons and no allowlist file, walk roots packages, examples, skills, content, scripts; it also pins the tombstone at four doors, the conversion's notices, idempotence, load-path retirement, stored-row replay and the registration. RIGHT. The radius it walks is already declared for @objectstack/spec in scripts/cross-package-test-inputs.mjs at the head (packages/** for the eleven scanned extensions, examples/** for the five non-code ones plus examples/*/src/**/*.ts, content/**, scripts/**, skills/**), the same roots and extensions the pin reads, so no half-declared tree-scoped pin rides this PR.

② Semver level

.changeset/21180-retire-public-picker.md: @objectstack/spec minor, @objectstack/rest minor, @objectstack/lint patch; a BREAKING banner; the FROM → TO table with the card's one-line fix verbatim ("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 HTML-comment ADR-0087 marker registered form-field-public-picker-removed, form-field-public-picker-retired; the "retirement kit" section in the house shape. The PR body carries Clause-②: yes (narrowing) and the changeset repeats it.

minor, not major, is what the skill requires ("Changeset: @objectstack/spec uses minor, never major: the per-PR Check Changeset runs check-changeset-no-major.mjs, which refuses major inside the launch window"), and the script's own header says the carriers of breaking-ness during the window are the BREAKING banner and the ADR-0087 disposition, both present. The level axis is met: a Clause-② declaration with @objectstack/spec and @objectstack/rest at minor. @objectstack/metadata-protocol moves only a comment and a test, so it owes no entry. The changeset matches what the diff publishes: an accept-set narrowing on @objectstack/spec (tombstone, three exports and one JSON Schema gone, two error codes gone), a route and a helper gone from @objectstack/rest, and a rule reader gone from @objectstack/lint. Clause-②: yes (narrowing) is the right line.

③ Boundary flags

The dev's open_questions is empty. The dev's deviations and the seat's nine questions, each answered:

  1. minor for a BREAKING retirement. Matches the skill and check-changeset-no-major; the BREAKING banner, the Clause-②: yes (narrowing) line and the ADR-0087 marker carry the break, as the script's header prescribes for the launch window. Answered.
  2. LOOKUP_TARGET_MISSING leaves with LOOKUP_NOT_PUBLIC. The ledger's own rule (last emitter deleted; no producer left in packages/**), not a widening; see ① item 3. The same rule asks that no consumer in objectui or cloud read the literal: objectui measured zero at both pins (e420df310f, 31971ff1e2); cloud is NOT MEASURED (the dev's search saw neither the codes nor a lit control). That residual is bounded by the ruling: a cloud reader of either code could only be a caller of the route ruling E deletes, so its branch is dead whichever way the row goes. Answered; recorded for the seat as an acceptance note owed to whoever bumps cloud, not a blocker.
  3. File surface beyond the claim. Each addition is in family (① item 17). Answered.
  4. The deleted route answers 404 ENDPOINT_NOT_FOUND with the seam installed. That is what "deleted" implies: the path is unregistered and the served composition's unmatched-request answer is the whole response (① item 4). On a bare HonoHttpServer without the seam the answer is Hono's text 404 for the picker path and the sibling alike; the pin installs the seam the way the plugin does, so it measures the served shape. Answered.
  5. The union-branch and save-union repros move to keyField; a picker body moves from ACCEPTED to REFUSED. The tombstone's intended verdict, with the measured property carried on the other strict nested block (① item 9). Answered.
  6. The STEP18_RATIONALE hand edit. Required by the skill section 3 and hand-written by the registry's own header; the generated tables are inside their markers (① item 6). The shared order: 56 is permitted by the table's header and pinned by the merge test. Answered.
  7. Strip and tombstone versus the card's FROM → TO. The strip is the type test alone with no new branch; the tombstone's prescription says "Delete the key (the whole publicPicker block) ... use a select field with static options ... put the form behind sign-in", the card's sentence in the house shape; the changeset's one-line fix is the card's sentence verbatim. No new gate. Answered.
  8. The three census re-pins. Numbers only, each equal to a count taken at the head, no assertion loosened and no row deleted (① item 11). Answered.
  9. Second lander after fix(plugin-security,spec)!: a non-system caller that carries a principal and resolves no permission set gets the deny baseline at object admission and at the row scope #21217. fix(plugin-security,spec)!: a non-system caller that carries a principal and resolves no permission set gets the deny baseline at object admission and at the row scope #21217's work in zero-set-masking and forms.mdx is byte-identical apart from the picker-only removals ruling E assigns to this card (① item 16). Answered.
  10. Dev deviation: no per-key liveness row, verifiedAt left. Right; the key sat under the undrilled sections row, which now records the removal (① item 13). Answered.
  11. Dev deviation: refinementSitesThatDidProject 369 left as is, NOT MEASURED. The deleted block's checks were zod bounds, not refinements that project, so no movement is expected; the build's own baseline gate decides. Answered, CI-judged.
  12. Dev deviation: the spec local project not re-run after the one-line ledger-header fix; coverage declared to CI. CI-judged by design of this review. Answered.
  13. Dev acceptance notes (the pending .changeset/21062-picker-queryable-key.md describing the deleted route; ISecurityService.getQueryableFields losing its only REST reader; objectui's parity-test exemption prose; two comment lines in plugin-security/src/zero-set-deny-baseline.test.ts naming the picker): none moves an accept set or a published surface in this diff; the first is the release compiler's, the rest belong to other lines. Noted, not blocking.

Check-runs on 89bfe194274678c602899e5e5fa4efdd383720e1 (read last, after the diff, in six polls five minutes apart until none was in progress or queued; the latest completed_at is 2026-10-01T21:03:30Z): 35 check-runs, 33 success, 2 skipped (Packed-tarball smoke (opt-in), and Console Pin Gate, which only an .objectui-sha move arms and this PR moves none), 0 failed. Among the successes are the gate families this diff touches: Check Changeset (the no-major guard and the level axis), Spec property liveness, Lint & Repo Gates, Build Core, Build Docs, Test Core (all six shards), Dogfood Regression Gate (all three shards), Dogfood Verify CLI, Temporal Conformance, the five type-check runs, Governed Surface Queue Guard, and the three claim and single-writer guards. No red check-run exists on this head, so none is caused by this diff.

Implemented-by: claude/issue-21180-retire-public-picker
Reviewed-by: session_017VaLJnYwhPsanVCe9dMCJU

VERDICT: PASS


Generated by Claude Code

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/xl tests tooling

Projects

None yet

2 participants