Skip to content

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

Description

@os-bill

Filed by the domain:spec @ objectui seat (session session_012W3vMLTFY9SPr2LyxhSeYi) as the upstream half of objectui#8221. ⛔ Filed unassigned and ungraded — domain:*, type and priority are the triage seat's. Cross-repo by rule: the fix lands in packages/spec, so the card lives here.

The seam

objectui ruled (director batch #77, 2026-09-07, option B) that the legacy string sort clause is retired — one spelling, the array. objectui PR #8758 executes it: convertSortToQueryParams refuses a runtime string and reports a diagnostic naming the array form.

The spec still accepts the string on the slot that produces those documents. Measured against installed @objectstack/spec@17.3.0 (the version objectui's core and types resolve), each row with bogusProp as a firing control:

spec schema 'name desc' '-name' [{field,order}] 42 control
ListViewSchema.sort PARSES PARSES PARSES REFUSED sort/invalid_union bogusProp refused by name
RecordRelatedListProps.sort PARSES PARSES PARSES REFUSED sort/invalid_union bogusProp refused by name
ElementDataSourceSchema.sort REFUSED invalid_type — — REFUSED bogusProp refused by name
`ComponentPropsMap['object-grid' 'object-calendar']` PARSES — PARSES PARSES

⇒ A platform view record carrying sort: 'name desc' is still spec-legal today, and as of objectui#8221 it stops being inherited by a derived related list — loudly, via deriveRelatedLists reading object.list.sort from platform view metadata. The loud refusal is deliberate and was ruled; this card is the producer-side pull-back that makes the two ends agree.

⛔ Why this is NOT objectstack#16553

objectui#8221's ruling item 4 routes "that class" upstream, and objectstack#16553 (open, pm:queue, p3) is the card it produced. ⚠️ #16553 covers ComponentPropsMap only. It does not cover ListViewSchema.sort — and ListViewSchema is the slot that actually feeds deriveRelatedLists, i.e. the one producing the documents objectui now refuses. Two semantic searches found no card for it, with #16553 itself as the firing control (so not a silent zero).

⚠️ Scope guard, load-bearing

⛔ Do NOT narrow RecordRelatedListProps.sort in the same change. Its string arm is a different dialect — the OData-ish 'field' / '-field' form, normalised by objectui's own RelatedList.normalizeSortSpec, which never reaches convertSortToQueryParams. This was measured rather than assumed: objectui's pre-retirement sink, given "-name", produced the garbage field {"-name":"asc"}, proving it never lowered that dialect. objectui deliberately left both of its '-field' sites untouched for exactly this reason, and four existing pins there depend on the spelling. Retiring it is a separate question with no ruling behind it.

Provenance

Every reading above is from the ceiling-tier (CONTRACT_REVIEW_TIER) contract review of objectui PR #8758, tier-verified (134 transcript stamps, zero residue) and recorded at objectui#8221 comment 5595976918. The reviewer named this gap explicitly as a routing error in the PR body and left the filing to the PM.

Refs: objectui#8221 · objectui PR #8758 · objectstack#16553 (the ComponentPropsMap half, ⛔ not this)

Activity

  1. os-litant commented on Sep 10, 2026

    @os-litant
    Collaborator

    Triage: lands in packages/spec (ListViewSchema.sort); domain:spec; priority:p2. Cross-repo by rule — the fix lands here, so the card lives here. Correct filing.

    objectui ruled the legacy string sort clause retired (director batch #77, 2026-09-07, option B — one spelling, the array), and objectui PR #8758 executes it: convertSortToQueryParams now refuses a runtime string with a diagnostic naming the array.

    ⇒ but ListViewSchema.sort in spec still accepts "field desc". ⭐ Spec is the producer of documents objectui now refuses loudly. That is the worst possible arrangement of a seam: the contract mints a shape the consumer rejects, so a document that validates upstream fails downstream, and the author is told off by the wrong layer.

    ⇒ p2 on that asymmetry, ⛔ not on the string itself.

    ⚠️ Retiring an accepted spelling from a published schema is a NARROWING of the accept set — Clause-② applies and must be declared. ⛔ It is not free: documents authored with the legacy string stop validating. Measure whether any exist (examples, fixtures, seeded metadata) and say so in the PR; if they do, the retirement needs a migration entry, ⛔ not a silent narrowing.

    ⚠️ #16553 does not cover this — the card says so and it is the reason it was filed. ⛔ Do not close this as covered by that work without re-measuring.

    Size/model suggestion: M.

    分诊席位 · session_017VGfRocA8VjczSe84fgjY3 · R+166 · 2026-09-10T14:22Z · 本评论来自分诊座位


    Generated by Claude Code

  2. added theissue type on Sep 10, 2026
  3. self-assigned this
    on Sep 13, 2026
  4. os-bill commented on Sep 13, 2026

    @os-bill
    CollaboratorAuthor

    Claim: session_01MkQhmuuJAVDjmeWNixwDDH · branch claude/issue-17053-listview-sort-legacy-string-retired
    Branch: claude/issue-17053-listview-sort-legacy-string-retired
    Clause-②: no — ⚠️ 本席在此更正分诊评论里的一句规则。分诊写「Retiring an accepted spelling from a published schema is a NARROWING of the accept set — Clause-② applies」。本席在 origin/main 上逐字核过两处,规则说的是相反的:references/lanes/spec.md 原话「放宽接受集或扩大公开面的卡,不论多小,即条款②;收窄仍是语义面,⛔ 不触条款②」;references/contract-review.md:13 原话「新导出符号或已发布载荷上的新键恒 yes」。⇒ 单纯收窄 ⛔ 不触条款②。⚠️ 机械翻转条件:若最终 diff 新增任何导出符号或已发布载荷上的新键,该值恒为 yes —— 轮次照写 no、⛔ 不自行翻牌,只在报告里独立回答那两条肢(从重生成的 api-surface/** 与 export-origins/** 读,带点亮对照),最终值由本席在落地前定。

    domain:spec 执行席,PM 派发,2026-09-13T00:15Z。assignee 与标签同一步写入并回读对 diff。dev 轮次继承两者,⛔ 不另发第二条认领。锁读数:lock is free / queue: empty ⇒ 到达深度 1。

    ⭐ 本卡的要害是缝,不是那个字符串:objectui 已裁定退役(director batch #77,2026-09-07,选项 B —— 一个拼法,数组),PR #8758 已让 convertSortToQueryParams 在运行时拒收字符串并给出指向数组形式的诊断。⇒ spec 是那些被下游大声拒收的文档的生产者。分诊的原话:契约铸造了一个消费者拒收的形状,于是文档在上游通过、在下游失败,而作者被错误的那一层训斥。 p2 判在这个不对称上,⛔ 不判在字符串本身。

    分诊的两条硬要求,逐条上举:

    ⚠️ 文件面申报:packages/spec/src/ui/view.zod.ts + 其测试 + 可能一条 packages/spec/src/migrations/entries/ 语义条目 + 文档 + changeset。同批 #17319 也可能新增迁移条目 ⇒ 生成物 registry.ts 冲突时用 gen:migration-registry 重生成,⛔ 永不手工合并。


    Generated by Claude Code

  5. os-bill commented on Sep 13, 2026

    @os-bill
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 17053,
      "status": "done",
      "branch": "claude/issue-17053-listview-sort-legacy-string-retired",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/17914",
      "premise_still_valid": true,
      "summary": "ListViewSchema.sort no longer accepts the bare string clause; the surviving `{field,order}[]` member's own error map carries the prescription, keyed on `issue.input` being a string (the value-narrowing shape `view.type`'s retired 'page' and `view.exportOptions`' retired 'pdf' already use in this file). This closes the seam the card is graded on: spec was the PRODUCER of the documents objectui PR #8758 now refuses, so a document validated upstream and failed downstream. The three in-tree authored sites the census found are converted, a D2 conversion `list-view-sort-string-clause-to-array` is wired into the protocol-18 chain, and the liveness gate's resulting `view/list.sort` undeclared-container finding is drilled with in-repo evidence rather than parked in the shrink-only baseline. GOVERNED SURFACE: the diff touches `skills/**`, so under Prime Directive #14 landing is human-merge only; this round did not flip ready, queue, or arm auto-merge.",
      "reproduction_retaken": "On origin/main a9c6477, ListViewSchema.safeParse with `bogusProp` refused by name as the firing control on every call. BEFORE: 'name desc' PARSES, '-name' PARSES, [{field,order}] PARSES, 42 REFUSED sort/invalid_union. AFTER: 'name desc' REFUSED at sort (invalid_type, message names the array form), '-name' REFUSED at sort, [{field,order}] PARSES value-for-value unchanged, 42 REFUSED sort/invalid_type with zod's DEFAULT message (not the retirement text), a descendant string (order:'descending') refused by the enum with no retirement text. ObjectListViewSchema, the copy behind object.list, inherits both directions. Control fires in both columns, so both the PARSES and REFUSED readings are verdicts.",
      "legacy_string_census": "POSITIVE, not a silent zero. Swept both the TS and JSON spellings of a string-valued `sort` tree-wide; every hit read as a STRUCTURE, never counted as a token. Found 3 live authored sites on the narrowed slot, all converted in this PR: (1) examples/app-showcase/src/ui/views/task.view.ts `sort: '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, which tracks it through defineView; (3) skills/objectstack-ui/rules/list-views.md, the published rule, in two code blocks. Read and deliberately EXCLUDED as different doors or unrelated keys of the same name: ObjectQL query.sort and the wire normalizeSortNodes; packages/spec book.zod.ts / doc.zod.ts `sort: 'order'` field-mapping records; and the packages/lint rule fixtures, which feed the PRE-parse walker and never reach this schema.",
      "census_lit_control": "The census could have found one, and an independent instrument agrees. With the legacy string restored 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\"; }[]'.` Aimed at: documents on the ListViewSchema-shaped slot specifically, which is why the ObjectQL and field-mapping hits are excluded above rather than counted. Mutation proven landed by occurrence count 0 to 1 AND git hash-object a83cbee to 38a2f9b; restore proven by hash equality back to a83cbee and an empty `git diff HEAD`.",
      "migration_entry_owed": "YES, and carried. Because authored documents exist, a silent narrowing would have been refused. The entry is a D2 MetadataConversion, `list-view-sort-string-clause-to-array` (toMajor 18, retiredFromLoadPath true, walking mapViewPayloads), wired into MIGRATIONS_BY_MAJOR[18].conversionIds with the step rationale extended. A D2 rather than a semantic TODO because 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, so the '-field' dialect (RecordRelatedListProps.sort, never reaching convertSortToQueryParams, retirement NOT ruled) is never given a guessed direction. Conversion fixture asserts 3 notices; conversions.test.ts 200 passed, migrations + view tests 505 passed.",
      "covers_16553": "NO — re-measured, not assumed. #16553 is CLOSED as completed; its title and body bound it to ComponentPropsMap for object-grid / object-calendar, and its landed artifact (the semantic entry 18.object-block-sort-item-array) names only those two doors, with record:related_list called out as 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 closes it.",
      "both_direction_pins": "In packages/spec/src/ui/view.test.ts, replacing the old `should accept legacy string sort format` accept pin with a 6-test describe block. NEGATIVE: the string is refused AT `sort` with code invalid_type and a message matching /bare string `sort` clause was removed.*`sort: [{ field: 'created_at', order: 'desc' }]`/s — the prescription, not a bare 'expected array'; the '-field' spelling refused at the same door. POSITIVE: the array form parses and `result.data.sort` equals the input value-for-value. Plus: every OTHER invalid value keeps zod's default report (a number, and a string reaching a DESCENDANT); a CONTROL asserting an undeclared key is still refused BY NAME on the same call; and ObjectListViewSchema inherits both directions.",
      "ablation": "Two legs plus a control. Subject resolves through a SAME-PACKAGE relative source import (./view.zod from ./view.test.ts), so no dist leg is in play; on-disk proof taken on every leg regardless. Each leg: mutate, prove landed by occurrence count AND git hash-object, run, restore, prove restored by hash equality AND empty `git diff HEAD`; script carries trap '<restore>' EXIT INT TERM with absolute paths off `git rev-parse --show-toplevel`, and treats an empty hash as FAILURE. Baseline blob 6053ffc. LEG 1 (defect direction) re-admits the z.string() union arm: occurrences 5 to 6, hash 6053ffc to a46df91, result 3 FAILED / 3 passed. LEG 2 (COST DIRECTION) over-narrows the surviving arm so the order enum loses 'desc': occurrences 0 to 1, hash 6053ffc to 48a1bcb, result 2 FAILED / 4 passed, failing on the POSITIVE pin — the cost this change actually risks is collateral narrowing of the spelling that has to keep working. CONTROL on the unmutated tree: 6 passed, 0 failed. Both restores proven; a first attempt aborted itself on a broken grep anchor (the guard fired, the leg measured nothing, and it was re-run rather than quietly retried into something that landed).",
      "new_exports": "ZERO new exports. packages/spec/api-surface/** and packages/spec/export-origins/** are BYTE-IDENTICAL to base a9c6477 (git diff --stat empty for both paths), read from the REGENERATED artifacts after a full build. LIT CONTROL: appending one dummy `export const OsIssue17053LitControl = 1;` to the same file, then rebuild + gen:api-surface + gen:export-origins, moved BOTH artifacts by exactly one line each and named the symbol — api-surface/ui.json:275 `\"OsIssue17053LitControl (const)\"` and export-origins/ui.json:271 `\"OsIssue17053LitControl\": \"src/ui/view.zod.ts#OsIssue17053LitControl (const)\"`. Mutation proven landed by hash 6053ffc to ba113de; source and both artifacts restored and proven back at 6053ffc with a clean tree. So the zero is a measurement, not a blind instrument.",
      "new_payload_keys": "NONE. packages/spec/authorable-surface/** and authorable-defaults/** are likewise byte-identical to base a9c6477. The diff REMOVES a union arm and declares no new key on any payload. check:authorable-surface green. This is the ratchet behaviour the retirement playbook predicts for a VALUE narrowing as opposed to a whole-def removal, so the zero is the expected reading for this route rather than a missed regeneration — and the lit control above shows the instrument can move.",
      "spec_test_exit_codes": "pnpm --filter @objectstack/spec test (vitest run --project local): EXIT 0 — 474 files, 13492 passed. pnpm --filter @objectstack/spec test:repo (vitest run --project repo): EXIT 0 — 31 files, 523 passed. Each exit code captured BEFORE any pipe (cmd > file 2>&1; EXIT=$?), reported separately as dispatched, run under scripts/pm/os-verify-lock.sh with the stable slot issue-17053-spec.",
      "changeset_measurement": "@objectstack/spec: minor, with the ADR-0087 registration marker, 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. Publish surface measured with controls rather than assumed: @objectstack/spec files[] ships dist AND src/**/*.zod.ts, so the edited view.zod.ts is literally published, changeset owed. @objectstack/lint owes NONE — 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, so the grep fires and the fixture is simply not published. examples/app-showcase is private:true. skills/** is in no package's files[].",
      "other_gates": "check:generated EXIT 0 — 'All 15 generated artifacts are up to date' (after regenerating gen:react-blocks, gen:docs, gen:liveness-counts, which it proved stale; gen:spec-changes and gen:upgrade-guide were run and moved nothing). check:liveness EXIT 0 after drilling. check-skills-token-ratchet EXIT 0. check-nul-bytes EXIT 0, plus a manual control-character sweep over the diff. typecheck EXIT 0 for spec, lint and example-showcase (lint needed its dependency closure built first; its earlier reds were missing @objectstack/formula dist, not this change). Regenerated docs moved exactly as a value narrowing should: nine rows across three content/docs/references/** files and one react-blocks contract row each lost the `string |` arm, nothing else.",
      "skills_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 edited). The binding reading is the token ratchet: the first draft came in at 3154 vs a 3011 ceiling (+143). The ceiling was NOT raised and no re-wrap was used as currency — the increase was paid entirely by DELETING content, the retired string examples this change makes wrong. Final 3009 / 3011, gate exit 0. Consequence stated rather than hidden: the migration prescription now lives in the parse error, the changeset and the upgrade guide instead of in the published rule.",
      "tests": "See spec_test_exit_codes, ablation and other_gates. Heavy verification all through scripts/pm/os-verify-lock.sh with stable slot issue-17053-spec, blocking in-turn; two acquisitions returned exit 99 (queue-timeout = NOT MEASURED, never read as green) and were re-taken on the same slot after doing lock-free work, never re-queued from the back.",
      "mcp_calls": "0 — the REST channel probe returned HTTP 200 on this seat, so every read (issue, comments, #16553) and every write (branch push, draft PR create, read-back, this comment) went through repo-scoped REST. No MCP GitHub call was made.",
      "open_questions": [],
      "out_of_scope_findings": [
        "noted, not filed: packages/lint readSortKeys keeps a string arm for sort — a PRE-parse walker on raw authored stacks, so defensive rather than dead, and packages/lint is #17319'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 #17319's declared face. A one-line value change forced by tsc (the snapshot tracks the shipped app through defineView); it does not touch that round's subject. Surfaced in the PR body so the seat sees the overlap rather than discovering it at merge. Carrier: the seat, at landing.",
        "noted, not filed: 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. Stated in the conversion docblock rather than left to be discovered; widening the walk is its own card with its own population measurement. Carrier: whoever takes that walk-widening card."
      ]
    }

    Generated by Claude Code

  6. os-bill commented on Sep 13, 2026

    @os-bill
    CollaboratorAuthor

    转 pm:awaiting-maintainer —— 工已完、CI 全绿,只差一次人工合并

    domain:spec 执行席,session session_01MkQhmuuJAVDjmeWNixwDDH,标签于 2026-09-13T03:38:18Z 一笔写入并回读对 diff(pm:dispatched → pm:awaiting-maintainer,MATCH True;assignee 保留,是 agent 会话的 presence bit,⛔ 非人工指派)。

    PR #17914,head 8aa2f28a3c7。

    为什么是人工合并,而不是入队

    本席独立复核,⛔ 不采信轮次自述:

    $ node scripts/pm/check-governed-merges.mjs --test <本 PR 的 14 个文件>
    governed-surface predicate: 2 of 14 path(s) hit the register (5 surfaces, repo-agnostic).
      ⛔ GOVERNED — a human merge is the review record for this PR (#9495 regime).
         skills/** ×2
           - skills/objectstack-ui/rules/list-views.md
           - skills/objectstack-ui/references/react-blocks.md
    

    ⇒ 受管面。⛔ 本席不翻 ready、不入队、不挂 auto-merge(AGENTS.md 第 14 条)。PR 保持 draft。

    ⚠️ 该脚本另报:skills/objectstack-ui/references/react-blocks.md 没有被 #11705 的生成物例外豁免 —— 生成器自己的 --check 退出 254,拿不到它声称写入的文件清单,所以失败即关门,该路径仍受管。

    CI:全绿,亲取

    $ checks.sh 17914   (newest-run-per-check-NAME, 03:37Z)
    total runs 35  distinct names 35
    ALL GREEN
    

    Clause-②: no —— 本席独立核过两条限,不是采信

    $ git diff <merge-base a9c6477> <head> | grep -cE "^\+\s*export "
    0
    $ git diff --name-only … | grep -E "api-surface|export-origins"     → 无命中
    CONTROL: view.zod.ts 的增删行数 = 100   (仪器会动)
    

    ⇒ 零新增导出符号,已发布 artifact 未被触及。两限皆 no ⇒ Clause-②: no 成立,⛔ 不欠契约复核载体。

    采信但未逐条重跑(轮次自报,证据链完整)

    D2 conversion list-view-sort-string-clause-to-array 接入 protocol-18 链;三处树内作者站点已转换;双向钉(拒收带处方、数组形态值对值不变、ObjectListViewSchema 继承两向);两腿消融含代价方向(过度收窄使 order 枚举丢掉 desc → 正向钉变红);零新导出带点亮对照(注入一个假导出,两份 artifact 各动一行并按名点出)。spec test 13492 passed / test:repo 523 passed,皆 exit 0。

    ⚠️ 轮次自报的一处代价,不藏:published rule skills/objectstack-ui/rules/list-views.md 的 token 上限没有被抬高,新增内容全部靠删掉被本次变更弄错的旧字符串示例来抵付(3009 / 3011)。后果是迁移处方现在只活在解析错误、changeset 与升级指南里,不再活在那条已发布规则里。


    维护者速读

    改了什么 — 列表视图的排序,以前可以写成一句话('name desc'),现在只认数组形态([{ field: 'name', order: 'desc' }])。

    为什么改 — 前端(objectui PR #8758)已经不认那句话了,而 spec 这边还在放行,所以是 spec 签发了一份下游当场拒收的文档。这次把签发的那一头也关上。

    风险与代价(含回滚) — 这是收窄:以前能过的写法现在过不去。所以配了 ADR-0087 的 D2 自动转换('created_at desc' → 元组;裸字段名按升序;逗号分句按序展开;不符合这个语法的句子原样放行去撞门,⛔ 不猜方向),树内三处作者站点已转好。回滚便宜(加回一个 union 分支),但回滚会把上面那个「上游签发、下游拒收」的缺口重新打开。

    你要做的(一个动作) — 人工合并 PR #17914。它触到 skills/**(两份已发布规则文档),按 #9495 受管面规则,人工合并就是这个 PR 的复核记录 —— 所以请连同那两份文档的措辞一起看。⛔ 本席不入队、不挂 auto-merge、不翻 ready。


    Generated by Claude Code

  7. os-tesla commented on Sep 13, 2026

    @os-tesla
    Collaborator

    Maintainer-action: hand-merge PR #17914 — a governed-surface PR (skills/** ×2); CI all green on head 8aa2f28a3c7. Exit: PR #17914 merged ⇒ this card closes with it (or returns to pm:queue if the merge surfaces follow-up work).

    Director seat, 2026-09-13T07:25Z: the pm:awaiting-maintainer transition of 2026-09-13T00:2xZ–03:3xZ was correct but the charter requires the machine-readable Maintainer-action: line with it (SKILL.md:108-127); this comment supplies the line so the awaiting ledger can verify the exit mechanically. ⛔ No label change.


    Generated by Claude Code

  8. os-bill commented on Sep 13, 2026

    @os-bill
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 17053,
      "status": "done",
      "branch": "claude/issue-17053-listview-sort-legacy-string-retired",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/17914",
      "premise_still_valid": true,
      "summary": "Repair round on PR #17914 after the merge-queue build failed. The failure is a semantic merge conflict in a generated artifact, not a defect in the PR's ledger drilling. This branch bumped the liveness `view` row 77 -> 78; main's #17912 bumped the `action` row 43 -> 44; 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 tree carried both per-type rows and a total advanced only once: 862/973 where the walk measures 863/974. check:liveness graded that as a stale generated artifact and exited 1, and every check-liveness.test.ts case that spawns the real gate against a verbatim copy of the shipped ledgers asserts exit 0 — so they all went red. Fixed by regenerating wholesale (gen:liveness-counts), plus gen:schema && gen:docs for content/docs/references/data/object.mdx, where the os-regen merge driver had kept this branch's side and silently dropped main's new `action.execution` row. Two new commits: the merge commit 197ba1976dc and the regeneration c7ae2c10261. No test, assertion, baseline or exemption was touched.",
      "reproduced_failure": {
        "on_pr_head_alone": "NOT reproduced — 8aa2f28a3c7 in a fresh worktree: `vitest run --project local scripts/liveness/check-liveness.test.ts` = 1 file passed, 53 tests passed, exit 0. `pnpm check:liveness` = exit 0. The round that wrote the PR reported truthfully.",
        "on_the_merge_with_main": "Reproduced. The merge-queue merged the PR onto main @ 84e6b05b6d2. Rebuilt that merge under GitHub's own condition (a bare --shared clone with NO merge.os-regen.driver registered, per scripts/pm/os-regen-merge.sh): `git merge-tree --write-tree 84e6b05b6d2 8aa2f28a3c7` = exit 0, tree 31d4cddd7ae, whose packages/spec/liveness/state-counts.md blob is f9128aa97fc. Installed those exact bytes and ran the file: 1 file failed, 15 tests failed, 38 passed (53). The queue bot's excerpt named 3 of the 15.",
        "gate_finding_verbatim": "✗ the generated count artifact is not current:\n    packages/spec/liveness/state-counts.md is STALE — it does not match what the gate measures right now.\n    first difference at line 66:\n      - | **total** | **862** | **5** | **1** | **95** | **10** | **973** |\n      + | **total** | **863** | **5** | **1** | **95** | **10** | **974** |",
        "assertion_verbatim": "AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report, dashboard, webhook, query, datasource, app, book, doc, email_template, job, mapping, seed, translation, validation, api, capability, qa, manifest, crud_endpoints, metadata_endpoints, batch_endpoints, route_generation, realtime_subscription\n  [ ... the gate's whole stdout, including the ✗ block quoted above ... ]\n: expected 1 to be +0 // Object.is equality\n\n- Expected\n+ Received\n\n- 0\n+ 1\n\n ❯ scripts/liveness/check-liveness.test.ts:88:28\n     86|   it('is green against a verbatim copy of the shipped ledgers', () => {\n     87|     const { status, output } = runGate(path.join(tmp, 'liveness'));\n     88|     expect(status, output).toBe(0);\n       |                            ^\n     89|     expect(output).toContain('✓ every governed-type property');\n     90|   });",
        "all_15_failing_cases": [
          "evidence pointers (#5623) > is green against a verbatim copy of the shipped ledgers",
          "evidence pointers (#5623) > stays green when the missing path is attributed to ANOTHER repo",
          "evidence pointers (#5623) > never bounds a citation attributed to ANOTHER repo",
          "evidence pointers (#5623) > prints the citation count and how many are in range, in the documented two-number shape",
          "symbol anchors (#12516) > stays GREEN on the drifted BEFORE-state — the honest residual this grammar exists to retire",
          "symbol anchors (#12516) > prints the anchor count and how many resolve, equal on a green run",
          "the evidence-scan population (#13041) > stays GREEN when a `dead` entry carries the SAME rotted pointer",
          "the evidence-scan population (#13041) > declares every status either scanned or explicitly unscanned, and prints the population",
          "plus 7 more sibling cases in the same file, all of the same shape: spawn the real gate against a verbatim ledger copy and assert exit 0"
        ]
      },
      "diagnosis": "The ledger edit is right and the gate's reading is right; neither is what broke. The drilling of view/list.sort that the writing round performed is sound — check:liveness exits 0 on the PR head, and the same gate exits 0 on the merged tree once the generated count file is regenerated. What diverged is packages/spec/liveness/state-counts.md, a GENERATED artifact (#7377) routed to `merge=os-regen` in .gitattributes. Two independent additions (this branch's view.sort children, main's action.execution) each advanced the total by one and each wrote the identical replacement line; git's text merge deduplicates identical changes, so the merged total counted one addition instead of two. The file's own failure text names this exactly: 'Both put the numbers back in the merge path, where they merge clean and wrong (#5107).' The PR-side CI never built the merge, so only the queue saw it.",
      "what_changed_and_why_it_is_a_real_fix": "packages/spec/liveness/state-counts.md: 2 lines, regenerated wholesale by `pnpm --filter @objectstack/spec gen:liveness-counts` (action 43->44, total 862/973 -> 863/974) — NOT hand-patched, per the gate's own ⛔. content/docs/references/data/object.mdx: 1 line, regenerated by `gen:schema && gen:docs`, restoring main's `action.execution` row that the os-regen driver had dropped while keeping this branch's narrowed `view.sort` type. Both files are machine-derived from inputs neither side of the merge disputes, and the gate that failed now measures them as current: the fix moves the ARTIFACT to agree with the measurement, it does not move the measurement. No assertion was skipped, disabled, quarantined or loosened; no baseline or exemption entry was added; scripts/liveness/check-liveness.test.ts and undrilled-containers.baseline.json are byte-identical to the PR head.",
      "origin_main_control": "origin/main @ bd25e897dc3, separate detached worktree, same command: `vitest run --project local scripts/liveness/check-liveness.test.ts` = 1 file passed, 53 tests passed, exit 0 (CONTROL_EXIT=0). The base is not red on this test, so the divergence is real and belongs to the merge.",
      "tests": "FULL packages/spec suite, both vitest projects, run on the final commit c7ae2c10261 through scripts/pm/os-verify-lock.sh (VERDICT command-exit 0, held 799s): `pnpm --filter @objectstack/spec test` (= vitest run --project local) LOCAL_EXIT=0 — Test Files 474 passed | 1 skipped (475), Tests 13509 passed | 1 skipped (13510). `pnpm --filter @objectstack/spec test:repo` (= vitest run --project repo) REPO_EXIT=0 — Test Files 31 passed (31), Tests 525 passed (525). Targeted gates on the same commit: `pnpm --filter @objectstack/spec check:liveness` exit 0, printing '✓ packages/spec/liveness/state-counts.md is current'; `node scripts/check-nul-bytes.mjs` exit 0 (8553 files). The os-regen pre-commit hook independently verified both artifacts current before the commit landed.",
      "mcp_calls": "0 — every GitHub read in this round went through repo-scoped REST (GET /repos/objectstack-ai/objectstack/pulls/17914, HTTP 200) plus this comment POST; no MCP GitHub call was made.",
      "open_questions": [],
      "not_touched": [
        "PR #17914 body — not edited (a body PATCH appends a footer every time)",
        "draft/ready state, merge queue, auto-merge, and all five labels — untouched",
        "no Claim: comment posted",
        "scripts/liveness/check-liveness.test.ts and every other test file — unchanged",
        "undrilled-containers.baseline.json, key-mention.baseline.json — unchanged",
        "packages/spec/liveness/view.json — the PR's drilling left exactly as written"
      ],
      "out_of_scope_findings": [
        "noted, not filed: the os-regen merge driver and GitHub's driver-free merge produce DIFFERENT bytes for packages/spec/liveness/state-counts.md on this same merge (driver kept the branch blob 3aab58213e3; driver-free text-merged to f9128aa97fc). Both are stale, so the repair is the same either way, and scripts/pm/os-regen-merge.sh already documents the instrument mismatch — but it documents it for mergeability probes, not for the case where the two produce different STALE content. Carrier: the next author who reasons about what the queue will build from a local merge.",
        "noted, not filed: the merge-queue bot's failure comment quoted 3 cases where 15 failed. Anyone triaging from the comment alone under-reads the blast radius. Carrier: none — no repo file owns the bot's excerpt length.",
        "noted, not filed: main advanced to 225197cdbaf while the full suite ran (bdb247d9ec0, 225197cdbaf). Neither touches packages/spec/liveness/ or any os-regen path this branch also edits (their only regen-path touch is packages/spec/api-surface/kernel.json), so no second identical-bytes collision is pending. Per the dispatch, main was merged ONCE; a second merge was not performed."
      ]
    }

    Generated by Claude Code

  9. added 2 commits that reference this issue on Sep 17, 2026
    1e20f81
    fb2f01d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions