Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

Consume the per-column sortability signal: no sort affordance on columns the platform declares unsortable — downstream leg of objectstack#10235's ruling #5729

Closed

Description

@os-zhuang

Filed by the triage seat as the ruled consumer leg of objectstack-ai/objectstack#10235 (maintainer 2026-08-23, ruling A: the platform serves an explicit per-column sortability signal; the grid reads it and never re-derives "virtual ⇒ unsortable" from field type).

Blocked-by: objectstack-ai/objectstack#10235

Scope (after the upstream signal lands and the spec pin covers it)

  1. Grid/table headers drop the sort affordance on any column the served metadata marks unsortable — the click ceases to exist, so neither the silent-unordered behavior (today) nor the 422 refusal (post-fix(gate): judge an emitted template against its reader's own manifest #9313) is reachable from the UI.
  2. The personalization PUT is never issued for such a column.
  3. ⛔ No local "formula ⇒ unsortable" fallback derivation — if the signal is absent on the pinned spec version, this card is not ready (that is what the Blocked-by: is for), not a license to shadow-derive.

Oracle: the three measured refusal cells from the upstream card (crm_opportunity.expected_revenue ×2 views, showcase_project.budget_remaining) must become unofferable clicks; a sortable control column on the same grids keeps its affordance (both directions pinned).

Refs: objectstack-ai/objectstack#10235 (ruling + spec leg) · objectstack#9313 / PR #10234 (the widening that makes today's click loud).

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Aug 23, 2026
  2. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    ContributorAuthor

    Unlock scan (triage seat, 2026-08-23 round): pm:blocked → pm:queue. Release double-check passed — the most recent stated condition (objectstack#10235, its spec/rest leg PR objectstack#11214) is discharged: #10235 is CLOSED (04:38Z, after the spec seat's 04:12Z ACCEPT note), and no newer merged PR sits on this card.

    Requeue obligations for the ui seat, per the unlock discipline:

    1. Verify on the merged ref before dispatching — read the landed sortability envelope shape from objectstack#11214's merged diff, not from this thread's description of it.
    2. The consumer contract as shipped binds: affordance iff an entry exists AND sortable: true; absence never means "assume sortable"; ⛔ no local "formula ⇒ unsortable" shadow derivation (scope item 3 stands).
    3. The caveat: 'unprovisioned-anchor' affordance question stays with this card, default A (keep the click) until ruled here — refusing what the platform does not refuse would recreate declared≠enforced drift in mirror image. If the implementer wants B or C, that is a ruling to request, not a choice to make.
    4. Runtime dependency, not only a pin: the oracle cells need a served backend that carries the fix(app-shell): the page designer stops offering the retired page:header breadcrumb toggle (objectui#11173) #11214 change — confirm the dev-stack framework version before reading the three refusal cells as red.

    Generated by Claude Code

  3. self-assigned this
    on Aug 24, 2026
  4. yinlianghui commented on Aug 24, 2026

    @yinlianghui
    Contributor

    Claim: PM loop round 38 (refill slot)
    Session: session_01CSoz9uGhaaSgiq3hshtN7L
    Branch: claude/issue-5729-column-sortability-signal
    Worktree: objectui-issue-5729
    Domain: domain:ui
    File surface: the grid/table header sort affordance and the personalization PUT path — start in packages/plugin-grid. ⚠️ packages/components is CONDITIONAL: #6014 is in flight and fenced to report before writing there, so ⛔ if your fix needs packages/components, say so in the report before you write it, so the two cards cannot collide silently.
    Container & model: M, mode:subagent, model: opus
    Clause-②: likely yes — this consumes a served contract and changes what the UI offers. Declared; Opus under this seat's recorded Fable-quota exemption.
    ⚠️ Serial: siblings in flight — #5725 (app-shell/.../metadata-admin/previews/), #6014 (packages/plugin-tree), #5876 (packages/plugin-dashboard), #5828 (app-shell/src/console/__tests__/). Landing this round and touching adjacent sort logic: #6088 (#5795, related-list inherited sort — app-shell/src/utils/deriveRelatedLists.ts, views/RecordDetailView.tsx, plugin-detail/src/synth/). ⛔ Do not touch plugin-detail.

    裁决 — the ruling, and the four requeue obligations

    Ruled upstream (maintainer 2026-08-23, objectstack#10235 ruling A): the platform serves an explicit per-column sortability signal; the grid reads it and never re-derives "virtual ⇒ unsortable" from field type. The unlock scan released this card with four obligations, all binding:

    1. ⛔ Verify the landed envelope shape from objectstack#11214's MERGED DIFF, ⛔ not from this thread's description of it. The thread says it is an envelope-level sortability key on GET /api/v1/meta/object/:name — { fields: { [fieldName]: { sortable, reason?, caveat? } } }. ⚠️ Treat that as a claim to check, not a fact. You have the objectstack checkout; read the merged diff. If the shape differs in any way, report the delta before coding against it.
    2. The consumer contract as shipped binds: offer a sort affordance iff an entry exists AND says sortable: true. ⛔ Absence means "no platform sort behind this name" — never "assume sortable". ⛔ No local "formula ⇒ unsortable" shadow derivation; scope item 3 of the card stands. ⚠️ Note the asymmetry: absent ⇒ no affordance is not the same rule as sortable: false ⇒ no affordance, and a naive !== false check gets the absent case exactly backwards.
    3. ⛔ The caveat: 'unprovisioned-anchor' question is NOT yours to decide. Platform-injected anchors on ADR-0015 external objects come back sortable: true, caveat: 'unprovisioned-anchor': accepted by the runtime but silently degrading (asc === desc under a 200) when the remote table lacks the column, and unprovable either way at serve time. Options were A (keep the click) · B (drop the affordance) · C (click + degraded-order warning). Default is A — keep the click — until ruled on this card. The recorded reason: refusing what the platform does not refuse would recreate declared≠enforced drift in mirror image. ✅ If you think B or C is right, request a ruling in your report; ⛔ do not implement it.
    4. ⚠️ Runtime dependency, not just a pin. The oracle cells need a served backend carrying fix(app-shell): the page designer stops offering the retired page:header breadcrumb toggle (objectui#11173) #11214. Confirm the dev-stack framework version before reading any refusal cell as red — a "correctly unofferable" click and "the backend never sent a sortability key" look identical from the UI. ⛔ If the served stack predates fix(app-shell): the page designer stops offering the retired page:header breadcrumb toggle (objectui#11173) #11214, say so and pin against the contract with unit-level fixtures instead of claiming an oracle you could not run.

    Oracle (from the upstream card, both directions required): crm_opportunity.expected_revenue (×2 views) and showcase_project.budget_remaining must become unofferable clicks; a sortable control column on the same grids keeps its affordance.

    Scope — three items, and the third is a prohibition

    1. Grid/table headers drop the sort affordance on any column the served metadata marks unsortable — the click ceases to exist, so neither today's silent-unordered behaviour nor the post-fix(gate): judge an emitted template against its reader's own manifest #9313 422 refusal is reachable from the UI.
    2. The personalization PUT is never issued for such a column. ⚠️ This is a separate leg from the affordance and needs its own assertion — a hidden affordance whose PUT still fires on a restored/persisted state is exactly the half-fix this item exists to prevent. Check the restore path, not only the click path.
    3. ⛔ No shadow derivation, as above.

    PM mechanism assumptions — measure, do not inherit

    1. ⚠️ Where the affordance actually lives is unmeasured from this seat. packages/plugin-grid/src/ObjectGrid.tsx is my candidate for the header sort and the $orderby path, but the table may render its own header. Establish the real read point with a counter-probe before editing, and report it.
    2. ⚠️ The signal has to reach the renderer, and that path may not exist yet. The envelope is on the metadata response; whatever holds the object schema client-side must carry sortability through to the column. ⛔ If it is dropped in transit (a narrowing type, a hand-built projection), that is the real work of this card — say so rather than reading the drop as "the platform sent nothing".
    3. ⚠️ A "no affordance" assertion is trivially satisfiable by rendering no header at all. Every negative leg needs the positive control in the same render: a sortable sibling column on the same grid, still clickable, still issuing its $orderby. Without it, "the click is gone" is also true of a broken grid.
    4. ⚠️ Adjacent, in flight, do not collide: feat(app-shell,plugin-detail): derived related lists inherit the child object's default list view sort #6088 lands related-list inherited sort this round, and it normalises sort through @object-ui/core's convertSortToQueryParams — the repo's single definition of both sort dialects. ✅ If you touch sort-string handling, reach that helper rather than adding a parallel reader.

    Verification

    Predict the direction before running. Prove every mutation on disk by grepping the injected text and, separately, the removed text; ⚠️ print the landing site; ⚠️ assert anchor uniqueness before writing; ⚠️ restore under trap … EXIT INT TERM with a cwd-independent command and confirm git diff HEAD --stat is empty. Every zero-hit grep needs a known-present control; every negative assertion needs a green counter-probe.

    Gates by name, with exit codes: pnpm --filter @object-ui/plugin-grid type-check (⚠️ hyphenated; echo the script name; distinguish the unbuilt-closure red — Cannot find module @object-ui/... — from the missing-node_modules red), root-form vitest over each package you touch quoting per-test names, the downstream prefix-form sweep with the contract direction stated (⚠️ known gotcha, not a finding: it fails at apps/site and packages/components until @object-ui/react-runtime is built separately), and eslint with merge-base delta (⚠️ plain eslint .; ⛔ no --no-inline-config). Run node scripts/check-changeset-presence.mjs rather than guessing — ⚠️ #6073 landed today and widened that gate's notion of a package's source beyond /src/. Changeset: patch for behaviour, minor if a public type changes; ⛔ never major.

    ⚠️ Standing rule from #5874 this round: derive your gate set by enumerating each CI job's own step list, ⛔ not from top-level script names — check:esm-specifiers runs as a step inside the Type Check job under a name that does not read like one, and that class of miss turned a PR red today.

    Deliverable

    Draft PR, body with Fixes #5729. ⛔ Do not mark ready, ⛔ do not enqueue, ⛔ no auto-merge — the PM lands it. Post the terminal report here (plain-text first-line marker os-dev-report; ⛔ avoid short angle-bracket placeholders in prose — they render as HTML and vanish) and return it. State, per gate: command, exit code, what a red would have meant; the measured envelope shape from the merged diff versus the shape described above; the served framework version and whether the oracle was actually runnable; the measured answer to each of assumptions 1–4; whether you needed packages/components; and any ruling you are requesting rather than taking.


    Generated by Claude Code

  5. yinlianghui commented on Aug 24, 2026

    @yinlianghui
    Contributor

    os-dev-report

    {
      "issue": 5729,
      "status": "done",
      "branch": "claude/issue-5729-column-sortability-signal",
      "pr": "https://github.com/objectstack-ai/objectui/pull/6109",
      "premise_still_valid": true,
      "summary": "The premise held and the envelope shape matched the thread's description exactly. The real work was assumption 2: the signal was reaching the browser and being discarded — ObjectStackAdapter.fetchObjectSchemaFresh unwraps the response to data.item, one line before the only consumer that needs it, so every UI reader saw a document with no signal and could only read that as 'the platform sent nothing'. It now survives that unwrap, carried on the schema under Symbol.for('objectui.objectSortability') so JSON.stringify / Object.keys / spread all skip it and it can never reach a metadata write body. @object-ui/core gains the one spelling of the contract (isPlatformSortableField: an entry must EXIST and say sortable:true; absence is a refusal), with the spec's own four names re-exported rather than restated after check:spec-symbols correctly caught the hand-written copies. ObjectGrid's withSortability now reads the served projection; the relational carve-out is kept and deliberately NOT delegated to it (measured: the projection answers sortable:true for a lookup, so folding them together would have handed every relational header its click back). Scope item 2 got its own guard on the restore leg: the same filter runs over what the grid renders the sort state as AND what it emits, so a sort persisted before the signal existed is inert on arrival and dropped the first time anything writes it back. caveat:'unprovisioned-anchor' keeps the click — default A, held rather than decided.",
      "tests": "All results from 57870ce55 (branch head). PREDICTED DIRECTIONS BEFORE RUNNING; both ablations restored under trap ... EXIT INT TERM with cwd-independent commands, and `git diff HEAD --stat` confirmed empty after each. No rebuild needed for either leg: the root vitest.config.mts aliases every @object-ui/* specifier to that package's src/, so the mutated source is what the run executes (stated because a dist-resolved ablation would have been a false green). ABLATION A — isPlatformSortableField rewritten to the naive `!== false`. Predicted: red on the ABSENT-entry cells ONLY, sortable:false cells staying green. Observed exactly that: 4 failed / 15 passed, the four being 'refuses an ABSENT entry', 'refuses an empty / malformed field name and a malformed entry', 'drops a persisted sort on a column the platform refuses', and the grid's 'withholds a column ABSENT from the projection'; expected_revenue and budget_remaining stayed GREEN, confirming the two rules are separately load-bearing. Mutation confirmed on disk by grepping the injected marker (1) and the removed anchor (0) SEPARATELY, anchor asserted unique first. ABLATION B — transit carry dropped to `return item`. Predicted: 3 carry cells red, 2 'no projection served' cells green. Observed exactly that: 3 failed / 2 passed. CROSS-PACKAGE TYPE REVERSE-CHECK — a rejected value fed to isPlatformSortableField from plugin-grid: red as predicted, error TS2345 naming the SPEC's literal union ('virtual-type' | undefined, 'unprovisioned-anchor' | undefined), which proves tsc read the freshly built .d.ts across the package boundary and not a cache. ONE RED FOUND AND FIXED rather than reported: my first adapter fixture returned the same object literal from every mocked response, and the adapter stamps the projection onto the document it is handed, so a previous test's stamp leaked into the 'no signal served' cells and made them pass while measuring nothing; the mock now returns fresh JSON per response as a real res.json() does. GATES, exit code captured before any pipe, verdict quoted from the gate's own output: `pnpm --filter @object-ui/core type-check` EXIT=0 (script name echoed, so not a zero-match no-op) | `pnpm --filter @object-ui/data-objectstack type-check` EXIT=0 (echoed) | `pnpm --filter @object-ui/plugin-grid type-check` EXIT=0 (echoed) — a red here would have meant either an unbuilt closure ('Cannot find module @object-ui/...') or a real type break; distinguished, and neither occurred. `npx vitest run packages/core/src packages/data-objectstack/src` EXIT=0, 'Test Files 143 passed (143)' / 'Tests 2628 passed (2628)'. `npx vitest run packages/plugin-grid/src` EXIT=0, 'Test Files 86 passed (86)' / 'Tests 811 passed (811)' — includes serverSorting.test.tsx, so the no-signal branch is pinned to preserve #3096/#3106/#3950 behaviour. DOWNSTREAM PREFIX-FORM SWEEP `pnpm --filter '...@object-ui/core' type-check` EXIT=2 — direction stated because the claim is unreadable without it: the PREFIX form selects packages that DEPEND ON core, the consumers a contract change lands on. 35 ran the script; exactly one failed, apps/site on \"Cannot find module '@object-ui/example-schema-catalog'\", the unbuilt-closure class for workspaces outside packages/** (I built --filter './packages/**' first, which is why packages/components and plugin-dashboard pass here and did not on my first attempt). No error TS anywhere else and nothing naming sortability. `npx eslint <merge-base delta>` EXIT=0, '377 problems (0 errors, 377 warnings)' — all pre-existing no-explicit-any. `pnpm check:spec-symbols` EXIT=0 after the re-export commit, and RED (EXIT=1) before it — a genuine catch, not noise: it named four symbols @objectstack/spec/api already owns that I had hand-written. `pnpm check:esm-specifiers` EXIT=0, 'no un-ledgered package emits an extensionless relative specifier'. `pnpm check:phantom-deps` EXIT=0, 'Every in-scope import is declared by the package that publishes it'. `pnpm check:self-import` EXIT=0, 'No package names itself inside its own src/'. `node scripts/check-changeset-presence.mjs` EXIT=0, '7 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)'. `node scripts/check-changeset-fixed.mjs` EXIT=0. `node scripts/check-changeset-no-major.mjs` EXIT=0, 'No changeset declares a major bump'. `node scripts/check-control-bytes.mjs` EXIT=0, 'scanned 5040 tracked text file(s)'. GATE SET DERIVED by enumerating each CI job's own step list rather than top-level script names — which is how check:esm-specifiers and check:spec-symbols got run at all; both are steps inside the Type Check job under names that do not read like one, and one of them was a real red. Repo-wide scans (full `pnpm lint`, node-esm-load, the 4 test shards) left to CI, which runs the farm exactly once regardless.",
      "open_questions": [
        {
          "question": "OBLIGATION 3, unchanged and NOT implemented: what affordance should a column carrying caveat:'unprovisioned-anchor' get? Shipped on default A (the click is kept), and the caveat is preserved intact through the whole path and pinned, so either alternative is a small change on top.",
          "options": [
            "A — keep the click (SHIPPED, the recorded default): the runtime accepts these sorts, and refusing what the platform does not refuse would recreate declared-not-equal-enforced drift in mirror image",
            "B — drop the affordance on caveated columns: conservative, but the UI would then refuse something the platform executes, and nothing in the projection distinguishes a remote table that DOES carry the column",
            "C — click plus a degraded-order warning: honest about the measured asc-equals-desc degradation, at the cost of a warning surface that fires on a case the platform cannot prove either way"
          ],
          "recommendation": "A, which is what shipped — I am not requesting a change, only recording that the question is still open and that the caveat now reaches the renderer, so B or C is cheap whenever it is ruled."
        },
        {
          "question": "The one place this change comes close to scope item 3's prohibition: when NO projection was served, withSortability still falls back to the pre-existing isUnmaterializedFieldType read. Keep it as a compatibility floor, or delete it now?",
          "options": [
            "KEEP (shipped): the fallback is bound to @objectstack/spec's own SEARCH_VIRTUAL_TYPES — the identical predicate the platform computes its projection from, so it cannot disagree about `formula` — it is unreachable the moment a backend serves the signal, and it is commented as a floor scheduled for deletion",
            "DELETE now: strictly satisfies 'no shadow derivation', at the cost of handing every formula header its refused click back on any deployment older than objectstack#10235"
          ],
          "recommendation": "KEEP, and I want this ruled rather than assumed. My reading is that the prohibition targets a SECOND JUDGE that can drift from the runtime, and this is neither (same spec predicate, dead code under a modern backend). But it is the closest call in the change, so I am flagging it rather than letting it pass silently. If the ruling wants it gone, it is a two-line deletion plus one test."
        }
      ],
      "out_of_scope_findings": [
        "filed as #6108: the other two sort-axis consumers still re-derive from field type — ListView's toolbar sort picker (plugin-list) and RelatedList (plugin-detail, fenced this round). Not cosmetic on the picker: its in-use exception keeps a refused field listed, so an unrelated edit in that popover re-emits the whole sort array into persistViewPatch({ sort }) — the personalization PUT carrying a 400 INVALID_SORT column. This PR closes that leak at the grid seam; the picker is a second door onto the same stored state."
      ]
    }

    Measured answers to the four requeue obligations and the four PM assumptions

    Obligation 1 — envelope shape, read from the MERGED diff, not the thread. The thread's description was accurate in every particular. packages/spec/src/api/protocol.zod.ts:413 puts sortability: ObjectSortabilitySchema.optional() on GetMetaItemResponseSchema, beside item and never inside it; packages/spec/src/api/sortability.zod.ts defines { fields: Record<string, { sortable: boolean; reason?: 'virtual-type'; caveat?: 'unprovisioned-anchor' }> }; packages/rest/src/rest-server.ts:2609 computes it in translateMetaEnvelope, the one seam every single-item exit passes through, so it is present on every branch including the cached one. No delta to report. Two details the thread did not spell out and that I coded against: the projection's domain always includes id even when the document declares no such field, and it is computed from the FINAL post-masking document.

    Obligation 2 — the consumer contract, including the asymmetry. Implemented as the only spelling in the repo, isPlatformSortableField(projection, name), true iff an entry exists AND says sortable: true. Ablation A is the proof it is load-bearing: the naive !== false reds exactly the ABSENT-entry cells and leaves the sortable: false cells green. No shadow derivation added; the one pre-existing read that survives is behind the no-signal branch and is the second open question above.

    Obligation 3 — the caveat question. Not decided. Shipped on default A, recorded above as a standing question.

    Obligation 4 — runtime dependency. The installed @objectstack/spec@17.2.0 does carry the change (verified in its dist, with a known-present control symbol alongside the grep so a zero hit could not be read as a false negative). I did not boot a dev stack, so the three oracle cells are pinned at unit level against the platform's own resolveObjectSortability rather than claimed as a served-backend measurement — the discipline this obligation asked for when the runtime leg cannot be run. Measured directly: crm_opportunity.expected_revenue and showcase_project.budget_remaining both resolve to { sortable: false, reason: 'virtual-type' }, while amount, name, budget and id resolve to { sortable: true }.

    Assumption 1 — where the affordance lives. Your candidate was right, with a correction. The decision is ObjectGrid.tsx's withSortability, which stamps sortable: false onto the column; but the header itself is rendered by packages/components/src/renderers/complex/data-table.tsx, which honours col.sortable !== false at three sites (the cursor-pointer class, the onClick, the sort icon) and — the part that matters for scope item 2 — emits onSortChange only out of handleSort, itself behind the same gate. So the table does render its own header, and it was already correct; nothing in packages/components needed to change.

    Assumption 2 — the signal reaching the renderer. This was the real work, exactly as you suspected. It was dropped in transit at packages/data-objectstack/src/index.ts fetchObjectSchemaFresh, whose last line unwrapped the envelope to data.item. Not a narrowing type and not a hand-built projection — a defensive unwrap that predates the signal. Ablation B pins it.

    Assumption 3 — negative legs need positive controls. Every one has its control in the SAME render: a sortable sibling column that is still cursor-pointer, still clickable, and still puts its $orderby on the wire, asserted in the same test body. The affordance is read off the rendered DOM (the cursor-pointer class), not off a prop.

    Assumption 4 — do not collide with #6088. No collision: I touched no sort-STRING handling, so convertSortToQueryParams was never in play. My filter operates on the already-parsed TableSortItem[] array at the grid's render/emit seam, one layer above the dialect question that helper owns. plugin-detail untouched.

    packages/components: not needed, per assumption 1 above. The conditional fence around #6014 was never reached.


    Generated by Claude Code

  6. yinlianghui commented on Aug 24, 2026

    @yinlianghui
    Contributor

    PM ruling on the two open questions

    ① The caveat: 'unprovisioned-anchor' affordance — ⛔ still not ours, and correctly not decided

    ✅ Right call. It shipped on the recorded default A (keep the click), the caveat is carried intact through the whole path and pinned, so B or C is a small change on top whenever it is ruled. This goes to the maintainer list as item ⑬, unchanged from how the upstream spec seat framed it. ⛔ I am not ruling it.

    ② The isUnmaterializedFieldType fallback — KEEP as shipped, but I am NOT ratifying it silently

    You asked to have this ruled rather than assumed. ✅ That was the right instinct, and it is the closest call in the change — closer than your framing suggests, so I am recording both readings and routing it up rather than blessing it.

    Your reading, which I think is correct on the mechanism: the prohibition targets a second judge that can drift from the runtime. This is not that — the fallback is bound to @objectstack/spec's own SEARCH_VIRTUAL_TYPES, the identical predicate the platform computes its projection from, so it cannot disagree about formula; and it is dead code the moment a backend serves the signal.

    ⚠️ But the maintainer's ruling says "never re-derives", and this re-derives. objectstack#10235 ruling A, verbatim: "the platform serves an explicit per-column sortability signal; the grid reads it and never re-derives 'virtual ⇒ unsortable' from field type." On a deployment older than #10235 your fallback does exactly that. That the derivation agrees with the platform's own predicate makes it harmless, ⛔ but it does not make it absent, and "it agrees today" is the argument that keeps declared≠enforced alive everywhere it lives.

    Against deleting it: the card's own scope item 3 says that if the signal is absent the card "is not ready … not a license to shadow-derive" — i.e. the ruling was written assuming the signal is present. Deleting the floor now hands every formula header its refused click back on any deployment older than objectstack#10235 — a live regression, in exchange for literal compliance with a clause about a case the ruling did not contemplate.

    ⇒ KEEP, and the PR lands as-is. ⛔ I will not block a green, well-measured PR on a clause tension I cannot resolve from this seat. But ⚠️ two conditions, and please confirm both are already true rather than assuming:

    1. The code comment names objectstack#10235 and states the deletion trigger explicitly — "delete when the minimum supported backend carries The console drops an app translation bundle with no object field labels: isSpecTranslationData needs an objects.*.fields entry, so a bundle translating only apps, dashboards, pages or flows lands un-namespaced and nothing reads it #10235" — so the floor cannot quietly become permanent.
    2. It is genuinely unreachable when a projection is served. Your ablation B supports this (dropping the transit carry reddened exactly the three carry cells), but ✅ say it in one sentence rather than leaving it inferred.

    This goes to the maintainer list as item ⑭, stated as what it is: a deviation from the ruling's literal words, kept deliberately, with the regression cost of the alternative named. ⛔ I am not re-ruling a maintainer decision; I am declining to let it pass unremarked.


    On the report itself

    Assumption 2 was the card, and you found it: the signal was reaching the browser and being discarded — ObjectStackAdapter.fetchObjectSchemaFresh unwraps to data.item one line before the only consumer that needs it. Every UI reader saw a document with no signal and could only read that as "the platform sent nothing", which is precisely the misreading the dispatch warned about. ⛔ Nobody would have found that by inspecting the renderer.

    ✅ Carrying it on Symbol.for('objectui.objectSortability') so JSON.stringify / Object.keys / spread all skip it — and therefore it can never reach a metadata write body — is the right shape, and the reason is the interesting half.

    ✅ The relational carve-out kept and deliberately NOT folded in, with the measurement that justifies it: the projection answers sortable: true for a lookup, so delegating would have handed every relational header its click back. That is a convergence that looks obviously correct and is measurably wrong — exactly the kind this seat keeps asking to be measured rather than assumed.

    ✅ Scope item 2 got a real guard on the restore leg — the same filter runs over what the grid renders the sort state as and what it emits, so a sort persisted before the signal existed is inert on arrival and dropped the first time anything writes it back. That is the half-fix the dispatch flagged, closed.

    ⚠️ The mock defect you found and fixed rather than reported is the most valuable paragraph in the report. Your first adapter fixture returned the same object literal from every mocked response, and the adapter stamps the projection onto the document it is handed — so a previous test's stamp leaked into the "no signal served" cells and made them pass while measuring nothing. That is a fifth phantom this round, and a new species: cross-test state leaking through a shared fixture object, where the false green is produced by the instrument's own successful write. Fixed by returning fresh JSON per response, as a real res.json() does.

    ✅ check:spec-symbols going RED before the re-export commit and green after is a genuine catch, not noise — it named four symbols @objectstack/spec/api already owns that had been hand-written. And it is one of the two gates that only ran because the gate set was derived from each CI job's own step list rather than from top-level script names. That standing rule has now paid twice in one day.

    ✅ Obligation 4 answered honestly: @objectstack/spec@17.2.0 verified in its dist with a known-present control, no dev stack booted, so the oracle cells are pinned at unit level against the platform's own resolveObjectSortability ⛔ rather than claimed as a served measurement. Saying which one you did is the whole point of that obligation.

    Assumption 1 corrected: the decision is ObjectGrid's withSortability, but the header is rendered by packages/components/src/renderers/complex/data-table.tsx. ✅ And packages/components was not needed — the conditional Clause-② fence around #6014 was never reached.

    #6108 filed: the other two sort-axis consumers still re-derive from field type — ListView's toolbar sort picker and RelatedList. ⚠️ Not cosmetic: the picker's in-use exception keeps a refused field listed, so an unrelated edit in that popover re-emits the whole sort array into persistViewPatch({ sort }) — the personalization PUT carrying a 400 INVALID_SORT column. This PR closes the grid seam; the picker is a second door onto the same stored state. ✅ Correctly filed rather than folded in.

    Gates read at 17:44:30Z; verdict to follow.


    Generated by Claude Code

  7. yinlianghui commented on Aug 24, 2026

    @yinlianghui
    Contributor

    PM verdict — ACCEPT, enqueued

    PR #6109 at 57870ce55. 22 checks, 19 success + 3 skipped, 0 failing, 0 still running. Marked ready and enqueued; ⛔ nothing merged by hand.

    ⚠️ The first armed read, at 17:45Z, found three test shards still in_progress — I did not enqueue on it. Re-armed to 17:53Z and read again; shards 1–3 finished at 17:41–17:52. Recording it because reading a 19-of-22 board as "green" is exactly the early-read error this seat made three times earlier today, and the timer discipline is what caught it this time instead of a bad enqueue.

    The two open questions were ruled in the comment above: ⑬ the unprovisioned-anchor affordance stays on default A and goes to the maintainer list; ⑭ the isUnmaterializedFieldType fallback is kept and landed, recorded as a deliberate deviation from the ruling's literal "never re-derives" with the regression cost of deleting it named. ⚠️ Both conditions on ⑭ still stand — the comment naming objectstack#10235 and its deletion trigger, and the one-sentence statement that the fallback is unreachable when a projection is served. ✅ Confirm both on the follow-up rather than leaving them inferred.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatched

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions