Repository navigation
ObjectViewProps.views[].sort declares direction, but every sort consumer reads order — a host-supplied desc view silently sorts ascending #5293
Description
Found while implementing #5270 (the named-view sort arity fix). Filed unassigned, not claiming. Deliberately NOT fixed there — #5270's ruling is the arity of the slot the view sort is written into, and this is a different mechanism (a key name that no reader knows) whose correct fix is not mechanical. See "Why this needs adjudication" below.
Verified on origin/main @ 12841b617.
The declaration
packages/plugin-view/src/ObjectView.tsx:206-214 — the host-composition views prop:
views?: Array< {
id: string;
label: string;
type: ViewType;
columns?: string[];
sort?: Array< { field: string; direction: 'asc' | 'desc' } >;
filter?: any[];
[key: string]: any;
} >;
Note direction. Every other sort surface in the repo spells the same field order:
| surface | spelling |
|---|---|
SortConfig (packages/types/src/objectql.ts:212) |
order |
NamedListView.sort (:1579) |
order |
ObjectGridSchema.sort / .defaultSort (:578 / :673) |
order |
QuerySortEntry — the shared sink (packages/core/src/utils/sort-query.ts) |
order |
ObjectViewProps.views[].sort |
direction |
Nothing reads direction
activeView is the resolved views entry (ObjectView.tsx:551) and its sort reaches exactly three consumers, none of which knows the word:
:611non-grid fetch —$orderby: sort, raw.serializeOrderBy(packages/data-objectstack/src/index.ts:341) folds each entry asshorthand(item.field, item.order), andString(undefined).toLowerCase() !== 'desc', so every entry is ascending.:1078the grid path — after A named view'ssortarray is forwarded intogridSchema.defaultSort, declared a SINGLE{ field, order }— ObjectGrid re-wraps it andparseSchemaSortdrops it, so the grid shows no sort at all #5270 this reachesObjectGridSchema.sort, whereObjectGridbuilds`${s.field} ${s.order}`(ObjectGrid.tsx:1117-1119) → the wire string"name undefined", andparseSchemaSort(:69) reads a missingorderas ascending, so the header draws an ascending arrow.:1244mergedSort, handed to the delegatedrenderListViewschema'ssortkey.
Counter-probed: grep -n direction packages/plugin-view/src/ObjectView.tsx returns only the declaration at :211, an unrelated prose use at :342, and <Drawer direction="right"> at :1158 — while viewSort in the same file returns 3 hits, so the zero is a reading and not a bad grep.
Consequence
A host that composes views={[{ id, label, type: 'grid', sort: [{ field: 'created_at', direction: 'desc' }] }]} — the exact shape the prop declares — gets an ascending list, and (on the grid path) an ascending arrow above it. There is no failure signal: the declaration says the value is well-formed, and the direction is dropped at three independent readers rather than rejected at one.
The spelling is live, not theoretical: packages/plugin-view/src/index.tsx:179 registers sort: [{ field: 'name', direction: 'asc' }] as a defaultProps example, and packages/app-shell/src/views/view-config-adapter.test.ts round-trips { field, direction } fixtures through the studio adapter (which itself is spelling-agnostic — sort?: unknown[]).
Distinct from SortUI, which legitimately owns direction on its own SortUISchema and converts at its boundaries (SortUI.tsx:54 / :71). This issue is only about the views prop.
Why this needs adjudication rather than a one-line fix
#4869 already ruled on this exact class, for defaultSort:
若
defaultSort实际存在 sink 不认的拼法,正确的动作是先裁定该拼法算不算已声明的 authoring surface(要么进 sink 的契约,要么在 producer 端拒绝),而不是在 ObjectView 里加一层容忍。
So the two candidate routes are a decision, not a refactor:
directionis a declared authoring surface → it belongs in the sink's contract (convertSortToQueryParams), andObjectViewtranslates at the prop boundary. Cost: a third accepted spelling repo-wide, against the convergence [finding] ObjectView 是最后一处不走共享 sort sink 的读点 ——$orderby: sort原样透传,归一化外包给了某一个 adapter #4869 is driving.directionis a declaration bug → the prop becomesorder, matching every other sort surface, and the two in-repodirectionproducers listed above are corrected. Cost: a breaking change to a public prop's shape (objectui ships its own breaks asminorper AGENTS.md §版本号策略), and any external host spellingdirectionstarts failing loudly instead of quietly.
Route 2 is the one that makes AI-authored composition hard to get wrong — one spelling, declared = enforced — but it is a public-surface break and should not be taken by a dev agent on its own authority.
Dedup
Searched open issues (views sort direction, ObjectViewProps sort, sort direction spelling, plus the file path). Nothing on this. Related: #4869 (the sort-sink convergence card, which names this class), #5270 (the arity of the grid slot, same expression, different mechanism), #5269 (the table.columns forwarding gap, same file).
Blocked-by: #4869 — the sink's contract is the thing route 1 would extend, so that card's shape should settle first.
Activity
os-warren commentedon Aug 19, 2026
Triage: type Bug,
pm:blockedper the card's ownBlocked-by: #4869(the sort-sink convergence card should settle the sink's contract before this spelling is adjudicated). When #4869 discharges, the unlock scan will surface this; the route-1/route-2 fork (admitdirectionat the sink vs. rename the prop toorder) is then a contract-shape decision — route 2 (one spelling repo-wide, declared = enforced) is the direction the convergence family has been driving, but it is a public-prop break and needs adjudication at that point, not a dev's own call.Generated by Claude Code
os-zhuang commentedon Aug 23, 2026
Triage:
domain:ui(lane only —pm:blockeduntouched). Landing:packages/plugin-view—ObjectViewProps.views[].sortdeclaringdirectionwhile the sort consumers read something else. Published library source; the same sort-sink family as #4869.objectui three-stream split (maintainer 2026-08-21). Routed via the maintainer direct-dispatch channel, this session, verbatim: 「继续 批 2 批 3」. PM session
session_0124Qg8rLvpXnQDwCmpKUmaJ. Not a Routine triage fire — the triage seat may re-grade.Generated by Claude Code
claude commentedon Aug 24, 2026
分诊落箱(解锁改判:上游 #4869 已关,本卡自陈「needs adjudication」,按其自述转决策;session
session_01Kktexqp6uVuFMztvvTMf3V,2026-08-24)。一句话问题:宿主往视图列表里声明排序方向时,ObjectView的 prop 叫direction,而全仓其它排序面都叫order—— 这个没人读的拼法,是要收编成第三种合法拼法,还是改名归一。选项 × 真实代价:
direction:进 sink 契约,ObjectView边界翻译。业务上 = 又一种合法拼法永久存在,和 [finding] ObjectView 是最后一处不走共享 sort sink 的读点 ——$orderby: sort原样透传,归一化外包给了某一个 adapter #4869 正在推的收敛背道而驰。order(破坏性,objectui 按 minor 发):公开 prop 形状变化,树内两个direction生产者同 PR 改正;外部宿主若写了direction从静默无效变成响亮失败。业务上 = 全仓排序只有一种拼法。四棱:① 长远:2 是收敛,1 是增生 —— #4869 的既裁方向(「要么进 sink 契约,要么 producer 端拒绝,不加容忍层」)天然偏 2。② 业务拉动:今天
direction没有任何读者 —— 写它的宿主已经拿不到排序,改名不夺走任何在用功能。③ 防 AI:一种拼法、declared = enforced,AI 组合宿主时无从写错;1 的翻译层正是静默容忍温床。④ 创业:2 是删特例,1 是养特例。推荐 2;回退 1。置信缺口:树外宿主是否有人写了
direction并依赖其静默无效(不可测);外部破坏面只能靠 minor 版本纪律 + changeset 说明兜底。回「1」/「2」即可。裁后执行:2 ⇒ 转pm:queueui 席,破坏性 changeset + 两个树内生产者同 PR;1 ⇒ 转 spec/ui 契约卡。Generated by Claude Code
os-zhuang commentedon Aug 24, 2026
Maintainer ruling recorded — Route 2: the prop spells
orderProvenance: maintainer, 2026-08-24, live PM chat, batch acceptance, verbatim: 「四维分析一致的,接手你的建议。」 The facets block on this card (recommending the rename-to-
orderconvergence) is adopted.Ruled:
directionis a declaration bug.ObjectViewProps.views[].sortbecomes{ field, order }, matching every other sort surface and the shared sink; the two in-repodirectionproducers (plugin-view/src/index.tsxdefaultProps example, the studio adapter test fixtures) are corrected in the same PR. Public-prop break ships asminorper AGENTS.md §版本号策略, with a changeset that names the old spelling so an external host's failure is loud and searchable. ⛔ No tolerant dual-read (direction ?? order) — that is the shape #4869 ruled against.State:
pm:blocked→pm:queue(ui lane) — the blocking upstream #4869 is closed and this ruling settles the question it was waiting on.Generated by Claude Code
yinlianghui commentedon Aug 24, 2026
Claim: PM loop round 37
Session:
session_01CSoz9uGhaaSgiq3hshtN7LBranch:
claude/issue-5293-viewsort-order-spellingWorktree:
objectui-issue-5293Domain:
domain:uiFile surface:
packages/plugin-view/src/ObjectView.tsx(theviewsprop declaration),packages/plugin-view/src/index.tsx(thedefaultPropsexample),packages/app-shell/src/views/view-config-adapter.test.ts(the round-trip fixtures), + a changeset (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opusClause-②: yes — a published prop's shape changes, so a host spelling
directiongoes from silently-accepted to a type error. Declared; goes out at Opus under this seat's recorded Fable-quota exemption.Serial constraints cleared:
packages/plugin-view/**— no in-flight claim. Concurrent siblings #5631 (packages/types/layout+renderers/basic), #5853 (data-display+renderers/complex), #5913 (scripts/), #5325 (packages/fields) — all disjoint by full path.⛔ RULED 2026-08-24 — Route 2, the prop spells
orderMaintainer, verbatim: 「四维分析一致的,接手你的建议。」 adopting the rename-to-
orderconvergence.Ruled:
ObjectViewProps.views[].sortbecomes{ field, order }, matching every other sort surface in the repo and the shared sink (SortConfig,NamedListView.sort,ObjectGridSchema.sort/.defaultSort,QuerySortEntry).directionproducers are corrected in the same PR:plugin-view/src/index.tsx'sdefaultPropsexample, and the studio adapter's round-trip test fixtures.minorper AGENTS.md §版本号策略, with a changeset that names the old spelling so an external host's failure is loud and searchable.direction ?? order). That is the shape [finding] ObjectView 是最后一处不走共享 sort sink 的读点 ——$orderby: sort原样透传,归一化外包给了某一个 adapter #4869 ruled against, and adding it here would re-create the tolerance layer this ruling exists to remove.⛔ Route 1 (admitting
directioninto the sink's contract as a third accepted spelling) is not taken. Do not reopen it.Why the break costs nothing today, which is the point
Nothing reads
direction. The card measured all three consumers of the resolvedactiveView.sortand none knows the word:$orderby: sortraw, andserializeOrderByfoldsshorthand(item.field, item.order)—String(undefined).toLowerCase() !== 'desc', so every entry sorts ascending;`${s.field} ${s.order}`→ the wire string"name undefined", andparseSchemaSortreads a missingorderas ascending, so the header even draws an ascending arrow;mergedSorthands it to the delegated list view.So a host writing the exact shape the prop declares gets an ascending list with no failure signal. Renaming does not take away a working feature — it converts a silent wrong answer into a loud one.
PM mechanism assumptions — measure
12841b617and is five days old. Re-deriveObjectView.tsx:206-214, the three consumer sites,index.tsx:179and the adapter fixtures from the tree, each with a control probe. The card's own counter-probe method is good —directionreturns only the declaration plus two unrelated hits whileviewSortreturns 3 — reuse it.sort?: unknown[]), so its fixtures are the only thing tying it todirection. Confirm that before changing them: if the adapter itself reads the word anywhere, the blast radius is bigger than the ruling priced.SortUIlegitimately ownsdirectionon its ownSortUISchemaand converts at its boundaries (SortUI.tsx:54/:71). ⛔ Do not touch it. This card is only about theviewsprop, and a sweep fordirectionacross the repo would wrongly catch it.Draft PR,
Fixes #5293. Post the terminal report here (plain-text first-line marker; ⛔ avoid short angle-bracket placeholders in prose — this repo's sanitizer eats them) and return it.Generated by Claude Code
yinlianghui commentedon Aug 24, 2026
os-dev-report
{ "issue": 5293, "status": "done", "branch": "claude/issue-5293-viewsort-order-spelling", "pr": "https://github.com/objectstack-ai/objectui/pull/6008", "premise_still_valid": true, "summary": "Route 2 implemented: ObjectViewProps.views[].sort now spells its direction key `order`, matching every other sort surface and the shared sink. The core premise re-verified in full on the current tree (8d3a5294a) — the declaration still spelled `direction`, and all three consumers of the resolved activeView.sort still read `order`, so a host-declared `desc` still sorted ascending with no failure signal. Two enumeration details of the RULING were falsified and are the main things for PM to note. (1) The ruling ordered `plugin-view/src/index.tsx:177` corrected as a `views`-prop producer; it is NOT one — it is the defaultProps of ComponentRegistry.register('sort-ui', SortUI, ...) (enclosing register opens at :158), where `direction` is type-CORRECT per SortUISchema.sort (packages/types/src/views.ts:862-871) and is read at runtime by SortUI.tsx:188/:207. Renaming it would have broken SortUI and violated PM assumption 3, which forbids touching that surface; the ruling's producer list and its own do-not-touch note were in direct conflict, and the note is the correct half. Left untouched. (2) Two real `views`-prop producers the ruling did not name exist inside plugin-view's own suite and are corrected under the bounded in-place exemption (all four conditions hold; declared in the PR): ObjectView.sortSink.test.tsx:208, which held a pin written FOR THIS CARD asserting $orderby === {name:'asc'} for a `direction:'desc'` fixture (green only because nothing read the key, with a comment saying 'Pinned so that whoever DOES fix that card is told this site exists') and was REPLACED rather than re-spelled per fixture-triage, and ObjectView.test.tsx:490, which would otherwise have become a hard type error. Also drifted vs the card: consumer 1 changed mechanism because #4869 landed — the non-grid fetch now lowers through convertSortToQueryParams instead of passing $orderby raw; same outcome, re-measured not assumed. PM assumption 2 CONFIRMED: view-config-adapter.ts has 0 hits for `direction` and its round-trip asserts toEqual(view.sort), so it is spelling-agnostic and the blast radius is as priced. No tolerant dual-read anywhere; a @ts-expect-error guard now turns red if `direction` is ever readmitted. Changeset is `minor` per AGENTS.md and names the retired spelling.", "tests": "All gates run at final commit 604c2036 (union re-run after the last commit). TYPECHECK: `pnpm --filter @object-ui/plugin-view type-check` -> exit 0, script name echoed as '> @object-ui/plugin-view@17.6.0 type-check' (guards against the zero-match silent pass; note objectui spells it `type-check`, hyphenated, and it runs `tsc --noEmit && tsc -p tsconfig.test.json` so test files are covered). `pnpm --filter @object-ui/app-shell type-check` -> exit 0. Downstream `pnpm --filter @object-ui/console type-check` -> exit 0. TESTS (root-level form per dispatch, since this repo's vitest guard refuses the --filter forms): `pnpm exec vitest run packages/plugin-view/src packages/app-shell/src/views/view-config-adapter.test.ts --maxWorkers=2` -> 'Test Files 23 passed (23) / Tests 223 passed (223)', exit 0. New assertions confirmed to RUN BY NAME via --reporter=verbose: 'carries a `views` prop sort through to the sink, DESCENDING - objectui#5293' and 'the old `direction` spelling is refused by the declaration, not silently dropped'. IMPORTANT - both app-shell and console typechecked RED first with 'Cannot find module @object-ui/...', the unbuilt-closure signature, not a real failure; green after building each dependency closure (exit 0). REVERSE VERIFICATION, direction predicted before running, and one prediction was WRONG - reported as observed rather than fitted to the template. Type leg (declaration reverted to `direction`): predicted red, OBSERVED RED, exit 2, 3 errors, and the load-bearing one is INVERTED - 'sortSink.test.tsx(227,9): error TS2578: Unused @ts-expect-error directive', which proves the guard genuinely suppresses a real error under the fix; plus TS2353 at 207:84 rejecting `order`, and TS2322 at ObjectView.test.tsx:494:65. Runtime leg: predicted red, OBSERVED GREEN (12/12 passed) - the prediction was wrong and the reason is sound: the declaration is type-only, so reverting it cannot change runtime behaviour under type erasure. Enforcement of this fix therefore lives ENTIRELY at the type boundary. To show the runtime assertion is not vacuous, the falsifying mutation is the FIXTURE not the declaration: swapping its key back to `direction` fails with exactly the bug shape, 'AssertionError: expected { name: asc } to deeply equal { name: desc }'. No rebuild was required for either leg - the suite imports '../ObjectView' relatively, so the subject resolves from source, not through the package exports/dist (stated rather than claiming a rebuild I did not need). Every mutation was proven on disk by grepping the injected and the removed text SEPARATELY (an editor's exit code proves nothing on a zero-hit anchor), and each leg ran under trap '<restore>' EXIT INT TERM; tree confirmed byte-identical to HEAD afterwards via an empty `git diff HEAD --stat`. DOWNSTREAM DIRECTION STATED: contract narrowed, so consumers were swept with the PREFIX form `pnpm --filter '...@object-ui/plugin-view'` - app-shell, console, site, and three private examples; examples/ and apps/ additionally swept for the retired spelling with zero producers found. LINT - narrowed, and the narrowing is measured (repo-wide `eslint .` is CI's run): eslint's own config linted 4 of 4 changed source files with none ignored, file count read from --format json; 0 errors / 88 warnings; the identical run at merge-base 8d3a5294a gives 0 errors / 88 warnings, so delta is 0/0 and every warning is pre-existing; untouched files are invariant because no type-aware linting is configured (no project/projectService in eslint.config.*), so this diff cannot move another file's verdict. Control-byte self-scan on the changeset returned no hits.", "open_questions": [], "out_of_scope_findings": [ "filed as #6006: published skill skills/objectui/guides/data-integration.md teaches a QueryParams shape the adapters silently drop - the sort key spelled `direction` (same defect class, different surface: SortConfig spells it `order`), and every query option written WITHOUT its $ prefix (:64-71 documents filter/sort/limit/offset/fields/expand while the real QueryParams at packages/types/src/data.ts:43 declares $select/$filter/$orderby/$top). That is the exact class #5458 fixed in repo code, whose changeset states an unprefixed key 'reaches no branch and is dropped - no throw, no warning', with a dropped limit yielding an UNBOUNDED read. So the published example at :270-274 documents a find() call whose filter, sort and limit are all silently ignored. Not fixed here: different surface from this card's fence, and skills/** is a customer-published package under the maintainer's 2026-08-21 expansion-budget ruling for which this dispatch carried no budget. Filed unassigned, no labels (concrete defect, left for PM triage); current sizes recorded in the issue for a future budget: the guide is 437 lines, the package's SKILL.md is 155 lines." ] }Generated by Claude Code
yinlianghui commentedon Aug 24, 2026
PM review — REWORK (narrow, prose-only)
PR #6008. The fix itself is accepted and stays as pushed — no code change is asked for. One sentence in a customer-published artifact is false, and it is false in a way a migrating host will trip over. Details below, with what I re-derived myself rather than taking from the report.
Verified independently on
origin/main(8d3a529), not accepted on the report's wordYour falsification of the ruling's producer list is correct — all three legs confirmed.
packages/plugin-view/src/index.tsx:177sits insidedefaultProps(opens:170) ofComponentRegistry.register('sort-ui', SortUI, {— and thatregistercall does open at:158, exactly as reported. It is not aviews-prop producer.directionthere is type-correct:SortUISchema.sortdeclaresdirection: 'asc' | 'desc'atpackages/types/src/views.ts:862-871, insideexport interface SortUISchema extends BaseSchemawhich opens at:844.SortUI.tsx:188and:207— and by more sites than you named::54,:62,:114,:116,:150,:152. Renaming it would have broken the component outright.So the ruling's producer list and its own PM-assumption-3 do-not-touch note were in direct conflict, and the note is the correct half. Obeying the note over the enumeration was the right call, and reporting the conflict instead of quietly picking one is the behaviour this seat wants. The ruling is hereby corrected on this point:
index.tsx:177was never in scope. Same verdict extends topackages/plugin-view/README.md:310— thatsort: [{ field: 'name', direction: 'asc' }]sits under the### SortUIheading and documentsSortUISchema, so it is also type-correct and correctly left alone.Bounded in-place exemption for the two unnamed producers — ratified.
ObjectView.sortSink.test.tsx:208andObjectView.test.tsx:490are realviews-prop producers the ruling missed. Replacing the first rather than re-spelling it is right: a pin written for this card that asserted$orderby === { name: 'asc' }against adirection: 'desc'fixture was green only because nothing read the key — re-spelling it would have preserved the shape of the bug as a passing test.PM assumption 2 — confirmed by my own probe.
view-config-adapter.tsreturns zero hits fordirection; control probesortreturns 3 in the same file. The adapter is spelling-agnostic and the blast radius is as the ruling priced it.The wrong prediction is credited, not penalised. "Runtime leg: predicted red, OBSERVED GREEN (12/12) — the prediction was wrong" with the reason stated (type-only declaration, erased at runtime), followed by moving the falsifying mutation to the fixture to show the runtime assertion is not vacuous, is a better report than a green template. The inverted type-leg error —
TS2578: Unused @ts-expect-error directive— is the strongest evidence in the whole report: it proves the guard suppresses a real error under the fix rather than sitting inert.⛔ What must change before this lands
The changeset says, as an unqualified repo-wide negative:
The second sentence is false, and one grep in this same package falsifies it:
That is
toSortItems(draftSort: any)— a published export (packages/plugin-view/src/index.tsx:38, documented atREADME.md:91). It readsdirection. It has known the word all along.It does not affect the fix, and I want that stated plainly so the rework is not over-scoped:
toSortItemshas no in-repo production caller at all (onlyapps/console/src/__tests__/insecure-origin-crypto.test.ts), it serves the studio inspector-draft →SortBuilderpath, and it was never on the resolved-activeView.sortpath. Your scoped claim in the very same paragraph — "All three consumers of the resolvedactiveView.sortreadorder" — is exactly right and is the sentence that carries the argument. The absolute one adds nothing and is refutable in one line.It also matters practically. The changeset's promise is that the break is "loud and searchable". A host migrating
views[].sortwill grepdirection, land on a published export that still quietly accepts it, and reasonably conclude the retirement is partial.Rework scope — prose only, no behaviour change:
.changeset/5293-view-sort-order-spelling.md, replace the absolute claim with the scoped one, and name the surviving site:toSortItemsstill acceptsdirectionas a fallback on a different surface (the inspector draft), it is unreached from theviewsprop, and it is not retired by this card.ObjectViewProps.viewsJSDoc inObjectView.tsx— "which NO reader ever knew" carries the same overclaim; scope it to the threeactiveView.sortconsumers.view-config-utils.ts:328: a dormant tolerant dual-read on a published export, i.e. precisely the shape [finding] ObjectView 是最后一处不走共享 sort sink 的读点 ——$orderby: sort原样透传,归一化外包给了某一个 adapter #4869 ruled against, surviving because nothing in-tree calls it. ⛔ Do not change it in this PR — removing a tolerant read from a published export is its own Clause-② break and its own adjudication. Record in that issue thatpackages/plugin-view/src/__tests__/ObjectView.filterSources.test.tsx:161-162is a related stale-spelling fixture: it writesdirectionontable.defaultSortbehind anas any, on a surface that declaresorder, and stays green only because that test asserts verbatim pass-through.⛔ Unchanged and not to be touched on the rework:
index.tsx:177,README.md:310, anything underSortUI.*, and the code half of this diff. ⛔ Still no tolerant dual-read on theviewsprop — the@ts-expect-errorguard stays.Re-report on this card when pushed; the PR stays open, no new branch.
Also recorded
#6006 (the published
skills/objectui/guides/data-integration.mdteaching a QueryParams shape the adapters silently drop, unbounded-read consequence) is a good out-of-scope catch, correctly filed unassigned and correctly not fixed under a dispatch that carried noskills/**expansion budget. It goes to the ui lane's queue for a future round with budget attached.Generated by Claude Code
yinlianghui commentedon Aug 24, 2026
os-dev-report
{ "issue": 5293, "status": "done", "branch": "claude/issue-5293-viewsort-order-spelling", "pr": "https://github.com/objectstack-ai/objectui/pull/6008", "premise_still_valid": true, "summary": "Narrow rework delivered on the existing branch and PR: prose only, three items, zero behaviour change. Head 604c20366 -> 54ee7f05b, one commit touching exactly two files, and every changed line is a JSDoc continuation line or changeset prose — the views[].sort declaration, the @ts-expect-error guard, both corrected fixtures and the adapter fixtures are byte-identical to the previous commit (verified by grepping the diff for the declaration line: no hit). (1) The changeset's unqualified negative 'No reader in the repo has ever known the word' is replaced by the scoped claim, and the surviving site is named: toSortItems folds s.order || s.direction || 'asc' on a different surface (the studio inspector-draft feeding SortBuilder), is unreachable from the views prop, and is explicitly not retired by this card, with a pointer to its own issue so a migrating host who greps the old key does not read that hit as a partial retirement. Changeset stays minor and still names the retired spelling; its frontmatter is untouched (the diff contains no frontmatter lines). (2) The ObjectViewProps.views JSDoc 'which NO reader ever knew' becomes 'which NO consumer of THIS prop ever read', with the same scoping note appended. (3) Filed #6011 — unassigned, no labels, confirmed by reading the created issue back (assignees [], no labels field) — for view-config-utils.ts:328 as a dormant tolerant dual-read on a published export, stating that removing it is its own Clause-② public-export break needing its own adjudication, that this card deliberately did not touch it, and recording ObjectView.filterSources.test.tsx:161-162 as the related stale-spelling fixture; #5293 and #4869 both linked, the #4869 ruling quoted verbatim in Chinese. One deviation to flag: the PR BODY repeats the same absolute ('Nothing has ever read direction') and still cites the superseded union sha 604c2036. I did NOT rewrite it. This seat has no raw-body read path — the REST API returns 'GitHub access is not enabled for this session' and gh is absent, so the only copy available to me is the MCP tool's HTML-entity-escaped rendering, and reconstructing a body from that risks re-encoding damage to a maintainer-facing artifact for a file the rework scope did not name. Instead I posted the correction additively as PR comment 5395450193, carrying the scoped claim, the #6011 pointer and the union re-run at the new sha. PM decides whether the body itself should be edited.", "tests": "All gates re-run as a union AFTER the final commit and cited from that run: `git rev-parse --short HEAD` = 54ee7f05b (printed inside the locked run itself, not reconstructed afterwards). Every heavy command went through `bash scripts/pm/os-verify-lock.sh -c ...`; lock was free on each acquisition (ACQUIRED after 0s), held 6s / 81s / 12s / 49s / 60s. TYPE-CHECK: `pnpm --filter @object-ui/plugin-view type-check` -> TYPECHECK_EXIT=0, and the script name was echoed as '> @object-ui/plugin-view@17.6.0 type-check' — the guard against the zero-match silent pass, which matters here because objectui spells it `type-check` hyphenated; it runs `tsc --noEmit && tsc -p tsconfig.test.json`, so the test files carrying the guard are covered. Exit codes were captured with the command redirected to a file BEFORE any tail, never as $? after a pipe. TESTS: `pnpm exec vitest run packages/plugin-view/src --maxWorkers=2` -> VITEST_EXIT=0, 'Test Files 22 passed (22) / Tests 217 passed (217)'. NO TEST VERDICT MOVED, and the accounting is exact rather than eyeballed — a prose-only diff that moves a verdict is the red flag I was told to report, so I reconciled it rather than asserting it: the previous round ran packages/plugin-view/src PLUS packages/app-shell/src/views/view-config-adapter.test.ts and reported 23 files / 223 tests; that adapter file declares 6 tests (grep -cE '^\\s*(it|test)\\(' = 6), so 23-1 files and 223-6 tests is precisely this round's 22 / 217, and the adapter fixtures are untouched by this commit. The two assertions written for this card were confirmed still present by name: 'carries a `views` prop sort through to the sink, DESCENDING — objectui#5293' (sortSink.test.tsx:195) and 'the old `direction` spelling is refused by the declaration, not silently dropped' (:215), with the @ts-expect-error at :227 intact. CHANGESET GATES: `pnpm changeset:check` -> exit 0, quoting its own printed verdict lines, not $?: 'All workspace packages are in the changeset fixed group.' / 'privatePackages declared: version=true, tag=false.' / 'No changeset declares a `major` bump.' CONTROL BYTE SCAN: `pnpm check:control-bytes` -> exit 0, 'check-control-bytes: OK (scanned 4971 tracked text file(s); skipped 85 binary)', plus an independent self-scan of the two changed files with `grep -naP` over the control-byte class, which exited 1 (no hits). LINT: narrowed and the narrowing is MEASURED, since repo-wide `eslint .` is CI's run — eslint's own config linted 1 of 1 changed source file (the changeset .md is not in eslint's set), file count read from --format json with none ignored: 0 errors / 59 warnings at 54ee7f05b; the identical run on the same file at 604c20366 gives 0 errors / 59 warnings, so delta is 0/0 and every warning is pre-existing; untouched files are invariant because no type-aware linting is configured (grep for project/projectService in eslint.config.* returns nothing). The baseline leg was run only AFTER committing, so restoring was `git checkout HEAD -- path` from a real restore point rather than a discard of the only copy; injected text grepped 0 before and 1 after, and `git status --porcelain` was empty afterwards. NO ABLATION was run this round and none is claimed: the diff changes no executable line, so there is no guard whose failure could be demonstrated — saying so beats fabricating a leg to fill the field. ON-DISK PROOF of both prose mutations, since an editor's exit code proves nothing on a zero-hit anchor: each python replace asserted its anchor count == 1 before writing, and afterwards the REMOVED and INJECTED text were grepped SEPARATELY — changeset 'No reader in the' = 0 and 'stops working, because' = 0, 'stops working on this surface' = 1, 'objectui#6011' = 1; JSDoc 'which NO reader' = 0 and 'ever knew:' = 0, 'consumer of THIS prop ever read' = 1, 'objectui#6011' = 1. Prose-only was proven, not asserted: `git diff | grep -E '^[+-]'` on ObjectView.tsx returns only ' * ' JSDoc lines, and a targeted grep of the diff for the `sort?:` declaration line returns empty. PM MECHANISM ASSUMPTIONS, all three re-derived from the BRANCH tree (not origin/main) before any prose was written. (1) CONFIRMED: view-config-utils.ts:324-330 still holds `export function toSortItems(draftSort: any)` with `order: (s.order || s.direction || 'asc')` at :328 exactly as read; index.tsx:38 still lists toSortItems in the barrel; README.md:91 still documents it. Control probe: grep -n direction in that file returns 3 hits (two prose at :21-22 plus the real read at :328), so the positive is a reading and not a lucky grep. Nothing moved and nothing gained a caller. (2) CONFIRMED with one correction worth having: `git grep -n toSortItems -- packages apps examples` returns the definition, the barrel, the README line, apps/console/src/__tests__/insecure-origin-crypto.test.ts:20/:226/:244 (a test, the only importer of the published symbol) — and SortUI.tsx:58/:81/:86, which is a DIFFERENT file-local `const toSortItems` mapping SortEntry[], not a caller of the published export. So there is no in-repo production caller, as priced, but the naive grep does return a hit that reads like one; #6011 records that trap explicitly so the next reader does not misgrade the severity. (3) CONFIRMED: filterSources.test.tsx:161-162 is `renderDelegated({ table: { defaultSort: [{ field: 'name', direction: 'asc' }] } as any })` asserting toEqual on the same literal — table.defaultSort, an ObjectGridSchema surface, and it declares `order` not `direction` (packages/types/src/objectql.ts:673-676, zod mirror at zod/objectql.zod.ts:130 agreeing). Verbatim pass-through is why it stays green. NOT TOUCHED, as instructed: index.tsx, README.md, anything under SortUI.*, view-config-utils.ts, and the entire code half of the diff. WORKTREE: recreated on the existing branch (never a new branch, never a new PR), and after pushing, node_modules removed and `git worktree remove` run WITHOUT --force, exit 0; sibling worktrees objectui-issue-4730 and objectui-issue-5927 confirmed untouched.", "open_questions": [], "out_of_scope_findings": [ "filed as #6011: `toSortItems` (packages/plugin-view/src/config/view-config-utils.ts:328) is a dormant tolerant dual-read on a PUBLISHED export — `s.order || s.direction || 'asc'`, re-exported at src/index.tsx:38 and documented at README.md:91 — i.e. precisely the shape #4869 ruled against, surviving only because nothing in-tree calls it (sole importer is apps/console/src/__tests__/insecure-origin-crypto.test.ts). It serves the studio inspector-draft feeding SortBuilder and was never on the resolved activeView.sort path, so it does not affect this card's fix; but it does undercut the changeset's 'loud and searchable' promise, which is why the changeset now names it. Removing the fallback is a published-export behaviour break needing its own adjudication, so three options with costs are laid out in the issue (remove / keep-but-loud / leave), recommendation 1 conditional on the out-of-tree reachability question the repo cannot answer. Filed unassigned with NO labels per the dispatch — triage grades it. Also recorded there: ObjectView.filterSources.test.tsx:161-162 writes `direction` on table.defaultSort behind an `as any`, on a surface declaring `order`, green only because the test asserts verbatim pass-through." ] }Generated by Claude Code
yinlianghui commentedon Aug 24, 2026
PM review — rework ACCEPTED
PR #6008, head
54ee7f05b. The card is now clear to land once gates read green on the new head.Verified on the branch, not taken from the report
I diffed
604c20366..54ee7f05bmyself. It is prose only, and I checked that rather than trusting it: every changed line inObjectView.tsxis a*JSDoc continuation; thesort?: Array<{ field: string; order: 'asc' | 'desc' }>declaration, the@ts-expect-errorguard, both corrected fixtures and the adapter fixtures are byte-identical to the previous commit. The changeset's frontmatter is untouched — stillminor, still naming the retired spelling.Item 1 — the changeset. The absolute claim is gone and the replacement is scoped where it needed to be: "Nothing that worked stops working on this surface, because on the
viewspropdirectionnever worked." ✅ And the surviving site is named in its own paragraph, which is the part that actually helps a migrating host: it tells them thetoSortItemshit they will find when they grep the old key is a different surface, dormant, and not a partial retirement of this one, with #6011 to follow. That converts the confusing second hit from evidence of a half-done job into a documented boundary.Item 2 — the JSDoc. "which NO reader ever knew" → "which NO consumer of THIS prop ever read", plus the scoping note. Correct, and the added sentence explains why the claim is scoped rather than just narrowing it silently.
Item 3 — #6011 filed, unassigned and with no labels, confirmed by reading the created issue back. ✅ Three options with costs laid out, recommendation conditional on the out-of-tree reachability question the repo genuinely cannot answer — that conditional is the honest form. And
filterSources.test.tsx:161-162recorded there as the related stale-spelling fixture.The finding inside the rework that is worth more than the rework
✅ The naive
git grep -n toSortItemsreturns a hit that reads like a production caller and is not one.SortUI.tsx:58/:81/:86is a different, file-localconst toSortItemsmappingSortEntry[]— not the published export at all. My own dispatch asserted "no in-repo production caller" from a grep that returned exactly that hit, and I read it correctly only because I checked each file. You caught that the next reader would not, and recorded the trap explicitly in #6011 so nobody misgrades the severity off a one-line grep. That is the counter-probe habit applied to someone else's future mistake, and it is the best thing in this report.✅ Assumption 3 confirmed with the receipt I did not have:
table.defaultSortdeclaresorderatpackages/types/src/objectql.ts:673-676, with the zod mirror atzod/objectql.zod.ts:130agreeing. So that fixture writes a spelling its own surface refuses, and stays green purely on verbatim pass-through. Exactly the shape worth recording.The deviation — ratified, and you were right to escalate it rather than act
The PR body still repeats the absolute ("Nothing has ever read
direction") and cites the superseded sha. You did not rewrite it, because the only copy reachable from your seat is the MCP tool's HTML-entity-escaped rendering (REST is dead in this container,ghis absent), and reconstructing a maintainer-facing body from that risks re-encoding damage — for a file the rework scope never named.That judgement is correct and I am not overriding it. My seat has the same constraint —
update_pull_requesttakes a whole body, and the only body I can read is the same escaped rendering — so "the PM does it instead" is not a better option, it is the same risk moved one level up. Weighing it plainly:toSortItems— a published export — still tolerantly reads the retireddirectionsort spelling (s.order || s.direction), dormant because nothing in-tree calls it #6011 pointer and the union re-run at the new sha, and it will be read by a human who can see both.✅ Posting the correction additively and handing the decision up was the right call, and stating the constraint that drove it is what made the call reviewable instead of a silent omission.
Also credited
✅ "NO ABLATION was run this round and none is claimed" — with the reason: the diff changes no executable line, so there is no guard whose failure could be demonstrated. Saying so beats fabricating a leg to fill the field, and this seat would rather read that sentence than a green one it cannot trust.
✅ The test-count reconciliation is exact rather than eyeballed. 23 files / 223 tests last round → 22 / 217 now, because the adapter file (6
it(/test(declarations, counted) was in the previous union and not this one. A prose-only diff that moves a verdict is the red flag the dispatch named, so you reconciled it instead of asserting nothing moved. That is the difference between a claim and a reading.✅ The worktree was removed without
--force, exit 0, and the sibling worktrees for #4730 and #5927 confirmed untouched. With four concurrent cards out this round, that check is not ceremony.Generated by Claude Code
docs(changeset,plugin-view): drop unregistered spec version literals;…
refactor(plugin-view,plugin-grid,types): retire table.defaultSort / o…