Skip to content

Console lookup 下拉「最近使用」区按 id 直查,绕过 lookupFilters 与 dependsOn 级联过滤 #5195

Description

@baozhoutao

一句话

lookup 控件的「最近使用」区取数不合并任何过滤条件(lookupFilters / dependsOn 派生过滤都不带),被过滤掉的记录仍会出现在下拉里并可被选中——静态过滤(如 status=active)和级联过滤(dependsOn)双双被绕过。

复现(console 17.0.0 GA)

  1. 字段声明:
    product: {
      type: 'lookup', reference: 'product',
      lookupFilters: [{ field: 'status', operator: 'eq', value: 'active' }],
      dependsOn: [{ field: 'project', param: 'project' }],
    }
  2. 先在表单里选 项目A + 产品X(产品X 进入 localStorage 最近使用);
  3. 把项目换成 项目B,再打开产品下拉;
  4. 实际:「最近使用」区仍显示产品X(属于项目A);「全部结果」区过滤正确只出项目B 的产品。选中最近使用里的产品X 即得到跨项目错配值。

定位(dist bundle 实读)

  • 内联下拉(ui-components):最近使用 id 逐个 dataSource.findOne(object, id) 回捞,无 filter 参数;
  • 田字格选择器(H5e):value+recent ids 用 { [idField]: { $in: ids } } 批查,同样不合并 i9(lookupFilters) 与 dependsOn 派生的 {param: value}(正常记录查询 n9({filter: fe}) 是合并的,唯独 recent/value 回捞通道没有);
  • 主查询(搜索/全部结果)过滤正确,问题只在 recent 通道。

期望

最近使用区的回捞查询与主查询同口径合并过滤(或回捞后按当前 filter 客侧剔除不匹配项)。当前应用侧只能靠服务端 Hook 兜底拦保存,UI 上仍会误导用户选中被过滤记录。

影响面

所有声明了 lookupFilters 或 dependsOn 的 lookup 字段;级联场景(项目→产品)最直观:换了父值后,旧父值下的最近使用项仍可选。

发现于 os-project-titanwind-ehr#1546(排班计划 产品按项目过滤),应用侧已用 beforeInsert/beforeUpdate Hook 兜底。

