Skip to content

fix(rest): resolve the public-form lookup picker target from reference only - #17449

Merged
os-justin merged 2 commits into
mainfrom
claude/issue-12920-lookup-picker-reference-only
Sep 10, 2026
Merged

fix(rest): resolve the public-form lookup picker target from reference only#17449
os-justin merged 2 commits into
mainfrom
claude/issue-12920-lookup-picker-reference-only

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #12920

The public-form lookup-picker route GET /forms/:slug/lookup/:field resolved its target
object through a four-spelling tolerant chain — reference ?? referenceTo ?? target ?? options.objectName. It now reads reference, and nothing else.

Clause-②: yes

Re-declared from the delivered diff, not from the claim: this is a wire-visible behaviour
change on a published route. A picker on a stored field def that spells the target
referenceTo / target / options.objectName and carries no reference used to answer
200 with rows from the aliased object; it now answers 500 LOOKUP_TARGET_MISSING and the
data engine is never called. needs:contract-review is on the card and on this PR.

What changed

file change
packages/rest/src/rest-server.ts the fallback read is def?.reference alone; the three tolerant arms are deleted. The block comment is rewritten around the ruling and the reason not to re-widen. The handler-local referenceTo is renamed referenceObject (5 lines, same handler) — a variable named after a spelling this route now refuses is exactly the trap the comment above it warns about.
packages/rest/src/public-form-lookup-picker.test.ts the three "a legacy spelling still resolves" cases become "is NOT resolved", asserting both halves — 500 + nested LOOKUP_TARGET_MISSING, and findData never called. Both of the file's "open production census" notes are replaced with the ruling that closed it.
content/docs/ui/forms.mdx the publicPicker.object row described the retired chain, so this change made it false. It now states the one key the route reads and the status an alias-spelled row gets.
.changeset/lookup-picker-reference-only.md @objectstack/rest: minor, BREAKING banner, ADR-0087 disposition.

The pin is red before and green after — proven by ablation

The valuable half of the ruling is a pin that an alias-spelled target is not resolved,
and a pin that passes in both states is not evidence. Both legs ran from the committed
state, on-disk mutation proved by anchor counts and by git hash-object, restored with
git checkout HEAD -- ... under a trap:

HEAD blob a858fef3a499137d73d68b853ccb4d7ff6d0cbc9
mutation  (the four-arm chain restored in rest-server.ts)
          canonical-only line 1 -> 0 · legacy arms 0 -> 1 · blob 990bae48a27f (differs)
          Test Files 1 failed (1) · Tests 3 failed | 16 passed (19)
          x a stored row spelling the target the LEGACY way (`referenceTo`) is NOT resolved
          x a stored row spelling the target the LEGACY way (`target`) is NOT resolved
          x a stored row spelling the target the LEGACY way (`options.objectName`) is NOT resolved
          AssertionError: expected 200 to be 500
restore   blob a858fef3a499137d73d68b853ccb4d7ff6d0cbc9 (equals HEAD) · `git diff HEAD` 0 bytes
delivered Test Files 1 passed (1) · Tests 19 passed (19)

Exactly the three new pins move; the 16 that do not include the GUARD case (a def with no
target at all still answers 500) and the canonical-key case — so the narrowing did not
make a canonical or partially-migrated def collateral damage. The test resolves
./rest-server.js relative inside its own package, so vitest reads the mutated source
directly; no dist/ sits between the mutation and the assertion.

Verification

what result
pnpm --filter @objectstack/rest test Test Files 188 passed (188) · Tests 3141 passed, 1 skipped, 3142 total
pnpm --filter @objectstack/rest typecheck OK — test layer compiles under tsconfig.test.json, 0 files / 0 errors held in test-typecheck-debt.json
pnpm --filter '@objectstack/rest^...' build 72/72 tasks successful (the closure, then the full package build the two prerequisite gates ask for)
node scripts/pm/dispatch-gates.mjs --ran 89 derived, 89 run, 0 NOT-MEASURED, 0 UNRUN — every family carries a recorded exit code, all 0
pnpm lint exit 0, repo-wide (eslint . --no-inline-config), re-run on the final commit 88e4227f

Two gates exit 3 (PREREQUISITE NOT MET) until their prerequisite is met, and neither is a
finding: check:dual-build-cjs-loads needs every package's dist/, and
check:type-check-debt --re-measure needs the built closure and its own 6144 MB tsc
ceiling — under a caller's tighter NODE_OPTIONS it OOMs and refuses to record. Both are
green once run as CI runs them, and only those green runs are recorded above.

Acceptance notes

