Repository navigation
fix(app-shell): an inherited member is not a URL filter operator suffix - #9845
Conversation
`parseUrlFilterTriples` resolved a `filter[<field>][<op>]` suffix by indexing `URL_FILTER_OPS` — a plain object literal — and testing the result for truthiness. The suffix comes from the address bar, so `Object.prototype` answered that question too: `[constructor]`, `[toString]`, `[hasOwnProperty]` each emitted a triple whose OPERATOR WAS A FUNCTION, and `[__proto__]` one whose operator was `Object.prototype` itself. The function's own contract says an unknown suffix is ignored, never silently downgraded to equality; for these it was neither. The map now has no prototype, so an own entry is the only thing a lookup can find. No denylist: that is a spelling-level patch the next member of a prototype this module does not own walks straight past. The exported face is unchanged — same name, same `Record<string, string>`, same four entries, same behaviour under spread and `Object.entries`. The accompanying sweep enumerates `Object.prototype` at run time rather than listing today's members, and is paired with an assertion that the four declared operators still resolve so it cannot pass vacuously. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xm4WFhEe5mwcgyqHjxR2hn
…-filter-op-prototype-chain
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
|
Reviewed, green and ready at 2026-09-18T09:49Z — ACCEPT recorded on objectui#9507, comment ⛔ This seat cannot put it in the merge queue — same refusal, same cause, already recorded in full on PR objectui#9804: ⇒ A pointer, not a fourth report: the blocker, what this seat will not do about it, and the two ways to unblock it all live on objectui#9804. ⛔ No auto-merge attempt was made here; the refusal is predictable and retrying a denied action class unchanged is repetition, not a reading. Divergence, measured with Nothing else is outstanding on this PR. It stays watched until merged or closed. Posted by the Generated by Claude Code |
Fixes #9507
Clause-②: no
parseUrlFilterTriplesdecided "is this suffix an operator" by indexingURL_FILTER_OPS— a plain object literal — with a suffix that comes from the address bar, and testing the result for truthiness.Object.prototypeanswered that question too, sofilter[amount][constructor]=1emitted a filter condition whose operator was a JavaScript function. The function's own docblock says an unknown operator suffix is ignored, never silently downgraded to equality; for these suffixes it was neither.The repair: remove the construction, not the spellings
URL_FILTER_OPSis built with no prototype (Object.assign(Object.create(null), {…})), so an own entry is the only thing a lookup in it can find.A denylist of
constructor/toString/hasOwnPropertywas considered and refused: it is a spelling-level patch that the next member of a prototype this module does not own walks straight past. The red run below shows why that is not hypothetical — the sweep that enumeratesObject.prototypefailed naming twelve leaking members on this tree, not the three the card had measured, and__proto__among them is a different shape of the same defect (its inherited accessor yieldedObject.prototypeitself, so that suffix produced an operator that was an object, not a function). A three-name denylist would have closed a quarter of the population and read as a fix.Nothing on the exported face moves (this is what
Clause-②: norests on): same exported name, sameRecordof string to string type, same four entriesgteltegtlt, same behaviour under spread,Object.keysandObject.entries— which is howObjectDataPageinverts the map to bridge a triple's operator to the spec's alias spelling, and whatdrillEmptyBucketNavHost-9085.test.tsalready pins. No exported symbol is added or removed.⭐ What the consumers actually did — the half the card and triage both flagged as NOT measured
Driven on the pre-change parser through each consumer's own real code (not a mock), then re-driven after the repair. None of the three was a crash, and neither "会崩" nor "无害" was the answer:
groupFilterChips)= VALUE) and drew a confidentamount = 1chip — the "silently downgraded to equality" outcome the contract rules out, rendered as if the user had asked for itbuildSaveAsViewSpec)normalizeFilterOperatorunchanged andViewFilterRuleSchemarefuses it; the rule was dropped with oneconsole.warnand the saved ViewItem carried nofilterkey at all, passing the record gateschema.filter→toFilterNode)JSON.stringifyturned the function intonullon the wire:$filter: [["amount",null,"1"]]— a condition with no operator in ittoFilterNodefolds the empty list toundefined, so the$filterslot is skipped entirelyamount = 1while the saved view silently contained no such condition and the list query went out malformed. Three surfaces, three different wrong answers, no error anywhere.The consumer measurement is reported rather than pinned: after this repair that state is unreachable from a URL, so a permanent test at a consumer would assert about an input the parser can no longer produce. The pin belongs where the defect is.
Tests
packages/app-shell/src/views/drillUrlFilters.test.ts— the module's own suite, where its siblings already live.Object.getOwnPropertyNames(Object.prototype)at run time rather than listing today's members, so a member added to the language is covered without anyone remembering to (AGENTS.md 完善设计器的每一个细节 #9 — the population is re-derived, never written down). Its failure message names every leaking suffix;filter[amount][nope]=1→ nothing, which was correct before and after and proves the suite would notice a regression in the opposite direction.Direction, decided before running: RED before, GREEN after.
Gates
All at
21ef238f(post-merge oforigin/main), each exit code captured before any pipe.pnpm exec vitest run packages/app-shell/(whole affected package)0— 717 files, 7058 passed, 1 skippedpnpm exec tsc --noEmit && pnpm exec tsc -p tsconfig.test.json(inpackages/app-shell)0— the secondtscis why the test file is typechecked toopnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build0— dependency closure, required before typecheck resolved@object-ui/*pnpm exec eslint .(inpackages/app-shell)0— 0 errors; the 3037 warnings are the package's pre-existing population, 0 of them on either changed filenode scripts/check-changeset-presence.mjs0pnpm check:control-bytes0pnpm check:new-line-citations0— 0 new citationspnpm check:changeset-claims0pnpm check:pending-changeset-literals0pnpm check:test-path-roots0The lint reading is a declared narrowing, with its three pieces of evidence: the universe is eslint's own resolution of the package directory (
eslint ., not a file list of mine); the file count is read from--format json; and the invariance holds because this config enables no type-aware linting (itslanguageOptionsdeclares noparserOptions.project/projectService) and no rule undereslint-rules/reads another file — so this diff cannot move the verdict on a file it does not contain. Repo-widepnpm lintover the other packages is CI's run.Acceptance notes
Out-of-scope observations, recorded here rather than filed or fixed:
RANGE_OP_PARAMas the inverse ofURL_FILTER_OPS, so it was checked: it is only ever read throughObject.entries()— an own-enumerable iteration — and never indexed by an externally chosen key.collectFilterParamsdoes index a caller-supplied object (ops[NULL_FILTER.key],ops[op]), but only with the fixed keys$null/$gte/$lte/$gt/$lt, none of which is anObject.prototypemember. No repair is owed and none was made. Whether it belonged in this PR was a scope call; it turned out not to be a defect.@objectstack/spec'snormalizeFilterOperatorindexes its own alias table the same bare way (VIEW_FILTER_OPERATOR_ALIASES[op]), so a string operator naming an inherited member returns a function. Not filed: it is a different repository, it is unreachable from this path (this parser's operators come fromURL_FILTER_OPS' values), and the outcome does not diverge — both the inherited member and the verbatim string land outsideVIEW_FILTER_OPERATORSand are refused by the enum. Named here because it is the same construction and a future reader deserves to know it was looked at rather than missed. Carrier if anyone picks it up: whoever next touches that alias table.Deviations
packages/app-shell/src/views/__tests__/. On this tree they do not —drillUrlFilters.test.tsand every sibling that exercises this module sit directly inpackages/app-shell/src/views/. The existing suite was extended in place rather than a new file opened in a directory this module has never used. The substantive fence (one source file, its tests, one changeset) is unbreached; the directory clause was falsified against the tree.🤖 Generated with Claude Code
https://claude.ai/code/session_01Xm4WFhEe5mwcgyqHjxR2hn
Generated by Claude Code