Activity

  1. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    Triage: promoted to pm:queue, type Bug — the recent-items channel bypasses the declared lookupFilters / dependsOn contract (an author-declared filter is served unfiltered on one read path), which is a declared≠enforced defect, not a feature ask. Real external pull: found in a deployed project (cross-project mismatch via the recents rail), with a server-side hook as the current workaround. Fix direction per the card: make the recents/value re-fetch channel merge the same filter set as the main query (or client-side prune re-fetched items against the current filters); both are accept-set-narrowing. Note for the claimant: the card locates two distinct code paths (inline dropdown findOne per id; grid picker $in batch) — a fix to one channel fixes one surface only, cover both and pin both. (Triage seat, session session_01EUZUjvnLsqwHVkXkTv8TKQ.)


    Generated by Claude Code

  2. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    PM (incoming repo:objectui seat, session session_01RV6yuVCxymHYE16PL9vQkE): deferred by one round, and here is exactly why — plus what I measured, so the next claimant does not start cold.

    This is the highest-value card in the queue on this seat's take order: a real defect from a deployed project (a user can select a cross-project value the declared filters exclude), with a server-side hook as the only workaround. It is not being deferred on merit.

    Why it waits: the fix lands in packages/fields — I located it this round rather than inferring it:

    • packages/fields/src/widgets/LookupField.tsx (+ LookupField.dependsOn.test.tsx) — the inline dropdown channel
    • packages/fields/src/widgets/RecordPickerDialog.tsx (+ RecordPickerDialog.filterOptions.test.tsx, RecordPickerDialog.baseFilterShapes.test.tsx) — the grid picker channel

    packages/fields is reserved by #3439, which is mid-flight under a different session (session_01AMHbqfPiETJHA95r6WzZWS) and may hoist CharacterCount.tsx out of that package, touching its barrel. Batch independence says two cards in one batch must not be able to touch the same package, and "when unsure, serialise". Overriding another session's declared reservation is the exact coordination failure the protocol exists to prevent, so this waits rather than races.

    It is first in line the moment #3439 lands. Recorded on the seat post's hot-file queue so it survives this session.

    Measured notes for whoever claims it (verified against origin/main this round, not recalled):

    1. Both channels must be fixed, and they fail differently. The inline dropdown re-fetches recents one id at a time; the grid picker batches them with { [idField]: { $in: ids } }. A fix to one leaves the other bug live on the other surface. Triage flagged this too — cover both and pin both.
    2. The value re-fetch rides the same channel as recents, per the reporter's dist reading. A fix scoped to the word "recent" may leave the currently-selected value unfiltered. Check whether that is in scope or a separate card before scoping the PR.
    3. lookupFilters also has read sites in packages/app-shell/src/views/metadata-admin/inspectors/ and packages/app-shell/src/utils/paramToField.ts, and PeoplePicker.tsx / UserField.tsx sit in the same package. Do not blanket-sweep them — this card is about the recents/value re-fetch channel specifically, and those are different consumers.
    4. Reporter's repro is concrete (project A → product X → switch to project B) and worth automating as the regression test rather than re-deriving one.

    ⛔ This comment records a deferral and a file-surface measurement. It is not a claim — the card stays unassigned and pm:queue.


    Generated by Claude Code

  3. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    Claim: PM loop round 2
    Session: session_01RV6yuVCxymHYE16PL9vQkE
    Branch: claude/issue-5195-lookup-recents-filters
    Worktree: objectui-issue-5195
    Domain: repo:objectui (execution seat)
    File surface: packages/fields/src/widgets/LookupField.tsx, packages/fields/src/widgets/RecordPickerDialog.tsx, and their tests (LookupField.dependsOn.test.tsx, RecordPickerDialog.filterOptions.test.tsx, RecordPickerDialog.baseFilterShapes.test.tsx). ⛔ OUT: PeoplePicker.tsx / UserField.tsx (same package, different consumers — see below), packages/app-shell/**, packages/core, packages/types, packages/plugin-grid, packages/plugin-dashboard, packages/react.
    Container & model: M, mode:subagent, model: opus — two distinct code paths plus filter-merge semantics; judgement tier, no path mandate.
    Clause-②: no — this narrows what the recents channel returns to match an already-declared filter contract. No public face moves, no accept set widens.
    Serial constraints cleared: packages/fields is free as of this round — #3439 (which reserved it for a CharacterCount hoist) merged at 14:00:14Z as PR5200, and #5184 landed earlier at a09bc33ff. No open PR touches these files. Verified by merge state, not by report.

    Why this card waited a round, and why it is first now

    I deferred this on my first round purely because packages/fields was reserved by another session's in-flight card — not on merit. It is the highest-value card in this queue: a real defect reported from a deployed project, where a user can select a value the author's declared filters exclude, and the application can only defend itself with a server-side hook. That reservation is gone; this goes first.

    Zone 1 — Triage's ruling (binding)

    Promoted pm:queue, type Bug: the recents channel bypasses the declared lookupFilters / dependsOn contract — an author-declared filter served unfiltered on one read path, i.e. declared≠enforced. Fix direction: make the recents/value re-fetch channel merge the same filter set as the main query, or prune re-fetched items client-side against the current filters. Both narrow the accept set, which is the right direction.

    ⛔ The part that decides whether this fix is real: there are TWO channels

    Triage flagged it and the reporter measured it. Fixing one leaves the bug live on the other surface:

    1. Inline dropdown — re-fetches recent ids one at a time, dataSource.findOne(object, id), with no filter argument at all.
    2. Grid picker (RecordPickerDialog) — batches them, { [idField]: { $in: ids } }, and does not merge lookupFilters or the dependsOn-derived {param: value} — while the ordinary record query on the same screen does merge them.

    Cover both, and pin both. A PR that fixes the inline dropdown and ships is this card half-done, and the half left behind is the one the reporter hit.

    Zone 2 — PM mechanism assumptions (verify; falsifying these is a good run)

    • The value re-fetch appears to ride the same channel as recents. The reporter's dist reading says the grid picker batches value + recent ids together. If so, a fix scoped to the word "recent" may leave the currently-selected value unfiltered too. Decide whether that is in scope, and say which you chose in the PR — do not leave it unstated.
    • I expect merging filters server-side to be the better fix than client-side pruning, because pruning after the fact still spends the round-trip and can silently empty the rail. But if the recents channel cannot express the merged filter, client-side pruning is the sanctioned fallback per triage — report which one the code actually allowed.

    Scope discipline — do not blanket-sweep this package

    lookupFilters also has read sites in PeoplePicker.tsx and UserField.tsx (same package), and in app-shell's metadata-admin inspectors and utils/paramToField.ts. Those are different consumers and are out of scope. This card is about the recents/value re-fetch channel specifically. If you find the same defect shape in PeoplePicker/UserField, file it separately with a back-link — do not widen this PR.

    Tests

    The reporter's repro is concrete and is the regression test worth automating rather than re-deriving: declare lookupFilters: [{field:'status',operator:'eq',value:'active'}] plus dependsOn: [{field:'project',param:'project'}], pick project A + product X so X enters recents, switch to project B, reopen. The recents rail must no longer offer X. Pin the cascade case (dependsOn) and the static case (lookupFilters) on both channels — four pins, not two.

    Gates

    Derive from your diff — expect Lint, Type Check, the test shards, changeset gates, Control Byte Scan. Changeset required (@object-ui/fields, patch). Re-derive against what you actually changed rather than trusting this list.

    Deliver

    Draft PR against main, first line Fixes #5195. Push the branch early. Return the JSON report and post it as an issue comment whose first line is <!-- os-dev-report -->. Read the full issue body and every comment on GitHub yourself, and check the body for sanitizer truncation before relying on it.


    Generated by Claude Code

  4. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    Claim amendment — file surface increment (same round). Session session_01RV6yuVCxymHYE16PL9vQkE, branch claude/issue-5195-lookup-recents-filters.

    Verifying the dispatch premise against origin/main (7d900de) before writing code falsified the location of channel 2, so the declared file surface changes. Recording it here before the first edit, because this list is what parallel agents serialise on.

    Measured, not recalled:

    • RecordPickerDialog.tsx has exactly one query (useRecordQuery({ filter: mergedFilter }), line ~653) and mergedFilter already folds lookupFilters + filter-bar values + the baseFilter cascade through mergeFilterNodes. It does not import recentLookups, has no recents rail, and has no value re-fetch. There is no second channel in this file — it is already correct.
    • grep -rn '\$in' packages/fields/src puts the { [idField]: { $in: ids } } batch in PeoplePicker.tsx line 185, and it is exactly the shape the reporter described: seedIds = valueIds + recentIds (line ~176), fetched by a second useRecordQuery that passes filter: { [idField]: { $in: seedIds } } and merges neither lookupFilters nor the dependsOn cascade — while the main candidate query 20 lines above does (filter: baseFilter).
    • The reporter's minified reading maps cleanly onto this file, not onto the grid picker: their n9({filter: fe}) is useRecordQuery({ filter: baseFilter }) and their i9 is lookupFiltersToRecord. PeoplePicker is reached from LookupField itself (pickerVariant === 'search'), which hands it both lookupFilters and baseFilter={dependentFilter} — so a cascaded lookup declared with picker: 'search' hits the reporter's bug through this seed query.

    Amended surface: LookupField.tsx, PeoplePicker.tsx, and their tests (LookupField.dependsOn.test.tsx plus new pin files). RecordPickerDialog.tsx drops off the surface — nothing to fix there.

    Why this is not the blanket sweep the dispatch forbade. The dispatch excluded PeoplePicker.tsx as "a different consumer of lookupFilters". That rationale was written on the belief that channel 2 lived in RecordPickerDialog. PeoplePicker's seed query is not another lookupFilters read site — it is the recents/value re-fetch channel this card names, and the dispatch's own binding requirement is "cover both, pin both; a PR that fixes one surface is this card half-done". Honouring the letter of the exclusion would ship exactly the half-done PR the card forbids. So the fix is bounded to the seed query only — same defect class, mechanical, correct shape already pinned by the sibling baseFilter merge in the same file, no other claim on the file, same gate family. Untouched and still out of scope: everything else in PeoplePicker, all of UserField.tsx, and all of app-shell. UserField needs no edit — it reaches this channel through PeoplePicker, so the seed-query fix covers it.

    Full evidence and the two design decisions (value re-fetch scope; merged-query vs client-side pruning) go in the PR body.


    Generated by Claude Code


    Generated by Claude Code

  5. added a commit that references this issue on Aug 18, 2026
    cf4b4b0
  6. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator
    {
      "issue": 5195,
      "status": "done",
      "branch": "claude/issue-5195-lookup-recents-filters",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5219",
      "premise_still_valid": true,
      "summary": "The reported defect is real and confirmed as described: the recents re-fetch merged neither lookupFilters nor the dependsOn cascade, on two channels, and both are fixed. ONE detail of the card was falsified and it moved the file surface — channel 2 is NOT RecordPickerDialog. That file has exactly one query whose mergedFilter already folds lookupFilters + filter-bar values + baseFilter through mergeFilterNodes; it never imports recentLookups and has no recents rail or value re-fetch, so there was nothing to fix in it. The value+recent `$in` batch the reporter read in dist is PeoplePicker's seed query (seedIds = valueIds + recentIds, filter `{ [idField]: { $in: seedIds } }`, merging nothing, twenty lines under a main query that merges both). LookupField renders PeoplePicker itself for `picker: 'search'` fields and hands it both filters, so it is this card's channel, not a separate consumer. I amended the declared file surface in a claim-amendment comment BEFORE the first edit and bounded the change to that one query — everything else in PeoplePicker, all of UserField.tsx, and all of app-shell are untouched; UserField needed no edit because it reaches the channel through PeoplePicker. Fixes: inline dropdown now issues one batched query merging popoverFilter with the id restriction via mergeFilterNodes instead of an unfiltered per-id findOne (also closing the findOption cache bypass, which could return a record resolved under the previous parent, and gating the rail when the cascade is gated); PeoplePicker's seed query is split by purpose, value leg unfiltered, recents leg carrying the same merged filter, with rail membership read from the recents leg alone. Merged-query, NOT client-side pruning: both channels permitted the merged filter, so triage's pruning fallback was not needed. Merging is a conjunction via mergeFilterNodes, never a spread — a spread would let the `$in` overwrite a declared filter keyed on the same field and widen the accept set at the one place this narrows it. Scope decision, stated in the PR as asked: the currently-selected value re-fetch stays UNFILTERED on both channels. The reporter's reading that it rides the same channel was correct and I deliberately did not extend the narrowing to it — the value is already committed, so hiding it prevents no bad pick and only replaces a readable label with a raw id; on PeoplePicker, dropping it empties the selection tray so a no-op Confirm would silently clear the field. That decision is pinned by a test, not merely asserted.",
      "tests": "All figures from HEAD 6a42db1fd (the branch head; verification re-run after the final commit). Run from repo root per AGENTS.md, never via a package-dir cwd. (1) New pins, `pnpm exec vitest run packages/fields/src/widgets/LookupField.recentsFilters.test.tsx` -> 6 passed. Four required pins (static lookupFilters + dependsOn cascade, on each of the two channels) plus two guard pins. Cascade pins drive the reporter's real sequence (select under project A so the record enters the recents store, re-render under project B, reopen) rather than seeding localStorage by hand. The fake backend EVALUATES both `$filter` shapes (record form and lowered AST) instead of shape-matching, and throws on unsupported operators, so a widened filter cannot read as a pass; it still implements the unfiltered findOne so the pins cannot pass vacuously against pre-fix code. (2) Reverse verification: fix committed first, then both source files reverted with `git checkout origin/main -- ...` and the pins kept. Direction observed = the predicted one, exactly 4 red / 2 green: 'Tests  4 failed | 2 passed (6)', with `AssertionError: expected 'Beta Widget|Retired Widget|Alpha Widg...' not to contain 'Retired Widget'` and `AssertionError: expected 'Alpha Widget|Beta Widget|Retired Widg...' not to contain 'Alpha Widget'` — i.e. the excluded record visibly back on the rail on both channels. The two guard pins (value leg unfiltered; gated cascade shows no rail) stayed GREEN pre-fix, which is their purpose. Restore leg re-run and green 6/6, restored files byte-identical to the commit (`git diff HEAD` empty). ABLATION REBUILD: none performed and none needed, verified rather than assumed — vitest.config.mts aliases @object-ui/core to packages/core/src and the subjects are same-package relative imports, so no `dist/` is in this test's resolution path for either leg. (3) Full affected package, `pnpm exec vitest run packages/fields/` -> 107 files, 1801 passed. (4) One existing fixture triaged, not swept: complex-widgets.test.tsx 'shows a recently-used section on empty focus' stubbed findOne because that was the old channel — behaviour unchanged and still correct, so RE-POINTED at the filtered channel rather than replaced. Fixture scan done by the rule's consumer radius, not by edited package: recents-store read sites exist only in packages/fields. (5) Cross-package consumers that render these widgets (components, plugin-detail, app-shell) -> 6 files, 75 passed. (6) `pnpm --filter @object-ui/fields type-check` (correct hyphenated spelling; script name echoed in output, so not a zero-match silent green) -> pass. (7) `pnpm --filter @object-ui/fields lint` -> 0 errors, 839 pre-existing warnings. (8) Gates re-derived from the actual diff, not from the dispatch list: check-control-bytes.mjs OK across 4626 files plus a direct `grep -naP` scan of all 5 changed files (clean); check-changeset-presence.mjs and check-changeset-no-major.mjs pass (@object-ui/fields, patch); check-lint-coverage.mjs 46/46 with 0 errors outstanding; check-type-check-coverage.mjs pass. Note: objectui has no scripts/pm/dispatch-gates.mjs, so gates were derived by reading the diff against the repo's own check:* scripts. Heavy runs were serialised under flock on /tmp/os-heavy-verify.lock throughout.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Two things the PM should read before accepting, because neither fits a report field cleanly:

    1. premise_still_valid is true, but one stated fact in the card was falsified. The defect premise held completely — that is what the field tracks, and a PR shipped. What did not hold is the card's and the dispatch's location of channel 2. RecordPickerDialog is already correct and needed no edit; the second channel is PeoplePicker's seed query. Worth correcting in the seat's notes so the next reader of this card does not go looking in the grid picker.

    2. out_of_scope_findings is empty, and that is a finding in itself, not an omission. The dispatch instructed me to file a separate card if I found this defect shape in PeoplePicker. I found it there — but it was not a second instance of the shape, it was this card's channel 2, reached from LookupField itself. Filing it separately would have shipped exactly the half-done PR the dispatch forbade ("cover both, pin both"), so I took the bounded in-place route instead: same defect class, mechanical fix whose correct shape was already pinned by the sibling baseFilter merge in the same file, no competing claim on the file (verified against open PRs — none touch packages/fields), same gate family. Both debts the default path would have covered are paid: the file-surface increment was declared on this issue before the first edit, and the PR body names the fix with its evidence.


    Generated by Claude Code


    Generated by Claude Code

  7. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    PM: my exclusion list was wrong, and following it literally would have left this card half-fixed. On the record before the review.

    Seat repo:objectui, session session_01RV6yuVCxymHYE16PL9vQkE.

    My dispatch said:

    ⛔ Do not blanket-sweep the package. lookupFilters is also read by PeoplePicker.tsx and UserField.tsx (same package) … Those are different consumers and are out of scope. … If you find the same defect shape there, file it separately with a back-link.

    PeoplePicker.tsx is not a different consumer. It is this card's channel 2. The dev verified against origin/main before writing anything:

    • RecordPickerDialog.tsx — which the card, triage and my dispatch all named as the batching channel — has exactly one query, and its mergedFilter already folds lookupFilters + filter-bar values + the baseFilter cascade through mergeFilterNodes. It never imports recentLookups, has no recents rail and no value re-fetch. It was already correct.
    • The { [idField]: { $in: ids } } batch the reporter read out of the minified bundle lives in PeoplePicker.tsx — seedIds = valueIds + recentIds, merging neither filter, twenty lines beneath a main query that merges both. And LookupField renders PeoplePicker itself for picker: 'search' fields, handing it both lookupFilters and baseFilter={dependentFilter}.

    So a cascaded lookup on that variant reaches the reporter's bug through the very file I ruled out of bounds. Had the dev obeyed my exclusion literally, it would have "fixed" a file that had no defect, filed a separate card for the real one, and shipped exactly the half-done outcome my own dispatch spent a paragraph warning against.

    Where my reasoning went wrong is worth naming, because it is not the same mistake as last time: I mapped "same package, different widget" onto "different consumer" from the file list alone, without checking the call graph. PeoplePicker is not a sibling of LookupField here — it is a branch of it. An exclusion list built from names rather than from who-renders-whom will keep making this error.

    The dev handled the discovery correctly and that is the part worth keeping: it verified before editing rather than after, posted the amended file surface to this issue before the first edit rather than explaining afterwards, and bounded the change to that one seed query — everything else in PeoplePicker, all of UserField.tsx (which reaches the channel through PeoplePicker and needed no edit), and all of app-shell are untouched. That is the shape a scope amendment is supposed to take.

    Two judgement calls it made that I want visible rather than buried in a diff:

    1. Merged query, not client-side pruning. Both channels permitted the merged filter, so triage's sanctioned pruning fallback was never reached. And the merge is a conjunction through mergeFilterNodes, not a spread — a spread would let the $in overwrite a declared filter keyed on the same field, widening the accept set at the exact point this card exists to narrow it.
    2. The currently-selected value re-fetch stays unfiltered — deliberately, on both channels. I flagged this as an open question and asked for a decision plus a statement; it decided not to extend the narrowing, on the grounds that the value is already committed so hiding it prevents no bad pick and only swaps a readable label for a raw id — and that on PeoplePicker, dropping the value from the seed empties the selection tray, so a no-op Confirm would silently clear the field. That is a worse bug than the one being fixed. It is pinned as a test rather than asserted in prose.

    Full review follows once CI on PR #5219 converges.


    Generated by Claude Code

  8. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    ACCEPT — PM review of record (seat repo:objectui, session session_01RV6yuVCxymHYE16PL9vQkE). PR #5219.

    • PR shape — draft, base main, first line Fixes #5195.
    • Scope — path surface taken from git, not the report: 5 files, packages/fields only — the changeset, LookupField.tsx, PeoplePicker.tsx, the new pin suite, and one re-pointed fixture. RecordPickerDialog.tsx untouched (it was already correct), UserField.tsx untouched (it reaches the channel through PeoplePicker), app-shell untouched. Matches the amended surface exactly.
    • ACCEPT path fork — zero governed paths. Clean → queue.
    • CI — 21 check runs, all completed: 19 success, 2 skipped, zero failures.
    • Changeset — @object-ui/fields, patch. Bump-policy gate green.

    The scope amendment was right, and it was mine to get wrong

    Recorded in full above: my dispatch ruled PeoplePicker.tsx out of scope as "a different consumer". It is not — LookupField renders it for picker: 'search' fields and hands it both filters, so it is channel 2. Following my exclusion literally would have shipped a fix to a file with no defect and left the reported bug live. The dev verified against origin/main before editing, posted the amended surface to the issue before the first change, and bounded the edit to the single seed query.

    Spot-checks — the things a green run does not prove

    The reverse verification is the strongest part. With both source files reverted and the pins kept, exactly 4 of 6 fail, and the failure text shows the defect rather than a generic mismatch:

    AssertionError: expected 'Beta Widget|Retired Widget|Alpha Widg…' not to contain 'Retired Widget'
    AssertionError: expected 'Alpha Widget|Beta Widget|Retired Widg…' not to contain 'Alpha Widget'
    

    That is the excluded record visibly back on the rail, on both channels, for both filter kinds — the four pins the dispatch required, each individually red. The two guard pins stayed green pre-fix, which is their purpose: they pin what must not change.

    The fake backend evaluates rather than shape-matches. It runs both $filter forms (record and lowered AST) and throws on unsupported operators, so a filter that is syntactically present but semantically wrong cannot read as a pass. It also still implements the unfiltered findOne, so the pins cannot pass vacuously against pre-fix code. This is the difference between "a filter was passed" and "the filter excluded the record", and only the second is the claim.

    The merge is a conjunction, not a spread. Via mergeFilterNodes, the repo's single filter sink. A spread would let the $in overwrite a declared filter keyed on the same field — widening the accept set at the exact point this PR narrows it. Getting this backwards would have produced a green suite and a live bug.

    Three bypasses closed, not one. Beyond the unfiltered re-fetch: the per-id findOption cache could return a record resolved under the previous parent value, and a gated cascade (dependenciesMissing) disabled the main query while leaving the recents query running. Both are the same defect by another route; a fix that closed only the obvious one would have left the cascade case alive.

    The judgement call, and why I agree with it

    The currently-selected value re-fetch stays unfiltered on both channels. I flagged this as an open question and asked for a decision plus a statement. The reasoning holds: the value is already committed, so hiding it prevents no bad pick — it only replaces a readable label with a raw id, concealing the mismatch from the user best placed to notice it. And on PeoplePicker, dropping the value from the seed empties the selection tray, so a no-op Confirm would silently clear the field — strictly worse than the bug being fixed. Pinned as a test rather than asserted.

    One fixture triaged rather than swept

    complex-widgets.test.tsx's "shows a recently-used section on empty focus" stubbed findOne because that was the old channel. The behaviour it pins is unchanged and still correct, so it was re-pointed at the filtered channel rather than deleted or rewritten. The scan was done by the rule's consumer radius (recents-store read sites, which exist only in packages/fields) rather than by edited package — and the cross-package consumers that render these widgets were run too (6 files, 75 passed).

    Declared deviation: none material. Heavy runs were serialised under the shared verify lock throughout, and the full packages/fields suite still ran to completion (107 files, 1801 passed).

    @baozhoutao — this is the fix for the cross-project value you reported from your deployment, and your repro is now the regression test. Your application's beforeInsert/beforeUpdate hook is no longer the only thing standing between a user and a mismatched value.

    Flipping out of draft and enqueueing. I will confirm MERGED before this card leaves the in-flight set.


    Generated by Claude Code

  9. added a commit that references this issue on Aug 23, 2026
    6c68b13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions