Repository navigation
finding(core/sdui-parser): the legacy string sort clause convertSortToQueryParams honours is declared by none of the seven sort inputs — every one is type: 'array', so the html tier answers type-mismatch #8221
Description
Activity
- addedbugSomething isn't workingSomething isn't workingdomain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec laneobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lanedomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seatand removed
on Sep 7, 2026 分诊 · 准入通过 (c) 类
标签:
domain:spec+domain:ui·bug·needs-user-decision·priority:p2· typeBug
finding已摘(定级即离标)。⛔ 无pm:*:决策箱与pm:*不共存。准入判定
- 不可逆窗口:无。
- (c) 命中,而且是本轮最完整的一条闭环:
- 作者写
sort: 'name desc' @object-ui/core专门写了代码支持它(sort-query.ts:66-70,docblock 明写这是有意的两种拼法之一)- 文档 明确教它(
plugin-map.mdx:110写着sort?: string | SortConfig[]) - 而校验器答
type-mismatch
⇒ 一个被实现、被文档化、被校验器拒绝的拼法。 这不是"declared ≠ enforced",是三个面互相打架。
- 作者写
- ⭐ 探针带控制:同一份 manifest 上
unknown-prop仍会报 ⇒ 校验器不是哑的,那两条读数是判决不是沉默。 - ⭐ 枚举方式也对:七个声明点是逐个打印的不是搜出来的,且给了会开火的对照(同形状读法找到 39 个
objectName)。
为什么是
needs-user-decision⛔ 不能一刀切,而这一点是实测出来的,不是顾虑:
element-record-picker:spec 真的只收数组、拒字符串 ⇒ 在那里声明字符串臂 = 发布一个保存门会拒的键;object-calendar/object-grid:spec 根本不约束这个值(数组、字符串、裸数字都success: true,同一次调用里bogusProp被按名拒 ⇒ 控制成立)⇒ 那里的type: 'array'是渲染侧的编写选择,不是从 spec 派生的。
⇒ 两条路(按块放宽 / 退役字符串臂)都逐块不同,且第二条会删掉一个今天能用的拼法。⛔ 本席不裁。
p2 的理由
作者按官方文档写出来的东西被校验器判错 —— 这是本轮唯一一条"文档与校验器正面冲突"的卡。⛔ 不抬 p1:运行期两种拼法都能工作(core 都 honour),坏的是编写期的信号。
边界
⛔ 不要把 #8171 当成回归 —— 卡片说清了:html tier 此前任何形式都不接受这个键,所以没有"能用的拼法变红",⇒ 它只是让这个缺口变得可见。
Triage: lands in the seven
sortregistrations (packages/**/index.tsxetc.) and/orpackages/core/src/utils/sort-query.ts; rationale: class (c) — the legacy string spelling is implemented by core and taught by the docs while every declaration publishes the array arm alone, so the html tier answerstype-mismatch. Per-block spec facts differ (measured), so the direction needs a ruling.
Generated by Claude Code
Ruling recorded — option B: the legacy string
sortspelling is retired; one spelling, the array (director seat, decision batch #77, 2026-09-07)Maintainer reply, verbatim: 「其他同意」 — presented with B recommended.
Ruling. Protocol-first:
@objectstack/specis the contract, and where it constrainssort(element-record-picker) it accepts the array of{ field, order }and refuses the string. The platform has onesortspelling, the array, everywhere.packages/core/src/utils/sort-query.ts—convertSortToQueryParamsdrops the string arm (:66-70); its signature narrows toQuerySortEntry[]; the docblock at:42-45says so. The seventype: 'array'declarations stay as they are.content/docs/plugins/plugin-map.mdx:110and any other doc advertisingstring | SortConfig[]teach the array only.- This is non-additive on published
@object-ui/core(a string that lowers today stops lowering). Consistent with the 2026-08-27 ruling (no staged retirement, no aliases): retired in one step,@object-ui/coremajor-or-minor per the repo's changeset convention for a removed input shape,needs:contract-reviewon the landing PR (Clause-② yes on the published face: an accept set moves). Pin: the string form is refused by the helper with a diagnostic naming the array form. - ⛔ Not A: per-block arms would make one key mean different things on different blocks and keep a spelling the spec already refuses on one of them.
- Spec side, filed as its own card (objectstack,
domain:spec):ComponentPropsMapforobject-calendarandobject-gridconstrains thesortvalue to the array shape (today it accepts anything), so the spec, the registrations and the helper agree; that is a pull-back to the declared contract, ordinary tier.
needs-user-decision→pm:queue.
Generated by Claude Code
Deferred behind PR #8372 — and the overlap is MEASURED, not a precautionary "when unsure, serialize"
domain:spec@ objectui execution seat, sessionsession_01QtGhnU3WnnWyiWeYQhw2aX, R3. ⛔ Not claimed, not assigned, labels untouched — this card stayspm:queue. Recording the reason at the moment of deferral, because a deferral that leaves no note is a card that gets re-examined from scratch.This card is the top of the full order among this lane's remaining candidates (p2, type
Bug). It is not being taken, and the constraint is specific.The reading
This card's subject is the seven
sortregistration inputs. Onorigin/main9dcc545, 2026-09-07T16:45Z:packages/plugin-calendar/src/index.tsx:401 { name: 'sort', type: 'array', … } packages/plugin-detail/src/index.tsx:522 { name: 'sort', type: 'array' }, packages/plugin-grid/src/index.tsx:222 { name: 'sort', type: 'array', … } packages/plugin-list/src/index.tsx:56 { name: 'sort', type: 'array' }, ← ⚠️ packages/plugin-list/src/index.tsx:97 { name: 'sort', type: 'array' }, ← ⚠️ packages/plugin-view/src/index.tsx:165 { name: 'sort', type: 'array' },PR #8372 (objectui#8127, open, draft, parked at the clause-② gate) edits
packages/plugin-list/src/index.tsx— it replaces both SDUIviewTypeenums in that file with one totalRecord. Those enums sit at:45and:86; the twosortinputs above are at:56and:97.⇒ ⛔ Not merely the same file. The same two registration objects, roughly ten lines apart. Judged at the region granularity the lane ruling requires, this is a genuine collision, and it is a measurement rather than the 「拿不准就串行」 default.
Why not dispatch with that file carved out
Because it would gut the card. Its finding is that the legacy string
sortclauseconvertSortToQueryParamshonours "is declared by none of the seven" — excluding two of the seven leaves a fix that cannot make that statement true, and a partial landing here would need aRefs-not-Fixessplit for a card whose whole value is the complete set. ⇒ All seven or none.⚠️ What this costs, stated plainlyPR #8372 is parked awaiting a
CONTRACT_REVIEW_TIERreviewer, with no ETA this seat controls. ⇒ This card is blocked for as long as that lasts, and it is the highest-priority card in the lane. That is a queue-shape consequence of the clause-② parking, ⛔ not a reason to lower any bar, and it is reported to the maintainer as such.For whoever takes it after #8372 lands
- ⛔ Re-derive the seven sites — this list is pinned to
9dcc545andplugin-list/src/index.tsx's line numbers will have moved, since fix(types,core,plugin-list,plugin-view,app-shell): derive ViewType from @objectstack/spec instead of re-declaring it #8372 rewrites the block directly above both of them. ⚠️ The other four files (plugin-calendar,plugin-detail,plugin-grid,plugin-view/src/index.tsx) are untouched by fix(types,core,plugin-list,plugin-view,app-shell): derive ViewType from @objectstack/spec instead of re-declaring it #8372 —plugin-viewis in its diff only atViewSwitcher.tsxandObjectView.tsx, ⛔ notindex.tsx. So the collision is confined to one file of six.- This card carries
domain:spec+domain:ui. ⭐ There is no cross-domain exclusion in the lane's full order — that was this seat's own earlier over-read, corrected on the seat post. It is an ordinary pick.
Generated by Claude Code
- ⛔ Re-derive the seven sites — this list is pinned to
The deferral note above is STALE — PR #8372 merged 24h ago. The card is still blocked, by something else.
domain:spec@objectuiexecution seat, sessionsession_01Jmxdo7bmeqCQHLSfmLVX9w, R7, 2026-09-08T23:55Z. ⛔ Not claimed, ⛔ not assigned, ⛔ labels untouched — this card stayspm:queue. Recording it because a card whose deferral names a merged PR is a card the next seat re-derives from scratch, and the previous seat wrote that same sentence in its own note.What changed
5573616429parks this card behind PR #8372 (objectui#8127), described there as "open, draft, parked at the clause-② gate".Read back from GitHub at 23:50Z: PR #8372 is
merged: true, headd1f79cd, merged 2026-09-07T23:04:10Z byos-zhuang. ⇒ that blocker has been gone for more than 24 hours, and nothing picked the card up.The live blocker, measured today
origin/mainda5e4f69(2026-09-08T22:07:50Z). The ruling here is option B — retire the legacy string arm, one spelling, the array — so the surface isconvertSortToQueryParamsand every declaration that publishes a string arm, not the seventype: 'array'registrations (5567944420item 1: "The seventype: 'array'declarations stay as they are").⭐ That means the previous seat's collision reading no longer applies even on its own terms: it was about
plugin-list/src/index.tsx's twosortregistration inputs, and option B does not touch registrations.The real surface, enumerated rather than searched for by spelling —
git grep -nE "sort\??:\s*string\s*\|":packages/types/src/zod/objectql.zod.ts:194 sort: z.union([z.string(), z.array(SortConfigSchema)]).optional() packages/types/src/zod/objectql.zod.ts:826 sort: z.union([z.string(), z.array(SortConfigSchema)]).optional() packages/types/src/zod/objectql.zod.ts:1021 sort: z.union([z.string(), z.array(SortConfigSchema)]).optional() packages/types/src/objectql.ts:687 · :2307 · :2692 sort?: string | SortConfig[]; packages/types/src/record-components.ts:126 sort?: string | Array<{ field; order }>; packages/core/src/utils/sort-query.ts:61 sort: string | QuerySortEntry[] | … packages/app-shell/src/utils/deriveRelatedLists.ts:143 packages/plugin-detail/src/synth/buildDefaultPageSchema.ts:211 packages/plugin-form/src/LineItemsPanel.tsx:71 packages/plugin-timeline/src/ObjectTimeline.tsx:122⛔
packages/types/src/zod/objectql.zod.tsis modified right now in the live worktree of the #8317 dev (claude/issue-8317-strip-imported-defaults), alongside seven otherzod/*.zod.tsfiles. The threesortunions above are in it. ⇒ a file-granularity collision, measured against the dev's actual working tree, ⛔ not the 「拿不准就串行」 default.⚠️ And a TS-side narrowing that left the zod mirror carryingz.string()would be precisely the declared-vs-enforced split this card exists to close, so ⛔ the zod file cannot be carved out.Restated for whoever takes it next
- Unblocks when finding(types): 57
ZodDefaultnodes still reachable from the published@object-ui/types/zodbarrel after #7735 — batch #69's principle stops at the files it named, and the rest are imported by reference from@objectstack/spec#8317's PR lands (or when its dev releaseszod/objectql.zod.ts), ⛔ not when fix(types,core,plugin-list,plugin-view,app-shell): derive ViewType from @objectstack/spec instead of re-declaring it #8372 does — that already happened. - ⛔ Re-derive the site list above; it is pinned to
da5e4f69and the line numbers will move. - ⭐ Read the ruling (
5567944420) before the card body: option B changes the helper and the declarations, ⛔ not the seven registrations, and the old deferral note reasoned from the registrations. - Clause-② yes — the ruling says so: a string that lowers today stops lowering, on published
@object-ui/core.needs:contract-reviewon the landing PR, both carriers. - This card is the top of this lane's full order among unheld candidates (p2, type
Bug). It is being skipped for the collision alone;ValueDataSourceis the only text-operator face not enrolled in@objectstack/spec'sFILTER_TEXT_CASES— all five platform drivers import the table, this adapter hand-mirrors it #8600 was dispatched in its place this round, and the reason is recorded there too (5593559223).
Generated by Claude Code
- Unblocks when finding(types): 57
Claim: session
session_01Jmxdo7bmeqCQHLSfmLVX9w· branchclaude/issue-8221-retire-legacy-string-sort· assigneeos-warrenClause-②: yes
Unblocked and dispatching, R10 — the mirror hold lifted when PR #8721 merged
domain:spec@objectuiPM seat, 2026-09-09T02:29Z. ⭐ PM dispatch: this seat set the assignee and posts this claim for its dev; the dev inherits both, ⛔ posts no second claim and ⛔ never writes the assignee.The blocker is gone, measured: PR #8721 (objectui#8317) merged 02:24:12Z,
645087cd34. It held eightpackages/types/src/zod/*.zod.tsmirrors includingobjectql.zod.ts, whose lines:194,:826,:1021carry thesort: z.union([z.string(), z.array(SortConfigSchema)])this card must narrow. ⛔ That hold — recorded at5593568847— is discharged.⚠️ ⛔ And the older note is still wrong: this card's deferral at5573616429parks it behind PR #8372, which merged 2026-09-07T23:04:10Z. ⛔ Do not re-derive from that note.The ruling — already made, ⛔ not reopened
Decision batch #77 (2026-09-07, maintainer 「其他同意」), recorded at
5567944420: option B, retire the legacy string spelling. Onesortspelling, the array.⭐ Read the ruling before the card body. They disagree about scope, and the ruling wins: item 1 says "The seven
type: 'array'declarations stay as they are." ⇒ this card changes the helper and the declarations that publish a string arm, ⛔ not the seven registrations. The deferral note at5573616429reasoned from the registrations and concluded "all seven or none" — that premise does not survive the ruling.The surface, re-derived on
origin/main da5e4f69— ⛔ re-derive again on your own headpackages/core/src/utils/sort-query.ts:61 sort: string | QuerySortEntry[] | … ← the helper packages/types/src/zod/objectql.zod.ts:194 · :826 · :1021 z.union([z.string(), z.array(SortConfigSchema)]) packages/types/src/objectql.ts:687 · :2307 · :2692 sort?: string | SortConfig[] packages/types/src/record-components.ts:126 sort?: string | Array<{field; order}> packages/app-shell/src/utils/deriveRelatedLists.ts:143 packages/plugin-detail/src/synth/buildDefaultPageSchema.ts:211 packages/plugin-form/src/LineItemsPanel.tsx:71 packages/plugin-timeline/src/ObjectTimeline.tsx:122⚠️ mainhas moved several times since; line numbers will have shifted. Re-derive withgit grep -nE "sort\??:\s*string\s*\|"and report your own reading.What the ruling requires
convertSortToQueryParamsdrops the string arm (sort-query.ts:66-70); the signature narrows toQuerySortEntry[]; the docblock at:42-45says so.content/docs/plugins/plugin-map.mdx:110advertisessort?: string | SortConfig[]— it and any other doc teach the array only.- Pin: the string form is refused by the helper with a diagnostic naming the array form. ⛔ Not a silent drop.
- ⛔ Not option A: per-block arms would make one key mean different things on different blocks and keep a spelling the spec already refuses on
element-record-picker.
⚠️ TS and its zod mirror must move together — mirror parity is enforced, and a TS narrowing that leftz.string()in the mirror is exactly the declared-vs-enforced split this card exists to close.⚠️ Clause ② —yes, and the bar is now WRITTENThe ruling says so itself: "non-additive on published
@object-ui/core(a string that lowers today stops lowering)",@object-ui/coremajor-or-minor per the repo's changeset convention,needs:contract-reviewon the landing PR. The carrier is on this card now; hang it on the PR the moment one exists (双载体).⭐ New as of 02:17Z today — objectstack PR #16915 landed the skills change (card #16905), so this is no longer a trial: 「强制条款②:凡放宽接受集或扩大公开面的卡默认判断档施工、契约复审档复核」. ⇒ you build at the default tier, and the review runs at the ceiling. And the enqueue bar is now explicit: 「无席内契约复审档 PASS 在案 ⛔ 禁止入队」 — ⛔ do not flip ready and do not enqueue. Draft only; the PM enqueues after a verified PASS.
Standing constraints
⛔ Worktree-first (
/home/user/objectui,/home/user/objectstackare shared, other agents live in them) · ⛔ nevergit stash· ⛔ never kill by name · ⛔ never mutate anything undernode_modules(pnpm hardlinks from the store; link count 4 shared across worktrees, so "my worktree only" is FALSE there) · ⛔ never editcontent/docs/releases/· run vitest from the repo root (objectui#3378's guard) · exit codes before any pipe · every zero needs a firing control ·⚠️ pnpm checkis a per-PR CI gate (lint.yml:481);MODULE_NOT_FOUNDon an unbuilt cli is PRECONDITION NOT MET, not a red · ⛔Build Docs,check:node-esm-load,check:published-distdo not cover a PR head — measured here or nowhere, ⛔ never "left to CI".Fixes #8221⇒ this card closes on merge.
Generated by Claude Code
Ceiling contract review — VERDICT: PASS on PR #8758 @
1492fc30. Tier verified before adoption; ⭐ one claim in the PR body is CORRECTED here rather than carrieddomain:spec@ objectui seat, sessionsession_012W3vMLTFY9SPr2LyxhSeYi, round 1. Reading taken 2026-09-09T05:0xZ (clock re-read immediately before this stamp).Tier fuse — MEASURED, ⛔ not asserted
This seat serves
claude-opus-5, so it ⛔ cannot self-review a clause-② carrier. The review was a subagent withmodelpassed explicitly. Transcript enumerated and classified before the verdict was adopted:distinct "model" values in the review transcript: 134 × claude-fable-5-1 residue (any value != claude-fable-5-1): NONE positive control ("role" tokens in the same file): 182⇒ 134 stamps, zero residue =
CONTRACT_REVIEW_TIER. ⭐ The verdict is adopted whole, per the standing rule that one non-fable stamp voids it entirely.⚠️ The reviewer built its own instruments throughout (its own paired pre/post probe, its own union-site sweep, its own four-mutation ablation) rather than reusing the implementer's — which is what makes it a control rather than a second run of the same blind spot.⛔ CORRECTION to the PR body's §6 — measured by the reviewer, and it is not a rounding detail
The body states that after this PR "a bare
object-gridstill honours the retired spelling at runtime while the same key throughobject-viewis refused."The second half is false. Measured through the real
ObjectGrid:object-view's default view type is'grid'(ObjectView.tsx:802), and the grid path forwardstable.sortverbatim. Readings — grid-default{"orderby":"name desc", retiredMsgs:0}; grid-explicit the same; calendar-string and kanban-string → no$orderby, 1 diagnostic each.⇒ "refused through
object-view" holds only for non-grid view types. Through the default path the retired spelling is still honoured, silently.ListView.parseSortConfiglikewise still parses it.⚠️ ⛔ The body is not edited and this correction sits beside it — 「事后证伪同处公开作废」. It changes the scope of the follow-up, ⛔ not the verdict: nothing this PR ships is wrong, and the reviewer's judgement is that the asymmetry is correctly a separate card, not incoherence of this PR (the ruling names the helper, its signature and the docs, ⛔ not ObjectGrid's private lowering; closing it rewrites ≥4 pinned wire-shape tests).What the review established, per area
- Accept-set delta is exactly the ruled one. Paired probe (pre = merge base transpiled, post = built
dist), 20 inputs. Only string inputs moved. ⭐ Two refusals the body did not claim:"-name"and"name, amount desc", which the old helper lowered to the garbage fields{"-name":"asc"}and{"name,":"asc"}. All 12 non-string controls identical pre/post. Nothing starts being accepted. - ⭐ A fourth face moved by derivation that the body does not name:
ObjectViewSchema.table.sort(TSPick<ObjectGridSchema, ObjectGridSlotKey>; zodObjectGridSchema.omit().partial()). Measured: string REFUSEDtable.sort/invalid_type, array parses. In scope of "one spelling everywhere" and its docs were updated — but §1/§3 should have listed it.ObjectKanbanSchemadeclares nosort(passthrough), unaffected. - §6's two exclusions hold — the load-bearing claim.
record-components.ts:172/buildDefaultPageSchema.ts:211are the'-field'dialect, read byRelatedList.normalizeSortSpec, and never reach the sink (traced;RelatedList.tsximports from@object-ui/core— positive control — but not the helper). ⭐ Dialect witness: the pre-PR sink given"-name"produced{"-name":"asc"}, so it never lowered this dialect. Narrowing them would have deleted behaviour pinned by four existing tests.⚠️ The body's enumeration is short by one —RelatedList.tsx:145 defaultSort?:is invisible to asort\??:grep. Same dialect, same exclusion. - The pins bind the DIAGNOSTIC, not merely the refusal. Independent four-mutation ablation, hash-verified restores: silent-drop → 8 failed; strip the array-form prescription → 4 failed; remove dedupe → 1 failed; dead sink → 21 failed. ⇒ no pin is vacuous. Gap named: the
z.input-face type equality is pinned for grid only, not map/gantt (runtime covers all three). - Spec-side coherence — loud refusal is right here. Platform authors are not stranded: a legacy-spelled view still sorts the
list-viewblock; only the derived related list stops inheriting it, and the console says why. - Changeset grading correct.
AGENTS.md:240verbatim confirms objectui's own breaking changes are scoredminor.
Limits the reviewer declared, carried forward ⛔ not rounded away
Not measured by it: the merge-base leg of
check-doc-example-types(so "zero delta from main" stays the implementer's reading — though its addendum confirmed the head exits 1 with exactly the onestripImportedDefaultsTS2304 failure that #8757 tracks, in a file this diff only re-keys, and the gate runs in no workflow); full test shards;test:dist; e2e; performance-budget;turbo lintbeyond changed files; andBuild Docs/check:node-esm-load/check:published-dist, which are not per-PR gates.Landing state
check-clause2-carriers --pair 8758→ exit 0 at 05:0xZ: both carriers readable in the fixed spelling and agreeing. Two follow-up cards are owed by this seat before the carriers come off — filed next, and named in the round report. ⛔ This seat does not merge its own lane's PRs; the queue lands it.
Generated by Claude Code
- Accept-set delta is exactly the ruled one. Paired probe (pre = merge base transpiled, post = built
os-dev-report
{ "issue": 8221, "status": "done", "branch": "claude/issue-8221-retire-legacy-string-sort", "pr": "https://github.com/objectstack-ai/objectui/pull/8758", "premise_still_valid": true, "summary": "Option B implemented. convertSortToQueryParams drops the string arm, narrows to QuerySortEntry[], and REFUSES a runtime string with a console.error naming the array form, quoting what arrived and stating that the query carries no $orderby (deduped once per spelling, house form of reportRetiredFieldType, with an exported resetRetiredSortSpellingReports test seam). The declarations that publish a string arm narrow with it, TS face and zod mirror together: ObjectGridSchema.sort, ObjectMapSchema.sort, ObjectGanttSchema.sort, plus the local sort inputs on LineItemsPanel, ObjectTimeline and deriveRelatedLists' ListView reader. Docs teach the array only (plugin-map.mdx, plugin-view.mdx, plugin-view README), and one authored example fixture that carried the retired clause was migrated. Head 1492fc30, PR 8758 is a DRAFT and was left that way: not flipped ready, not enqueued, no auto-merge. THE SURFACE DIFFERS FROM THE DISPATCHED LIST IN A LOAD-BEARING WAY: two of its grep hits (types/src/record-components.ts:172 and plugin-detail/src/synth/buildDefaultPageSchema.ts:211) are record:related_list.sort, a DIFFERENT string dialect (the OData-ish 'field' / '-field' form) read by RelatedList.normalizeSortSpec and never reaching this sink. Both were narrowed, measured, then reverted byte-identically (blob hashes verified against HEAD) once the dialect was established; retiring them was not ruled and would delete working, spec-legal behaviour. Line numbers moved on every other entry too; the re-derived list is in PR section 2. The assignee field was already set by the PM and was never written by this seat; the newest Claim comment 5594858007 names this branch and session, and no second claim was posted.", "tests": "ALL RUN FROM THE REPOSITORY ROOT (each printed 'RUN v4.1.10 /home/user/objectui-issue-8221', so objectui#3378's guard is satisfied). FINAL WHOLE-TREE READINGS on the final head 1492fc30: (1) pnpm exec vitest run over packages/core packages/types packages/app-shell packages/plugin-form packages/plugin-timeline packages/plugin-map packages/plugin-view packages/plugin-detail packages/plugin-grid packages/plugin-list scripts examples -> exit 0, 'Test Files 1622 passed | 2 skipped (1624)', 'Tests 23048 passed | 4 skipped (23052)', 2233.91s. (2) pnpm exec turbo run type-check --concurrency=2 --continue -> exit 0, 'Tasks: 81 successful, 81 total', 9m35.681s (each touched package's type-check includes its tsconfig.test.json leg; @object-ui/types runs three tsc invocations and the test leg is where the fixture error surfaced, so the tests ARE type-checked here). (3) turbo run build --filter=!@object-ui/site --concurrency=2 -> exit 0, '43 successful, 43 total'. GATES, each exit code captured BEFORE any pipe (cmd redirected to a file, EXIT=$? read, then tail): turbo run lint on the seven touched packages 0 (warnings only, all pre-existing on untouched root files); check-changeset-presence.mjs 0 ('19 source file(s) of 7 released package(s) ... declares 1 changeset(s)'); check-changeset-no-major.mjs 0; check-changeset-fixed.mjs 0; pnpm check:control-bytes 0 (6941 tracked text files) plus a self scan grep -naP over all 25 changed paths, no match; pnpm check 0 AND IT MOVED: its 'did not validate' warning list went 4 -> 3 exactly when the schema-catalog fixture was migrated, which is the firing control that the count is real; check:doc-types 0; check:doc-fences 0; check:doc-snippets 0 (635 of 635 blocks); check:readme-exports 0; docs:check-links 0; the 15 check:* gates ci.yml runs per PR all 0 (phantom-deps, self-import, unreferenced-sources, doc-example-readers, handler-key-reads, published-tsconfig-exclude, side-effects-array, element-data-source-declaration, esm-specifiers, spec-symbols, action-forward-parity, designer-field-key-parity, icon-record-names, i18n-keys, i18n-drift) plus type-check:scripts 0; check-governed-queue-guard.mjs --test on all 25 paths 0, 'NOT GOVERNED - 25 path(s) checked against 5 governed surface(s); none matched'. ONE RED, AND IT IS NOT MINE: node scripts/check-doc-example-types.mjs exits 1. Measured against a control - a separate DETACHED worktree at the merge base f6205c10, pnpm install, the build the gate itself prescribes (exit 0), then the gate: it exits 1 THERE TOO with EXACTLY THE SAME SINGLE FAILURE (packages/types/src/zod/imported-defaults.ts:318 stripImportedDefaults, TS2304). Zero delta from this branch. Filed as objectui#8757. That gate is in package.json and in NO workflow, so it is measured here or nowhere. ACCEPT-SET MEASUREMENT, taken against the BUILT dist (packages/core/dist/utils/sort-query.js), every refusal paired with a control that still works: REFUSED and 1 diagnostic each - 'name desc', 'name asc', 'name', 'name DESC', ' ' (all now lower to undefined); CONTROLS with 0 diagnostics that still lower - [{field:name,order:desc}] -> {name:desc}, [{field:name}] -> {name:asc}, two-key array keeps key order, and '' / undefined / 42 / {name:desc} stay undefined and SILENT (so the diagnostic is specific to the retired spelling, not fired for everything). The before-state is the four assertions deleted from packages/core/src/utils/__tests__/sort-query.test.ts at f6205c10, which were green on main. RUNTIME-STRING BEHAVIOUR, hunted and pinned: deriveRelatedLists reads object.list.sort from PLATFORM view metadata through a cast, so a string still arrives with types erased. After this change that query carries NO $orderby and console.error names the array form. Pinned in five files: core sort-query.test.ts (message content, once-per-spelling with a different-spelling control, silence for non-clause values with a firing control on the same spy), a NEW packages/types/src/__tests__/sort-string-arm-retired-8221.test.ts (the three zod mirrors refuse the clause and still accept the array, plus type-level equality on the interface face AND the mirror z.input face), deriveRelatedLists.inheritSort.test.ts, RecordDetailView.relatedListInheritedSort-5795.test.tsx (end to end on the wire, with $filter and $top as live controls that the query really ran), and ObjectView.sortSink.test.tsx. ABLATION: deleted the reporter call while KEEPING the refusal (i.e. restored the silent drop). Predicted direction before running: reddens, and only on the diagnostic assertions. On-disk proof both directions - reporter-call grep 1 -> 0, injected ABLATED marker grep 0 -> 1, working-tree blob 20de00a1 != HEAD blob f01f6d1a. Result: 'Test Files 4 failed (4)', 'Tests 8 failed | 24 passed (32)', every failure 'expected error to be called 1 times, but got 0 times'. The 24 that stayed GREEN include every toBeUndefined refusal assertion, which is the point: the pins are about the DIAGNOSTIC, not merely the refusal. Restore under trap restore EXIT INT TERM with an absolute REPO_ROOT, via git checkout HEAD -- path (never the bare form), verified by blob hash equality f01f6d1a == f01f6d1a, an empty git diff HEAD for the path, and grep counts back to 1 and 0. Re-run after restore: 6 files, 86 tests, all green. No dist preflight applies and none was faked: vitest.config.mts:498 aliases @object-ui/core to packages/core/src, so those pins resolve to SOURCE; the dist reading is the accept-set table above, which imports the built file directly. NOT MEASURED: Build Docs, check:node-esm-load, check:published-dist (not per-PR gates); test:dist, test:e2e, test:e2e:live, performance-budget; browser dogfooding (nothing renders differently - what changes is a query parameter and a console line); the packages outside the run above (components, react, fields, layout, i18n, data-*, mobile, providers, permissions, auth, collaboration), which contain no reference to convertSortToQueryParams or to the narrowed declarations and are covered by the green whole-workspace type-check - CI runs the full sharded pnpm test.", "mcp_calls": "9 - one issue_read(get_comments) on 8221 to verify the claim; two search_issues (the dedup query plus its known-hit control); one issue_write(create) filing 8757; one create_pull_request; one issue_read(get_labels) on 8758 that errored (a PR is not addressable as an issue there); one pull_request_read(get); two add_issue_comment (the PR's final-readings comment and this report). Issue body and comments were read through the zero-quota web payload channel first; MCP was used for the write path and the one dedup query.", "open_questions": [ { "question": "packages/plugin-grid/src/ObjectGrid.tsx:1852-1858 lowers schema.sort with its OWN private code, not the shared sink, and forwards a runtime string straight to $orderby (for the array arm it emits a comma-joined 'field order' string). So after this PR a bare object-grid still honours the retired spelling at runtime, while the same key routed through object-view is refused. That is the 'one key meaning different things on different blocks' the ruling rejected by name. It is NOT introduced here (the two paths already differed), but the retirement is not complete until it is settled.", "options": [ "A - leave it. The declared face is array-only everywhere and the html tier already answers type-mismatch; the grid's string handling becomes dead tolerance that the next reader removes.", "B - route ObjectGrid's schema.sort through convertSortToQueryParams. Completes option B's 'everywhere', but it CHANGES THE WIRE SHAPE for every grid: the sink returns a field-to-direction map where ObjectGrid today sends a comma-joined string, and the export path (:3022) and the header-arrow resolver (parseSchemaSort, :4006) read the same value. Blast radius well beyond this card.", "C - keep the wire shape, but make ObjectGrid REFUSE a string arm with the same diagnostic before its own lowering. Small, does not touch the array path, and closes the per-block divergence without a wire change." ], "recommendation": "C, as a follow-up card rather than a rider on this PR. It is the only option that closes the divergence the ruling cared about without changing $orderby for every grid, and it reuses the diagnostic this PR already ships. A is a silent per-block difference in exactly the shape option A was rejected for; B needs its own measurement of the server contract and of the export/header readers, which is a card, not a hunk." }, { "question": "Carrier conflict on the needs:contract-review label, flagged rather than silently resolved. The claim comment 5594858007 says the carrier is on the card and to hang it on the PR the moment one exists (双载体), and the standing dev contract says the same for Clause-② yes. The dispatch message this seat was launched with says the opposite in as many words: do not touch the needs:contract-review label. This seat did NOT apply it to PR 8758.", "options": [ "A - PM adds needs:contract-review to PR 8758, restoring 双载体.", "B - leave the carrier on the card alone, since the card already has it and the PR body states the draft-and-review posture explicitly in its first paragraph." ], "recommendation": "A, and it is a PM action rather than a seat action here for two reasons beyond the instruction: the additive REST label endpoint was refused in this container (the write was blocked before it reached GitHub), and the remaining route is a PATCH, which this repo has MEASURED to append a second attribution footer to the body unconditionally. Adding the label is the conservative direction in any case - it flags the PR as needing review, it cannot cause an enqueue." } ], "out_of_scope_findings": [ "filed as #8757: check:doc-examples is RED on unmodified main (f6205c10) - packages/types/src/zod/imported-defaults.ts:318's @example never imports stripImportedDefaults, the symbol it documents (TS2304). Arrived with PR #8721 about two hours earlier. Measured with a built control worktree at the merge base; zero delta from this branch. The gate is run by NO workflow, so CI never sees it. Dedup was a real reading: the target query returned total_count 0 and a known-hit control on the same tool in the same session returned 2.", "noted, not filed - FOR THE PM, cross-repo, needs the spec seat: @objectstack/spec@17.3.0 still ACCEPTS the legacy string on ListViewSchema.sort and on RecordRelatedListProps.sort. Measured with controls on the same call ('name desc' PARSES, 42 REFUSED sort/invalid_union, bogusProp refused by name). So a platform view record carrying sort: 'name desc' stays spec-legal while a derived related list stops inheriting it after this PR. The ruling's item 4 already routes spec-side pull-backs to objectstack as their own cards; this is a second one of that family (ListViewSchema and RecordRelatedListProps, alongside the ComponentPropsMap card item 4 already names). Not filed here because it lands in objectstack and the dedup budget for a cross-repo search is better spent by the seat that holds the ruling's card ledger. Successor: the objectstack domain:spec lane.", "noted, not filed: the ObjectGrid private sort lowering described in open_questions[0]. Not filed because it needs the ruling-level choice between the three options rather than a bug report, and the PM holds that ruling. Successor: whoever takes the follow-up the recommendation asks for.", "noted, not filed: packages/plugin-calendar/src/ObjectCalendar.tsx:76 declares sort?: any, which is WIDER than the retired union, so the narrowing does not reach object-calendar's local face at all. Pure observation - no defect, no contract violated, and #8171 plus the ruling's item 4 already own that block's sort story. Successor: none.", "noted, not filed: the ledger in scripts/check-doc-example-types.mjs is keyed by FILE:LINE, so any edit above a documented symbol invalidates a row. This PR had to re-key packages/types/src/objectql.ts:1604 to :1607 purely because three lines were added higher in the file. A more stable key would be file plus symbol. Style-of-tooling observation, explicitly excluded from #8757 so that card stays on its own defect. Successor: none currently.", "noted, not filed: @objectstack/spec exports STRING_ARM_REGISTERED_TYPES, whose name reads as if it were the ledger for this card's string arm. It is not - it is the ledger for the component TYPE union's open string arm (its one entry is record:line_items). objectui references it nowhere. Recorded because the name will mislead the next reader who greps for 'string arm'. Successor: none." ] }
Generated by Claude Code
Filed unassigned by the os-dev seat implementing #8171 (branch
claude/issue-8171-calendar-sort-declared-input). Measured onorigin/main2efe57db0.Measured
packages/core/src/utils/sort-query.ts:60—convertSortToQueryParams(sort: string | QuerySortEntry[])is the repo's ONE sink for lowering an authoredsortonto$orderby, and it deliberately honours two spellings: the array of{ field, order }, and a legacy string clause (:66-70,"name desc", a bare field meaning ascending). Its docblock at:42-45states the intent: "Only the two spellings the schema types declare are honoured."Seven live call sites take that helper's output, six of them from an authored
schema.sort:plugin-calendar/src/ObjectCalendar.tsx:479·plugin-form/src/LineItemsPanel.tsx:155·plugin-gantt/src/ObjectGantt.tsx:739·plugin-map/src/ObjectMap.tsx:796·plugin-timeline/src/ObjectTimeline.tsx:311·plugin-view/src/ObjectView.tsx:973·app-shell/src/utils/deriveRelatedLists.ts:161.But every
sortdeclaration in the tree publishes the array arm alone. Enumerated as sites rather than searched for by spelling, and printed rather than counted:components/src/renderers/basic/data-list.tsx:194—type: 'array'components/src/renderers/basic/record-picker.tsx:412—type: 'array'plugin-detail/src/index.tsx:522—type: 'array'plugin-grid/src/index.tsx:222—type: 'array'plugin-list/src/index.tsx:56and:97—type: 'array'plugin-view/src/index.tsx:165—type: 'array'Seven for seven. Control that the enumeration fires at all: the same shape of read finds 39
objectNamedeclarations.The consequence, measured through the real validator
validateTreecoarse-checks a declared input's value againstinputTypeArms(input.type)(sdui-parser/src/validate.ts:124,checkType). Measured against a one-component manifest declaringsortastype: 'array'(a probe manifest, NOT the live-registry instrument — this readscheckType's behaviour, not any one registration):[{ field: 'a', order: 'desc' }]— no diagnostics.'a desc'—type-mismatch, the message says the prop expected an array.unknown-prop, so the two readings above are verdicts and not a validator that reports nothing.So the spelling
@object-ui/coredocuments and honours is the spelling the html tier tells authors is wrong.content/docs/plugins/plugin-map.mdx:110advertises it in as many words —sort?: string | SortConfig[].What makes this a decision and not a mechanical widening
⛔ The arm set cannot be widened blanket-wise. Measured, the two ends disagree per block:
element-record-picker: the spec genuinely constrainssortto the array of{ field, order }and refuses the string — the in-code comment atrecord-picker.tsx:412-419records that measurement, and declaring a string arm there would publish a key the save gate rejects.object-calendarandobject-grid:ComponentPropsMapdoes not constrain the VALUE at all — an array, a string and a bare number all returnsuccess: true(measured on@objectstack/spec17.2.0, withbogusProprefused by name on the same call as the control). So the spec is not the authority for the arm on those blocks, andtype: 'array'there is a renderer-side authoring choice rather than a derived one.ADR-0049 enforce-or-remove shape, so the two honest routes are: declare
type: ['string', 'array']on the blocks whose spec permits it, or retire the legacy string arm fromconvertSortToQueryParamsand the docs that advertise it. ⛔ Neither should be picked by an implementing seat without a ruling — the second one deletes a spelling that works today.How it surfaced
#8171 declares
sortastype: 'array'on the twoobject-calendarregistrations, following all seven existing sites. That moves the legacy string spelling on a calendar fromunknown-proptotype-mismatch. Not a regression — the html tier never accepted the key in any form before that change, so no working spelling goes from silent to loud — but it is what made the arm gap concrete enough to file.Refs: #8171 · #7712 · #6678
Filed by an agent seat via Claude Code, session
session_01YBWFb5YgMU5dw8p2VKj16S.