Governing text, as the ruling names it:

  • the stance recorded 2026-08-30, verbatim 「同意」「折叠即契约」 — the spec spelling
    reference is the contract, and a stored row spelling the target the old way is a
    producer defect;
  • ADR-0087's fieldReferenceToAlias (id field-reference-to-alias) — the declared,
    removable conversion seam, which converts the reference_to spelling on stored-row
    rehydration and deliberately converts no other, so the tolerance this PR deletes was
    never the conversion layer's;
  • ObjectSchema / FieldSchema refusing relatedTo / referenceTo / target /
    targetObject / lookupObject by name, with a rename hint rather than a fold —
    pinned three ways at the bottom of the same test file.

The ruling: director seat summon #20, decision batch #107 item 5, 2026-09-09T05:22:52Z,
maintainer verbatim 「其他同意」 = option A. Option B (a migration card) and option C
(per-consumer status quo) were refused; the production-census prerequisite was answered by
the maintainer — none to preserve.

Deviations and judgement calls, all six:

  1. CONTRACT_REVIEW_TIER review before enqueue is not performed. The ruling asks for
    it; the tier notice on the card (2026-09-10T03:12Z, rule text merged as pm-dispatch: reserve the contract-review tier for the skills seat and the spec seat’s clause-② review; default tier elsewhere; the director’s tier is the maintainer’s per-summon choice #17294) lifted it
    for this lane, reserving that tier for the skills seat, the spec seat's clause-② review
    and the summoned director. Build and review both ran at the default tier.
  2. Every coordinate in the ruling had moved. rest-server.ts:10502-10545 is now
    :10661-10723 (the comment block plus the chain); the test file's :378-379 census note
    is now :386-397. Located by symbol, as instructed.
  3. A second census note, beyond the one spot the ruling names. The #13137 suite's own
    SCOPE docblock also called the census open. Leaving one sentence in the file saying the
    question is unanswered, next to three cases that answer it, would rot immediately — it
    now records who closed it and that a schema pin was never the instrument for it.
  4. One rename inside the handler. referenceTo to referenceObject, 5 lines, no wire
    or type surface.
  5. content/docs/ui/forms.mdx is in the diff. Not scope creep: the row documented the
    chain being retired, so this change falsifies a published statement. Fixing it here is
    cheaper than a card, and this PR is the only place the two halves are consistent.
  6. The changeset's ADR-0087 category is no-migration-prescription, and that choice
    removed the FROM/TO block a breaking changeset usually owes.
    The ruling says
    not-required; the closed vocabulary then offers exactly one honest category.
    already-registered field-reference-to-alias would be false — that entry converts
    reference_to, a spelling this route never read, disjoint from all three it drops.
    runtime-interface-only and type-surface-only do not fit a route's runtime read, and
    unpublished is untrue. no-migration-prescription is mechanically refused by a body
    carrying a rewrite prescription, so the body carries none — which is also the honest
    position: with zero producers, a write door that refuses the spellings by name and a
    maintainer ruling that no deployment holds such rows, there is no population to migrate.
    ⚠️ If a reviewer thinks a prescription is owed anyway, that is a disagreement with the
    not-required half of the ruling, not with this changeset — say so and it becomes a new
    ledger entry.

Out of scope, noted and not filed. The card body frames this as a two-way choice over
roughly 30 files across 6 packages; triage narrowed it to this route alone and this PR keeps
that boundary. The other tolerant consumers are untouched and no finding is filed for them —
they are that measurement's subject, already recorded on the card, and #13137 covers the
comment defect on this route (⛔ not addressed here; it landed already). The two files this
PR edits in packages/rest/ had 0 holders across the 15 open PRs at claim time, and PR
#17420's packages/rest/ rows stay untouched.

Line budget: not applicable — no skills/** path in the diff.


Generated by Claude Code

…ce` only

The lookup-picker route read a four-spelling tolerant chain
(`reference ?? referenceTo ?? target ?? options.objectName`). Retire the
tolerant tail: the spec spelling is the contract, and a stored row spelling
the target the old way is a producer defect, not a dialect this route
accommodates.

The three cases that pinned "a legacy spelling still resolves" now pin the
refusal in both halves -- `500 LOOKUP_TARGET_MISSING` and the data engine
never called -- and the file's two "open production census" notes are
replaced with the ruling that closed it.

Claude-Session: https://claude.ai/code/session_01DapQyvYrFb1MxSYe7BL2nt
Co-authored-by: Claude <noreply@anthropic.com>
The `publicPicker.object` row described the tolerant chain this branch just
retired, so it now states the opposite of what the route does. Say what the
route does, and name the status an alias-spelled stored row now gets.

Claude-Session: https://claude.ai/code/session_01DapQyvYrFb1MxSYe7BL2nt
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 1 changed file(s) yielded no anchor (packages/rest/src/rest-server.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/rest/src/rest-server.ts) — pages documenting those are invisible to this run
  • 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.

Coarse fallback — 14 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 ab56ea3a1ea6fde6161781c93a013fd494d39193packageMentionDocs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants