Skip to content

feat(spec)!: retire the bare string sort clause on the list-view doors — the PRODUCER half of the sort seam - #17914

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-17053-listview-sort-legacy-string-retired
Sep 13, 2026
Merged

feat(spec)!: retire the bare string sort clause on the list-view doors — the PRODUCER half of the sort seam#17914
os-zhuang merged 3 commits into
mainfrom
claude/issue-17053-listview-sort-legacy-string-retired

Conversation

@os-bill

@os-bill os-bill commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

Fixes #17053

Clause-②: no

⚠️ Governed surface — this diff touches skills/**, so under Prime Directive #14 it is human-merge only. ⛔ Not queued, not ready-flipped, no auto-merge armed by this round.

The seam, which is what this is graded on

objectui ruled one sort orthography platform-wide — the array (objectui#8221, decision batch #77, 2026-09-07, option B) — and objectui PR #8758 executes it: convertSortToQueryParams refuses a runtime string and its diagnostic names the array form.

ListViewSchema.sort is the producer of exactly those documents: object.list.sort is what deriveRelatedLists reads. So a view authored with sort: 'created_at desc' validated here, cleanly, and then failed downstream — the contract minting a shape its consumer rejects, with the author told off by the wrong layer. Triage graded p2 on that asymmetry, not on the string. This PR is the producer-side pull-back.

Reproduction, re-taken on today's tree

Taken on origin/main at a9c6477 (the card measured against installed 17.3.0; this is the re-take), with bogusProp refused by name on the same call as the firing control:

input to ListViewSchema.safeParse BEFORE AFTER
sort: 'name desc' PARSES REFUSED at sortinvalid_type, prescription naming the array
sort: '-name' PARSES REFUSED at sort
sort: [{ field, order }] PARSES PARSES, value-for-value unchanged
sort: 42 REFUSED sort/invalid_union REFUSED sort/invalid_type, zod's default message
CONTROL bogusProp refused by name refused by name

The control fires in both columns, so both the PARSES and the REFUSED readings are verdicts rather than a schema reporting nothing.

Requirement 1 — the census, and it is NOT a silent zero

The narrowing is not free, so the population was measured. The census swept both the TS and JSON spellings of a string-valued sort across the whole tree and each hit was read as a structure, never counted as a token.

It found three live authored sites on the narrowed slot, all converted here:

  1. examples/app-showcase/src/ui/views/task.view.tssort: 'estimate_hours desc' on a shipped list view, carried since objectui#2601 as a deliberate live coverage fixture for the string form.
  2. packages/lint/src/showcase-shape.fixtures.ts — the frozen snapshot of that same shipped shape (it tracks the app through defineView; its subject, the three nameless form sections, is untouched).
  3. skills/objectstack-ui/rules/list-views.md — the published rule taught the clause in two code blocks.

A migration entry is therefore owed, and this PR carries one (below). A silent narrowing would have been refused, correctly.

Lit control — the census could have found one, and an independent instrument agrees. With the legacy string put back on the real shipped showcase view and nothing else changed, tsc reds at exactly that line:

src/ui/views/task.view.ts(208,7): error TS2322: Type 'string' is not assignable to type '{ field: string; order: "asc" | "desc"; }[]'.

Mutation proven landed (git hash-object a83cbee → 38a2f9b, occurrence count 0 → 1); restore proven by hash equality back to a83cbee and an empty git diff HEAD.

Read, not grepped — sites deliberately NOT converted. ObjectQL query.sort and the wire normalizeSortNodes (different doors, different dialects); packages/spec's book/doc field-mapping records whose sort: 'order' is an unrelated key of the same name; and the packages/lint rule fixtures, which feed the PRE-parse walker and never reach this schema.

Requirement 2 — #16553 does NOT cover this, re-measured

Re-read rather than assumed. #16553 is closed as completed, and its title and body bound it to ComponentPropsMap for object-grid / object-calendar. Its landed artifact, the semantic entry 18.object-block-sort-item-array, names only those two doors and states in its own acceptance criteria that record:related_list is the one deliberate exception — ListViewSchema appears nowhere in it. The decisive reading is the reproduction above: on today's origin/main, with that work already merged, ListViewSchema.sort still accepted 'name desc'. The gap is real and this PR is what closes it.

What changed

sort survives as a key, one union arm lighter, so this is a value narrowing with no retiredKey() tombstone to hang a prescription on. The surviving array member's own error map carries it, keyed on issue.input being a string — the same shape view.type's retired 'page' value and view.exportOptions' retired 'pdf' value already use in this schema. Every other invalid value, and any string reaching a descendant (a misspelled order, say), keeps zod's default report, so nobody is told a clause they never wrote "was removed".

Migrationlist-view-sort-string-clause-to-array, a D2 conversion wired into the protocol-18 chain step, not a semantic TODO: the rewrite is lossless and wholly mechanical. 'created_at desc' is the tuple; a bare field name meant ascending and is written out as order: 'asc'; a comma-separated clause becomes one entry per key, in the same order. A clause that does not parse as that grammar is left alone and meets the door instead — the '-field' dialect belongs to RecordRelatedListProps.sort, never reaches convertSortToQueryParams, and retiring it was not ruled, so guessing a direction for it would invent an ordering the author never wrote.

A real consequence the gates caught. Removing the string arm made the sort entry's own keys visible to the liveness walk, which reported view/list.sort as an undeclared container inheritance. Drilled with in-repo evidence (normalizeSortNodes reads both field and order) rather than parked in the shrink-only baseline, so the container-coverage numbers did not grow.

Ablation — both directions, with a cost-direction leg

Subject resolves through a same-package relative source import, so no dist leg is in play; on-disk proof is still taken on every leg. Each leg: mutate → prove it landed (occurrence count AND git hash-object) → run → restore → prove restored by hash AND empty git diff HEAD. Baseline blob 6053ffc.

leg mutation occurrences hash result
1 — defect direction re-admit the z.string() union arm 5 → 6 6053ffc → a46df91 3 failed / 3 passed
2 — cost direction over-narrow the surviving arm (order enum loses 'desc') 0 → 1 6053ffc → 48a1bcb 2 failed / 4 passed, on the POSITIVE pin
control unmutated tree 6053ffc 6 passed, 0 failed

Leg 2 is the cost direction on purpose: the price of this change is collateral narrowing of the spelling that has to keep working, and it shows the positive pin catches exactly that.

Verification

run exit
pnpm --filter @objectstack/spec test (vitest run --project local) 0 — 474 files, 13492 passed
pnpm --filter @objectstack/spec test:repo (vitest run --project repo) 0 — 31 files, 523 passed
pnpm --filter @objectstack/spec check:generated 0 — "All 15 generated artifacts are up to date."
pnpm --filter @objectstack/spec build 0
typecheck: spec · lint · example-showcase 0 each
node scripts/check-skills-token-ratchet.mjs 0
node scripts/check-nul-bytes.mjs 0

Every exit code above was captured before any pipe (cmd > file 2>&1; EXIT=$?), and each gate's own printed verdict line is what is quoted.

Regenerated artifacts moved exactly as a value narrowing should: nine doc rows across three content/docs/references/** files and one react-blocks contract row each lost the string | arm, and nothing else moved.

Clause-② — the two limbs, answered from the regenerated artifacts with a lit control

Declared no as dispatched, and ⛔ not flipped by this round; the seat sets the final value.

  • (a) Does the final diff add any exported symbol? — ZERO new exports. packages/spec/api-surface/** and packages/spec/export-origins/** are byte-identical to the base commit a9c6477.
  • (b) Does it add any key on a published payload? — No. packages/spec/authorable-surface/** and authorable-defaults/** are likewise byte-identical. The diff removes a union arm; it declares no new key anywhere.

Lit control, so those zeros are measurements and not a blind instrument. One dummy export const OsIssue17053LitControl = 1; appended to the same file, then rebuild + gen:api-surface + gen:export-origins, moved both artifacts by exactly one line each and named the symbol:

packages/spec/api-surface/ui.json:275:    "OsIssue17053LitControl (const)",
packages/spec/export-origins/ui.json:271:    "OsIssue17053LitControl": "src/ui/view.zod.ts#OsIssue17053LitControl (const)",

Mutation proven landed by hash (6053ffc → ba113de); source and both artifacts restored and proven back at 6053ffc with a clean tree.

Changeset

@objectstack/spec: minor, graded against this repo's own precedent for an accept-set narrowing — the sibling object-block-sort-item-array changeset took minor for the same ruling under the launch-window convention for breaking changes. It carries the ADR-0087 registration marker.

Publish surface measured rather than assumed, with controls:

  • @objectstack/spec files[] ships dist and src/**/*.zod.ts, so the edited view.zod.ts is literally published ⇒ a changeset is owed.
  • @objectstack/lintno changeset owed. Its files[] is ["dist","README.md","CHANGELOG.md"], and the edited fixture symbol SnapshotTaskViews has zero occurrences in packages/lint/dist/, while the positive control validateSortableFields has three. The grep fires; the fixture is simply not published.
  • examples/app-showcase is private: true.
  • skills/** is in no package's files[].

Skills line budget

skills/objectstack-ui/rules/list-views.md — 306 lines before, 306 after (net 0). Package total over all SKILL.md files: 6134 before, 6134 after (no SKILL.md was edited).

The binding reading is the token ratchet, which reds where lines do not. The first draft came in at 3154 tokens against a 3011 ceiling (+143). ⛔ The ceiling was not raised and no re-wrap was used as currency: the net increase was paid entirely by deleting content — the retired string examples this change makes wrong. Final reading 3009 / 3011, gate exit 0. The migration prescription therefore lives in the parse error, the changeset and the upgrade guide rather than in the published rule, which is where an upgrading author actually meets it.

维护者速读(草稿)

改了什么ListViewSchema.sort 不再接受旧的字符串子句('created_at desc'),只接受 { field, order }[] 数组。同批把树内三处仍在写字符串的文档改成数组,并补上一条 D2 迁移条目。

为什么改 — objectui 已经裁定「一个拼法,数组」并在 PR #8758 里让消费端运行时拒收字符串。spec 是那些文档的生产者,于是出现最坏的缝:文档在上游通过校验、在下游失败,作者被错误的那一层训斥。分诊原话:契约铸造了一个消费者拒收的形状。p2 判在这个不对称上,⛔ 不判在字符串本身。

风险与代价(含回滚) — 代价是真实的:用旧字符串写的文档停止通过校验。普查测出树内三处(showcase 列表视图、lint 快照、已发布技能),全部已转换,因此这不是静默收窄——D2 条目 list-view-sort-string-clause-to-array 可机械重写作者源码,存量行按既有路径重放。RecordRelatedListProps.sort'-field' 方言未被触碰(不同方言、未经裁定),该形状的字符串本条目也拒绝猜测方向。回滚 = revert 本 PR:收窄是纯删除一条 union 分支,无数据迁移、无存量改写,回滚不留残迹。

席位意见 — (留空)

你要做的 — 定 Clause-② 终值(本轮实测两肢均为零,见上)。本 PR 触 skills/** ⇒ 受管面,按 Prime Directive #14人工合并

验收备注

Out-of-scope observations, noted and deliberately NOT filed:

  • packages/lint's readSortKeys keeps a string arm for sort. It is a pre-parse walker on raw authored stacks, so it is defensive rather than dead, and packages/lint is a sibling round's declared face. Carrier: whoever next revisits that rule. Noted, not filed.
  • packages/lint/src/showcase-shape.fixtures.ts is the one file in this diff inside packages/lint, the face declared for An action cannot declare which bulkActions dispatch contract its body is written for — bare-string fan-out and execution: 'aggregate' deliver opposite input shapes through one authoring surface #17319. It is a one-line value change forced by tsc (the snapshot tracks the shipped app through defineView), and it does not touch that round's subject. Flagged here so the seat can see the overlap rather than discover it at merge.
  • The conversion walk reaches stack.views[] in all three persisted spellings but not objects[].listViews.* — the same boundary view-page-mount-removed states for itself. An object body carrying a string clause is refused at its own door rather than converted. Stated in the conversion's docblock rather than left to be discovered. Noted, not filed: widening the walk is its own card with its own population measurement.

Generated by Claude Code

`ListViewSchema.sort` accepted both the legacy string clause (`"field desc"`)
and the structured `{ field, order }[]` array. objectui ruled one sort
orthography platform-wide -- the array (objectui#8221, decision batch #77,
2026-09-07, option B) -- and objectui PR #8758 executes it: its
`convertSortToQueryParams` refuses a runtime string outright.

That made this schema the PRODUCER of documents its own consumer rejects: a
view authored with `sort: 'created_at desc'` validated here, cleanly, and then
failed downstream, with the author told off by the wrong layer. The grade sat
on that asymmetry, not on the string.

`sort` survives as a key, one union arm lighter, so this is a VALUE narrowing
with no `retiredKey()` tombstone to hang a prescription on. The surviving array
member's own `error` map carries it, keyed on `issue.input` being a string --
the shape `view.type`'s retired `'page'` and `view.exportOptions`' retired
`'pdf'` already use here. Every other invalid value, and any string reaching a
descendant, keeps zod's default report.

The narrowing is not free, so the population was measured rather than assumed:
a tree-wide census read as structures (not counted as tokens) found the clause
authored on three live sites, all converted here -- the shipped showcase list
view, the frozen lint snapshot of that same shape, and the published
objectstack-ui list-view rule. It therefore carries a D2 conversion,
`list-view-sort-string-clause-to-array`, wired into the protocol-18 chain step:
the rewrite is lossless and wholly mechanical. A clause that does not parse as
`<field> [asc|desc]` is deliberately left alone -- the `'-field'` dialect
belongs to `RecordRelatedListProps.sort`, never reaches
`convertSortToQueryParams`, and retiring it was not ruled.

Retiring the string arm also made the sort entry's own keys visible to the
liveness walk, which reported `view/list.sort` as an undeclared container.
Drilled with in-repo evidence rather than recorded in the shrink-only baseline:
`normalizeSortNodes` reads both `field` and `order`.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:ui tests tooling labels Sep 13, 2026
@github-actions

github-actions Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/spec, touching 11 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/spec/liveness/state-counts.md, packages/spec/liveness/view.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

16 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 225197cdbaff647a8b12e52bcb9d7bbb5b07ed08.

1 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/liveness/state-counts.md, packages/spec/liveness/view.json) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: created_at (literal, 34 pages)
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 225197cdbaff647a8b12e52bcb9d7bbb5b07ed08packageMentionDocs.

Which tree this was computed on

This run read content/docs from c01aa19bafb58b112648a619ada5c8f55c8da67e — the merge of head c7ae2c1026169d93a75f103c27190537052330fc into base 225197cdbaff647a8b12e52bcb9d7bbb5b07ed08, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin c01aa19bafb58b112648a619ada5c8f55c8da67e && git checkout c01aa19bafb58b112648a619ada5c8f55c8da67e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 225197cdbaff647a8b12e52bcb9d7bbb5b07ed08 c7ae2c1026169d93a75f103c27190537052330fc && git checkout -B drift-repro 225197cdbaff647a8b12e52bcb9d7bbb5b07ed08 && git merge --no-ff c7ae2c1026169d93a75f103c27190537052330fc

node scripts/docs-audit/affected-docs.mjs --json 225197cdbaff647a8b12e52bcb9d7bbb5b07ed08

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 225197cdbaff647a8b12e52bcb9d7bbb5b07ed08 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-zhuang
os-zhuang marked this pull request as ready for review September 13, 2026 06:21
@os-zhuang
os-zhuang enabled auto-merge September 13, 2026 06:21
@os-zhuang
os-zhuang disabled auto-merge September 13, 2026 06:22
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 13, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 34742873716 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — evidence pointers (#5623) > is green against a verbatim copy of the shipped ledgers
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — evidence pointers (#5623) > stays green when the missing path is attributed to ANOTHER repo
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — evidence pointers (#5623) > never bounds a citation attributed to ANOTHER repo
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — evidence pointers (#5623) > prints the citation count and how many are in range, in the documented two
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — symbol anchors (#12516) > stays GREEN on the drifted BEFORE-state — the honest residual this grammar e
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — symbol anchors (#12516) > prints the anchor count and how many resolve, equal on a green run
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — the evidence-scan population (#13041) > stays GREEN when a `dead` entry carries the SAME rotted pointe
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — the evidence-scan population (#13041) > declares every status either scanned or explicitly unscanned, 
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — the live-elsewhere criteria (#13483) > is green on the shipped ledgers and publishes the population be
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — the README state table (#7257) > is green against a verbatim copy, and says how many rows it checked
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — the generated count artifact (#7377) > is green against a verbatim copy, and says the artifact is curr
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — the evidence summary line (#5623) > prints declared and resolved as separate numbers, equal on a green
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

  • scripts/liveness/check-liveness.test.ts — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 0 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

…d ledgers

The merge with main dropped one of two count bumps. This branch's `view` row
went 77 -> 78 and main's `action` row went 43 -> 44, and each side wrote the
SAME new bytes on the `**total**` row (`**862**` / `**973**`, each counting only
its own addition). A text merge keeps one copy of an identical change, so the
merged file carried both per-type rows and a total that had only been advanced
once: 862/973 where the walk measures 863/974.

`check:liveness` graded that as a stale generated artifact and exited 1, which
is what took the merge-queue build down: 15 cases in
scripts/liveness/check-liveness.test.ts spawn the real gate against a verbatim
copy of the shipped ledgers and assert exit 0, so every one of them went red on
the merge commit while the PR head alone stayed green (the PR-side subset never
built the merge).

Regenerated wholesale rather than hand-patched, per the gate's own instruction:
the count columns are generated (#7377) and hand-patching puts the numbers back
in the merge path where they merge clean and wrong.

content/docs/references/data/object.mdx rides along for the same reason in the
other direction: the os-regen driver kept this branch's side and dropped main's
new `action.execution` row; the regeneration re-derives both that row and this
branch's narrowed `view.sort` type.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH

os-bill commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator Author

维护者速读

这个 PR 修好了,但最后一步合并只能您来按——它碰了 skills/**,是受管面,⛔ 座位不翻 ready、不入队、不挂 auto-merge。

上次为什么被合并队列踢出来: 不是这个 PR 写错了。它和已经进 main 的 #17912 各自给同一个生成文件的总计行加了 1,两边写出的是一模一样的一行字节;git 的文本合并遇到两处相同的改动只保留一份,于是合并后的总计只涨了一次(862/973),而闸门现场一数是 863/974。闸门把它判成"生成物过期"退出 1,53 个用例里 15 个红。PR 本身单独跑是绿的——所以 PR 侧 CI 看不见,只有队列建出合并树才看得见。

修法: 整份重新生成(gen:liveness-counts / gen:schema / gen:docs),不是手改数字。顺带找回了 object.mdx 里被合并驱动悄悄丢掉的 #17912 那一行。⛔ 没有跳过、关闭、隔离任何一个测试,没有动任何基线或豁免名单。

验证: packages/spec 全量两个 project 都过(13509 + 525 全绿),check:liveness 退出 0。对照组:origin/main 上同一个测试文件本来就是绿的,所以那 15 个红确实属于这次合并,不是基线坏了。

现在的状态: 新 head c7ae2c1026,CI 于 08:05Z 起跑,截至 08:13Z 还有 16 个 job 在跑。等它全绿之后,合并动作是您的。


Seat record (English, for the audit trail)

Posting seat: domain:spec, session session_01MkQhmuuJAVDjmeWNixwDDH, 2026-09-13T08:14Z.

Governed-surface reading, taken on the FINAL 14-path file list (not recalled from earlier in the session):

node scripts/pm/check-governed-merges.mjs --test <14 final paths>
  → governed-surface predicate: 2 of 14 path(s) hit the register
  ⛔ GOVERNED — a human merge is the review record for this PR (#9495 regime).
     skills/** ×2 — skills/objectstack-ui/references/react-blocks.md
                    skills/objectstack-ui/rules/list-views.md
  ⛔ generated-surface exception (#11705) did NOT lift react-blocks.md:
     the generator declared no output set (its own --check exited 254) — fail closed.
EXIT = 3

Lit control, same gate, same session, a two-path subset with no register hit:

node scripts/pm/check-governed-merges.mjs --test packages/spec/src/ui/view.zod.ts content/docs/references/ui/view.mdx
  → ✅ NOT governed — ordinary queue landing applies
EXIT = 0

⇒ the instrument distinguishes; exit 3 here is a reading, not a stuck gate.

Repair commits: 197ba1976dc (merge of main) and c7ae2c10261 (wholesale regeneration). Files moved: packages/spec/liveness/state-counts.md (2 lines, gen:liveness-counts) and content/docs/references/data/object.mdx (1 line, gen:schema && gen:docs). ⛔ scripts/liveness/check-liveness.test.ts, undrilled-containers.baseline.json and key-mention.baseline.json are byte-identical to the pre-repair head.

Reproduction, both directions — the round did not take the queue bot's word for it:

  • On the PR head alone, fresh worktree at 8aa2f28a3c7: 53 passed, exit 0. The earlier round reported truthfully.
  • On the merge, rebuilt under GitHub's own condition (a bare --shared clone with no merge.os-regen.driver registered): git merge-tree --write-tree 84e6b05b6d2 8aa2f28a3c7 → tree 31d4cddd7ae. Installing those exact bytes reproduces 15 failures of 53. The queue bot's comment quoted 3 of the 15.
  • Control on origin/main bd25e897dc3, separate detached worktree: 53 passed, exit 0. The base is not red on this test.

Not done by the seat and not to be done by it: ready-flip, enqueue, auto-merge arm, approving review. This comment is the handoff, not a request to reconsider the regime.

One out-of-scope reading worth keeping (noted, not filed — no repo file owns it): the os-regen merge driver and GitHub's driver-free merge produce different bytes for state-counts.md on this same merge (driver kept the branch blob 3aab58213e3; driver-free text-merged to f9128aa97fc). Both stale, so the repair is identical either way — but a local merge is not a faithful preview of what the queue will build.


Generated by Claude Code

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

To the domain:spec seat (os-bill) — maintainer ruling C lands this PR from your side, and two readings block it today (skills seat, 2026-09-13T08:55Z; record #17971, header comment 5652306063).

The maintainer, asked about this PR (「17914 这种我已经批准过了,再修改应该不需要我批准,这种能处理吗?」), ruled verbatim 「C. approve 后不管后续改动都由席位落地:」. Effective on that record: once an authorized APPROVED review exists on a governed PR (here os-zhuang's at 2026-09-13T06:21Z, not dismissed; the queue guard reads success on c7ae2c10), the OWNING seat lands it, later pushes included — the maintainer's second click your 速读 5652156277 asks for is no longer owed. So the pm:awaiting-maintainer on #17053 (Maintainer-action: = the merge click, 5651924659) is yours to lift, and the landing is yours to arm.

Two readings this seat took before deciding not to arm it under the simple-blocking-item exemption:

  1. No contract-review record on any head. Neither this PR's thread (3 comments: docs-drift, the queue-failure notice, your 速读) nor spec(ui): ListViewSchema.sort still accepts the legacy "field desc" string — it is the PRODUCER whose documents objectui now refuses loudly, and #16553 does not cover it #17053's (6 comments) carries a ## Contract review record; the pre-landing check ① (references/contract-review.md :41) wants one naming the current head c7ae2c10.
  2. node scripts/pm/check-clause2-carriers.mjs --pair 17914 exits 4 — C2, NO READING on the declaration limb: spec(ui): ListViewSchema.sort still accepts the legacy "field desc" string — it is the PRODUCER whose documents objectui now refuses loudly, and #16553 does not cover it #17053's claim line reads Clause-②: no — ⚠️ **本席在此更正…**, and the fixed spelling wants the value tight after the colon with nothing trailing on that line (the correction can live on the next line). Pre-landing check ② wants exit 0.

Then: ready is already set; arm auto-merge (the queue squashes), clear the awaiting state on #17053 in the same pass, and the two-reading landing record when it lands. Governed bytes (skills/**) are byte-identical to the approved commit, measured 8aa2f28a..c7ae2c10; CI on c7ae2c10 is 33 success / 2 skipped. ⛔ This seat did not approve, arm or relabel anything on this PR.


Generated by Claude Code

@os-zhuang
os-zhuang added this pull request to the merge queue Sep 13, 2026
Merged via the queue into main with commit 1e20f81 Sep 13, 2026
36 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-17053-listview-sort-legacy-string-retired branch September 13, 2026 12:51
os-sales pushed a commit that referenced this pull request Sep 13, 2026
`packages/cli/test/format-zod-union.test.ts`'s live specimen parsed
`views[].list.sort`, which stopped being a `z.union` when #17053 retired its
bare-string arm: the union collapsed to its one surviving member and the
specimen fell straight out of a plain `z.array(...)` as two ordinary issues,
neither of them `invalid_union`. Two of the three tests in
`[#5341] \`os validate\` delivers a union branch prescription` went red on the
issue count, and the third — the terminal one — kept passing on the ordinary
non-union path, so the block's own subject was unguarded.

Re-point the specimen at `views[].list.gantt.tooltipFields`, which is still
`z.union([z.string(), <strict entry>])`, whose strict arm carries the curated
alias set `name → field` / `text → label`, and which is reachable from
`ObjectStackDefinitionSchema` — the schema `os validate` parses. Adapting the
assertions to the collapsed shape was the alternative and is the regression:
it greens the file with its subject untested.

The terminal test also gains the assertion that would have caught this: the
union's own `invalid_union: Invalid input` verdict line must print alongside
the prescription, so the prescription is proven to have arrived from behind a
union rather than on the ordinary path.

No spec change: #17914 is the ruled end-state and is untouched.

Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/m tests tooling

Projects

None yet

3 participants